Skip to content

Commit 7ac8168

Browse files
authored
Merge pull request #3738 from tonistiigi/policy-eval-upt
Updated for policy eval
2 parents be5de2c + 64cd413 commit 7ac8168

5 files changed

Lines changed: 157 additions & 24 deletions

File tree

commands/policy/eval.go

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"io"
78
"io/fs"
89
"maps"
910
"os"
@@ -33,6 +34,7 @@ type evalOpts struct {
3334
filename string
3435
printOutput bool
3536
fields []string
37+
platform string
3638
builder *string
3739
}
3840

@@ -49,9 +51,14 @@ func evalCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
4951
return runEval(cmd.Context(), dockerCli, args[0], opts)
5052
},
5153
}
52-
cmd.Flags().StringVar(&opts.filename, "filename", "Dockerfile", "Policy filename to evaluate")
53-
cmd.Flags().BoolVar(&opts.printOutput, "print", false, "Print policy output")
54-
cmd.Flags().StringSliceVar(&opts.fields, "fields", nil, "Fields to evaluate")
54+
flags := cmd.Flags()
55+
flags.StringVarP(&opts.filename, "file", "f", "Dockerfile", "Policy filename to evaluate")
56+
flags.BoolVar(&opts.printOutput, "print", false, "Print policy output")
57+
flags.StringSliceVar(&opts.fields, "fields", nil, "Fields to evaluate")
58+
flags.StringVar(&opts.platform, "platform", "", "Target platform for policy evaluation")
59+
// Deprecated: use --file instead
60+
flags.StringVar(&opts.filename, "filename", "Dockerfile", "Policy filename to evaluate")
61+
flags.MarkHidden("filename")
5562
return cmd
5663
}
5764

@@ -81,29 +88,29 @@ func runEval(ctx context.Context, dockerCli command.Cli, source string, opts eva
8188
return err
8289
}
8390

84-
workers, err := c.ListWorkers(ctx)
85-
if err != nil {
86-
return err
87-
}
91+
var p ocispecs.Platform
92+
if opts.platform != "" {
93+
parsedPlatform, err := parsePlatform(opts.platform)
94+
if err != nil {
95+
return err
96+
}
97+
p = *parsedPlatform
98+
} else {
99+
workers, err := c.ListWorkers(ctx)
100+
if err != nil {
101+
return err
102+
}
88103

89-
if len(workers) == 0 {
90-
return errors.New("no workers available in the builder")
91-
}
104+
if len(workers) == 0 {
105+
return errors.New("no workers available in the builder")
106+
}
92107

93-
defaultPlatform := workers[0].Platforms[0]
94-
p := ocispecs.Platform{
95-
Architecture: defaultPlatform.Architecture,
96-
OS: defaultPlatform.OS,
97-
Variant: defaultPlatform.Variant,
108+
p = workers[0].Platforms[0]
98109
}
99110
metaResolver := sourcemeta.NewResolver(c)
100111
defer metaResolver.Close()
101112

102-
platform := &pb.Platform{
103-
Architecture: p.Architecture,
104-
OS: p.OS,
105-
Variant: p.Variant,
106-
}
113+
platform := toPBPlatform(p)
107114
verifier := policy.SignatureVerifier(confutil.NewConfig(dockerCli))
108115

109116
if opts.printOutput {
@@ -185,9 +192,8 @@ func runEval(ctx context.Context, dockerCli command.Cli, source string, opts eva
185192
if opts.filename == "" {
186193
return errors.New("filename is required")
187194
}
188-
policyName := opts.filename
189-
policyFile := policyName + ".rego"
190-
policyData, err := os.ReadFile(policyFile)
195+
policyName, policyFile := policyFileNames(opts.filename)
196+
policyData, err := readPolicyData(policyFile, os.Stdin)
191197
if err != nil {
192198
return errors.Wrapf(err, "failed to read policy file %s", policyFile)
193199
}
@@ -261,6 +267,20 @@ func runEval(ctx context.Context, dockerCli command.Cli, source string, opts eva
261267
}
262268
}
263269

270+
func policyFileNames(filename string) (string, string) {
271+
if filename == "-" {
272+
return "stdin", filename
273+
}
274+
return filename, filename + ".rego"
275+
}
276+
277+
func readPolicyData(filename string, stdin io.Reader) ([]byte, error) {
278+
if filename == "-" {
279+
return io.ReadAll(stdin)
280+
}
281+
return os.ReadFile(filename)
282+
}
283+
264284
func selectReloadFields(fields []string, unknowns []string) ([]string, []string) {
265285
if len(fields) == 0 {
266286
return nil, nil

commands/policy/eval_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,39 @@ import (
66
policytypes "github.com/docker/buildx/policy"
77
"github.com/docker/buildx/util/sourcemeta"
88
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
9+
"github.com/moby/buildkit/solver/pb"
910
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
1011
"github.com/stretchr/testify/require"
1112
)
1213

14+
func TestParsePlatform(t *testing.T) {
15+
t.Run("normalize", func(t *testing.T) {
16+
platform, err := parsePlatform("linux/arm/v7")
17+
require.NoError(t, err)
18+
require.Equal(t, &ocispecs.Platform{
19+
OS: "linux",
20+
Architecture: "arm",
21+
Variant: "v7",
22+
}, platform)
23+
})
24+
25+
t.Run("invalid", func(t *testing.T) {
26+
platform, err := parsePlatform("not-a-platform")
27+
require.Nil(t, platform)
28+
require.Error(t, err)
29+
require.ErrorContains(t, err, "invalid platform \"not-a-platform\"")
30+
require.ErrorContains(t, err, "unknown operating system or architecture")
31+
})
32+
}
33+
34+
func TestToPBPlatform(t *testing.T) {
35+
platform := ocispecs.Platform{OS: "linux", Architecture: "amd64"}
36+
require.Equal(t, &pb.Platform{
37+
OS: "linux",
38+
Architecture: "amd64",
39+
}, toPBPlatform(platform))
40+
}
41+
1342
func TestSourceResolverOptIncludesResolveAttestations(t *testing.T) {
1443
req := &gwpb.ResolveSourceMetaRequest{
1544
ResolveMode: "default",

commands/policy/platform.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package policy
2+
3+
import (
4+
"github.com/containerd/platforms"
5+
"github.com/moby/buildkit/solver/pb"
6+
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
7+
"github.com/pkg/errors"
8+
)
9+
10+
func parsePlatform(platform string) (*ocispecs.Platform, error) {
11+
p, err := platforms.Parse(platform)
12+
if err != nil {
13+
return nil, errors.Wrapf(err, "invalid platform %q", platform)
14+
}
15+
p = platforms.Normalize(p)
16+
return &p, nil
17+
}
18+
19+
func toPBPlatform(platform ocispecs.Platform) *pb.Platform {
20+
return &pb.Platform{
21+
Architecture: platform.Architecture,
22+
OS: platform.OS,
23+
Variant: platform.Variant,
24+
}
25+
}

docs/reference/buildx_policy_eval.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ Evaluate policy for a source
1010
| `--builder` | `string` | | Override the configured builder instance |
1111
| `-D`, `--debug` | `bool` | | Enable debug logging |
1212
| `--fields` | `stringSlice` | | Fields to evaluate |
13-
| `--filename` | `string` | `Dockerfile` | Policy filename to evaluate |
13+
| `-f`, `--file` | `string` | `Dockerfile` | Policy filename to evaluate |
14+
| `--platform` | `string` | | Target platform for policy evaluation |
1415
| `--print` | `bool` | | Print policy output |
1516

1617

tests/policy_eval.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
var policyEvalTests = []func(t *testing.T, sb integration.Sandbox){
2222
testPolicyEvalAllow,
2323
testPolicyEvalDeny,
24+
testPolicyEvalStdinFile,
2425
testPolicyEvalPrint,
2526
testPolicyEvalFields,
2627
testPolicyEvalLabel,
@@ -88,6 +89,63 @@ decision := {"allow": allow}
8889
require.Contains(t, string(out), "policy denied")
8990
}
9091

92+
func testPolicyEvalStdinFile(t *testing.T, sb integration.Sandbox) {
93+
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
94+
testCases := []struct {
95+
name string
96+
policy string
97+
wantErrContains string
98+
}{
99+
{
100+
name: "allow",
101+
policy: `
102+
package docker
103+
104+
default allow = false
105+
106+
allow if not input.image
107+
108+
allow if input.image.repo == "busybox"
109+
110+
decision := {"allow": allow}
111+
`,
112+
},
113+
{
114+
name: "deny",
115+
policy: `
116+
package docker
117+
118+
default allow = false
119+
120+
allow if input.image.repo == "alpine"
121+
122+
decision := {"allow": allow}
123+
`,
124+
wantErrContains: "policy denied",
125+
},
126+
}
127+
128+
for _, tc := range testCases {
129+
t.Run(tc.name, func(t *testing.T) {
130+
cmd := buildxCmd(sb, withArgs(
131+
"policy",
132+
"eval",
133+
"--file",
134+
"-",
135+
"docker-image://busybox:latest",
136+
))
137+
cmd.Stdin = strings.NewReader(tc.policy)
138+
out, err := cmd.CombinedOutput()
139+
if tc.wantErrContains == "" {
140+
require.NoError(t, err, string(out))
141+
return
142+
}
143+
require.Error(t, err, string(out))
144+
require.Contains(t, string(out), tc.wantErrContains)
145+
})
146+
}
147+
}
148+
91149
func testPolicyEvalPrint(t *testing.T, sb integration.Sandbox) {
92150
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
93151
cmd := buildxCmd(sb, withArgs(

0 commit comments

Comments
 (0)