policy: implement policy logging via progress printer

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi
2026-01-14 09:10:34 -08:00
parent 5ad09ce3eb
commit be42b48462
8 changed files with 188 additions and 60 deletions
+4 -4
View File
@@ -132,7 +132,7 @@ type policyOpt struct {
Files []policy.File Files []policy.File
FS func() (fs.StatFS, func() error, error) FS func() (fs.StatFS, func() error, error)
Strict bool Strict bool
LogLevel logrus.Level LogLevel *logrus.Level
} }
func withPolicyConfig(defaultPolicy policyOpt, configs []PolicyConfig) ([]policyOpt, error) { func withPolicyConfig(defaultPolicy policyOpt, configs []PolicyConfig) ([]policyOpt, error) {
@@ -177,7 +177,7 @@ func withPolicyConfig(defaultPolicy policyOpt, configs []PolicyConfig) ([]policy
last.Strict = *cfg.Strict last.Strict = *cfg.Strict
} }
if cfg.LogLevel != nil { if cfg.LogLevel != nil {
last.LogLevel = *cfg.LogLevel last.LogLevel = cfg.LogLevel
} }
} }
continue continue
@@ -190,13 +190,13 @@ func withPolicyConfig(defaultPolicy policyOpt, configs []PolicyConfig) ([]policy
opt.Strict = *last.Strict opt.Strict = *last.Strict
} }
if last.LogLevel != nil { if last.LogLevel != nil {
opt.LogLevel = *last.LogLevel opt.LogLevel = last.LogLevel
} }
if cfg.Strict != nil { if cfg.Strict != nil {
opt.Strict = *cfg.Strict opt.Strict = *cfg.Strict
} }
if cfg.LogLevel != nil { if cfg.LogLevel != nil {
opt.LogLevel = *cfg.LogLevel opt.LogLevel = cfg.LogLevel
} }
opt.FS = defaultPolicy.FS opt.FS = defaultPolicy.FS
out = append(out, opt) out = append(out, opt)
+106 -6
View File
@@ -3,9 +3,9 @@ package build
import ( import (
"bytes" "bytes"
"context" "context"
"fmt"
"io" "io"
"io/fs" "io/fs"
"log"
"maps" "maps"
"os" "os"
"path" "path"
@@ -15,6 +15,7 @@ import (
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
"time"
awsconfig "github.com/aws/aws-sdk-go-v2/config" awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/containerd/console" "github.com/containerd/console"
@@ -48,6 +49,7 @@ import (
"github.com/moby/buildkit/util/gitutil" "github.com/moby/buildkit/util/gitutil"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/tonistiigi/fsutil" "github.com/tonistiigi/fsutil"
) )
@@ -60,6 +62,79 @@ var sendGitQueryAsInput = sync.OnceValue(func() bool {
return false return false
}) })
type policyProgressLogger struct {
ch chan *client.SolveStatus
done chan struct{}
dgst digest.Digest
started time.Time
name string
}
func newPolicyProgressLogger(pw progress.Writer, name string) *policyProgressLogger {
if pw == nil {
return nil
}
ch, done := progress.NewChannel(pw)
dgst := digest.FromBytes([]byte(identity.NewID()))
tm := time.Now()
vtx := client.Vertex{
Digest: dgst,
Name: name,
Started: &tm,
}
ch <- &client.SolveStatus{Vertexes: []*client.Vertex{&vtx}}
return &policyProgressLogger{
ch: ch,
done: done,
dgst: dgst,
started: tm,
name: name,
}
}
func (l *policyProgressLogger) Log(msg string) {
if l == nil || msg == "" {
return
}
if !strings.HasSuffix(msg, "\n") {
msg += "\n"
}
l.ch <- &client.SolveStatus{
Logs: []*client.VertexLog{{
Vertex: l.dgst,
Stream: 1,
Data: []byte(msg),
Timestamp: time.Now(),
}},
}
}
func (l *policyProgressLogger) Write(p []byte) (int, error) {
if len(p) > 0 {
l.Log(string(p))
}
return len(p), nil
}
func (l *policyProgressLogger) Close(err error) {
if l == nil {
return
}
tm := time.Now()
vtx := client.Vertex{
Digest: l.dgst,
Name: l.name,
Started: &l.started,
Completed: &tm,
}
if err != nil {
vtx.Error = err.Error()
}
l.ch <- &client.SolveStatus{Vertexes: []*client.Vertex{&vtx}}
close(l.ch)
<-l.done
}
func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt *Options, bopts gateway.BuildOpts, cfg *confutil.Config, pw progress.Writer, docker *dockerutil.Client) (_ *client.SolveOpt, release func(), err error) { func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt *Options, bopts gateway.BuildOpts, cfg *confutil.Config, pw progress.Writer, docker *dockerutil.Client) (_ *client.SolveOpt, release func(), err error) {
nodeDriver := node.Driver nodeDriver := node.Driver
defers := make([]func(), 0, 2) defers := make([]func(), 0, 2)
@@ -347,14 +422,39 @@ func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt *O
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
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, ", ")))
}
if policyLogger != nil {
defers = append(defers, func() {
policyLogger.Close(nil)
})
}
var cbs []policysession.PolicyCallback var cbs []policysession.PolicyCallback
for _, popt := range popts { 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{ p := policy.NewPolicy(policy.Opt{
Files: popt.Files, Files: popt.Files,
Env: env, Env: env,
Log: func(msg string) { Log: logf,
log.Printf("[policy] %s", msg)
},
FS: opt.Inputs.policy.FS, FS: opt.Inputs.policy.FS,
VerifierProvider: policy.SignatureVerifier(cfg), VerifierProvider: policy.SignatureVerifier(cfg),
}) })
+8 -7
View File
@@ -33,7 +33,7 @@ func TestWithPolicyConfigDefaults(t *testing.T) {
require.Len(t, out, 1) require.Len(t, out, 1)
require.Equal(t, defaultPolicy.Files, out[0].Files) require.Equal(t, defaultPolicy.Files, out[0].Files)
require.False(t, out[0].Strict) require.False(t, out[0].Strict)
require.Equal(t, logrus.Level(0), out[0].LogLevel) require.Nil(t, out[0].LogLevel)
require.NotNil(t, out[0].FS) require.NotNil(t, out[0].FS)
} }
@@ -54,11 +54,10 @@ func TestWithPolicyConfigDisabled(t *testing.T) {
}) })
require.Error(t, err) require.Error(t, err)
out, err := withPolicyConfig(policyOpt{}, []PolicyConfig{ _, err = withPolicyConfig(policyOpt{}, []PolicyConfig{
{Disabled: true, LogLevel: levelPtr(logrus.WarnLevel)}, {Disabled: true, LogLevel: levelPtr(logrus.WarnLevel)},
}) })
require.NoError(t, err) require.Error(t, err)
require.Nil(t, out)
_, err = withPolicyConfig(policyOpt{}, []PolicyConfig{ _, err = withPolicyConfig(policyOpt{}, []PolicyConfig{
{Disabled: true}, {Disabled: true},
@@ -66,7 +65,7 @@ func TestWithPolicyConfigDisabled(t *testing.T) {
}) })
require.Error(t, err) require.Error(t, err)
out, err = withPolicyConfig(policyOpt{}, []PolicyConfig{ out, err := withPolicyConfig(policyOpt{}, []PolicyConfig{
{Disabled: true}, {Disabled: true},
}) })
require.NoError(t, err) require.NoError(t, err)
@@ -104,7 +103,8 @@ func TestWithPolicyConfigStrictAndLogLevel(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Len(t, out, 1) require.Len(t, out, 1)
require.True(t, out[0].Strict) require.True(t, out[0].Strict)
require.Equal(t, logrus.WarnLevel, out[0].LogLevel) require.NotNil(t, out[0].LogLevel)
require.Equal(t, logrus.WarnLevel, *out[0].LogLevel)
} }
// TestWithPolicyConfigStrictIgnoredWithoutPolicy ensures strict without any policy produces no entries. // TestWithPolicyConfigStrictIgnoredWithoutPolicy ensures strict without any policy produces no entries.
@@ -135,7 +135,8 @@ func TestWithPolicyConfigMultipleFilesAndOverrides(t *testing.T) {
require.Equal(t, "default.rego", out[0].Files[0].Filename) require.Equal(t, "default.rego", out[0].Files[0].Filename)
require.Equal(t, "a.rego", out[1].Files[0].Filename) require.Equal(t, "a.rego", out[1].Files[0].Filename)
require.True(t, out[1].Strict) require.True(t, out[1].Strict)
require.Equal(t, logrus.WarnLevel, out[1].LogLevel) 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.Equal(t, "b.rego", out[2].Files[0].Filename)
require.True(t, out[2].Strict) require.True(t, out[2].Strict)
require.NotNil(t, out[1].FS) require.NotNil(t, out[1].FS)
+5 -5
View File
@@ -39,9 +39,10 @@ func evalCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
var opts evalOpts var opts evalOpts
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "eval source", Use: "eval [OPTIONS] source",
Short: "Evaluate policy for a source", Short: "Evaluate policy for a source",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
DisableFlagsInUseLine: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.builder = rootOpts.Builder opts.builder = rootOpts.Builder
return runEval(cmd.Context(), dockerCli, args[0], opts) return runEval(cmd.Context(), dockerCli, args[0], opts)
@@ -380,8 +381,7 @@ func evalDecisionError(decision *policysession.DecisionResponse) error {
} }
func parseSource(input string) (*pb.SourceOp, error) { func parseSource(input string) (*pb.SourceOp, error) {
if strings.HasPrefix(input, "docker-image://") { if refstr, ok := strings.CutPrefix(input, "docker-image://"); ok {
refstr := strings.TrimPrefix(input, "docker-image://")
ref, err := reference.ParseNormalizedNamed(refstr) ref, err := reference.ParseNormalizedNamed(refstr)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to parse image source reference") return nil, errors.Wrapf(err, "failed to parse image source reference")
+4 -3
View File
@@ -7,9 +7,10 @@ import (
func jsonSchemaCmd() *cobra.Command { func jsonSchemaCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "json-schema", Use: "json-schema",
Short: "Print policy JSON schema", Short: "Print policy JSON schema",
Args: cobra.NoArgs, Args: cobra.NoArgs,
DisableFlagsInUseLine: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runJSONSchema() return runJSONSchema()
}, },
+3 -2
View File
@@ -12,8 +12,9 @@ type RootOptions struct {
// RootCmd creates the policy command tree. // RootCmd creates the policy command tree.
func RootCmd(rootcmd *cobra.Command, dockerCli command.Cli, rootOpts RootOptions) *cobra.Command { func RootCmd(rootcmd *cobra.Command, dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "policy", Use: "policy",
Short: "Commands for working with build policies", Short: "Commands for working with build policies",
DisableFlagsInUseLine: true,
} }
cmd.AddCommand( cmd.AddCommand(
+4 -3
View File
@@ -7,9 +7,10 @@ import (
func testCmd() *cobra.Command { func testCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "test <path>", Use: "test <path>",
Short: "Run policy tests", Short: "Run policy tests",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
DisableFlagsInUseLine: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runTest(args[0]) return runTest(args[0])
}, },
+54 -30
View File
@@ -3,16 +3,13 @@ package policy
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"io/fs" "io/fs"
"log"
"maps" "maps"
"net/url" "net/url"
"os"
"path" "path"
"slices" "slices"
"strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/containerd/platforms" "github.com/containerd/platforms"
@@ -29,23 +26,9 @@ import (
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1" ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus"
) )
// this is tempory debug, to be replaced with progressbar logging later
var isDebug = sync.OnceValue(func() bool {
if v, ok := os.LookupEnv("BUILDX_POLICY_DEBUG"); ok {
b, _ := strconv.ParseBool(v)
return b
}
return false
})
func debugf(format string, v ...any) {
if isDebug() {
log.Printf(format, v...)
}
}
type Policy struct { type Policy struct {
opt Opt opt Opt
funcs []fun funcs []fun
@@ -73,7 +56,7 @@ type fun struct {
type Opt struct { type Opt struct {
Files []File Files []File
Env Env Env Env
Log func(string) Log func(logrus.Level, string)
FS func() (fs.StatFS, func() error, error) FS func() (fs.StatFS, func() error, error)
VerifierProvider PolicyVerifierProvider VerifierProvider PolicyVerifierProvider
} }
@@ -93,6 +76,13 @@ func NewPolicy(opt Opt) *Policy {
return p return p
} }
func (p *Policy) log(level logrus.Level, format string, v ...any) {
if p == nil || p.opt.Log == nil {
return
}
p.opt.Log(level, fmt.Sprintf(format, v...))
}
func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *gwpb.ResolveSourceMetaRequest, error) { func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *gwpb.ResolveSourceMetaRequest, error) {
if req.Source == nil || req.Source.Source == nil { if req.Source == nil || req.Source.Source == nil {
return nil, nil, errors.Errorf("no source info in request") return nil, nil, errors.Errorf("no source info in request")
@@ -112,7 +102,7 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
platform = &pl platform = &pl
} }
inp, unknowns, err := SourceToInput(ctx, p.opt.VerifierProvider, src, platform) inp, unknowns, err := SourceToInputWithLogger(ctx, p.opt.VerifierProvider, src, platform, p.opt.Log)
if err != nil { if err != nil {
return nil, nil, errors.Wrapf(err, "failed to convert source to policy input") return nil, nil, errors.Wrapf(err, "failed to convert source to policy input")
} }
@@ -220,10 +210,11 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
if err != nil { if err != nil {
return nil, nil, errors.Wrapf(err, "failed to marshal policy input") return nil, nil, errors.Wrapf(err, "failed to marshal policy input")
} }
debugf("policy input: %s", dt) p.log(logrus.InfoLevel, "checking policy for source %s", src.Source.Identifier)
p.log(logrus.DebugLevel, "policy input: %s", dt)
if len(unknowns) > 0 { if len(unknowns) > 0 {
debugf("unknowns for policy evaluation: %+v", unknowns) p.log(logrus.DebugLevel, "unknowns for policy evaluation: %+v", unknowns)
opts = append(opts, rego.Unknowns(unknowns)) opts = append(opts, rego.Unknowns(unknowns))
} }
r := rego.New(opts...) r := rego.New(opts...)
@@ -242,11 +233,11 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
Source: req.Source.Source, Source: req.Source.Source,
Platform: req.Platform, Platform: req.Platform,
} }
if err := AddUnknowns(next, unk); err != nil { if err := AddUnknownsWithLogger(p.opt.Log, next, unk); err != nil {
return nil, nil, err return nil, nil, err
} }
if next.Image != nil || next.Git != nil { if next.Image != nil || next.Git != nil {
debugf("next resolve meta request: %+v", next) p.log(logrus.InfoLevel, "policy decision for source %s: resolve missing fields %+v", src.Source.Identifier, summarizeUnknownsForLog(unk))
return nil, next, nil return nil, next, nil
} }
} }
@@ -274,7 +265,7 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
resp := &policysession.DecisionResponse{ resp := &policysession.DecisionResponse{
Action: moby_buildkit_v1_sourcepolicy.PolicyAction_DENY, Action: moby_buildkit_v1_sourcepolicy.PolicyAction_DENY,
} }
debugf("policy response: %+v", vt) p.log(logrus.DebugLevel, "policy response: %+v", vt)
if v, ok := vt["allow"]; ok { if v, ok := vt["allow"]; ok {
if vv, ok := v.(bool); !ok { if vv, ok := v.(bool); !ok {
@@ -305,6 +296,8 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
if err != nil { if err != nil {
return nil, nil, errors.Wrapf(err, "failed to add image pin to source") return nil, nil, errors.Wrapf(err, "failed to add image pin to source")
} }
p.log(logrus.InfoLevel, "policy decision for source %s: convert to %s", src.Source.Identifier, newSrc.Identifier)
return &policysession.DecisionResponse{ return &policysession.DecisionResponse{
Action: moby_buildkit_v1_sourcepolicy.PolicyAction_CONVERT, Action: moby_buildkit_v1_sourcepolicy.PolicyAction_CONVERT,
Update: newSrc, Update: newSrc,
@@ -312,19 +305,23 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
} }
} }
debugf("policy decision: %s %v", resp.Action, resp.DenyMessages) p.log(logrus.InfoLevel, "policy decision for source %s: %s %v", src.Source.Identifier, resp.Action, resp.DenyMessages)
return resp, nil, nil return resp, nil, nil
} }
func (p *Policy) Print(ctx print.Context, msg string) error { func (p *Policy) Print(ctx print.Context, msg string) error {
if p.opt.Log != nil { if p.opt.Log != nil {
p.opt.Log(ctx.Location.Format("%s", msg)) p.opt.Log(logrus.InfoLevel, ctx.Location.Format("%s", msg))
} }
return nil return nil
} }
func SourceToInput(ctx context.Context, getVerifier PolicyVerifierProvider, src *gwpb.ResolveSourceMetaResponse, platform *ocispecs.Platform) (Input, []string, error) { func SourceToInput(ctx context.Context, getVerifier PolicyVerifierProvider, src *gwpb.ResolveSourceMetaResponse, platform *ocispecs.Platform) (Input, []string, error) {
return SourceToInputWithLogger(ctx, getVerifier, src, platform, nil)
}
func SourceToInputWithLogger(ctx context.Context, getVerifier PolicyVerifierProvider, src *gwpb.ResolveSourceMetaResponse, platform *ocispecs.Platform, logf func(logrus.Level, string)) (Input, []string, error) {
var inp Input var inp Input
var unknowns []string var unknowns []string
@@ -558,7 +555,9 @@ func SourceToInput(ctx context.Context, getVerifier PolicyVerifierProvider, src
if getVerifier != nil { if getVerifier != nil {
signatures, err := parseSignatures(ctx, getVerifier, ac, platform) signatures, err := parseSignatures(ctx, getVerifier, ac, platform)
if err != nil { if err != nil {
debugf("failed to parse image signatures: %v", err) if logf != nil {
logf(logrus.DebugLevel, fmt.Sprintf("failed to parse image signatures: %v", err))
}
} else { } else {
inp.Image.Signatures = signatures inp.Image.Signatures = signatures
} }
@@ -588,6 +587,10 @@ func withPrefix(arr []string, prefix string) []string {
} }
func AddUnknowns(req *gwpb.ResolveSourceMetaRequest, unk []string) error { func AddUnknowns(req *gwpb.ResolveSourceMetaRequest, unk []string) error {
return AddUnknownsWithLogger(nil, req, unk)
}
func AddUnknownsWithLogger(logf func(logrus.Level, string), req *gwpb.ResolveSourceMetaRequest, unk []string) error {
unk2 := make([]string, 0, len(unk)) unk2 := make([]string, 0, len(unk))
for _, u := range unk { for _, u := range unk {
k := strings.TrimPrefix(u, "input.") k := strings.TrimPrefix(u, "input.")
@@ -604,7 +607,9 @@ func AddUnknowns(req *gwpb.ResolveSourceMetaRequest, unk []string) error {
return nil return nil
} }
debugf("collected unknowns: %+v", unk2) if logf != nil {
logf(logrus.DebugLevel, fmt.Sprintf("collected unknowns: %+v", unk2))
}
for _, u := range unk2 { for _, u := range unk2 {
switch u { switch u {
case "image.checksum", "image.labels", "image.user", "image.volumes", "image.workingDir", "image.env": case "image.checksum", "image.labels", "image.user", "image.volumes", "image.workingDir", "image.env":
@@ -654,6 +659,25 @@ func collectUnknowns(mods []*ast.Module) []string {
return out return out
} }
func summarizeUnknownsForLog(unk []string) []string {
out := make([]string, 0, len(unk))
seen := map[string]struct{}{}
for _, u := range unk {
if strings.HasPrefix(u, "input.image.signatures") {
u = "input.image.signatures"
}
if u == "input.image" {
continue
}
if _, ok := seen[u]; ok {
continue
}
seen[u] = struct{}{}
out = append(out, u)
}
return out
}
func trimKey(s string) string { func trimKey(s string) string {
const ( const (
dot = '.' dot = '.'