Skip to content

Commit 8a192c8

Browse files
committed
Add subpath support to volumes in --mount option
All the backend work was done a while back for image volumes, so this is effectively just plumbing the option in for volumes in the parser logic. We do need to change the return type of the volume parser as it only worked on spec.Mount before (which does not have subpath support, so we'd have to pass it as an option and parse it again) but that is cleaner than the alternative. Fixes #20661 Signed-off-by: Matt Heon <mheon@redhat.com>
1 parent 50714d2 commit 8a192c8

File tree

3 files changed

+74
-35
lines changed

3 files changed

+74
-35
lines changed

libpod/container.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ type ContainerNamedVolume struct {
257257
// This is used for emptyDir volumes from a kube yaml
258258
IsAnonymous bool `json:"setAnonymous,omitempty"`
259259
// SubPath determines which part of the Source will be mounted in the container
260-
SubPath string
260+
SubPath string `json:",omitempty"`
261261
}
262262

263263
// ContainerOverlayVolume is an overlay volume that will be mounted into the

pkg/specgenutil/volumes.go

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ var (
2222
errNoDest = errors.New("must set volume destination")
2323
)
2424

25+
type universalMount struct {
26+
mount spec.Mount
27+
// Used only with Named Volume type mounts
28+
subPath string
29+
}
30+
2531
// Parse all volume-related options in the create config into a set of mounts
2632
// and named volumes to add to the container.
2733
// Handles --volumes, --mount, and --tmpfs flags.
@@ -253,18 +259,18 @@ func Mounts(mountFlag []string, configMounts []string) (map[string]spec.Mount, m
253259
return finalMounts, finalNamedVolumes, finalImageVolumes, nil
254260
}
255261

256-
func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
262+
func parseMountOptions(mountType string, args []string) (*universalMount, error) {
257263
var setTmpcopyup, setRORW, setSuid, setDev, setExec, setRelabel, setOwnership, setSwap bool
258264

259-
mnt := spec.Mount{}
265+
mnt := new(universalMount)
260266
for _, arg := range args {
261267
name, value, hasValue := strings.Cut(arg, "=")
262268
switch name {
263269
case "bind-nonrecursive":
264270
if mountType != define.TypeBind {
265271
return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
266272
}
267-
mnt.Options = append(mnt.Options, define.TypeBind)
273+
mnt.mount.Options = append(mnt.mount.Options, define.TypeBind)
268274
case "bind-propagation":
269275
if mountType != define.TypeBind {
270276
return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
@@ -278,16 +284,16 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
278284
default:
279285
return nil, fmt.Errorf("invalid value %q", arg)
280286
}
281-
mnt.Options = append(mnt.Options, value)
287+
mnt.mount.Options = append(mnt.mount.Options, value)
282288
case "consistency":
283289
// Often used on MACs and mistakenly on Linux platforms.
284290
// Since Docker ignores this option so shall we.
285291
continue
286292
case "idmap":
287293
if hasValue {
288-
mnt.Options = append(mnt.Options, fmt.Sprintf("idmap=%s", value))
294+
mnt.mount.Options = append(mnt.mount.Options, fmt.Sprintf("idmap=%s", value))
289295
} else {
290-
mnt.Options = append(mnt.Options, "idmap")
296+
mnt.mount.Options = append(mnt.mount.Options, "idmap")
291297
}
292298
case "readonly", "ro", "rw":
293299
if setRORW {
@@ -307,35 +313,35 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
307313
if hasValue {
308314
switch strings.ToLower(value) {
309315
case "true":
310-
mnt.Options = append(mnt.Options, name)
316+
mnt.mount.Options = append(mnt.mount.Options, name)
311317
case "false":
312318
// Set the opposite only for rw
313319
// ro's opposite is the default
314320
if name == "rw" {
315-
mnt.Options = append(mnt.Options, "ro")
321+
mnt.mount.Options = append(mnt.mount.Options, "ro")
316322
}
317323
}
318324
} else {
319-
mnt.Options = append(mnt.Options, name)
325+
mnt.mount.Options = append(mnt.mount.Options, name)
320326
}
321327
case "nodev", "dev":
322328
if setDev {
323329
return nil, fmt.Errorf("cannot pass 'nodev' and 'dev' mnt.Options more than once: %w", errOptionArg)
324330
}
325331
setDev = true
326-
mnt.Options = append(mnt.Options, name)
332+
mnt.mount.Options = append(mnt.mount.Options, name)
327333
case "noexec", "exec":
328334
if setExec {
329335
return nil, fmt.Errorf("cannot pass 'noexec' and 'exec' mnt.Options more than once: %w", errOptionArg)
330336
}
331337
setExec = true
332-
mnt.Options = append(mnt.Options, name)
338+
mnt.mount.Options = append(mnt.mount.Options, name)
333339
case "nosuid", "suid":
334340
if setSuid {
335341
return nil, fmt.Errorf("cannot pass 'nosuid' and 'suid' mnt.Options more than once: %w", errOptionArg)
336342
}
337343
setSuid = true
338-
mnt.Options = append(mnt.Options, name)
344+
mnt.mount.Options = append(mnt.mount.Options, name)
339345
case "noswap":
340346
if setSwap {
341347
return nil, fmt.Errorf("cannot pass 'noswap' mnt.Options more than once: %w", errOptionArg)
@@ -344,7 +350,7 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
344350
return nil, fmt.Errorf("the 'noswap' option is only allowed with rootful tmpfs mounts: %w", errOptionArg)
345351
}
346352
setSwap = true
347-
mnt.Options = append(mnt.Options, name)
353+
mnt.mount.Options = append(mnt.mount.Options, name)
348354
case "relabel":
349355
if setRelabel {
350356
return nil, fmt.Errorf("cannot pass 'relabel' option more than once: %w", errOptionArg)
@@ -355,19 +361,19 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
355361
}
356362
switch value {
357363
case "private":
358-
mnt.Options = append(mnt.Options, "Z")
364+
mnt.mount.Options = append(mnt.mount.Options, "Z")
359365
case "shared":
360-
mnt.Options = append(mnt.Options, "z")
366+
mnt.mount.Options = append(mnt.mount.Options, "z")
361367
default:
362368
return nil, fmt.Errorf("%s mount option must be 'private' or 'shared': %w", name, util.ErrBadMntOption)
363369
}
364370
case "shared", "rshared", "private", "rprivate", "slave", "rslave", "unbindable", "runbindable", "Z", "z", "no-dereference":
365-
mnt.Options = append(mnt.Options, name)
371+
mnt.mount.Options = append(mnt.mount.Options, name)
366372
case "src", "source":
367373
if mountType == define.TypeTmpfs {
368374
return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
369375
}
370-
if mnt.Source != "" {
376+
if mnt.mount.Source != "" {
371377
return nil, fmt.Errorf("cannot pass %q option more than once: %w", name, errOptionArg)
372378
}
373379
if !hasValue {
@@ -376,9 +382,17 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
376382
if len(value) == 0 {
377383
return nil, fmt.Errorf("host directory cannot be empty: %w", errOptionArg)
378384
}
379-
mnt.Source = value
385+
mnt.mount.Source = value
386+
case "subpath", "volume-subpath":
387+
if mountType != define.TypeVolume {
388+
return nil, fmt.Errorf("cannot set option %q on non-volume mounts", name)
389+
}
390+
if !hasValue {
391+
return nil, fmt.Errorf("%v: %w", name, errOptionArg)
392+
}
393+
mnt.subPath = value
380394
case "target", "dst", "destination":
381-
if mnt.Destination != "" {
395+
if mnt.mount.Destination != "" {
382396
return nil, fmt.Errorf("cannot pass %q option more than once: %w", name, errOptionArg)
383397
}
384398
if !hasValue {
@@ -387,7 +401,7 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
387401
if err := parse.ValidateVolumeCtrDir(value); err != nil {
388402
return nil, err
389403
}
390-
mnt.Destination = unixPathClean(value)
404+
mnt.mount.Destination = unixPathClean(value)
391405
case "tmpcopyup", "notmpcopyup":
392406
if mountType != define.TypeTmpfs {
393407
return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
@@ -396,23 +410,23 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
396410
return nil, fmt.Errorf("cannot pass 'tmpcopyup' and 'notmpcopyup' mnt.Options more than once: %w", errOptionArg)
397411
}
398412
setTmpcopyup = true
399-
mnt.Options = append(mnt.Options, name)
413+
mnt.mount.Options = append(mnt.mount.Options, name)
400414
case "tmpfs-mode":
401415
if mountType != define.TypeTmpfs {
402416
return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
403417
}
404418
if !hasValue {
405419
return nil, fmt.Errorf("%v: %w", name, errOptionArg)
406420
}
407-
mnt.Options = append(mnt.Options, fmt.Sprintf("mode=%s", value))
421+
mnt.mount.Options = append(mnt.mount.Options, fmt.Sprintf("mode=%s", value))
408422
case "tmpfs-size":
409423
if mountType != define.TypeTmpfs {
410424
return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
411425
}
412426
if !hasValue {
413427
return nil, fmt.Errorf("%v: %w", name, errOptionArg)
414428
}
415-
mnt.Options = append(mnt.Options, fmt.Sprintf("size=%s", value))
429+
mnt.mount.Options = append(mnt.mount.Options, fmt.Sprintf("size=%s", value))
416430
case "U", "chown":
417431
if setOwnership {
418432
return nil, fmt.Errorf("cannot pass 'U' or 'chown' option more than once: %w", errOptionArg)
@@ -422,7 +436,7 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
422436
return nil, err
423437
}
424438
if ok {
425-
mnt.Options = append(mnt.Options, "U")
439+
mnt.mount.Options = append(mnt.mount.Options, "U")
426440
}
427441
setOwnership = true
428442
case "volume-label":
@@ -434,25 +448,26 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
434448
if mountType != define.TypeVolume {
435449
return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
436450
}
437-
mnt.Options = append(mnt.Options, arg)
451+
mnt.mount.Options = append(mnt.mount.Options, arg)
438452
default:
439453
return nil, fmt.Errorf("%s: %w", name, util.ErrBadMntOption)
440454
}
441455
}
442-
if mountType != "glob" && len(mnt.Destination) == 0 {
456+
if mountType != "glob" && len(mnt.mount.Destination) == 0 {
443457
return nil, errNoDest
444458
}
445-
return &mnt, nil
459+
return mnt, nil
446460
}
447461

448462
// Parse glob mounts entry from the --mount flag.
449463
func getGlobMounts(args []string) ([]spec.Mount, error) {
450464
mounts := []spec.Mount{}
451465

452-
mnt, err := parseMountOptions("glob", args)
466+
uMnt, err := parseMountOptions("glob", args)
453467
if err != nil {
454468
return nil, err
455469
}
470+
mnt := uMnt.mount
456471

457472
globs, err := filepath.Glob(mnt.Source)
458473
if err != nil {
@@ -488,10 +503,11 @@ func getBindMount(args []string) (spec.Mount, error) {
488503
Type: define.TypeBind,
489504
}
490505
var err error
491-
mnt, err := parseMountOptions(newMount.Type, args)
506+
uMnt, err := parseMountOptions(newMount.Type, args)
492507
if err != nil {
493508
return newMount, err
494509
}
510+
mnt := uMnt.mount
495511

496512
if len(mnt.Destination) == 0 {
497513
return newMount, errNoDest
@@ -519,10 +535,11 @@ func parseMemoryMount(args []string, mountType string) (spec.Mount, error) {
519535
}
520536

521537
var err error
522-
mnt, err := parseMountOptions(newMount.Type, args)
538+
uMnt, err := parseMountOptions(newMount.Type, args)
523539
if err != nil {
524540
return newMount, err
525541
}
542+
mnt := uMnt.mount
526543
if len(mnt.Destination) == 0 {
527544
return newMount, errNoDest
528545
}
@@ -576,12 +593,14 @@ func getNamedVolume(args []string) (*specgen.NamedVolume, error) {
576593
if err != nil {
577594
return nil, err
578595
}
579-
if len(mnt.Destination) == 0 {
596+
if len(mnt.mount.Destination) == 0 {
580597
return nil, errNoDest
581598
}
582-
newVolume.Options = mnt.Options
583-
newVolume.Name = mnt.Source
584-
newVolume.Dest = mnt.Destination
599+
600+
newVolume.Options = mnt.mount.Options
601+
newVolume.SubPath = mnt.subPath
602+
newVolume.Name = mnt.mount.Source
603+
newVolume.Dest = mnt.mount.Destination
585604
return newVolume, nil
586605
}
587606

test/e2e/run_volume_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,4 +1069,24 @@ RUN chmod 755 /test1 /test2 /test3`, ALPINE)
10691069

10701070
mountVolumeAndCheckDirectory(volName, "/test3", "test2", imgName)
10711071
})
1072+
1073+
It("podman run --mount type=volume,subpath=", func() {
1074+
volName := "testvol"
1075+
mkvol := podmanTest.Podman([]string{"volume", "create", volName})
1076+
mkvol.WaitWithDefaultTimeout()
1077+
Expect(mkvol).Should(ExitCleanly())
1078+
1079+
subvol := "/test/test2/"
1080+
pathInCtr := "/mnt"
1081+
pathToCreate := filepath.Join(pathInCtr, subvol)
1082+
popvol := podmanTest.Podman([]string{"run", "-v", fmt.Sprintf("%s:/mnt", volName), ALPINE, "sh", "-c", fmt.Sprintf("mkdir -p %s; touch %s; touch %s", pathToCreate, filepath.Join(pathToCreate, "foo"), filepath.Join(pathToCreate, "bar"))})
1083+
popvol.WaitWithDefaultTimeout()
1084+
Expect(popvol).Should(ExitCleanly())
1085+
1086+
checkCtr := podmanTest.Podman([]string{"run", "--mount", fmt.Sprintf("type=volume,source=%s,target=%s,subpath=%s", volName, pathInCtr, subvol), ALPINE, "ls", pathInCtr})
1087+
checkCtr.WaitWithDefaultTimeout()
1088+
Expect(checkCtr).To(ExitCleanly())
1089+
Expect(checkCtr.OutputToString()).To(ContainSubstring("foo"))
1090+
Expect(checkCtr.OutputToString()).To(ContainSubstring("bar"))
1091+
})
10721092
})

0 commit comments

Comments
 (0)