Files
buildx/commands/policy/test.go
T
Tonis Tiigi dce5289722 policy: separate sourcemetaresolver utility to reusable pkg
Turn the current utility used by policy helper commands that
allows getting gateway.Client lazily into reusable package.

This is preparation for using this utility also in the policy
verification, outside of commands.

Only expose SourceMetaResolver as other methods of gateway client
are not needed atm by any callers.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2026-02-13 13:59:33 -08:00

211 lines
5.4 KiB
Go

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/buildx/util/sourcemeta"
"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(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 {
optionsProvider := newPolicyTestOptionsProvider(dockerCli, rootOpts.Builder)
opts.Provider = optionsProvider.TestOptionsProvider()
defer optionsProvider.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(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(withInputPrefix(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))
}
func withInputPrefix(keys []string) []string {
out := make([]string, len(keys))
for i, k := range keys {
out[i] = "input." + k
}
return out
}
type policyTestOptionsProvider struct {
dockerCli command.Cli
builderName *string
once sync.Once
platform *ocispecs.Platform
metaResolver *sourcemeta.Resolver
err error
}
func newPolicyTestOptionsProvider(dockerCli command.Cli, builderName *string) *policyTestOptionsProvider {
return &policyTestOptionsProvider{
dockerCli: dockerCli,
builderName: builderName,
}
}
func (r *policyTestOptionsProvider) TestOptionsProvider() *policy.TestOptionsProvider {
return &policy.TestOptionsProvider{
Resolve: r.Resolve,
Platform: r.Platform,
VerifierProvider: policy.SignatureVerifier(confutil.NewConfig(r.dockerCli)),
}
}
func (r *policyTestOptionsProvider) Close() error {
if r.metaResolver == nil {
return nil
}
return r.metaResolver.Close()
}
func (r *policyTestOptionsProvider) Platform(ctx context.Context) (*ocispecs.Platform, error) {
if err := r.init(ctx); err != nil {
return nil, err
}
return r.platform, nil
}
func (r *policyTestOptionsProvider) Resolve(ctx context.Context, source *pb.SourceOp, req *gwpb.ResolveSourceMetaRequest) (*gwpb.ResolveSourceMetaResponse, error) {
if err := r.init(ctx); err != nil {
return nil, err
}
opt := sourceResolverOpt(req, r.platform)
resp, err := r.metaResolver.ResolveSourceMetadata(ctx, source, opt)
if err != nil {
return nil, err
}
return buildSourceMetaResponse(resp), nil
}
func (r *policyTestOptionsProvider) 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,
}
r.metaResolver = sourcemeta.NewResolver(c)
})
return r.err
}