Merge commit from fork
[release-1.37] fix TOCTOU error when bind and cache mounts use "src" values
This commit is contained in:
commit
419d6fb9b3
|
@ -7,10 +7,13 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/containers/buildah"
|
||||
"github.com/containers/buildah/internal/tmpdir"
|
||||
"github.com/containers/buildah/internal/volumes"
|
||||
buildahcli "github.com/containers/buildah/pkg/cli"
|
||||
"github.com/containers/buildah/pkg/overlay"
|
||||
"github.com/containers/buildah/pkg/parse"
|
||||
"github.com/containers/buildah/util"
|
||||
"github.com/containers/storage/pkg/mount"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -108,6 +111,16 @@ func runCmd(c *cobra.Command, args []string, iopts runInputOptions) error {
|
|||
return errors.New("command must be specified")
|
||||
}
|
||||
|
||||
tmpDir, err := os.MkdirTemp(tmpdir.GetTempDir(), "buildahvolume")
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating temporary directory: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Remove(tmpDir); err != nil {
|
||||
logrus.Debugf("removing should-be-empty temporary directory %q: %v", tmpDir, err)
|
||||
}
|
||||
}()
|
||||
|
||||
store, err := getStore(c)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -179,14 +192,30 @@ 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 := volumes.GetVolumes(systemContext, store, iopts.volumes, iopts.mounts, iopts.contextDir, iopts.workingDir)
|
||||
mounts, mountedImages, intermediateMounts, _, targetLocks, err := volumes.GetVolumes(systemContext, store, builder.MountLabel, iopts.volumes, iopts.mounts, iopts.contextDir, iopts.workingDir, tmpDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer volumes.UnlockLockArray(targetLocks)
|
||||
defer func() {
|
||||
if err := overlay.CleanupContent(tmpDir); err != nil {
|
||||
logrus.Debugf("unmounting overlay mounts under %q: %v", tmpDir, err)
|
||||
}
|
||||
for _, intermediateMount := range intermediateMounts {
|
||||
if err := mount.Unmount(intermediateMount); err != nil {
|
||||
logrus.Debugf("unmounting mount %q: %v", intermediateMount, err)
|
||||
}
|
||||
if err := os.Remove(intermediateMount); err != nil {
|
||||
logrus.Debugf("removing should-be-empty mount directory %q: %v", intermediateMount, err)
|
||||
}
|
||||
}
|
||||
for _, mountedImage := range mountedImages {
|
||||
if _, err := store.UnmountImage(mountedImage, false); err != nil {
|
||||
logrus.Debugf("unmounting image %q: %v", mountedImage, err)
|
||||
}
|
||||
}
|
||||
volumes.UnlockLockArray(targetLocks)
|
||||
}()
|
||||
options.Mounts = mounts
|
||||
// Run() will automatically clean them up.
|
||||
options.ExternalImageMounts = mountedImages
|
||||
options.CgroupManager = globalFlagResults.CgroupManager
|
||||
|
||||
runerr := builder.Run(args, options)
|
||||
|
|
|
@ -169,13 +169,13 @@ type SBOMScanOptions struct {
|
|||
MergeStrategy SBOMMergeStrategy // how to merge the outputs of multiple scans
|
||||
}
|
||||
|
||||
// TempDirForURL checks if the passed-in string looks like a URL or -. If it is,
|
||||
// TempDirForURL creates a temporary directory, arranges for its contents to be
|
||||
// the contents of that URL, and returns the temporary directory's path, along
|
||||
// with the name of a subdirectory which should be used as the build context
|
||||
// (which may be empty or "."). Removal of the temporary directory is the
|
||||
// responsibility of the caller. If the string doesn't look like a URL,
|
||||
// TempDirForURL returns empty strings and a nil error code.
|
||||
// TempDirForURL checks if the passed-in string looks like a URL or "-". If it
|
||||
// is, TempDirForURL creates a temporary directory, arranges for its contents
|
||||
// to be the contents of that URL, and returns the temporary directory's path,
|
||||
// along with the relative name of a subdirectory which should be used as the
|
||||
// build context (which may be empty or "."). Removal of the temporary
|
||||
// directory is the responsibility of the caller. If the string doesn't look
|
||||
// like a URL or "-", TempDirForURL returns empty strings and a nil error code.
|
||||
func TempDirForURL(dir, prefix, url string) (name string, subdir string, err error) {
|
||||
if !strings.HasPrefix(url, "http://") &&
|
||||
!strings.HasPrefix(url, "https://") &&
|
||||
|
@ -188,19 +188,24 @@ func TempDirForURL(dir, prefix, url string) (name string, subdir string, err err
|
|||
if err != nil {
|
||||
return "", "", fmt.Errorf("creating temporary directory for %q: %w", url, err)
|
||||
}
|
||||
downloadDir := filepath.Join(name, "download")
|
||||
if err = os.MkdirAll(downloadDir, 0o700); err != nil {
|
||||
return "", "", fmt.Errorf("creating directory %q for %q: %w", downloadDir, url, err)
|
||||
}
|
||||
urlParsed, err := urlpkg.Parse(url)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("parsing url %q: %w", url, err)
|
||||
}
|
||||
if strings.HasPrefix(url, "git://") || strings.HasSuffix(urlParsed.Path, ".git") {
|
||||
combinedOutput, gitSubDir, err := cloneToDirectory(url, name)
|
||||
combinedOutput, gitSubDir, err := cloneToDirectory(url, downloadDir)
|
||||
if err != nil {
|
||||
if err2 := os.RemoveAll(name); err2 != nil {
|
||||
logrus.Debugf("error removing temporary directory %q: %v", name, err2)
|
||||
}
|
||||
return "", "", fmt.Errorf("cloning %q to %q:\n%s: %w", url, name, string(combinedOutput), err)
|
||||
}
|
||||
return name, gitSubDir, nil
|
||||
logrus.Debugf("Build context is at %q", filepath.Join(downloadDir, gitSubDir))
|
||||
return name, filepath.Join(filepath.Base(downloadDir), gitSubDir), nil
|
||||
}
|
||||
if strings.HasPrefix(url, "github.com/") {
|
||||
ghurl := url
|
||||
|
@ -209,28 +214,29 @@ func TempDirForURL(dir, prefix, url string) (name string, subdir string, err err
|
|||
subdir = path.Base(ghurl) + "-master"
|
||||
}
|
||||
if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
|
||||
err = downloadToDirectory(url, name)
|
||||
err = downloadToDirectory(url, downloadDir)
|
||||
if err != nil {
|
||||
if err2 := os.RemoveAll(name); err2 != nil {
|
||||
logrus.Debugf("error removing temporary directory %q: %v", name, err2)
|
||||
}
|
||||
return "", subdir, err
|
||||
return "", "", err
|
||||
}
|
||||
return name, subdir, nil
|
||||
logrus.Debugf("Build context is at %q", filepath.Join(downloadDir, subdir))
|
||||
return name, filepath.Join(filepath.Base(downloadDir), subdir), nil
|
||||
}
|
||||
if url == "-" {
|
||||
err = stdinToDirectory(name)
|
||||
err = stdinToDirectory(downloadDir)
|
||||
if err != nil {
|
||||
if err2 := os.RemoveAll(name); err2 != nil {
|
||||
logrus.Debugf("error removing temporary directory %q: %v", name, err2)
|
||||
}
|
||||
return "", subdir, err
|
||||
return "", "", err
|
||||
}
|
||||
logrus.Debugf("Build context is at %q", name)
|
||||
return name, subdir, nil
|
||||
logrus.Debugf("Build context is at %q", filepath.Join(downloadDir, subdir))
|
||||
return name, filepath.Join(filepath.Base(downloadDir), subdir), nil
|
||||
}
|
||||
logrus.Debugf("don't know how to retrieve %q", url)
|
||||
if err2 := os.Remove(name); err2 != nil {
|
||||
if err2 := os.RemoveAll(name); err2 != nil {
|
||||
logrus.Debugf("error removing temporary directory %q: %v", name, err2)
|
||||
}
|
||||
return "", "", errors.New("unreachable code reached")
|
||||
|
|
|
@ -118,7 +118,7 @@ BUILDAH\_ISOLATION environment variable. `export BUILDAH_ISOLATION=oci`
|
|||
|
||||
Attach a filesystem mount to the container
|
||||
|
||||
Current supported mount TYPES are bind, cache, secret and tmpfs.
|
||||
Current supported mount TYPES are bind, cache, secret and tmpfs. Writes to `bind` and `tmpfs` mounts are discarded after the command finishes, while changes to `cache` mounts persist across uses.
|
||||
|
||||
e.g.
|
||||
|
||||
|
@ -130,11 +130,11 @@ Current supported mount TYPES are bind, cache, secret and tmpfs.
|
|||
|
||||
Common Options:
|
||||
|
||||
· src, source: mount source spec for bind and volume. Mandatory for bind. If `from` is specified, `src` is the subpath in the `from` field.
|
||||
· src, source: mount source spec for bind and cache. Mandatory for bind. If `from` is specified, `src` is the subpath in the `from` field.
|
||||
|
||||
· dst, destination, target: mount destination spec.
|
||||
· dst, destination, target: location where the command being run should see the content being mounted.
|
||||
|
||||
· ro, read-only: true or false (default).
|
||||
· ro, read-only: (default true for `type=bind`, false for `type=tmpfs`, `type=cache`).
|
||||
|
||||
Options specific to bind:
|
||||
|
||||
|
@ -142,7 +142,7 @@ Current supported mount TYPES are bind, cache, secret and tmpfs.
|
|||
|
||||
. bind-nonrecursive: do not setup a recursive bind mount. By default it is recursive.
|
||||
|
||||
· from: stage or image name for the root of the source. Defaults to the build context.
|
||||
· from: image name for the root of the source. Defaults to **--contextdir**, mandatory if **--contextdir** was not specified.
|
||||
|
||||
· z: Set shared SELinux label on mounted destination. Use if SELinux is enabled on host machine.
|
||||
|
||||
|
@ -162,7 +162,7 @@ Current supported mount TYPES are bind, cache, secret and tmpfs.
|
|||
|
||||
Options specific to cache:
|
||||
|
||||
· id: Create a separate cache directory for a particular id.
|
||||
· id: Distinguish this cache from other caches using this ID rather than the target mount path.
|
||||
|
||||
· mode: File mode for new cache directory in octal. Default 0755.
|
||||
|
||||
|
@ -174,6 +174,8 @@ Current supported mount TYPES are bind, cache, secret and tmpfs.
|
|||
|
||||
· from: stage name for the root of the source. Defaults to host cache directory.
|
||||
|
||||
· sharing: Whether other users of this cache need to wait for this command to complete (`sharing=locked`) or not (`sharing=shared`, which is the default).
|
||||
|
||||
· z: Set shared SELinux label on mounted destination. Enabled by default if SELinux is enabled on the host machine.
|
||||
|
||||
· Z: Set private SELinux label on mounted destination. Use if SELinux is enabled on host machine.
|
||||
|
|
|
@ -635,7 +635,12 @@ func (s *StageExecutor) runStageMountPoints(mountList []string) (map[string]inte
|
|||
// to `mountPoint` replaced from additional
|
||||
// build-context. Reason: Parser will use this
|
||||
// `from` to refer from stageMountPoints map later.
|
||||
stageMountPoints[from] = internal.StageMountDetails{IsStage: false, DidExecute: true, MountPoint: mountPoint}
|
||||
stageMountPoints[from] = internal.StageMountDetails{
|
||||
IsAdditionalBuildContext: true,
|
||||
IsImage: true,
|
||||
DidExecute: true,
|
||||
MountPoint: mountPoint,
|
||||
}
|
||||
break
|
||||
} else {
|
||||
// Most likely this points to path on filesystem
|
||||
|
@ -667,7 +672,11 @@ func (s *StageExecutor) runStageMountPoints(mountList []string) (map[string]inte
|
|||
mountPoint = additionalBuildContext.DownloadedCache
|
||||
}
|
||||
}
|
||||
stageMountPoints[from] = internal.StageMountDetails{IsStage: true, DidExecute: true, MountPoint: mountPoint}
|
||||
stageMountPoints[from] = internal.StageMountDetails{
|
||||
IsAdditionalBuildContext: true,
|
||||
DidExecute: true,
|
||||
MountPoint: mountPoint,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -678,14 +687,22 @@ func (s *StageExecutor) runStageMountPoints(mountList []string) (map[string]inte
|
|||
return nil, err
|
||||
}
|
||||
if otherStage, ok := s.executor.stages[from]; ok && otherStage.index < s.index {
|
||||
stageMountPoints[from] = internal.StageMountDetails{IsStage: true, DidExecute: otherStage.didExecute, MountPoint: otherStage.mountPoint}
|
||||
stageMountPoints[from] = internal.StageMountDetails{
|
||||
IsStage: true,
|
||||
DidExecute: otherStage.didExecute,
|
||||
MountPoint: otherStage.mountPoint,
|
||||
}
|
||||
break
|
||||
} else {
|
||||
mountPoint, err := s.getImageRootfs(s.ctx, from)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s from=%s: no stage or image found with that name", flag, from)
|
||||
}
|
||||
stageMountPoints[from] = internal.StageMountDetails{IsStage: false, DidExecute: true, MountPoint: mountPoint}
|
||||
stageMountPoints[from] = internal.StageMountDetails{
|
||||
IsImage: true,
|
||||
DidExecute: true,
|
||||
MountPoint: mountPoint,
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package open
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// InChroot opens the file at `path` after chrooting to `root` and then
|
||||
// changing its working directory to `wd`. Both `wd` and `path` are evaluated
|
||||
// in the chroot.
|
||||
// Returns a file handle, an Errno value if there was an error and the
|
||||
// underlying error was a standard library error code, and a non-empty error if
|
||||
// one was detected.
|
||||
func InChroot(root, wd, path string, mode int, perm uint32) (fd int, errno syscall.Errno, err error) {
|
||||
requests := requests{
|
||||
Root: root,
|
||||
Wd: wd,
|
||||
Open: []request{
|
||||
{
|
||||
Path: path,
|
||||
Mode: mode,
|
||||
Perms: perm,
|
||||
},
|
||||
},
|
||||
}
|
||||
results := inChroot(requests)
|
||||
if len(results.Open) != 1 {
|
||||
return -1, 0, fmt.Errorf("got %d results back instead of 1", len(results.Open))
|
||||
}
|
||||
if results.Open[0].Err != "" {
|
||||
if results.Open[0].Errno != 0 {
|
||||
err = fmt.Errorf("%s: %w", results.Open[0].Err, results.Open[0].Errno)
|
||||
} else {
|
||||
err = errors.New(results.Open[0].Err)
|
||||
}
|
||||
}
|
||||
return int(results.Open[0].Fd), results.Open[0].Errno, err
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package open
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/storage/pkg/reexec"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
bindFdToPathCommand = "buildah-bind-fd-to-path"
|
||||
)
|
||||
|
||||
func init() {
|
||||
reexec.Register(bindFdToPathCommand, bindFdToPathMain)
|
||||
}
|
||||
|
||||
// BindFdToPath creates a bind mount from the open file (which is actually a
|
||||
// directory) to the specified location. If it succeeds, the caller will need
|
||||
// to unmount the targetPath when it's finished using it. Regardless, it
|
||||
// closes the passed-in descriptor.
|
||||
func BindFdToPath(fd uintptr, targetPath string) error {
|
||||
f := os.NewFile(fd, "passed-in directory descriptor")
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
logrus.Debugf("closing descriptor %d after attempting to bind to %q: %v", fd, targetPath, err)
|
||||
}
|
||||
}()
|
||||
pipeReader, pipeWriter, err := os.Pipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := reexec.Command(bindFdToPathCommand)
|
||||
cmd.Stdin = pipeReader
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout, cmd.Stderr = &stdout, &stderr
|
||||
cmd.ExtraFiles = append(cmd.ExtraFiles, f)
|
||||
|
||||
err = cmd.Start()
|
||||
pipeReader.Close()
|
||||
if err != nil {
|
||||
pipeWriter.Close()
|
||||
return fmt.Errorf("starting child: %w", err)
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(pipeWriter)
|
||||
if err := encoder.Encode(&targetPath); err != nil {
|
||||
return fmt.Errorf("sending target path to child: %w", err)
|
||||
}
|
||||
pipeWriter.Close()
|
||||
err = cmd.Wait()
|
||||
trimmedOutput := strings.TrimSpace(stdout.String()) + strings.TrimSpace(stderr.String())
|
||||
if err != nil {
|
||||
if len(trimmedOutput) > 0 {
|
||||
err = fmt.Errorf("%s: %w", trimmedOutput, err)
|
||||
}
|
||||
} else {
|
||||
if len(trimmedOutput) > 0 {
|
||||
err = errors.New(trimmedOutput)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func bindFdToPathMain() {
|
||||
var targetPath string
|
||||
decoder := json.NewDecoder(os.Stdin)
|
||||
if err := decoder.Decode(&targetPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error decoding target path")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := unix.Fchdir(3); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "fchdir(): %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := unix.Mount(".", targetPath, "bind", unix.MS_BIND, ""); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "bind-mounting passed-in directory to %q: %v", targetPath, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package open
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestBindFdToPath(t *testing.T) {
|
||||
first := t.TempDir()
|
||||
sampleData := []byte("sample data")
|
||||
err := os.WriteFile(filepath.Join(first, "testfile"), sampleData, 0o600)
|
||||
require.NoError(t, err, "writing sample data to first directory")
|
||||
fd, err := unix.Open(first, unix.O_DIRECTORY, 0)
|
||||
require.NoError(t, err, "opening descriptor for first directory")
|
||||
second := t.TempDir()
|
||||
err = BindFdToPath(uintptr(fd), second)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err := unix.Unmount(second, unix.MNT_DETACH)
|
||||
require.NoError(t, err, "unmounting as part of cleanup")
|
||||
})
|
||||
readBack, err := os.ReadFile(filepath.Join(second, "testfile"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, sampleData, readBack, "expected to read back data via the bind mount")
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package open
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/containers/storage/pkg/reexec"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if reexec.Init() {
|
||||
return
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestOpenInChroot(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
firstContents := []byte{0, 1, 2, 3}
|
||||
secondContents := []byte{4, 5, 6, 7}
|
||||
require.NoErrorf(t, os.WriteFile(filepath.Join(tmpdir, "a"), firstContents, 0o644), "creating first test file")
|
||||
require.NoErrorf(t, os.MkdirAll(filepath.Join(tmpdir, tmpdir), 0o755), "creating test subdirectory")
|
||||
require.NoErrorf(t, os.WriteFile(filepath.Join(tmpdir, tmpdir, "a"), secondContents, 0o644), "creating second test file")
|
||||
|
||||
result := inChroot(requests{
|
||||
Open: []request{
|
||||
{
|
||||
Path: filepath.Join(tmpdir, "a"),
|
||||
Mode: unix.O_RDONLY,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Empty(t, result.Err, "result from first client")
|
||||
require.Equal(t, 1, len(result.Open), "results from first client")
|
||||
require.Empty(t, result.Open[0].Err, "first (only) result from first client")
|
||||
f := os.NewFile(result.Open[0].Fd, "file from first subprocess")
|
||||
contents, err := io.ReadAll(f)
|
||||
require.NoErrorf(t, err, "reading from file from first subprocess")
|
||||
require.Equalf(t, firstContents, contents, "contents of file from first subprocess")
|
||||
f.Close()
|
||||
|
||||
result = inChroot(requests{
|
||||
Root: tmpdir,
|
||||
Open: []request{
|
||||
{
|
||||
Path: filepath.Join(tmpdir, "a"),
|
||||
Mode: unix.O_RDONLY,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Empty(t, result.Err, "result from second client")
|
||||
require.Equal(t, 1, len(result.Open), "results from second client")
|
||||
require.Empty(t, result.Open[0].Err, "first (only) result from second client")
|
||||
f = os.NewFile(result.Open[0].Fd, "file from second subprocess")
|
||||
contents, err = io.ReadAll(f)
|
||||
require.NoErrorf(t, err, "reading from file from second subprocess")
|
||||
require.Equalf(t, secondContents, contents, "contents of file from second subprocess")
|
||||
f.Close()
|
||||
|
||||
fd, errno, err := InChroot(tmpdir, "", filepath.Join(tmpdir, "a"), unix.O_RDONLY, 0)
|
||||
require.NoErrorf(t, err, "wrapper for opening just one item")
|
||||
require.Zero(t, errno, "errno from open file")
|
||||
f = os.NewFile(uintptr(fd), "file from third subprocess")
|
||||
require.NoErrorf(t, err, "reading from file from third subprocess")
|
||||
require.Equalf(t, secondContents, contents, "contents of file from third subprocess")
|
||||
f.Close()
|
||||
|
||||
fd, errno, err = InChroot(tmpdir, "", filepath.Join(tmpdir, "b"), unix.O_RDONLY, 0)
|
||||
require.Errorf(t, err, "attempting to open a non-existent file")
|
||||
require.NotZero(t, errno, "attempting to open a non-existent file")
|
||||
require.Equal(t, -1, fd, "returned descriptor when open fails")
|
||||
f.Close()
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package open
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type request struct {
|
||||
Path string
|
||||
Mode int
|
||||
Perms uint32
|
||||
}
|
||||
|
||||
type requests struct {
|
||||
Root string
|
||||
Wd string
|
||||
Open []request
|
||||
}
|
||||
|
||||
type result struct {
|
||||
Fd uintptr // as returned by open()
|
||||
Err string // if err was not `nil`, err.Error()
|
||||
Errno syscall.Errno // if err was not `nil` and included a syscall.Errno, its value
|
||||
}
|
||||
|
||||
type results struct {
|
||||
Err string
|
||||
Open []result
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
//go:build linux || freebsd || darwin
|
||||
|
||||
package open
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/containers/storage/pkg/reexec"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
inChrootCommand = "buildah-open-in-chroot"
|
||||
)
|
||||
|
||||
func init() {
|
||||
reexec.Register(inChrootCommand, inChrootMain)
|
||||
}
|
||||
|
||||
func inChroot(requests requests) results {
|
||||
sock, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
|
||||
if err != nil {
|
||||
return results{Err: fmt.Errorf("creating socket pair: %w", err).Error()}
|
||||
}
|
||||
parentSock := sock[0]
|
||||
childSock := sock[1]
|
||||
parentEnd := os.NewFile(uintptr(parentSock), "parent end of socket pair")
|
||||
childEnd := os.NewFile(uintptr(childSock), "child end of socket pair")
|
||||
cmd := reexec.Command(inChrootCommand)
|
||||
cmd.ExtraFiles = append(cmd.ExtraFiles, childEnd)
|
||||
err = cmd.Start()
|
||||
childEnd.Close()
|
||||
defer parentEnd.Close()
|
||||
if err != nil {
|
||||
return results{Err: err.Error()}
|
||||
}
|
||||
encoder := json.NewEncoder(parentEnd)
|
||||
if err := encoder.Encode(&requests); err != nil {
|
||||
return results{Err: fmt.Errorf("sending request down socket: %w", err).Error()}
|
||||
}
|
||||
if err := unix.Shutdown(parentSock, unix.SHUT_WR); err != nil {
|
||||
return results{Err: fmt.Errorf("finishing sending request down socket: %w", err).Error()}
|
||||
}
|
||||
b := make([]byte, 65536)
|
||||
oob := make([]byte, 65536)
|
||||
n, oobn, _, _, err := unix.Recvmsg(parentSock, b, oob, 0)
|
||||
if err != nil {
|
||||
return results{Err: fmt.Errorf("receiving message: %w", err).Error()}
|
||||
}
|
||||
if err := unix.Shutdown(parentSock, unix.SHUT_RD); err != nil {
|
||||
return results{Err: fmt.Errorf("finishing socket: %w", err).Error()}
|
||||
}
|
||||
if n > len(b) {
|
||||
return results{Err: fmt.Errorf("too much regular data: %d > %d", n, len(b)).Error()}
|
||||
}
|
||||
if oobn > len(oob) {
|
||||
return results{Err: fmt.Errorf("too much OOB data: %d > %d", oobn, len(oob)).Error()}
|
||||
}
|
||||
scms, err := unix.ParseSocketControlMessage(oob[:oobn])
|
||||
if err != nil {
|
||||
return results{Err: fmt.Errorf("parsing control message: %w", err).Error()}
|
||||
}
|
||||
var receivedFds []int
|
||||
for i := range scms {
|
||||
fds, err := unix.ParseUnixRights(&scms[i])
|
||||
if err != nil {
|
||||
return results{Err: fmt.Errorf("parsing rights message %d: %w", i, err).Error()}
|
||||
}
|
||||
receivedFds = append(receivedFds, fds...)
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(b[:n]))
|
||||
var result results
|
||||
if err := decoder.Decode(&result); err != nil {
|
||||
return results{Err: fmt.Errorf("decoding results: %w", err).Error()}
|
||||
}
|
||||
j := 0
|
||||
for i := range result.Open {
|
||||
if result.Open[i].Err == "" {
|
||||
if j >= len(receivedFds) {
|
||||
for _, fd := range receivedFds {
|
||||
unix.Close(fd)
|
||||
}
|
||||
return results{Err: fmt.Errorf("didn't receive enough FDs").Error()}
|
||||
}
|
||||
result.Open[i].Fd = uintptr(receivedFds[j])
|
||||
j++
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func inChrootMain() {
|
||||
var theseRequests requests
|
||||
var theseResults results
|
||||
sockFd := 3
|
||||
sock := os.NewFile(uintptr(sockFd), "socket connection to parent process")
|
||||
defer sock.Close()
|
||||
encoder := json.NewEncoder(sock)
|
||||
decoder := json.NewDecoder(sock)
|
||||
if err := decoder.Decode(&theseRequests); err != nil {
|
||||
if err := encoder.Encode(results{Err: fmt.Errorf("decoding request: %w", err).Error()}); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if theseRequests.Root != "" {
|
||||
if err := os.Chdir(theseRequests.Root); err != nil {
|
||||
if err := encoder.Encode(results{Err: fmt.Errorf("changing to %q: %w", theseRequests.Root, err).Error()}); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := unix.Chroot(theseRequests.Root); err != nil {
|
||||
if err := encoder.Encode(results{Err: fmt.Errorf("chrooting to %q: %w", theseRequests.Root, err).Error()}); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := os.Chdir("/"); err != nil {
|
||||
if err := encoder.Encode(results{Err: fmt.Errorf("changing to new root: %w", err).Error()}); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if theseRequests.Wd != "" {
|
||||
if err := os.Chdir(theseRequests.Wd); err != nil {
|
||||
if err := encoder.Encode(results{Err: fmt.Errorf("changing to %q in chroot: %w", theseRequests.Wd, err).Error()}); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
var fds []int
|
||||
for _, request := range theseRequests.Open {
|
||||
fd, err := unix.Open(request.Path, request.Mode, request.Perms)
|
||||
thisResult := result{Fd: uintptr(fd)}
|
||||
if err == nil {
|
||||
fds = append(fds, fd)
|
||||
} else {
|
||||
var errno syscall.Errno
|
||||
thisResult.Err = err.Error()
|
||||
if errors.As(err, &errno) {
|
||||
thisResult.Errno = errno
|
||||
}
|
||||
}
|
||||
theseResults.Open = append(theseResults.Open, thisResult)
|
||||
}
|
||||
rights := unix.UnixRights(fds...)
|
||||
inband, err := json.Marshal(&theseResults)
|
||||
if err != nil {
|
||||
if err := encoder.Encode(results{Err: fmt.Errorf("sending response: %w", err).Error()}); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := unix.Sendmsg(sockFd, inband, rights, nil, 0); err != nil {
|
||||
if err := encoder.Encode(results{Err: fmt.Errorf("sending response: %w", err).Error()}); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
//go:build !linux && !freebsd && !darwin
|
||||
|
||||
package open
|
||||
|
||||
func inChroot(requests requests) results {
|
||||
return results{Err: "open-in-chroot not available on this platform"}
|
||||
}
|
|
@ -12,7 +12,9 @@ const (
|
|||
// StageExecutor has ability to mount stages/images in current context and
|
||||
// automatically clean them up.
|
||||
type StageMountDetails struct {
|
||||
DidExecute bool // tells if the stage which is being mounted was freshly executed or was part of older cache
|
||||
IsStage bool // tells if mountpoint returned from stage executor is stage or image
|
||||
MountPoint string // mountpoint of stage/image
|
||||
DidExecute bool // true if this is a freshly-executed stage, or an image, possibly from a non-local cache
|
||||
IsStage bool // true if the mountpoint is a stage's rootfs
|
||||
IsImage bool // true if the mountpoint is an image's rootfs
|
||||
IsAdditionalBuildContext bool // true if the mountpoint is an additional build context
|
||||
MountPoint string // mountpoint of the stage or image's root directory or path of the additional build context
|
||||
}
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
package volumes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/containers/buildah/internal/open"
|
||||
"github.com/containers/storage/pkg/mount"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// bindFromChroot opens "path" inside of "root" using a chrooted subprocess
|
||||
// that returns a descriptor, then creates a uniquely-named temporary directory
|
||||
// or file under "tmp" and bind-mounts the opened descriptor to it, returning
|
||||
// the path of the temporary file or directory. The caller is responsible for
|
||||
// unmounting and removing the temporary.
|
||||
func bindFromChroot(root, path, tmp string) (string, error) {
|
||||
fd, _, err := open.InChroot(root, "", path, unix.O_DIRECTORY|unix.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
if !errors.Is(err, unix.ENOTDIR) {
|
||||
return "", fmt.Errorf("opening directory %q under %q: %w", path, root, err)
|
||||
}
|
||||
fd, _, err = open.InChroot(root, "", path, unix.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("opening non-directory %q under %q: %w", path, root, err)
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
if err := unix.Close(fd); err != nil {
|
||||
logrus.Debugf("closing %q under %q: %v", path, root, err)
|
||||
}
|
||||
}()
|
||||
|
||||
succeeded := false
|
||||
var dest string
|
||||
var destF *os.File
|
||||
defer func() {
|
||||
if !succeeded {
|
||||
if destF != nil {
|
||||
if err := destF.Close(); err != nil {
|
||||
logrus.Debugf("closing bind target %q: %v", dest, err)
|
||||
}
|
||||
}
|
||||
if dest != "" {
|
||||
if err := os.Remove(dest); err != nil {
|
||||
logrus.Debugf("removing bind target %q: %v", dest, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var st unix.Stat_t
|
||||
if err = unix.Fstat(fd, &st); err != nil {
|
||||
return "", fmt.Errorf("checking if %q under %q was a directory: %w", path, root, err)
|
||||
}
|
||||
|
||||
if st.Mode&unix.S_IFDIR == unix.S_IFDIR {
|
||||
if dest, err = os.MkdirTemp(tmp, "bind"); err != nil {
|
||||
return "", fmt.Errorf("creating a bind target directory: %w", err)
|
||||
}
|
||||
} else {
|
||||
if destF, err = os.CreateTemp(tmp, "bind"); err != nil {
|
||||
return "", fmt.Errorf("creating a bind target non-directory: %w", err)
|
||||
}
|
||||
if err := destF.Close(); err != nil {
|
||||
logrus.Debugf("closing bind target %q: %v", dest, err)
|
||||
}
|
||||
dest = destF.Name()
|
||||
}
|
||||
defer func() {
|
||||
if !succeeded {
|
||||
if err := os.Remove(dest); err != nil {
|
||||
logrus.Debugf("removing bind target %q: %v", dest, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := unix.Mount(fmt.Sprintf("/proc/self/fd/%d", fd), dest, "bind", unix.MS_BIND, ""); err != nil {
|
||||
return "", fmt.Errorf("bind-mounting passed-in descriptor to %q: %w", dest, err)
|
||||
}
|
||||
defer func() {
|
||||
if !succeeded {
|
||||
if err := mount.Unmount(dest); err != nil {
|
||||
logrus.Debugf("unmounting bound target %q: %v", dest, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var st2 unix.Stat_t
|
||||
if err = unix.Stat(dest, &st2); err != nil {
|
||||
return "", fmt.Errorf("looking up device/inode of newly-bind-mounted %q: %w", dest, err)
|
||||
}
|
||||
|
||||
if st2.Dev != st.Dev || st2.Ino != st.Ino {
|
||||
return "", fmt.Errorf("device/inode weren't what we expected after bind mounting: %w", err)
|
||||
}
|
||||
|
||||
succeeded = true
|
||||
return dest, nil
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package volumes
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/containers/storage/pkg/mount"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBindFromChroot(t *testing.T) {
|
||||
if os.Getuid() != 0 {
|
||||
t.Skip("not running as root, assuming we can't mount or chroot")
|
||||
}
|
||||
contents1 := "file1"
|
||||
contents2 := "file2"
|
||||
rootdir := t.TempDir()
|
||||
destdir := t.TempDir()
|
||||
require.NoError(t, os.Mkdir(filepath.Join(rootdir, "subdirectory"), 0o700), "creating bind mount source directory")
|
||||
require.NoError(t, os.WriteFile(filepath.Join(rootdir, "subdirectory", "file"), []byte(contents1), 0o600))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(rootdir, "file"), []byte(contents2), 0o600))
|
||||
subdir, err := bindFromChroot(rootdir, "subdirectory", destdir)
|
||||
require.NoError(t, err, "bind mounting from a directory")
|
||||
bytes1, err := os.ReadFile(filepath.Join(subdir, "file"))
|
||||
require.NoError(t, err, "reading file from bind-mounted directory")
|
||||
subfile, err := bindFromChroot(rootdir, "file", destdir)
|
||||
require.NoError(t, err, "bind mounting from a file")
|
||||
bytes2, err := os.ReadFile(subfile)
|
||||
require.NoError(t, err, "reading file from bind mounted file")
|
||||
require.Equal(t, contents1, string(bytes1), "contents of file in bind-mounted directory")
|
||||
require.Equal(t, contents2, string(bytes2), "contents of bind-mounted file")
|
||||
require.NoError(t, mount.Unmount(subdir), "unmounting bind-mounted directory")
|
||||
require.NoError(t, mount.Unmount(subfile), "unmounting bind-mounted file")
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
//go:build !linux
|
||||
|
||||
package volumes
|
||||
|
||||
import "errors"
|
||||
|
||||
// bindFromChroot would open "path" inside of "root" using a chrooted
|
||||
// subprocess that returns a descriptor, then would create a uniquely-named
|
||||
// temporary directory or file under "tmp" and bind-mount the opened descriptor
|
||||
// to it, returning the path of the temporary file or directory. The caller
|
||||
// would be responsible for unmounting and removing the temporary. For now,
|
||||
// this just returns an error because it is not implemented for this platform.
|
||||
func bindFromChroot(root, path, tmp string) (string, error) {
|
||||
return "", errors.New("not available on this system")
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package volumes
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/containers/storage/pkg/reexec"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if reexec.Init() {
|
||||
return
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
|
@ -17,15 +17,19 @@ import (
|
|||
internalParse "github.com/containers/buildah/internal/parse"
|
||||
"github.com/containers/buildah/internal/tmpdir"
|
||||
internalUtil "github.com/containers/buildah/internal/util"
|
||||
"github.com/containers/buildah/pkg/overlay"
|
||||
"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/mount"
|
||||
"github.com/containers/storage/pkg/unshare"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
selinux "github.com/opencontainers/selinux/go-selinux"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -56,18 +60,84 @@ func CacheParent() string {
|
|||
return filepath.Join(tmpdir.GetTempDir(), buildahCacheDir+"-"+strconv.Itoa(unshare.GetRootlessUID()))
|
||||
}
|
||||
|
||||
func mountIsReadWrite(m specs.Mount) bool {
|
||||
// in case of conflicts, the last one wins, so it's not enough
|
||||
// to check for the presence of either "rw" or "ro" anywhere
|
||||
// with e.g. slices.Contains()
|
||||
rw := true
|
||||
for _, option := range m.Options {
|
||||
switch option {
|
||||
case "rw":
|
||||
rw = true
|
||||
case "ro":
|
||||
rw = false
|
||||
}
|
||||
}
|
||||
return rw
|
||||
}
|
||||
|
||||
func convertToOverlay(m specs.Mount, store storage.Store, mountLabel, tmpDir string, uid, gid int) (specs.Mount, string, error) {
|
||||
overlayDir, err := overlay.TempDir(tmpDir, uid, gid)
|
||||
if err != nil {
|
||||
return specs.Mount{}, "", fmt.Errorf("setting up overlay for %q: %w", m.Destination, err)
|
||||
}
|
||||
options := overlay.Options{GraphOpts: slices.Clone(store.GraphOptions()), ForceMount: true, MountLabel: mountLabel}
|
||||
fileInfo, err := os.Stat(m.Source)
|
||||
if err != nil {
|
||||
return specs.Mount{}, "", fmt.Errorf("setting up overlay of %q: %w", m.Source, err)
|
||||
}
|
||||
// we might be trying to "overlay" for a non-directory, and the kernel doesn't like that very much
|
||||
var mountThisInstead specs.Mount
|
||||
if fileInfo.IsDir() {
|
||||
// do the normal thing of mounting this directory as a lower with a temporary upper
|
||||
mountThisInstead, err = overlay.MountWithOptions(overlayDir, m.Source, m.Destination, &options)
|
||||
if err != nil {
|
||||
return specs.Mount{}, "", fmt.Errorf("setting up overlay of %q: %w", m.Source, err)
|
||||
}
|
||||
} else {
|
||||
// mount the parent directory as the lower with a temporary upper, and return a
|
||||
// bind mount from the non-directory in the merged directory to the destination
|
||||
sourceDir := filepath.Dir(m.Source)
|
||||
sourceBase := filepath.Base(m.Source)
|
||||
destination := m.Destination
|
||||
mountedOverlay, err := overlay.MountWithOptions(overlayDir, sourceDir, destination, &options)
|
||||
if err != nil {
|
||||
return specs.Mount{}, "", fmt.Errorf("setting up overlay of %q: %w", sourceDir, err)
|
||||
}
|
||||
if mountedOverlay.Type != define.TypeBind {
|
||||
if err2 := overlay.RemoveTemp(overlayDir); err2 != nil {
|
||||
return specs.Mount{}, "", fmt.Errorf("cleaning up after failing to set up overlay: %v, while setting up overlay for %q: %w", err2, destination, err)
|
||||
}
|
||||
return specs.Mount{}, "", fmt.Errorf("setting up overlay for %q at %q: %w", mountedOverlay.Source, destination, err)
|
||||
}
|
||||
mountThisInstead = mountedOverlay
|
||||
mountThisInstead.Source = filepath.Join(mountedOverlay.Source, sourceBase)
|
||||
mountThisInstead.Destination = destination
|
||||
}
|
||||
return mountThisInstead, overlayDir, nil
|
||||
}
|
||||
|
||||
// FIXME: this code needs to be merged with pkg/parse/parse.go ValidateVolumeOpts
|
||||
//
|
||||
// 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) {
|
||||
//
|
||||
// Returns a Mount to add to the runtime spec's list of mounts, the ID of the
|
||||
// image we mounted if we mounted one, the path of a mounted location if one
|
||||
// needs to be unmounted and removed, and the path of an overlay mount if one
|
||||
// needs to be cleaned up, or an error.
|
||||
//
|
||||
// The caller is expected to, after the command which uses the mount exits,
|
||||
// clean up the overlay filesystem (if we provided a path to it), unmount and
|
||||
// remove the mountpoint for the mounted filesystem (if we provided the path to
|
||||
// its mountpoint), and then unmount the image (if we mounted one).
|
||||
func GetBindMount(sys *types.SystemContext, args []string, contextDir string, store storage.Store, mountLabel string, additionalMountPoints map[string]internal.StageMountDetails, workDir, tmpDir string) (specs.Mount, string, string, string, error) {
|
||||
newMount := specs.Mount{
|
||||
Type: define.TypeBind,
|
||||
}
|
||||
|
||||
setRelabel := false
|
||||
mountReadability := false
|
||||
setDest := false
|
||||
setRelabel := ""
|
||||
mountReadability := ""
|
||||
setDest := ""
|
||||
bindNonRecursive := false
|
||||
fromImage := ""
|
||||
|
||||
|
@ -80,86 +150,85 @@ func GetBindMount(ctx *types.SystemContext, args []string, contextDir string, st
|
|||
case "bind-nonrecursive":
|
||||
newMount.Options = append(newMount.Options, "bind")
|
||||
bindNonRecursive = true
|
||||
case "ro", "nosuid", "nodev", "noexec":
|
||||
case "nosuid", "nodev", "noexec":
|
||||
// TODO: detect duplication of these options.
|
||||
// (Is this necessary?)
|
||||
newMount.Options = append(newMount.Options, argName)
|
||||
mountReadability = true
|
||||
case "rw", "readwrite":
|
||||
newMount.Options = append(newMount.Options, "rw")
|
||||
mountReadability = true
|
||||
case "readonly":
|
||||
// Alias for "ro"
|
||||
mountReadability = "rw"
|
||||
case "ro", "readonly":
|
||||
newMount.Options = append(newMount.Options, "ro")
|
||||
mountReadability = true
|
||||
mountReadability = "ro"
|
||||
case "shared", "rshared", "private", "rprivate", "slave", "rslave", "Z", "z", "U", "no-dereference":
|
||||
if hasArgValue {
|
||||
return newMount, "", fmt.Errorf("%v: %w", val, errBadOptionArg)
|
||||
return newMount, "", "", "", fmt.Errorf("%v: %w", val, errBadOptionArg)
|
||||
}
|
||||
newMount.Options = append(newMount.Options, argName)
|
||||
case "from":
|
||||
if !hasArgValue {
|
||||
return newMount, "", fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
return newMount, "", "", "", fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
}
|
||||
fromImage = argValue
|
||||
case "bind-propagation":
|
||||
if !hasArgValue {
|
||||
return newMount, "", fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
return newMount, "", "", "", fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
}
|
||||
switch argValue {
|
||||
default:
|
||||
return newMount, "", fmt.Errorf("%v: %q: %w", argName, argValue, errBadMntOption)
|
||||
return newMount, "", "", "", fmt.Errorf("%v: %q: %w", argName, argValue, errBadMntOption)
|
||||
case "shared", "rshared", "private", "rprivate", "slave", "rslave":
|
||||
// this should be the relevant parts of the same list of options we accepted above
|
||||
}
|
||||
newMount.Options = append(newMount.Options, argValue)
|
||||
case "src", "source":
|
||||
if !hasArgValue {
|
||||
return newMount, "", fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
return newMount, "", "", "", fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
}
|
||||
newMount.Source = argValue
|
||||
case "target", "dst", "destination":
|
||||
if !hasArgValue {
|
||||
return newMount, "", fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
return newMount, "", "", "", fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
}
|
||||
targetPath := argValue
|
||||
setDest = targetPath
|
||||
if !path.IsAbs(targetPath) {
|
||||
targetPath = filepath.Join(workDir, targetPath)
|
||||
}
|
||||
if err := parse.ValidateVolumeCtrDir(targetPath); err != nil {
|
||||
return newMount, "", err
|
||||
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)
|
||||
if setRelabel != "" {
|
||||
return newMount, "", "", "", fmt.Errorf("cannot pass 'relabel' option more than once: %w", errBadOptionArg)
|
||||
}
|
||||
setRelabel = true
|
||||
setRelabel = argValue
|
||||
switch argValue {
|
||||
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", argName, errBadMntOption)
|
||||
return newMount, "", "", "", fmt.Errorf("%s mount option must be 'private' or 'shared': %w", argName, 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", argName, errBadMntOption)
|
||||
return newMount, "", "", "", fmt.Errorf("%v: %w", argName, errBadMntOption)
|
||||
}
|
||||
}
|
||||
|
||||
// default mount readability is always readonly
|
||||
if !mountReadability {
|
||||
if mountReadability == "" {
|
||||
newMount.Options = append(newMount.Options, "ro")
|
||||
}
|
||||
|
||||
// Following variable ensures that we return imagename only if we did additional mount
|
||||
isImageMounted := false
|
||||
succeeded := false
|
||||
mountedImage := ""
|
||||
if fromImage != "" {
|
||||
mountPoint := ""
|
||||
if additionalMountPoints != nil {
|
||||
|
@ -170,16 +239,23 @@ func GetBindMount(ctx *types.SystemContext, args []string, contextDir string, st
|
|||
// 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)
|
||||
image, err := internalUtil.LookupImage(sys, store, fromImage)
|
||||
if err != nil {
|
||||
return newMount, "", err
|
||||
return newMount, "", "", "", err
|
||||
}
|
||||
|
||||
mountPoint, err = image.Mount(context.Background(), nil, imageMountLabel)
|
||||
mountPoint, err = image.Mount(context.Background(), nil, mountLabel)
|
||||
if err != nil {
|
||||
return newMount, "", err
|
||||
return newMount, "", "", "", err
|
||||
}
|
||||
isImageMounted = true
|
||||
mountedImage = image.ID()
|
||||
defer func() {
|
||||
if !succeeded {
|
||||
if _, err := store.UnmountImage(mountedImage, false); err != nil {
|
||||
logrus.Debugf("unmounting bind-mounted image %q: %v", fromImage, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
contextDir = mountPoint
|
||||
}
|
||||
|
@ -190,48 +266,73 @@ func GetBindMount(ctx *types.SystemContext, args []string, contextDir string, st
|
|||
newMount.Options = append(newMount.Options, "rbind")
|
||||
}
|
||||
|
||||
if !setDest {
|
||||
return newMount, fromImage, errBadVolDest
|
||||
if setDest == "" {
|
||||
return newMount, "", "", "", errBadVolDest
|
||||
}
|
||||
|
||||
// buildkit parity: support absolute path for sources from current build context
|
||||
if contextDir != "" {
|
||||
// path should be /contextDir/specified path
|
||||
evaluated, err := copier.Eval(contextDir, newMount.Source, copier.EvalOptions{})
|
||||
evaluated, err := copier.Eval(contextDir, contextDir+string(filepath.Separator)+newMount.Source, copier.EvalOptions{})
|
||||
if err != nil {
|
||||
return newMount, "", err
|
||||
return newMount, "", "", "", err
|
||||
}
|
||||
newMount.Source = evaluated
|
||||
} 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
|
||||
return newMount, "", "", "", errBadVolSrc
|
||||
}
|
||||
if err := parse.ValidateVolumeHostDir(newMount.Source); err != nil {
|
||||
return newMount, "", err
|
||||
return newMount, "", "", "", err
|
||||
}
|
||||
}
|
||||
|
||||
opts, err := parse.ValidateVolumeOpts(newMount.Options)
|
||||
if err != nil {
|
||||
return newMount, fromImage, err
|
||||
return newMount, "", "", "", err
|
||||
}
|
||||
newMount.Options = opts
|
||||
|
||||
if !isImageMounted {
|
||||
// we don't want any cleanups if image was not mounted explicitly
|
||||
// so dont return anything
|
||||
fromImage = ""
|
||||
var intermediateMount string
|
||||
if contextDir != "" && newMount.Source != contextDir {
|
||||
rel, err := filepath.Rel(contextDir, newMount.Source)
|
||||
if err != nil {
|
||||
return newMount, "", "", "", fmt.Errorf("computing pathname of bind subdirectory: %w", err)
|
||||
}
|
||||
if rel != "." && rel != "/" {
|
||||
mnt, err := bindFromChroot(contextDir, rel, tmpDir)
|
||||
if err != nil {
|
||||
return newMount, "", "", "", fmt.Errorf("sanitizing bind subdirectory %q: %w", newMount.Source, err)
|
||||
}
|
||||
logrus.Debugf("bind-mounted %q under %q to %q", rel, contextDir, mnt)
|
||||
intermediateMount = mnt
|
||||
newMount.Source = intermediateMount
|
||||
}
|
||||
}
|
||||
|
||||
return newMount, fromImage, nil
|
||||
overlayDir := ""
|
||||
if mountedImage != "" || mountIsReadWrite(newMount) {
|
||||
if newMount, overlayDir, err = convertToOverlay(newMount, store, mountLabel, tmpDir, 0, 0); err != nil {
|
||||
return newMount, "", "", "", err
|
||||
}
|
||||
}
|
||||
|
||||
succeeded = true
|
||||
return newMount, mountedImage, intermediateMount, overlayDir, 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) {
|
||||
// Returns a Mount to add to the runtime spec's list of mounts, the path of a
|
||||
// mounted filesystem if one needs to be unmounted, and an optional lock that
|
||||
// needs to be released, or an error.
|
||||
//
|
||||
// The caller is expected to, after the command which uses the mount exits,
|
||||
// unmount and remove the mountpoint of the mounted filesystem (if we provided
|
||||
// the path to its mountpoint) and release the lock (if we took one).
|
||||
func GetCacheMount(args []string, additionalMountPoints map[string]internal.StageMountDetails, workDir, tmpDir string) (specs.Mount, string, *lockfile.LockFile, error) {
|
||||
var err error
|
||||
var mode uint64
|
||||
var buildahLockFilesDir string
|
||||
|
@ -282,69 +383,69 @@ func GetCacheMount(args []string, store storage.Store, imageMountLabel string, a
|
|||
sharing = argValue
|
||||
case "bind-propagation":
|
||||
if !hasArgValue {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
return newMount, "", nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
}
|
||||
switch argValue {
|
||||
default:
|
||||
return newMount, nil, fmt.Errorf("%v: %q: %w", argName, argValue, errBadMntOption)
|
||||
return newMount, "", nil, fmt.Errorf("%v: %q: %w", argName, argValue, errBadMntOption)
|
||||
case "shared", "rshared", "private", "rprivate", "slave", "rslave":
|
||||
// this should be the relevant parts of the same list of options we accepted above
|
||||
}
|
||||
newMount.Options = append(newMount.Options, argValue)
|
||||
case "id":
|
||||
if !hasArgValue {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
return newMount, "", nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
}
|
||||
id = argValue
|
||||
case "from":
|
||||
if !hasArgValue {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
return newMount, "", nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
}
|
||||
fromStage = argValue
|
||||
case "target", "dst", "destination":
|
||||
if !hasArgValue {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
return newMount, "", nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
}
|
||||
targetPath := argValue
|
||||
if !path.IsAbs(targetPath) {
|
||||
targetPath = filepath.Join(workDir, targetPath)
|
||||
}
|
||||
if err := parse.ValidateVolumeCtrDir(targetPath); err != nil {
|
||||
return newMount, nil, err
|
||||
return newMount, "", nil, err
|
||||
}
|
||||
newMount.Destination = targetPath
|
||||
setDest = true
|
||||
case "src", "source":
|
||||
if !hasArgValue {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
return newMount, "", nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
}
|
||||
newMount.Source = argValue
|
||||
case "mode":
|
||||
if !hasArgValue {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
return newMount, "", nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
}
|
||||
mode, err = strconv.ParseUint(argValue, 8, 32)
|
||||
if err != nil {
|
||||
return newMount, nil, fmt.Errorf("unable to parse cache mode: %w", err)
|
||||
return newMount, "", nil, fmt.Errorf("unable to parse cache mode: %w", err)
|
||||
}
|
||||
case "uid":
|
||||
if !hasArgValue {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
return newMount, "", nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
}
|
||||
uid, err = strconv.Atoi(argValue)
|
||||
if err != nil {
|
||||
return newMount, nil, fmt.Errorf("unable to parse cache uid: %w", err)
|
||||
return newMount, "", nil, fmt.Errorf("unable to parse cache uid: %w", err)
|
||||
}
|
||||
case "gid":
|
||||
if !hasArgValue {
|
||||
return newMount, nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
return newMount, "", nil, fmt.Errorf("%v: %w", argName, errBadOptionArg)
|
||||
}
|
||||
gid, err = strconv.Atoi(argValue)
|
||||
if err != nil {
|
||||
return newMount, nil, fmt.Errorf("unable to parse cache gid: %w", err)
|
||||
return newMount, "", nil, fmt.Errorf("unable to parse cache gid: %w", err)
|
||||
}
|
||||
default:
|
||||
return newMount, nil, fmt.Errorf("%v: %w", argName, errBadMntOption)
|
||||
return newMount, "", nil, fmt.Errorf("%v: %w", argName, errBadMntOption)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -355,16 +456,18 @@ func GetCacheMount(args []string, store storage.Store, imageMountLabel string, a
|
|||
}
|
||||
|
||||
if !setDest {
|
||||
return newMount, nil, errBadVolDest
|
||||
return newMount, "", nil, errBadVolDest
|
||||
}
|
||||
|
||||
thisCacheRoot := ""
|
||||
if fromStage != "" {
|
||||
// do not create cache on host
|
||||
// instead use read-only mounted stage as cache
|
||||
// do not create and use a cache directory on the host,
|
||||
// instead use the location in the mounted stage or
|
||||
// temporary directory as the cache
|
||||
mountPoint := ""
|
||||
if additionalMountPoints != nil {
|
||||
if val, ok := additionalMountPoints[fromStage]; ok {
|
||||
if val.IsStage {
|
||||
if !val.IsImage {
|
||||
mountPoint = val.MountPoint
|
||||
}
|
||||
}
|
||||
|
@ -372,14 +475,9 @@ func GetCacheMount(args []string, store storage.Store, imageMountLabel string, a
|
|||
// 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)
|
||||
return newMount, "", nil, fmt.Errorf("no stage or additional build context found with name %s", fromStage)
|
||||
}
|
||||
// path should be /contextDir/specified path
|
||||
evaluated, err := copier.Eval(mountPoint, string(filepath.Separator)+newMount.Source, copier.EvalOptions{})
|
||||
if err != nil {
|
||||
return newMount, nil, err
|
||||
}
|
||||
newMount.Source = evaluated
|
||||
thisCacheRoot = mountPoint
|
||||
} else {
|
||||
// we need to create cache on host if no image is being used
|
||||
|
||||
|
@ -389,64 +487,73 @@ func GetCacheMount(args []string, store storage.Store, imageMountLabel string, a
|
|||
|
||||
// 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)
|
||||
return newMount, "", nil, fmt.Errorf("unable to create build cache directory: %w", err)
|
||||
}
|
||||
|
||||
if id != "" {
|
||||
// Don't let the user control where we place the directory.
|
||||
dirID := digest.FromString(id).Encoded()[:16]
|
||||
newMount.Source = filepath.Join(cacheParent, dirID)
|
||||
thisCacheRoot = filepath.Join(cacheParent, dirID)
|
||||
buildahLockFilesDir = filepath.Join(BuildahCacheLockfileDir, dirID)
|
||||
} else {
|
||||
// Don't let the user control where we place the directory.
|
||||
dirID := digest.FromString(newMount.Destination).Encoded()[:16]
|
||||
newMount.Source = filepath.Join(cacheParent, dirID)
|
||||
thisCacheRoot = filepath.Join(cacheParent, dirID)
|
||||
buildahLockFilesDir = filepath.Join(BuildahCacheLockfileDir, dirID)
|
||||
}
|
||||
|
||||
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)
|
||||
// buildkit parity: change uid and gid if specified, otherwise keep `0`
|
||||
err = idtools.MkdirAllAndChownNew(thisCacheRoot, os.FileMode(mode), idPair)
|
||||
if err != nil {
|
||||
return newMount, nil, fmt.Errorf("unable to change uid,gid of cache directory: %w", err)
|
||||
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)
|
||||
return newMount, "", nil, fmt.Errorf("unable to create build cache lockfiles directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var targetLock *lockfile.LockFile // = nil
|
||||
// path should be /mountPoint/specified path
|
||||
evaluated, err := copier.Eval(thisCacheRoot, thisCacheRoot+string(filepath.Separator)+newMount.Source, copier.EvalOptions{})
|
||||
if err != nil {
|
||||
return newMount, "", nil, err
|
||||
}
|
||||
newMount.Source = evaluated
|
||||
|
||||
succeeded := false
|
||||
defer func() {
|
||||
if !succeeded && targetLock != nil {
|
||||
targetLock.Unlock()
|
||||
}
|
||||
}()
|
||||
var targetLock *lockfile.LockFile
|
||||
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)
|
||||
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
|
||||
defer func() {
|
||||
if !succeeded {
|
||||
targetLock.Unlock()
|
||||
}
|
||||
}()
|
||||
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)
|
||||
return newMount, "", nil, fmt.Errorf("unrecognized value %q for field `sharing`: %w", sharing, err)
|
||||
}
|
||||
|
||||
// buildkit parity: default sharing should be shared
|
||||
|
@ -464,12 +571,29 @@ func GetCacheMount(args []string, store storage.Store, imageMountLabel string, a
|
|||
|
||||
opts, err := parse.ValidateVolumeOpts(newMount.Options)
|
||||
if err != nil {
|
||||
return newMount, nil, err
|
||||
return newMount, "", nil, err
|
||||
}
|
||||
newMount.Options = opts
|
||||
|
||||
var intermediateMount string
|
||||
if newMount.Source != thisCacheRoot {
|
||||
rel, err := filepath.Rel(thisCacheRoot, newMount.Source)
|
||||
if err != nil {
|
||||
return newMount, "", nil, fmt.Errorf("computing pathname of cache subdirectory: %w", err)
|
||||
}
|
||||
if rel != "." && rel != "/" {
|
||||
mnt, err := bindFromChroot(thisCacheRoot, rel, tmpDir)
|
||||
if err != nil {
|
||||
return newMount, "", nil, fmt.Errorf("sanitizing cache subdirectory %q: %w", newMount.Source, err)
|
||||
}
|
||||
logrus.Debugf("bind-mounted %q under %q to %q", rel, thisCacheRoot, mnt)
|
||||
intermediateMount = mnt
|
||||
newMount.Source = intermediateMount
|
||||
}
|
||||
}
|
||||
|
||||
succeeded = true
|
||||
return newMount, targetLock, nil
|
||||
return newMount, intermediateMount, targetLock, nil
|
||||
}
|
||||
|
||||
func getVolumeMounts(volumes []string) (map[string]specs.Mount, error) {
|
||||
|
@ -495,27 +619,53 @@ func UnlockLockArray(locks []*lockfile.LockFile) {
|
|||
}
|
||||
}
|
||||
|
||||
// GetVolumes gets the volumes from --volume and --mount
|
||||
// GetVolumes gets the volumes from --volume and --mount flags.
|
||||
//
|
||||
// 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)
|
||||
// Returns a slice of Mounts to add to the runtime spec's list of mounts, the
|
||||
// IDs of any images we mounted, a slice of bind-mounted paths, a slice of
|
||||
// overlay directories and a slice of locks that we acquired, or an error.
|
||||
//
|
||||
// The caller is expected to, after the command which uses the mounts and
|
||||
// volumes exits, clean up the overlay directories, unmount and remove the
|
||||
// mountpoints for the bind-mounted paths, unmount any images we mounted, and
|
||||
// release the locks we returned (either using UnlockLockArray() or by
|
||||
// iterating over them and unlocking them).
|
||||
func GetVolumes(ctx *types.SystemContext, store storage.Store, mountLabel string, volumes []string, mounts []string, contextDir, workDir, tmpDir string) ([]specs.Mount, []string, []string, []string, []*lockfile.LockFile, error) {
|
||||
unifiedMounts, mountedImages, intermediateMounts, overlayMounts, targetLocks, err := getMounts(ctx, store, mountLabel, mounts, contextDir, workDir, tmpDir)
|
||||
if err != nil {
|
||||
return nil, mountedImages, nil, err
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
succeeded := false
|
||||
defer func() {
|
||||
if !succeeded {
|
||||
for _, overlayMount := range overlayMounts {
|
||||
if err := overlay.RemoveTemp(overlayMount); err != nil {
|
||||
logrus.Debugf("unmounting overlay at %q: %v", overlayMount, err)
|
||||
}
|
||||
}
|
||||
for _, intermediateMount := range intermediateMounts {
|
||||
if err := mount.Unmount(intermediateMount); err != nil {
|
||||
logrus.Debugf("unmounting intermediate mount point %q: %v", intermediateMount, err)
|
||||
}
|
||||
if err := os.Remove(intermediateMount); err != nil {
|
||||
logrus.Debugf("removing should-be-empty directory %q: %v", intermediateMount, err)
|
||||
}
|
||||
}
|
||||
for _, image := range mountedImages {
|
||||
if _, err := store.UnmountImage(image, false); err != nil {
|
||||
logrus.Debugf("unmounting image %q: %v", image, err)
|
||||
}
|
||||
}
|
||||
UnlockLockArray(targetLocks)
|
||||
}
|
||||
}()
|
||||
volumeMounts, err := getVolumeMounts(volumes)
|
||||
if err != nil {
|
||||
return nil, mountedImages, nil, err
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
for dest, mount := range volumeMounts {
|
||||
if _, ok := unifiedMounts[dest]; ok {
|
||||
return nil, mountedImages, nil, fmt.Errorf("%v: %w", dest, errDuplicateDest)
|
||||
return nil, nil, nil, nil, nil, fmt.Errorf("%v: %w", dest, errDuplicateDest)
|
||||
}
|
||||
unifiedMounts[dest] = mount
|
||||
}
|
||||
|
@ -525,24 +675,53 @@ func GetVolumes(ctx *types.SystemContext, store storage.Store, volumes []string,
|
|||
finalMounts = append(finalMounts, mount)
|
||||
}
|
||||
succeeded = true
|
||||
return finalMounts, mountedImages, targetLocks, nil
|
||||
return finalMounts, mountedImages, intermediateMounts, overlayMounts, 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 ...
|
||||
// getMounts takes user-provided inputs from the --mount flag and returns a
|
||||
// slice of OCI spec mounts, a slice of mounted image IDs, a slice of other
|
||||
// mount locations, a slice of overlay mounts, and a slice of locks, or an
|
||||
// error.
|
||||
//
|
||||
// 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) {
|
||||
// buildah run --mount type=bind,src=/etc/resolv.conf,target=/etc/resolv.conf ...
|
||||
// buildah run --mount type=cache,target=/var/cache ...
|
||||
// buildah run --mount type=tmpfs,target=/dev/shm ...
|
||||
//
|
||||
// The caller is expected to, after the command which uses the mounts exits,
|
||||
// unmount the overlay filesystems (if we mounted any), unmount the other
|
||||
// mounted filesystems and remove their mountpoints (if we provided any paths
|
||||
// to mountpoints), unmount any mounted images (if we provided the IDs of any),
|
||||
// and then unlock the locks we returned (either using UnlockLockArray() or by
|
||||
// iterating over them and unlocking them).
|
||||
func getMounts(ctx *types.SystemContext, store storage.Store, mountLabel string, mounts []string, contextDir, workDir, tmpDir string) (map[string]specs.Mount, []string, []string, []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)
|
||||
finalMounts := make(map[string]specs.Mount, len(mounts))
|
||||
mountedImages := make([]string, 0, len(mounts))
|
||||
intermediateMounts := make([]string, 0, len(mounts))
|
||||
overlayMounts := make([]string, 0, len(mounts))
|
||||
targetLocks := make([]*lockfile.LockFile, 0, len(mounts))
|
||||
succeeded := false
|
||||
defer func() {
|
||||
if !succeeded {
|
||||
for _, overlayDir := range overlayMounts {
|
||||
if err := overlay.RemoveTemp(overlayDir); err != nil {
|
||||
logrus.Debugf("unmounting overlay mount at %q: %v", overlayDir, err)
|
||||
}
|
||||
}
|
||||
for _, intermediateMount := range intermediateMounts {
|
||||
if err := mount.Unmount(intermediateMount); err != nil {
|
||||
logrus.Debugf("unmounting intermediate mount point %q: %v", intermediateMount, err)
|
||||
}
|
||||
if err := os.Remove(intermediateMount); err != nil {
|
||||
logrus.Debugf("removing should-be-empty directory %q: %v", intermediateMount, err)
|
||||
}
|
||||
}
|
||||
for _, image := range mountedImages {
|
||||
if _, err := store.UnmountImage(image, false); err != nil {
|
||||
logrus.Debugf("unmounting image %q: %v", image, err)
|
||||
}
|
||||
}
|
||||
UnlockLockArray(targetLocks)
|
||||
}
|
||||
}()
|
||||
|
@ -555,56 +734,67 @@ func getMounts(ctx *types.SystemContext, store storage.Store, mounts []string, c
|
|||
for _, mount := range mounts {
|
||||
tokens := strings.Split(mount, ",")
|
||||
if len(tokens) < 2 {
|
||||
return nil, mountedImages, nil, fmt.Errorf("%q: %w", mount, errInvalidSyntax)
|
||||
return nil, nil, nil, nil, 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)
|
||||
return nil, nil, nil, nil, 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)
|
||||
mount, image, intermediateMount, overlayMount, err := GetBindMount(ctx, tokens, contextDir, store, mountLabel, nil, workDir, tmpDir)
|
||||
if err != nil {
|
||||
return nil, mountedImages, nil, err
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
if image != "" {
|
||||
mountedImages = append(mountedImages, image)
|
||||
}
|
||||
if intermediateMount != "" {
|
||||
intermediateMounts = append(intermediateMounts, intermediateMount)
|
||||
}
|
||||
if overlayMount != "" {
|
||||
overlayMounts = append(overlayMounts, overlayMount)
|
||||
}
|
||||
if _, ok := finalMounts[mount.Destination]; ok {
|
||||
return nil, mountedImages, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest)
|
||||
return nil, nil, nil, nil, 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)
|
||||
mount, intermediateMount, tl, err := GetCacheMount(tokens, nil, workDir, tmpDir)
|
||||
if err != nil {
|
||||
return nil, mountedImages, nil, err
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
if intermediateMount != "" {
|
||||
intermediateMounts = append(intermediateMounts, intermediateMount)
|
||||
}
|
||||
if tl != nil {
|
||||
targetLocks = append(targetLocks, tl)
|
||||
}
|
||||
if _, ok := finalMounts[mount.Destination]; ok {
|
||||
return nil, mountedImages, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest)
|
||||
return nil, nil, nil, nil, 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
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
if _, ok := finalMounts[mount.Destination]; ok {
|
||||
return nil, mountedImages, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest)
|
||||
return nil, nil, nil, nil, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest)
|
||||
}
|
||||
finalMounts[mount.Destination] = mount
|
||||
default:
|
||||
return nil, mountedImages, nil, fmt.Errorf("invalid filesystem type %q", mountType)
|
||||
return nil, nil, nil, nil, nil, fmt.Errorf("invalid filesystem type %q", mountType)
|
||||
}
|
||||
}
|
||||
|
||||
succeeded = true
|
||||
return finalMounts, mountedImages, targetLocks, nil
|
||||
return finalMounts, mountedImages, intermediateMounts, overlayMounts, targetLocks, nil
|
||||
}
|
||||
|
||||
// GetTmpfsMount parses a single tmpfs mount entry from the --mount flag
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"errors"
|
||||
|
||||
"github.com/containers/storage/pkg/idtools"
|
||||
"github.com/containers/storage/pkg/mount"
|
||||
"github.com/containers/storage/pkg/system"
|
||||
"github.com/containers/storage/pkg/unshare"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
|
@ -49,6 +50,12 @@ type Options struct {
|
|||
RootUID int
|
||||
// RootGID is not used yet but keeping it here for legacy reasons.
|
||||
RootGID int
|
||||
// Force overlay mounting and return a bind mount, rather than
|
||||
// attempting to optimize by having the runtime actually mount and
|
||||
// manage the overlay filesystem.
|
||||
ForceMount bool
|
||||
// MountLabel is a label to force for the overlay filesystem.
|
||||
MountLabel string
|
||||
}
|
||||
|
||||
// TempDir generates an overlay Temp directory in the container content
|
||||
|
@ -145,6 +152,12 @@ func mountWithMountProgram(mountProgram, overlayOptions, mergeDir string) error
|
|||
return nil
|
||||
}
|
||||
|
||||
// mountNatively mounts an overlay at mergeDir using the kernel's mount()
|
||||
// system call.
|
||||
func mountNatively(overlayOptions, mergeDir string) error {
|
||||
return mount.Mount("overlay", mergeDir, "overlay", overlayOptions)
|
||||
}
|
||||
|
||||
// Convert ":" to "\:", the path which will be overlay mounted need to be escaped
|
||||
func escapeColon(source string) string {
|
||||
return strings.ReplaceAll(source, ":", "\\:")
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"github.com/containers/storage/pkg/unshare"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/opencontainers/selinux/go-selinux/label"
|
||||
)
|
||||
|
||||
// MountWithOptions creates a subdir of the contentDir based on the source directory
|
||||
|
@ -55,6 +56,9 @@ func MountWithOptions(contentDir, source, dest string, opts *Options) (mount spe
|
|||
}
|
||||
overlayOptions = fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s,private", escapeColon(source), upperDir, workDir)
|
||||
}
|
||||
if opts.MountLabel != "" {
|
||||
overlayOptions = overlayOptions + "," + label.FormatMountLabel("", opts.MountLabel)
|
||||
}
|
||||
|
||||
mountProgram := findMountProgram(opts.GraphOpts)
|
||||
if mountProgram != "" {
|
||||
|
@ -79,5 +83,17 @@ func MountWithOptions(contentDir, source, dest string, opts *Options) (mount spe
|
|||
mount.Type = "overlay"
|
||||
mount.Options = strings.Split(overlayOptions, ",")
|
||||
|
||||
if opts.ForceMount {
|
||||
if err := mountNatively(overlayOptions, mergeDir); err != nil {
|
||||
return mount, err
|
||||
}
|
||||
|
||||
mount.Source = mergeDir
|
||||
mount.Destination = dest
|
||||
mount.Type = "bind"
|
||||
mount.Options = []string{"bind", "slave"}
|
||||
return mount, nil
|
||||
}
|
||||
|
||||
return mount, nil
|
||||
}
|
||||
|
|
12
run.go
12
run.go
|
@ -180,18 +180,22 @@ type RunOptions struct {
|
|||
|
||||
// RunMountArtifacts are the artifacts created when using a run mount.
|
||||
type runMountArtifacts struct {
|
||||
// RunMountTargets are the run mount targets inside the container
|
||||
// RunMountTargets are the run mount targets inside the container which should be removed
|
||||
RunMountTargets []string
|
||||
// RunOverlayDirs are overlay directories which will need to be cleaned up using overlay.RemoveTemp()
|
||||
RunOverlayDirs []string
|
||||
// TmpFiles are artifacts that need to be removed outside the container
|
||||
TmpFiles []string
|
||||
// Any external images which were mounted inside container
|
||||
// Any images which were mounted, which should be unmounted
|
||||
MountedImages []string
|
||||
// Agents are the ssh agents started
|
||||
// Agents are the ssh agents started, which should have their Shutdown() methods called
|
||||
Agents []*sshagent.AgentServer
|
||||
// SSHAuthSock is the path to the ssh auth sock inside the container
|
||||
SSHAuthSock string
|
||||
// TargetLocks to be unlocked if there are any.
|
||||
// Lock files, which should have their Unlock() methods called
|
||||
TargetLocks []*lockfile.LockFile
|
||||
// Intermediate mount points, which should be Unmount()ed and Removed()d
|
||||
IntermediateMounts []string
|
||||
}
|
||||
|
||||
// RunMountInfo are the available run mounts for this run
|
||||
|
|
174
run_common.go
174
run_common.go
|
@ -27,7 +27,6 @@ import (
|
|||
"github.com/containers/buildah/copier"
|
||||
"github.com/containers/buildah/define"
|
||||
"github.com/containers/buildah/internal"
|
||||
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"
|
||||
|
@ -40,15 +39,14 @@ import (
|
|||
"github.com/containers/common/pkg/config"
|
||||
"github.com/containers/common/pkg/subscriptions"
|
||||
"github.com/containers/image/v5/types"
|
||||
imageTypes "github.com/containers/image/v5/types"
|
||||
"github.com/containers/storage"
|
||||
"github.com/containers/storage/pkg/fileutils"
|
||||
"github.com/containers/storage/pkg/idtools"
|
||||
"github.com/containers/storage/pkg/ioutils"
|
||||
"github.com/containers/storage/pkg/lockfile"
|
||||
"github.com/containers/storage/pkg/mount"
|
||||
"github.com/containers/storage/pkg/reexec"
|
||||
"github.com/containers/storage/pkg/unshare"
|
||||
storageTypes "github.com/containers/storage/types"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/opencontainers/runtime-tools/generate"
|
||||
|
@ -1304,7 +1302,9 @@ func init() {
|
|||
reexec.Register(runUsingRuntimeCommand, runUsingRuntimeMain)
|
||||
}
|
||||
|
||||
// If this succeeds, the caller must call cleanupMounts().
|
||||
// If this succeeds, after the command which uses the spec finishes running,
|
||||
// the caller must call b.cleanupRunMounts() on the returned runMountArtifacts
|
||||
// structure.
|
||||
func (b *Builder) setupMounts(mountPoint string, spec *specs.Spec, bundlePath string, optionMounts []specs.Mount, bindFiles map[string]string, builtinVolumes []string, compatBuiltinVolumes types.OptionalBool, volumeMounts []string, runFileMounts []string, runMountInfo runMountInfo) (*runMountArtifacts, error) {
|
||||
// Start building a new list of mounts.
|
||||
var mounts []specs.Mount
|
||||
|
@ -1363,14 +1363,16 @@ func (b *Builder) setupMounts(mountPoint string, spec *specs.Spec, bundlePath st
|
|||
processGID: int(processGID),
|
||||
}
|
||||
// Get the list of mounts that are just for this Run() call.
|
||||
runMounts, mountArtifacts, err := b.runSetupRunMounts(mountPoint, runFileMounts, runMountInfo, idMaps)
|
||||
runMounts, mountArtifacts, err := b.runSetupRunMounts(mountPoint, bundlePath, runFileMounts, runMountInfo, idMaps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
succeeded := false
|
||||
defer func() {
|
||||
if !succeeded {
|
||||
volumes.UnlockLockArray(mountArtifacts.TargetLocks)
|
||||
if err := b.cleanupRunMounts(mountPoint, mountArtifacts); err != nil {
|
||||
b.Logger.Debugf("cleaning up run mounts: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
// Add temporary copies of the contents of volume locations at the
|
||||
|
@ -1525,28 +1527,61 @@ func checkIfMountDestinationPreExists(root string, dest string) (bool, error) {
|
|||
|
||||
// runSetupRunMounts sets up mounts that exist only in this RUN, not in subsequent runs
|
||||
//
|
||||
// If this function succeeds, the caller must unlock runMountArtifacts.TargetLocks (when??)
|
||||
func (b *Builder) runSetupRunMounts(mountPoint string, mounts []string, sources runMountInfo, idMaps IDMaps) ([]specs.Mount, *runMountArtifacts, error) {
|
||||
mountTargets := make([]string, 0, 10)
|
||||
// If this function succeeds, the caller must free the returned
|
||||
// runMountArtifacts by calling b.cleanupRunMounts() after the command being
|
||||
// executed with those mounts has finished.
|
||||
func (b *Builder) runSetupRunMounts(mountPoint, bundlePath string, mounts []string, sources runMountInfo, idMaps IDMaps) ([]specs.Mount, *runMountArtifacts, error) {
|
||||
mountTargets := make([]string, 0, len(mounts))
|
||||
tmpFiles := make([]string, 0, len(mounts))
|
||||
mountImages := make([]string, 0, 10)
|
||||
mountImages := make([]string, 0, len(mounts))
|
||||
intermediateMounts := make([]string, 0, len(mounts))
|
||||
finalMounts := make([]specs.Mount, 0, len(mounts))
|
||||
agents := make([]*sshagent.AgentServer, 0, len(mounts))
|
||||
sshCount := 0
|
||||
defaultSSHSock := ""
|
||||
targetLocks := []*lockfile.LockFile{}
|
||||
var overlayDirs []string
|
||||
succeeded := false
|
||||
defer func() {
|
||||
if !succeeded {
|
||||
for _, agent := range agents {
|
||||
servePath := agent.ServePath()
|
||||
if err := agent.Shutdown(); err != nil {
|
||||
b.Logger.Errorf("shutting down SSH agent at %q: %v", servePath, err)
|
||||
}
|
||||
}
|
||||
for _, overlayDir := range overlayDirs {
|
||||
if err := overlay.RemoveTemp(overlayDir); err != nil {
|
||||
b.Logger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
for _, intermediateMount := range intermediateMounts {
|
||||
if err := mount.Unmount(intermediateMount); err != nil {
|
||||
b.Logger.Errorf("unmounting %q: %v", intermediateMount, err)
|
||||
}
|
||||
if err := os.Remove(intermediateMount); err != nil {
|
||||
b.Logger.Errorf("removing should-be-empty directory %q: %v", intermediateMount, err)
|
||||
}
|
||||
}
|
||||
for _, mountImage := range mountImages {
|
||||
if _, err := b.store.UnmountImage(mountImage, false); err != nil {
|
||||
b.Logger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
for _, tmpFile := range tmpFiles {
|
||||
if err := os.Remove(tmpFile); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
b.Logger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
volumes.UnlockLockArray(targetLocks)
|
||||
}
|
||||
}()
|
||||
for _, mount := range mounts {
|
||||
var mountSpec *specs.Mount
|
||||
var err error
|
||||
var envFile, image string
|
||||
var envFile, image, bundleMountsDir, overlayDir, intermediateMount string
|
||||
var agent *sshagent.AgentServer
|
||||
var tl *lockfile.LockFile
|
||||
|
||||
tokens := strings.Split(mount, ",")
|
||||
|
||||
// If `type` is not set default to TypeBind
|
||||
|
@ -1574,29 +1609,37 @@ func (b *Builder) runSetupRunMounts(mountPoint string, mounts []string, sources
|
|||
}
|
||||
}
|
||||
case "ssh":
|
||||
mountSpec, agent, err = b.getSSHMount(tokens, sshCount, sources.SSHSources, idMaps)
|
||||
mountSpec, agent, err = b.getSSHMount(tokens, len(agents), sources.SSHSources, idMaps)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if mountSpec != nil {
|
||||
finalMounts = append(finalMounts, *mountSpec)
|
||||
agents = append(agents, agent)
|
||||
if sshCount == 0 {
|
||||
if len(agents) == 0 {
|
||||
defaultSSHSock = mountSpec.Destination
|
||||
}
|
||||
// Count is needed as the default destination of the ssh sock inside the container is /run/buildkit/ssh_agent.{i}
|
||||
sshCount++
|
||||
agents = append(agents, agent)
|
||||
}
|
||||
case define.TypeBind:
|
||||
mountSpec, image, err = b.getBindMount(tokens, sources.SystemContext, sources.ContextDir, sources.StageMountPoints, idMaps, sources.WorkDir)
|
||||
if bundleMountsDir == "" {
|
||||
if bundleMountsDir, err = os.MkdirTemp(bundlePath, "mounts"); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
mountSpec, image, intermediateMount, overlayDir, err = b.getBindMount(tokens, sources.SystemContext, sources.ContextDir, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
finalMounts = append(finalMounts, *mountSpec)
|
||||
// only perform cleanup if image was mounted ignore everything else
|
||||
if image != "" {
|
||||
mountImages = append(mountImages, image)
|
||||
}
|
||||
if overlayDir != "" {
|
||||
overlayDirs = append(overlayDirs, overlayDir)
|
||||
}
|
||||
if intermediateMount != "" {
|
||||
intermediateMounts = append(intermediateMounts, intermediateMount)
|
||||
}
|
||||
finalMounts = append(finalMounts, *mountSpec)
|
||||
case "tmpfs":
|
||||
mountSpec, err = b.getTmpfsMount(tokens, idMaps)
|
||||
if err != nil {
|
||||
|
@ -1604,14 +1647,22 @@ func (b *Builder) runSetupRunMounts(mountPoint string, mounts []string, sources
|
|||
}
|
||||
finalMounts = append(finalMounts, *mountSpec)
|
||||
case "cache":
|
||||
mountSpec, tl, err = b.getCacheMount(tokens, sources.StageMountPoints, idMaps, sources.WorkDir)
|
||||
if bundleMountsDir == "" {
|
||||
if bundleMountsDir, err = os.MkdirTemp(bundlePath, "mounts"); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
mountSpec, intermediateMount, tl, err = b.getCacheMount(tokens, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
finalMounts = append(finalMounts, *mountSpec)
|
||||
if intermediateMount != "" {
|
||||
intermediateMounts = append(intermediateMounts, intermediateMount)
|
||||
}
|
||||
if tl != nil {
|
||||
targetLocks = append(targetLocks, tl)
|
||||
}
|
||||
finalMounts = append(finalMounts, *mountSpec)
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("invalid mount type %q", mountType)
|
||||
}
|
||||
|
@ -1631,31 +1682,33 @@ func (b *Builder) runSetupRunMounts(mountPoint string, mounts []string, sources
|
|||
}
|
||||
succeeded = true
|
||||
artifacts := &runMountArtifacts{
|
||||
RunMountTargets: mountTargets,
|
||||
TmpFiles: tmpFiles,
|
||||
Agents: agents,
|
||||
MountedImages: mountImages,
|
||||
SSHAuthSock: defaultSSHSock,
|
||||
TargetLocks: targetLocks,
|
||||
RunMountTargets: mountTargets,
|
||||
RunOverlayDirs: overlayDirs,
|
||||
TmpFiles: tmpFiles,
|
||||
Agents: agents,
|
||||
MountedImages: mountImages,
|
||||
SSHAuthSock: defaultSSHSock,
|
||||
TargetLocks: targetLocks,
|
||||
IntermediateMounts: intermediateMounts,
|
||||
}
|
||||
return finalMounts, artifacts, nil
|
||||
}
|
||||
|
||||
func (b *Builder) getBindMount(tokens []string, context *imageTypes.SystemContext, contextDir string, stageMountPoints map[string]internal.StageMountDetails, idMaps IDMaps, workDir string) (*specs.Mount, string, error) {
|
||||
func (b *Builder) getBindMount(tokens []string, sys *types.SystemContext, contextDir string, stageMountPoints map[string]internal.StageMountDetails, idMaps IDMaps, workDir, tmpDir string) (*specs.Mount, string, string, string, error) {
|
||||
if contextDir == "" {
|
||||
return nil, "", errors.New("Context Directory for current run invocation is not configured")
|
||||
return nil, "", "", "", errors.New("context directory for current run invocation is not configured")
|
||||
}
|
||||
var optionMounts []specs.Mount
|
||||
mount, image, err := volumes.GetBindMount(context, tokens, contextDir, b.store, b.MountLabel, stageMountPoints, workDir)
|
||||
mount, image, intermediateMount, overlayMount, err := volumes.GetBindMount(sys, tokens, contextDir, b.store, b.MountLabel, stageMountPoints, workDir, tmpDir)
|
||||
if err != nil {
|
||||
return nil, image, err
|
||||
return nil, "", "", "", err
|
||||
}
|
||||
optionMounts = append(optionMounts, mount)
|
||||
volumes, err := b.runSetupVolumeMounts(b.MountLabel, nil, optionMounts, idMaps)
|
||||
if err != nil {
|
||||
return nil, image, err
|
||||
return nil, "", "", "", err
|
||||
}
|
||||
return &volumes[0], image, nil
|
||||
return &volumes[0], image, intermediateMount, overlayMount, nil
|
||||
}
|
||||
|
||||
func (b *Builder) getTmpfsMount(tokens []string, idMaps IDMaps) (*specs.Mount, error) {
|
||||
|
@ -1932,52 +1985,53 @@ func (b *Builder) cleanupTempVolumes() {
|
|||
}
|
||||
|
||||
// cleanupRunMounts cleans up run mounts so they only appear in this run.
|
||||
func (b *Builder) cleanupRunMounts(context *imageTypes.SystemContext, mountpoint string, artifacts *runMountArtifacts) error {
|
||||
func (b *Builder) cleanupRunMounts(mountpoint string, artifacts *runMountArtifacts) error {
|
||||
for _, agent := range artifacts.Agents {
|
||||
err := agent.Shutdown()
|
||||
if err != nil {
|
||||
servePath := agent.ServePath()
|
||||
if err := agent.Shutdown(); err != nil {
|
||||
return fmt.Errorf("shutting down SSH agent at %q: %v", servePath, err)
|
||||
}
|
||||
}
|
||||
// clean up any overlays we mounted
|
||||
for _, overlayDirectory := range artifacts.RunOverlayDirs {
|
||||
if err := overlay.RemoveTemp(overlayDirectory); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
//cleanup any mounted images for this run
|
||||
// unmount anything that needs unmounting
|
||||
for _, intermediateMount := range artifacts.IntermediateMounts {
|
||||
if err := mount.Unmount(intermediateMount); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("unmounting %q: %w", intermediateMount, err)
|
||||
}
|
||||
if err := os.Remove(intermediateMount); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("removing should-be-empty directory %q: %w", intermediateMount, err)
|
||||
}
|
||||
}
|
||||
// unmount any images we mounted for this run
|
||||
for _, image := range artifacts.MountedImages {
|
||||
if image != "" {
|
||||
// if flow hits here some image was mounted for this run
|
||||
i, err := internalUtil.LookupImage(context, b.store, image)
|
||||
if err == nil {
|
||||
// silently try to unmount and do nothing
|
||||
// if image is being used by something else
|
||||
_ = i.Unmount(false)
|
||||
}
|
||||
if errors.Is(err, storageTypes.ErrImageUnknown) {
|
||||
// Ignore only if ErrImageUnknown
|
||||
// Reason: Image is already unmounted do nothing
|
||||
continue
|
||||
}
|
||||
return err
|
||||
if _, err := b.store.UnmountImage(image, false); err != nil {
|
||||
logrus.Debugf("umounting image %q: %v", image, err)
|
||||
}
|
||||
}
|
||||
// remove mount targets that were created for this run
|
||||
opts := copier.RemoveOptions{
|
||||
All: true,
|
||||
}
|
||||
for _, path := range artifacts.RunMountTargets {
|
||||
err := copier.Remove(mountpoint, path, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
if err := copier.Remove(mountpoint, path, opts); err != nil {
|
||||
return fmt.Errorf("removing mount target %q %q: %w", mountpoint, path, err)
|
||||
}
|
||||
}
|
||||
var prevErr error
|
||||
for _, path := range artifacts.TmpFiles {
|
||||
err := os.Remove(path)
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
if prevErr != nil {
|
||||
logrus.Error(prevErr)
|
||||
}
|
||||
prevErr = err
|
||||
prevErr = fmt.Errorf("removing temporary file: %w", err)
|
||||
}
|
||||
}
|
||||
// unlock if any locked files from this RUN statement
|
||||
// unlock locks we took, most likely for cache mounts
|
||||
volumes.UnlockLockArray(artifacts.TargetLocks)
|
||||
return prevErr
|
||||
}
|
||||
|
|
|
@ -276,7 +276,7 @@ func (b *Builder) Run(command []string, options RunOptions) error {
|
|||
}
|
||||
|
||||
defer func() {
|
||||
if err := b.cleanupRunMounts(options.SystemContext, mountPoint, runArtifacts); err != nil {
|
||||
if err := b.cleanupRunMounts(mountPoint, runArtifacts); err != nil {
|
||||
options.Logger.Errorf("unable to cleanup run mounts %v", err)
|
||||
}
|
||||
}()
|
||||
|
@ -351,9 +351,12 @@ func setupSpecialMountSpecChanges(spec *spec.Spec, shmSize string) ([]specs.Moun
|
|||
return spec.Mounts, nil
|
||||
}
|
||||
|
||||
// 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) (*spec.Mount, *lockfile.LockFile, error) {
|
||||
return nil, nil, errors.New("cache mounts not supported on freebsd")
|
||||
// If this succeeded, the caller would be expected to, after the command which
|
||||
// uses the mount exits, unmount the mounted filesystem and remove its
|
||||
// mountpoint (if we provided the path to its mountpoint), and release the lock
|
||||
// (if we took one).
|
||||
func (b *Builder) getCacheMount(tokens []string, stageMountPoints map[string]internal.StageMountDetails, idMaps IDMaps, workDir, tmpDir string) (*specs.Mount, string, *lockfile.LockFile, error) {
|
||||
return nil, "", nil, errors.New("cache mounts not supported on freebsd")
|
||||
}
|
||||
|
||||
func (b *Builder) runSetupVolumeMounts(mountLabel string, volumeMounts []string, optionMounts []specs.Mount, idMaps IDMaps) (mounts []specs.Mount, Err error) {
|
||||
|
|
40
run_linux.go
40
run_linux.go
|
@ -38,6 +38,7 @@ import (
|
|||
"github.com/containers/storage/pkg/idtools"
|
||||
"github.com/containers/storage/pkg/ioutils"
|
||||
"github.com/containers/storage/pkg/lockfile"
|
||||
"github.com/containers/storage/pkg/mount"
|
||||
"github.com/containers/storage/pkg/stringid"
|
||||
"github.com/containers/storage/pkg/unshare"
|
||||
"github.com/docker/go-units"
|
||||
|
@ -492,7 +493,7 @@ rootless=%d
|
|||
}
|
||||
|
||||
defer func() {
|
||||
if err := b.cleanupRunMounts(options.SystemContext, mountPoint, runArtifacts); err != nil {
|
||||
if err := b.cleanupRunMounts(mountPoint, runArtifacts); err != nil {
|
||||
options.Logger.Errorf("unable to cleanup run mounts %v", err)
|
||||
}
|
||||
}()
|
||||
|
@ -1112,7 +1113,7 @@ func (b *Builder) runSetupVolumeMounts(mountLabel string, volumeMounts []string,
|
|||
RootGID: idMaps.rootGID,
|
||||
UpperDirOptionFragment: upperDir,
|
||||
WorkDirOptionFragment: workDir,
|
||||
GraphOpts: b.store.GraphOptions(),
|
||||
GraphOpts: slices.Clone(b.store.GraphOptions()),
|
||||
}
|
||||
|
||||
overlayMount, err := overlay.MountWithOptions(contentDir, host, container, &overlayOpts)
|
||||
|
@ -1121,7 +1122,7 @@ func (b *Builder) runSetupVolumeMounts(mountLabel string, volumeMounts []string,
|
|||
}
|
||||
|
||||
// If chown true, add correct ownership to the overlay temp directories.
|
||||
if foundU {
|
||||
if err == nil && foundU {
|
||||
if err := chown.ChangeHostPathOwnership(contentDir, true, idMaps.processUID, idMaps.processGID); err != nil {
|
||||
return specs.Mount{}, err
|
||||
}
|
||||
|
@ -1379,24 +1380,39 @@ func checkIdsGreaterThan5(ids []specs.LinuxIDMapping) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Returns a Mount to add to the runtime spec's list of mounts, an optional
|
||||
// path of a mounted filesystem, unmounted, and an optional lock, or an error.
|
||||
//
|
||||
// The caller is expected to, after the command which uses the mount exits,
|
||||
// unmount the mounted filesystem (if we provided the path to its mountpoint)
|
||||
// and remove its mountpoint, , and release the lock (if we took one).
|
||||
func (b *Builder) getCacheMount(tokens []string, stageMountPoints map[string]internal.StageMountDetails, idMaps IDMaps, workDir, tmpDir string) (*specs.Mount, string, *lockfile.LockFile, error) {
|
||||
var optionMounts []specs.Mount
|
||||
mount, targetLock, err := volumes.GetCacheMount(tokens, b.store, b.MountLabel, stageMountPoints, workDir)
|
||||
optionMount, intermediateMount, targetLock, err := volumes.GetCacheMount(tokens, stageMountPoints, workDir, tmpDir)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, "", nil, err
|
||||
}
|
||||
succeeded := false
|
||||
defer func() {
|
||||
if !succeeded && targetLock != nil {
|
||||
targetLock.Unlock()
|
||||
if !succeeded {
|
||||
if intermediateMount != "" {
|
||||
if err := mount.Unmount(intermediateMount); err != nil {
|
||||
b.Logger.Debugf("unmounting %q: %v", intermediateMount, err)
|
||||
}
|
||||
if err := os.Remove(intermediateMount); err != nil {
|
||||
b.Logger.Debugf("removing should-be-empty directory %q: %v", intermediateMount, err)
|
||||
}
|
||||
}
|
||||
if targetLock != nil {
|
||||
targetLock.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
optionMounts = append(optionMounts, mount)
|
||||
optionMounts = append(optionMounts, optionMount)
|
||||
volumes, err := b.runSetupVolumeMounts(b.MountLabel, nil, optionMounts, idMaps)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, "", nil, err
|
||||
}
|
||||
succeeded = true
|
||||
return &volumes[0], targetLock, nil
|
||||
return &volumes[0], intermediateMount, targetLock, nil
|
||||
}
|
||||
|
|
|
@ -6570,8 +6570,8 @@ _EOF
|
|||
run_buildah build -t buildkitbase $WITH_POLICY_JSON -f $contextdir/Dockerfilebuildkitbase $contextdir/
|
||||
|
||||
# try reading something from persistent cache in a different build
|
||||
run_buildah 125 build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfilecachefromimage
|
||||
expect_output --substring "no stage found with name buildkitbase"
|
||||
TMPDIR=${TEST_SCRATCH_DIR} run_buildah 125 build -t testbud $WITH_POLICY_JSON -f $contextdir/Dockerfilecachefromimage
|
||||
expect_output --substring "no stage or additional build context found with name buildkitbase"
|
||||
}
|
||||
|
||||
@test "bud-with-mount-cache-multiple-from-like-buildkit" {
|
||||
|
@ -6938,8 +6938,8 @@ RUN --mount=type=cache,source=../../../../../../../../../../../$TEST_SCRATCH_DIR
|
|||
ls -l /var/tmp && cat /var/tmp/file.txt
|
||||
EOF
|
||||
|
||||
run_buildah 1 build --no-cache ${TEST_SCRATCH_DIR}
|
||||
expect_output --substring "cat: can't open '/var/tmp/file.txt': No such file or directory"
|
||||
run_buildah 125 build --no-cache ${TEST_SCRATCH_DIR}
|
||||
expect_output --substring "no such file or directory"
|
||||
|
||||
mkdir ${TEST_SCRATCH_DIR}/cve20249675
|
||||
cat > ${TEST_SCRATCH_DIR}/cve20249675/Containerfile <<EOF
|
||||
|
@ -6948,6 +6948,33 @@ RUN --mount=type=cache,from=testbuild,source=../,target=/var/tmp \
|
|||
ls -l /var/tmp && cat /var/tmp/file.txt
|
||||
EOF
|
||||
|
||||
run_buildah 1 build --security-opt label=disable --build-context testbuild=${TEST_SCRATCH_DIR}/cve20249675/ --no-cache ${TEST_SCRATCH_DIR}/cve20249675/
|
||||
mkdir ${TEST_SCRATCH_DIR}/cachedir
|
||||
|
||||
run_buildah 1 build --security-opt label=disable --build-context testbuild=${TEST_SCRATCH_DIR}/cachedir/ --no-cache ${TEST_SCRATCH_DIR}/cve20249675/
|
||||
expect_output --substring "cat: can't open '/var/tmp/file.txt': No such file or directory"
|
||||
}
|
||||
|
||||
@test "build-mounts-build-context-rw" {
|
||||
zflag=
|
||||
if which selinuxenabled > /dev/null 2> /dev/null ; then
|
||||
if selinuxenabled ; then
|
||||
zflag=,z
|
||||
fi
|
||||
fi
|
||||
base=busybox
|
||||
_prefetch $base
|
||||
mkdir -p ${TEST_SCRATCH_DIR}/buildcontext
|
||||
cat > ${TEST_SCRATCH_DIR}/buildcontext/Dockerfile << EOF
|
||||
FROM $base
|
||||
RUN --mount=type=bind,dst=/dst,source=/,rw${zflag} \
|
||||
mkdir /dst/subdir ; \
|
||||
chown 1000:1000 /dst/subdir ; \
|
||||
chmod 777 /dst/subdir ; \
|
||||
touch /dst/subdir/file-suid ; \
|
||||
chmod 4777 /dst/subdir/file-suid
|
||||
EOF
|
||||
run_buildah build ${TEST_SCRATCH_DIR}/buildcontext
|
||||
run find ${TEST_SCRATCH_DIR}/buildcontext -name file-suid -ls
|
||||
find ${TEST_SCRATCH_DIR}/buildcontext -ls
|
||||
expect_output "" "build should not be able to write to build context"
|
||||
}
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
FROM scratch
|
||||
COPY hello .
|
||||
COPY hello hello1 .
|
||||
COPY hello2 /subdir/hello
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
hello1
|
|
@ -1,6 +1,8 @@
|
|||
FROM alpine AS base
|
||||
RUN touch -r /etc/os-release /1.txt
|
||||
|
||||
FROM alpine AS interloper
|
||||
RUN --mount=type=bind,from=base,source=/,destination=/base,rw touch -r /etc/os-release /base/2.txt
|
||||
FROM base
|
||||
RUN --mount=type=bind,from=interloper,source=/etc,destination=/etc2 touch -r /etc2/os-release /3.txt
|
||||
|
||||
FROM interloper
|
||||
RUN --mount=type=bind,from=base,source=/,destination=/base mkdir /base2 && cp -a /base/*.txt /base2/ && touch -r /etc/os-release /base2 && ls -la /base2
|
||||
|
|
|
@ -427,13 +427,25 @@ function configure_and_check_user() {
|
|||
mkdir -p ${TEST_SCRATCH_DIR}/was:empty
|
||||
# As a baseline, this should succeed.
|
||||
run_buildah run --mount type=tmpfs,dst=/var/tmpfs-not-empty $cid touch /var/tmpfs-not-empty/testfile
|
||||
# This should succeed, but the writes should effectively be discarded
|
||||
run_buildah run --mount type=bind,src=${TEST_SCRATCH_DIR}/was:empty,dst=/var/not-empty,rw${zflag:+,${zflag}} $cid touch /var/not-empty/testfile
|
||||
if test -r ${TEST_SCRATCH_DIR}/was:empty/testfile ; then
|
||||
die write to mounted type=bind was not discarded, ${TEST_SCRATCH_DIR}/was:empty/testfile exists
|
||||
fi
|
||||
# If we're parsing the options at all, this should be read-only, so it should fail.
|
||||
run_buildah 1 run --mount type=bind,src=${TEST_SCRATCH_DIR}/was:empty,dst=/var/not-empty,ro${zflag:+,${zflag}} $cid touch /var/not-empty/testfile
|
||||
# Even if the parent directory doesn't exist yet, this should succeed.
|
||||
run_buildah run --mount type=bind,src=${TEST_SCRATCH_DIR}/was:empty,dst=/var/multi-level/subdirectory,rw $cid touch /var/multi-level/subdirectory/testfile
|
||||
# And check the same for file volumes.
|
||||
run_buildah run --mount type=bind,src=${TEST_SCRATCH_DIR}/was:empty/testfile,dst=/var/different-multi-level/subdirectory/testfile,rw $cid touch /var/different-multi-level/subdirectory/testfile
|
||||
# Even if the parent directory doesn't exist yet, this should succeed, but again the write should be discarded.
|
||||
run_buildah run --mount type=bind,src=${TEST_SCRATCH_DIR}/was:empty,dst=/var/multi-level/subdirectory,rw${zflag:+,${zflag}} $cid touch /var/multi-level/subdirectory/testfile
|
||||
if test -r ${TEST_SCRATCH_DIR}/was:empty/testfile ; then
|
||||
die write to mounted type=bind was not discarded, ${TEST_SCRATCH_DIR}/was:empty/testfile exists
|
||||
fi
|
||||
# And check the same for file volumes, which make life harder because the kernel's overlay
|
||||
# filesystem really only wants to be dealing with directories.
|
||||
: > ${TEST_SCRATCH_DIR}/was:empty/testfile
|
||||
run_buildah run --mount type=bind,src=${TEST_SCRATCH_DIR}/was:empty/testfile,dst=/var/different-multi-level/subdirectory/testfile,rw${zflag:+,${zflag}} $cid sh -c 'echo wrote > /var/different-multi-level/subdirectory/testfile'
|
||||
if test -s ${TEST_SCRATCH_DIR}/was:empty/testfile ; then
|
||||
die write to mounted type=bind was not discarded, ${TEST_SCRATCH_DIR}/was:empty/testfile was written to
|
||||
fi
|
||||
}
|
||||
|
||||
@test "run --mount=type=bind with from like buildkit" {
|
||||
|
@ -441,16 +453,17 @@ function configure_and_check_user() {
|
|||
zflag=
|
||||
if which selinuxenabled > /dev/null 2> /dev/null ; then
|
||||
if selinuxenabled ; then
|
||||
skip "skip if selinux enabled, since stages have different selinux label"
|
||||
zflag=,z
|
||||
fi
|
||||
fi
|
||||
run_buildah build -t buildkitbase $WITH_POLICY_JSON -f $BUDFILES/buildkit-mount-from/Dockerfilebuildkitbase $BUDFILES/buildkit-mount-from/
|
||||
_prefetch alpine
|
||||
run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine
|
||||
cid=$output
|
||||
run_buildah run --mount type=bind,source=.,from=buildkitbase,target=/test,z $cid cat /test/hello
|
||||
expect_output --substring "hello"
|
||||
run_buildah rmi -f buildkitbase
|
||||
run_buildah run --mount type=bind,source=.,from=buildkitbase,target=/test${zflag} $cid cat /test/hello1
|
||||
expect_output --substring "hello1"
|
||||
run_buildah run --mount type=bind,source=subdir,from=buildkitbase,target=/test${zflag} $cid cat /test/hello
|
||||
expect_output --substring "hello2"
|
||||
}
|
||||
|
||||
@test "run --mount=type=cache like buildkit" {
|
||||
|
@ -458,14 +471,14 @@ function configure_and_check_user() {
|
|||
zflag=
|
||||
if which selinuxenabled > /dev/null 2> /dev/null ; then
|
||||
if selinuxenabled ; then
|
||||
skip "skip if selinux enabled, since stages have different selinux label"
|
||||
zflag=,z
|
||||
fi
|
||||
fi
|
||||
_prefetch alpine
|
||||
run_buildah from --quiet --pull=false $WITH_POLICY_JSON alpine
|
||||
cid=$output
|
||||
run_buildah run --mount type=cache,target=/test,z $cid sh -c 'echo "hello" > /test/hello && cat /test/hello'
|
||||
run_buildah run --mount type=cache,target=/test,z $cid cat /test/hello
|
||||
run_buildah run --mount type=cache,target=/test${zflag} $cid sh -c 'mkdir -p /test/subdir && echo "hello" > /test/subdir/h.txt && cat /test/subdir/h.txt'
|
||||
run_buildah run --mount type=cache,src=subdir,target=/test${zflag} $cid cat /test/h.txt
|
||||
expect_output --substring "hello"
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue