build: add --policy flag for controlling policy configuration
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
+83
-2
@@ -97,6 +97,15 @@ type Options struct {
|
||||
SourcePolicy *spb.Policy
|
||||
GroupRef string
|
||||
Annotations map[exptypes.AnnotationKey]string // Not used during build, annotations are already set in Exports. Just used to check for support with drivers.
|
||||
Policy []PolicyConfig
|
||||
}
|
||||
|
||||
type PolicyConfig struct {
|
||||
Files []policy.File
|
||||
Reset bool
|
||||
Disabled bool
|
||||
Strict *bool
|
||||
LogLevel *logrus.Level
|
||||
}
|
||||
|
||||
type CallFunc struct {
|
||||
@@ -120,8 +129,80 @@ type Inputs struct {
|
||||
}
|
||||
|
||||
type policyOpt struct {
|
||||
Files []policy.File
|
||||
FS func() (fs.StatFS, func() error, error)
|
||||
Files []policy.File
|
||||
FS func() (fs.StatFS, func() error, error)
|
||||
Strict bool
|
||||
LogLevel logrus.Level
|
||||
}
|
||||
|
||||
func withPolicyConfig(defaultPolicy policyOpt, configs []PolicyConfig) ([]policyOpt, error) {
|
||||
if len(configs) == 0 {
|
||||
if len(defaultPolicy.Files) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return []policyOpt{defaultPolicy}, nil
|
||||
}
|
||||
|
||||
for _, cfg := range configs {
|
||||
if !cfg.Disabled {
|
||||
continue
|
||||
}
|
||||
if cfg.Reset || cfg.Strict != nil || cfg.LogLevel != nil || len(cfg.Files) > 0 {
|
||||
return nil, errors.New("disabled policy cannot be combined with other policy flags")
|
||||
}
|
||||
if len(configs) > 1 {
|
||||
return nil, errors.New("disabled policy cannot be combined with other policy flags")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
out := make([]policyOpt, 0, len(configs)+1)
|
||||
if len(defaultPolicy.Files) != 0 {
|
||||
out = append(out, defaultPolicy)
|
||||
}
|
||||
|
||||
var last PolicyConfig
|
||||
|
||||
for _, cfg := range configs {
|
||||
if cfg.Reset {
|
||||
out = nil
|
||||
}
|
||||
|
||||
if len(cfg.Files) == 0 {
|
||||
if len(out) == 0 {
|
||||
last = cfg
|
||||
} else {
|
||||
last := &out[len(out)-1]
|
||||
if cfg.Strict != nil {
|
||||
last.Strict = *cfg.Strict
|
||||
}
|
||||
if cfg.LogLevel != nil {
|
||||
last.LogLevel = *cfg.LogLevel
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
opt := policyOpt{
|
||||
Files: cfg.Files,
|
||||
}
|
||||
if last.Strict != nil {
|
||||
opt.Strict = *last.Strict
|
||||
}
|
||||
if last.LogLevel != nil {
|
||||
opt.LogLevel = *last.LogLevel
|
||||
}
|
||||
if cfg.Strict != nil {
|
||||
opt.Strict = *cfg.Strict
|
||||
}
|
||||
if cfg.LogLevel != nil {
|
||||
opt.LogLevel = *cfg.LogLevel
|
||||
}
|
||||
opt.FS = defaultPolicy.FS
|
||||
out = append(out, opt)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type NamedContext struct {
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/buildx/policy"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func boolPtr(v bool) *bool {
|
||||
return &v
|
||||
}
|
||||
|
||||
func levelPtr(v logrus.Level) *logrus.Level {
|
||||
return &v
|
||||
}
|
||||
|
||||
// TestWithPolicyConfigDefaults ensures default policy is returned when no configs are provided.
|
||||
func TestWithPolicyConfigDefaults(t *testing.T) {
|
||||
defaultPolicy := policyOpt{
|
||||
Files: []policy.File{
|
||||
{Filename: "default.rego", Data: []byte("package policy")},
|
||||
},
|
||||
FS: func() (fs.StatFS, func() error, error) {
|
||||
return nil, nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
out, err := withPolicyConfig(defaultPolicy, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out, 1)
|
||||
require.Equal(t, defaultPolicy.Files, out[0].Files)
|
||||
require.False(t, out[0].Strict)
|
||||
require.Equal(t, logrus.Level(0), out[0].LogLevel)
|
||||
require.NotNil(t, out[0].FS)
|
||||
}
|
||||
|
||||
// TestWithPolicyConfigDisabled validates disabled policy behavior across invalid and valid combinations.
|
||||
func TestWithPolicyConfigDisabled(t *testing.T) {
|
||||
_, err := withPolicyConfig(policyOpt{}, []PolicyConfig{
|
||||
{Disabled: true, Files: []policy.File{{Filename: "x.rego"}}},
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = withPolicyConfig(policyOpt{}, []PolicyConfig{
|
||||
{Disabled: true, Reset: true},
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = withPolicyConfig(policyOpt{}, []PolicyConfig{
|
||||
{Disabled: true, Strict: boolPtr(true)},
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
out, err := withPolicyConfig(policyOpt{}, []PolicyConfig{
|
||||
{Disabled: true, LogLevel: levelPtr(logrus.WarnLevel)},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, out)
|
||||
|
||||
_, err = withPolicyConfig(policyOpt{}, []PolicyConfig{
|
||||
{Disabled: true},
|
||||
{},
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
out, err = withPolicyConfig(policyOpt{}, []PolicyConfig{
|
||||
{Disabled: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, out)
|
||||
}
|
||||
|
||||
// TestWithPolicyConfigResetAndFiles ensures reset drops defaults and uses explicitly provided files.
|
||||
func TestWithPolicyConfigResetAndFiles(t *testing.T) {
|
||||
defaultPolicy := policyOpt{
|
||||
Files: []policy.File{{Filename: "default.rego"}},
|
||||
FS: func() (fs.StatFS, func() error, error) {
|
||||
return nil, nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
out, err := withPolicyConfig(defaultPolicy, []PolicyConfig{
|
||||
{Reset: true},
|
||||
{Files: []policy.File{{Filename: "a.rego"}}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out, 1)
|
||||
require.Equal(t, "a.rego", out[0].Files[0].Filename)
|
||||
require.NotNil(t, out[0].FS)
|
||||
}
|
||||
|
||||
// TestWithPolicyConfigStrictAndLogLevel ensures strict and log level apply to existing policy.
|
||||
func TestWithPolicyConfigStrictAndLogLevel(t *testing.T) {
|
||||
defaultPolicy := policyOpt{
|
||||
Files: []policy.File{{Filename: "default.rego"}},
|
||||
}
|
||||
|
||||
out, err := withPolicyConfig(defaultPolicy, []PolicyConfig{
|
||||
{Strict: boolPtr(true), LogLevel: levelPtr(logrus.WarnLevel)},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out, 1)
|
||||
require.True(t, out[0].Strict)
|
||||
require.Equal(t, logrus.WarnLevel, out[0].LogLevel)
|
||||
}
|
||||
|
||||
// TestWithPolicyConfigStrictIgnoredWithoutPolicy ensures strict without any policy produces no entries.
|
||||
func TestWithPolicyConfigStrictIgnoredWithoutPolicy(t *testing.T) {
|
||||
out, err := withPolicyConfig(policyOpt{}, []PolicyConfig{
|
||||
{Strict: boolPtr(true)},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out, 0)
|
||||
}
|
||||
|
||||
// TestWithPolicyConfigMultipleFilesAndOverrides ensures per-entry overrides and carryover apply across multiple files.
|
||||
func TestWithPolicyConfigMultipleFilesAndOverrides(t *testing.T) {
|
||||
defaultPolicy := policyOpt{
|
||||
Files: []policy.File{{Filename: "default.rego"}},
|
||||
FS: func() (fs.StatFS, func() error, error) {
|
||||
return nil, nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
out, err := withPolicyConfig(defaultPolicy, []PolicyConfig{
|
||||
{Files: []policy.File{{Filename: "a.rego"}}},
|
||||
{Strict: boolPtr(true), LogLevel: levelPtr(logrus.WarnLevel)},
|
||||
{Files: []policy.File{{Filename: "b.rego"}}, Strict: boolPtr(true)},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out, 3)
|
||||
require.Equal(t, "default.rego", out[0].Files[0].Filename)
|
||||
require.Equal(t, "a.rego", out[1].Files[0].Filename)
|
||||
require.True(t, out[1].Strict)
|
||||
require.Equal(t, logrus.WarnLevel, out[1].LogLevel)
|
||||
require.Equal(t, "b.rego", out[2].Files[0].Filename)
|
||||
require.True(t, out[2].Strict)
|
||||
require.NotNil(t, out[1].FS)
|
||||
require.NotNil(t, out[2].FS)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/containerd/console"
|
||||
"github.com/docker/buildx/build"
|
||||
"github.com/docker/buildx/builder"
|
||||
"github.com/docker/buildx/policy"
|
||||
"github.com/docker/buildx/store"
|
||||
"github.com/docker/buildx/store/storeutil"
|
||||
"github.com/docker/buildx/util/buildflags"
|
||||
@@ -56,6 +57,7 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/tonistiigi/go-csvvalue"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"google.golang.org/grpc/codes"
|
||||
@@ -79,6 +81,7 @@ type buildOptions struct {
|
||||
noCacheFilter []string
|
||||
outputs []string
|
||||
platforms []string
|
||||
policy []string
|
||||
callFunc string
|
||||
secrets []string
|
||||
shmSize dockeropts.MemBytes
|
||||
@@ -150,6 +153,11 @@ func (o *buildOptions) toOptions() (*BuildOptions, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts.Policy, err = parsePolicyConfigs(o.policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inAttests := slices.Clone(o.attests)
|
||||
if o.provenance != "" {
|
||||
inAttests = append(inAttests, buildflags.CanonicalizeAttest("provenance", o.provenance))
|
||||
@@ -228,6 +236,64 @@ func (o *buildOptions) toDisplayMode() (progressui.DisplayMode, error) {
|
||||
return progress, nil
|
||||
}
|
||||
|
||||
func parsePolicyConfigs(in []string) ([]build.PolicyConfig, error) {
|
||||
if len(in) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
out := make([]build.PolicyConfig, 0, len(in))
|
||||
for _, s := range in {
|
||||
fields, err := csvvalue.Fields(s, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := build.PolicyConfig{}
|
||||
for _, field := range fields {
|
||||
key, value, ok := strings.Cut(field, "=")
|
||||
if !ok {
|
||||
return nil, errors.Errorf("invalid value %s", field)
|
||||
}
|
||||
key = strings.TrimSpace(strings.ToLower(key))
|
||||
switch key {
|
||||
case "filename":
|
||||
if value == "" {
|
||||
return nil, errors.Errorf("invalid value %s", field)
|
||||
}
|
||||
cfg.Files = append(cfg.Files, policy.File{Filename: value})
|
||||
case "reset":
|
||||
b, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "invalid value %s", field)
|
||||
}
|
||||
cfg.Reset = b
|
||||
case "disabled":
|
||||
b, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "invalid value %s", field)
|
||||
}
|
||||
cfg.Disabled = b
|
||||
case "strict":
|
||||
b, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "invalid value %s", field)
|
||||
}
|
||||
cfg.Strict = &b
|
||||
case "log-level":
|
||||
lvl, err := logrus.ParseLevel(value)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "invalid value %s", field)
|
||||
}
|
||||
cfg.LogLevel = &lvl
|
||||
default:
|
||||
return nil, errors.Errorf("invalid value %s", field)
|
||||
}
|
||||
}
|
||||
out = append(out, cfg)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
const (
|
||||
commandNameAttribute = attribute.Key("command.name")
|
||||
commandOptionsHash = attribute.Key("command.options.hash")
|
||||
@@ -542,6 +608,8 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugger debuggerOpt
|
||||
|
||||
flags.StringArrayVar(&options.platforms, "platform", platformsDefault, "Set target platform for build")
|
||||
|
||||
flags.StringArrayVar(&options.policy, "policy", []string{}, `Policy configuration (format: "filename=path[,filename=path][,reset=true|false][,disabled=true|false][,strict=true|false][,log-level=level]")`)
|
||||
|
||||
flags.BoolVar(&options.exportPush, "push", false, `Shorthand for "--output=type=registry,unpack=false"`)
|
||||
|
||||
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the build output and print image ID on success")
|
||||
@@ -976,6 +1044,7 @@ type BuildOptions struct {
|
||||
GroupRef string
|
||||
Annotations []string
|
||||
ProvenanceResponseMode string
|
||||
Policy []build.PolicyConfig
|
||||
}
|
||||
|
||||
// RunBuild runs the specified build and returns the result.
|
||||
@@ -1103,6 +1172,7 @@ func RunBuild(ctx context.Context, dockerCli command.Cli, in *BuildOptions, inSt
|
||||
opts.Attests = in.Attests.ToMap()
|
||||
|
||||
opts.SourcePolicy = in.SourcePolicy
|
||||
opts.Policy = in.Policy
|
||||
|
||||
allow, err := buildflags.ParseEntitlements(in.Allow)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user