From 473c6ef3066cb49c6290583a0dd5315308334916 Mon Sep 17 00:00:00 2001 From: CrazyMax Date: Wed, 3 Jun 2026 16:19:16 +0200 Subject: [PATCH] bake: requires explicit allow for local output delete mode Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- bake/entitlements.go | 60 ++++++++++++------ bake/entitlements_test.go | 111 ++++++++++++++++++++++++++++++++++ commands/bake.go | 6 +- docs/bake-reference.md | 4 ++ docs/reference/buildx_bake.md | 3 + 5 files changed, 165 insertions(+), 19 deletions(-) diff --git a/bake/entitlements.go b/bake/entitlements.go index bd93a7f34..386b973c8 100644 --- a/bake/entitlements.go +++ b/bake/entitlements.go @@ -17,6 +17,7 @@ import ( "github.com/containerd/console" "github.com/docker/buildx/build" "github.com/docker/buildx/util/osutil" + "github.com/moby/buildkit/client" "github.com/moby/buildkit/util/entitlements" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -26,27 +27,29 @@ import ( type EntitlementKey string const ( - EntitlementKeyNetworkHost EntitlementKey = "network.host" - EntitlementKeySecurityInsecure EntitlementKey = "security.insecure" - EntitlementKeyDevice EntitlementKey = "device" - EntitlementKeyFSRead EntitlementKey = "fs.read" - EntitlementKeyFSWrite EntitlementKey = "fs.write" - EntitlementKeyFS EntitlementKey = "fs" - EntitlementKeyImagePush EntitlementKey = "image.push" - EntitlementKeyImageLoad EntitlementKey = "image.load" - EntitlementKeyImage EntitlementKey = "image" - EntitlementKeySSH EntitlementKey = "ssh" + EntitlementKeyNetworkHost EntitlementKey = "network.host" + EntitlementKeySecurityInsecure EntitlementKey = "security.insecure" + EntitlementKeyDevice EntitlementKey = "device" + EntitlementKeyFSRead EntitlementKey = "fs.read" + EntitlementKeyFSWrite EntitlementKey = "fs.write" + EntitlementKeyFS EntitlementKey = "fs" + EntitlementKeyImagePush EntitlementKey = "image.push" + EntitlementKeyImageLoad EntitlementKey = "image.load" + EntitlementKeyImage EntitlementKey = "image" + EntitlementKeySSH EntitlementKey = "ssh" + EntitlementKeyLocalOutputDelete EntitlementKey = "local-output-delete" ) type EntitlementConf struct { - NetworkHost bool - SecurityInsecure bool - Devices *EntitlementsDevicesConf - FSRead []string - FSWrite []string - ImagePush []string - ImageLoad []string - SSH bool + NetworkHost bool + SecurityInsecure bool + Devices *EntitlementsDevicesConf + FSRead []string + FSWrite []string + ImagePush []string + ImageLoad []string + SSH bool + LocalOutputDelete bool } type EntitlementsDevicesConf struct { @@ -64,6 +67,8 @@ func ParseEntitlements(in []string) (EntitlementConf, error) { conf.SecurityInsecure = true case string(EntitlementKeySSH): conf.SSH = true + case string(EntitlementKeyLocalOutputDelete): + conf.LocalOutputDelete = true default: k, v, _ := strings.Cut(e, "=") switch k { @@ -97,6 +102,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) default: return conf, errors.Errorf("unknown entitlement key %q", k) } @@ -164,6 +171,19 @@ func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) erro rwPaths[p] = struct{}{} } + for _, ex := range bo.Exports { + if ex.Type != client.ExporterLocal { + continue + } + mode, err := client.ParseLocalExporterMode(ex.Attrs["mode"]) + if err != nil { + return err + } + if mode == client.LocalExporterModeDelete && !c.LocalOutputDelete { + expected.LocalOutputDelete = true + } + } + for _, ce := range bo.CacheTo { if ce.Type == "local" { if dest, ok := ce.Attrs["dest"]; ok { @@ -249,6 +269,10 @@ func (c EntitlementConf) Prompt(ctx context.Context, isRemote bool, out io.Write msgsFS = append(msgsFS, " - Forwarding default SSH agent socket") flagsFS = append(flagsFS, string(EntitlementKeySSH)) } + if c.LocalOutputDelete { + msgs = append(msgs, " - Deleting stale files from local output destinations") + flags = append(flags, string(EntitlementKeyLocalOutputDelete)) + } roPaths, rwPaths, commonPaths := groupSamePaths(c.FSRead, c.FSWrite) wd, err := os.Getwd() diff --git a/bake/entitlements_test.go b/bake/entitlements_test.go index b00cfe239..445609941 100644 --- a/bake/entitlements_test.go +++ b/bake/entitlements_test.go @@ -1,6 +1,8 @@ package bake import ( + "bytes" + "context" "fmt" "os" "path/filepath" @@ -10,6 +12,7 @@ import ( "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/client/llb" "github.com/moby/buildkit/util/entitlements" "github.com/stretchr/testify/require" @@ -405,6 +408,73 @@ func TestValidateEntitlements(t *testing.T) { FSWrite: []string{dir1}, }, }, + { + name: "LocalOutputDeleteMissing", + conf: EntitlementConf{ + FSWrite: []string{"*"}, + }, + opt: build.Options{ + Inputs: build.Inputs{ + ContextState: &llb.State{}, + }, + Exports: []client.ExportEntry{ + { + Type: client.ExporterLocal, + OutputDir: dir1, + Attrs: map[string]string{ + "mode": string(client.LocalExporterModeDelete), + }, + }, + }, + ExportsLocalPathsTemporary: []string{dir1}, + }, + expected: EntitlementConf{ + LocalOutputDelete: true, + }, + }, + { + name: "LocalOutputDeleteSet", + conf: EntitlementConf{ + FSWrite: []string{"*"}, + LocalOutputDelete: true, + }, + opt: build.Options{ + Inputs: build.Inputs{ + ContextState: &llb.State{}, + }, + Exports: []client.ExportEntry{ + { + Type: client.ExporterLocal, + OutputDir: dir1, + Attrs: map[string]string{ + "mode": string(client.LocalExporterModeDelete), + }, + }, + }, + ExportsLocalPathsTemporary: []string{dir1}, + }, + }, + { + name: "LocalOutputCopy", + conf: EntitlementConf{ + FSWrite: []string{"*"}, + }, + opt: build.Options{ + Inputs: build.Inputs{ + ContextState: &llb.State{}, + }, + Exports: []client.ExportEntry{ + { + Type: client.ExporterLocal, + OutputDir: dir1, + Attrs: map[string]string{ + "mode": string(client.LocalExporterModeCopy), + }, + }, + }, + ExportsLocalPathsTemporary: []string{dir1}, + }, + }, } for _, tc := range tcases { @@ -416,6 +486,47 @@ func TestValidateEntitlements(t *testing.T) { } } +func TestValidateEntitlementsInvalidLocalOutputMode(t *testing.T) { + _, err := EntitlementConf{}.Validate(map[string]build.Options{ + "test": { + Inputs: build.Inputs{ + ContextState: &llb.State{}, + }, + Exports: []client.ExportEntry{ + { + Type: client.ExporterLocal, + Attrs: map[string]string{ + "mode": "backup", + }, + }, + }, + }, + }) + require.ErrorContains(t, err, `invalid local exporter mode "backup"`) +} + +func TestParseEntitlementsLocalOutputDelete(t *testing.T) { + conf, err := ParseEntitlements([]string{string(EntitlementKeyLocalOutputDelete)}) + 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") +} + +func TestPromptLocalOutputDeleteCannotBeDisabledWithFSEntitlements(t *testing.T) { + t.Setenv("BUILDX_BAKE_ENTITLEMENTS_FS", "0") + + ctx, cancel := context.WithCancelCause(context.Background()) + cancel(nil) + + var out bytes.Buffer + 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") +} + func TestGroupSamePaths(t *testing.T) { tests := []struct { name string diff --git a/commands/bake.go b/commands/bake.go index 480949841..119ea8af7 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -329,7 +329,11 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba if err != nil { return err } - if progressMode != progressui.RawJSONMode { + if progressMode == progressui.RawJSONMode { + if exp.LocalOutputDelete { + return errors.Errorf("additional privileges requested: pass %q to grant requested privileges", "--allow="+string(bake.EntitlementKeyLocalOutputDelete)) + } + } else { if err := exp.Prompt(ctx, url != "", &syncWriter{w: dockerCli.Err(), wait: printer.Wait}); err != nil { return err } diff --git a/docs/bake-reference.md b/docs/bake-reference.md index 704c8d065..f347fbbdc 100644 --- a/docs/bake-reference.md +++ b/docs/bake-reference.md @@ -901,6 +901,10 @@ target "default" { } ``` +> [!NOTE] +> Local outputs with `mode=delete` require granting `--allow=local-output-delete` +> when invoking `docker buildx bake`. + ### `target.policy` Policies to validate build sources and metadata. Each entry uses the same keys diff --git a/docs/reference/buildx_bake.md b/docs/reference/buildx_bake.md index 939341747..dd12d7358 100644 --- a/docs/reference/buildx_bake.md +++ b/docs/reference/buildx_bake.md @@ -85,6 +85,9 @@ 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 +permission to delete stale files when `mode=delete` is set. + ### Example: fs.read Given the following Bake configuration, Bake would need to access the parent