policy: implement policy support for bake
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
+43
-26
@@ -588,7 +588,7 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error)
|
||||
// IMPORTANT: if you add more fields here, do not forget to update
|
||||
// docs/reference/buildx_bake.md (--set) and https://docs.docker.com/build/bake/overrides/
|
||||
switch keys[1] {
|
||||
case "output", "cache-to", "cache-from", "tags", "platform", "secrets", "ssh", "attest", "entitlements", "network", "annotations":
|
||||
case "output", "cache-to", "cache-from", "tags", "platform", "secrets", "ssh", "attest", "entitlements", "network", "annotations", "policy":
|
||||
if len(parts) == 2 {
|
||||
override.Append = appendTo
|
||||
override.ArrValue = append(override.ArrValue, parts[1])
|
||||
@@ -732,31 +732,32 @@ type Target struct {
|
||||
// Inherits is the only field that cannot be overridden with --set
|
||||
Inherits []string `json:"inherits,omitempty" hcl:"inherits,optional" cty:"inherits"`
|
||||
|
||||
Annotations []string `json:"annotations,omitempty" hcl:"annotations,optional" cty:"annotations"`
|
||||
Attest buildflags.Attests `json:"attest,omitempty" hcl:"attest,optional" cty:"attest"`
|
||||
Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"`
|
||||
Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"`
|
||||
Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional" cty:"dockerfile"`
|
||||
DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional" cty:"dockerfile-inline"`
|
||||
Args map[string]*string `json:"args,omitempty" hcl:"args,optional" cty:"args"`
|
||||
Labels map[string]*string `json:"labels,omitempty" hcl:"labels,optional" cty:"labels"`
|
||||
Tags []string `json:"tags,omitempty" hcl:"tags,optional" cty:"tags"`
|
||||
CacheFrom buildflags.CacheOptions `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"`
|
||||
CacheTo buildflags.CacheOptions `json:"cache-to,omitempty" hcl:"cache-to,optional" cty:"cache-to"`
|
||||
Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"`
|
||||
Secrets buildflags.Secrets `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"`
|
||||
SSH buildflags.SSHKeys `json:"ssh,omitempty" hcl:"ssh,optional" cty:"ssh"`
|
||||
Platforms []string `json:"platforms,omitempty" hcl:"platforms,optional" cty:"platforms"`
|
||||
Outputs buildflags.Exports `json:"output,omitempty" hcl:"output,optional" cty:"output"`
|
||||
Pull *bool `json:"pull,omitempty" hcl:"pull,optional" cty:"pull"`
|
||||
NoCache *bool `json:"no-cache,omitempty" hcl:"no-cache,optional" cty:"no-cache"`
|
||||
NetworkMode *string `json:"network,omitempty" hcl:"network,optional" cty:"network"`
|
||||
NoCacheFilter []string `json:"no-cache-filter,omitempty" hcl:"no-cache-filter,optional" cty:"no-cache-filter"`
|
||||
ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional" cty:"shm-size"`
|
||||
Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional" cty:"ulimits"`
|
||||
Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"`
|
||||
Entitlements []string `json:"entitlements,omitempty" hcl:"entitlements,optional" cty:"entitlements"`
|
||||
ExtraHosts map[string]*string `json:"extra-hosts,omitempty" hcl:"extra-hosts,optional" cty:"extra-hosts"`
|
||||
Annotations []string `json:"annotations,omitempty" hcl:"annotations,optional" cty:"annotations"`
|
||||
Attest buildflags.Attests `json:"attest,omitempty" hcl:"attest,optional" cty:"attest"`
|
||||
Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"`
|
||||
Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"`
|
||||
Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional" cty:"dockerfile"`
|
||||
DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional" cty:"dockerfile-inline"`
|
||||
Args map[string]*string `json:"args,omitempty" hcl:"args,optional" cty:"args"`
|
||||
Labels map[string]*string `json:"labels,omitempty" hcl:"labels,optional" cty:"labels"`
|
||||
Tags []string `json:"tags,omitempty" hcl:"tags,optional" cty:"tags"`
|
||||
CacheFrom buildflags.CacheOptions `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"`
|
||||
CacheTo buildflags.CacheOptions `json:"cache-to,omitempty" hcl:"cache-to,optional" cty:"cache-to"`
|
||||
Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"`
|
||||
Secrets buildflags.Secrets `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"`
|
||||
SSH buildflags.SSHKeys `json:"ssh,omitempty" hcl:"ssh,optional" cty:"ssh"`
|
||||
Platforms []string `json:"platforms,omitempty" hcl:"platforms,optional" cty:"platforms"`
|
||||
Outputs buildflags.Exports `json:"output,omitempty" hcl:"output,optional" cty:"output"`
|
||||
Pull *bool `json:"pull,omitempty" hcl:"pull,optional" cty:"pull"`
|
||||
NoCache *bool `json:"no-cache,omitempty" hcl:"no-cache,optional" cty:"no-cache"`
|
||||
NetworkMode *string `json:"network,omitempty" hcl:"network,optional" cty:"network"`
|
||||
NoCacheFilter []string `json:"no-cache-filter,omitempty" hcl:"no-cache-filter,optional" cty:"no-cache-filter"`
|
||||
ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional" cty:"shm-size"`
|
||||
Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional" cty:"ulimits"`
|
||||
Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"`
|
||||
Entitlements []string `json:"entitlements,omitempty" hcl:"entitlements,optional" cty:"entitlements"`
|
||||
ExtraHosts map[string]*string `json:"extra-hosts,omitempty" hcl:"extra-hosts,optional" cty:"extra-hosts"`
|
||||
Policy buildflags.PolicyConfigs `json:"policy,omitempty" hcl:"policy,optional" cty:"policy"`
|
||||
// IMPORTANT: if you add more fields here, do not forget to update newOverrides/AddOverrides and docs/bake-reference.md.
|
||||
|
||||
// linked is a private field to mark a target used as a linked one
|
||||
@@ -891,6 +892,9 @@ func (t *Target) Merge(t2 *Target) {
|
||||
if t2.Attest != nil { // merge
|
||||
t.Attest = t.Attest.Merge(t2.Attest)
|
||||
}
|
||||
if t2.Policy != nil { // merge
|
||||
t.Policy = append(t.Policy, t2.Policy...)
|
||||
}
|
||||
if t2.Secrets != nil { // merge
|
||||
t.Secrets = t.Secrets.Merge(t2.Secrets)
|
||||
}
|
||||
@@ -986,6 +990,17 @@ func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementCon
|
||||
} else {
|
||||
t.Tags = o.ArrValue
|
||||
}
|
||||
case "policy":
|
||||
if !o.Append {
|
||||
t.Policy = nil
|
||||
}
|
||||
for _, v := range o.ArrValue {
|
||||
cfg, err := buildflags.ParsePolicyConfig(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Policy = append(t.Policy, cfg)
|
||||
}
|
||||
case "cache-from":
|
||||
cacheFrom, err := buildflags.ParseCacheEntry(o.ArrValue)
|
||||
if err != nil {
|
||||
@@ -1548,6 +1563,8 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
|
||||
|
||||
bo.Attests = t.Attest.ToMap()
|
||||
|
||||
bo.Policy = []buildflags.PolicyConfig(t.Policy)
|
||||
|
||||
bo.SourcePolicy, err = build.ReadSourcePolicy()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+3
-11
@@ -97,15 +97,7 @@ 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
|
||||
Policy []buildflags.PolicyConfig
|
||||
}
|
||||
|
||||
type CallFunc struct {
|
||||
@@ -135,7 +127,7 @@ type policyOpt struct {
|
||||
LogLevel *logrus.Level
|
||||
}
|
||||
|
||||
func withPolicyConfig(defaultPolicy policyOpt, configs []PolicyConfig) ([]policyOpt, error) {
|
||||
func withPolicyConfig(defaultPolicy policyOpt, configs []buildflags.PolicyConfig) ([]policyOpt, error) {
|
||||
if len(configs) == 0 {
|
||||
if len(defaultPolicy.Files) == 0 {
|
||||
return nil, nil
|
||||
@@ -161,7 +153,7 @@ func withPolicyConfig(defaultPolicy policyOpt, configs []PolicyConfig) ([]policy
|
||||
out = append(out, defaultPolicy)
|
||||
}
|
||||
|
||||
var last PolicyConfig
|
||||
var last buildflags.PolicyConfig
|
||||
|
||||
for _, cfg := range configs {
|
||||
if cfg.Reset {
|
||||
|
||||
+11
-10
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/buildx/policy"
|
||||
"github.com/docker/buildx/util/buildflags"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -39,33 +40,33 @@ func TestWithPolicyConfigDefaults(t *testing.T) {
|
||||
|
||||
// TestWithPolicyConfigDisabled validates disabled policy behavior across invalid and valid combinations.
|
||||
func TestWithPolicyConfigDisabled(t *testing.T) {
|
||||
_, err := withPolicyConfig(policyOpt{}, []PolicyConfig{
|
||||
_, err := withPolicyConfig(policyOpt{}, []buildflags.PolicyConfig{
|
||||
{Disabled: true, Files: []policy.File{{Filename: "x.rego"}}},
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = withPolicyConfig(policyOpt{}, []PolicyConfig{
|
||||
_, err = withPolicyConfig(policyOpt{}, []buildflags.PolicyConfig{
|
||||
{Disabled: true, Reset: true},
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = withPolicyConfig(policyOpt{}, []PolicyConfig{
|
||||
_, err = withPolicyConfig(policyOpt{}, []buildflags.PolicyConfig{
|
||||
{Disabled: true, Strict: boolPtr(true)},
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = withPolicyConfig(policyOpt{}, []PolicyConfig{
|
||||
_, err = withPolicyConfig(policyOpt{}, []buildflags.PolicyConfig{
|
||||
{Disabled: true, LogLevel: levelPtr(logrus.WarnLevel)},
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = withPolicyConfig(policyOpt{}, []PolicyConfig{
|
||||
_, err = withPolicyConfig(policyOpt{}, []buildflags.PolicyConfig{
|
||||
{Disabled: true},
|
||||
{},
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
out, err := withPolicyConfig(policyOpt{}, []PolicyConfig{
|
||||
out, err := withPolicyConfig(policyOpt{}, []buildflags.PolicyConfig{
|
||||
{Disabled: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -81,7 +82,7 @@ func TestWithPolicyConfigResetAndFiles(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
out, err := withPolicyConfig(defaultPolicy, []PolicyConfig{
|
||||
out, err := withPolicyConfig(defaultPolicy, []buildflags.PolicyConfig{
|
||||
{Reset: true},
|
||||
{Files: []policy.File{{Filename: "a.rego"}}},
|
||||
})
|
||||
@@ -97,7 +98,7 @@ func TestWithPolicyConfigStrictAndLogLevel(t *testing.T) {
|
||||
Files: []policy.File{{Filename: "default.rego"}},
|
||||
}
|
||||
|
||||
out, err := withPolicyConfig(defaultPolicy, []PolicyConfig{
|
||||
out, err := withPolicyConfig(defaultPolicy, []buildflags.PolicyConfig{
|
||||
{Strict: boolPtr(true), LogLevel: levelPtr(logrus.WarnLevel)},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -109,7 +110,7 @@ func TestWithPolicyConfigStrictAndLogLevel(t *testing.T) {
|
||||
|
||||
// TestWithPolicyConfigStrictIgnoredWithoutPolicy ensures strict without any policy produces no entries.
|
||||
func TestWithPolicyConfigStrictIgnoredWithoutPolicy(t *testing.T) {
|
||||
out, err := withPolicyConfig(policyOpt{}, []PolicyConfig{
|
||||
out, err := withPolicyConfig(policyOpt{}, []buildflags.PolicyConfig{
|
||||
{Strict: boolPtr(true)},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -125,7 +126,7 @@ func TestWithPolicyConfigMultipleFilesAndOverrides(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
out, err := withPolicyConfig(defaultPolicy, []PolicyConfig{
|
||||
out, err := withPolicyConfig(defaultPolicy, []buildflags.PolicyConfig{
|
||||
{Files: []policy.File{{Filename: "a.rego"}}},
|
||||
{Strict: boolPtr(true), LogLevel: levelPtr(logrus.WarnLevel)},
|
||||
{Files: []policy.File{{Filename: "b.rego"}}, Strict: boolPtr(true)},
|
||||
|
||||
+2
-66
@@ -20,7 +20,6 @@ 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"
|
||||
@@ -57,7 +56,6 @@ 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"
|
||||
@@ -153,7 +151,7 @@ func (o *buildOptions) toOptions() (*BuildOptions, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts.Policy, err = parsePolicyConfigs(o.policy)
|
||||
opts.Policy, err = buildflags.ParsePolicyConfigs(o.policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -236,68 +234,6 @@ 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)
|
||||
}
|
||||
dt, err := os.ReadFile(value)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to read policy file %s", value)
|
||||
}
|
||||
cfg.Files = append(cfg.Files, policy.File{Filename: value, Data: dt})
|
||||
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")
|
||||
@@ -1048,7 +984,7 @@ type BuildOptions struct {
|
||||
GroupRef string
|
||||
Annotations []string
|
||||
ProvenanceResponseMode string
|
||||
Policy []build.PolicyConfig
|
||||
Policy []buildflags.PolicyConfig
|
||||
}
|
||||
|
||||
// RunBuild runs the specified build and returns the result.
|
||||
|
||||
@@ -236,6 +236,7 @@ The following table shows the complete list of attributes that you can assign to
|
||||
| [`no-cache-filter`](#targetno-cache-filter) | List | Disable build cache for specific stages |
|
||||
| [`no-cache`](#targetno-cache) | Boolean | Disable build cache completely |
|
||||
| [`output`](#targetoutput) | List | Output destinations |
|
||||
| [`policy`](#targetpolicy) | List | Policies to validate build sources and metadata |
|
||||
| [`platforms`](#targetplatforms) | List | Target platforms |
|
||||
| [`pull`](#targetpull) | Boolean | Always pull images |
|
||||
| [`secret`](#targetsecret) | List | Secrets to expose to the build |
|
||||
@@ -899,6 +900,21 @@ target "default" {
|
||||
}
|
||||
```
|
||||
|
||||
### `target.policy`
|
||||
|
||||
Policies to validate build sources and metadata. Each entry uses the same keys
|
||||
as the `--policy` flag for `docker buildx build` (`filename`, `reset`,
|
||||
`disabled`, `strict`, `log-level`). Bake also automatically loads
|
||||
`Dockerfile.rego` alongside the target Dockerfile when present.
|
||||
|
||||
```hcl
|
||||
target "default" {
|
||||
policy = [
|
||||
{ filename = "extra.rego" },
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `target.platforms`
|
||||
|
||||
Set target platforms for the build target.
|
||||
|
||||
@@ -25,6 +25,7 @@ func TestIntegration(t *testing.T) {
|
||||
tests = append(tests, buildTests...)
|
||||
tests = append(tests, policyBuildTests...)
|
||||
tests = append(tests, policyEvalTests...)
|
||||
tests = append(tests, policyBakeTests...)
|
||||
tests = append(tests, bakeTests...)
|
||||
tests = append(tests, historyTests...)
|
||||
tests = append(tests, inspectTests...)
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/continuity/fs/fstest"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var policyBakeTests = []func(t *testing.T, sb integration.Sandbox){
|
||||
testBakePolicyConfigFlags,
|
||||
}
|
||||
|
||||
func testBakePolicyConfigFlags(t *testing.T, sb integration.Sandbox) {
|
||||
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
|
||||
|
||||
dockerfile := []byte("FROM scratch\n")
|
||||
defaultPolicy := []byte(`
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if input.env.args["DEFAULT_OK"] == "1"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`)
|
||||
extraPolicy := []byte(`
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if input.env.labels["com.example.extra"] == "1"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`)
|
||||
bakeFile := []byte(`
|
||||
target "pass-both" {
|
||||
context = "."
|
||||
dockerfile = "Dockerfile"
|
||||
args = {
|
||||
DEFAULT_OK = "1"
|
||||
}
|
||||
labels = {
|
||||
"com.example.extra" = "1"
|
||||
}
|
||||
policy = [
|
||||
{ filename = "extra.rego" },
|
||||
]
|
||||
output = ["type=cacheonly"]
|
||||
}
|
||||
|
||||
target "fail-default" {
|
||||
context = "."
|
||||
dockerfile = "Dockerfile"
|
||||
labels = {
|
||||
"com.example.extra" = "1"
|
||||
}
|
||||
policy = [
|
||||
{ filename = "extra.rego" },
|
||||
]
|
||||
output = ["type=cacheonly"]
|
||||
}
|
||||
|
||||
target "fail-extra" {
|
||||
context = "."
|
||||
dockerfile = "Dockerfile"
|
||||
args = {
|
||||
DEFAULT_OK = "1"
|
||||
}
|
||||
policy = [
|
||||
{ filename = "extra.rego" },
|
||||
]
|
||||
output = ["type=cacheonly"]
|
||||
}
|
||||
|
||||
target "reset-pass" {
|
||||
context = "."
|
||||
dockerfile = "Dockerfile"
|
||||
labels = {
|
||||
"com.example.extra" = "1"
|
||||
}
|
||||
policy = [
|
||||
{ filename = "extra.rego", reset = true },
|
||||
]
|
||||
output = ["type=cacheonly"]
|
||||
}
|
||||
|
||||
target "reset-fail" {
|
||||
context = "."
|
||||
dockerfile = "Dockerfile"
|
||||
policy = [
|
||||
{ filename = "extra.rego", reset = true },
|
||||
]
|
||||
output = ["type=cacheonly"]
|
||||
}
|
||||
|
||||
target "disabled" {
|
||||
context = "."
|
||||
dockerfile = "Dockerfile"
|
||||
policy = [
|
||||
{ disabled = true },
|
||||
]
|
||||
output = ["type=cacheonly"]
|
||||
}
|
||||
|
||||
target "disabled-combined" {
|
||||
context = "."
|
||||
dockerfile = "Dockerfile"
|
||||
policy = [
|
||||
{ filename = "extra.rego" },
|
||||
{ disabled = true },
|
||||
]
|
||||
output = ["type=cacheonly"]
|
||||
}
|
||||
`)
|
||||
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||
fstest.CreateFile("Dockerfile.rego", defaultPolicy, 0600),
|
||||
fstest.CreateFile("extra.rego", extraPolicy, 0600),
|
||||
fstest.CreateFile("docker-bake.hcl", bakeFile, 0600),
|
||||
)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
target string
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "additional-policy-requires-default",
|
||||
target: "pass-both",
|
||||
},
|
||||
{
|
||||
name: "additional-policy-missing-default",
|
||||
target: "fail-default",
|
||||
wantErrContains: "not allowed by policy",
|
||||
},
|
||||
{
|
||||
name: "additional-policy-missing-extra",
|
||||
target: "fail-extra",
|
||||
wantErrContains: "not allowed by policy",
|
||||
},
|
||||
{
|
||||
name: "reset-ignores-default",
|
||||
target: "reset-pass",
|
||||
},
|
||||
{
|
||||
name: "reset-requires-extra",
|
||||
target: "reset-fail",
|
||||
wantErrContains: "not allowed by policy",
|
||||
},
|
||||
{
|
||||
name: "disabled-skips-default",
|
||||
target: "disabled",
|
||||
},
|
||||
{
|
||||
name: "disabled-cannot-combine",
|
||||
target: "disabled-combined",
|
||||
wantErrContains: "disabled policy cannot be combined with other policy flags",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := buildxCmd(sb, withDir(dir), withArgs(
|
||||
"bake",
|
||||
"--progress=plain",
|
||||
"--file", "docker-bake.hcl",
|
||||
tc.target,
|
||||
))
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -703,9 +703,10 @@ decision := {"allow": allow}
|
||||
}
|
||||
skipNoCompatBuildKit(t, sb, ">= 0.26.3-0", "http checksum policy input")
|
||||
}
|
||||
dockerfile := fmt.Appendf(nil, "FROM busybox:latest\nADD %s /tmp/file\n", tc.addURL)
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("Dockerfile", []byte(fmt.Sprintf("FROM busybox:latest\nADD %s /tmp/file\n", tc.addURL)), 0600),
|
||||
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||
fstest.CreateFile("policy.rego", []byte(tc.policy), 0600),
|
||||
)
|
||||
policyPath := filepath.Join(dir, "policy.rego")
|
||||
@@ -747,8 +748,7 @@ func testBuildPolicyGit(t *testing.T, sb integration.Sandbox) {
|
||||
gittestutil.GitAdd(git, t, "Dockerfile", "a")
|
||||
gittestutil.GitCommit(git, t, "initial commit")
|
||||
|
||||
_, err = git.Run("tag", "-a", "v0.1", "-m", "v0.1release")
|
||||
require.NoError(t, err)
|
||||
gittestutil.GitTagAnnotated(git, t, "v0.1", "v0.1release")
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "b"), []byte("b"), 0600))
|
||||
gittestutil.GitAdd(git, t, "b")
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
urlpkg "net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/continuity/fs/fstest"
|
||||
@@ -14,7 +15,6 @@ import (
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
"github.com/stretchr/testify/require"
|
||||
urlpkg "net/url"
|
||||
)
|
||||
|
||||
var policyEvalTests = []func(t *testing.T, sb integration.Sandbox){
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package buildflags
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/buildx/policy"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/tonistiigi/go-csvvalue"
|
||||
)
|
||||
|
||||
type PolicyConfig struct {
|
||||
Files []policy.File
|
||||
Reset bool
|
||||
Disabled bool
|
||||
Strict *bool
|
||||
LogLevel *logrus.Level
|
||||
}
|
||||
|
||||
func ParsePolicyConfigs(in []string) ([]PolicyConfig, error) {
|
||||
if len(in) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
out := make([]PolicyConfig, 0, len(in))
|
||||
for _, s := range in {
|
||||
cfg, err := ParsePolicyConfig(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, cfg)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ParsePolicyConfig(value string) (PolicyConfig, error) {
|
||||
fields, err := csvvalue.Fields(value, nil)
|
||||
if err != nil {
|
||||
return PolicyConfig{}, err
|
||||
}
|
||||
return parsePolicyFields(fields)
|
||||
}
|
||||
|
||||
func parsePolicyFields(fields []string) (PolicyConfig, error) {
|
||||
cfg := PolicyConfig{}
|
||||
for _, field := range fields {
|
||||
key, value, ok := strings.Cut(field, "=")
|
||||
if !ok {
|
||||
return PolicyConfig{}, errors.Errorf("invalid value %s", field)
|
||||
}
|
||||
key = strings.TrimSpace(strings.ToLower(key))
|
||||
switch key {
|
||||
case "filename":
|
||||
if value == "" {
|
||||
return PolicyConfig{}, errors.Errorf("invalid value %s", field)
|
||||
}
|
||||
dt, err := os.ReadFile(value)
|
||||
if err != nil {
|
||||
return PolicyConfig{}, errors.Wrapf(err, "failed to read policy file %s", value)
|
||||
}
|
||||
cfg.Files = append(cfg.Files, policy.File{Filename: value, Data: dt})
|
||||
case "reset":
|
||||
b, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return PolicyConfig{}, errors.Wrapf(err, "invalid value %s", field)
|
||||
}
|
||||
cfg.Reset = b
|
||||
case "disabled":
|
||||
b, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return PolicyConfig{}, errors.Wrapf(err, "invalid value %s", field)
|
||||
}
|
||||
cfg.Disabled = b
|
||||
case "strict":
|
||||
b, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return PolicyConfig{}, errors.Wrapf(err, "invalid value %s", field)
|
||||
}
|
||||
cfg.Strict = &b
|
||||
case "log-level":
|
||||
lvl, err := logrus.ParseLevel(value)
|
||||
if err != nil {
|
||||
return PolicyConfig{}, errors.Wrapf(err, "invalid value %s", field)
|
||||
}
|
||||
cfg.LogLevel = &lvl
|
||||
default:
|
||||
return PolicyConfig{}, errors.Errorf("invalid value %s", field)
|
||||
}
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package buildflags
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
)
|
||||
|
||||
type PolicyConfigs []PolicyConfig
|
||||
|
||||
var policyConfigType = sync.OnceValue(func() cty.Type {
|
||||
return cty.Map(cty.String)
|
||||
})
|
||||
|
||||
func (p *PolicyConfigs) FromCtyValue(in cty.Value, path cty.Path) error {
|
||||
got := in.Type()
|
||||
if got.IsTupleType() || got.IsListType() {
|
||||
return p.fromCtyValue(in, path)
|
||||
}
|
||||
|
||||
want := cty.List(policyConfigType())
|
||||
return path.NewErrorf("%s", convert.MismatchMessage(got, want))
|
||||
}
|
||||
|
||||
func (p *PolicyConfigs) fromCtyValue(in cty.Value, path cty.Path) (retErr error) {
|
||||
*p = make([]PolicyConfig, 0, in.LengthInt())
|
||||
|
||||
yield := func(value cty.Value) bool {
|
||||
if value.Type() == cty.String {
|
||||
var cfg PolicyConfig
|
||||
cfg, retErr = ParsePolicyConfig(value.AsString())
|
||||
if retErr != nil {
|
||||
return false
|
||||
}
|
||||
*p = append(*p, cfg)
|
||||
return true
|
||||
}
|
||||
|
||||
if value.Type().IsObjectType() || value.Type().IsMapType() {
|
||||
var cfg PolicyConfig
|
||||
cfg, retErr = policyConfigFromMap(value)
|
||||
if retErr != nil {
|
||||
return false
|
||||
}
|
||||
*p = append(*p, cfg)
|
||||
return true
|
||||
}
|
||||
|
||||
retErr = path.NewErrorf("%s", convert.MismatchMessage(value.Type(), policyConfigType()))
|
||||
return false
|
||||
}
|
||||
eachElement(in)(yield)
|
||||
return retErr
|
||||
}
|
||||
|
||||
func (p PolicyConfigs) ToCtyValue() cty.Value {
|
||||
if len(p) == 0 {
|
||||
return cty.ListValEmpty(policyConfigType())
|
||||
}
|
||||
|
||||
vals := make([]cty.Value, len(p))
|
||||
for i, entry := range p {
|
||||
vals[i] = entry.ToCtyValue()
|
||||
}
|
||||
return cty.ListVal(vals)
|
||||
}
|
||||
|
||||
func (p *PolicyConfig) FromCtyValue(in cty.Value, path cty.Path) error {
|
||||
if in.Type() == cty.String {
|
||||
cfg, err := ParsePolicyConfig(in.AsString())
|
||||
if err != nil {
|
||||
return path.NewError(err)
|
||||
}
|
||||
*p = cfg
|
||||
return nil
|
||||
}
|
||||
|
||||
if in.Type().IsObjectType() || in.Type().IsMapType() {
|
||||
cfg, err := policyConfigFromMap(in)
|
||||
if err != nil {
|
||||
return path.NewError(err)
|
||||
}
|
||||
*p = cfg
|
||||
return nil
|
||||
}
|
||||
|
||||
return path.NewErrorf("%s", convert.MismatchMessage(in.Type(), policyConfigType()))
|
||||
}
|
||||
|
||||
func (p PolicyConfig) ToCtyValue() cty.Value {
|
||||
vals := map[string]cty.Value{}
|
||||
if len(p.Files) > 0 {
|
||||
vals["filename"] = cty.StringVal(p.Files[0].Filename)
|
||||
}
|
||||
if p.Reset {
|
||||
vals["reset"] = cty.StringVal(strconv.FormatBool(p.Reset))
|
||||
}
|
||||
if p.Disabled {
|
||||
vals["disabled"] = cty.StringVal(strconv.FormatBool(p.Disabled))
|
||||
}
|
||||
if p.Strict != nil {
|
||||
vals["strict"] = cty.StringVal(strconv.FormatBool(*p.Strict))
|
||||
}
|
||||
if p.LogLevel != nil {
|
||||
vals["log-level"] = cty.StringVal(p.LogLevel.String())
|
||||
}
|
||||
if len(vals) == 0 {
|
||||
return cty.MapValEmpty(cty.String)
|
||||
}
|
||||
return cty.MapVal(vals)
|
||||
}
|
||||
|
||||
func policyConfigFromMap(in cty.Value) (PolicyConfig, error) {
|
||||
fields := make([]string, 0)
|
||||
for k, v := range in.AsValueMap() {
|
||||
if v.IsNull() || !v.IsKnown() {
|
||||
continue
|
||||
}
|
||||
if v.Type() == cty.String && v.AsString() == "" {
|
||||
continue
|
||||
}
|
||||
field, err := policyField(k, v)
|
||||
if err != nil {
|
||||
return PolicyConfig{}, err
|
||||
}
|
||||
fields = append(fields, field)
|
||||
}
|
||||
return parsePolicyFields(fields)
|
||||
}
|
||||
|
||||
func policyField(key string, value cty.Value) (string, error) {
|
||||
switch value.Type() {
|
||||
case cty.String:
|
||||
return fmt.Sprintf("%s=%s", key, value.AsString()), nil
|
||||
case cty.Bool:
|
||||
return fmt.Sprintf("%s=%t", key, value.True()), nil
|
||||
case cty.Number:
|
||||
var f big.Float
|
||||
f.Set(value.AsBigFloat())
|
||||
return fmt.Sprintf("%s=%s", key, f.Text('f', -1)), nil
|
||||
default:
|
||||
return "", errors.Errorf("%s", convert.MismatchMessage(value.Type(), cty.String))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package buildflags
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/buildx/policy"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestPolicyConfigs_FromCtyValue(t *testing.T) {
|
||||
policyDir := t.TempDir()
|
||||
policyPath := filepath.Join(policyDir, "policy.rego")
|
||||
policyData := []byte("package docker\n")
|
||||
require.NoError(t, os.WriteFile(policyPath, policyData, 0o600))
|
||||
|
||||
in := cty.TupleVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"filename": cty.StringVal(policyPath),
|
||||
"reset": cty.BoolVal(true),
|
||||
"strict": cty.BoolVal(true),
|
||||
"log-level": cty.StringVal("warn"),
|
||||
}),
|
||||
cty.StringVal("filename=" + policyPath + ",disabled=true"),
|
||||
})
|
||||
|
||||
var actual PolicyConfigs
|
||||
err := actual.FromCtyValue(in, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, actual, 2)
|
||||
|
||||
require.Equal(t, policyPath, actual[0].Files[0].Filename)
|
||||
require.Equal(t, policyData, actual[0].Files[0].Data)
|
||||
require.True(t, actual[0].Reset)
|
||||
require.NotNil(t, actual[0].Strict)
|
||||
require.True(t, *actual[0].Strict)
|
||||
require.NotNil(t, actual[0].LogLevel)
|
||||
require.Equal(t, logrus.WarnLevel, *actual[0].LogLevel)
|
||||
|
||||
require.Equal(t, policyPath, actual[1].Files[0].Filename)
|
||||
require.Equal(t, policyData, actual[1].Files[0].Data)
|
||||
require.True(t, actual[1].Disabled)
|
||||
}
|
||||
|
||||
func TestPolicyConfigs_ToCtyValue(t *testing.T) {
|
||||
lvl := logrus.InfoLevel
|
||||
strict := true
|
||||
in := PolicyConfigs{
|
||||
{
|
||||
Files: []policy.File{{Filename: "a.rego"}},
|
||||
Reset: true,
|
||||
},
|
||||
{
|
||||
Files: []policy.File{{Filename: "b.rego"}},
|
||||
Disabled: true,
|
||||
Strict: &strict,
|
||||
LogLevel: &lvl,
|
||||
},
|
||||
}
|
||||
|
||||
actual := in.ToCtyValue()
|
||||
expected := cty.ListVal([]cty.Value{
|
||||
cty.MapVal(map[string]cty.Value{
|
||||
"filename": cty.StringVal("a.rego"),
|
||||
"reset": cty.StringVal("true"),
|
||||
}),
|
||||
cty.MapVal(map[string]cty.Value{
|
||||
"filename": cty.StringVal("b.rego"),
|
||||
"disabled": cty.StringVal("true"),
|
||||
"strict": cty.StringVal("true"),
|
||||
"log-level": cty.StringVal("info"),
|
||||
}),
|
||||
})
|
||||
|
||||
result := actual.Equals(expected)
|
||||
require.True(t, result.True())
|
||||
}
|
||||
|
||||
func TestPolicyConfig_FromCtyValue(t *testing.T) {
|
||||
policyDir := t.TempDir()
|
||||
policyPath := filepath.Join(policyDir, "policy.rego")
|
||||
policyData := []byte("package docker\n")
|
||||
require.NoError(t, os.WriteFile(policyPath, policyData, 0o600))
|
||||
|
||||
var actual PolicyConfig
|
||||
err := actual.FromCtyValue(cty.StringVal("filename="+policyPath+",disabled=true"), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, policyPath, actual.Files[0].Filename)
|
||||
require.Equal(t, policyData, actual.Files[0].Data)
|
||||
require.True(t, actual.Disabled)
|
||||
}
|
||||
@@ -33,6 +33,13 @@ func GitTag(c *gitutil.Git, tb testing.TB, tag string) {
|
||||
require.Empty(tb, out)
|
||||
}
|
||||
|
||||
func GitTagAnnotated(c *gitutil.Git, tb testing.TB, tag, message string) {
|
||||
tb.Helper()
|
||||
out, err := fakeGit(c, "tag", "-a", tag, "-m", message)
|
||||
require.NoError(tb, err)
|
||||
require.Empty(tb, out)
|
||||
}
|
||||
|
||||
func GitCheckoutBranch(c *gitutil.Git, tb testing.TB, name string) {
|
||||
tb.Helper()
|
||||
out, err := fakeGit(c, "checkout", "-b", name)
|
||||
|
||||
Reference in New Issue
Block a user