github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/opts/mount.go (about) 1 package opts 2 3 import ( 4 "encoding/csv" 5 "errors" 6 "fmt" 7 "os" 8 "path/filepath" 9 "strconv" 10 "strings" 11 12 mounttypes "github.com/docker/docker/api/types/mount" 13 "github.com/khulnasoft-lab/go-units" 14 "github.com/sirupsen/logrus" 15 ) 16 17 // MountOpt is a Value type for parsing mounts 18 type MountOpt struct { 19 values []mounttypes.Mount 20 } 21 22 // Set a new mount value 23 // 24 //nolint:gocyclo 25 func (m *MountOpt) Set(value string) error { 26 csvReader := csv.NewReader(strings.NewReader(value)) 27 fields, err := csvReader.Read() 28 if err != nil { 29 return err 30 } 31 32 mount := mounttypes.Mount{} 33 34 volumeOptions := func() *mounttypes.VolumeOptions { 35 if mount.VolumeOptions == nil { 36 mount.VolumeOptions = &mounttypes.VolumeOptions{ 37 Labels: make(map[string]string), 38 } 39 } 40 if mount.VolumeOptions.DriverConfig == nil { 41 mount.VolumeOptions.DriverConfig = &mounttypes.Driver{} 42 } 43 return mount.VolumeOptions 44 } 45 46 bindOptions := func() *mounttypes.BindOptions { 47 if mount.BindOptions == nil { 48 mount.BindOptions = new(mounttypes.BindOptions) 49 } 50 return mount.BindOptions 51 } 52 53 tmpfsOptions := func() *mounttypes.TmpfsOptions { 54 if mount.TmpfsOptions == nil { 55 mount.TmpfsOptions = new(mounttypes.TmpfsOptions) 56 } 57 return mount.TmpfsOptions 58 } 59 60 setValueOnMap := func(target map[string]string, value string) { 61 k, v, _ := strings.Cut(value, "=") 62 if k != "" { 63 target[k] = v 64 } 65 } 66 67 mount.Type = mounttypes.TypeVolume // default to volume mounts 68 // Set writable as the default 69 for _, field := range fields { 70 key, val, ok := strings.Cut(field, "=") 71 72 // TODO(thaJeztah): these options should not be case-insensitive. 73 key = strings.ToLower(key) 74 75 if !ok { 76 switch key { 77 case "readonly", "ro": 78 mount.ReadOnly = true 79 continue 80 case "volume-nocopy": 81 volumeOptions().NoCopy = true 82 continue 83 case "bind-nonrecursive": 84 bindOptions().NonRecursive = true 85 continue 86 default: 87 return fmt.Errorf("invalid field '%s' must be a key=value pair", field) 88 } 89 } 90 91 switch key { 92 case "type": 93 mount.Type = mounttypes.Type(strings.ToLower(val)) 94 case "source", "src": 95 mount.Source = val 96 if strings.HasPrefix(val, "."+string(filepath.Separator)) || val == "." { 97 if abs, err := filepath.Abs(val); err == nil { 98 mount.Source = abs 99 } 100 } 101 case "target", "dst", "destination": 102 mount.Target = val 103 case "readonly", "ro": 104 mount.ReadOnly, err = strconv.ParseBool(val) 105 if err != nil { 106 return fmt.Errorf("invalid value for %s: %s", key, val) 107 } 108 case "consistency": 109 mount.Consistency = mounttypes.Consistency(strings.ToLower(val)) 110 case "bind-propagation": 111 bindOptions().Propagation = mounttypes.Propagation(strings.ToLower(val)) 112 case "bind-nonrecursive": 113 bindOptions().NonRecursive, err = strconv.ParseBool(val) 114 if err != nil { 115 return fmt.Errorf("invalid value for %s: %s", key, val) 116 } 117 logrus.Warn("bind-nonrecursive is deprecated, use bind-recursive=disabled instead") 118 case "bind-recursive": 119 switch val { 120 case "enabled": // read-only mounts are recursively read-only if Engine >= v25 && kernel >= v5.12, otherwise writable 121 // NOP 122 case "disabled": // alias of bind-nonrecursive=true 123 bindOptions().NonRecursive = true 124 case "writable": // conforms to the default read-only bind-mount of Docker v24; read-only mounts are recursively mounted but not recursively read-only 125 bindOptions().ReadOnlyNonRecursive = true 126 case "readonly": // force recursively read-only, or raise an error 127 bindOptions().ReadOnlyForceRecursive = true 128 // TODO: implicitly set propagation and error if the user specifies a propagation in a future refactor/UX polish pass 129 // https://github.com/khulnasoft/cli/pull/4316#discussion_r1341974730 130 default: 131 return fmt.Errorf("invalid value for %s: %s (must be \"enabled\", \"disabled\", \"writable\", or \"readonly\")", 132 key, val) 133 } 134 case "volume-subpath": 135 volumeOptions().Subpath = val 136 case "volume-nocopy": 137 volumeOptions().NoCopy, err = strconv.ParseBool(val) 138 if err != nil { 139 return fmt.Errorf("invalid value for volume-nocopy: %s", val) 140 } 141 case "volume-label": 142 setValueOnMap(volumeOptions().Labels, val) 143 case "volume-driver": 144 volumeOptions().DriverConfig.Name = val 145 case "volume-opt": 146 if volumeOptions().DriverConfig.Options == nil { 147 volumeOptions().DriverConfig.Options = make(map[string]string) 148 } 149 setValueOnMap(volumeOptions().DriverConfig.Options, val) 150 case "tmpfs-size": 151 sizeBytes, err := units.RAMInBytes(val) 152 if err != nil { 153 return fmt.Errorf("invalid value for %s: %s", key, val) 154 } 155 tmpfsOptions().SizeBytes = sizeBytes 156 case "tmpfs-mode": 157 ui64, err := strconv.ParseUint(val, 8, 32) 158 if err != nil { 159 return fmt.Errorf("invalid value for %s: %s", key, val) 160 } 161 tmpfsOptions().Mode = os.FileMode(ui64) 162 default: 163 return fmt.Errorf("unexpected key '%s' in '%s'", key, field) 164 } 165 } 166 167 if mount.Type == "" { 168 return fmt.Errorf("type is required") 169 } 170 171 if mount.Target == "" { 172 return fmt.Errorf("target is required") 173 } 174 175 if mount.VolumeOptions != nil && mount.Type != mounttypes.TypeVolume { 176 return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", mount.Type) 177 } 178 if mount.BindOptions != nil && mount.Type != mounttypes.TypeBind { 179 return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", mount.Type) 180 } 181 if mount.TmpfsOptions != nil && mount.Type != mounttypes.TypeTmpfs { 182 return fmt.Errorf("cannot mix 'tmpfs-*' options with mount type '%s'", mount.Type) 183 } 184 185 if mount.BindOptions != nil { 186 if mount.BindOptions.ReadOnlyNonRecursive { 187 if !mount.ReadOnly { 188 return errors.New("option 'bind-recursive=writable' requires 'readonly' to be specified in conjunction") 189 } 190 } 191 if mount.BindOptions.ReadOnlyForceRecursive { 192 if !mount.ReadOnly { 193 return errors.New("option 'bind-recursive=readonly' requires 'readonly' to be specified in conjunction") 194 } 195 if mount.BindOptions.Propagation != mounttypes.PropagationRPrivate { 196 return errors.New("option 'bind-recursive=readonly' requires 'bind-propagation=rprivate' to be specified in conjunction") 197 } 198 } 199 } 200 201 m.values = append(m.values, mount) 202 return nil 203 } 204 205 // Type returns the type of this option 206 func (m *MountOpt) Type() string { 207 return "mount" 208 } 209 210 // String returns a string repr of this option 211 func (m *MountOpt) String() string { 212 mounts := []string{} 213 for _, mount := range m.values { 214 repr := fmt.Sprintf("%s %s %s", mount.Type, mount.Source, mount.Target) 215 mounts = append(mounts, repr) 216 } 217 return strings.Join(mounts, ", ") 218 } 219 220 // Value returns the mounts 221 func (m *MountOpt) Value() []mounttypes.Mount { 222 return m.values 223 }