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:
+20
-7
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user