podman
643 строки · 20.9 Кб
1package volumes
2
3import (
4"context"
5"fmt"
6"os"
7"path"
8"path/filepath"
9"strconv"
10"strings"
11
12"errors"
13
14"github.com/containers/buildah/copier"
15"github.com/containers/buildah/define"
16"github.com/containers/buildah/internal"
17internalParse "github.com/containers/buildah/internal/parse"
18"github.com/containers/buildah/internal/tmpdir"
19internalUtil "github.com/containers/buildah/internal/util"
20"github.com/containers/common/pkg/parse"
21"github.com/containers/image/v5/types"
22"github.com/containers/storage"
23"github.com/containers/storage/pkg/idtools"
24"github.com/containers/storage/pkg/lockfile"
25"github.com/containers/storage/pkg/unshare"
26specs "github.com/opencontainers/runtime-spec/specs-go"
27selinux "github.com/opencontainers/selinux/go-selinux"
28)
29
30const (
31// TypeTmpfs is the type for mounting tmpfs
32TypeTmpfs = "tmpfs"
33// TypeCache is the type for mounting a common persistent cache from host
34TypeCache = "cache"
35// mount=type=cache must create a persistent directory on host so its available for all consecutive builds.
36// Lifecycle of following directory will be inherited from how host machine treats temporary directory
37buildahCacheDir = "buildah-cache"
38// mount=type=cache allows users to lock a cache store while its being used by another build
39BuildahCacheLockfile = "buildah-cache-lockfile"
40// All the lockfiles are stored in a separate directory inside `BuildahCacheDir`
41// Example `/var/tmp/buildah-cache/<target>/buildah-cache-lockfile`
42BuildahCacheLockfileDir = "buildah-cache-lockfiles"
43)
44
45var (
46errBadMntOption = errors.New("invalid mount option")
47errBadOptionArg = errors.New("must provide an argument for option")
48errBadVolDest = errors.New("must set volume destination")
49errBadVolSrc = errors.New("must set volume source")
50errDuplicateDest = errors.New("duplicate mount destination")
51)
52
53// CacheParent returns a cache parent for --mount=type=cache
54func CacheParent() string {
55return filepath.Join(tmpdir.GetTempDir(), buildahCacheDir+"-"+strconv.Itoa(unshare.GetRootlessUID()))
56}
57
58// FIXME: this code needs to be merged with pkg/parse/parse.go ValidateVolumeOpts
59// GetBindMount parses a single bind mount entry from the --mount flag.
60// Returns specifiedMount and a string which contains name of image that we mounted otherwise its empty.
61// Caller is expected to perform unmount of any mounted images
62func GetBindMount(ctx *types.SystemContext, args []string, contextDir string, store storage.Store, imageMountLabel string, additionalMountPoints map[string]internal.StageMountDetails, workDir string) (specs.Mount, string, error) {
63newMount := specs.Mount{
64Type: define.TypeBind,
65}
66
67setRelabel := false
68mountReadability := false
69setDest := false
70bindNonRecursive := false
71fromImage := ""
72
73for _, val := range args {
74argName, argValue, hasArgValue := strings.Cut(val, "=")
75switch argName {
76case "type":
77// This is already processed
78continue
79case "bind-nonrecursive":
80newMount.Options = append(newMount.Options, "bind")
81bindNonRecursive = true
82case "ro", "nosuid", "nodev", "noexec":
83// TODO: detect duplication of these options.
84// (Is this necessary?)
85newMount.Options = append(newMount.Options, argName)
86mountReadability = true
87case "rw", "readwrite":
88newMount.Options = append(newMount.Options, "rw")
89mountReadability = true
90case "readonly":
91// Alias for "ro"
92newMount.Options = append(newMount.Options, "ro")
93mountReadability = true
94case "shared", "rshared", "private", "rprivate", "slave", "rslave", "Z", "z", "U", "no-dereference":
95if hasArgValue {
96return newMount, "", fmt.Errorf("%v: %w", val, errBadOptionArg)
97}
98newMount.Options = append(newMount.Options, argName)
99case "from":
100if !hasArgValue {
101return newMount, "", fmt.Errorf("%v: %w", argName, errBadOptionArg)
102}
103fromImage = argValue
104case "bind-propagation":
105if !hasArgValue {
106return newMount, "", fmt.Errorf("%v: %w", argName, errBadOptionArg)
107}
108newMount.Options = append(newMount.Options, argValue)
109case "src", "source":
110if !hasArgValue {
111return newMount, "", fmt.Errorf("%v: %w", argName, errBadOptionArg)
112}
113newMount.Source = argValue
114case "target", "dst", "destination":
115if !hasArgValue {
116return newMount, "", fmt.Errorf("%v: %w", argName, errBadOptionArg)
117}
118targetPath := argValue
119if !path.IsAbs(targetPath) {
120targetPath = filepath.Join(workDir, targetPath)
121}
122if err := parse.ValidateVolumeCtrDir(targetPath); err != nil {
123return newMount, "", err
124}
125newMount.Destination = targetPath
126setDest = true
127case "relabel":
128if setRelabel {
129return newMount, "", fmt.Errorf("cannot pass 'relabel' option more than once: %w", errBadOptionArg)
130}
131setRelabel = true
132switch argValue {
133case "private":
134newMount.Options = append(newMount.Options, "Z")
135case "shared":
136newMount.Options = append(newMount.Options, "z")
137default:
138return newMount, "", fmt.Errorf("%s mount option must be 'private' or 'shared': %w", argName, errBadMntOption)
139}
140case "consistency":
141// Option for OS X only, has no meaning on other platforms
142// and can thus be safely ignored.
143// See also the handling of the equivalent "delegated" and "cached" in ValidateVolumeOpts
144default:
145return newMount, "", fmt.Errorf("%v: %w", argName, errBadMntOption)
146}
147}
148
149// default mount readability is always readonly
150if !mountReadability {
151newMount.Options = append(newMount.Options, "ro")
152}
153
154// Following variable ensures that we return imagename only if we did additional mount
155isImageMounted := false
156if fromImage != "" {
157mountPoint := ""
158if additionalMountPoints != nil {
159if val, ok := additionalMountPoints[fromImage]; ok {
160mountPoint = val.MountPoint
161}
162}
163// if mountPoint of image was not found in additionalMap
164// or additionalMap was nil, try mounting image
165if mountPoint == "" {
166image, err := internalUtil.LookupImage(ctx, store, fromImage)
167if err != nil {
168return newMount, "", err
169}
170
171mountPoint, err = image.Mount(context.Background(), nil, imageMountLabel)
172if err != nil {
173return newMount, "", err
174}
175isImageMounted = true
176}
177contextDir = mountPoint
178}
179
180// buildkit parity: default bind option must be `rbind`
181// unless specified
182if !bindNonRecursive {
183newMount.Options = append(newMount.Options, "rbind")
184}
185
186if !setDest {
187return newMount, fromImage, errBadVolDest
188}
189
190// buildkit parity: support absolute path for sources from current build context
191if contextDir != "" {
192// path should be /contextDir/specified path
193evaluated, err := copier.Eval(contextDir, newMount.Source, copier.EvalOptions{})
194if err != nil {
195return newMount, "", err
196}
197newMount.Source = evaluated
198} else {
199// looks like its coming from `build run --mount=type=bind` allow using absolute path
200// error out if no source is set
201if newMount.Source == "" {
202return newMount, "", errBadVolSrc
203}
204if err := parse.ValidateVolumeHostDir(newMount.Source); err != nil {
205return newMount, "", err
206}
207}
208
209opts, err := parse.ValidateVolumeOpts(newMount.Options)
210if err != nil {
211return newMount, fromImage, err
212}
213newMount.Options = opts
214
215if !isImageMounted {
216// we don't want any cleanups if image was not mounted explicitly
217// so dont return anything
218fromImage = ""
219}
220
221return newMount, fromImage, nil
222}
223
224// GetCacheMount parses a single cache mount entry from the --mount flag.
225//
226// If this function succeeds and returns a non-nil *lockfile.LockFile, the caller must unlock it (when??).
227func GetCacheMount(args []string, store storage.Store, imageMountLabel string, additionalMountPoints map[string]internal.StageMountDetails, workDir string) (specs.Mount, *lockfile.LockFile, error) {
228var err error
229var mode uint64
230var buildahLockFilesDir string
231var (
232setDest bool
233setShared bool
234setReadOnly bool
235foundSElinuxLabel bool
236)
237fromStage := ""
238newMount := specs.Mount{
239Type: define.TypeBind,
240}
241// if id is set a new subdirectory with `id` will be created under /host-temp/buildah-build-cache/id
242id := ""
243// buildkit parity: cache directory defaults to 755
244mode = 0o755
245// buildkit parity: cache directory defaults to uid 0 if not specified
246uid := 0
247// buildkit parity: cache directory defaults to gid 0 if not specified
248gid := 0
249// sharing mode
250sharing := "shared"
251
252for _, val := range args {
253argName, argValue, hasArgValue := strings.Cut(val, "=")
254switch argName {
255case "type":
256// This is already processed
257continue
258case "nosuid", "nodev", "noexec":
259// TODO: detect duplication of these options.
260// (Is this necessary?)
261newMount.Options = append(newMount.Options, argName)
262case "rw", "readwrite":
263newMount.Options = append(newMount.Options, "rw")
264case "readonly", "ro":
265// Alias for "ro"
266newMount.Options = append(newMount.Options, "ro")
267setReadOnly = true
268case "Z", "z":
269newMount.Options = append(newMount.Options, argName)
270foundSElinuxLabel = true
271case "shared", "rshared", "private", "rprivate", "slave", "rslave", "U":
272newMount.Options = append(newMount.Options, argName)
273setShared = true
274case "sharing":
275sharing = argValue
276case "bind-propagation":
277if !hasArgValue {
278return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
279}
280newMount.Options = append(newMount.Options, argValue)
281case "id":
282if !hasArgValue {
283return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
284}
285id = argValue
286case "from":
287if !hasArgValue {
288return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
289}
290fromStage = argValue
291case "target", "dst", "destination":
292if !hasArgValue {
293return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
294}
295targetPath := argValue
296if !path.IsAbs(targetPath) {
297targetPath = filepath.Join(workDir, targetPath)
298}
299if err := parse.ValidateVolumeCtrDir(targetPath); err != nil {
300return newMount, nil, err
301}
302newMount.Destination = targetPath
303setDest = true
304case "src", "source":
305if !hasArgValue {
306return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
307}
308newMount.Source = argValue
309case "mode":
310if !hasArgValue {
311return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
312}
313mode, err = strconv.ParseUint(argValue, 8, 32)
314if err != nil {
315return newMount, nil, fmt.Errorf("unable to parse cache mode: %w", err)
316}
317case "uid":
318if !hasArgValue {
319return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
320}
321uid, err = strconv.Atoi(argValue)
322if err != nil {
323return newMount, nil, fmt.Errorf("unable to parse cache uid: %w", err)
324}
325case "gid":
326if !hasArgValue {
327return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
328}
329gid, err = strconv.Atoi(argValue)
330if err != nil {
331return newMount, nil, fmt.Errorf("unable to parse cache gid: %w", err)
332}
333default:
334return newMount, nil, fmt.Errorf("%v: %w", argName, errBadMntOption)
335}
336}
337
338// If selinux is enabled and no selinux option was configured
339// default to `z` i.e shared content label.
340if !foundSElinuxLabel && (selinux.EnforceMode() != selinux.Disabled) && fromStage == "" {
341newMount.Options = append(newMount.Options, "z")
342}
343
344if !setDest {
345return newMount, nil, errBadVolDest
346}
347
348if fromStage != "" {
349// do not create cache on host
350// instead use read-only mounted stage as cache
351mountPoint := ""
352if additionalMountPoints != nil {
353if val, ok := additionalMountPoints[fromStage]; ok {
354if val.IsStage {
355mountPoint = val.MountPoint
356}
357}
358}
359// Cache does not supports using image so if not stage found
360// return with error
361if mountPoint == "" {
362return newMount, nil, fmt.Errorf("no stage found with name %s", fromStage)
363}
364// path should be /contextDir/specified path
365newMount.Source = filepath.Join(mountPoint, filepath.Clean(string(filepath.Separator)+newMount.Source))
366} else {
367// we need to create cache on host if no image is being used
368
369// since type is cache and cache can be reused by consecutive builds
370// create a common cache directory, which persists on hosts within temp lifecycle
371// add subdirectory if specified
372
373// cache parent directory: creates separate cache parent for each user.
374cacheParent := CacheParent()
375// create cache on host if not present
376err = os.MkdirAll(cacheParent, os.FileMode(0755))
377if err != nil {
378return newMount, nil, fmt.Errorf("unable to create build cache directory: %w", err)
379}
380
381if id != "" {
382newMount.Source = filepath.Join(cacheParent, filepath.Clean(id))
383buildahLockFilesDir = filepath.Join(BuildahCacheLockfileDir, filepath.Clean(id))
384} else {
385newMount.Source = filepath.Join(cacheParent, filepath.Clean(newMount.Destination))
386buildahLockFilesDir = filepath.Join(BuildahCacheLockfileDir, filepath.Clean(newMount.Destination))
387}
388idPair := idtools.IDPair{
389UID: uid,
390GID: gid,
391}
392// buildkit parity: change uid and gid if specified otheriwise keep `0`
393err = idtools.MkdirAllAndChownNew(newMount.Source, os.FileMode(mode), idPair)
394if err != nil {
395return newMount, nil, fmt.Errorf("unable to change uid,gid of cache directory: %w", err)
396}
397
398// create a subdirectory inside `cacheParent` just to store lockfiles
399buildahLockFilesDir = filepath.Join(cacheParent, buildahLockFilesDir)
400err = os.MkdirAll(buildahLockFilesDir, os.FileMode(0700))
401if err != nil {
402return newMount, nil, fmt.Errorf("unable to create build cache lockfiles directory: %w", err)
403}
404}
405
406var targetLock *lockfile.LockFile // = nil
407succeeded := false
408defer func() {
409if !succeeded && targetLock != nil {
410targetLock.Unlock()
411}
412}()
413switch sharing {
414case "locked":
415// lock parent cache
416lockfile, err := lockfile.GetLockFile(filepath.Join(buildahLockFilesDir, BuildahCacheLockfile))
417if err != nil {
418return newMount, nil, fmt.Errorf("unable to acquire lock when sharing mode is locked: %w", err)
419}
420// Will be unlocked after the RUN step is executed.
421lockfile.Lock()
422targetLock = lockfile
423case "shared":
424// do nothing since default is `shared`
425break
426default:
427// error out for unknown values
428return newMount, nil, fmt.Errorf("unrecognized value %q for field `sharing`: %w", sharing, err)
429}
430
431// buildkit parity: default sharing should be shared
432// unless specified
433if !setShared {
434newMount.Options = append(newMount.Options, "shared")
435}
436
437// buildkit parity: cache must writable unless `ro` or `readonly` is configured explicitly
438if !setReadOnly {
439newMount.Options = append(newMount.Options, "rw")
440}
441
442newMount.Options = append(newMount.Options, "bind")
443
444opts, err := parse.ValidateVolumeOpts(newMount.Options)
445if err != nil {
446return newMount, nil, err
447}
448newMount.Options = opts
449
450succeeded = true
451return newMount, targetLock, nil
452}
453
454func getVolumeMounts(volumes []string) (map[string]specs.Mount, error) {
455finalVolumeMounts := make(map[string]specs.Mount)
456
457for _, volume := range volumes {
458volumeMount, err := internalParse.Volume(volume)
459if err != nil {
460return nil, err
461}
462if _, ok := finalVolumeMounts[volumeMount.Destination]; ok {
463return nil, fmt.Errorf("%v: %w", volumeMount.Destination, errDuplicateDest)
464}
465finalVolumeMounts[volumeMount.Destination] = volumeMount
466}
467return finalVolumeMounts, nil
468}
469
470// UnlockLockArray is a helper for cleaning up after GetVolumes and the like.
471func UnlockLockArray(locks []*lockfile.LockFile) {
472for _, lock := range locks {
473lock.Unlock()
474}
475}
476
477// GetVolumes gets the volumes from --volume and --mount
478//
479// If this function succeeds, the caller must unlock the returned *lockfile.LockFile s if any (when??).
480func GetVolumes(ctx *types.SystemContext, store storage.Store, volumes []string, mounts []string, contextDir string, workDir string) ([]specs.Mount, []string, []*lockfile.LockFile, error) {
481unifiedMounts, mountedImages, targetLocks, err := getMounts(ctx, store, mounts, contextDir, workDir)
482if err != nil {
483return nil, mountedImages, nil, err
484}
485succeeded := false
486defer func() {
487if !succeeded {
488UnlockLockArray(targetLocks)
489}
490}()
491volumeMounts, err := getVolumeMounts(volumes)
492if err != nil {
493return nil, mountedImages, nil, err
494}
495for dest, mount := range volumeMounts {
496if _, ok := unifiedMounts[dest]; ok {
497return nil, mountedImages, nil, fmt.Errorf("%v: %w", dest, errDuplicateDest)
498}
499unifiedMounts[dest] = mount
500}
501
502finalMounts := make([]specs.Mount, 0, len(unifiedMounts))
503for _, mount := range unifiedMounts {
504finalMounts = append(finalMounts, mount)
505}
506succeeded = true
507return finalMounts, mountedImages, targetLocks, nil
508}
509
510// getMounts takes user-provided input from the --mount flag and creates OCI
511// spec mounts.
512// buildah run --mount type=bind,src=/etc/resolv.conf,target=/etc/resolv.conf ...
513// buildah run --mount type=tmpfs,target=/dev/shm ...
514//
515// If this function succeeds, the caller must unlock the returned *lockfile.LockFile s if any (when??).
516func getMounts(ctx *types.SystemContext, store storage.Store, mounts []string, contextDir string, workDir string) (map[string]specs.Mount, []string, []*lockfile.LockFile, error) {
517// If `type` is not set default to "bind"
518mountType := define.TypeBind
519finalMounts := make(map[string]specs.Mount)
520mountedImages := make([]string, 0)
521targetLocks := make([]*lockfile.LockFile, 0)
522succeeded := false
523defer func() {
524if !succeeded {
525UnlockLockArray(targetLocks)
526}
527}()
528
529errInvalidSyntax := errors.New("incorrect mount format: should be --mount type=<bind|tmpfs>,[src=<host-dir>,]target=<ctr-dir>[,options]")
530
531// TODO(vrothberg): the manual parsing can be replaced with a regular expression
532// to allow a more robust parsing of the mount format and to give
533// precise errors regarding supported format versus supported options.
534for _, mount := range mounts {
535tokens := strings.Split(mount, ",")
536if len(tokens) < 2 {
537return nil, mountedImages, nil, fmt.Errorf("%q: %w", mount, errInvalidSyntax)
538}
539for _, field := range tokens {
540if strings.HasPrefix(field, "type=") {
541kv := strings.Split(field, "=")
542if len(kv) != 2 {
543return nil, mountedImages, nil, fmt.Errorf("%q: %w", mount, errInvalidSyntax)
544}
545mountType = kv[1]
546}
547}
548switch mountType {
549case define.TypeBind:
550mount, image, err := GetBindMount(ctx, tokens, contextDir, store, "", nil, workDir)
551if err != nil {
552return nil, mountedImages, nil, err
553}
554if _, ok := finalMounts[mount.Destination]; ok {
555return nil, mountedImages, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest)
556}
557finalMounts[mount.Destination] = mount
558mountedImages = append(mountedImages, image)
559case TypeCache:
560mount, tl, err := GetCacheMount(tokens, store, "", nil, workDir)
561if err != nil {
562return nil, mountedImages, nil, err
563}
564if tl != nil {
565targetLocks = append(targetLocks, tl)
566}
567if _, ok := finalMounts[mount.Destination]; ok {
568return nil, mountedImages, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest)
569}
570finalMounts[mount.Destination] = mount
571case TypeTmpfs:
572mount, err := GetTmpfsMount(tokens)
573if err != nil {
574return nil, mountedImages, nil, err
575}
576if _, ok := finalMounts[mount.Destination]; ok {
577return nil, mountedImages, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest)
578}
579finalMounts[mount.Destination] = mount
580default:
581return nil, mountedImages, nil, fmt.Errorf("invalid filesystem type %q", mountType)
582}
583}
584
585succeeded = true
586return finalMounts, mountedImages, targetLocks, nil
587}
588
589// GetTmpfsMount parses a single tmpfs mount entry from the --mount flag
590func GetTmpfsMount(args []string) (specs.Mount, error) {
591newMount := specs.Mount{
592Type: TypeTmpfs,
593Source: TypeTmpfs,
594}
595
596setDest := false
597
598for _, val := range args {
599argName, argValue, hasArgValue := strings.Cut(val, "=")
600switch argName {
601case "type":
602// This is already processed
603continue
604case "ro", "nosuid", "nodev", "noexec":
605newMount.Options = append(newMount.Options, argName)
606case "readonly":
607// Alias for "ro"
608newMount.Options = append(newMount.Options, "ro")
609case "tmpcopyup":
610// the path that is shadowed by the tmpfs mount is recursively copied up to the tmpfs itself.
611newMount.Options = append(newMount.Options, argName)
612case "tmpfs-mode":
613if !hasArgValue {
614return newMount, fmt.Errorf("%v: %w", argName, errBadOptionArg)
615}
616newMount.Options = append(newMount.Options, fmt.Sprintf("mode=%s", argValue))
617case "tmpfs-size":
618if !hasArgValue {
619return newMount, fmt.Errorf("%v: %w", argName, errBadOptionArg)
620}
621newMount.Options = append(newMount.Options, fmt.Sprintf("size=%s", argValue))
622case "src", "source":
623return newMount, errors.New("source is not supported with tmpfs mounts")
624case "target", "dst", "destination":
625if !hasArgValue {
626return newMount, fmt.Errorf("%v: %w", argName, errBadOptionArg)
627}
628if err := parse.ValidateVolumeCtrDir(argValue); err != nil {
629return newMount, err
630}
631newMount.Destination = argValue
632setDest = true
633default:
634return newMount, fmt.Errorf("%v: %w", argName, errBadMntOption)
635}
636}
637
638if !setDest {
639return newMount, errBadVolDest
640}
641
642return newMount, nil
643}
644