Skip to content

Commit 8f2d501

Browse files
Merge pull request #24532 from mheon/subpath
Add subpath support to volumes in `--mount` option
2 parents 3f10c91 + 8a192c8 commit 8f2d501

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
@@ -251,7 +251,7 @@ type ContainerNamedVolume struct {
251251
// This is used for emptyDir volumes from a kube yaml
252252
IsAnonymous bool `json:"setAnonymous,omitempty"`
253253
// SubPath determines which part of the Source will be mounted in the container
254-
SubPath string
254+
SubPath string `json:",omitempty"`
255255
}
256256

257257
// 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)