build: resolve policy files from context state and unified FS

Support remote context policy files via resolved context state and cwd:// override.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi
2026-02-23 16:22:19 -08:00
parent 5830f90beb
commit b62fc30524
7 changed files with 897 additions and 153 deletions
+20 -7
View File
@@ -8,7 +8,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/fs"
"maps"
"os"
"slices"
@@ -22,7 +21,6 @@ import (
noderesolver "github.com/docker/buildx/build/resolver"
"github.com/docker/buildx/builder"
"github.com/docker/buildx/driver"
"github.com/docker/buildx/policy"
"github.com/docker/buildx/util/buildflags"
"github.com/docker/buildx/util/confutil"
"github.com/docker/buildx/util/desktop"
@@ -120,13 +118,24 @@ type Inputs struct {
policy *policyOpt
}
type policyOpt struct {
Files []policy.File
FS func() (fs.StatFS, func() error, error)
type policyFileSpec struct {
Filename string
Optional bool
Data []byte
}
type policyEvalOpt struct {
Strict bool
LogLevel *logrus.Level
}
type policyOpt struct {
Files []policyFileSpec
ContextDir string
ContextState *llb.State
policyEvalOpt
}
func withPolicyConfig(defaultPolicy policyOpt, configs []buildflags.PolicyConfig) ([]policyOpt, error) {
if len(configs) == 0 {
if len(defaultPolicy.Files) == 0 {
@@ -176,7 +185,10 @@ func withPolicyConfig(defaultPolicy policyOpt, configs []buildflags.PolicyConfig
}
opt := policyOpt{
Files: cfg.Files,
Files: make([]policyFileSpec, 0, len(cfg.Files)),
}
for _, f := range cfg.Files {
opt.Files = append(opt.Files, policyFileSpec{Filename: f.Filename})
}
if last.Strict != nil {
opt.Strict = *last.Strict
@@ -190,7 +202,8 @@ func withPolicyConfig(defaultPolicy policyOpt, configs []buildflags.PolicyConfig
if cfg.LogLevel != nil {
opt.LogLevel = cfg.LogLevel
}
opt.FS = defaultPolicy.FS
opt.ContextDir = defaultPolicy.ContextDir
opt.ContextState = defaultPolicy.ContextState
out = append(out, opt)
}
+166 -113
View File
@@ -5,7 +5,6 @@ import (
"context"
"fmt"
"io"
"io/fs"
"maps"
"os"
"path"
@@ -495,90 +494,11 @@ func toSolveOpt(ctx context.Context, np *noderesolver.ResolvedNode, multiDriver
releaseLoad()
})
if opt.Inputs.policy == nil {
if len(opt.Policy) > 0 {
return nil, nil, errors.New("policy file specified but no policy FS in build context")
}
} else {
env := policy.Env{}
for k, v := range opt.BuildArgs {
if env.Args == nil {
env.Args = map[string]*string{}
}
env.Args[k] = &v
}
env.Filename = path.Base(opt.Inputs.DockerfilePath)
env.Target = opt.Target
env.Labels = opt.Labels
popts, err := withPolicyConfig(*opt.Inputs.policy, opt.Policy)
if err != nil {
return nil, nil, err
}
var sourceResolver *sourcemeta.Resolver
if len(popts) > 0 {
c, err := np.Client(ctx)
if err != nil {
return nil, nil, err
}
sourceResolver = sourcemeta.NewResolver(c, sourcemeta.WithProgressWriter(pw))
defers = append(defers, func(error) {
_ = sourceResolver.Close()
})
}
var policyFiles []string
for _, popt := range popts {
for _, f := range popt.Files {
if f.Filename != "" {
policyFiles = append(policyFiles, f.Filename)
}
}
}
var policyLogger *policyProgressLogger
if len(policyFiles) > 0 {
policyLogger = newPolicyProgressLogger(pw, fmt.Sprintf("loading policies %s", strings.Join(policyFiles, ", ")))
}
var policies []*policy.Policy
if policyLogger != nil {
defers = append(defers, func(inErr error) {
if len(policysession.DenyMessages(inErr)) > 0 || isPolicyEvaluationError(policies, inErr) {
policyLogger.Close(inErr)
return
}
policyLogger.Close(nil)
})
}
var cbs []policysession.PolicyCallback
for _, popt := range popts {
policyLevel := logrus.GetLevel()
if popt.LogLevel != nil {
policyLevel = *popt.LogLevel
}
logf := func(level logrus.Level, msg string) {
if policyLogger == nil || level > policyLevel {
return
}
policyLogger.Log(msg)
}
p := policy.NewPolicy(policy.Opt{
Files: popt.Files,
Env: env,
Log: logf,
FS: opt.Inputs.policy.FS,
VerifierProvider: policy.SignatureVerifier(cfg),
DefaultPlatform: defaultPlatform(bopts),
SourceResolver: sourceResolver,
})
policies = append(policies, p)
cbs = append(cbs, p.CheckPolicy)
if popt.Strict {
if bopts.LLBCaps.Supports(pb.CapSourcePolicySession) != nil {
return nil, nil, errors.New("strict policy is not supported by the current BuildKit daemon, please upgrade to version v0.27+")
}
}
}
so.SourcePolicyProvider = policysession.NewPolicyProvider(policy.MultiPolicyCallback(cbs...))
policyDefers, err := configureSourcePolicy(ctx, np, opt, cfg, bopts, &so, pw)
if err != nil {
return nil, nil, err
}
defers = append(defers, policyDefers...)
// add node identifier to shared key if one was specified
nodeID := cfg.TryNodeIdentifier()
@@ -667,6 +587,114 @@ func toSolveOpt(ctx context.Context, np *noderesolver.ResolvedNode, multiDriver
return &so, releaseF, nil
}
func configureSourcePolicy(ctx context.Context, np *noderesolver.ResolvedNode, opt *Options, cfg *confutil.Config, bopts gateway.BuildOpts, so *client.SolveOpt, pw progress.Writer) (defers []func(error), err error) {
if opt.Inputs.policy == nil {
if len(opt.Policy) > 0 {
return nil, errors.New("policy file specified but no policy FS in build context")
}
so.SourcePolicyProvider = nil
return nil, nil
}
env := policy.Env{}
for k, v := range opt.BuildArgs {
if env.Args == nil {
env.Args = map[string]*string{}
}
env.Args[k] = &v
}
env.Filename = path.Base(opt.Inputs.DockerfilePath)
env.Target = opt.Target
env.Labels = opt.Labels
popts, err := withPolicyConfig(*opt.Inputs.policy, opt.Policy)
if err != nil {
return nil, err
}
if len(popts) == 0 {
so.SourcePolicyProvider = nil
return nil, nil
}
c, err := np.Client(ctx)
if err != nil {
return nil, err
}
sourceResolver := sourcemeta.NewResolver(c, sourcemeta.WithProgressWriter(pw))
defers = []func(error){
func(error) {
_ = sourceResolver.Close()
},
}
defer func() {
if err == nil {
return
}
for _, f := range defers {
f(err)
}
defers = nil
}()
loadedOpts, err := resolvePolicyOpts(ctx, popts, sourceResolver)
if err != nil {
return nil, err
}
var policyFiles []string
for _, popt := range loadedOpts {
for _, f := range popt.Files {
if f.Filename != "" {
policyFiles = append(policyFiles, f.Filename)
}
}
}
var policyLogger *policyProgressLogger
if len(policyFiles) > 0 {
policyLogger = newPolicyProgressLogger(pw, fmt.Sprintf("loading policies %s", strings.Join(policyFiles, ", ")))
}
var policies []*policy.Policy
if policyLogger != nil {
defers = append(defers, func(inErr error) {
if len(policysession.DenyMessages(inErr)) > 0 || isPolicyEvaluationError(policies, inErr) {
policyLogger.Close(inErr)
return
}
policyLogger.Close(nil)
})
}
var cbs []policysession.PolicyCallback
for _, popt := range loadedOpts {
policyLevel := logrus.GetLevel()
if popt.LogLevel != nil {
policyLevel = *popt.LogLevel
}
logf := func(level logrus.Level, msg string) {
if policyLogger == nil || level > policyLevel {
return
}
policyLogger.Log(msg)
}
p := policy.NewPolicy(policy.Opt{
Files: popt.Files,
Env: env,
Log: logf,
FS: popt.FS,
VerifierProvider: policy.SignatureVerifier(cfg),
DefaultPlatform: defaultPlatform(bopts),
SourceResolver: sourceResolver,
})
policies = append(policies, p)
cbs = append(cbs, p.CheckPolicy)
if popt.Strict {
if bopts.LLBCaps.Supports(pb.CapSourcePolicySession) != nil {
return nil, errors.New("strict policy is not supported by the current BuildKit daemon, please upgrade to version v0.27+")
}
}
}
so.SourcePolicyProvider = policysession.NewPolicyProvider(policy.MultiPolicyCallback(cbs...))
return defers, nil
}
func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw progress.Writer, target *client.SolveOpt) (func(), error) {
if inp.ContextPath == "" {
return nil, errors.New("please specify build context (e.g. \".\" for the current directory)")
@@ -678,6 +706,8 @@ func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw pro
err error
dockerfileReader io.ReadCloser
contextDir string
remoteContext bool
remotePolicyState *llb.State
dockerfileDir string
dockerfileName = inp.DockerfilePath
dockerfileSrcName = inp.DockerfilePath
@@ -687,6 +717,7 @@ func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw pro
switch {
case inp.ContextState != nil:
remotePolicyState = inp.ContextState
if target.FrontendInputs == nil {
target.FrontendInputs = make(map[string]llb.State)
}
@@ -745,6 +776,7 @@ func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw pro
dockerfileName = filepath.Base(inp.DockerfilePath)
}
case urlutil.IsRemoteURL(inp.ContextPath):
remoteContext = true
if inp.DockerfilePath == "-" {
dockerfileReader = inp.InStream.NewReadCloser()
} else if filepath.IsAbs(inp.DockerfilePath) {
@@ -758,6 +790,7 @@ func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw pro
return nil, err
}
if st, ok := target.FrontendInputs["context"]; ok {
remotePolicyState = &st
if dockerfileReader == nil && !filepath.IsAbs(inp.DockerfilePath) {
target.FrontendInputs["dockerfile"] = st
}
@@ -801,22 +834,11 @@ func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw pro
}
p := &policyOpt{
FS: func() (fs.StatFS, func() error, error) {
if contextDir == "" {
return nil, nil, errors.Errorf("unimplemented, cannot use policy file without a local build context")
}
root, err := os.OpenRoot(contextDir)
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to open root for policy file %s.rego", dockerfileName)
}
baseFS := root.FS()
statFS, ok := baseFS.(fs.StatFS)
if !ok {
root.Close()
return nil, nil, errors.Errorf("invalid root FS type %T", baseFS)
}
return statFS, root.Close, nil
},
ContextDir: contextDir,
}
p.ContextState = remotePolicyState
if p.ContextState == nil && remoteContext {
p.ContextState = resolveRemotePolicyContextState(inp.ContextPath, target)
}
if dockerfileDir != "" {
@@ -824,22 +846,31 @@ func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw pro
return nil, err
}
dockerfileName = handleLowercaseDockerfile(dockerfileDir, dockerfileName)
if fi, err := os.Lstat(filepath.Join(dockerfileDir, dockerfileName+".rego")); err == nil {
if fi.Mode().IsRegular() {
dt, err := os.ReadFile(filepath.Join(dockerfileDir, dockerfileName+".rego"))
if err != nil {
return nil, errors.Wrapf(err, "failed to read policy file %s.rego", dockerfileName)
}
p.Files = []policy.File{
{
Filename: dockerfileName + ".rego",
Data: dt,
},
}
}
defaultPolicyFilename := dockerfileName + ".rego"
if dockerfileDir != "" {
defaultPolicyFilename = filepath.Join(dockerfileDir, defaultPolicyFilename)
}
defaultPolicy := policyFileSpec{
Filename: defaultPolicyFilename,
Optional: true,
}
includeDefaultPolicy := true
if dockerfileDir != "" && p.ContextState == nil {
dt, err := os.ReadFile(defaultPolicyFilename)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return nil, errors.Wrapf(err, "failed to read policy file %s", defaultPolicyFilename)
}
includeDefaultPolicy = false
} else {
defaultPolicy.Data = dt
}
}
if includeDefaultPolicy {
p.Files = append(p.Files, defaultPolicy)
}
inp.policy = p
target.FrontendAttrs["filename"] = dockerfileName
@@ -928,6 +959,28 @@ func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw pro
return release, nil
}
func resolveRemotePolicyContextState(contextPath string, target *client.SolveOpt) *llb.State {
if target != nil && target.FrontendInputs != nil {
if st, ok := target.FrontendInputs["context"]; ok {
return &st
}
}
keepGitDir := false
if st, ok, _ := dockerui.DetectGitContext(contextPath, &keepGitDir); ok {
return st
}
st, filename, ok := dockerui.DetectHTTPContext(contextPath)
if !ok || filename == "" {
return nil
}
bc := llb.Scratch().File(llb.Copy(*st, filename, "/", &llb.CopyInfo{
AttemptUnpack: true,
}))
return &bc
}
func resolveDigest(localPath, tag string) (dig string, _ error) {
idx := ociindex.NewStoreIndex(localPath)
+391
View File
@@ -0,0 +1,391 @@
package build
import (
"bytes"
"context"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
"sync"
"time"
"github.com/docker/buildx/policy"
"github.com/docker/buildx/util/sourcemeta"
"github.com/moby/buildkit/client/llb"
gwclient "github.com/moby/buildkit/frontend/gateway/client"
"github.com/pkg/errors"
"github.com/tonistiigi/fsutil/types"
)
type loadedPolicyOpt struct {
Files []policy.File
FS func() (fs.StatFS, func() error, error)
policyEvalOpt
}
func resolvePolicyOpts(ctx context.Context, in []policyOpt, resolver *sourcemeta.Resolver) ([]loadedPolicyOpt, error) {
if len(in) == 0 {
return nil, nil
}
out := make([]loadedPolicyOpt, 0, len(in))
for _, popt := range in {
provider := newPolicyPathFS(ctx, resolver, popt)
loaded := loadedPolicyOpt{
policyEvalOpt: popt.policyEvalOpt,
FS: provider,
}
for _, f := range popt.Files {
if f.Data != nil {
loaded.Files = append(loaded.Files, policy.File{
Filename: f.Filename,
Data: f.Data,
})
continue
}
dt, ok, err := loadPolicyData(provider, f.Filename)
if err != nil {
return nil, err
}
if !ok {
if f.Optional {
continue
}
return nil, errors.Errorf("policy file %s not found", f.Filename)
}
loaded.Files = append(loaded.Files, policy.File{
Filename: f.Filename,
Data: dt,
})
}
if len(loaded.Files) > 0 {
out = append(out, loaded)
}
}
return out, nil
}
func loadPolicyData(provider func() (fs.StatFS, func() error, error), filename string) ([]byte, bool, error) {
root, closeFS, err := provider()
if err != nil {
return nil, false, errors.Wrapf(err, "failed to get policy FS for %s", filename)
}
if closeFS != nil {
defer closeFS()
}
if root == nil {
return nil, false, nil
}
if _, err := root.Stat(filename); err != nil {
if isFileNotFoundError(err) {
return nil, false, nil
}
return nil, false, errors.Wrapf(err, "failed to stat policy file %s", filename)
}
dt, err := fs.ReadFile(root, filename)
if err != nil {
if isFileNotFoundError(err) {
return nil, false, nil
}
return nil, false, errors.Wrapf(err, "failed to read policy file %s", filename)
}
return dt, true, nil
}
type policyPathFS struct {
ctx context.Context
resolver *sourcemeta.Resolver
contextDir string
contextState *llb.State
cwdFS memoizedPolicyFS
contextFS memoizedPolicyFS
}
func newPolicyPathFS(ctx context.Context, resolver *sourcemeta.Resolver, popt policyOpt) func() (fs.StatFS, func() error, error) {
p := &policyPathFS{
ctx: context.WithoutCancel(ctx),
resolver: resolver,
contextDir: popt.ContextDir,
contextState: popt.ContextState,
}
p.cwdFS.init = func() (fs.StatFS, func() error, error) {
root, err := os.OpenRoot(".")
if err != nil {
return nil, nil, err
}
baseFS := root.FS()
statFS, ok := baseFS.(fs.StatFS)
if !ok {
root.Close()
return nil, nil, errors.Errorf("invalid root FS type %T", baseFS)
}
return statFS, root.Close, nil
}
p.contextFS.init = func() (fs.StatFS, func() error, error) {
if p.contextState != nil {
if resolver == nil {
return nil, nil, errors.New("policy resolver is not configured")
}
return newRemotePolicyFS(p.ctx, resolver, *p.contextState), nil, nil
}
if p.contextDir == "" {
return nil, nil, nil
}
root, err := os.OpenRoot(p.contextDir)
if err != nil {
return nil, nil, err
}
baseFS := root.FS()
statFS, ok := baseFS.(fs.StatFS)
if !ok {
root.Close()
return nil, nil, errors.Errorf("invalid root FS type %T", baseFS)
}
return statFS, root.Close, nil
}
return func() (fs.StatFS, func() error, error) {
return p, p.Close, nil
}
}
func (p *policyPathFS) Open(name string) (fs.File, error) {
backend, target, err := p.resolve(name)
if err != nil {
return nil, err
}
if backend == nil {
return nil, fs.ErrNotExist
}
return backend.Open(target)
}
func (p *policyPathFS) Stat(name string) (fs.FileInfo, error) {
backend, target, err := p.resolve(name)
if err != nil {
return nil, err
}
if backend == nil {
return nil, fs.ErrNotExist
}
return backend.Stat(target)
}
func (p *policyPathFS) Close() error {
if err := p.cwdFS.close(); err != nil {
return err
}
return p.contextFS.close()
}
func (p *policyPathFS) resolve(name string) (fs.StatFS, string, error) {
if name == "" {
return nil, "", errors.New("policy filename is empty")
}
if v, ok := strings.CutPrefix(name, "cwd://"); ok {
if v == "" {
return nil, "", errors.Errorf("invalid policy filename %q", name)
}
cwd, err := p.cwdFS.get()
if err != nil {
return nil, "", err
}
return cwd, filepath.Clean(v), nil
}
contextFS, err := p.contextFS.get()
if err != nil {
return nil, "", err
}
if p.contextState != nil {
target, err := normalizeRemotePolicyPath(name)
if err != nil {
return nil, "", err
}
return contextFS, target, nil
}
return contextFS, normalizeLocalPolicyPath(name, p.contextDir), nil
}
func normalizeLocalPolicyPath(name, contextDir string) string {
if filepath.IsAbs(name) && contextDir != "" {
if rel, err := filepath.Rel(contextDir, name); err == nil {
rel = filepath.Clean(rel)
if rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return rel
}
}
}
return filepath.Clean(name)
}
type memoizedPolicyFS struct {
init func() (fs.StatFS, func() error, error)
once sync.Once
fs fs.StatFS
closeFn func() error
err error
}
func (m *memoizedPolicyFS) get() (fs.StatFS, error) {
m.once.Do(func() {
if m.init == nil {
return
}
m.fs, m.closeFn, m.err = m.init()
})
if m.err != nil {
return nil, m.err
}
return m.fs, nil
}
func (m *memoizedPolicyFS) close() error {
if m.closeFn != nil {
return m.closeFn()
}
return nil
}
func normalizeRemotePolicyPath(raw string) (string, error) {
clean := strings.TrimPrefix(path.Join("/", filepath.ToSlash(raw)), "/")
if clean == "." || clean == "" {
return "", errors.Errorf("invalid remote policy filename %q", raw)
}
return clean, nil
}
func isFileNotFoundError(err error) bool {
if err == nil {
return false
}
if errors.Is(err, fs.ErrNotExist) {
return true
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "not found") || strings.Contains(msg, "no such file")
}
type remotePolicyFS struct {
ctx context.Context
resolver *sourcemeta.Resolver
state llb.State
once sync.Once
ref gwclient.Reference
err error
}
func newRemotePolicyFS(ctx context.Context, resolver *sourcemeta.Resolver, state llb.State) *remotePolicyFS {
return &remotePolicyFS{
ctx: context.WithoutCancel(ctx),
resolver: resolver,
state: state,
}
}
func (r *remotePolicyFS) Open(name string) (fs.File, error) {
p, err := normalizeRemotePolicyPath(name)
if err != nil {
return nil, err
}
ref, err := r.resolveRef()
if err != nil {
return nil, err
}
st, err := ref.StatFile(r.ctx, gwclient.StatRequest{Path: p})
if err != nil {
return nil, err
}
dt, err := ref.ReadFile(r.ctx, gwclient.ReadRequest{Filename: p})
if err != nil {
return nil, err
}
fi := policyFileInfo{
name: path.Base(p),
size: int64(len(dt)),
mode: fs.FileMode(st.Mode),
tm: time.Unix(0, st.ModTime),
}
if fi.size == 0 {
fi.size = st.Size
}
return &policyReadFile{
Reader: bytes.NewReader(dt),
info: fi,
}, nil
}
func (r *remotePolicyFS) Stat(name string) (fs.FileInfo, error) {
p, err := normalizeRemotePolicyPath(name)
if err != nil {
return nil, err
}
ref, err := r.resolveRef()
if err != nil {
return nil, err
}
st, err := ref.StatFile(r.ctx, gwclient.StatRequest{Path: p})
if err != nil {
return nil, err
}
return policyFileInfo{
name: path.Base(p),
size: st.Size,
mode: fs.FileMode(st.Mode),
tm: time.Unix(0, st.ModTime),
}, nil
}
func (r *remotePolicyFS) resolveRef() (gwclient.Reference, error) {
r.once.Do(func() {
r.ref, r.err = r.resolver.ResolveState(r.ctx, r.state)
})
if r.err != nil {
return nil, r.err
}
return r.ref, nil
}
type policyReadFile struct {
*bytes.Reader
info policyFileInfo
}
func (f *policyReadFile) Stat() (fs.FileInfo, error) {
return f.info, nil
}
func (f *policyReadFile) Close() error {
return nil
}
type policyFileInfo struct {
name string
size int64
mode fs.FileMode
tm time.Time
}
func (i policyFileInfo) Name() string { return i.name }
func (i policyFileInfo) Size() int64 { return i.size }
func (i policyFileInfo) Mode() fs.FileMode { return i.mode }
func (i policyFileInfo) ModTime() time.Time { return i.tm }
func (i policyFileInfo) IsDir() bool { return i.mode.IsDir() }
func (i policyFileInfo) Sys() any {
return &types.Stat{Mode: uint32(i.mode), Size: i.size, ModTime: i.tm.UnixNano()}
}
var _ fs.StatFS = (*policyPathFS)(nil)
var _ fs.StatFS = (*remotePolicyFS)(nil)
var _ fs.File = (*policyReadFile)(nil)
var _ io.ReaderAt = (*bytes.Reader)(nil)
+9 -19
View File
@@ -1,7 +1,6 @@
package build
import (
"io/fs"
"testing"
"github.com/docker/buildx/policy"
@@ -21,11 +20,8 @@ func levelPtr(v logrus.Level) *logrus.Level {
// TestWithPolicyConfigDefaults ensures default policy is returned when no configs are provided.
func TestWithPolicyConfigDefaults(t *testing.T) {
defaultPolicy := policyOpt{
Files: []policy.File{
{Filename: "default.rego", Data: []byte("package policy")},
},
FS: func() (fs.StatFS, func() error, error) {
return nil, nil, nil
Files: []policyFileSpec{
{Filename: "default.rego", Optional: true},
},
}
@@ -35,7 +31,6 @@ func TestWithPolicyConfigDefaults(t *testing.T) {
require.Equal(t, defaultPolicy.Files, out[0].Files)
require.False(t, out[0].Strict)
require.Nil(t, out[0].LogLevel)
require.NotNil(t, out[0].FS)
}
// TestWithPolicyConfigDisabled validates disabled policy behavior across invalid and valid combinations.
@@ -76,10 +71,7 @@ func TestWithPolicyConfigDisabled(t *testing.T) {
// TestWithPolicyConfigResetAndFiles ensures reset drops defaults and uses explicitly provided files.
func TestWithPolicyConfigResetAndFiles(t *testing.T) {
defaultPolicy := policyOpt{
Files: []policy.File{{Filename: "default.rego"}},
FS: func() (fs.StatFS, func() error, error) {
return nil, nil, nil
},
Files: []policyFileSpec{{Filename: "default.rego", Optional: true}},
}
out, err := withPolicyConfig(defaultPolicy, []buildflags.PolicyConfig{
@@ -89,13 +81,13 @@ func TestWithPolicyConfigResetAndFiles(t *testing.T) {
require.NoError(t, err)
require.Len(t, out, 1)
require.Equal(t, "a.rego", out[0].Files[0].Filename)
require.NotNil(t, out[0].FS)
require.False(t, out[0].Files[0].Optional)
}
// TestWithPolicyConfigStrictAndLogLevel ensures strict and log level apply to existing policy.
func TestWithPolicyConfigStrictAndLogLevel(t *testing.T) {
defaultPolicy := policyOpt{
Files: []policy.File{{Filename: "default.rego"}},
Files: []policyFileSpec{{Filename: "default.rego", Optional: true}},
}
out, err := withPolicyConfig(defaultPolicy, []buildflags.PolicyConfig{
@@ -120,10 +112,7 @@ func TestWithPolicyConfigStrictIgnoredWithoutPolicy(t *testing.T) {
// TestWithPolicyConfigMultipleFilesAndOverrides ensures per-entry overrides and carryover apply across multiple files.
func TestWithPolicyConfigMultipleFilesAndOverrides(t *testing.T) {
defaultPolicy := policyOpt{
Files: []policy.File{{Filename: "default.rego"}},
FS: func() (fs.StatFS, func() error, error) {
return nil, nil, nil
},
Files: []policyFileSpec{{Filename: "default.rego", Optional: true}},
}
out, err := withPolicyConfig(defaultPolicy, []buildflags.PolicyConfig{
@@ -134,12 +123,13 @@ func TestWithPolicyConfigMultipleFilesAndOverrides(t *testing.T) {
require.NoError(t, err)
require.Len(t, out, 3)
require.Equal(t, "default.rego", out[0].Files[0].Filename)
require.True(t, out[0].Files[0].Optional)
require.Equal(t, "a.rego", out[1].Files[0].Filename)
require.False(t, out[1].Files[0].Optional)
require.True(t, out[1].Strict)
require.NotNil(t, out[1].LogLevel)
require.Equal(t, logrus.WarnLevel, *out[1].LogLevel)
require.Equal(t, "b.rego", out[2].Files[0].Filename)
require.False(t, out[2].Files[0].Optional)
require.True(t, out[2].Strict)
require.NotNil(t, out[1].FS)
require.NotNil(t, out[2].FS)
}
+305 -1
View File
@@ -1,6 +1,8 @@
package tests
import (
"archive/tar"
"bytes"
"encoding/json"
"errors"
"fmt"
@@ -33,6 +35,8 @@ var policyBuildTests = []func(t *testing.T, sb integration.Sandbox){
testBuildPolicyEnv,
testBuildPolicyHTTP,
testBuildPolicyGit,
testBuildPolicyRemotePolicyFiles,
testBuildPolicyRemoteHTTPPolicyFiles,
testBuildPolicyConfigFlags,
}
@@ -1089,7 +1093,7 @@ decision := {"allow": allow}
t,
fstest.CreateFile("policy.rego", []byte(tc.policy), 0600),
)
policyPath := filepath.Join(dir, "policy.rego")
policyPath := "cwd://policy.rego"
cmd := buildxCmd(sb, withDir(dir), withArgs(
"build",
@@ -1257,3 +1261,303 @@ decision := {"allow": allow}
require.Contains(t, string(out), "disabled policy cannot be combined with other policy flags")
})
}
func testBuildPolicyRemotePolicyFiles(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 remote-policy\n"), 0600))
require.NoError(t, os.MkdirAll(filepath.Join(gitDir, "policy"), 0700))
require.NoError(t, os.MkdirAll(filepath.Join(gitDir, "hack"), 0700))
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "policy", "import.rego"), []byte(`
package docker
import data.hack.allow as allowlib
default allow = false
allow if allowlib.required
decision := {"allow": allow}
`), 0600))
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "hack", "allow.rego"), []byte(`
package hack.allow
required := true
`), 0600))
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "policy", "json.rego"), []byte(`
package docker
default allow = false
cfg := load_json("policy/config.json")
allow if cfg.allow == true
decision := {"allow": allow}
`), 0600))
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "policy", "config.json"), []byte(`{"allow": true}`), 0600))
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "policy", "deny.rego"), []byte(`
package docker
default allow = false
decision := {"allow": allow}
`), 0600))
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "remote-only.rego"), []byte(`
package docker
default allow = false
decision := {"allow": allow}
`), 0600))
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "Dockerfile.rego"), []byte(`
package docker
default allow = true
decision := {"allow": allow}
`), 0600))
git, err := gitutil.New(gitutil.WithWorkingDir(gitDir))
require.NoError(t, err)
gittestutil.GitInit(git, t)
gittestutil.GitAdd(git, t, ".")
gittestutil.GitCommit(git, t, "initial commit")
baseURL := gittestutil.GitServeHTTP(git, t)
workDir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(workDir, "local-allow.rego"), []byte(`
package docker
default allow = true
decision := {"allow": allow}
`), 0600))
require.NoError(t, os.WriteFile(filepath.Join(workDir, "local-only.rego"), []byte(`
package docker
default allow = true
decision := {"allow": allow}
`), 0600))
t.Run("remote-policy-import", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--policy", "filename=policy/import.rego",
"--output=type=cacheonly",
baseURL,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.Contains(t, string(out), "loading policies")
require.Contains(t, string(out), "policy/import.rego")
})
t.Run("remote-policy-load-json", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--policy", "filename=policy/json.rego",
"--output=type=cacheonly",
baseURL,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.Contains(t, string(out), "loading policies")
require.Contains(t, string(out), "policy/json.rego")
})
t.Run("remote-policy-cwd-override", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--policy", "filename=cwd://local-allow.rego",
"--output=type=cacheonly",
baseURL,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.Contains(t, string(out), "loading policies")
require.Contains(t, string(out), "cwd://local-allow.rego")
})
t.Run("remote-policy-no-local-fallback", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--policy", "filename=does-not-exist.rego",
"--output=type=cacheonly",
baseURL,
))
out, err := cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Contains(t, string(out), "policy file does-not-exist.rego not found")
})
t.Run("remote-policy-prefers-remote-over-cwd", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--policy", "filename=remote-only.rego",
"--output=type=cacheonly",
baseURL,
))
out, err := cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Contains(t, string(out), "not allowed by policy")
require.Contains(t, string(out), "loading policies")
require.Contains(t, string(out), "remote-only.rego")
})
t.Run("remote-policy-cwd-prefix-uses-local", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--policy", "filename=cwd://local-only.rego",
"--output=type=cacheonly",
baseURL,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.Contains(t, string(out), "loading policies")
require.Contains(t, string(out), "cwd://local-only.rego")
})
t.Run("remote-policy-default-from-remote-git", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--output=type=cacheonly",
baseURL,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.Contains(t, string(out), "loading policies Dockerfile.rego")
})
}
func testBuildPolicyRemoteHTTPPolicyFiles(t *testing.T, sb integration.Sandbox) {
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
makeTar := func(t *testing.T, files map[string]string) []byte {
t.Helper()
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
for name, data := range files {
dt := []byte(data)
require.NoError(t, tw.WriteHeader(&tar.Header{
Name: name,
Mode: 0600,
Size: int64(len(dt)),
}))
_, err := tw.Write(dt)
require.NoError(t, err)
}
require.NoError(t, tw.Close())
return buf.Bytes()
}
archiveURLPath := "/context.tar"
singleURLPath := "/Dockerfile"
server := httpserver.NewTestServer(map[string]*httpserver.Response{
archiveURLPath: {
Content: makeTar(t, map[string]string{
"Dockerfile": `FROM busybox:latest
RUN echo http-archive-policy
`,
"policy/allow.rego": `
package docker
default allow = true
decision := {"allow": allow}
`,
}),
},
singleURLPath: {
Content: []byte("FROM busybox:latest\nRUN echo http-single-file\n"),
},
})
defer server.Close()
workDir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(workDir, "Dockerfile.rego"), []byte(`
package docker
default allow = false
decision := {"allow": allow}
`), 0600))
t.Run("http-archive-policy-file", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--policy", "filename=policy/allow.rego",
"--output=type=cacheonly",
server.URL+archiveURLPath,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.Contains(t, string(out), "loading policies")
require.Contains(t, string(out), "policy/allow.rego")
})
t.Run("http-single-file-required-policy-not-found", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--policy", "filename=policy/allow.rego",
"--output=type=cacheonly",
server.URL+singleURLPath,
))
out, err := cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Contains(t, string(out), "policy file policy/allow.rego not found")
})
t.Run("http-single-file-no-local-default-fallback", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--output=type=cacheonly",
server.URL+singleURLPath,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
})
t.Run("http-archive-default-from-remote-context", func(t *testing.T) {
serverWithDefault := httpserver.NewTestServer(map[string]*httpserver.Response{
archiveURLPath: {
Content: makeTar(t, map[string]string{
"Dockerfile": `FROM busybox:latest
RUN echo http-archive-default
`,
"Dockerfile.rego": `
package docker
default allow = true
decision := {"allow": allow}
`,
}),
},
})
defer serverWithDefault.Close()
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--output=type=cacheonly",
serverWithDefault.URL+archiveURLPath,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.Contains(t, string(out), "loading policies Dockerfile.rego")
})
}
+1 -6
View File
@@ -1,7 +1,6 @@
package buildflags
import (
"os"
"strconv"
"strings"
@@ -56,11 +55,7 @@ func parsePolicyFields(fields []string) (PolicyConfig, error) {
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})
cfg.Files = append(cfg.Files, policy.File{Filename: value})
case "reset":
b, err := strconv.ParseBool(value)
if err != nil {
+5 -7
View File
@@ -14,8 +14,7 @@ import (
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))
require.NoError(t, os.WriteFile(policyPath, []byte("package docker\n"), 0o600))
in := cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
@@ -33,7 +32,7 @@ func TestPolicyConfigs_FromCtyValue(t *testing.T) {
require.Len(t, actual, 2)
require.Equal(t, policyPath, actual[0].Files[0].Filename)
require.Equal(t, policyData, actual[0].Files[0].Data)
require.Nil(t, actual[0].Files[0].Data)
require.True(t, actual[0].Reset)
require.NotNil(t, actual[0].Strict)
require.True(t, *actual[0].Strict)
@@ -41,7 +40,7 @@ func TestPolicyConfigs_FromCtyValue(t *testing.T) {
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.Nil(t, actual[1].Files[0].Data)
require.True(t, actual[1].Disabled)
}
@@ -82,13 +81,12 @@ func TestPolicyConfigs_ToCtyValue(t *testing.T) {
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))
require.NoError(t, os.WriteFile(policyPath, []byte("package docker\n"), 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.Nil(t, actual.Files[0].Data)
require.True(t, actual.Disabled)
}