build: gate local delete outputs
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
+9
-126
@@ -6,16 +6,15 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/docker/buildx/build"
|
||||
"github.com/docker/buildx/util/buildflags"
|
||||
"github.com/docker/buildx/util/osutil"
|
||||
"github.com/moby/buildkit/client"
|
||||
"github.com/moby/buildkit/util/entitlements"
|
||||
@@ -37,7 +36,7 @@ const (
|
||||
EntitlementKeyImageLoad EntitlementKey = "image.load"
|
||||
EntitlementKeyImage EntitlementKey = "image"
|
||||
EntitlementKeySSH EntitlementKey = "ssh"
|
||||
EntitlementKeyLocalOutputDelete EntitlementKey = "local-output-delete"
|
||||
EntitlementKeyBuildxLocalDelete EntitlementKey = EntitlementKey(buildflags.EntitlementBuildxLocalDelete)
|
||||
)
|
||||
|
||||
type EntitlementConf struct {
|
||||
@@ -67,7 +66,7 @@ func ParseEntitlements(in []string) (EntitlementConf, error) {
|
||||
conf.SecurityInsecure = true
|
||||
case string(EntitlementKeySSH):
|
||||
conf.SSH = true
|
||||
case string(EntitlementKeyLocalOutputDelete):
|
||||
case string(EntitlementKeyBuildxLocalDelete):
|
||||
conf.LocalOutputDelete = true
|
||||
default:
|
||||
k, v, _ := strings.Cut(e, "=")
|
||||
@@ -102,8 +101,8 @@ func ParseEntitlements(in []string) (EntitlementConf, error) {
|
||||
case string(EntitlementKeyImage):
|
||||
conf.ImagePush = append(conf.ImagePush, v)
|
||||
conf.ImageLoad = append(conf.ImageLoad, v)
|
||||
case string(EntitlementKeyLocalOutputDelete):
|
||||
return conf, errors.Errorf("%s does not accept a value", EntitlementKeyLocalOutputDelete)
|
||||
case string(EntitlementKeyBuildxLocalDelete):
|
||||
return conf, errors.Errorf("%s does not accept a value", EntitlementKeyBuildxLocalDelete)
|
||||
default:
|
||||
return conf, errors.Errorf("unknown entitlement key %q", k)
|
||||
}
|
||||
@@ -271,7 +270,7 @@ func (c EntitlementConf) Prompt(ctx context.Context, isRemote bool, out io.Write
|
||||
}
|
||||
if c.LocalOutputDelete {
|
||||
msgs = append(msgs, " - Deleting stale files from local output destinations")
|
||||
flags = append(flags, string(EntitlementKeyLocalOutputDelete))
|
||||
flags = append(flags, string(EntitlementKeyBuildxLocalDelete))
|
||||
}
|
||||
|
||||
roPaths, rwPaths, commonPaths := groupSamePaths(c.FSRead, c.FSWrite)
|
||||
@@ -530,7 +529,7 @@ func evaluatePaths(in []string) ([]string, bool, error) {
|
||||
logrus.Warnf("failed to evaluate entitlement path %q: %v", p, err)
|
||||
continue
|
||||
}
|
||||
v, rest, err := evaluateToExistingPath(v)
|
||||
v, rest, err := osutil.EvaluateToExistingPath(v)
|
||||
if err != nil {
|
||||
return nil, false, errors.Wrapf(err, "failed to evaluate path %q", p)
|
||||
}
|
||||
@@ -549,7 +548,7 @@ func evaluatePaths(in []string) ([]string, bool, error) {
|
||||
func evaluateToExistingPaths(in map[string]struct{}) (map[string]struct{}, error) {
|
||||
m := make(map[string]struct{}, len(in))
|
||||
for p := range in {
|
||||
v, _, err := evaluateToExistingPath(p)
|
||||
v, _, err := osutil.EvaluateToExistingPath(p)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to evaluate path %q", p)
|
||||
}
|
||||
@@ -563,121 +562,5 @@ func evaluateToExistingPaths(in map[string]struct{}) (map[string]struct{}, error
|
||||
}
|
||||
|
||||
func evaluateToExistingPath(in string) (string, string, error) {
|
||||
in, err := filepath.Abs(in)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
volLen := volumeNameLen(in)
|
||||
pathSeparator := string(os.PathSeparator)
|
||||
|
||||
if volLen < len(in) && os.IsPathSeparator(in[volLen]) {
|
||||
volLen++
|
||||
}
|
||||
vol := in[:volLen]
|
||||
dest := vol
|
||||
linksWalked := 0
|
||||
var end int
|
||||
for start := volLen; start < len(in); start = end {
|
||||
for start < len(in) && os.IsPathSeparator(in[start]) {
|
||||
start++
|
||||
}
|
||||
end = start
|
||||
for end < len(in) && !os.IsPathSeparator(in[end]) {
|
||||
end++
|
||||
}
|
||||
|
||||
if end == start {
|
||||
break
|
||||
} else if in[start:end] == "." {
|
||||
continue
|
||||
} else if in[start:end] == ".." {
|
||||
var r int
|
||||
for r = len(dest) - 1; r >= volLen; r-- {
|
||||
if os.IsPathSeparator(dest[r]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if r < volLen || dest[r+1:] == ".." {
|
||||
if len(dest) > volLen {
|
||||
dest += pathSeparator
|
||||
}
|
||||
dest += ".."
|
||||
} else {
|
||||
dest = dest[:r]
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if len(dest) > volumeNameLen(dest) && !os.IsPathSeparator(dest[len(dest)-1]) {
|
||||
dest += pathSeparator
|
||||
}
|
||||
dest += in[start:end]
|
||||
|
||||
fi, err := os.Lstat(dest)
|
||||
if err != nil {
|
||||
// If the component doesn't exist, return the last valid path
|
||||
if os.IsNotExist(err) {
|
||||
for r := len(dest) - 1; r >= volLen; r-- {
|
||||
if os.IsPathSeparator(dest[r]) {
|
||||
return dest[:r], in[start:], nil
|
||||
}
|
||||
}
|
||||
return vol, in[start:], nil
|
||||
}
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if fi.Mode()&fs.ModeSymlink == 0 {
|
||||
if !fi.Mode().IsDir() && end < len(in) {
|
||||
return "", "", syscall.ENOTDIR
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
linksWalked++
|
||||
if linksWalked > 255 {
|
||||
return "", "", errors.New("too many symlinks")
|
||||
}
|
||||
|
||||
link, err := os.Readlink(dest)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
in = link + in[end:]
|
||||
|
||||
v := volumeNameLen(link)
|
||||
if v > 0 {
|
||||
if v < len(link) && os.IsPathSeparator(link[v]) {
|
||||
v++
|
||||
}
|
||||
vol = link[:v]
|
||||
dest = vol
|
||||
end = len(vol)
|
||||
} else if len(link) > 0 && os.IsPathSeparator(link[0]) {
|
||||
dest = link[:1]
|
||||
end = 1
|
||||
vol = link[:1]
|
||||
volLen = 1
|
||||
} else {
|
||||
var r int
|
||||
for r = len(dest) - 1; r >= volLen; r-- {
|
||||
if os.IsPathSeparator(dest[r]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if r < volLen {
|
||||
dest = vol
|
||||
} else {
|
||||
dest = dest[:r]
|
||||
}
|
||||
end = 0
|
||||
}
|
||||
}
|
||||
return filepath.Clean(dest), "", nil
|
||||
}
|
||||
|
||||
func volumeNameLen(s string) int {
|
||||
return len(filepath.VolumeName(s))
|
||||
return osutil.EvaluateToExistingPath(in)
|
||||
}
|
||||
|
||||
@@ -506,12 +506,12 @@ func TestValidateEntitlementsInvalidLocalOutputMode(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseEntitlementsLocalOutputDelete(t *testing.T) {
|
||||
conf, err := ParseEntitlements([]string{string(EntitlementKeyLocalOutputDelete)})
|
||||
conf, err := ParseEntitlements([]string{string(EntitlementKeyBuildxLocalDelete)})
|
||||
require.NoError(t, err)
|
||||
require.True(t, conf.LocalOutputDelete)
|
||||
|
||||
_, err = ParseEntitlements([]string{string(EntitlementKeyLocalOutputDelete) + "=true"})
|
||||
require.ErrorContains(t, err, "local-output-delete does not accept a value")
|
||||
_, err = ParseEntitlements([]string{string(EntitlementKeyBuildxLocalDelete) + "=true"})
|
||||
require.ErrorContains(t, err, "buildx.local.delete does not accept a value")
|
||||
}
|
||||
|
||||
func TestPromptLocalOutputDeleteCannotBeDisabledWithFSEntitlements(t *testing.T) {
|
||||
@@ -524,7 +524,7 @@ func TestPromptLocalOutputDeleteCannotBeDisabledWithFSEntitlements(t *testing.T)
|
||||
err := EntitlementConf{LocalOutputDelete: true}.Prompt(ctx, true, &out)
|
||||
require.ErrorContains(t, err, "additional privileges requested")
|
||||
require.Contains(t, out.String(), "Deleting stale files from local output destinations")
|
||||
require.Contains(t, out.String(), "--allow=local-output-delete")
|
||||
require.Contains(t, out.String(), "--allow=buildx.local.delete")
|
||||
}
|
||||
|
||||
func TestGroupSamePaths(t *testing.T) {
|
||||
|
||||
@@ -1329,6 +1329,69 @@ func CreateExports(entries []*buildflags.ExportEntry) ([]client.ExportEntry, []s
|
||||
return outs, localPaths, nil
|
||||
}
|
||||
|
||||
func ValidateLocalExportDelete(outputs []client.ExportEntry, allowDelete bool) error {
|
||||
for _, ex := range outputs {
|
||||
if ex.Type != client.ExporterLocal {
|
||||
continue
|
||||
}
|
||||
mode, err := client.ParseLocalExporterMode(ex.Attrs["mode"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if mode != client.LocalExporterModeDelete || allowDelete {
|
||||
continue
|
||||
}
|
||||
ok, err := isSafeLocalDeleteDest(ex.OutputDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return errors.Errorf("local output mode=delete for destination %q requires --allow=%s", ex.OutputDir, buildflags.EntitlementBuildxLocalDelete)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isSafeLocalDeleteDest(dest string) (bool, error) {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to get current working directory")
|
||||
}
|
||||
wd, err = resolveOutputPath(wd)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to evaluate current working directory")
|
||||
}
|
||||
|
||||
dest, err = resolveOutputPath(dest)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "failed to evaluate local output destination %q", dest)
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(wd, dest)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
if rel == "." || rel == ".." || filepath.IsAbs(rel) || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func resolveOutputPath(p string) (string, error) {
|
||||
p, rest, err := osutil.EvaluateToExistingPath(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
p, err = osutil.GetLongPathName(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if rest != "" {
|
||||
p = filepath.Join(p, rest)
|
||||
}
|
||||
return filepath.Clean(p), nil
|
||||
}
|
||||
|
||||
func wrapWriteCloser(wc io.WriteCloser) func(map[string]string) (io.WriteCloser, error) {
|
||||
return func(map[string]string) (io.WriteCloser, error) {
|
||||
return wc, nil
|
||||
|
||||
+1
-1
@@ -331,7 +331,7 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
|
||||
}
|
||||
if progressMode == progressui.RawJSONMode {
|
||||
if exp.LocalOutputDelete {
|
||||
return errors.Errorf("additional privileges requested: pass %q to grant requested privileges", "--allow="+string(bake.EntitlementKeyLocalOutputDelete))
|
||||
return errors.Errorf("additional privileges requested: pass %q to grant requested privileges", "--allow="+string(bake.EntitlementKeyBuildxLocalDelete))
|
||||
}
|
||||
} else {
|
||||
if err := exp.Prompt(ctx, url != "", &syncWriter{w: dockerCli.Err(), wait: printer.Wait}); err != nil {
|
||||
|
||||
+9
-5
@@ -554,7 +554,7 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugger debuggerOpt
|
||||
|
||||
flags.StringSliceVar(&options.extraHosts, "add-host", []string{}, `Add a custom host-to-IP mapping (format: "host:ip")`)
|
||||
|
||||
flags.StringArrayVar(&options.allow, "allow", []string{}, `Allow extra privileged entitlement (e.g., "network.host", "security.insecure", "device")`)
|
||||
flags.StringArrayVar(&options.allow, "allow", []string{}, `Allow extra privileged entitlement (e.g., "network.host", "security.insecure", "device", "buildx.local.delete")`)
|
||||
|
||||
flags.StringArrayVarP(&options.annotations, "annotation", "", []string{}, "Add annotation to the image")
|
||||
|
||||
@@ -1138,6 +1138,14 @@ func RunBuild(ctx context.Context, dockerCli command.Cli, in *BuildOptions, inSt
|
||||
}
|
||||
}
|
||||
|
||||
allow, allowLocalOutputDelete, err := buildflags.ParseEntitlements(in.Allow)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := build.ValidateLocalExportDelete(outputs, allowLocalOutputDelete); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
opts.Annotations, err = buildflags.ParseAnnotations(in.Annotations)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "parse annotations")
|
||||
@@ -1159,10 +1167,6 @@ func RunBuild(ctx context.Context, dockerCli command.Cli, in *BuildOptions, inSt
|
||||
opts.SourcePolicy = in.SourcePolicy
|
||||
opts.Policy = in.Policy
|
||||
|
||||
allow, err := buildflags.ParseEntitlements(in.Allow)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
opts.Allow = allow
|
||||
|
||||
if in.CallFunc != nil {
|
||||
|
||||
@@ -902,7 +902,7 @@ target "default" {
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Local outputs with `mode=delete` require granting `--allow=local-output-delete`
|
||||
> Local outputs with `mode=delete` require granting `--allow=buildx.local.delete`
|
||||
> when invoking `docker buildx bake`.
|
||||
|
||||
### `target.policy`
|
||||
|
||||
@@ -85,7 +85,7 @@ The `fs` entitlements take a path value (relative or absolute) to a directory
|
||||
on the filesystem. Alternatively, you can pass a wildcard (`*`) to allow Bake
|
||||
to access the entire filesystem.
|
||||
|
||||
Bake also supports `--allow=local-output-delete` to grant local outputs
|
||||
Bake also supports `--allow=buildx.local.delete` to grant local outputs
|
||||
permission to delete stale files when `mode=delete` is set.
|
||||
|
||||
### Example: fs.read
|
||||
|
||||
@@ -16,7 +16,7 @@ Start a build
|
||||
| Name | Type | Default | Description |
|
||||
|:----------------------------------------|:--------------|:----------|:-------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [`--add-host`](#add-host) | `stringSlice` | | Add a custom host-to-IP mapping (format: `host:ip`) |
|
||||
| [`--allow`](#allow) | `stringArray` | | Allow extra privileged entitlement (e.g., `network.host`, `security.insecure`, `device`) |
|
||||
| [`--allow`](#allow) | `stringArray` | | Allow extra privileged entitlement (e.g., `network.host`, `security.insecure`, `device`, `buildx.local.delete`) |
|
||||
| [`--annotation`](#annotation) | `stringArray` | | Add annotation to the image |
|
||||
| [`--attest`](#attest) | `stringArray` | | Attestation parameters (format: `type=sbom,generator=image`) |
|
||||
| [`--build-arg`](#build-arg) | `stringArray` | | Set build-time variables |
|
||||
@@ -179,9 +179,14 @@ Allow extra privileged entitlement. List of entitlements:
|
||||
- `--allow device` - Grants access to all devices.
|
||||
- `--allow device=kind|name` - Grants access to a specific device.
|
||||
- `--allow device=kind|name,alias=kind|name` - Grants access to a specific device, with optional aliasing.
|
||||
- `buildx.local.delete` - Allows local outputs using `mode=delete` to delete
|
||||
stale destination files when the destination is the current working directory
|
||||
or outside it.
|
||||
|
||||
For entitlements to be enabled, the BuildKit daemon also needs to allow them
|
||||
with `--allow-insecure-entitlement` (see [`create --buildkitd-flags`](buildx_create.md#buildkitd-flags)).
|
||||
For BuildKit entitlements to be enabled, the BuildKit daemon also needs to allow
|
||||
them with `--allow-insecure-entitlement` (see [`create --buildkitd-flags`](buildx_create.md#buildkitd-flags)).
|
||||
The `buildx.local.delete` entitlement is checked by Buildx and isn't sent to the
|
||||
BuildKit daemon.
|
||||
|
||||
```console
|
||||
$ docker buildx create --use --name insecure-builder --buildkitd-flags '--allow-insecure-entitlement security.insecure'
|
||||
@@ -753,6 +758,11 @@ will be put in subdirectories by their platform.
|
||||
Attribute key:
|
||||
|
||||
- `dest` - destination directory where files will be written
|
||||
- `mode` - write mode, either `copy` or `delete`. The default is `copy`.
|
||||
`delete` removes stale files from the destination after exporting the build
|
||||
result. It can be used without `--allow` when `dest` resolves to a
|
||||
subdirectory of the current working directory. If `dest` is the current working
|
||||
directory or resolves outside it, pass `--allow=buildx.local.delete`.
|
||||
|
||||
For more information, see
|
||||
[Local and tar exporters](https://docs.docker.com/build/exporters/local-tar/).
|
||||
|
||||
@@ -8,7 +8,7 @@ Start a build
|
||||
| Name | Type | Default | Description |
|
||||
|:--------------------|:--------------|:----------|:-------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `--add-host` | `stringSlice` | | Add a custom host-to-IP mapping (format: `host:ip`) |
|
||||
| `--allow` | `stringArray` | | Allow extra privileged entitlement (e.g., `network.host`, `security.insecure`, `device`) |
|
||||
| `--allow` | `stringArray` | | Allow extra privileged entitlement (e.g., `network.host`, `security.insecure`, `device`, `buildx.local.delete`) |
|
||||
| `--annotation` | `stringArray` | | Add annotation to the image |
|
||||
| `--attest` | `stringArray` | | Attestation parameters (format: `type=sbom,generator=image`) |
|
||||
| `--build-arg` | `stringArray` | | Set build-time variables |
|
||||
|
||||
@@ -12,7 +12,7 @@ Start a build
|
||||
| Name | Type | Default | Description |
|
||||
|:--------------------|:--------------|:----------|:-------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `--add-host` | `stringSlice` | | Add a custom host-to-IP mapping (format: `host:ip`) |
|
||||
| `--allow` | `stringArray` | | Allow extra privileged entitlement (e.g., `network.host`, `security.insecure`, `device`) |
|
||||
| `--allow` | `stringArray` | | Allow extra privileged entitlement (e.g., `network.host`, `security.insecure`, `device`, `buildx.local.delete`) |
|
||||
| `--annotation` | `stringArray` | | Add annotation to the image |
|
||||
| `--attest` | `stringArray` | | Attestation parameters (format: `type=sbom,generator=image`) |
|
||||
| `--build-arg` | `stringArray` | | Set build-time variables |
|
||||
@@ -51,4 +51,3 @@ Start a build
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){
|
||||
testBakePrintRemoteContextSubdir,
|
||||
testBakeLocal,
|
||||
testBakeLocalMulti,
|
||||
testBakeLocalExportDeleteMode,
|
||||
testBakeRemote,
|
||||
testBakeRemoteAuth,
|
||||
testBakeRemoteCmdContext,
|
||||
@@ -673,6 +674,80 @@ services:
|
||||
require.FileExists(t, filepath.Join(dirDest2, "foo"))
|
||||
}
|
||||
|
||||
func testBakeLocalExportDeleteMode(t *testing.T, sb integration.Sandbox) {
|
||||
dockerfile := []byte(`
|
||||
FROM scratch
|
||||
COPY foo /foo
|
||||
`)
|
||||
bakefile := []byte(`
|
||||
target "default" {
|
||||
output = ["type=local,dest=out,mode=delete"]
|
||||
}
|
||||
`)
|
||||
|
||||
t.Run("definition requires allow", func(t *testing.T) {
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("docker-bake.hcl", bakefile, 0600),
|
||||
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||
fstest.CreateFile("foo", []byte("foo"), 0600),
|
||||
)
|
||||
|
||||
cmd := buildxCmd(sb, withDir(dir), withArgs("bake", "--progress=rawjson"))
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.Error(t, err, string(out))
|
||||
require.Contains(t, string(out), "--allow=buildx.local.delete")
|
||||
})
|
||||
|
||||
t.Run("set requires allow", func(t *testing.T) {
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("docker-bake.hcl", []byte(`target "default" {}`), 0600),
|
||||
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||
fstest.CreateFile("foo", []byte("foo"), 0600),
|
||||
)
|
||||
|
||||
cmd := buildxCmd(sb, withDir(dir), withArgs("bake", "--progress=rawjson", "--set", "*.output=type=local,dest=out,mode=delete"))
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.Error(t, err, string(out))
|
||||
require.Contains(t, string(out), "--allow=buildx.local.delete")
|
||||
})
|
||||
|
||||
t.Run("allow does not accept value", func(t *testing.T) {
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("docker-bake.hcl", bakefile, 0600),
|
||||
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||
fstest.CreateFile("foo", []byte("foo"), 0600),
|
||||
)
|
||||
|
||||
out, err := bakeCmd(sb, withDir(dir), withArgs("--allow=buildx.local.delete=true"))
|
||||
require.Error(t, err, out)
|
||||
require.Contains(t, out, "buildx.local.delete does not accept a value")
|
||||
})
|
||||
|
||||
t.Run("allow deletes stale files", func(t *testing.T) {
|
||||
skipNoCompatBuildKit(t, sb, ">= 0.31.0-0", "local exporter mode=delete")
|
||||
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("docker-bake.hcl", bakefile, 0600),
|
||||
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||
fstest.CreateFile("foo", []byte("foo"), 0600),
|
||||
)
|
||||
dest := filepath.Join(dir, "out")
|
||||
stale := filepath.Join(dest, "stale")
|
||||
require.NoError(t, os.MkdirAll(dest, 0o755))
|
||||
require.NoError(t, os.WriteFile(stale, []byte("stale"), 0o600))
|
||||
|
||||
out, err := bakeCmd(sb, withDir(dir), withArgs("--allow=buildx.local.delete"))
|
||||
require.NoError(t, err, out)
|
||||
require.FileExists(t, filepath.Join(dest, "foo"))
|
||||
_, err = os.Stat(stale)
|
||||
require.ErrorIs(t, err, os.ErrNotExist)
|
||||
})
|
||||
}
|
||||
|
||||
func testBakeRemote(t *testing.T, sb integration.Sandbox) {
|
||||
bakefile := []byte(`
|
||||
target "default" {
|
||||
|
||||
@@ -63,6 +63,7 @@ var buildTests = []func(t *testing.T, sb integration.Sandbox){
|
||||
testBuildLocalStateRemote,
|
||||
testImageIDOutput,
|
||||
testBuildLocalExport,
|
||||
testBuildLocalExportDeleteMode,
|
||||
testBuildRegistryExport,
|
||||
testBuildRegistryExportAttestations,
|
||||
testBuildTarExport,
|
||||
@@ -491,6 +492,69 @@ func testBuildLocalExport(t *testing.T, sb integration.Sandbox) {
|
||||
require.Equal(t, "foo", string(dt))
|
||||
}
|
||||
|
||||
func testBuildLocalExportDeleteMode(t *testing.T, sb integration.Sandbox) {
|
||||
t.Run("requires allow for current working directory", func(t *testing.T) {
|
||||
dir := createTestProject(t)
|
||||
out, err := buildCmd(sb, withDir(dir), withArgs("--output=type=local,dest=.,mode=delete", "."))
|
||||
require.Error(t, err, out)
|
||||
require.Contains(t, out, "--allow=buildx.local.delete")
|
||||
})
|
||||
|
||||
t.Run("requires allow for outside destination", func(t *testing.T) {
|
||||
dir := createTestProject(t)
|
||||
dest := filepath.Join(t.TempDir(), "out")
|
||||
out, err := buildCmd(sb, withDir(dir), withArgs("--output=type=local,dest="+dest+",mode=delete", "."))
|
||||
require.Error(t, err, out)
|
||||
require.Contains(t, out, "--allow=buildx.local.delete")
|
||||
})
|
||||
|
||||
t.Run("requires allow for symlink outside destination", func(t *testing.T) {
|
||||
dir := createTestProject(t)
|
||||
require.NoError(t, os.Symlink(t.TempDir(), filepath.Join(dir, "out")))
|
||||
|
||||
out, err := buildCmd(sb, withDir(dir), withArgs("--output=type=local,dest=out,mode=delete", "."))
|
||||
require.Error(t, err, out)
|
||||
require.Contains(t, out, "--allow=buildx.local.delete")
|
||||
})
|
||||
|
||||
t.Run("allow does not accept value", func(t *testing.T) {
|
||||
dir := createTestProject(t)
|
||||
out, err := buildCmd(sb, withDir(dir), withArgs("--allow=buildx.local.delete=true", "--output=type=local,dest=out", "."))
|
||||
require.Error(t, err, out)
|
||||
require.Contains(t, out, "buildx.local.delete does not accept a value")
|
||||
})
|
||||
|
||||
t.Run("deletes stale files in subdirectory without allow", func(t *testing.T) {
|
||||
skipNoCompatBuildKit(t, sb, ">= 0.31.0-0", "local exporter mode=delete")
|
||||
|
||||
dir := createTestProject(t)
|
||||
dest := filepath.Join(dir, "out")
|
||||
stale := filepath.Join(dest, "stale")
|
||||
require.NoError(t, os.MkdirAll(dest, 0o755))
|
||||
require.NoError(t, os.WriteFile(stale, []byte("stale"), 0o600))
|
||||
|
||||
out, err := buildCmd(sb, withDir(dir), withArgs("--output=type=local,dest=out,mode=delete", "."))
|
||||
require.NoError(t, err, out)
|
||||
require.FileExists(t, filepath.Join(dest, "bar"))
|
||||
_, err = os.Stat(stale)
|
||||
require.ErrorIs(t, err, os.ErrNotExist)
|
||||
})
|
||||
|
||||
t.Run("allow permits current working directory", func(t *testing.T) {
|
||||
skipNoCompatBuildKit(t, sb, ">= 0.31.0-0", "local exporter mode=delete")
|
||||
|
||||
dir := createTestProject(t)
|
||||
stale := filepath.Join(dir, "stale")
|
||||
require.NoError(t, os.WriteFile(stale, []byte("stale"), 0o600))
|
||||
|
||||
out, err := buildCmd(sb, withDir(dir), withArgs("--allow=buildx.local.delete", "--output=type=local,dest=.,mode=delete", "."))
|
||||
require.NoError(t, err, out)
|
||||
require.FileExists(t, filepath.Join(dir, "bar"))
|
||||
_, err = os.Stat(stale)
|
||||
require.ErrorIs(t, err, os.ErrNotExist)
|
||||
})
|
||||
}
|
||||
|
||||
func testBuildTarExport(t *testing.T, sb integration.Sandbox) {
|
||||
dir := createTestProject(t)
|
||||
outdir := path.Join(dir, "out")
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
package buildflags
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/moby/buildkit/util/entitlements"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func ParseEntitlements(in []string) ([]string, error) {
|
||||
const EntitlementBuildxLocalDelete = "buildx.local.delete"
|
||||
|
||||
func ParseEntitlements(in []string) (_ []string, allowLocalOutputDelete bool, _ error) {
|
||||
out := make([]string, 0, len(in))
|
||||
for _, v := range in {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
k, _, hasValue := strings.Cut(v, "=")
|
||||
if k == EntitlementBuildxLocalDelete {
|
||||
if hasValue {
|
||||
return nil, false, errors.Errorf("%s does not accept a value", EntitlementBuildxLocalDelete)
|
||||
}
|
||||
allowLocalOutputDelete = true
|
||||
continue
|
||||
}
|
||||
if _, _, err := entitlements.Parse(v); err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
out = append(out, v)
|
||||
}
|
||||
return out, nil
|
||||
return out, allowLocalOutputDelete, nil
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// GetWd retrieves the current working directory.
|
||||
@@ -28,3 +31,122 @@ func ToAbs(path string) string {
|
||||
}
|
||||
return SanitizePath(path)
|
||||
}
|
||||
|
||||
func EvaluateToExistingPath(in string) (string, string, error) {
|
||||
in, err := filepath.Abs(in)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
volLen := volumeNameLen(in)
|
||||
pathSeparator := string(os.PathSeparator)
|
||||
|
||||
if volLen < len(in) && os.IsPathSeparator(in[volLen]) {
|
||||
volLen++
|
||||
}
|
||||
vol := in[:volLen]
|
||||
dest := vol
|
||||
linksWalked := 0
|
||||
var end int
|
||||
for start := volLen; start < len(in); start = end {
|
||||
for start < len(in) && os.IsPathSeparator(in[start]) {
|
||||
start++
|
||||
}
|
||||
end = start
|
||||
for end < len(in) && !os.IsPathSeparator(in[end]) {
|
||||
end++
|
||||
}
|
||||
|
||||
if end == start {
|
||||
break
|
||||
} else if in[start:end] == "." {
|
||||
continue
|
||||
} else if in[start:end] == ".." {
|
||||
var r int
|
||||
for r = len(dest) - 1; r >= volLen; r-- {
|
||||
if os.IsPathSeparator(dest[r]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if r < volLen || dest[r+1:] == ".." {
|
||||
if len(dest) > volLen {
|
||||
dest += pathSeparator
|
||||
}
|
||||
dest += ".."
|
||||
} else {
|
||||
dest = dest[:r]
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if len(dest) > volumeNameLen(dest) && !os.IsPathSeparator(dest[len(dest)-1]) {
|
||||
dest += pathSeparator
|
||||
}
|
||||
dest += in[start:end]
|
||||
|
||||
fi, err := os.Lstat(dest)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
for r := len(dest) - 1; r >= volLen; r-- {
|
||||
if os.IsPathSeparator(dest[r]) {
|
||||
return dest[:r], in[start:], nil
|
||||
}
|
||||
}
|
||||
return vol, in[start:], nil
|
||||
}
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if fi.Mode()&fs.ModeSymlink == 0 {
|
||||
if !fi.Mode().IsDir() && end < len(in) {
|
||||
return "", "", syscall.ENOTDIR
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
linksWalked++
|
||||
if linksWalked > 255 {
|
||||
return "", "", errors.New("too many symlinks")
|
||||
}
|
||||
|
||||
link, err := os.Readlink(dest)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
in = link + in[end:]
|
||||
|
||||
v := volumeNameLen(link)
|
||||
if v > 0 {
|
||||
if v < len(link) && os.IsPathSeparator(link[v]) {
|
||||
v++
|
||||
}
|
||||
vol = link[:v]
|
||||
dest = vol
|
||||
end = len(vol)
|
||||
} else if len(link) > 0 && os.IsPathSeparator(link[0]) {
|
||||
dest = link[:1]
|
||||
end = 1
|
||||
vol = link[:1]
|
||||
volLen = 1
|
||||
} else {
|
||||
var r int
|
||||
for r = len(dest) - 1; r >= volLen; r-- {
|
||||
if os.IsPathSeparator(dest[r]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if r < volLen {
|
||||
dest = vol
|
||||
} else {
|
||||
dest = dest[:r]
|
||||
}
|
||||
end = 0
|
||||
}
|
||||
}
|
||||
return filepath.Clean(dest), "", nil
|
||||
}
|
||||
|
||||
func volumeNameLen(s string) int {
|
||||
return len(filepath.VolumeName(s))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user