Move most of internal/parse to internal/volumes
internal/parse does not need to depend on libimage. This allows for a smaller podman remote client. Based on Miloslav's work: https://github.com/containers/podman/pull/19718 Signed-off-by: Paul Holzinger <pholzing@redhat.com>
This commit is contained in:
parent
292b429f6c
commit
6e6827b270
|
@ -4,9 +4,9 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
internalParse "github.com/containers/buildah/internal/parse"
|
||||
buildahcli "github.com/containers/buildah/pkg/cli"
|
||||
"github.com/containers/buildah/pkg/parse"
|
||||
"github.com/containers/buildah/pkg/volumes"
|
||||
"github.com/containers/common/libimage"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -63,7 +63,7 @@ func pruneCmd(c *cobra.Command, args []string, iopts pruneOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
err = internalParse.CleanCacheMount()
|
||||
err = volumes.CleanCacheMount()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/containers/buildah"
|
||||
internalParse "github.com/containers/buildah/internal/parse"
|
||||
"github.com/containers/buildah/internal/volumes"
|
||||
buildahcli "github.com/containers/buildah/pkg/cli"
|
||||
"github.com/containers/buildah/pkg/parse"
|
||||
"github.com/containers/buildah/util"
|
||||
|
@ -169,11 +169,11 @@ func runCmd(c *cobra.Command, args []string, iopts runInputOptions) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("building system context: %w", err)
|
||||
}
|
||||
mounts, mountedImages, targetLocks, err := internalParse.GetVolumes(systemContext, store, iopts.volumes, iopts.mounts, iopts.contextDir, iopts.workingDir)
|
||||
mounts, mountedImages, targetLocks, err := volumes.GetVolumes(systemContext, store, iopts.volumes, iopts.mounts, iopts.contextDir, iopts.workingDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer internalParse.UnlockLockArray(targetLocks)
|
||||
defer volumes.UnlockLockArray(targetLocks)
|
||||
options.Mounts = mounts
|
||||
// Run() will automatically clean them up.
|
||||
options.ExternalImageMounts = mountedImages
|
||||
|
|
|
@ -1,449 +1,15 @@
|
|||
package parse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/containers/buildah/define"
|
||||
"github.com/containers/buildah/internal"
|
||||
internalUtil "github.com/containers/buildah/internal/util"
|
||||
"github.com/containers/common/pkg/parse"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/containers/storage"
|
||||
"github.com/containers/storage/pkg/idtools"
|
||||
"github.com/containers/storage/pkg/lockfile"
|
||||
"github.com/containers/storage/pkg/unshare"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
selinux "github.com/opencontainers/selinux/go-selinux"
|
||||
)
|
||||
|
||||
const (
|
||||
// TypeTmpfs is the type for mounting tmpfs
|
||||
TypeTmpfs = "tmpfs"
|
||||
// TypeCache is the type for mounting a common persistent cache from host
|
||||
TypeCache = "cache"
|
||||
// mount=type=cache must create a persistent directory on host so its available for all consecutive builds.
|
||||
// Lifecycle of following directory will be inherited from how host machine treats temporary directory
|
||||
BuildahCacheDir = "buildah-cache"
|
||||
// mount=type=cache allows users to lock a cache store while its being used by another build
|
||||
BuildahCacheLockfile = "buildah-cache-lockfile"
|
||||
// All the lockfiles are stored in a separate directory inside `BuildahCacheDir`
|
||||
// Example `/var/tmp/buildah-cache/<target>/buildah-cache-lockfile`
|
||||
BuildahCacheLockfileDir = "buildah-cache-lockfiles"
|
||||
)
|
||||
|
||||
var (
|
||||
errBadMntOption = errors.New("invalid mount option")
|
||||
errBadOptionArg = errors.New("must provide an argument for option")
|
||||
errBadVolDest = errors.New("must set volume destination")
|
||||
errBadVolSrc = errors.New("must set volume source")
|
||||
errDuplicateDest = errors.New("duplicate mount destination")
|
||||
)
|
||||
|
||||
// GetBindMount parses a single bind mount entry from the --mount flag.
|
||||
// Returns specifiedMount and a string which contains name of image that we mounted otherwise its empty.
|
||||
// Caller is expected to perform unmount of any mounted images
|
||||
func GetBindMount(ctx *types.SystemContext, args []string, contextDir string, store storage.Store, imageMountLabel string, additionalMountPoints map[string]internal.StageMountDetails, workDir string) (specs.Mount, string, error) {
|
||||
newMount := specs.Mount{
|
||||
Type: define.TypeBind,
|
||||
}
|
||||
|
||||
setRelabel := false
|
||||
mountReadability := false
|
||||
setDest := false
|
||||
bindNonRecursive := false
|
||||
fromImage := ""
|
||||
|
||||
for _, val := range args {
|
||||
kv := strings.SplitN(val, "=", 2)
|
||||
switch kv[0] {
|
||||
case "type":
|
||||
// This is already processed
|
||||
continue
|
||||
case "bind-nonrecursive":
|
||||
newMount.Options = append(newMount.Options, "bind")
|
||||
bindNonRecursive = true
|
||||
case "ro", "nosuid", "nodev", "noexec":
|
||||
// TODO: detect duplication of these options.
|
||||
// (Is this necessary?)
|
||||
newMount.Options = append(newMount.Options, kv[0])
|
||||
mountReadability = true
|
||||
case "rw", "readwrite":
|
||||
newMount.Options = append(newMount.Options, "rw")
|
||||
mountReadability = true
|
||||
case "readonly":
|
||||
// Alias for "ro"
|
||||
newMount.Options = append(newMount.Options, "ro")
|
||||
mountReadability = true
|
||||
case "shared", "rshared", "private", "rprivate", "slave", "rslave", "Z", "z", "U":
|
||||
newMount.Options = append(newMount.Options, kv[0])
|
||||
case "from":
|
||||
if len(kv) == 1 {
|
||||
return newMount, "", fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
fromImage = kv[1]
|
||||
case "bind-propagation":
|
||||
if len(kv) == 1 {
|
||||
return newMount, "", fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
newMount.Options = append(newMount.Options, kv[1])
|
||||
case "src", "source":
|
||||
if len(kv) == 1 {
|
||||
return newMount, "", fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
newMount.Source = kv[1]
|
||||
case "target", "dst", "destination":
|
||||
if len(kv) == 1 {
|
||||
return newMount, "", fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
targetPath := kv[1]
|
||||
if !path.IsAbs(targetPath) {
|
||||
targetPath = filepath.Join(workDir, targetPath)
|
||||
}
|
||||
if err := parse.ValidateVolumeCtrDir(targetPath); err != nil {
|
||||
return newMount, "", err
|
||||
}
|
||||
newMount.Destination = targetPath
|
||||
setDest = true
|
||||
case "relabel":
|
||||
if setRelabel {
|
||||
return newMount, "", fmt.Errorf("cannot pass 'relabel' option more than once: %w", errBadOptionArg)
|
||||
}
|
||||
setRelabel = true
|
||||
if len(kv) != 2 {
|
||||
return newMount, "", fmt.Errorf("%s mount option must be 'private' or 'shared': %w", kv[0], errBadMntOption)
|
||||
}
|
||||
switch kv[1] {
|
||||
case "private":
|
||||
newMount.Options = append(newMount.Options, "Z")
|
||||
case "shared":
|
||||
newMount.Options = append(newMount.Options, "z")
|
||||
default:
|
||||
return newMount, "", fmt.Errorf("%s mount option must be 'private' or 'shared': %w", kv[0], errBadMntOption)
|
||||
}
|
||||
case "consistency":
|
||||
// Option for OS X only, has no meaning on other platforms
|
||||
// and can thus be safely ignored.
|
||||
// See also the handling of the equivalent "delegated" and "cached" in ValidateVolumeOpts
|
||||
default:
|
||||
return newMount, "", fmt.Errorf("%v: %w", kv[0], errBadMntOption)
|
||||
}
|
||||
}
|
||||
|
||||
// default mount readability is always readonly
|
||||
if !mountReadability {
|
||||
newMount.Options = append(newMount.Options, "ro")
|
||||
}
|
||||
|
||||
// Following variable ensures that we return imagename only if we did additional mount
|
||||
isImageMounted := false
|
||||
if fromImage != "" {
|
||||
mountPoint := ""
|
||||
if additionalMountPoints != nil {
|
||||
if val, ok := additionalMountPoints[fromImage]; ok {
|
||||
mountPoint = val.MountPoint
|
||||
}
|
||||
}
|
||||
// if mountPoint of image was not found in additionalMap
|
||||
// or additionalMap was nil, try mounting image
|
||||
if mountPoint == "" {
|
||||
image, err := internalUtil.LookupImage(ctx, store, fromImage)
|
||||
if err != nil {
|
||||
return newMount, "", err
|
||||
}
|
||||
|
||||
mountPoint, err = image.Mount(context.Background(), nil, imageMountLabel)
|
||||
if err != nil {
|
||||
return newMount, "", err
|
||||
}
|
||||
isImageMounted = true
|
||||
}
|
||||
contextDir = mountPoint
|
||||
}
|
||||
|
||||
// buildkit parity: default bind option must be `rbind`
|
||||
// unless specified
|
||||
if !bindNonRecursive {
|
||||
newMount.Options = append(newMount.Options, "rbind")
|
||||
}
|
||||
|
||||
if !setDest {
|
||||
return newMount, fromImage, errBadVolDest
|
||||
}
|
||||
|
||||
// buildkit parity: support absolute path for sources from current build context
|
||||
if contextDir != "" {
|
||||
// path should be /contextDir/specified path
|
||||
newMount.Source = filepath.Join(contextDir, filepath.Clean(string(filepath.Separator)+newMount.Source))
|
||||
} else {
|
||||
// looks like its coming from `build run --mount=type=bind` allow using absolute path
|
||||
// error out if no source is set
|
||||
if newMount.Source == "" {
|
||||
return newMount, "", errBadVolSrc
|
||||
}
|
||||
if err := parse.ValidateVolumeHostDir(newMount.Source); err != nil {
|
||||
return newMount, "", err
|
||||
}
|
||||
}
|
||||
|
||||
opts, err := parse.ValidateVolumeOpts(newMount.Options)
|
||||
if err != nil {
|
||||
return newMount, fromImage, err
|
||||
}
|
||||
newMount.Options = opts
|
||||
|
||||
if !isImageMounted {
|
||||
// we don't want any cleanups if image was not mounted explicitly
|
||||
// so dont return anything
|
||||
fromImage = ""
|
||||
}
|
||||
|
||||
return newMount, fromImage, nil
|
||||
}
|
||||
|
||||
// CleanCacheMount gets the cache parent created by `--mount=type=cache` and removes it.
|
||||
func CleanCacheMount() error {
|
||||
cacheParent := filepath.Join(internalUtil.GetTempDir(), BuildahCacheDir+"-"+strconv.Itoa(unshare.GetRootlessUID()))
|
||||
return os.RemoveAll(cacheParent)
|
||||
}
|
||||
|
||||
// GetCacheMount parses a single cache mount entry from the --mount flag.
|
||||
//
|
||||
// If this function succeeds and returns a non-nil *lockfile.LockFile, the caller must unlock it (when??).
|
||||
func GetCacheMount(args []string, store storage.Store, imageMountLabel string, additionalMountPoints map[string]internal.StageMountDetails, workDir string) (specs.Mount, *lockfile.LockFile, error) {
|
||||
var err error
|
||||
var mode uint64
|
||||
var buildahLockFilesDir string
|
||||
var (
|
||||
setDest bool
|
||||
setShared bool
|
||||
setReadOnly bool
|
||||
foundSElinuxLabel bool
|
||||
)
|
||||
fromStage := ""
|
||||
newMount := specs.Mount{
|
||||
Type: define.TypeBind,
|
||||
}
|
||||
// if id is set a new subdirectory with `id` will be created under /host-temp/buildah-build-cache/id
|
||||
id := ""
|
||||
//buidkit parity: cache directory defaults to 755
|
||||
mode = 0o755
|
||||
//buidkit parity: cache directory defaults to uid 0 if not specified
|
||||
uid := 0
|
||||
//buidkit parity: cache directory defaults to gid 0 if not specified
|
||||
gid := 0
|
||||
// sharing mode
|
||||
sharing := "shared"
|
||||
|
||||
for _, val := range args {
|
||||
kv := strings.SplitN(val, "=", 2)
|
||||
switch kv[0] {
|
||||
case "type":
|
||||
// This is already processed
|
||||
continue
|
||||
case "nosuid", "nodev", "noexec":
|
||||
// TODO: detect duplication of these options.
|
||||
// (Is this necessary?)
|
||||
newMount.Options = append(newMount.Options, kv[0])
|
||||
case "rw", "readwrite":
|
||||
newMount.Options = append(newMount.Options, "rw")
|
||||
case "readonly", "ro":
|
||||
// Alias for "ro"
|
||||
newMount.Options = append(newMount.Options, "ro")
|
||||
setReadOnly = true
|
||||
case "Z", "z":
|
||||
newMount.Options = append(newMount.Options, kv[0])
|
||||
foundSElinuxLabel = true
|
||||
case "shared", "rshared", "private", "rprivate", "slave", "rslave", "U":
|
||||
newMount.Options = append(newMount.Options, kv[0])
|
||||
setShared = true
|
||||
case "sharing":
|
||||
sharing = kv[1]
|
||||
case "bind-propagation":
|
||||
if len(kv) == 1 {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
newMount.Options = append(newMount.Options, kv[1])
|
||||
case "id":
|
||||
if len(kv) == 1 {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
id = kv[1]
|
||||
case "from":
|
||||
if len(kv) == 1 {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
fromStage = kv[1]
|
||||
case "target", "dst", "destination":
|
||||
if len(kv) == 1 {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
targetPath := kv[1]
|
||||
if !path.IsAbs(targetPath) {
|
||||
targetPath = filepath.Join(workDir, targetPath)
|
||||
}
|
||||
if err := parse.ValidateVolumeCtrDir(targetPath); err != nil {
|
||||
return newMount, nil, err
|
||||
}
|
||||
newMount.Destination = targetPath
|
||||
setDest = true
|
||||
case "src", "source":
|
||||
if len(kv) == 1 {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
newMount.Source = kv[1]
|
||||
case "mode":
|
||||
if len(kv) == 1 {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
mode, err = strconv.ParseUint(kv[1], 8, 32)
|
||||
if err != nil {
|
||||
return newMount, nil, fmt.Errorf("unable to parse cache mode: %w", err)
|
||||
}
|
||||
case "uid":
|
||||
if len(kv) == 1 {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
uid, err = strconv.Atoi(kv[1])
|
||||
if err != nil {
|
||||
return newMount, nil, fmt.Errorf("unable to parse cache uid: %w", err)
|
||||
}
|
||||
case "gid":
|
||||
if len(kv) == 1 {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
gid, err = strconv.Atoi(kv[1])
|
||||
if err != nil {
|
||||
return newMount, nil, fmt.Errorf("unable to parse cache gid: %w", err)
|
||||
}
|
||||
default:
|
||||
return newMount, nil, fmt.Errorf("%v: %w", kv[0], errBadMntOption)
|
||||
}
|
||||
}
|
||||
|
||||
// If selinux is enabled and no selinux option was configured
|
||||
// default to `z` i.e shared content label.
|
||||
if !foundSElinuxLabel && (selinux.EnforceMode() != selinux.Disabled) && fromStage == "" {
|
||||
newMount.Options = append(newMount.Options, "z")
|
||||
}
|
||||
|
||||
if !setDest {
|
||||
return newMount, nil, errBadVolDest
|
||||
}
|
||||
|
||||
if fromStage != "" {
|
||||
// do not create cache on host
|
||||
// instead use read-only mounted stage as cache
|
||||
mountPoint := ""
|
||||
if additionalMountPoints != nil {
|
||||
if val, ok := additionalMountPoints[fromStage]; ok {
|
||||
if val.IsStage {
|
||||
mountPoint = val.MountPoint
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cache does not supports using image so if not stage found
|
||||
// return with error
|
||||
if mountPoint == "" {
|
||||
return newMount, nil, fmt.Errorf("no stage found with name %s", fromStage)
|
||||
}
|
||||
// path should be /contextDir/specified path
|
||||
newMount.Source = filepath.Join(mountPoint, filepath.Clean(string(filepath.Separator)+newMount.Source))
|
||||
} else {
|
||||
// we need to create cache on host if no image is being used
|
||||
|
||||
// since type is cache and cache can be reused by consecutive builds
|
||||
// create a common cache directory, which persists on hosts within temp lifecycle
|
||||
// add subdirectory if specified
|
||||
|
||||
// cache parent directory: creates separate cache parent for each user.
|
||||
cacheParent := filepath.Join(internalUtil.GetTempDir(), BuildahCacheDir+"-"+strconv.Itoa(unshare.GetRootlessUID()))
|
||||
// create cache on host if not present
|
||||
err = os.MkdirAll(cacheParent, os.FileMode(0755))
|
||||
if err != nil {
|
||||
return newMount, nil, fmt.Errorf("unable to create build cache directory: %w", err)
|
||||
}
|
||||
|
||||
if id != "" {
|
||||
newMount.Source = filepath.Join(cacheParent, filepath.Clean(id))
|
||||
buildahLockFilesDir = filepath.Join(BuildahCacheLockfileDir, filepath.Clean(id))
|
||||
} else {
|
||||
newMount.Source = filepath.Join(cacheParent, filepath.Clean(newMount.Destination))
|
||||
buildahLockFilesDir = filepath.Join(BuildahCacheLockfileDir, filepath.Clean(newMount.Destination))
|
||||
}
|
||||
idPair := idtools.IDPair{
|
||||
UID: uid,
|
||||
GID: gid,
|
||||
}
|
||||
//buildkit parity: change uid and gid if specified otheriwise keep `0`
|
||||
err = idtools.MkdirAllAndChownNew(newMount.Source, os.FileMode(mode), idPair)
|
||||
if err != nil {
|
||||
return newMount, nil, fmt.Errorf("unable to change uid,gid of cache directory: %w", err)
|
||||
}
|
||||
|
||||
// create a subdirectory inside `cacheParent` just to store lockfiles
|
||||
buildahLockFilesDir = filepath.Join(cacheParent, buildahLockFilesDir)
|
||||
err = os.MkdirAll(buildahLockFilesDir, os.FileMode(0700))
|
||||
if err != nil {
|
||||
return newMount, nil, fmt.Errorf("unable to create build cache lockfiles directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var targetLock *lockfile.LockFile // = nil
|
||||
succeeded := false
|
||||
defer func() {
|
||||
if !succeeded && targetLock != nil {
|
||||
targetLock.Unlock()
|
||||
}
|
||||
}()
|
||||
switch sharing {
|
||||
case "locked":
|
||||
// lock parent cache
|
||||
lockfile, err := lockfile.GetLockFile(filepath.Join(buildahLockFilesDir, BuildahCacheLockfile))
|
||||
if err != nil {
|
||||
return newMount, nil, fmt.Errorf("unable to acquire lock when sharing mode is locked: %w", err)
|
||||
}
|
||||
// Will be unlocked after the RUN step is executed.
|
||||
lockfile.Lock()
|
||||
targetLock = lockfile
|
||||
case "shared":
|
||||
// do nothing since default is `shared`
|
||||
break
|
||||
default:
|
||||
// error out for unknown values
|
||||
return newMount, nil, fmt.Errorf("unrecognized value %q for field `sharing`: %w", sharing, err)
|
||||
}
|
||||
|
||||
// buildkit parity: default sharing should be shared
|
||||
// unless specified
|
||||
if !setShared {
|
||||
newMount.Options = append(newMount.Options, "shared")
|
||||
}
|
||||
|
||||
// buildkit parity: cache must writable unless `ro` or `readonly` is configured explicitly
|
||||
if !setReadOnly {
|
||||
newMount.Options = append(newMount.Options, "rw")
|
||||
}
|
||||
|
||||
newMount.Options = append(newMount.Options, "bind")
|
||||
|
||||
opts, err := parse.ValidateVolumeOpts(newMount.Options)
|
||||
if err != nil {
|
||||
return newMount, nil, err
|
||||
}
|
||||
newMount.Options = opts
|
||||
|
||||
succeeded = true
|
||||
return newMount, targetLock, nil
|
||||
}
|
||||
|
||||
// ValidateVolumeMountHostDir validates the host path of buildah --volume
|
||||
func ValidateVolumeMountHostDir(hostDir string) error {
|
||||
if !filepath.IsAbs(hostDir) {
|
||||
|
@ -484,22 +50,6 @@ func SplitStringWithColonEscape(str string) []string {
|
|||
return result
|
||||
}
|
||||
|
||||
func getVolumeMounts(volumes []string) (map[string]specs.Mount, error) {
|
||||
finalVolumeMounts := make(map[string]specs.Mount)
|
||||
|
||||
for _, volume := range volumes {
|
||||
volumeMount, err := Volume(volume)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := finalVolumeMounts[volumeMount.Destination]; ok {
|
||||
return nil, fmt.Errorf("%v: %w", volumeMount.Destination, errDuplicateDest)
|
||||
}
|
||||
finalVolumeMounts[volumeMount.Destination] = volumeMount
|
||||
}
|
||||
return finalVolumeMounts, nil
|
||||
}
|
||||
|
||||
// Volume parses the input of --volume
|
||||
func Volume(volume string) (specs.Mount, error) {
|
||||
mount := specs.Mount{}
|
||||
|
@ -527,178 +77,3 @@ func Volume(volume string) (specs.Mount, error) {
|
|||
mount.Options = mountOpts
|
||||
return mount, nil
|
||||
}
|
||||
|
||||
// UnlockLockArray is a helper for cleaning up after GetVolumes and the like.
|
||||
func UnlockLockArray(locks []*lockfile.LockFile) {
|
||||
for _, lock := range locks {
|
||||
lock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// GetVolumes gets the volumes from --volume and --mount
|
||||
//
|
||||
// If this function succeeds, the caller must unlock the returned *lockfile.LockFile s if any (when??).
|
||||
func GetVolumes(ctx *types.SystemContext, store storage.Store, volumes []string, mounts []string, contextDir string, workDir string) ([]specs.Mount, []string, []*lockfile.LockFile, error) {
|
||||
unifiedMounts, mountedImages, targetLocks, err := getMounts(ctx, store, mounts, contextDir, workDir)
|
||||
if err != nil {
|
||||
return nil, mountedImages, nil, err
|
||||
}
|
||||
succeeded := false
|
||||
defer func() {
|
||||
if !succeeded {
|
||||
UnlockLockArray(targetLocks)
|
||||
}
|
||||
}()
|
||||
volumeMounts, err := getVolumeMounts(volumes)
|
||||
if err != nil {
|
||||
return nil, mountedImages, nil, err
|
||||
}
|
||||
for dest, mount := range volumeMounts {
|
||||
if _, ok := unifiedMounts[dest]; ok {
|
||||
return nil, mountedImages, nil, fmt.Errorf("%v: %w", dest, errDuplicateDest)
|
||||
}
|
||||
unifiedMounts[dest] = mount
|
||||
}
|
||||
|
||||
finalMounts := make([]specs.Mount, 0, len(unifiedMounts))
|
||||
for _, mount := range unifiedMounts {
|
||||
finalMounts = append(finalMounts, mount)
|
||||
}
|
||||
succeeded = true
|
||||
return finalMounts, mountedImages, targetLocks, nil
|
||||
}
|
||||
|
||||
// getMounts takes user-provided input from the --mount flag and creates OCI
|
||||
// spec mounts.
|
||||
// buildah run --mount type=bind,src=/etc/resolv.conf,target=/etc/resolv.conf ...
|
||||
// buildah run --mount type=tmpfs,target=/dev/shm ...
|
||||
//
|
||||
// If this function succeeds, the caller must unlock the returned *lockfile.LockFile s if any (when??).
|
||||
func getMounts(ctx *types.SystemContext, store storage.Store, mounts []string, contextDir string, workDir string) (map[string]specs.Mount, []string, []*lockfile.LockFile, error) {
|
||||
// If `type` is not set default to "bind"
|
||||
mountType := define.TypeBind
|
||||
finalMounts := make(map[string]specs.Mount)
|
||||
mountedImages := make([]string, 0)
|
||||
targetLocks := make([]*lockfile.LockFile, 0)
|
||||
succeeded := false
|
||||
defer func() {
|
||||
if !succeeded {
|
||||
UnlockLockArray(targetLocks)
|
||||
}
|
||||
}()
|
||||
|
||||
errInvalidSyntax := errors.New("incorrect mount format: should be --mount type=<bind|tmpfs>,[src=<host-dir>,]target=<ctr-dir>[,options]")
|
||||
|
||||
// TODO(vrothberg): the manual parsing can be replaced with a regular expression
|
||||
// to allow a more robust parsing of the mount format and to give
|
||||
// precise errors regarding supported format versus supported options.
|
||||
for _, mount := range mounts {
|
||||
tokens := strings.Split(mount, ",")
|
||||
if len(tokens) < 2 {
|
||||
return nil, mountedImages, nil, fmt.Errorf("%q: %w", mount, errInvalidSyntax)
|
||||
}
|
||||
for _, field := range tokens {
|
||||
if strings.HasPrefix(field, "type=") {
|
||||
kv := strings.Split(field, "=")
|
||||
if len(kv) != 2 {
|
||||
return nil, mountedImages, nil, fmt.Errorf("%q: %w", mount, errInvalidSyntax)
|
||||
}
|
||||
mountType = kv[1]
|
||||
}
|
||||
}
|
||||
switch mountType {
|
||||
case define.TypeBind:
|
||||
mount, image, err := GetBindMount(ctx, tokens, contextDir, store, "", nil, workDir)
|
||||
if err != nil {
|
||||
return nil, mountedImages, nil, err
|
||||
}
|
||||
if _, ok := finalMounts[mount.Destination]; ok {
|
||||
return nil, mountedImages, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest)
|
||||
}
|
||||
finalMounts[mount.Destination] = mount
|
||||
mountedImages = append(mountedImages, image)
|
||||
case TypeCache:
|
||||
mount, tl, err := GetCacheMount(tokens, store, "", nil, workDir)
|
||||
if err != nil {
|
||||
return nil, mountedImages, nil, err
|
||||
}
|
||||
if tl != nil {
|
||||
targetLocks = append(targetLocks, tl)
|
||||
}
|
||||
if _, ok := finalMounts[mount.Destination]; ok {
|
||||
return nil, mountedImages, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest)
|
||||
}
|
||||
finalMounts[mount.Destination] = mount
|
||||
case TypeTmpfs:
|
||||
mount, err := GetTmpfsMount(tokens)
|
||||
if err != nil {
|
||||
return nil, mountedImages, nil, err
|
||||
}
|
||||
if _, ok := finalMounts[mount.Destination]; ok {
|
||||
return nil, mountedImages, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest)
|
||||
}
|
||||
finalMounts[mount.Destination] = mount
|
||||
default:
|
||||
return nil, mountedImages, nil, fmt.Errorf("invalid filesystem type %q", mountType)
|
||||
}
|
||||
}
|
||||
|
||||
succeeded = true
|
||||
return finalMounts, mountedImages, targetLocks, nil
|
||||
}
|
||||
|
||||
// GetTmpfsMount parses a single tmpfs mount entry from the --mount flag
|
||||
func GetTmpfsMount(args []string) (specs.Mount, error) {
|
||||
newMount := specs.Mount{
|
||||
Type: TypeTmpfs,
|
||||
Source: TypeTmpfs,
|
||||
}
|
||||
|
||||
setDest := false
|
||||
|
||||
for _, val := range args {
|
||||
kv := strings.SplitN(val, "=", 2)
|
||||
switch kv[0] {
|
||||
case "type":
|
||||
// This is already processed
|
||||
continue
|
||||
case "ro", "nosuid", "nodev", "noexec":
|
||||
newMount.Options = append(newMount.Options, kv[0])
|
||||
case "readonly":
|
||||
// Alias for "ro"
|
||||
newMount.Options = append(newMount.Options, "ro")
|
||||
case "tmpcopyup":
|
||||
//the path that is shadowed by the tmpfs mount is recursively copied up to the tmpfs itself.
|
||||
newMount.Options = append(newMount.Options, kv[0])
|
||||
case "tmpfs-mode":
|
||||
if len(kv) == 1 {
|
||||
return newMount, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
newMount.Options = append(newMount.Options, fmt.Sprintf("mode=%s", kv[1]))
|
||||
case "tmpfs-size":
|
||||
if len(kv) == 1 {
|
||||
return newMount, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
newMount.Options = append(newMount.Options, fmt.Sprintf("size=%s", kv[1]))
|
||||
case "src", "source":
|
||||
return newMount, errors.New("source is not supported with tmpfs mounts")
|
||||
case "target", "dst", "destination":
|
||||
if len(kv) == 1 {
|
||||
return newMount, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil {
|
||||
return newMount, err
|
||||
}
|
||||
newMount.Destination = kv[1]
|
||||
setDest = true
|
||||
default:
|
||||
return newMount, fmt.Errorf("%v: %w", kv[0], errBadMntOption)
|
||||
}
|
||||
}
|
||||
|
||||
if !setDest {
|
||||
return newMount, errBadVolDest
|
||||
}
|
||||
|
||||
return newMount, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,636 @@
|
|||
package volumes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/containers/buildah/define"
|
||||
"github.com/containers/buildah/internal"
|
||||
internalParse "github.com/containers/buildah/internal/parse"
|
||||
internalUtil "github.com/containers/buildah/internal/util"
|
||||
"github.com/containers/common/pkg/parse"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/containers/storage"
|
||||
"github.com/containers/storage/pkg/idtools"
|
||||
"github.com/containers/storage/pkg/lockfile"
|
||||
"github.com/containers/storage/pkg/unshare"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
selinux "github.com/opencontainers/selinux/go-selinux"
|
||||
)
|
||||
|
||||
const (
|
||||
// TypeTmpfs is the type for mounting tmpfs
|
||||
TypeTmpfs = "tmpfs"
|
||||
// TypeCache is the type for mounting a common persistent cache from host
|
||||
TypeCache = "cache"
|
||||
// mount=type=cache must create a persistent directory on host so its available for all consecutive builds.
|
||||
// Lifecycle of following directory will be inherited from how host machine treats temporary directory
|
||||
buildahCacheDir = "buildah-cache"
|
||||
// mount=type=cache allows users to lock a cache store while its being used by another build
|
||||
BuildahCacheLockfile = "buildah-cache-lockfile"
|
||||
// All the lockfiles are stored in a separate directory inside `BuildahCacheDir`
|
||||
// Example `/var/tmp/buildah-cache/<target>/buildah-cache-lockfile`
|
||||
BuildahCacheLockfileDir = "buildah-cache-lockfiles"
|
||||
)
|
||||
|
||||
var (
|
||||
errBadMntOption = errors.New("invalid mount option")
|
||||
errBadOptionArg = errors.New("must provide an argument for option")
|
||||
errBadVolDest = errors.New("must set volume destination")
|
||||
errBadVolSrc = errors.New("must set volume source")
|
||||
errDuplicateDest = errors.New("duplicate mount destination")
|
||||
)
|
||||
|
||||
// CacheParent returns a cache parent for --mount=type=cache
|
||||
func CacheParent() string {
|
||||
return filepath.Join(internalUtil.GetTempDir(), buildahCacheDir+"-"+strconv.Itoa(unshare.GetRootlessUID()))
|
||||
}
|
||||
|
||||
// GetBindMount parses a single bind mount entry from the --mount flag.
|
||||
// Returns specifiedMount and a string which contains name of image that we mounted otherwise its empty.
|
||||
// Caller is expected to perform unmount of any mounted images
|
||||
func GetBindMount(ctx *types.SystemContext, args []string, contextDir string, store storage.Store, imageMountLabel string, additionalMountPoints map[string]internal.StageMountDetails, workDir string) (specs.Mount, string, error) {
|
||||
newMount := specs.Mount{
|
||||
Type: define.TypeBind,
|
||||
}
|
||||
|
||||
setRelabel := false
|
||||
mountReadability := false
|
||||
setDest := false
|
||||
bindNonRecursive := false
|
||||
fromImage := ""
|
||||
|
||||
for _, val := range args {
|
||||
kv := strings.SplitN(val, "=", 2)
|
||||
switch kv[0] {
|
||||
case "type":
|
||||
// This is already processed
|
||||
continue
|
||||
case "bind-nonrecursive":
|
||||
newMount.Options = append(newMount.Options, "bind")
|
||||
bindNonRecursive = true
|
||||
case "ro", "nosuid", "nodev", "noexec":
|
||||
// TODO: detect duplication of these options.
|
||||
// (Is this necessary?)
|
||||
newMount.Options = append(newMount.Options, kv[0])
|
||||
mountReadability = true
|
||||
case "rw", "readwrite":
|
||||
newMount.Options = append(newMount.Options, "rw")
|
||||
mountReadability = true
|
||||
case "readonly":
|
||||
// Alias for "ro"
|
||||
newMount.Options = append(newMount.Options, "ro")
|
||||
mountReadability = true
|
||||
case "shared", "rshared", "private", "rprivate", "slave", "rslave", "Z", "z", "U":
|
||||
newMount.Options = append(newMount.Options, kv[0])
|
||||
case "from":
|
||||
if len(kv) == 1 {
|
||||
return newMount, "", fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
fromImage = kv[1]
|
||||
case "bind-propagation":
|
||||
if len(kv) == 1 {
|
||||
return newMount, "", fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
newMount.Options = append(newMount.Options, kv[1])
|
||||
case "src", "source":
|
||||
if len(kv) == 1 {
|
||||
return newMount, "", fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
newMount.Source = kv[1]
|
||||
case "target", "dst", "destination":
|
||||
if len(kv) == 1 {
|
||||
return newMount, "", fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
targetPath := kv[1]
|
||||
if !path.IsAbs(targetPath) {
|
||||
targetPath = filepath.Join(workDir, targetPath)
|
||||
}
|
||||
if err := parse.ValidateVolumeCtrDir(targetPath); err != nil {
|
||||
return newMount, "", err
|
||||
}
|
||||
newMount.Destination = targetPath
|
||||
setDest = true
|
||||
case "relabel":
|
||||
if setRelabel {
|
||||
return newMount, "", fmt.Errorf("cannot pass 'relabel' option more than once: %w", errBadOptionArg)
|
||||
}
|
||||
setRelabel = true
|
||||
if len(kv) != 2 {
|
||||
return newMount, "", fmt.Errorf("%s mount option must be 'private' or 'shared': %w", kv[0], errBadMntOption)
|
||||
}
|
||||
switch kv[1] {
|
||||
case "private":
|
||||
newMount.Options = append(newMount.Options, "Z")
|
||||
case "shared":
|
||||
newMount.Options = append(newMount.Options, "z")
|
||||
default:
|
||||
return newMount, "", fmt.Errorf("%s mount option must be 'private' or 'shared': %w", kv[0], errBadMntOption)
|
||||
}
|
||||
case "consistency":
|
||||
// Option for OS X only, has no meaning on other platforms
|
||||
// and can thus be safely ignored.
|
||||
// See also the handling of the equivalent "delegated" and "cached" in ValidateVolumeOpts
|
||||
default:
|
||||
return newMount, "", fmt.Errorf("%v: %w", kv[0], errBadMntOption)
|
||||
}
|
||||
}
|
||||
|
||||
// default mount readability is always readonly
|
||||
if !mountReadability {
|
||||
newMount.Options = append(newMount.Options, "ro")
|
||||
}
|
||||
|
||||
// Following variable ensures that we return imagename only if we did additional mount
|
||||
isImageMounted := false
|
||||
if fromImage != "" {
|
||||
mountPoint := ""
|
||||
if additionalMountPoints != nil {
|
||||
if val, ok := additionalMountPoints[fromImage]; ok {
|
||||
mountPoint = val.MountPoint
|
||||
}
|
||||
}
|
||||
// if mountPoint of image was not found in additionalMap
|
||||
// or additionalMap was nil, try mounting image
|
||||
if mountPoint == "" {
|
||||
image, err := internalUtil.LookupImage(ctx, store, fromImage)
|
||||
if err != nil {
|
||||
return newMount, "", err
|
||||
}
|
||||
|
||||
mountPoint, err = image.Mount(context.Background(), nil, imageMountLabel)
|
||||
if err != nil {
|
||||
return newMount, "", err
|
||||
}
|
||||
isImageMounted = true
|
||||
}
|
||||
contextDir = mountPoint
|
||||
}
|
||||
|
||||
// buildkit parity: default bind option must be `rbind`
|
||||
// unless specified
|
||||
if !bindNonRecursive {
|
||||
newMount.Options = append(newMount.Options, "rbind")
|
||||
}
|
||||
|
||||
if !setDest {
|
||||
return newMount, fromImage, errBadVolDest
|
||||
}
|
||||
|
||||
// buildkit parity: support absolute path for sources from current build context
|
||||
if contextDir != "" {
|
||||
// path should be /contextDir/specified path
|
||||
newMount.Source = filepath.Join(contextDir, filepath.Clean(string(filepath.Separator)+newMount.Source))
|
||||
} else {
|
||||
// looks like its coming from `build run --mount=type=bind` allow using absolute path
|
||||
// error out if no source is set
|
||||
if newMount.Source == "" {
|
||||
return newMount, "", errBadVolSrc
|
||||
}
|
||||
if err := parse.ValidateVolumeHostDir(newMount.Source); err != nil {
|
||||
return newMount, "", err
|
||||
}
|
||||
}
|
||||
|
||||
opts, err := parse.ValidateVolumeOpts(newMount.Options)
|
||||
if err != nil {
|
||||
return newMount, fromImage, err
|
||||
}
|
||||
newMount.Options = opts
|
||||
|
||||
if !isImageMounted {
|
||||
// we don't want any cleanups if image was not mounted explicitly
|
||||
// so dont return anything
|
||||
fromImage = ""
|
||||
}
|
||||
|
||||
return newMount, fromImage, nil
|
||||
}
|
||||
|
||||
// GetCacheMount parses a single cache mount entry from the --mount flag.
|
||||
//
|
||||
// If this function succeeds and returns a non-nil *lockfile.LockFile, the caller must unlock it (when??).
|
||||
func GetCacheMount(args []string, store storage.Store, imageMountLabel string, additionalMountPoints map[string]internal.StageMountDetails, workDir string) (specs.Mount, *lockfile.LockFile, error) {
|
||||
var err error
|
||||
var mode uint64
|
||||
var buildahLockFilesDir string
|
||||
var (
|
||||
setDest bool
|
||||
setShared bool
|
||||
setReadOnly bool
|
||||
foundSElinuxLabel bool
|
||||
)
|
||||
fromStage := ""
|
||||
newMount := specs.Mount{
|
||||
Type: define.TypeBind,
|
||||
}
|
||||
// if id is set a new subdirectory with `id` will be created under /host-temp/buildah-build-cache/id
|
||||
id := ""
|
||||
//buidkit parity: cache directory defaults to 755
|
||||
mode = 0o755
|
||||
//buidkit parity: cache directory defaults to uid 0 if not specified
|
||||
uid := 0
|
||||
//buidkit parity: cache directory defaults to gid 0 if not specified
|
||||
gid := 0
|
||||
// sharing mode
|
||||
sharing := "shared"
|
||||
|
||||
for _, val := range args {
|
||||
kv := strings.SplitN(val, "=", 2)
|
||||
switch kv[0] {
|
||||
case "type":
|
||||
// This is already processed
|
||||
continue
|
||||
case "nosuid", "nodev", "noexec":
|
||||
// TODO: detect duplication of these options.
|
||||
// (Is this necessary?)
|
||||
newMount.Options = append(newMount.Options, kv[0])
|
||||
case "rw", "readwrite":
|
||||
newMount.Options = append(newMount.Options, "rw")
|
||||
case "readonly", "ro":
|
||||
// Alias for "ro"
|
||||
newMount.Options = append(newMount.Options, "ro")
|
||||
setReadOnly = true
|
||||
case "Z", "z":
|
||||
newMount.Options = append(newMount.Options, kv[0])
|
||||
foundSElinuxLabel = true
|
||||
case "shared", "rshared", "private", "rprivate", "slave", "rslave", "U":
|
||||
newMount.Options = append(newMount.Options, kv[0])
|
||||
setShared = true
|
||||
case "sharing":
|
||||
sharing = kv[1]
|
||||
case "bind-propagation":
|
||||
if len(kv) == 1 {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
newMount.Options = append(newMount.Options, kv[1])
|
||||
case "id":
|
||||
if len(kv) == 1 {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
id = kv[1]
|
||||
case "from":
|
||||
if len(kv) == 1 {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
fromStage = kv[1]
|
||||
case "target", "dst", "destination":
|
||||
if len(kv) == 1 {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
targetPath := kv[1]
|
||||
if !path.IsAbs(targetPath) {
|
||||
targetPath = filepath.Join(workDir, targetPath)
|
||||
}
|
||||
if err := parse.ValidateVolumeCtrDir(targetPath); err != nil {
|
||||
return newMount, nil, err
|
||||
}
|
||||
newMount.Destination = targetPath
|
||||
setDest = true
|
||||
case "src", "source":
|
||||
if len(kv) == 1 {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
newMount.Source = kv[1]
|
||||
case "mode":
|
||||
if len(kv) == 1 {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
mode, err = strconv.ParseUint(kv[1], 8, 32)
|
||||
if err != nil {
|
||||
return newMount, nil, fmt.Errorf("unable to parse cache mode: %w", err)
|
||||
}
|
||||
case "uid":
|
||||
if len(kv) == 1 {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
uid, err = strconv.Atoi(kv[1])
|
||||
if err != nil {
|
||||
return newMount, nil, fmt.Errorf("unable to parse cache uid: %w", err)
|
||||
}
|
||||
case "gid":
|
||||
if len(kv) == 1 {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
gid, err = strconv.Atoi(kv[1])
|
||||
if err != nil {
|
||||
return newMount, nil, fmt.Errorf("unable to parse cache gid: %w", err)
|
||||
}
|
||||
default:
|
||||
return newMount, nil, fmt.Errorf("%v: %w", kv[0], errBadMntOption)
|
||||
}
|
||||
}
|
||||
|
||||
// If selinux is enabled and no selinux option was configured
|
||||
// default to `z` i.e shared content label.
|
||||
if !foundSElinuxLabel && (selinux.EnforceMode() != selinux.Disabled) && fromStage == "" {
|
||||
newMount.Options = append(newMount.Options, "z")
|
||||
}
|
||||
|
||||
if !setDest {
|
||||
return newMount, nil, errBadVolDest
|
||||
}
|
||||
|
||||
if fromStage != "" {
|
||||
// do not create cache on host
|
||||
// instead use read-only mounted stage as cache
|
||||
mountPoint := ""
|
||||
if additionalMountPoints != nil {
|
||||
if val, ok := additionalMountPoints[fromStage]; ok {
|
||||
if val.IsStage {
|
||||
mountPoint = val.MountPoint
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cache does not supports using image so if not stage found
|
||||
// return with error
|
||||
if mountPoint == "" {
|
||||
return newMount, nil, fmt.Errorf("no stage found with name %s", fromStage)
|
||||
}
|
||||
// path should be /contextDir/specified path
|
||||
newMount.Source = filepath.Join(mountPoint, filepath.Clean(string(filepath.Separator)+newMount.Source))
|
||||
} else {
|
||||
// we need to create cache on host if no image is being used
|
||||
|
||||
// since type is cache and cache can be reused by consecutive builds
|
||||
// create a common cache directory, which persists on hosts within temp lifecycle
|
||||
// add subdirectory if specified
|
||||
|
||||
// cache parent directory: creates separate cache parent for each user.
|
||||
cacheParent := CacheParent()
|
||||
// create cache on host if not present
|
||||
err = os.MkdirAll(cacheParent, os.FileMode(0755))
|
||||
if err != nil {
|
||||
return newMount, nil, fmt.Errorf("unable to create build cache directory: %w", err)
|
||||
}
|
||||
|
||||
if id != "" {
|
||||
newMount.Source = filepath.Join(cacheParent, filepath.Clean(id))
|
||||
buildahLockFilesDir = filepath.Join(BuildahCacheLockfileDir, filepath.Clean(id))
|
||||
} else {
|
||||
newMount.Source = filepath.Join(cacheParent, filepath.Clean(newMount.Destination))
|
||||
buildahLockFilesDir = filepath.Join(BuildahCacheLockfileDir, filepath.Clean(newMount.Destination))
|
||||
}
|
||||
idPair := idtools.IDPair{
|
||||
UID: uid,
|
||||
GID: gid,
|
||||
}
|
||||
//buildkit parity: change uid and gid if specified otheriwise keep `0`
|
||||
err = idtools.MkdirAllAndChownNew(newMount.Source, os.FileMode(mode), idPair)
|
||||
if err != nil {
|
||||
return newMount, nil, fmt.Errorf("unable to change uid,gid of cache directory: %w", err)
|
||||
}
|
||||
|
||||
// create a subdirectory inside `cacheParent` just to store lockfiles
|
||||
buildahLockFilesDir = filepath.Join(cacheParent, buildahLockFilesDir)
|
||||
err = os.MkdirAll(buildahLockFilesDir, os.FileMode(0700))
|
||||
if err != nil {
|
||||
return newMount, nil, fmt.Errorf("unable to create build cache lockfiles directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var targetLock *lockfile.LockFile // = nil
|
||||
succeeded := false
|
||||
defer func() {
|
||||
if !succeeded && targetLock != nil {
|
||||
targetLock.Unlock()
|
||||
}
|
||||
}()
|
||||
switch sharing {
|
||||
case "locked":
|
||||
// lock parent cache
|
||||
lockfile, err := lockfile.GetLockFile(filepath.Join(buildahLockFilesDir, BuildahCacheLockfile))
|
||||
if err != nil {
|
||||
return newMount, nil, fmt.Errorf("unable to acquire lock when sharing mode is locked: %w", err)
|
||||
}
|
||||
// Will be unlocked after the RUN step is executed.
|
||||
lockfile.Lock()
|
||||
targetLock = lockfile
|
||||
case "shared":
|
||||
// do nothing since default is `shared`
|
||||
break
|
||||
default:
|
||||
// error out for unknown values
|
||||
return newMount, nil, fmt.Errorf("unrecognized value %q for field `sharing`: %w", sharing, err)
|
||||
}
|
||||
|
||||
// buildkit parity: default sharing should be shared
|
||||
// unless specified
|
||||
if !setShared {
|
||||
newMount.Options = append(newMount.Options, "shared")
|
||||
}
|
||||
|
||||
// buildkit parity: cache must writable unless `ro` or `readonly` is configured explicitly
|
||||
if !setReadOnly {
|
||||
newMount.Options = append(newMount.Options, "rw")
|
||||
}
|
||||
|
||||
newMount.Options = append(newMount.Options, "bind")
|
||||
|
||||
opts, err := parse.ValidateVolumeOpts(newMount.Options)
|
||||
if err != nil {
|
||||
return newMount, nil, err
|
||||
}
|
||||
newMount.Options = opts
|
||||
|
||||
succeeded = true
|
||||
return newMount, targetLock, nil
|
||||
}
|
||||
|
||||
func getVolumeMounts(volumes []string) (map[string]specs.Mount, error) {
|
||||
finalVolumeMounts := make(map[string]specs.Mount)
|
||||
|
||||
for _, volume := range volumes {
|
||||
volumeMount, err := internalParse.Volume(volume)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := finalVolumeMounts[volumeMount.Destination]; ok {
|
||||
return nil, fmt.Errorf("%v: %w", volumeMount.Destination, errDuplicateDest)
|
||||
}
|
||||
finalVolumeMounts[volumeMount.Destination] = volumeMount
|
||||
}
|
||||
return finalVolumeMounts, nil
|
||||
}
|
||||
|
||||
// UnlockLockArray is a helper for cleaning up after GetVolumes and the like.
|
||||
func UnlockLockArray(locks []*lockfile.LockFile) {
|
||||
for _, lock := range locks {
|
||||
lock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// GetVolumes gets the volumes from --volume and --mount
|
||||
//
|
||||
// If this function succeeds, the caller must unlock the returned *lockfile.LockFile s if any (when??).
|
||||
func GetVolumes(ctx *types.SystemContext, store storage.Store, volumes []string, mounts []string, contextDir string, workDir string) ([]specs.Mount, []string, []*lockfile.LockFile, error) {
|
||||
unifiedMounts, mountedImages, targetLocks, err := getMounts(ctx, store, mounts, contextDir, workDir)
|
||||
if err != nil {
|
||||
return nil, mountedImages, nil, err
|
||||
}
|
||||
succeeded := false
|
||||
defer func() {
|
||||
if !succeeded {
|
||||
UnlockLockArray(targetLocks)
|
||||
}
|
||||
}()
|
||||
volumeMounts, err := getVolumeMounts(volumes)
|
||||
if err != nil {
|
||||
return nil, mountedImages, nil, err
|
||||
}
|
||||
for dest, mount := range volumeMounts {
|
||||
if _, ok := unifiedMounts[dest]; ok {
|
||||
return nil, mountedImages, nil, fmt.Errorf("%v: %w", dest, errDuplicateDest)
|
||||
}
|
||||
unifiedMounts[dest] = mount
|
||||
}
|
||||
|
||||
finalMounts := make([]specs.Mount, 0, len(unifiedMounts))
|
||||
for _, mount := range unifiedMounts {
|
||||
finalMounts = append(finalMounts, mount)
|
||||
}
|
||||
succeeded = true
|
||||
return finalMounts, mountedImages, targetLocks, nil
|
||||
}
|
||||
|
||||
// getMounts takes user-provided input from the --mount flag and creates OCI
|
||||
// spec mounts.
|
||||
// buildah run --mount type=bind,src=/etc/resolv.conf,target=/etc/resolv.conf ...
|
||||
// buildah run --mount type=tmpfs,target=/dev/shm ...
|
||||
//
|
||||
// If this function succeeds, the caller must unlock the returned *lockfile.LockFile s if any (when??).
|
||||
func getMounts(ctx *types.SystemContext, store storage.Store, mounts []string, contextDir string, workDir string) (map[string]specs.Mount, []string, []*lockfile.LockFile, error) {
|
||||
// If `type` is not set default to "bind"
|
||||
mountType := define.TypeBind
|
||||
finalMounts := make(map[string]specs.Mount)
|
||||
mountedImages := make([]string, 0)
|
||||
targetLocks := make([]*lockfile.LockFile, 0)
|
||||
succeeded := false
|
||||
defer func() {
|
||||
if !succeeded {
|
||||
UnlockLockArray(targetLocks)
|
||||
}
|
||||
}()
|
||||
|
||||
errInvalidSyntax := errors.New("incorrect mount format: should be --mount type=<bind|tmpfs>,[src=<host-dir>,]target=<ctr-dir>[,options]")
|
||||
|
||||
// TODO(vrothberg): the manual parsing can be replaced with a regular expression
|
||||
// to allow a more robust parsing of the mount format and to give
|
||||
// precise errors regarding supported format versus supported options.
|
||||
for _, mount := range mounts {
|
||||
tokens := strings.Split(mount, ",")
|
||||
if len(tokens) < 2 {
|
||||
return nil, mountedImages, nil, fmt.Errorf("%q: %w", mount, errInvalidSyntax)
|
||||
}
|
||||
for _, field := range tokens {
|
||||
if strings.HasPrefix(field, "type=") {
|
||||
kv := strings.Split(field, "=")
|
||||
if len(kv) != 2 {
|
||||
return nil, mountedImages, nil, fmt.Errorf("%q: %w", mount, errInvalidSyntax)
|
||||
}
|
||||
mountType = kv[1]
|
||||
}
|
||||
}
|
||||
switch mountType {
|
||||
case define.TypeBind:
|
||||
mount, image, err := GetBindMount(ctx, tokens, contextDir, store, "", nil, workDir)
|
||||
if err != nil {
|
||||
return nil, mountedImages, nil, err
|
||||
}
|
||||
if _, ok := finalMounts[mount.Destination]; ok {
|
||||
return nil, mountedImages, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest)
|
||||
}
|
||||
finalMounts[mount.Destination] = mount
|
||||
mountedImages = append(mountedImages, image)
|
||||
case TypeCache:
|
||||
mount, tl, err := GetCacheMount(tokens, store, "", nil, workDir)
|
||||
if err != nil {
|
||||
return nil, mountedImages, nil, err
|
||||
}
|
||||
if tl != nil {
|
||||
targetLocks = append(targetLocks, tl)
|
||||
}
|
||||
if _, ok := finalMounts[mount.Destination]; ok {
|
||||
return nil, mountedImages, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest)
|
||||
}
|
||||
finalMounts[mount.Destination] = mount
|
||||
case TypeTmpfs:
|
||||
mount, err := GetTmpfsMount(tokens)
|
||||
if err != nil {
|
||||
return nil, mountedImages, nil, err
|
||||
}
|
||||
if _, ok := finalMounts[mount.Destination]; ok {
|
||||
return nil, mountedImages, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest)
|
||||
}
|
||||
finalMounts[mount.Destination] = mount
|
||||
default:
|
||||
return nil, mountedImages, nil, fmt.Errorf("invalid filesystem type %q", mountType)
|
||||
}
|
||||
}
|
||||
|
||||
succeeded = true
|
||||
return finalMounts, mountedImages, targetLocks, nil
|
||||
}
|
||||
|
||||
// GetTmpfsMount parses a single tmpfs mount entry from the --mount flag
|
||||
func GetTmpfsMount(args []string) (specs.Mount, error) {
|
||||
newMount := specs.Mount{
|
||||
Type: TypeTmpfs,
|
||||
Source: TypeTmpfs,
|
||||
}
|
||||
|
||||
setDest := false
|
||||
|
||||
for _, val := range args {
|
||||
kv := strings.SplitN(val, "=", 2)
|
||||
switch kv[0] {
|
||||
case "type":
|
||||
// This is already processed
|
||||
continue
|
||||
case "ro", "nosuid", "nodev", "noexec":
|
||||
newMount.Options = append(newMount.Options, kv[0])
|
||||
case "readonly":
|
||||
// Alias for "ro"
|
||||
newMount.Options = append(newMount.Options, "ro")
|
||||
case "tmpcopyup":
|
||||
//the path that is shadowed by the tmpfs mount is recursively copied up to the tmpfs itself.
|
||||
newMount.Options = append(newMount.Options, kv[0])
|
||||
case "tmpfs-mode":
|
||||
if len(kv) == 1 {
|
||||
return newMount, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
newMount.Options = append(newMount.Options, fmt.Sprintf("mode=%s", kv[1]))
|
||||
case "tmpfs-size":
|
||||
if len(kv) == 1 {
|
||||
return newMount, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
newMount.Options = append(newMount.Options, fmt.Sprintf("size=%s", kv[1]))
|
||||
case "src", "source":
|
||||
return newMount, errors.New("source is not supported with tmpfs mounts")
|
||||
case "target", "dst", "destination":
|
||||
if len(kv) == 1 {
|
||||
return newMount, fmt.Errorf("%v: %w", kv[0], errBadOptionArg)
|
||||
}
|
||||
if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil {
|
||||
return newMount, err
|
||||
}
|
||||
newMount.Destination = kv[1]
|
||||
setDest = true
|
||||
default:
|
||||
return newMount, fmt.Errorf("%v: %w", kv[0], errBadMntOption)
|
||||
}
|
||||
}
|
||||
|
||||
if !setDest {
|
||||
return newMount, errBadVolDest
|
||||
}
|
||||
|
||||
return newMount, nil
|
||||
}
|
|
@ -70,11 +70,6 @@ func RepoNamesToNamedReferences(destList []string) ([]reference.Named, error) {
|
|||
return result, nil
|
||||
}
|
||||
|
||||
// CleanCacheMount gets the cache parent created by `--mount=type=cache` and removes it.
|
||||
func CleanCacheMount() error {
|
||||
return internalParse.CleanCacheMount()
|
||||
}
|
||||
|
||||
// CommonBuildOptions parses the build options from the bud cli
|
||||
func CommonBuildOptions(c *cobra.Command) (*define.CommonBuildOptions, error) {
|
||||
return CommonBuildOptionsFromFlagSet(c.Flags(), c.Flag)
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package volumes
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/containers/buildah/internal/volumes"
|
||||
)
|
||||
|
||||
// CleanCacheMount gets the cache parent created by `--mount=type=cache` and removes it.
|
||||
func CleanCacheMount() error {
|
||||
cacheParent := volumes.CacheParent()
|
||||
return os.RemoveAll(cacheParent)
|
||||
}
|
|
@ -26,8 +26,8 @@ import (
|
|||
"github.com/containers/buildah/copier"
|
||||
"github.com/containers/buildah/define"
|
||||
"github.com/containers/buildah/internal"
|
||||
internalParse "github.com/containers/buildah/internal/parse"
|
||||
internalUtil "github.com/containers/buildah/internal/util"
|
||||
"github.com/containers/buildah/internal/volumes"
|
||||
"github.com/containers/buildah/pkg/overlay"
|
||||
"github.com/containers/buildah/pkg/sshagent"
|
||||
"github.com/containers/buildah/util"
|
||||
|
@ -1358,7 +1358,7 @@ func (b *Builder) setupMounts(mountPoint string, spec *specs.Spec, bundlePath st
|
|||
succeeded := false
|
||||
defer func() {
|
||||
if !succeeded {
|
||||
internalParse.UnlockLockArray(mountArtifacts.TargetLocks)
|
||||
volumes.UnlockLockArray(mountArtifacts.TargetLocks)
|
||||
}
|
||||
}()
|
||||
// Add temporary copies of the contents of volume locations at the
|
||||
|
@ -1522,7 +1522,7 @@ func (b *Builder) runSetupRunMounts(mountPoint string, mounts []string, sources
|
|||
succeeded := false
|
||||
defer func() {
|
||||
if !succeeded {
|
||||
internalParse.UnlockLockArray(targetLocks)
|
||||
volumes.UnlockLockArray(targetLocks)
|
||||
}
|
||||
}()
|
||||
for _, mount := range mounts {
|
||||
|
@ -1626,7 +1626,7 @@ func (b *Builder) getBindMount(tokens []string, context *imageTypes.SystemContex
|
|||
return nil, "", errors.New("Context Directory for current run invocation is not configured")
|
||||
}
|
||||
var optionMounts []specs.Mount
|
||||
mount, image, err := internalParse.GetBindMount(context, tokens, contextDir, b.store, b.MountLabel, stageMountPoints, workDir)
|
||||
mount, image, err := volumes.GetBindMount(context, tokens, contextDir, b.store, b.MountLabel, stageMountPoints, workDir)
|
||||
if err != nil {
|
||||
return nil, image, err
|
||||
}
|
||||
|
@ -1640,7 +1640,7 @@ func (b *Builder) getBindMount(tokens []string, context *imageTypes.SystemContex
|
|||
|
||||
func (b *Builder) getTmpfsMount(tokens []string, idMaps IDMaps) (*specs.Mount, error) {
|
||||
var optionMounts []specs.Mount
|
||||
mount, err := internalParse.GetTmpfsMount(tokens)
|
||||
mount, err := volumes.GetTmpfsMount(tokens)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1953,7 +1953,7 @@ func (b *Builder) cleanupRunMounts(context *imageTypes.SystemContext, mountpoint
|
|||
}
|
||||
}
|
||||
// unlock if any locked files from this RUN statement
|
||||
internalParse.UnlockLockArray(artifacts.TargetLocks)
|
||||
volumes.UnlockLockArray(artifacts.TargetLocks)
|
||||
return prevErr
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import (
|
|||
"github.com/containers/buildah/copier"
|
||||
"github.com/containers/buildah/define"
|
||||
"github.com/containers/buildah/internal"
|
||||
internalParse "github.com/containers/buildah/internal/parse"
|
||||
"github.com/containers/buildah/internal/volumes"
|
||||
"github.com/containers/buildah/pkg/overlay"
|
||||
"github.com/containers/buildah/pkg/parse"
|
||||
butil "github.com/containers/buildah/pkg/util"
|
||||
|
@ -1254,7 +1254,7 @@ func checkIdsGreaterThan5(ids []specs.LinuxIDMapping) bool {
|
|||
// If this function succeeds and returns a non-nil *lockfile.LockFile, the caller must unlock it (when??).
|
||||
func (b *Builder) getCacheMount(tokens []string, stageMountPoints map[string]internal.StageMountDetails, idMaps IDMaps, workDir string) (*specs.Mount, *lockfile.LockFile, error) {
|
||||
var optionMounts []specs.Mount
|
||||
mount, targetLock, err := internalParse.GetCacheMount(tokens, b.store, b.MountLabel, stageMountPoints, workDir)
|
||||
mount, targetLock, err := volumes.GetCacheMount(tokens, b.store, b.MountLabel, stageMountPoints, workDir)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue