commands: implement policy test command
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
@@ -20,7 +20,7 @@ func RootCmd(rootcmd *cobra.Command, dockerCli command.Cli, rootOpts RootOptions
|
||||
cmd.AddCommand(
|
||||
jsonSchemaCmd(),
|
||||
evalCmd(dockerCli, rootOpts),
|
||||
testCmd(),
|
||||
testCmd(dockerCli, rootOpts),
|
||||
)
|
||||
|
||||
return cmd
|
||||
|
||||
+193
-5
@@ -1,24 +1,212 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/buildx/builder"
|
||||
"github.com/docker/buildx/policy"
|
||||
"github.com/docker/buildx/util/cobrautil"
|
||||
"github.com/docker/buildx/util/confutil"
|
||||
"github.com/docker/cli/cli/command"
|
||||
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
|
||||
"github.com/moby/buildkit/solver/pb"
|
||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func testCmd() *cobra.Command {
|
||||
func testCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
|
||||
var opts policy.TestOptions
|
||||
cmd := &cobra.Command{
|
||||
Use: "test <path>",
|
||||
Short: "Run policy tests",
|
||||
Args: cobra.ExactArgs(1),
|
||||
DisableFlagsInUseLine: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runTest(args[0])
|
||||
resolver := newPolicyTestResolver(dockerCli, rootOpts.Builder)
|
||||
opts.Resolver = resolver.Options()
|
||||
defer resolver.Close()
|
||||
return runTest(cmd.Context(), cmd.OutOrStdout(), args[0], opts)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&opts.Run, "run", "", "Run only tests with name containing this substring")
|
||||
cmd.Flags().StringVar(&opts.Filename, "filename", "Dockerfile", "Name of the Dockerfile to validate")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runTest(path string) error {
|
||||
_ = path
|
||||
return errors.New("not implemented")
|
||||
func runTest(ctx context.Context, out io.Writer, path string, opts policy.TestOptions) error {
|
||||
root := os.DirFS(".")
|
||||
statFS, ok := root.(fs.StatFS)
|
||||
if !ok {
|
||||
return errors.New("policy test root does not support stat")
|
||||
}
|
||||
opts.Root = statFS
|
||||
|
||||
summary, err := policy.RunPolicyTests(ctx, path, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, result := range summary.Results {
|
||||
status := "PASS"
|
||||
if !result.Passed {
|
||||
status = "FAIL"
|
||||
}
|
||||
allowStr := "n/a"
|
||||
if result.Allow != nil {
|
||||
allowStr = fmt.Sprintf("%v", *result.Allow)
|
||||
}
|
||||
if len(result.DenyMessages) > 0 {
|
||||
_, _ = fmt.Fprintf(out, "%s: %s (allow=%s, deny_msg=%s)\n", result.Name, status, allowStr, strings.Join(result.DenyMessages, "; "))
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(out, "%s: %s (allow=%s)\n", result.Name, status, allowStr)
|
||||
}
|
||||
|
||||
if result.Passed {
|
||||
continue
|
||||
}
|
||||
|
||||
if result.Input != nil {
|
||||
writeJSON(out, "input", result.Input)
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(out, "input: <nil>")
|
||||
}
|
||||
if result.Decision != nil {
|
||||
writeJSON(out, "decision", result.Decision)
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(out, "decision: <nil>")
|
||||
}
|
||||
if len(result.MissingInput) > 0 {
|
||||
_, _ = fmt.Fprintf(out, "missing_input: %s\n", strings.Join(result.MissingInput, ", "))
|
||||
}
|
||||
if len(result.MetadataNeeded) > 0 {
|
||||
_, _ = fmt.Fprintf(out, "metadata_resolve: %s\n", strings.Join(result.MetadataNeeded, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
if summary.Failed > 0 {
|
||||
return cobrautil.ExitCodeError(1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeJSON(out io.Writer, label string, v any) {
|
||||
dt, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(out, "%s: <error encoding>\n", label)
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprintf(out, "%s:\n%s\n", label, string(dt))
|
||||
}
|
||||
|
||||
type policyTestResolver struct {
|
||||
dockerCli command.Cli
|
||||
builderName *string
|
||||
|
||||
once sync.Once
|
||||
platform *ocispecs.Platform
|
||||
openClient gatewayClientOpener
|
||||
release func() error
|
||||
err error
|
||||
}
|
||||
|
||||
func newPolicyTestResolver(dockerCli command.Cli, builderName *string) *policyTestResolver {
|
||||
return &policyTestResolver{
|
||||
dockerCli: dockerCli,
|
||||
builderName: builderName,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *policyTestResolver) Options() *policy.TestResolver {
|
||||
return &policy.TestResolver{
|
||||
Resolve: r.Resolve,
|
||||
Platform: r.Platform,
|
||||
VerifierProvider: policy.SignatureVerifier(confutil.NewConfig(r.dockerCli)),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *policyTestResolver) Close() error {
|
||||
if r.release == nil {
|
||||
return nil
|
||||
}
|
||||
return r.release()
|
||||
}
|
||||
|
||||
func (r *policyTestResolver) Platform(ctx context.Context) (*ocispecs.Platform, error) {
|
||||
if err := r.init(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.platform, nil
|
||||
}
|
||||
|
||||
func (r *policyTestResolver) Resolve(ctx context.Context, source *pb.SourceOp, req *gwpb.ResolveSourceMetaRequest) (*gwpb.ResolveSourceMetaResponse, error) {
|
||||
if err := r.init(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gwClient, err := r.openClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opt := sourceResolverOpt(req, r.platform)
|
||||
resp, err := gwClient.ResolveSourceMetadata(ctx, source, opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buildSourceMetaResponse(resp, req), nil
|
||||
}
|
||||
|
||||
func (r *policyTestResolver) init(ctx context.Context) error {
|
||||
r.once.Do(func() {
|
||||
bopts := []builder.Option{}
|
||||
if r.builderName != nil {
|
||||
bopts = append(bopts, builder.WithName(*r.builderName))
|
||||
}
|
||||
b, err := builder.New(r.dockerCli, bopts...)
|
||||
if err != nil {
|
||||
r.err = err
|
||||
return
|
||||
}
|
||||
|
||||
nodes, err := b.LoadNodes(ctx)
|
||||
if err != nil {
|
||||
r.err = err
|
||||
return
|
||||
}
|
||||
c, err := nodes[0].Driver.Client(ctx)
|
||||
if err != nil {
|
||||
r.err = err
|
||||
return
|
||||
}
|
||||
workers, err := c.ListWorkers(ctx)
|
||||
if err != nil {
|
||||
r.err = err
|
||||
return
|
||||
}
|
||||
if len(workers) == 0 {
|
||||
r.err = errors.New("no workers available in the builder")
|
||||
return
|
||||
}
|
||||
|
||||
defaultPlatform := workers[0].Platforms[0]
|
||||
r.platform = &ocispecs.Platform{
|
||||
Architecture: defaultPlatform.Architecture,
|
||||
OS: defaultPlatform.OS,
|
||||
Variant: defaultPlatform.Variant,
|
||||
}
|
||||
openClient, release, err := gatewayClientFactory(c)
|
||||
if err != nil {
|
||||
r.err = err
|
||||
return
|
||||
}
|
||||
r.openClient = openClient
|
||||
r.release = release
|
||||
})
|
||||
return r.err
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@ Run policy tests
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:----------------|:---------|:--------|:-----------------------------------------|
|
||||
|:----------------|:---------|:-------------|:---------------------------------------------------|
|
||||
| `--builder` | `string` | | Override the configured builder instance |
|
||||
| `-D`, `--debug` | `bool` | | Enable debug logging |
|
||||
| `--filename` | `string` | `Dockerfile` | Name of the Dockerfile to validate |
|
||||
| `--run` | `string` | | Run only tests with name containing this substring |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
|
||||
@@ -0,0 +1,835 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"maps"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/platforms"
|
||||
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
|
||||
"github.com/moby/buildkit/solver/pb"
|
||||
"github.com/moby/buildkit/sourcepolicy/policysession"
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
"github.com/open-policy-agent/opa/v1/rego"
|
||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type TestOptions struct {
|
||||
Run string
|
||||
Filename string
|
||||
Root fs.StatFS
|
||||
Resolver *TestResolver
|
||||
}
|
||||
|
||||
type TestSummary struct {
|
||||
Results []TestResult
|
||||
Failed int
|
||||
}
|
||||
|
||||
type TestResult struct {
|
||||
Name string
|
||||
Package string
|
||||
Passed bool
|
||||
Allow *bool
|
||||
DenyMessages []string
|
||||
Input *Input
|
||||
Decision *Decision
|
||||
MissingInput []string
|
||||
MetadataNeeded []string
|
||||
}
|
||||
|
||||
type testDef struct {
|
||||
Name string
|
||||
PkgPath string
|
||||
}
|
||||
|
||||
type TestResolver struct {
|
||||
Resolve func(context.Context, *pb.SourceOp, *gwpb.ResolveSourceMetaRequest) (*gwpb.ResolveSourceMetaResponse, error)
|
||||
Platform func(context.Context) (*ocispecs.Platform, error)
|
||||
VerifierProvider PolicyVerifierProvider
|
||||
}
|
||||
|
||||
func RunPolicyTests(ctx context.Context, path string, opts TestOptions) (TestSummary, error) {
|
||||
var summary TestSummary
|
||||
if opts.Root == nil {
|
||||
return summary, errors.New("policy root filesystem is required")
|
||||
}
|
||||
policyModules, policyFiles, err := loadPolicyModules(opts.Root, opts.Filename)
|
||||
if err != nil {
|
||||
return summary, err
|
||||
}
|
||||
|
||||
testModules, _, err := LoadTestModules(opts.Root, path)
|
||||
if err != nil {
|
||||
return summary, err
|
||||
}
|
||||
|
||||
modules := make(map[string]*ast.Module, len(policyModules)+len(testModules))
|
||||
maps.Copy(modules, policyModules)
|
||||
maps.Copy(modules, testModules)
|
||||
|
||||
fsProvider := func() (fs.StatFS, func() error, error) {
|
||||
return opts.Root, nil, nil
|
||||
}
|
||||
|
||||
p := NewPolicy(Opt{
|
||||
FS: fsProvider,
|
||||
})
|
||||
|
||||
comp, closeLoader, err := compilePolicyModules(modules, p, fsProvider)
|
||||
if err != nil {
|
||||
if closeLoader != nil {
|
||||
_ = closeLoader()
|
||||
}
|
||||
return summary, err
|
||||
}
|
||||
if closeLoader != nil {
|
||||
defer closeLoader()
|
||||
}
|
||||
|
||||
tests := findPolicyTests(testModules)
|
||||
if opts.Run != "" {
|
||||
tests = filterPolicyTests(tests, opts.Run)
|
||||
}
|
||||
if len(tests) == 0 {
|
||||
return summary, errors.New("no tests found")
|
||||
}
|
||||
|
||||
for _, t := range tests {
|
||||
result, err := runPolicyTest(ctx, policyModules, testModules, policyFiles, comp, p, t, opts, fsProvider)
|
||||
if err != nil {
|
||||
return summary, err
|
||||
}
|
||||
if !result.Passed {
|
||||
summary.Failed++
|
||||
}
|
||||
summary.Results = append(summary.Results, result)
|
||||
}
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
func LoadTestModules(root fs.StatFS, path string) (map[string]*ast.Module, []File, error) {
|
||||
path = filepath.ToSlash(path)
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
if path == "" {
|
||||
path = "."
|
||||
}
|
||||
info, err := root.Stat(path)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "stat %s", path)
|
||||
}
|
||||
|
||||
var files []string
|
||||
if info.IsDir() {
|
||||
entries, err := fs.ReadDir(root, path)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "read dir %s", path)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
if !strings.HasSuffix(name, "_test.rego") {
|
||||
continue
|
||||
}
|
||||
files = append(files, filepath.ToSlash(filepath.Join(path, name)))
|
||||
}
|
||||
} else {
|
||||
if !strings.HasSuffix(path, "_test.rego") {
|
||||
return nil, nil, errors.Errorf("test file must have _test.rego suffix: %s", path)
|
||||
}
|
||||
files = append(files, filepath.ToSlash(path))
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return nil, nil, errors.New("no policy tests found")
|
||||
}
|
||||
|
||||
sort.Strings(files)
|
||||
modules := make(map[string]*ast.Module, len(files))
|
||||
entries := make([]File, 0, len(files))
|
||||
for _, file := range files {
|
||||
dt, err := fs.ReadFile(root, file)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "read policy test module %s", file)
|
||||
}
|
||||
mod, err := ast.ParseModuleWithOpts(file, string(dt), ast.ParserOptions{
|
||||
RegoVersion: ast.RegoV1,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "parse policy test module %s", file)
|
||||
}
|
||||
modules[file] = mod
|
||||
entries = append(entries, File{
|
||||
Filename: file,
|
||||
Data: dt,
|
||||
})
|
||||
}
|
||||
|
||||
return modules, entries, nil
|
||||
}
|
||||
|
||||
func loadPolicyModules(root fs.StatFS, filename string) (map[string]*ast.Module, []File, error) {
|
||||
if filename == "" {
|
||||
return nil, nil, errors.New("policy filename is required")
|
||||
}
|
||||
policyFile := filename + ".rego"
|
||||
dt, err := fs.ReadFile(root, policyFile)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "read policy module %s", policyFile)
|
||||
}
|
||||
mod, err := ast.ParseModuleWithOpts(policyFile, string(dt), ast.ParserOptions{
|
||||
RegoVersion: ast.RegoV1,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "parse policy module %s", policyFile)
|
||||
}
|
||||
modules := map[string]*ast.Module{
|
||||
filepath.ToSlash(policyFile): mod,
|
||||
}
|
||||
files := []File{
|
||||
{
|
||||
Filename: filepath.ToSlash(policyFile),
|
||||
Data: dt,
|
||||
},
|
||||
}
|
||||
return modules, files, nil
|
||||
}
|
||||
|
||||
func compilePolicyModules(modules map[string]*ast.Module, p *Policy, fsProvider func() (fs.StatFS, func() error, error)) (*ast.Compiler, func() error, error) {
|
||||
caps := &ast.Capabilities{
|
||||
Builtins: builtins(),
|
||||
Features: slices.Clone(ast.Features),
|
||||
}
|
||||
comp := ast.NewCompiler().WithCapabilities(caps).WithKeepModules(true)
|
||||
|
||||
builtinDefs := make(map[string]*ast.Builtin)
|
||||
for _, f := range p.funcs {
|
||||
builtinDefs[f.decl.Name] = &ast.Builtin{
|
||||
Name: f.decl.Name,
|
||||
Decl: f.decl.Decl,
|
||||
}
|
||||
}
|
||||
comp = comp.WithBuiltins(builtinDefs)
|
||||
|
||||
loader, closeLoader := newPolicyModuleLoader(fsProvider)
|
||||
comp = comp.WithModuleLoader(loader)
|
||||
|
||||
comp.Compile(modules)
|
||||
if comp.Failed() {
|
||||
return nil, closeLoader, errors.Errorf("compile: %v", comp.Errors)
|
||||
}
|
||||
return comp, closeLoader, nil
|
||||
}
|
||||
|
||||
func findPolicyTests(modules map[string]*ast.Module) []testDef {
|
||||
seen := map[string]testDef{}
|
||||
for _, mod := range modules {
|
||||
pkgPath := mod.Package.Path.String()
|
||||
for _, rule := range mod.Rules {
|
||||
if len(rule.Head.Args) > 0 {
|
||||
continue
|
||||
}
|
||||
if rule.Head.Value != nil {
|
||||
if _, ok := rule.Head.Value.Value.(ast.Boolean); !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
name := string(rule.Head.Name)
|
||||
if strings.HasPrefix(name, "test_") {
|
||||
seen[name] = testDef{Name: name, PkgPath: pkgPath}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out := make([]testDef, 0, len(seen))
|
||||
for _, t := range seen {
|
||||
out = append(out, t)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out
|
||||
}
|
||||
|
||||
func filterPolicyTests(tests []testDef, substr string) []testDef {
|
||||
out := make([]testDef, 0, len(tests))
|
||||
for _, t := range tests {
|
||||
if strings.Contains(t.Name, substr) {
|
||||
out = append(out, t)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func runPolicyTest(ctx context.Context, policyModules map[string]*ast.Module, testModules map[string]*ast.Module, policyFiles []File, compiler *ast.Compiler, p *Policy, t testDef, opts TestOptions, fsProvider func() (fs.StatFS, func() error, error)) (TestResult, error) {
|
||||
result := TestResult{
|
||||
Name: t.Name,
|
||||
Package: t.PkgPath,
|
||||
}
|
||||
policyPackageModules := modulesForPackage(policyModules, t.PkgPath)
|
||||
input, err := lookupTestInput(testModules, t)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
effectiveInput := input
|
||||
if opts.Resolver != nil {
|
||||
resolvedInput, ok, err := resolveTestInput(ctx, policyFiles, opts.Resolver, policyPackageModules, input, fsProvider)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if ok {
|
||||
effectiveInput = resolvedInput
|
||||
}
|
||||
}
|
||||
result.Input = effectiveInput
|
||||
|
||||
testState := stateFromInput(effectiveInput)
|
||||
query := fmt.Sprintf("%s.%s", t.PkgPath, t.Name)
|
||||
ok, err := evalBool(ctx, compiler, p, testState, query, effectiveInput)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.Passed = ok
|
||||
|
||||
decisionState := stateFromInput(effectiveInput)
|
||||
decision, allow, deny := evalDecision(ctx, compiler, p, decisionState, t.PkgPath, effectiveInput)
|
||||
result.Decision = decision
|
||||
result.Allow = allow
|
||||
result.DenyMessages = deny
|
||||
|
||||
missing := missingInputRefs(policyPackageModules, effectiveInput)
|
||||
result.MissingInput = uniqueSortedStrings(missing)
|
||||
result.MetadataNeeded = summarizeMetadataRequests(result.MissingInput)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func lookupTestInput(testModules map[string]*ast.Module, t testDef) (*Input, error) {
|
||||
if len(testModules) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var inputTerm *ast.Term
|
||||
for _, mod := range modulesForPackage(testModules, t.PkgPath) {
|
||||
for _, rule := range mod.Rules {
|
||||
if string(rule.Head.Name) != t.Name {
|
||||
continue
|
||||
}
|
||||
term, err := inputTermFromRule(rule, t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if term == nil {
|
||||
continue
|
||||
}
|
||||
if inputTerm != nil && !inputTerm.Equal(term) {
|
||||
return nil, errors.Errorf("multiple input overrides for %s", t.Name)
|
||||
}
|
||||
inputTerm = term
|
||||
}
|
||||
}
|
||||
if inputTerm == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var inp Input
|
||||
if err := ast.As(inputTerm.Value, &inp); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to decode test input for %s", t.Name)
|
||||
}
|
||||
return &inp, nil
|
||||
}
|
||||
|
||||
func inputTermFromRule(rule *ast.Rule, t testDef) (*ast.Term, error) {
|
||||
var inputTerm *ast.Term
|
||||
for _, expr := range rule.Body {
|
||||
for _, w := range expr.With {
|
||||
if w == nil || w.Target == nil || w.Value == nil {
|
||||
continue
|
||||
}
|
||||
ref, ok := w.Target.Value.(ast.Ref)
|
||||
if !ok || !ref.Equal(ast.InputRootRef) {
|
||||
continue
|
||||
}
|
||||
if inputTerm != nil && !inputTerm.Equal(w.Value) {
|
||||
return nil, errors.Errorf("multiple input overrides for %s", t.Name)
|
||||
}
|
||||
inputTerm = w.Value
|
||||
}
|
||||
}
|
||||
return inputTerm, nil
|
||||
}
|
||||
|
||||
func evalDecision(ctx context.Context, compiler *ast.Compiler, p *Policy, st *state, pkgPath string, input *Input) (*Decision, *bool, []string) {
|
||||
query := fmt.Sprintf("%s.decision", pkgPath)
|
||||
val, err := evalValue(ctx, compiler, p, st, query, input)
|
||||
if err != nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
decision := decodeDecision(val)
|
||||
if decision == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
deny := decision.DenyMessages
|
||||
if len(deny) == 0 {
|
||||
deny = nil
|
||||
}
|
||||
return decision, decision.Allow, deny
|
||||
}
|
||||
|
||||
func stateFromInput(input *Input) *state {
|
||||
st := &state{}
|
||||
if input == nil {
|
||||
return st
|
||||
}
|
||||
st.Input = *input
|
||||
return st
|
||||
}
|
||||
|
||||
func resolveTestInput(ctx context.Context, files []File, resolver *TestResolver, policyModules []*ast.Module, input *Input, fsProvider func() (fs.StatFS, func() error, error)) (*Input, bool, error) {
|
||||
if resolver == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
source, err := sourceFromInput(input)
|
||||
if err != nil || source == nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
platform, err := inputPlatform(input)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if platform == nil && strings.HasPrefix(source.Identifier, "docker-image://") {
|
||||
if resolver.Platform == nil {
|
||||
return nil, false, errors.New("resolver platform not configured")
|
||||
}
|
||||
platform, err = resolver.Platform(ctx)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
var env Env
|
||||
if input != nil && hasEnv(input.Env) {
|
||||
env = input.Env
|
||||
}
|
||||
|
||||
policyEval := NewPolicy(Opt{
|
||||
Files: files,
|
||||
Env: env,
|
||||
FS: fsProvider,
|
||||
VerifierProvider: resolver.VerifierProvider,
|
||||
})
|
||||
|
||||
srcReq := &gwpb.ResolveSourceMetaResponse{
|
||||
Source: source,
|
||||
}
|
||||
|
||||
var platformPB *pb.Platform
|
||||
if platform != nil {
|
||||
platformPB = &pb.Platform{
|
||||
Architecture: platform.Architecture,
|
||||
OS: platform.OS,
|
||||
Variant: platform.Variant,
|
||||
}
|
||||
}
|
||||
|
||||
for range 5 {
|
||||
_, next, err := policyEval.CheckPolicy(ctx, &policysession.CheckPolicyRequest{
|
||||
Platform: platformPB,
|
||||
Source: srcReq,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if next == nil {
|
||||
inp, _, err := SourceToInputWithLogger(ctx, resolver.VerifierProvider, srcReq, platform, nil)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if hasEnv(env) {
|
||||
inp.Env = env
|
||||
}
|
||||
if resolver.Resolve != nil && len(policyModules) > 0 {
|
||||
missing := missingInputRefs(policyModules, &inp)
|
||||
resolveMissing := filterResolvableMissing(missing)
|
||||
if len(resolveMissing) > 0 {
|
||||
req := &gwpb.ResolveSourceMetaRequest{}
|
||||
if err := AddUnknowns(req, resolveMissing); err == nil && (req.Image != nil || req.Git != nil) {
|
||||
resp, err := resolver.Resolve(ctx, source, req)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
srcReq = resp
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return mergeInputOverrides(inp, input), true, nil
|
||||
}
|
||||
if resolver.Resolve == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
resp, err := resolver.Resolve(ctx, source, next)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
srcReq = resp
|
||||
}
|
||||
return nil, false, errors.New("maximum attempts reached for resolving policy metadata")
|
||||
}
|
||||
|
||||
func sourceFromInput(input *Input) (*pb.SourceOp, error) {
|
||||
if input != nil && input.Image != nil {
|
||||
ref := ""
|
||||
switch {
|
||||
case input.Image.Ref != "":
|
||||
ref = input.Image.Ref
|
||||
case input.Image.FullRepo != "":
|
||||
ref = input.Image.FullRepo
|
||||
case input.Image.Repo != "":
|
||||
ref = input.Image.Repo
|
||||
}
|
||||
if ref != "" && input.Image.Tag != "" && !strings.Contains(ref, ":") {
|
||||
ref = ref + ":" + input.Image.Tag
|
||||
}
|
||||
if ref != "" {
|
||||
return &pb.SourceOp{Identifier: "docker-image://" + ref}, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func mergeInputOverrides(resolved Input, input *Input) *Input {
|
||||
if input == nil {
|
||||
return &resolved
|
||||
}
|
||||
if hasEnv(input.Env) {
|
||||
resolved.Env = input.Env
|
||||
}
|
||||
return &resolved
|
||||
}
|
||||
|
||||
func hasEnv(env Env) bool {
|
||||
if env.Filename != "" || env.Target != "" {
|
||||
return true
|
||||
}
|
||||
if len(env.Args) > 0 || len(env.Labels) > 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func filterResolvableMissing(missing []string) []string {
|
||||
out := make([]string, 0, len(missing))
|
||||
for _, m := range missing {
|
||||
if strings.HasPrefix(m, "input.image.") || strings.HasPrefix(m, "input.git.") {
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func evalBool(ctx context.Context, compiler *ast.Compiler, p *Policy, st *state, query string, input *Input) (bool, error) {
|
||||
r := newPolicyRego(compiler, p, st, query, input)
|
||||
rs, err := r.Eval(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(rs) == 0 || len(rs[0].Expressions) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
val, ok := rs[0].Expressions[0].Value.(bool)
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func evalValue(ctx context.Context, compiler *ast.Compiler, p *Policy, st *state, query string, input *Input) (any, error) {
|
||||
r := newPolicyRego(compiler, p, st, query, input)
|
||||
rs, err := r.Eval(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rs) == 0 || len(rs[0].Expressions) == 0 {
|
||||
return nil, errors.New("empty result")
|
||||
}
|
||||
return rs[0].Expressions[0].Value, nil
|
||||
}
|
||||
|
||||
func newPolicyRego(compiler *ast.Compiler, p *Policy, st *state, query string, input *Input) *rego.Rego {
|
||||
opts := []func(*rego.Rego){
|
||||
rego.SetRegoVersion(ast.RegoV1),
|
||||
rego.Query(query),
|
||||
rego.SkipPartialNamespace(true),
|
||||
rego.Compiler(compiler),
|
||||
}
|
||||
if input != nil {
|
||||
opts = append(opts, rego.Input(input))
|
||||
}
|
||||
for _, f := range p.funcs {
|
||||
opts = append(opts, f.impl(st))
|
||||
}
|
||||
return rego.New(opts...)
|
||||
}
|
||||
|
||||
func modulesForPackage(modules map[string]*ast.Module, pkgPath string) []*ast.Module {
|
||||
out := make([]*ast.Module, 0, len(modules))
|
||||
for _, mod := range modules {
|
||||
if mod.Package.Path.String() == pkgPath {
|
||||
out = append(out, mod)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func missingInputRefs(mods []*ast.Module, input *Input) []string {
|
||||
if len(mods) == 0 {
|
||||
return nil
|
||||
}
|
||||
inputMap := normalizeInput(input)
|
||||
refs := collectUnknowns(mods)
|
||||
missing := make([]string, 0, len(refs))
|
||||
for _, ref := range refs {
|
||||
key := strings.TrimPrefix(ref, "input.")
|
||||
if key == ref {
|
||||
continue
|
||||
}
|
||||
key = trimKey(key)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if !inputHasPath(inputMap, strings.Split(key, ".")) {
|
||||
missing = append(missing, "input."+key)
|
||||
}
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
type inputMap map[string]json.RawMessage
|
||||
|
||||
func inputHasPath(input inputMap, path []string) bool {
|
||||
if input == nil {
|
||||
return false
|
||||
}
|
||||
cur := input
|
||||
for i, p := range path {
|
||||
next, ok := cur[p]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if i == len(path)-1 {
|
||||
return true
|
||||
}
|
||||
var decoded inputMap
|
||||
if err := json.Unmarshal(next, &decoded); err != nil {
|
||||
return false
|
||||
}
|
||||
cur = decoded
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func normalizeInput(input *Input) inputMap {
|
||||
if input == nil {
|
||||
return nil
|
||||
}
|
||||
out, err := decodeJSONValue[inputMap](input)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func decodeDecision(decision any) *Decision {
|
||||
obj, err := decodeJSONValue[map[string]any](decision)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var allow *bool
|
||||
if v, ok := obj["allow"]; ok {
|
||||
if b, ok := v.(bool); ok {
|
||||
allow = &b
|
||||
}
|
||||
}
|
||||
denyMsgs := []string{}
|
||||
if v, ok := obj["deny_msg"]; ok {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
denyMsgs = append(denyMsgs, val)
|
||||
case []any:
|
||||
for _, entry := range val {
|
||||
if s, ok := entry.(string); ok {
|
||||
denyMsgs = append(denyMsgs, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(denyMsgs) == 0 {
|
||||
denyMsgs = nil
|
||||
}
|
||||
return &Decision{
|
||||
Allow: allow,
|
||||
DenyMessages: denyMsgs,
|
||||
}
|
||||
}
|
||||
|
||||
func decodeJSONValue[T any](v any) (T, error) {
|
||||
var out T
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if err := json.Unmarshal(b, &out); err != nil {
|
||||
return out, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func summarizeMetadataRequests(missing []string) []string {
|
||||
if len(missing) == 0 {
|
||||
return nil
|
||||
}
|
||||
req := &gwpb.ResolveSourceMetaRequest{}
|
||||
trimmed := make([]string, 0, len(missing))
|
||||
for _, m := range missing {
|
||||
trimmed = append(trimmed, strings.TrimPrefix(m, "input."))
|
||||
}
|
||||
if err := AddUnknowns(req, trimmed); err != nil {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
if req.Image != nil {
|
||||
out = append(out, "image")
|
||||
}
|
||||
if req.Git != nil {
|
||||
out = append(out, "git")
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func inputPlatform(input *Input) (*ocispecs.Platform, error) {
|
||||
if input == nil || input.Image == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if input.Image.Platform != "" {
|
||||
p, err := platforms.Parse(input.Image.Platform)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "invalid platform %s", input.Image.Platform)
|
||||
}
|
||||
p = platforms.Normalize(p)
|
||||
return &ocispecs.Platform{
|
||||
OS: p.OS,
|
||||
Architecture: p.Architecture,
|
||||
Variant: p.Variant,
|
||||
}, nil
|
||||
}
|
||||
if input.Image.OS != "" || input.Image.Architecture != "" || input.Image.Variant != "" {
|
||||
return &ocispecs.Platform{
|
||||
OS: input.Image.OS,
|
||||
Architecture: input.Image.Architecture,
|
||||
Variant: input.Image.Variant,
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func uniqueSortedStrings(in []string) []string {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := map[string]struct{}{}
|
||||
for _, s := range in {
|
||||
seen[s] = struct{}{}
|
||||
}
|
||||
out := make([]string, 0, len(seen))
|
||||
for s := range seen {
|
||||
out = append(out, s)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func newPolicyModuleLoader(fsProvider func() (fs.StatFS, func() error, error)) (func(map[string]*ast.Module) (map[string]*ast.Module, error), func() error) {
|
||||
var (
|
||||
root fs.StatFS
|
||||
closeFS func() error
|
||||
)
|
||||
loader := func(resolved map[string]*ast.Module) (map[string]*ast.Module, error) {
|
||||
out := make(map[string]*ast.Module)
|
||||
for k, v := range resolved {
|
||||
for _, imp := range v.Imports {
|
||||
pv := imp.Path.Value.String()
|
||||
pkgPath, ok := strings.CutPrefix(pv, "data.")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if resolvedHasPackage(resolved, pkgPath) {
|
||||
continue
|
||||
}
|
||||
fn := strings.ReplaceAll(pkgPath, ".", "/") + ".rego"
|
||||
if _, ok := resolved[fn]; ok {
|
||||
continue
|
||||
}
|
||||
if root == nil {
|
||||
if fsProvider == nil {
|
||||
return nil, errors.Errorf("no policy FS defined for import %s", pv)
|
||||
}
|
||||
f, cf, err := fsProvider()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get policy FS for import %s", pv)
|
||||
}
|
||||
root = f
|
||||
closeFS = cf
|
||||
}
|
||||
loadName := fn
|
||||
if _, err := root.Stat(loadName); err != nil {
|
||||
return nil, errors.Wrapf(err, "import %s not found for module %s", pv, k)
|
||||
}
|
||||
dt, err := fs.ReadFile(root, loadName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to read imported policy file %s for module %s", loadName, k)
|
||||
}
|
||||
mod, err := ast.ParseModuleWithOpts(loadName, string(dt), ast.ParserOptions{
|
||||
RegoVersion: ast.RegoV1,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse imported policy file %s for module %s", loadName, k)
|
||||
}
|
||||
// rewrite package to be less strict
|
||||
pkgParts := strings.Split(pkgPath, ".")
|
||||
ref := ast.Ref{mod.Package.Path[0]}
|
||||
for _, p := range pkgParts {
|
||||
ref = append(ref, ast.StringTerm(p))
|
||||
}
|
||||
mod.Package = &ast.Package{Path: ref}
|
||||
out[fn] = mod
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
return loader, func() error {
|
||||
if closeFS != nil {
|
||||
return closeFS()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func resolvedHasPackage(resolved map[string]*ast.Module, pkgPath string) bool {
|
||||
for _, mod := range resolved {
|
||||
if mod.Package != nil && mod.Package.Path.String() == pkgPath {
|
||||
return true
|
||||
}
|
||||
if mod.Package != nil && strings.TrimPrefix(mod.Package.Path.String(), "data.") == pkgPath {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -15,6 +15,11 @@ type Input struct {
|
||||
Git *Git `json:"git,omitempty"`
|
||||
}
|
||||
|
||||
type Decision struct {
|
||||
Allow *bool `json:"allow,omitempty"`
|
||||
DenyMessages []string `json:"deny_msg,omitempty"`
|
||||
}
|
||||
|
||||
type Env struct {
|
||||
Args map[string]*string `json:"args,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
|
||||
@@ -26,6 +26,7 @@ func TestIntegration(t *testing.T) {
|
||||
tests = append(tests, policyBuildTests...)
|
||||
tests = append(tests, policyEvalTests...)
|
||||
tests = append(tests, policyBakeTests...)
|
||||
tests = append(tests, policyTestTests...)
|
||||
tests = append(tests, bakeTests...)
|
||||
tests = append(tests, historyTests...)
|
||||
tests = append(tests, inspectTests...)
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/continuity/fs/fstest"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var policyTestTests = []func(t *testing.T, sb integration.Sandbox){
|
||||
testPolicyTestRunFilter,
|
||||
testPolicyTestFailMissingInput,
|
||||
testPolicyTestNestedPath,
|
||||
}
|
||||
|
||||
func testPolicyTestRunFilter(t *testing.T, sb integration.Sandbox) {
|
||||
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("policy.rego", []byte(`
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if input.image.repo == "example/allowlist"
|
||||
|
||||
allow if {
|
||||
input.image.repo == "example/docs"
|
||||
input.image.tag == "doc"
|
||||
}
|
||||
|
||||
deny_msg[msg] if {
|
||||
not allow
|
||||
msg := "repository not allowed"
|
||||
}
|
||||
|
||||
decision := {"allow": allow, "deny_msg": deny_msg}
|
||||
`), 0600),
|
||||
fstest.CreateFile("policy_test.rego", []byte(`
|
||||
package docker
|
||||
|
||||
# Images from allowlisted repo are allowed
|
||||
test_allowlisted_repo if {
|
||||
result := data.docker.decision with input as {"image": {"repo": "example/allowlist"}}
|
||||
result.allow
|
||||
count(result.deny_msg) == 0
|
||||
}
|
||||
|
||||
# Other repos are denied
|
||||
test_non_allowlisted_repo if {
|
||||
result := data.docker.decision with input as {"image": {"repo": "example/blocked"}}
|
||||
not result.allow
|
||||
result.deny_msg["repository not allowed"]
|
||||
}
|
||||
|
||||
# Docs images are allowed from a specific repo
|
||||
test_docs_tag_allowed if {
|
||||
result := data.docker.decision with input as {"image": {"repo": "example/docs", "tag": "doc"}}
|
||||
result.allow
|
||||
}
|
||||
`), 0600),
|
||||
)
|
||||
|
||||
cmd := buildxCmd(sb, withDir(dir), withArgs(
|
||||
"policy",
|
||||
"test",
|
||||
"--filename",
|
||||
"policy",
|
||||
".",
|
||||
))
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(out))
|
||||
require.Contains(t, string(out), "test_allowlisted_repo: PASS")
|
||||
require.Contains(t, string(out), "test_non_allowlisted_repo: PASS")
|
||||
require.Contains(t, string(out), "test_docs_tag_allowed: PASS")
|
||||
|
||||
cmd = buildxCmd(sb, withDir(dir), withArgs(
|
||||
"policy",
|
||||
"test",
|
||||
"--filename",
|
||||
"policy",
|
||||
"--run",
|
||||
"test_allowlisted_repo",
|
||||
".",
|
||||
))
|
||||
out, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(out))
|
||||
require.Contains(t, string(out), "test_allowlisted_repo: PASS")
|
||||
require.NotContains(t, string(out), "test_guest")
|
||||
}
|
||||
|
||||
func testPolicyTestFailMissingInput(t *testing.T, sb integration.Sandbox) {
|
||||
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("policy.rego", []byte(`
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if input.image.repo == "example/allowlist"
|
||||
|
||||
deny_msg[msg] if {
|
||||
not allow
|
||||
msg := "repository required"
|
||||
}
|
||||
decision := {"allow": allow, "deny_msg": deny_msg}
|
||||
`), 0600),
|
||||
fstest.CreateFile("policy_test.rego", []byte(`
|
||||
package docker
|
||||
|
||||
test_missing_repo if {
|
||||
result := data.docker.decision with input as {"image": {}}
|
||||
result.allow
|
||||
}
|
||||
|
||||
test_allowlisted_ok if {
|
||||
result := data.docker.decision with input as {"image": {"repo": "example/allowlist"}}
|
||||
result.allow
|
||||
}
|
||||
`), 0600),
|
||||
)
|
||||
|
||||
cmd := buildxCmd(sb, withDir(dir), withArgs(
|
||||
"policy",
|
||||
"test",
|
||||
"--filename",
|
||||
"policy",
|
||||
".",
|
||||
))
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.Error(t, err, string(out))
|
||||
require.Contains(t, string(out), "test_missing_repo: FAIL")
|
||||
require.Contains(t, string(out), "test_allowlisted_ok: PASS")
|
||||
require.Contains(t, string(out), "missing_input: input.image.repo")
|
||||
}
|
||||
|
||||
func testPolicyTestNestedPath(t *testing.T, sb integration.Sandbox) {
|
||||
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("Dockerfile.rego", []byte(`
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if input.image.repo == "example/allowlist"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`), 0600),
|
||||
fstest.CreateDir("scripts", 0700),
|
||||
fstest.CreateFile("scripts/policy_test.rego", []byte(`
|
||||
package docker
|
||||
|
||||
test_allowlisted_repo if {
|
||||
result := data.docker.decision with input as {"image": {"repo": "example/allowlist"}}
|
||||
result.allow
|
||||
}
|
||||
`), 0600),
|
||||
)
|
||||
|
||||
cmd := buildxCmd(sb, withDir(dir), withArgs(
|
||||
"policy",
|
||||
"test",
|
||||
"--filename",
|
||||
"Dockerfile",
|
||||
"scripts/policy_test.rego",
|
||||
))
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(out))
|
||||
require.Contains(t, string(out), "test_allowlisted_repo: PASS")
|
||||
|
||||
cmd = buildxCmd(sb, withDir(dir), withArgs(
|
||||
"policy",
|
||||
"test",
|
||||
"--filename",
|
||||
"Dockerfile",
|
||||
"scripts",
|
||||
))
|
||||
out, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(out))
|
||||
require.Contains(t, string(out), "test_allowlisted_repo: PASS")
|
||||
}
|
||||
Reference in New Issue
Block a user