From 9dcb37f47c2179f80c3c950bc5e3b72538c57b43 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Fri, 20 Mar 2026 22:21:56 -0700 Subject: [PATCH 1/3] policy: add --platform to eval Allow buildx policy eval to accept an explicit target platform and skip worker platform discovery when the flag is provided. Signed-off-by: Tonis Tiigi --- commands/policy/eval.go | 36 +++++++++++++++++++----------------- commands/policy/eval_test.go | 29 +++++++++++++++++++++++++++++ commands/policy/platform.go | 25 +++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 commands/policy/platform.go diff --git a/commands/policy/eval.go b/commands/policy/eval.go index 9cd484cdd..7454a61df 100644 --- a/commands/policy/eval.go +++ b/commands/policy/eval.go @@ -33,6 +33,7 @@ type evalOpts struct { filename string printOutput bool fields []string + platform string builder *string } @@ -52,6 +53,7 @@ func evalCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command { cmd.Flags().StringVar(&opts.filename, "filename", "Dockerfile", "Policy filename to evaluate") cmd.Flags().BoolVar(&opts.printOutput, "print", false, "Print policy output") cmd.Flags().StringSliceVar(&opts.fields, "fields", nil, "Fields to evaluate") + cmd.Flags().StringVar(&opts.platform, "platform", "", "Target platform for policy evaluation") return cmd } @@ -81,29 +83,29 @@ func runEval(ctx context.Context, dockerCli command.Cli, source string, opts eva return err } - workers, err := c.ListWorkers(ctx) - if err != nil { - return err - } + var p ocispecs.Platform + if opts.platform != "" { + parsedPlatform, err := parsePlatform(opts.platform) + if err != nil { + return err + } + p = *parsedPlatform + } else { + workers, err := c.ListWorkers(ctx) + if err != nil { + return err + } - if len(workers) == 0 { - return errors.New("no workers available in the builder") - } + if len(workers) == 0 { + return errors.New("no workers available in the builder") + } - defaultPlatform := workers[0].Platforms[0] - p := ocispecs.Platform{ - Architecture: defaultPlatform.Architecture, - OS: defaultPlatform.OS, - Variant: defaultPlatform.Variant, + p = workers[0].Platforms[0] } metaResolver := sourcemeta.NewResolver(c) defer metaResolver.Close() - platform := &pb.Platform{ - Architecture: p.Architecture, - OS: p.OS, - Variant: p.Variant, - } + platform := toPBPlatform(p) verifier := policy.SignatureVerifier(confutil.NewConfig(dockerCli)) if opts.printOutput { diff --git a/commands/policy/eval_test.go b/commands/policy/eval_test.go index 77522a5b3..e1cc6f46b 100644 --- a/commands/policy/eval_test.go +++ b/commands/policy/eval_test.go @@ -6,10 +6,39 @@ import ( policytypes "github.com/docker/buildx/policy" "github.com/docker/buildx/util/sourcemeta" gwpb "github.com/moby/buildkit/frontend/gateway/pb" + "github.com/moby/buildkit/solver/pb" ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/require" ) +func TestParsePlatform(t *testing.T) { + t.Run("normalize", func(t *testing.T) { + platform, err := parsePlatform("linux/arm/v7") + require.NoError(t, err) + require.Equal(t, &ocispecs.Platform{ + OS: "linux", + Architecture: "arm", + Variant: "v7", + }, platform) + }) + + t.Run("invalid", func(t *testing.T) { + platform, err := parsePlatform("not-a-platform") + require.Nil(t, platform) + require.Error(t, err) + require.ErrorContains(t, err, "invalid platform \"not-a-platform\"") + require.ErrorContains(t, err, "unknown operating system or architecture") + }) +} + +func TestToPBPlatform(t *testing.T) { + platform := ocispecs.Platform{OS: "linux", Architecture: "amd64"} + require.Equal(t, &pb.Platform{ + OS: "linux", + Architecture: "amd64", + }, toPBPlatform(platform)) +} + func TestSourceResolverOptIncludesResolveAttestations(t *testing.T) { req := &gwpb.ResolveSourceMetaRequest{ ResolveMode: "default", diff --git a/commands/policy/platform.go b/commands/policy/platform.go new file mode 100644 index 000000000..cca1be0d8 --- /dev/null +++ b/commands/policy/platform.go @@ -0,0 +1,25 @@ +package policy + +import ( + "github.com/containerd/platforms" + "github.com/moby/buildkit/solver/pb" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" +) + +func parsePlatform(platform string) (*ocispecs.Platform, error) { + p, err := platforms.Parse(platform) + if err != nil { + return nil, errors.Wrapf(err, "invalid platform %q", platform) + } + p = platforms.Normalize(p) + return &p, nil +} + +func toPBPlatform(platform ocispecs.Platform) *pb.Platform { + return &pb.Platform{ + Architecture: platform.Architecture, + OS: platform.OS, + Variant: platform.Variant, + } +} From 7df7b4205bba81c58d7142ea195e14d9a34031a9 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Fri, 20 Mar 2026 22:29:32 -0700 Subject: [PATCH 2/3] policy: align eval file flag Use --file/-f for policy eval and keep hidden --filename as a backward-compatible alias. Signed-off-by: Tonis Tiigi --- commands/policy/eval.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/commands/policy/eval.go b/commands/policy/eval.go index 7454a61df..3c7361a90 100644 --- a/commands/policy/eval.go +++ b/commands/policy/eval.go @@ -50,10 +50,14 @@ func evalCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command { return runEval(cmd.Context(), dockerCli, args[0], opts) }, } - cmd.Flags().StringVar(&opts.filename, "filename", "Dockerfile", "Policy filename to evaluate") - cmd.Flags().BoolVar(&opts.printOutput, "print", false, "Print policy output") - cmd.Flags().StringSliceVar(&opts.fields, "fields", nil, "Fields to evaluate") - cmd.Flags().StringVar(&opts.platform, "platform", "", "Target platform for policy evaluation") + flags := cmd.Flags() + flags.StringVarP(&opts.filename, "file", "f", "Dockerfile", "Policy filename to evaluate") + flags.BoolVar(&opts.printOutput, "print", false, "Print policy output") + flags.StringSliceVar(&opts.fields, "fields", nil, "Fields to evaluate") + flags.StringVar(&opts.platform, "platform", "", "Target platform for policy evaluation") + // Deprecated: use --file instead + flags.StringVar(&opts.filename, "filename", "Dockerfile", "Policy filename to evaluate") + flags.MarkHidden("filename") return cmd } From 64cd4136d974ae5b188c9280cfb25744b5a26870 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Fri, 20 Mar 2026 22:58:15 -0700 Subject: [PATCH 3/3] policy: support reading policy from stdin via --file - Allow passing policy content through stdin by specifying "--file -" in the eval command. This enables piping policy data without requiring a file on disk. Signed-off-by: Tonis Tiigi --- commands/policy/eval.go | 20 ++++++++-- docs/reference/buildx_policy_eval.md | 3 +- tests/policy_eval.go | 58 ++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/commands/policy/eval.go b/commands/policy/eval.go index 3c7361a90..c13d46dda 100644 --- a/commands/policy/eval.go +++ b/commands/policy/eval.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "io/fs" "maps" "os" @@ -191,9 +192,8 @@ func runEval(ctx context.Context, dockerCli command.Cli, source string, opts eva if opts.filename == "" { return errors.New("filename is required") } - policyName := opts.filename - policyFile := policyName + ".rego" - policyData, err := os.ReadFile(policyFile) + policyName, policyFile := policyFileNames(opts.filename) + policyData, err := readPolicyData(policyFile, os.Stdin) if err != nil { return errors.Wrapf(err, "failed to read policy file %s", policyFile) } @@ -267,6 +267,20 @@ func runEval(ctx context.Context, dockerCli command.Cli, source string, opts eva } } +func policyFileNames(filename string) (string, string) { + if filename == "-" { + return "stdin", filename + } + return filename, filename + ".rego" +} + +func readPolicyData(filename string, stdin io.Reader) ([]byte, error) { + if filename == "-" { + return io.ReadAll(stdin) + } + return os.ReadFile(filename) +} + func selectReloadFields(fields []string, unknowns []string) ([]string, []string) { if len(fields) == 0 { return nil, nil diff --git a/docs/reference/buildx_policy_eval.md b/docs/reference/buildx_policy_eval.md index ad8d8d0b0..9dc5dc378 100644 --- a/docs/reference/buildx_policy_eval.md +++ b/docs/reference/buildx_policy_eval.md @@ -10,7 +10,8 @@ Evaluate policy for a source | `--builder` | `string` | | Override the configured builder instance | | `-D`, `--debug` | `bool` | | Enable debug logging | | `--fields` | `stringSlice` | | Fields to evaluate | -| `--filename` | `string` | `Dockerfile` | Policy filename to evaluate | +| `-f`, `--file` | `string` | `Dockerfile` | Policy filename to evaluate | +| `--platform` | `string` | | Target platform for policy evaluation | | `--print` | `bool` | | Print policy output | diff --git a/tests/policy_eval.go b/tests/policy_eval.go index 19018648d..f0b695db1 100644 --- a/tests/policy_eval.go +++ b/tests/policy_eval.go @@ -21,6 +21,7 @@ import ( var policyEvalTests = []func(t *testing.T, sb integration.Sandbox){ testPolicyEvalAllow, testPolicyEvalDeny, + testPolicyEvalStdinFile, testPolicyEvalPrint, testPolicyEvalFields, testPolicyEvalLabel, @@ -88,6 +89,63 @@ decision := {"allow": allow} require.Contains(t, string(out), "policy denied") } +func testPolicyEvalStdinFile(t *testing.T, sb integration.Sandbox) { + skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+") + testCases := []struct { + name string + policy string + wantErrContains string + }{ + { + name: "allow", + policy: ` +package docker + +default allow = false + +allow if not input.image + +allow if input.image.repo == "busybox" + +decision := {"allow": allow} +`, + }, + { + name: "deny", + policy: ` +package docker + +default allow = false + +allow if input.image.repo == "alpine" + +decision := {"allow": allow} +`, + wantErrContains: "policy denied", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cmd := buildxCmd(sb, withArgs( + "policy", + "eval", + "--file", + "-", + "docker-image://busybox:latest", + )) + cmd.Stdin = strings.NewReader(tc.policy) + out, err := cmd.CombinedOutput() + if tc.wantErrContains == "" { + require.NoError(t, err, string(out)) + return + } + require.Error(t, err, string(out)) + require.Contains(t, string(out), tc.wantErrContains) + }) + } +} + func testPolicyEvalPrint(t *testing.T, sb integration.Sandbox) { skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+") cmd := buildxCmd(sb, withArgs(