Skip to content

Commit 32b6b75

Browse files
authored
Merge pull request #3593 from tonistiigi/opa-bake-tests
policy: add policy to bake and improve test coverage
2 parents 5a5d59f + 9f1daff commit 32b6b75

17 files changed

Lines changed: 1533 additions & 121 deletions

File tree

bake/bake.go

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -588,7 +588,7 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error)
588588
// IMPORTANT: if you add more fields here, do not forget to update
589589
// docs/reference/buildx_bake.md (--set) and https://docs.docker.com/build/bake/overrides/
590590
switch keys[1] {
591-
case "output", "cache-to", "cache-from", "tags", "platform", "secrets", "ssh", "attest", "entitlements", "network", "annotations":
591+
case "output", "cache-to", "cache-from", "tags", "platform", "secrets", "ssh", "attest", "entitlements", "network", "annotations", "policy":
592592
if len(parts) == 2 {
593593
override.Append = appendTo
594594
override.ArrValue = append(override.ArrValue, parts[1])
@@ -732,31 +732,32 @@ type Target struct {
732732
// Inherits is the only field that cannot be overridden with --set
733733
Inherits []string `json:"inherits,omitempty" hcl:"inherits,optional" cty:"inherits"`
734734

735-
Annotations []string `json:"annotations,omitempty" hcl:"annotations,optional" cty:"annotations"`
736-
Attest buildflags.Attests `json:"attest,omitempty" hcl:"attest,optional" cty:"attest"`
737-
Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"`
738-
Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"`
739-
Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional" cty:"dockerfile"`
740-
DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional" cty:"dockerfile-inline"`
741-
Args map[string]*string `json:"args,omitempty" hcl:"args,optional" cty:"args"`
742-
Labels map[string]*string `json:"labels,omitempty" hcl:"labels,optional" cty:"labels"`
743-
Tags []string `json:"tags,omitempty" hcl:"tags,optional" cty:"tags"`
744-
CacheFrom buildflags.CacheOptions `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"`
745-
CacheTo buildflags.CacheOptions `json:"cache-to,omitempty" hcl:"cache-to,optional" cty:"cache-to"`
746-
Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"`
747-
Secrets buildflags.Secrets `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"`
748-
SSH buildflags.SSHKeys `json:"ssh,omitempty" hcl:"ssh,optional" cty:"ssh"`
749-
Platforms []string `json:"platforms,omitempty" hcl:"platforms,optional" cty:"platforms"`
750-
Outputs buildflags.Exports `json:"output,omitempty" hcl:"output,optional" cty:"output"`
751-
Pull *bool `json:"pull,omitempty" hcl:"pull,optional" cty:"pull"`
752-
NoCache *bool `json:"no-cache,omitempty" hcl:"no-cache,optional" cty:"no-cache"`
753-
NetworkMode *string `json:"network,omitempty" hcl:"network,optional" cty:"network"`
754-
NoCacheFilter []string `json:"no-cache-filter,omitempty" hcl:"no-cache-filter,optional" cty:"no-cache-filter"`
755-
ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional" cty:"shm-size"`
756-
Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional" cty:"ulimits"`
757-
Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"`
758-
Entitlements []string `json:"entitlements,omitempty" hcl:"entitlements,optional" cty:"entitlements"`
759-
ExtraHosts map[string]*string `json:"extra-hosts,omitempty" hcl:"extra-hosts,optional" cty:"extra-hosts"`
735+
Annotations []string `json:"annotations,omitempty" hcl:"annotations,optional" cty:"annotations"`
736+
Attest buildflags.Attests `json:"attest,omitempty" hcl:"attest,optional" cty:"attest"`
737+
Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"`
738+
Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"`
739+
Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional" cty:"dockerfile"`
740+
DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional" cty:"dockerfile-inline"`
741+
Args map[string]*string `json:"args,omitempty" hcl:"args,optional" cty:"args"`
742+
Labels map[string]*string `json:"labels,omitempty" hcl:"labels,optional" cty:"labels"`
743+
Tags []string `json:"tags,omitempty" hcl:"tags,optional" cty:"tags"`
744+
CacheFrom buildflags.CacheOptions `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"`
745+
CacheTo buildflags.CacheOptions `json:"cache-to,omitempty" hcl:"cache-to,optional" cty:"cache-to"`
746+
Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"`
747+
Secrets buildflags.Secrets `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"`
748+
SSH buildflags.SSHKeys `json:"ssh,omitempty" hcl:"ssh,optional" cty:"ssh"`
749+
Platforms []string `json:"platforms,omitempty" hcl:"platforms,optional" cty:"platforms"`
750+
Outputs buildflags.Exports `json:"output,omitempty" hcl:"output,optional" cty:"output"`
751+
Pull *bool `json:"pull,omitempty" hcl:"pull,optional" cty:"pull"`
752+
NoCache *bool `json:"no-cache,omitempty" hcl:"no-cache,optional" cty:"no-cache"`
753+
NetworkMode *string `json:"network,omitempty" hcl:"network,optional" cty:"network"`
754+
NoCacheFilter []string `json:"no-cache-filter,omitempty" hcl:"no-cache-filter,optional" cty:"no-cache-filter"`
755+
ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional" cty:"shm-size"`
756+
Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional" cty:"ulimits"`
757+
Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"`
758+
Entitlements []string `json:"entitlements,omitempty" hcl:"entitlements,optional" cty:"entitlements"`
759+
ExtraHosts map[string]*string `json:"extra-hosts,omitempty" hcl:"extra-hosts,optional" cty:"extra-hosts"`
760+
Policy buildflags.PolicyConfigs `json:"policy,omitempty" hcl:"policy,optional" cty:"policy"`
760761
// IMPORTANT: if you add more fields here, do not forget to update newOverrides/AddOverrides and docs/bake-reference.md.
761762

762763
// linked is a private field to mark a target used as a linked one
@@ -891,6 +892,9 @@ func (t *Target) Merge(t2 *Target) {
891892
if t2.Attest != nil { // merge
892893
t.Attest = t.Attest.Merge(t2.Attest)
893894
}
895+
if t2.Policy != nil { // merge
896+
t.Policy = append(t.Policy, t2.Policy...)
897+
}
894898
if t2.Secrets != nil { // merge
895899
t.Secrets = t.Secrets.Merge(t2.Secrets)
896900
}
@@ -986,6 +990,17 @@ func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementCon
986990
} else {
987991
t.Tags = o.ArrValue
988992
}
993+
case "policy":
994+
if !o.Append {
995+
t.Policy = nil
996+
}
997+
for _, v := range o.ArrValue {
998+
cfg, err := buildflags.ParsePolicyConfig(v)
999+
if err != nil {
1000+
return err
1001+
}
1002+
t.Policy = append(t.Policy, cfg)
1003+
}
9891004
case "cache-from":
9901005
cacheFrom, err := buildflags.ParseCacheEntry(o.ArrValue)
9911006
if err != nil {
@@ -1548,6 +1563,8 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
15481563

15491564
bo.Attests = t.Attest.ToMap()
15501565

1566+
bo.Policy = []buildflags.PolicyConfig(t.Policy)
1567+
15511568
bo.SourcePolicy, err = build.ReadSourcePolicy()
15521569
if err != nil {
15531570
return nil, err

build/build.go

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,7 @@ type Options struct {
9797
SourcePolicy *spb.Policy
9898
GroupRef string
9999
Annotations map[exptypes.AnnotationKey]string // Not used during build, annotations are already set in Exports. Just used to check for support with drivers.
100-
Policy []PolicyConfig
101-
}
102-
103-
type PolicyConfig struct {
104-
Files []policy.File
105-
Reset bool
106-
Disabled bool
107-
Strict *bool
108-
LogLevel *logrus.Level
100+
Policy []buildflags.PolicyConfig
109101
}
110102

111103
type CallFunc struct {
@@ -135,7 +127,7 @@ type policyOpt struct {
135127
LogLevel *logrus.Level
136128
}
137129

138-
func withPolicyConfig(defaultPolicy policyOpt, configs []PolicyConfig) ([]policyOpt, error) {
130+
func withPolicyConfig(defaultPolicy policyOpt, configs []buildflags.PolicyConfig) ([]policyOpt, error) {
139131
if len(configs) == 0 {
140132
if len(defaultPolicy.Files) == 0 {
141133
return nil, nil
@@ -161,7 +153,7 @@ func withPolicyConfig(defaultPolicy policyOpt, configs []PolicyConfig) ([]policy
161153
out = append(out, defaultPolicy)
162154
}
163155

164-
var last PolicyConfig
156+
var last buildflags.PolicyConfig
165157

166158
for _, cfg := range configs {
167159
if cfg.Reset {

build/policy_test.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66

77
"github.com/docker/buildx/policy"
8+
"github.com/docker/buildx/util/buildflags"
89
"github.com/sirupsen/logrus"
910
"github.com/stretchr/testify/require"
1011
)
@@ -39,33 +40,33 @@ func TestWithPolicyConfigDefaults(t *testing.T) {
3940

4041
// TestWithPolicyConfigDisabled validates disabled policy behavior across invalid and valid combinations.
4142
func TestWithPolicyConfigDisabled(t *testing.T) {
42-
_, err := withPolicyConfig(policyOpt{}, []PolicyConfig{
43+
_, err := withPolicyConfig(policyOpt{}, []buildflags.PolicyConfig{
4344
{Disabled: true, Files: []policy.File{{Filename: "x.rego"}}},
4445
})
4546
require.Error(t, err)
4647

47-
_, err = withPolicyConfig(policyOpt{}, []PolicyConfig{
48+
_, err = withPolicyConfig(policyOpt{}, []buildflags.PolicyConfig{
4849
{Disabled: true, Reset: true},
4950
})
5051
require.Error(t, err)
5152

52-
_, err = withPolicyConfig(policyOpt{}, []PolicyConfig{
53+
_, err = withPolicyConfig(policyOpt{}, []buildflags.PolicyConfig{
5354
{Disabled: true, Strict: boolPtr(true)},
5455
})
5556
require.Error(t, err)
5657

57-
_, err = withPolicyConfig(policyOpt{}, []PolicyConfig{
58+
_, err = withPolicyConfig(policyOpt{}, []buildflags.PolicyConfig{
5859
{Disabled: true, LogLevel: levelPtr(logrus.WarnLevel)},
5960
})
6061
require.Error(t, err)
6162

62-
_, err = withPolicyConfig(policyOpt{}, []PolicyConfig{
63+
_, err = withPolicyConfig(policyOpt{}, []buildflags.PolicyConfig{
6364
{Disabled: true},
6465
{},
6566
})
6667
require.Error(t, err)
6768

68-
out, err := withPolicyConfig(policyOpt{}, []PolicyConfig{
69+
out, err := withPolicyConfig(policyOpt{}, []buildflags.PolicyConfig{
6970
{Disabled: true},
7071
})
7172
require.NoError(t, err)
@@ -81,7 +82,7 @@ func TestWithPolicyConfigResetAndFiles(t *testing.T) {
8182
},
8283
}
8384

84-
out, err := withPolicyConfig(defaultPolicy, []PolicyConfig{
85+
out, err := withPolicyConfig(defaultPolicy, []buildflags.PolicyConfig{
8586
{Reset: true},
8687
{Files: []policy.File{{Filename: "a.rego"}}},
8788
})
@@ -97,7 +98,7 @@ func TestWithPolicyConfigStrictAndLogLevel(t *testing.T) {
9798
Files: []policy.File{{Filename: "default.rego"}},
9899
}
99100

100-
out, err := withPolicyConfig(defaultPolicy, []PolicyConfig{
101+
out, err := withPolicyConfig(defaultPolicy, []buildflags.PolicyConfig{
101102
{Strict: boolPtr(true), LogLevel: levelPtr(logrus.WarnLevel)},
102103
})
103104
require.NoError(t, err)
@@ -109,7 +110,7 @@ func TestWithPolicyConfigStrictAndLogLevel(t *testing.T) {
109110

110111
// TestWithPolicyConfigStrictIgnoredWithoutPolicy ensures strict without any policy produces no entries.
111112
func TestWithPolicyConfigStrictIgnoredWithoutPolicy(t *testing.T) {
112-
out, err := withPolicyConfig(policyOpt{}, []PolicyConfig{
113+
out, err := withPolicyConfig(policyOpt{}, []buildflags.PolicyConfig{
113114
{Strict: boolPtr(true)},
114115
})
115116
require.NoError(t, err)
@@ -125,7 +126,7 @@ func TestWithPolicyConfigMultipleFilesAndOverrides(t *testing.T) {
125126
},
126127
}
127128

128-
out, err := withPolicyConfig(defaultPolicy, []PolicyConfig{
129+
out, err := withPolicyConfig(defaultPolicy, []buildflags.PolicyConfig{
129130
{Files: []policy.File{{Filename: "a.rego"}}},
130131
{Strict: boolPtr(true), LogLevel: levelPtr(logrus.WarnLevel)},
131132
{Files: []policy.File{{Filename: "b.rego"}}, Strict: boolPtr(true)},

commands/build.go

Lines changed: 2 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"github.com/containerd/console"
2121
"github.com/docker/buildx/build"
2222
"github.com/docker/buildx/builder"
23-
"github.com/docker/buildx/policy"
2423
"github.com/docker/buildx/store"
2524
"github.com/docker/buildx/store/storeutil"
2625
"github.com/docker/buildx/util/buildflags"
@@ -57,7 +56,6 @@ import (
5756
"github.com/sirupsen/logrus"
5857
"github.com/spf13/cobra"
5958
"github.com/spf13/pflag"
60-
"github.com/tonistiigi/go-csvvalue"
6159
"go.opentelemetry.io/otel/attribute"
6260
"go.opentelemetry.io/otel/metric"
6361
"google.golang.org/grpc/codes"
@@ -153,7 +151,7 @@ func (o *buildOptions) toOptions() (*BuildOptions, error) {
153151
return nil, err
154152
}
155153

156-
opts.Policy, err = parsePolicyConfigs(o.policy)
154+
opts.Policy, err = buildflags.ParsePolicyConfigs(o.policy)
157155
if err != nil {
158156
return nil, err
159157
}
@@ -236,68 +234,6 @@ func (o *buildOptions) toDisplayMode() (progressui.DisplayMode, error) {
236234
return progress, nil
237235
}
238236

239-
func parsePolicyConfigs(in []string) ([]build.PolicyConfig, error) {
240-
if len(in) == 0 {
241-
return nil, nil
242-
}
243-
244-
out := make([]build.PolicyConfig, 0, len(in))
245-
for _, s := range in {
246-
fields, err := csvvalue.Fields(s, nil)
247-
if err != nil {
248-
return nil, err
249-
}
250-
251-
cfg := build.PolicyConfig{}
252-
for _, field := range fields {
253-
key, value, ok := strings.Cut(field, "=")
254-
if !ok {
255-
return nil, errors.Errorf("invalid value %s", field)
256-
}
257-
key = strings.TrimSpace(strings.ToLower(key))
258-
switch key {
259-
case "filename":
260-
if value == "" {
261-
return nil, errors.Errorf("invalid value %s", field)
262-
}
263-
dt, err := os.ReadFile(value)
264-
if err != nil {
265-
return nil, errors.Wrapf(err, "failed to read policy file %s", value)
266-
}
267-
cfg.Files = append(cfg.Files, policy.File{Filename: value, Data: dt})
268-
case "reset":
269-
b, err := strconv.ParseBool(value)
270-
if err != nil {
271-
return nil, errors.Wrapf(err, "invalid value %s", field)
272-
}
273-
cfg.Reset = b
274-
case "disabled":
275-
b, err := strconv.ParseBool(value)
276-
if err != nil {
277-
return nil, errors.Wrapf(err, "invalid value %s", field)
278-
}
279-
cfg.Disabled = b
280-
case "strict":
281-
b, err := strconv.ParseBool(value)
282-
if err != nil {
283-
return nil, errors.Wrapf(err, "invalid value %s", field)
284-
}
285-
cfg.Strict = &b
286-
case "log-level":
287-
lvl, err := logrus.ParseLevel(value)
288-
if err != nil {
289-
return nil, errors.Wrapf(err, "invalid value %s", field)
290-
}
291-
cfg.LogLevel = &lvl
292-
default:
293-
return nil, errors.Errorf("invalid value %s", field)
294-
}
295-
}
296-
out = append(out, cfg)
297-
}
298-
return out, nil
299-
}
300-
301237
const (
302238
commandNameAttribute = attribute.Key("command.name")
303239
commandOptionsHash = attribute.Key("command.options.hash")
@@ -1048,7 +984,7 @@ type BuildOptions struct {
1048984
GroupRef string
1049985
Annotations []string
1050986
ProvenanceResponseMode string
1051-
Policy []build.PolicyConfig
987+
Policy []buildflags.PolicyConfig
1052988
}
1053989

1054990
// RunBuild runs the specified build and returns the result.

commands/policy/eval.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"slices"
1111
"strings"
1212

13+
"github.com/containerd/errdefs"
1314
"github.com/distribution/reference"
1415
"github.com/docker/buildx/builder"
1516
"github.com/docker/buildx/policy"
@@ -408,7 +409,7 @@ func parseSource(input string) (*pb.SourceOp, error) {
408409
},
409410
}, nil
410411
}
411-
if err != nil {
412+
if err != nil && !errors.Is(err, errdefs.ErrInvalidArgument) {
412413
return nil, err
413414
}
414415
return &pb.SourceOp{Identifier: input}, nil

docs/bake-reference.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ The following table shows the complete list of attributes that you can assign to
236236
| [`no-cache-filter`](#targetno-cache-filter) | List | Disable build cache for specific stages |
237237
| [`no-cache`](#targetno-cache) | Boolean | Disable build cache completely |
238238
| [`output`](#targetoutput) | List | Output destinations |
239+
| [`policy`](#targetpolicy) | List | Policies to validate build sources and metadata |
239240
| [`platforms`](#targetplatforms) | List | Target platforms |
240241
| [`pull`](#targetpull) | Boolean | Always pull images |
241242
| [`secret`](#targetsecret) | List | Secrets to expose to the build |
@@ -899,6 +900,21 @@ target "default" {
899900
}
900901
```
901902

903+
### `target.policy`
904+
905+
Policies to validate build sources and metadata. Each entry uses the same keys
906+
as the `--policy` flag for `docker buildx build` (`filename`, `reset`,
907+
`disabled`, `strict`, `log-level`). Bake also automatically loads
908+
`Dockerfile.rego` alongside the target Dockerfile when present.
909+
910+
```hcl
911+
target "default" {
912+
policy = [
913+
{ filename = "extra.rego" },
914+
]
915+
}
916+
```
917+
902918
### `target.platforms`
903919

904920
Set target platforms for the build target.

0 commit comments

Comments
 (0)