Skip to content

Commit 435384f

Browse files
authored
Merge pull request #6773 from thaJeztah/improve_mountopts
opts: MountOpt: improve validation, and refactor
2 parents b22f1ae + df3e923 commit 435384f

File tree

3 files changed

+200
-127
lines changed

3 files changed

+200
-127
lines changed

opts/mount.go

Lines changed: 37 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -33,66 +33,32 @@ func (m *MountOpt) Set(value string) error {
3333
return err
3434
}
3535

36-
mount := mounttypes.Mount{}
37-
38-
volumeOptions := func() *mounttypes.VolumeOptions {
39-
if mount.VolumeOptions == nil {
40-
mount.VolumeOptions = &mounttypes.VolumeOptions{
41-
Labels: make(map[string]string),
42-
}
43-
}
44-
if mount.VolumeOptions.DriverConfig == nil {
45-
mount.VolumeOptions.DriverConfig = &mounttypes.Driver{}
46-
}
47-
return mount.VolumeOptions
36+
mount := mounttypes.Mount{
37+
Type: mounttypes.TypeVolume, // default to volume mounts
4838
}
4939

50-
imageOptions := func() *mounttypes.ImageOptions {
51-
if mount.ImageOptions == nil {
52-
mount.ImageOptions = new(mounttypes.ImageOptions)
53-
}
54-
return mount.ImageOptions
55-
}
56-
57-
bindOptions := func() *mounttypes.BindOptions {
58-
if mount.BindOptions == nil {
59-
mount.BindOptions = new(mounttypes.BindOptions)
60-
}
61-
return mount.BindOptions
62-
}
63-
64-
tmpfsOptions := func() *mounttypes.TmpfsOptions {
65-
if mount.TmpfsOptions == nil {
66-
mount.TmpfsOptions = new(mounttypes.TmpfsOptions)
40+
for _, field := range fields {
41+
key, val, hasValue := strings.Cut(field, "=")
42+
if k := strings.TrimSpace(key); k != key {
43+
return fmt.Errorf("invalid option '%s' in '%s': option should not have whitespace", k, field)
6744
}
68-
return mount.TmpfsOptions
69-
}
70-
71-
setValueOnMap := func(target map[string]string, value string) {
72-
k, v, _ := strings.Cut(value, "=")
73-
if k != "" {
74-
target[k] = v
45+
if hasValue {
46+
v := strings.TrimSpace(val)
47+
if v == "" {
48+
return fmt.Errorf("invalid value for '%s': value is empty", key)
49+
}
50+
if v != val {
51+
return fmt.Errorf("invalid value for '%s' in '%s': value should not have whitespace", key, field)
52+
}
7553
}
76-
}
77-
78-
mount.Type = mounttypes.TypeVolume // default to volume mounts
79-
// Set writable as the default
80-
for _, field := range fields {
81-
key, val, ok := strings.Cut(field, "=")
8254

8355
// TODO(thaJeztah): these options should not be case-insensitive.
8456
key = strings.ToLower(key)
8557

86-
if !ok {
58+
if !hasValue {
8759
switch key {
88-
case "readonly", "ro":
89-
mount.ReadOnly = true
90-
continue
91-
case "volume-nocopy":
92-
volumeOptions().NoCopy = true
93-
continue
94-
case "bind-nonrecursive":
95-
return errors.New("bind-nonrecursive is deprecated, use bind-recursive=disabled instead")
60+
case "readonly", "ro", "volume-nocopy", "bind-nonrecursive":
61+
// boolean values
9662
default:
9763
return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
9864
}
@@ -111,97 +77,67 @@ func (m *MountOpt) Set(value string) error {
11177
case "target", "dst", "destination":
11278
mount.Target = val
11379
case "readonly", "ro":
114-
mount.ReadOnly, err = strconv.ParseBool(val)
80+
mount.ReadOnly, err = parseBoolValue(key, val, hasValue)
11581
if err != nil {
116-
return fmt.Errorf("invalid value for %s: %s", key, val)
82+
return err
11783
}
11884
case "consistency":
11985
mount.Consistency = mounttypes.Consistency(strings.ToLower(val))
12086
case "bind-propagation":
121-
bindOptions().Propagation = mounttypes.Propagation(strings.ToLower(val))
87+
ensureBindOptions(&mount).Propagation = mounttypes.Propagation(strings.ToLower(val))
12288
case "bind-nonrecursive":
12389
return errors.New("bind-nonrecursive is deprecated, use bind-recursive=disabled instead")
12490
case "bind-recursive":
12591
switch val {
12692
case "enabled": // read-only mounts are recursively read-only if Engine >= v25 && kernel >= v5.12, otherwise writable
12793
// NOP
12894
case "disabled": // previously "bind-nonrecursive=true"
129-
bindOptions().NonRecursive = true
95+
ensureBindOptions(&mount).NonRecursive = true
13096
case "writable": // conforms to the default read-only bind-mount of Docker v24; read-only mounts are recursively mounted but not recursively read-only
131-
bindOptions().ReadOnlyNonRecursive = true
97+
ensureBindOptions(&mount).ReadOnlyNonRecursive = true
13298
case "readonly": // force recursively read-only, or raise an error
133-
bindOptions().ReadOnlyForceRecursive = true
99+
ensureBindOptions(&mount).ReadOnlyForceRecursive = true
134100
// TODO: implicitly set propagation and error if the user specifies a propagation in a future refactor/UX polish pass
135101
// https://github.com/docker/cli/pull/4316#discussion_r1341974730
136102
default:
137103
return fmt.Errorf(`invalid value for %s: %s (must be "enabled", "disabled", "writable", or "readonly")`, key, val)
138104
}
139105
case "volume-subpath":
140-
volumeOptions().Subpath = val
106+
ensureVolumeOptions(&mount).Subpath = val
141107
case "volume-nocopy":
142-
volumeOptions().NoCopy, err = strconv.ParseBool(val)
108+
ensureVolumeOptions(&mount).NoCopy, err = parseBoolValue(key, val, hasValue)
143109
if err != nil {
144-
return fmt.Errorf("invalid value for volume-nocopy: %s", val)
110+
return err
145111
}
146112
case "volume-label":
147-
setValueOnMap(volumeOptions().Labels, val)
113+
volumeOpts := ensureVolumeOptions(&mount)
114+
volumeOpts.Labels = setValueOnMap(volumeOpts.Labels, val)
148115
case "volume-driver":
149-
volumeOptions().DriverConfig.Name = val
116+
ensureVolumeDriver(&mount).Name = val
150117
case "volume-opt":
151-
if volumeOptions().DriverConfig.Options == nil {
152-
volumeOptions().DriverConfig.Options = make(map[string]string)
153-
}
154-
setValueOnMap(volumeOptions().DriverConfig.Options, val)
118+
volumeDriver := ensureVolumeDriver(&mount)
119+
volumeDriver.Options = setValueOnMap(volumeDriver.Options, val)
155120
case "image-subpath":
156-
imageOptions().Subpath = val
121+
ensureImageOptions(&mount).Subpath = val
157122
case "tmpfs-size":
158123
sizeBytes, err := units.RAMInBytes(val)
159124
if err != nil {
160125
return fmt.Errorf("invalid value for %s: %s", key, val)
161126
}
162-
tmpfsOptions().SizeBytes = sizeBytes
127+
ensureTmpfsOptions(&mount).SizeBytes = sizeBytes
163128
case "tmpfs-mode":
164129
ui64, err := strconv.ParseUint(val, 8, 32)
165130
if err != nil {
166131
return fmt.Errorf("invalid value for %s: %s", key, val)
167132
}
168-
tmpfsOptions().Mode = os.FileMode(ui64)
133+
ensureTmpfsOptions(&mount).Mode = os.FileMode(ui64)
169134
default:
170-
return fmt.Errorf("unexpected key '%s' in '%s'", key, field)
135+
return fmt.Errorf("unknown option '%s' in '%s'", key, field)
171136
}
172137
}
173138

174-
if mount.Type == "" {
175-
return errors.New("type is required")
176-
}
177-
178-
if mount.VolumeOptions != nil && mount.Type != mounttypes.TypeVolume {
179-
return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", mount.Type)
180-
}
181-
if mount.ImageOptions != nil && mount.Type != mounttypes.TypeImage {
182-
return fmt.Errorf("cannot mix 'image-*' options with mount type '%s'", mount.Type)
183-
}
184-
if mount.BindOptions != nil && mount.Type != mounttypes.TypeBind {
185-
return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", mount.Type)
186-
}
187-
if mount.TmpfsOptions != nil && mount.Type != mounttypes.TypeTmpfs {
188-
return fmt.Errorf("cannot mix 'tmpfs-*' options with mount type '%s'", mount.Type)
189-
}
190-
191-
if mount.BindOptions != nil {
192-
if mount.BindOptions.ReadOnlyNonRecursive {
193-
if !mount.ReadOnly {
194-
return errors.New("option 'bind-recursive=writable' requires 'readonly' to be specified in conjunction")
195-
}
196-
}
197-
if mount.BindOptions.ReadOnlyForceRecursive {
198-
if !mount.ReadOnly {
199-
return errors.New("option 'bind-recursive=readonly' requires 'readonly' to be specified in conjunction")
200-
}
201-
if mount.BindOptions.Propagation != mounttypes.PropagationRPrivate {
202-
return errors.New("option 'bind-recursive=readonly' requires 'bind-propagation=rprivate' to be specified in conjunction")
203-
}
204-
}
139+
if err := validateMountOptions(&mount); err != nil {
140+
return err
205141
}
206142

207143
m.values = append(m.values, mount)

opts/mount_test.go

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -116,17 +116,32 @@ func TestMountOptErrors(t *testing.T) {
116116
{
117117
doc: "invalid key=value",
118118
value: "type=volume,target=/foo,bogus=foo",
119-
expErr: "unexpected key 'bogus' in 'bogus=foo'",
119+
expErr: "unknown option 'bogus' in 'bogus=foo'",
120120
},
121121
{
122122
doc: "invalid key with leading whitespace",
123123
value: "type=volume, src=/foo,target=/foo",
124-
expErr: "unexpected key ' src' in ' src=/foo'",
124+
expErr: "invalid option 'src' in ' src=/foo': option should not have whitespace",
125125
},
126126
{
127127
doc: "invalid key with trailing whitespace",
128128
value: "type=volume,src =/foo,target=/foo",
129-
expErr: "unexpected key 'src ' in 'src =/foo'",
129+
expErr: "invalid option 'src' in 'src =/foo': option should not have whitespace",
130+
},
131+
{
132+
doc: "invalid value is empty",
133+
value: "type=volume,src=,target=/foo",
134+
expErr: "invalid value for 'src': value is empty",
135+
},
136+
{
137+
doc: "invalid value with leading whitespace",
138+
value: "type=volume,src= /foo,target=/foo",
139+
expErr: "invalid value for 'src' in 'src= /foo': value should not have whitespace",
140+
},
141+
{
142+
doc: "invalid value with trailing whitespace",
143+
value: "type=volume,src=/foo ,target=/foo",
144+
expErr: "invalid value for 'src' in 'src=/foo ': value should not have whitespace",
130145
},
131146
{
132147
doc: "missing value",
@@ -171,9 +186,9 @@ func TestMountOptReadOnly(t *testing.T) {
171186
}{
172187
{value: "", exp: false},
173188
{value: "readonly", exp: true},
174-
{value: "readonly=", expErr: `invalid value for readonly: `},
175-
{value: "readonly= true", expErr: `invalid value for readonly: true`},
176-
{value: "readonly=no", expErr: `invalid value for readonly: no`},
189+
{value: "readonly=", expErr: `invalid value for 'readonly': value is empty`},
190+
{value: "readonly= true", expErr: `invalid value for 'readonly' in 'readonly= true': value should not have whitespace`},
191+
{value: "readonly=no", expErr: `invalid value for 'readonly': invalid boolean value ("no"): must be one of "true", "1", "false", or "0" (default "true")`},
177192
{value: "readonly=1", exp: true},
178193
{value: "readonly=true", exp: true},
179194
{value: "readonly=0", exp: false},
@@ -215,9 +230,9 @@ func TestMountOptVolumeNoCopy(t *testing.T) {
215230
}{
216231
{value: "", exp: false},
217232
{value: "volume-nocopy", exp: true},
218-
{value: "volume-nocopy=", expErr: `invalid value for volume-nocopy: `},
219-
{value: "volume-nocopy= true", expErr: `invalid value for volume-nocopy: true`},
220-
{value: "volume-nocopy=no", expErr: `invalid value for volume-nocopy: no`},
233+
{value: "volume-nocopy=", expErr: `invalid value for 'volume-nocopy': value is empty`},
234+
{value: "volume-nocopy= true", expErr: `invalid value for 'volume-nocopy' in 'volume-nocopy= true': value should not have whitespace`},
235+
{value: "volume-nocopy=no", expErr: `invalid value for 'volume-nocopy': invalid boolean value ("no"): must be one of "true", "1", "false", or "0" (default "true")`},
221236
{value: "volume-nocopy=1", exp: true},
222237
{value: "volume-nocopy=true", exp: true},
223238
{value: "volume-nocopy=0", exp: false},
@@ -267,7 +282,6 @@ func TestMountOptVolumeOptions(t *testing.T) {
267282
Labels: map[string]string{
268283
"foo": "foo-value",
269284
},
270-
DriverConfig: &mount.Driver{},
271285
},
272286
},
273287
},
@@ -282,7 +296,6 @@ func TestMountOptVolumeOptions(t *testing.T) {
282296
"foo": "foo-value",
283297
"bar": "bar-value",
284298
},
285-
DriverConfig: &mount.Driver{},
286299
},
287300
},
288301
},
@@ -297,7 +310,6 @@ func TestMountOptVolumeOptions(t *testing.T) {
297310
"foo": "",
298311
"bar": "",
299312
},
300-
DriverConfig: &mount.Driver{},
301313
},
302314
},
303315
},
@@ -306,12 +318,9 @@ func TestMountOptVolumeOptions(t *testing.T) {
306318
doc: "volume-label empty key",
307319
value: `type=volume,target=/foo,volume-label==foo-value`,
308320
exp: mount.Mount{
309-
Type: mount.TypeVolume,
310-
Target: "/foo",
311-
VolumeOptions: &mount.VolumeOptions{
312-
Labels: map[string]string{},
313-
DriverConfig: &mount.Driver{},
314-
},
321+
Type: mount.TypeVolume,
322+
Target: "/foo",
323+
VolumeOptions: &mount.VolumeOptions{},
315324
},
316325
},
317326
{
@@ -321,7 +330,6 @@ func TestMountOptVolumeOptions(t *testing.T) {
321330
Type: mount.TypeVolume,
322331
Target: "/foo",
323332
VolumeOptions: &mount.VolumeOptions{
324-
Labels: map[string]string{},
325333
DriverConfig: &mount.Driver{
326334
Name: "my-driver",
327335
},
@@ -335,7 +343,6 @@ func TestMountOptVolumeOptions(t *testing.T) {
335343
Type: mount.TypeVolume,
336344
Target: "/foo",
337345
VolumeOptions: &mount.VolumeOptions{
338-
Labels: map[string]string{},
339346
DriverConfig: &mount.Driver{
340347
Options: map[string]string{
341348
"foo": "foo-value",
@@ -351,7 +358,6 @@ func TestMountOptVolumeOptions(t *testing.T) {
351358
Type: mount.TypeVolume,
352359
Target: "/foo",
353360
VolumeOptions: &mount.VolumeOptions{
354-
Labels: map[string]string{},
355361
DriverConfig: &mount.Driver{
356362
Options: map[string]string{
357363
"foo": "foo-value",
@@ -368,7 +374,6 @@ func TestMountOptVolumeOptions(t *testing.T) {
368374
Type: mount.TypeVolume,
369375
Target: "/foo",
370376
VolumeOptions: &mount.VolumeOptions{
371-
Labels: map[string]string{},
372377
DriverConfig: &mount.Driver{
373378
Options: map[string]string{
374379
"foo": "",
@@ -386,10 +391,7 @@ func TestMountOptVolumeOptions(t *testing.T) {
386391
Type: mount.TypeVolume,
387392
Target: "/foo",
388393
VolumeOptions: &mount.VolumeOptions{
389-
Labels: map[string]string{},
390-
DriverConfig: &mount.Driver{
391-
Options: map[string]string{},
392-
},
394+
DriverConfig: &mount.Driver{},
393395
},
394396
},
395397
},

0 commit comments

Comments
 (0)