Merge pull request #3593 from tonistiigi/opa-bake-tests
policy: add policy to bake and improve test coverage
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.
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/buildx/builder"
|
||||
"github.com/docker/buildx/policy"
|
||||
@@ -408,7 +409,7 @@ func parseSource(input string) (*pb.SourceOp, error) {
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
if err != nil {
|
||||
if err != nil && !errors.Is(err, errdefs.ErrInvalidArgument) {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.SourceOp{Identifier: input}, nil
|
||||
|
||||
@@ -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.
|
||||
|
||||
+22
-7
@@ -236,7 +236,7 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
|
||||
if err := AddUnknownsWithLogger(p.opt.Log, next, unk); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if next.Image != nil || next.Git != nil {
|
||||
if next.Image != nil || next.Git != nil || hasHTTPUnknowns(unk) {
|
||||
p.log(logrus.InfoLevel, "policy decision for source %s: resolve missing fields %+v", src.Source.Identifier, summarizeUnknownsForLog(unk))
|
||||
return nil, next, nil
|
||||
}
|
||||
@@ -347,14 +347,15 @@ func SourceToInputWithLogger(ctx context.Context, getVerifier PolicyVerifierProv
|
||||
Path: u.Path,
|
||||
Query: u.Query(),
|
||||
}
|
||||
if src.HTTP != nil {
|
||||
inp.HTTP.Checksum = src.HTTP.Checksum
|
||||
}
|
||||
if inp.HTTP.Checksum == "" {
|
||||
unknowns = append(unknowns, "input.http.checksum")
|
||||
}
|
||||
if _, ok := src.Source.Attrs[pb.AttrHTTPAuthHeaderSecret]; ok {
|
||||
inp.HTTP.HasAuth = true
|
||||
}
|
||||
if src.Image == nil {
|
||||
unknowns = append(unknowns, "input.http.checksum")
|
||||
} else {
|
||||
inp.HTTP.Checksum = src.Image.Digest
|
||||
}
|
||||
case "git":
|
||||
if !gitutil.IsGitTransport(refstr) {
|
||||
refstr = "https://" + refstr
|
||||
@@ -625,8 +626,13 @@ func AddUnknownsWithLogger(logf func(logrus.Level, string), req *gwpb.ResolveSou
|
||||
}
|
||||
req.Image.AttestationChain = true
|
||||
|
||||
case "git.ref", "git.checksum", "git.commitChecksum", "git.isAnnotatedTag", "git.isSHA256", "git.tagName", "git.branch":
|
||||
case "http.checksum":
|
||||
// HTTP checksums are resolved by BuildKit for the HTTP source itself.
|
||||
|
||||
case "git.ref", "git.checksum", "git.commitChecksum", "git.isAnnotatedTag", "git.isSHA256", "git.tagName", "git.branch":
|
||||
if req.Git == nil {
|
||||
req.Git = &gwpb.ResolveSourceGitRequest{}
|
||||
}
|
||||
case "git.commit", "git.tag":
|
||||
if req.Git == nil {
|
||||
req.Git = &gwpb.ResolveSourceGitRequest{}
|
||||
@@ -678,6 +684,15 @@ func summarizeUnknownsForLog(unk []string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func hasHTTPUnknowns(unk []string) bool {
|
||||
for _, u := range unk {
|
||||
if strings.HasPrefix(u, "input.http.") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func trimKey(s string) string {
|
||||
const (
|
||||
dot = '.'
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,22 @@ package tests
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/continuity/fs/fstest"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/buildx/util/gitutil"
|
||||
"github.com/docker/buildx/util/gitutil/gittestutil"
|
||||
"github.com/moby/buildkit/identity"
|
||||
"github.com/moby/buildkit/util/contentutil"
|
||||
"github.com/moby/buildkit/util/testutil"
|
||||
"github.com/moby/buildkit/util/testutil/httpserver"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -21,6 +27,9 @@ var policyBuildTests = []func(t *testing.T, sb integration.Sandbox){
|
||||
testBuildPolicyDeny,
|
||||
testBuildPolicyImageName,
|
||||
testBuildPolicyEnv,
|
||||
testBuildPolicyHTTP,
|
||||
testBuildPolicyGit,
|
||||
testBuildPolicyConfigFlags,
|
||||
}
|
||||
|
||||
func testBuildPolicyAllow(t *testing.T, sb integration.Sandbox) {
|
||||
@@ -537,3 +546,636 @@ decision := {"allow": allow}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildPolicyHTTP(t *testing.T, sb integration.Sandbox) {
|
||||
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
|
||||
resp := &httpserver.Response{Content: []byte("policy-http")}
|
||||
server := httpserver.NewTestServer(map[string]*httpserver.Response{
|
||||
"/file": resp,
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
parsedURL, err := url.Parse(server.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
baseURL := server.URL + "/file"
|
||||
queryURL := baseURL + "?policy=allow&case=http"
|
||||
checksum := digest.FromBytes(resp.Content).String()
|
||||
testCases := []struct {
|
||||
name string
|
||||
policy string
|
||||
addURL string
|
||||
wantErrContains string
|
||||
requiresHTTPChecksum bool
|
||||
}{
|
||||
{
|
||||
name: "http-url-allow",
|
||||
policy: fmt.Sprintf(`
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.http
|
||||
|
||||
allow if input.http.url == "%s"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`, queryURL),
|
||||
addURL: queryURL,
|
||||
},
|
||||
{
|
||||
name: "http-schema-allow",
|
||||
policy: `
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.http
|
||||
|
||||
allow if input.http.schema == "http"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`,
|
||||
addURL: baseURL,
|
||||
},
|
||||
{
|
||||
name: "http-host-allow",
|
||||
policy: fmt.Sprintf(`
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.http
|
||||
|
||||
allow if input.http.host == "%s"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`, parsedURL.Host),
|
||||
addURL: baseURL,
|
||||
},
|
||||
{
|
||||
name: "http-path-allow",
|
||||
policy: `
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.http
|
||||
|
||||
allow if input.http.path == "/file"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`,
|
||||
addURL: baseURL,
|
||||
},
|
||||
{
|
||||
name: "http-query-allow",
|
||||
policy: `
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.http
|
||||
|
||||
allow if input.http.query["policy"][_] == "allow"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`,
|
||||
addURL: queryURL,
|
||||
},
|
||||
{
|
||||
name: "http-checksum-allow",
|
||||
policy: fmt.Sprintf(`
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.http
|
||||
|
||||
allow if input.http.checksum == "%s"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`, checksum),
|
||||
addURL: baseURL,
|
||||
requiresHTTPChecksum: true,
|
||||
},
|
||||
{
|
||||
name: "http-checksum-deny",
|
||||
policy: `
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.http
|
||||
|
||||
allow if input.http.checksum == "sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`,
|
||||
addURL: baseURL,
|
||||
wantErrContains: "not allowed by policy",
|
||||
requiresHTTPChecksum: true,
|
||||
},
|
||||
{
|
||||
name: "http-host-deny",
|
||||
policy: `
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.http
|
||||
|
||||
allow if input.http.host == "example.invalid"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`,
|
||||
addURL: baseURL,
|
||||
wantErrContains: "not allowed by policy",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.requiresHTTPChecksum {
|
||||
sbDriver, _, _ := driverName(sb.Name())
|
||||
if sbDriver != "remote" {
|
||||
t.Skip("http checksum policy input requires remote driver")
|
||||
}
|
||||
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", dockerfile, 0600),
|
||||
fstest.CreateFile("policy.rego", []byte(tc.policy), 0600),
|
||||
)
|
||||
policyPath := filepath.Join(dir, "policy.rego")
|
||||
policyArg := "filename=" + policyPath
|
||||
if tc.name == "git-schema-allow" {
|
||||
policyArg = "log-level=debug," + policyArg
|
||||
}
|
||||
|
||||
cmd := buildxCmd(sb, withDir(dir), withArgs(
|
||||
"build",
|
||||
"--progress=plain",
|
||||
"--policy", policyArg,
|
||||
"--output=type=cacheonly",
|
||||
dir,
|
||||
))
|
||||
out, err := cmd.CombinedOutput()
|
||||
if tc.wantErrContains == "" {
|
||||
require.NoError(t, err, string(out))
|
||||
require.Contains(t, string(out), "loading policies "+policyPath)
|
||||
} else {
|
||||
require.Error(t, err, string(out))
|
||||
require.Contains(t, string(out), tc.wantErrContains)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildPolicyGit(t *testing.T, sb integration.Sandbox) {
|
||||
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
|
||||
|
||||
gitDir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "Dockerfile"), []byte("FROM busybox:latest\nRUN echo git\n"), 0600))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "a"), []byte("a"), 0600))
|
||||
|
||||
git, err := gitutil.New(gitutil.WithWorkingDir(gitDir))
|
||||
require.NoError(t, err)
|
||||
|
||||
gittestutil.GitInit(git, t)
|
||||
gittestutil.GitAdd(git, t, "Dockerfile", "a")
|
||||
gittestutil.GitCommit(git, t, "initial commit")
|
||||
|
||||
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")
|
||||
gittestutil.GitCommit(git, t, "b")
|
||||
_, err = git.Run("checkout", "-B", "v2")
|
||||
require.NoError(t, err)
|
||||
|
||||
commitHead, err := git.Run("rev-parse", "HEAD")
|
||||
require.NoError(t, err)
|
||||
commitTag, err := git.Run("rev-parse", "v0.1")
|
||||
require.NoError(t, err)
|
||||
commitTagCommit, err := git.Run("rev-parse", "v0.1^{commit}")
|
||||
require.NoError(t, err)
|
||||
baseURL := gittestutil.GitServeHTTP(git, t)
|
||||
tagURL := baseURL + "#v0.1"
|
||||
branchURL := baseURL + "#v2"
|
||||
parsedURL, err := url.Parse(baseURL)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
policy string
|
||||
context string
|
||||
wantErrContains string
|
||||
requiresGitResolve bool
|
||||
}{
|
||||
{
|
||||
name: "git-schema-allow",
|
||||
policy: `
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.git
|
||||
|
||||
allow if input.git.schema != ""
|
||||
|
||||
decision := {"allow": allow}
|
||||
`,
|
||||
context: baseURL,
|
||||
},
|
||||
{
|
||||
name: "git-host-allow",
|
||||
policy: fmt.Sprintf(`
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.git
|
||||
|
||||
allow if input.git.host == "%s"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`, parsedURL.Host),
|
||||
context: baseURL,
|
||||
},
|
||||
{
|
||||
name: "git-remote-allow",
|
||||
policy: fmt.Sprintf(`
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.git
|
||||
|
||||
allow if endswith(input.git.remote, "%s")
|
||||
|
||||
decision := {"allow": allow}
|
||||
`, parsedURL.Path),
|
||||
context: baseURL,
|
||||
},
|
||||
{
|
||||
name: "git-ref-tag-allow",
|
||||
policy: `
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.git
|
||||
|
||||
allow if input.git.ref == "refs/tags/v0.1"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`,
|
||||
context: tagURL,
|
||||
requiresGitResolve: true,
|
||||
},
|
||||
{
|
||||
name: "git-branch-allow",
|
||||
policy: `
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.git
|
||||
|
||||
allow if input.git.branch == "v2"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`,
|
||||
context: branchURL,
|
||||
requiresGitResolve: true,
|
||||
},
|
||||
{
|
||||
name: "git-tagname-allow",
|
||||
policy: `
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.git
|
||||
|
||||
allow if input.git.tagName == "v0.1"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`,
|
||||
context: tagURL,
|
||||
requiresGitResolve: true,
|
||||
},
|
||||
{
|
||||
name: "git-checksum-allow",
|
||||
policy: fmt.Sprintf(`
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.git
|
||||
|
||||
allow if input.git.checksum == "%s"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`, commitTag),
|
||||
context: tagURL,
|
||||
requiresGitResolve: true,
|
||||
},
|
||||
{
|
||||
name: "git-commit-checksum-allow",
|
||||
policy: fmt.Sprintf(`
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.git
|
||||
|
||||
allow if input.git.commitChecksum == "%s"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`, commitTagCommit),
|
||||
context: tagURL,
|
||||
requiresGitResolve: true,
|
||||
},
|
||||
{
|
||||
name: "git-annotated-tag-allow",
|
||||
policy: `
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.git
|
||||
|
||||
allow if input.git.isAnnotatedTag == true
|
||||
|
||||
decision := {"allow": allow}
|
||||
`,
|
||||
context: tagURL,
|
||||
requiresGitResolve: true,
|
||||
},
|
||||
{
|
||||
name: "git-commit-message-allow",
|
||||
policy: `
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.git
|
||||
|
||||
allow if input.git.commit.message == "initial commit"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`,
|
||||
context: tagURL,
|
||||
requiresGitResolve: true,
|
||||
},
|
||||
{
|
||||
name: "git-tag-object-allow",
|
||||
policy: `
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.git
|
||||
|
||||
allow if input.git.tag.tag == "v0.1"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`,
|
||||
context: tagURL,
|
||||
requiresGitResolve: true,
|
||||
},
|
||||
{
|
||||
name: "git-checksum-deny",
|
||||
policy: `
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.git
|
||||
|
||||
allow if input.git.checksum == "deadbeef"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`,
|
||||
context: tagURL,
|
||||
wantErrContains: "not allowed by policy",
|
||||
requiresGitResolve: true,
|
||||
},
|
||||
{
|
||||
name: "git-commit-ref-allow",
|
||||
policy: `
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.git
|
||||
|
||||
allow if input.git.isCommitRef == true
|
||||
|
||||
decision := {"allow": allow}
|
||||
`,
|
||||
context: baseURL + "#" + commitHead,
|
||||
requiresGitResolve: true,
|
||||
},
|
||||
{
|
||||
name: "git-host-deny",
|
||||
policy: `
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if not input.git
|
||||
|
||||
allow if input.git.host == "example.invalid"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`,
|
||||
context: tagURL,
|
||||
wantErrContains: "not allowed by policy",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.requiresGitResolve {
|
||||
sbDriver, _, _ := driverName(sb.Name())
|
||||
if sbDriver != "remote" {
|
||||
t.Skip("git policy metadata requires remote driver")
|
||||
}
|
||||
}
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("policy.rego", []byte(tc.policy), 0600),
|
||||
)
|
||||
policyPath := filepath.Join(dir, "policy.rego")
|
||||
|
||||
cmd := buildxCmd(sb, withDir(dir), withArgs(
|
||||
"build",
|
||||
"--progress=plain",
|
||||
"--policy", "filename="+policyPath,
|
||||
"--output=type=cacheonly",
|
||||
tc.context,
|
||||
))
|
||||
out, err := cmd.CombinedOutput()
|
||||
if tc.wantErrContains == "" {
|
||||
require.NoError(t, err, string(out))
|
||||
require.Contains(t, string(out), "loading policies "+policyPath)
|
||||
} else {
|
||||
require.Error(t, err, string(out))
|
||||
require.Contains(t, string(out), tc.wantErrContains)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildPolicyConfigFlags(t *testing.T, sb integration.Sandbox) {
|
||||
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
|
||||
|
||||
dockerfile := []byte("FROM busybox:latest\nRUN echo policy-flags\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}
|
||||
`)
|
||||
denyPolicy := []byte(`
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
decision := {"allow": allow}
|
||||
`)
|
||||
|
||||
t.Run("additional-policy-requires-default", func(t *testing.T) {
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||
fstest.CreateFile("Dockerfile.rego", defaultPolicy, 0600),
|
||||
fstest.CreateFile("extra.rego", extraPolicy, 0600),
|
||||
)
|
||||
extraPath := filepath.Join(dir, "extra.rego")
|
||||
|
||||
cmd := buildxCmd(sb, withDir(dir), withArgs(
|
||||
"build",
|
||||
"--progress=plain",
|
||||
"--policy", "filename="+extraPath,
|
||||
"--build-arg", "DEFAULT_OK=1",
|
||||
"--label", "com.example.extra=1",
|
||||
"--output=type=cacheonly",
|
||||
dir,
|
||||
))
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(out))
|
||||
|
||||
cmd = buildxCmd(sb, withDir(dir), withArgs(
|
||||
"build",
|
||||
"--progress=plain",
|
||||
"--policy", "filename="+extraPath,
|
||||
"--label", "com.example.extra=1",
|
||||
"--output=type=cacheonly",
|
||||
dir,
|
||||
))
|
||||
out, err = cmd.CombinedOutput()
|
||||
require.Error(t, err, string(out))
|
||||
require.Contains(t, string(out), "not allowed by policy")
|
||||
|
||||
cmd = buildxCmd(sb, withDir(dir), withArgs(
|
||||
"build",
|
||||
"--progress=plain",
|
||||
"--policy", "filename="+extraPath,
|
||||
"--build-arg", "DEFAULT_OK=1",
|
||||
"--output=type=cacheonly",
|
||||
dir,
|
||||
))
|
||||
out, err = cmd.CombinedOutput()
|
||||
require.Error(t, err, string(out))
|
||||
require.Contains(t, string(out), "not allowed by policy")
|
||||
})
|
||||
|
||||
t.Run("reset-ignores-default", func(t *testing.T) {
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||
fstest.CreateFile("Dockerfile.rego", defaultPolicy, 0600),
|
||||
fstest.CreateFile("extra.rego", extraPolicy, 0600),
|
||||
)
|
||||
extraPath := filepath.Join(dir, "extra.rego")
|
||||
|
||||
cmd := buildxCmd(sb, withDir(dir), withArgs(
|
||||
"build",
|
||||
"--progress=plain",
|
||||
"--policy", "reset=true,filename="+extraPath,
|
||||
"--label", "com.example.extra=1",
|
||||
"--output=type=cacheonly",
|
||||
dir,
|
||||
))
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(out))
|
||||
|
||||
cmd = buildxCmd(sb, withDir(dir), withArgs(
|
||||
"build",
|
||||
"--progress=plain",
|
||||
"--policy", "reset=true,filename="+extraPath,
|
||||
"--output=type=cacheonly",
|
||||
dir,
|
||||
))
|
||||
out, err = cmd.CombinedOutput()
|
||||
require.Error(t, err, string(out))
|
||||
require.Contains(t, string(out), "not allowed by policy")
|
||||
})
|
||||
|
||||
t.Run("disabled-skips-default", func(t *testing.T) {
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||
fstest.CreateFile("Dockerfile.rego", denyPolicy, 0600),
|
||||
)
|
||||
|
||||
cmd := buildxCmd(sb, withDir(dir), withArgs(
|
||||
"build",
|
||||
"--progress=plain",
|
||||
"--policy", "disabled=true",
|
||||
"--output=type=cacheonly",
|
||||
dir,
|
||||
))
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(out))
|
||||
})
|
||||
|
||||
t.Run("disabled-cannot-combine-with-extra", func(t *testing.T) {
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||
fstest.CreateFile("extra.rego", denyPolicy, 0600),
|
||||
)
|
||||
extraPath := filepath.Join(dir, "extra.rego")
|
||||
|
||||
cmd := buildxCmd(sb, withDir(dir), withArgs(
|
||||
"build",
|
||||
"--progress=plain",
|
||||
"--policy", "filename="+extraPath,
|
||||
"--policy", "disabled=true",
|
||||
"--output=type=cacheonly",
|
||||
dir,
|
||||
))
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.Error(t, err, string(out))
|
||||
require.Contains(t, string(out), "disabled policy cannot be combined with other policy flags")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,12 +5,15 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
urlpkg "net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/continuity/fs/fstest"
|
||||
"github.com/docker/buildx/policy"
|
||||
"github.com/moby/buildkit/identity"
|
||||
"github.com/moby/buildkit/util/testutil/httpserver"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -20,6 +23,7 @@ var policyEvalTests = []func(t *testing.T, sb integration.Sandbox){
|
||||
testPolicyEvalPrint,
|
||||
testPolicyEvalFields,
|
||||
testPolicyEvalLabel,
|
||||
testPolicyEvalHTTP,
|
||||
}
|
||||
|
||||
func testPolicyEvalAllow(t *testing.T, sb integration.Sandbox) {
|
||||
@@ -215,3 +219,151 @@ decision := {"allow": allow}
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(out))
|
||||
}
|
||||
|
||||
func testPolicyEvalHTTP(t *testing.T, sb integration.Sandbox) {
|
||||
resp := &httpserver.Response{Content: []byte("policy-eval-http")}
|
||||
server := httpserver.NewTestServer(map[string]*httpserver.Response{
|
||||
"/file": resp,
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
url := server.URL + "/file"
|
||||
queryURL := url + "?policy=allow"
|
||||
checksum := digest.FromBytes(resp.Content).String()
|
||||
parsedURL, err := urlpkg.Parse(server.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
policy string
|
||||
sourceURL string
|
||||
wantErrContains string
|
||||
needsChecksum bool
|
||||
}{
|
||||
{
|
||||
name: "http-url-allow",
|
||||
policy: fmt.Sprintf(`
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if input.http.url == "%s"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`, queryURL),
|
||||
sourceURL: queryURL,
|
||||
},
|
||||
{
|
||||
name: "http-host-allow",
|
||||
policy: fmt.Sprintf(`
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if input.http.host == "%s"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`, parsedURL.Host),
|
||||
sourceURL: url,
|
||||
},
|
||||
{
|
||||
name: "http-path-allow",
|
||||
policy: `
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if input.http.path == "/file"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`,
|
||||
sourceURL: url,
|
||||
},
|
||||
{
|
||||
name: "http-query-allow",
|
||||
policy: `
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if input.http.query["policy"][_] == "allow"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`,
|
||||
sourceURL: queryURL,
|
||||
},
|
||||
{
|
||||
name: "http-checksum-allow",
|
||||
policy: fmt.Sprintf(`
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if input.http.checksum == "%s"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`, checksum),
|
||||
sourceURL: url,
|
||||
needsChecksum: true,
|
||||
},
|
||||
{
|
||||
name: "http-checksum-deny",
|
||||
policy: `
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if input.http.checksum == "sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`,
|
||||
sourceURL: url,
|
||||
wantErrContains: "policy denied",
|
||||
needsChecksum: true,
|
||||
},
|
||||
{
|
||||
name: "http-host-deny",
|
||||
policy: `
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if input.http.host == "example.invalid"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`,
|
||||
sourceURL: url,
|
||||
wantErrContains: "policy denied",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.needsChecksum {
|
||||
sbDriver, _, _ := driverName(sb.Name())
|
||||
if sbDriver != "remote" {
|
||||
t.Skip("http checksum policy eval requires remote driver")
|
||||
}
|
||||
skipNoCompatBuildKit(t, sb, ">= 0.26.3-0", "http checksum policy input")
|
||||
}
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("policy.rego", []byte(tc.policy), 0600),
|
||||
)
|
||||
cmd := buildxCmd(sb, withDir(dir), withArgs(
|
||||
"policy",
|
||||
"eval",
|
||||
"--filename",
|
||||
"policy",
|
||||
tc.sourceURL,
|
||||
))
|
||||
out, err := cmd.CombinedOutput()
|
||||
if tc.wantErrContains == "" {
|
||||
require.NoError(t, err, string(out))
|
||||
} else {
|
||||
require.Error(t, err, string(out))
|
||||
require.Contains(t, string(out), tc.wantErrContains)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TestServer struct {
|
||||
*httptest.Server
|
||||
mu sync.Mutex
|
||||
routes map[string]*Response
|
||||
stats map[string]*Stat
|
||||
}
|
||||
|
||||
func NewTestServer(routes map[string]*Response) *TestServer {
|
||||
ts := &TestServer{
|
||||
routes: routes,
|
||||
stats: map[string]*Stat{},
|
||||
}
|
||||
ts.Server = httptest.NewServer(ts)
|
||||
return ts
|
||||
}
|
||||
|
||||
func (s *TestServer) SetRoute(name string, resp *Response) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.routes[name] = resp
|
||||
}
|
||||
|
||||
func (s *TestServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.mu.Lock()
|
||||
resp, ok := s.routes[r.URL.Path]
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := s.stats[r.URL.Path]; !ok {
|
||||
s.stats[r.URL.Path] = &Stat{}
|
||||
}
|
||||
|
||||
s.stats[r.URL.Path].AllRequests++
|
||||
s.stats[r.URL.Path].Requests = append(s.stats[r.URL.Path].Requests, newRequest(r))
|
||||
|
||||
if resp.LastModified != nil {
|
||||
w.Header().Set("Last-Modified", resp.LastModified.Format(time.RFC850))
|
||||
}
|
||||
|
||||
if resp.ContentEncoding != "" {
|
||||
w.Header().Set("Content-Encoding", resp.ContentEncoding)
|
||||
}
|
||||
|
||||
if resp.ContentDisposition != "" {
|
||||
w.Header().Set("Content-Disposition", resp.ContentDisposition)
|
||||
}
|
||||
|
||||
if resp.Etag != "" {
|
||||
w.Header().Set("ETag", resp.Etag)
|
||||
if match := r.Header.Get("If-None-Match"); match == resp.Etag {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
s.stats[r.URL.Path].CachedRequests++
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s.mu.Unlock()
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
io.Copy(w, bytes.NewReader(resp.Content))
|
||||
}
|
||||
|
||||
func (s *TestServer) Stats(name string) (st Stat) {
|
||||
if st, ok := s.stats[name]; ok {
|
||||
return *st
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Content []byte
|
||||
Etag string
|
||||
LastModified *time.Time
|
||||
ContentEncoding string
|
||||
ContentDisposition string
|
||||
}
|
||||
|
||||
type Stat struct {
|
||||
AllRequests, CachedRequests int
|
||||
Requests []Request
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Method string
|
||||
Header http.Header
|
||||
}
|
||||
|
||||
func newRequest(r *http.Request) Request {
|
||||
headers := http.Header{}
|
||||
for k, v := range r.Header {
|
||||
headers[k] = slices.Clone(v)
|
||||
}
|
||||
return Request{
|
||||
Method: r.Method,
|
||||
Header: headers,
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -733,6 +733,7 @@ github.com/moby/buildkit/util/system
|
||||
github.com/moby/buildkit/util/testutil
|
||||
github.com/moby/buildkit/util/testutil/dockerd
|
||||
github.com/moby/buildkit/util/testutil/dockerd/client
|
||||
github.com/moby/buildkit/util/testutil/httpserver
|
||||
github.com/moby/buildkit/util/testutil/integration
|
||||
github.com/moby/buildkit/util/testutil/workers
|
||||
github.com/moby/buildkit/util/tracing
|
||||
|
||||
Reference in New Issue
Block a user