podman
678 строк · 20.3 Кб
1package specgenutil
2
3import (
4"errors"
5"fmt"
6"path"
7"path/filepath"
8"strings"
9
10"github.com/containers/common/pkg/config"
11"github.com/containers/common/pkg/parse"
12"github.com/containers/podman/v5/libpod/define"
13"github.com/containers/podman/v5/pkg/rootless"
14"github.com/containers/podman/v5/pkg/specgen"
15"github.com/containers/podman/v5/pkg/specgenutilexternal"
16"github.com/containers/podman/v5/pkg/util"
17spec "github.com/opencontainers/runtime-spec/specs-go"
18)
19
20var (
21errOptionArg = errors.New("must provide an argument for option")
22errNoDest = errors.New("must set volume destination")
23)
24
25// Parse all volume-related options in the create config into a set of mounts
26// and named volumes to add to the container.
27// Handles --volumes, --mount, and --tmpfs flags.
28// Does not handle image volumes, init, and --volumes-from flags.
29// Can also add tmpfs mounts from read-only tmpfs.
30// TODO: handle options parsing/processing via containers/storage/pkg/mount
31func parseVolumes(rtc *config.Config, volumeFlag, mountFlag, tmpfsFlag []string) ([]spec.Mount, []*specgen.NamedVolume, []*specgen.OverlayVolume, []*specgen.ImageVolume, error) {
32// Get mounts from the --mounts flag.
33// TODO: The runtime config part of this needs to move into pkg/specgen/generate to avoid querying containers.conf on the client.
34unifiedMounts, unifiedVolumes, unifiedImageVolumes, err := Mounts(mountFlag, rtc.Mounts())
35if err != nil {
36return nil, nil, nil, nil, err
37}
38
39// Next --volumes flag.
40volumeMounts, volumeVolumes, overlayVolumes, err := specgen.GenVolumeMounts(volumeFlag)
41if err != nil {
42return nil, nil, nil, nil, err
43}
44
45// Next --tmpfs flag.
46tmpfsMounts, err := getTmpfsMounts(tmpfsFlag)
47if err != nil {
48return nil, nil, nil, nil, err
49}
50
51// Unify mounts from --mount, --volume, --tmpfs.
52// Start with --volume.
53for dest, mount := range volumeMounts {
54if vol, ok := unifiedMounts[dest]; ok {
55if mount.Source == vol.Source &&
56specgen.StringSlicesEqual(vol.Options, mount.Options) {
57continue
58}
59return nil, nil, nil, nil, fmt.Errorf("%v: %w", dest, specgen.ErrDuplicateDest)
60}
61unifiedMounts[dest] = mount
62}
63for dest, volume := range volumeVolumes {
64if vol, ok := unifiedVolumes[dest]; ok {
65if volume.Name == vol.Name &&
66specgen.StringSlicesEqual(vol.Options, volume.Options) {
67continue
68}
69return nil, nil, nil, nil, fmt.Errorf("%v: %w", dest, specgen.ErrDuplicateDest)
70}
71unifiedVolumes[dest] = volume
72}
73// Now --tmpfs
74for dest, tmpfs := range tmpfsMounts {
75if vol, ok := unifiedMounts[dest]; ok {
76if vol.Type != define.TypeTmpfs {
77return nil, nil, nil, nil, fmt.Errorf("%v: %w", dest, specgen.ErrDuplicateDest)
78}
79continue
80}
81unifiedMounts[dest] = tmpfs
82}
83
84// Check for conflicts between named volumes, overlay & image volumes,
85// and mounts
86allMounts := make(map[string]bool)
87testAndSet := func(dest string) error {
88if _, ok := allMounts[dest]; ok {
89return fmt.Errorf("%v: %w", dest, specgen.ErrDuplicateDest)
90}
91allMounts[dest] = true
92return nil
93}
94for dest := range unifiedMounts {
95if err := testAndSet(dest); err != nil {
96return nil, nil, nil, nil, err
97}
98}
99for dest := range unifiedVolumes {
100if err := testAndSet(dest); err != nil {
101return nil, nil, nil, nil, err
102}
103}
104for dest := range overlayVolumes {
105if err := testAndSet(dest); err != nil {
106return nil, nil, nil, nil, err
107}
108}
109for dest := range unifiedImageVolumes {
110if err := testAndSet(dest); err != nil {
111return nil, nil, nil, nil, err
112}
113}
114
115// Final step: maps to arrays
116finalMounts := make([]spec.Mount, 0, len(unifiedMounts))
117for _, mount := range unifiedMounts {
118if mount.Type == define.TypeBind {
119absSrc, err := specgen.ConvertWinMountPath(mount.Source)
120if err != nil {
121return nil, nil, nil, nil, fmt.Errorf("getting absolute path of %s: %w", mount.Source, err)
122}
123mount.Source = absSrc
124}
125finalMounts = append(finalMounts, mount)
126}
127finalVolumes := make([]*specgen.NamedVolume, 0, len(unifiedVolumes))
128for _, volume := range unifiedVolumes {
129finalVolumes = append(finalVolumes, volume)
130}
131finalOverlayVolume := make([]*specgen.OverlayVolume, 0)
132for _, volume := range overlayVolumes {
133finalOverlayVolume = append(finalOverlayVolume, volume)
134}
135finalImageVolumes := make([]*specgen.ImageVolume, 0, len(unifiedImageVolumes))
136for _, volume := range unifiedImageVolumes {
137finalImageVolumes = append(finalImageVolumes, volume)
138}
139
140return finalMounts, finalVolumes, finalOverlayVolume, finalImageVolumes, nil
141}
142
143// Mounts takes user-provided input from the --mount flag as well as Mounts
144// specified in containers.conf and creates OCI spec mounts and Libpod named volumes.
145// podman run --mount type=bind,src=/etc/resolv.conf,target=/etc/resolv.conf ...
146// podman run --mount type=tmpfs,target=/dev/shm ...
147// podman run --mount type=volume,source=test-volume, ...
148func Mounts(mountFlag []string, configMounts []string) (map[string]spec.Mount, map[string]*specgen.NamedVolume, map[string]*specgen.ImageVolume, error) {
149finalMounts := make(map[string]spec.Mount)
150finalNamedVolumes := make(map[string]*specgen.NamedVolume)
151finalImageVolumes := make(map[string]*specgen.ImageVolume)
152parseMounts := func(mounts []string, ignoreDup bool) error {
153for _, mount := range mounts {
154// TODO: Docker defaults to "volume" if no mount type is specified.
155mountType, tokens, err := specgenutilexternal.FindMountType(mount)
156if err != nil {
157return err
158}
159switch mountType {
160case define.TypeBind:
161mount, err := getBindMount(tokens)
162if err != nil {
163return err
164}
165if _, ok := finalMounts[mount.Destination]; ok {
166if ignoreDup {
167continue
168}
169return fmt.Errorf("%v: %w", mount.Destination, specgen.ErrDuplicateDest)
170}
171finalMounts[mount.Destination] = mount
172case "glob":
173mounts, err := getGlobMounts(tokens)
174if err != nil {
175return err
176}
177for _, mount := range mounts {
178if _, ok := finalMounts[mount.Destination]; ok {
179if ignoreDup {
180continue
181}
182return fmt.Errorf("%v: %w", mount.Destination, specgen.ErrDuplicateDest)
183}
184finalMounts[mount.Destination] = mount
185}
186case define.TypeTmpfs, define.TypeRamfs:
187mount, err := parseMemoryMount(tokens, mountType)
188if err != nil {
189return err
190}
191if _, ok := finalMounts[mount.Destination]; ok {
192if ignoreDup {
193continue
194}
195return fmt.Errorf("%v: %w", mount.Destination, specgen.ErrDuplicateDest)
196}
197finalMounts[mount.Destination] = mount
198case define.TypeDevpts:
199mount, err := getDevptsMount(tokens)
200if err != nil {
201return err
202}
203if _, ok := finalMounts[mount.Destination]; ok {
204if ignoreDup {
205continue
206}
207return fmt.Errorf("%v: %w", mount.Destination, specgen.ErrDuplicateDest)
208}
209finalMounts[mount.Destination] = mount
210case "image":
211volume, err := getImageVolume(tokens)
212if err != nil {
213return err
214}
215if _, ok := finalImageVolumes[volume.Destination]; ok {
216if ignoreDup {
217continue
218}
219return fmt.Errorf("%v: %w", volume.Destination, specgen.ErrDuplicateDest)
220}
221finalImageVolumes[volume.Destination] = volume
222case "volume":
223volume, err := getNamedVolume(tokens)
224if err != nil {
225return err
226}
227if _, ok := finalNamedVolumes[volume.Dest]; ok {
228if ignoreDup {
229continue
230}
231return fmt.Errorf("%v: %w", volume.Dest, specgen.ErrDuplicateDest)
232}
233finalNamedVolumes[volume.Dest] = volume
234default:
235return fmt.Errorf("invalid filesystem type %q", mountType)
236}
237}
238return nil
239}
240
241// Parse mounts passed in from the user
242if err := parseMounts(mountFlag, false); err != nil {
243return nil, nil, nil, err
244}
245
246// If user specified a mount flag that conflicts with a containers.conf flag, then ignore
247// the duplicate. This means that the parsing of the containers.conf configMounts should always
248// happen second.
249if err := parseMounts(configMounts, true); err != nil {
250return nil, nil, nil, fmt.Errorf("parsing containers.conf mounts: %w", err)
251}
252
253return finalMounts, finalNamedVolumes, finalImageVolumes, nil
254}
255
256func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
257var setTmpcopyup, setRORW, setSuid, setDev, setExec, setRelabel, setOwnership, setSwap bool
258
259mnt := spec.Mount{}
260for _, arg := range args {
261name, value, hasValue := strings.Cut(arg, "=")
262switch name {
263case "bind-nonrecursive":
264if mountType != define.TypeBind {
265return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
266}
267mnt.Options = append(mnt.Options, define.TypeBind)
268case "bind-propagation":
269if mountType != define.TypeBind {
270return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
271}
272if !hasValue {
273return nil, fmt.Errorf("%v: %w", name, errOptionArg)
274}
275mnt.Options = append(mnt.Options, value)
276case "consistency":
277// Often used on MACs and mistakenly on Linux platforms.
278// Since Docker ignores this option so shall we.
279continue
280case "idmap":
281if hasValue {
282mnt.Options = append(mnt.Options, fmt.Sprintf("idmap=%s", value))
283} else {
284mnt.Options = append(mnt.Options, "idmap")
285}
286case "readonly", "ro", "rw":
287if setRORW {
288return nil, fmt.Errorf("cannot pass 'readonly', 'ro', or 'rw' mnt.Options more than once: %w", errOptionArg)
289}
290setRORW = true
291// Can be formatted as one of:
292// readonly
293// readonly=[true|false]
294// ro
295// ro=[true|false]
296// rw
297// rw=[true|false]
298if name == "readonly" {
299name = "ro"
300}
301if hasValue {
302switch strings.ToLower(value) {
303case "true":
304mnt.Options = append(mnt.Options, name)
305case "false":
306// Set the opposite only for rw
307// ro's opposite is the default
308if name == "rw" {
309mnt.Options = append(mnt.Options, "ro")
310}
311}
312} else {
313mnt.Options = append(mnt.Options, name)
314}
315case "nodev", "dev":
316if setDev {
317return nil, fmt.Errorf("cannot pass 'nodev' and 'dev' mnt.Options more than once: %w", errOptionArg)
318}
319setDev = true
320mnt.Options = append(mnt.Options, name)
321case "noexec", "exec":
322if setExec {
323return nil, fmt.Errorf("cannot pass 'noexec' and 'exec' mnt.Options more than once: %w", errOptionArg)
324}
325setExec = true
326mnt.Options = append(mnt.Options, name)
327case "nosuid", "suid":
328if setSuid {
329return nil, fmt.Errorf("cannot pass 'nosuid' and 'suid' mnt.Options more than once: %w", errOptionArg)
330}
331setSuid = true
332mnt.Options = append(mnt.Options, name)
333case "noswap":
334if setSwap {
335return nil, fmt.Errorf("cannot pass 'noswap' mnt.Options more than once: %w", errOptionArg)
336}
337if rootless.IsRootless() {
338return nil, fmt.Errorf("the 'noswap' option is only allowed with rootful tmpfs mounts: %w", errOptionArg)
339}
340setSwap = true
341mnt.Options = append(mnt.Options, name)
342case "relabel":
343if setRelabel {
344return nil, fmt.Errorf("cannot pass 'relabel' option more than once: %w", errOptionArg)
345}
346setRelabel = true
347if !hasValue {
348return nil, fmt.Errorf("%s mount option must be 'private' or 'shared': %w", name, util.ErrBadMntOption)
349}
350switch value {
351case "private":
352mnt.Options = append(mnt.Options, "Z")
353case "shared":
354mnt.Options = append(mnt.Options, "z")
355default:
356return nil, fmt.Errorf("%s mount option must be 'private' or 'shared': %w", name, util.ErrBadMntOption)
357}
358case "shared", "rshared", "private", "rprivate", "slave", "rslave", "unbindable", "runbindable", "Z", "z", "no-dereference":
359mnt.Options = append(mnt.Options, name)
360case "src", "source":
361if mountType == define.TypeTmpfs {
362return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
363}
364if mnt.Source != "" {
365return nil, fmt.Errorf("cannot pass %q option more than once: %w", name, errOptionArg)
366}
367if !hasValue {
368return nil, fmt.Errorf("%v: %w", name, errOptionArg)
369}
370if len(value) == 0 {
371return nil, fmt.Errorf("host directory cannot be empty: %w", errOptionArg)
372}
373mnt.Source = value
374case "target", "dst", "destination":
375if mnt.Destination != "" {
376return nil, fmt.Errorf("cannot pass %q option more than once: %w", name, errOptionArg)
377}
378if !hasValue {
379return nil, fmt.Errorf("%v: %w", name, errOptionArg)
380}
381if err := parse.ValidateVolumeCtrDir(value); err != nil {
382return nil, err
383}
384mnt.Destination = unixPathClean(value)
385case "tmpcopyup", "notmpcopyup":
386if mountType != define.TypeTmpfs {
387return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
388}
389if setTmpcopyup {
390return nil, fmt.Errorf("cannot pass 'tmpcopyup' and 'notmpcopyup' mnt.Options more than once: %w", errOptionArg)
391}
392setTmpcopyup = true
393mnt.Options = append(mnt.Options, name)
394case "tmpfs-mode":
395if mountType != define.TypeTmpfs {
396return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
397}
398if !hasValue {
399return nil, fmt.Errorf("%v: %w", name, errOptionArg)
400}
401mnt.Options = append(mnt.Options, fmt.Sprintf("mode=%s", value))
402case "tmpfs-size":
403if mountType != define.TypeTmpfs {
404return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
405}
406if !hasValue {
407return nil, fmt.Errorf("%v: %w", name, errOptionArg)
408}
409mnt.Options = append(mnt.Options, fmt.Sprintf("size=%s", value))
410case "U", "chown":
411if setOwnership {
412return nil, fmt.Errorf("cannot pass 'U' or 'chown' option more than once: %w", errOptionArg)
413}
414ok, err := validChownFlag(value)
415if err != nil {
416return nil, err
417}
418if ok {
419mnt.Options = append(mnt.Options, "U")
420}
421setOwnership = true
422case "volume-label":
423if mountType != define.TypeVolume {
424return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
425}
426return nil, fmt.Errorf("the --volume-label option is not presently implemented")
427case "volume-opt":
428if mountType != define.TypeVolume {
429return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
430}
431mnt.Options = append(mnt.Options, arg)
432default:
433return nil, fmt.Errorf("%s: %w", name, util.ErrBadMntOption)
434}
435}
436if mountType != "glob" && len(mnt.Destination) == 0 {
437return nil, errNoDest
438}
439return &mnt, nil
440}
441
442// Parse glob mounts entry from the --mount flag.
443func getGlobMounts(args []string) ([]spec.Mount, error) {
444mounts := []spec.Mount{}
445
446mnt, err := parseMountOptions("glob", args)
447if err != nil {
448return nil, err
449}
450
451globs, err := filepath.Glob(mnt.Source)
452if err != nil {
453return nil, err
454}
455if len(globs) == 0 {
456return nil, fmt.Errorf("no file paths matching glob %q", mnt.Source)
457}
458
459options, err := parse.ValidateVolumeOpts(mnt.Options)
460if err != nil {
461return nil, err
462}
463for _, src := range globs {
464var newMount spec.Mount
465newMount.Type = define.TypeBind
466newMount.Options = options
467newMount.Source = src
468if len(mnt.Destination) == 0 {
469newMount.Destination = src
470} else {
471newMount.Destination = filepath.Join(mnt.Destination, filepath.Base(src))
472}
473mounts = append(mounts, newMount)
474}
475
476return mounts, nil
477}
478
479// Parse a single bind mount entry from the --mount flag.
480func getBindMount(args []string) (spec.Mount, error) {
481newMount := spec.Mount{
482Type: define.TypeBind,
483}
484var err error
485mnt, err := parseMountOptions(newMount.Type, args)
486if err != nil {
487return newMount, err
488}
489
490if len(mnt.Destination) == 0 {
491return newMount, errNoDest
492}
493
494if len(mnt.Source) == 0 {
495mnt.Source = mnt.Destination
496}
497
498options, err := parse.ValidateVolumeOpts(mnt.Options)
499if err != nil {
500return newMount, err
501}
502newMount.Source = mnt.Source
503newMount.Destination = mnt.Destination
504newMount.Options = options
505return newMount, nil
506}
507
508// Parse a single tmpfs/ramfs mount entry from the --mount flag
509func parseMemoryMount(args []string, mountType string) (spec.Mount, error) {
510newMount := spec.Mount{
511Type: mountType,
512Source: mountType,
513}
514
515var err error
516mnt, err := parseMountOptions(newMount.Type, args)
517if err != nil {
518return newMount, err
519}
520if len(mnt.Destination) == 0 {
521return newMount, errNoDest
522}
523newMount.Destination = mnt.Destination
524newMount.Options = mnt.Options
525return newMount, nil
526}
527
528// Parse a single devpts mount entry from the --mount flag
529func getDevptsMount(args []string) (spec.Mount, error) {
530newMount := spec.Mount{
531Type: define.TypeDevpts,
532Source: define.TypeDevpts,
533}
534
535var setDest bool
536
537for _, arg := range args {
538name, value, hasValue := strings.Cut(arg, "=")
539switch name {
540case "uid", "gid", "mode", "ptxmode", "newinstance", "max":
541newMount.Options = append(newMount.Options, arg)
542case "target", "dst", "destination":
543if !hasValue {
544return newMount, fmt.Errorf("%v: %w", name, errOptionArg)
545}
546if err := parse.ValidateVolumeCtrDir(value); err != nil {
547return newMount, err
548}
549newMount.Destination = unixPathClean(value)
550setDest = true
551default:
552return newMount, fmt.Errorf("%s: %w", name, util.ErrBadMntOption)
553}
554}
555
556if !setDest {
557return newMount, errNoDest
558}
559
560return newMount, nil
561}
562
563// Parse a single volume mount entry from the --mount flag.
564// Note that the volume-label option for named volumes is currently NOT supported.
565// TODO: add support for --volume-label
566func getNamedVolume(args []string) (*specgen.NamedVolume, error) {
567newVolume := new(specgen.NamedVolume)
568
569mnt, err := parseMountOptions(define.TypeVolume, args)
570if err != nil {
571return nil, err
572}
573if len(mnt.Destination) == 0 {
574return nil, errNoDest
575}
576newVolume.Options = mnt.Options
577newVolume.Name = mnt.Source
578newVolume.Dest = mnt.Destination
579return newVolume, nil
580}
581
582// Parse the arguments into an image volume. An image volume is a volume based
583// on a container image. The container image is first mounted on the host and
584// is then bind-mounted into the container. An ImageVolume is always mounted
585// read-only.
586func getImageVolume(args []string) (*specgen.ImageVolume, error) {
587newVolume := new(specgen.ImageVolume)
588
589for _, arg := range args {
590name, value, hasValue := strings.Cut(arg, "=")
591switch name {
592case "src", "source":
593if !hasValue {
594return nil, fmt.Errorf("%v: %w", name, errOptionArg)
595}
596newVolume.Source = value
597case "target", "dst", "destination":
598if !hasValue {
599return nil, fmt.Errorf("%v: %w", name, errOptionArg)
600}
601if err := parse.ValidateVolumeCtrDir(value); err != nil {
602return nil, err
603}
604newVolume.Destination = unixPathClean(value)
605case "rw", "readwrite":
606switch value {
607case "true":
608newVolume.ReadWrite = true
609case "false":
610// Nothing to do. RO is default.
611default:
612return nil, fmt.Errorf("invalid rw value %q: %w", value, util.ErrBadMntOption)
613}
614case "consistency":
615// Often used on MACs and mistakenly on Linux platforms.
616// Since Docker ignores this option so shall we.
617continue
618default:
619return nil, fmt.Errorf("%s: %w", name, util.ErrBadMntOption)
620}
621}
622
623if len(newVolume.Source)*len(newVolume.Destination) == 0 {
624return nil, errors.New("must set source and destination for image volume")
625}
626
627return newVolume, nil
628}
629
630// GetTmpfsMounts creates spec.Mount structs for user-requested tmpfs mounts
631func getTmpfsMounts(tmpfsFlag []string) (map[string]spec.Mount, error) {
632m := make(map[string]spec.Mount)
633for _, i := range tmpfsFlag {
634// Default options if nothing passed
635var options []string
636spliti := strings.Split(i, ":")
637destPath := spliti[0]
638if err := parse.ValidateVolumeCtrDir(spliti[0]); err != nil {
639return nil, err
640}
641if len(spliti) > 1 {
642options = strings.Split(spliti[1], ",")
643}
644
645if vol, ok := m[destPath]; ok {
646if specgen.StringSlicesEqual(vol.Options, options) {
647continue
648}
649return nil, fmt.Errorf("%v: %w", destPath, specgen.ErrDuplicateDest)
650}
651mount := spec.Mount{
652Destination: unixPathClean(destPath),
653Type: define.TypeTmpfs,
654Options: options,
655Source: define.TypeTmpfs,
656}
657m[destPath] = mount
658}
659return m, nil
660}
661
662// validChownFlag ensures that the U or chown flag is correctly used
663func validChownFlag(value string) (bool, error) {
664// U=[true|false]
665switch {
666case strings.EqualFold(value, "true"), value == "":
667return true, nil
668case strings.EqualFold(value, "false"):
669return false, nil
670default:
671return false, fmt.Errorf("'U' or 'chown' must be set to true or false, instead received %q: %w", value, errOptionArg)
672}
673}
674
675// Use path instead of filepath to preserve Unix style paths on Windows
676func unixPathClean(p string) string {
677return path.Clean(p)
678}
679