Merge pull request #3656 from tonistiigi/policy-error
policy: mark policy progress vertex on DENY build failures
This commit is contained in:
+8
-6
@@ -272,18 +272,18 @@ func warnOnNoOutput(ctx context.Context, nodes []builder.Node, opts map[string]O
|
|||||||
logrus.Warnf("%s. Build result will only remain in the build cache. To push result image into registry use --push or to load image into docker use --load", warnNoOutputBuf.String())
|
logrus.Warnf("%s. Build result will only remain in the build cache. To push result image into registry use --push or to load image into docker use --load", warnNoOutputBuf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBuildRequests(ctx context.Context, docker *dockerutil.Client, cfg *confutil.Config, drivers map[string][]*noderesolver.ResolvedNode, w progress.Writer, opts map[string]Options) (_ map[string][]*reqForNode, _ func(), retErr error) {
|
func newBuildRequests(ctx context.Context, docker *dockerutil.Client, cfg *confutil.Config, drivers map[string][]*noderesolver.ResolvedNode, w progress.Writer, opts map[string]Options) (_ map[string][]*reqForNode, _ func(error), retErr error) {
|
||||||
reqForNodes := make(map[string][]*reqForNode)
|
reqForNodes := make(map[string][]*reqForNode)
|
||||||
|
|
||||||
var releasers []func()
|
var releasers []func(error)
|
||||||
releaseAll := func() {
|
releaseAll := func(inErr error) {
|
||||||
for _, fn := range releasers {
|
for _, fn := range releasers {
|
||||||
fn()
|
fn(inErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if retErr != nil {
|
if retErr != nil {
|
||||||
releaseAll()
|
releaseAll(retErr)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -432,7 +432,9 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer release()
|
defer func() {
|
||||||
|
release(err)
|
||||||
|
}()
|
||||||
|
|
||||||
// validate that all links between targets use same drivers
|
// validate that all links between targets use same drivers
|
||||||
if err := validateTargetLinks(reqForNodes, drivers, opts); err != nil {
|
if err := validateTargetLinks(reqForNodes, drivers, opts); err != nil {
|
||||||
|
|||||||
+27
-8
@@ -213,18 +213,27 @@ func (l *policyProgressLogger) sendVertexComplete(started time.Time, err error)
|
|||||||
l.ch <- &client.SolveStatus{Vertexes: []*client.Vertex{&vtx}}
|
l.ch <- &client.SolveStatus{Vertexes: []*client.Vertex{&vtx}}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 isPolicyEvaluationError(policies []*policy.Policy, err error) bool {
|
||||||
|
for _, p := range policies {
|
||||||
|
if p != nil && p.IsPolicyError(err) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
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(error), err error) {
|
||||||
nodeDriver := node.Driver
|
nodeDriver := node.Driver
|
||||||
defers := make([]func(), 0, 2)
|
defers := make([]func(error), 0, 2)
|
||||||
releaseF := func() {
|
releaseF := func(inErr error) {
|
||||||
for _, f := range defers {
|
for _, f := range defers {
|
||||||
f()
|
f(inErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
releaseF()
|
releaseF(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -432,7 +441,9 @@ 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
|
||||||
}
|
}
|
||||||
defers = append(defers, cancel)
|
defers = append(defers, func(error) {
|
||||||
|
cancel()
|
||||||
|
})
|
||||||
opt.Exports[i].Output = func(_ map[string]string) (io.WriteCloser, error) {
|
opt.Exports[i].Output = func(_ map[string]string) (io.WriteCloser, error) {
|
||||||
return w, nil
|
return w, nil
|
||||||
}
|
}
|
||||||
@@ -478,7 +489,9 @@ 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
|
||||||
}
|
}
|
||||||
defers = append(defers, releaseLoad)
|
defers = append(defers, func(error) {
|
||||||
|
releaseLoad()
|
||||||
|
})
|
||||||
|
|
||||||
if opt.Inputs.policy == nil {
|
if opt.Inputs.policy == nil {
|
||||||
if len(opt.Policy) > 0 {
|
if len(opt.Policy) > 0 {
|
||||||
@@ -512,8 +525,13 @@ func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt *O
|
|||||||
if len(policyFiles) > 0 {
|
if len(policyFiles) > 0 {
|
||||||
policyLogger = newPolicyProgressLogger(pw, fmt.Sprintf("loading policies %s", strings.Join(policyFiles, ", ")))
|
policyLogger = newPolicyProgressLogger(pw, fmt.Sprintf("loading policies %s", strings.Join(policyFiles, ", ")))
|
||||||
}
|
}
|
||||||
|
var policies []*policy.Policy
|
||||||
if policyLogger != nil {
|
if policyLogger != nil {
|
||||||
defers = append(defers, func() {
|
defers = append(defers, func(inErr error) {
|
||||||
|
if len(policysession.DenyMessages(inErr)) > 0 || isPolicyEvaluationError(policies, inErr) {
|
||||||
|
policyLogger.Close(inErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
policyLogger.Close(nil)
|
policyLogger.Close(nil)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -537,6 +555,7 @@ func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt *O
|
|||||||
VerifierProvider: policy.SignatureVerifier(cfg),
|
VerifierProvider: policy.SignatureVerifier(cfg),
|
||||||
DefaultPlatform: defaultPlatform(bopts),
|
DefaultPlatform: defaultPlatform(bopts),
|
||||||
})
|
})
|
||||||
|
policies = append(policies, p)
|
||||||
cbs = append(cbs, p.CheckPolicy)
|
cbs = append(cbs, p.CheckPolicy)
|
||||||
if popt.Strict {
|
if popt.Strict {
|
||||||
if bopts.LLBCaps.Supports(pb.CapSourcePolicySession) != nil {
|
if bopts.LLBCaps.Supports(pb.CapSourcePolicySession) != nil {
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
|
||||||
|
solverpb "github.com/moby/buildkit/solver/pb"
|
||||||
|
"github.com/moby/buildkit/sourcepolicy/policysession"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPolicyIsPolicyError(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "matches-recorded-source",
|
||||||
|
err: errors.New("failed to solve: error evaluating the source policy: source \"docker-image://busybox:latest\" not allowed by policy: action DENY"),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does-not-match-without-buildkit-pattern",
|
||||||
|
err: errors.New("failed to parse dockerfile for docker-image://busybox:latest"),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does-not-match-unrelated-error",
|
||||||
|
err: errors.New("failed to solve: error evaluating the source policy: source \"docker-image://alpine:latest\" not allowed by policy: action DENY"),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
p := NewPolicy(Opt{})
|
||||||
|
req := &policysession.CheckPolicyRequest{
|
||||||
|
Source: &gwpb.ResolveSourceMetaResponse{
|
||||||
|
Source: &solverpb.SourceOp{
|
||||||
|
Identifier: "docker-image://busybox:latest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p.recordDenyIdentifier(req)
|
||||||
|
|
||||||
|
require.Equal(t, tt.want, p.IsPolicyError(tt.err))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/containerd/platforms"
|
"github.com/containerd/platforms"
|
||||||
@@ -32,6 +33,9 @@ import (
|
|||||||
type Policy struct {
|
type Policy struct {
|
||||||
opt Opt
|
opt Opt
|
||||||
funcs []fun
|
funcs []fun
|
||||||
|
|
||||||
|
denyMu sync.Mutex
|
||||||
|
denyIdentifiers map[string]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type state struct {
|
type state struct {
|
||||||
@@ -84,6 +88,43 @@ func (p *Policy) log(level logrus.Level, format string, v ...any) {
|
|||||||
p.opt.Log(level, fmt.Sprintf(format, v...))
|
p.opt.Log(level, fmt.Sprintf(format, v...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Policy) recordDenyIdentifier(req *policysession.CheckPolicyRequest) {
|
||||||
|
if p == nil || req == nil || req.Source == nil || req.Source.Source == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.denyMu.Lock()
|
||||||
|
defer p.denyMu.Unlock()
|
||||||
|
|
||||||
|
if p.denyIdentifiers == nil {
|
||||||
|
p.denyIdentifiers = make(map[string]struct{})
|
||||||
|
}
|
||||||
|
id := strings.TrimSpace(req.Source.Source.Identifier)
|
||||||
|
if id == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.denyIdentifiers[id] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Policy) IsPolicyError(err error) bool {
|
||||||
|
if p == nil || err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
errText := err.Error()
|
||||||
|
// TODO: replace this string matching with a typed BuildKit error that is
|
||||||
|
// always attached for policy DENY decisions.
|
||||||
|
p.denyMu.Lock()
|
||||||
|
defer p.denyMu.Unlock()
|
||||||
|
for id := range p.denyIdentifiers {
|
||||||
|
pattern := fmt.Sprintf("source %q not allowed by policy: action %s", id, moby_buildkit_v1_sourcepolicy.PolicyAction_DENY.String())
|
||||||
|
if strings.Contains(errText, pattern) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
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")
|
||||||
@@ -307,6 +348,9 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
|
|||||||
for _, dm := range resp.DenyMessages {
|
for _, dm := range resp.DenyMessages {
|
||||||
p.log(logrus.InfoLevel, " - %s", dm.Message)
|
p.log(logrus.InfoLevel, " - %s", dm.Message)
|
||||||
}
|
}
|
||||||
|
if resp.Action == moby_buildkit_v1_sourcepolicy.PolicyAction_DENY {
|
||||||
|
p.recordDenyIdentifier(req)
|
||||||
|
}
|
||||||
|
|
||||||
return resp, nil, nil
|
return resp, nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package tests
|
package tests
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/containerd/continuity/fs/fstest"
|
"github.com/containerd/continuity/fs/fstest"
|
||||||
@@ -13,6 +15,7 @@ import (
|
|||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/buildx/util/gitutil"
|
"github.com/docker/buildx/util/gitutil"
|
||||||
"github.com/docker/buildx/util/gitutil/gittestutil"
|
"github.com/docker/buildx/util/gitutil/gittestutil"
|
||||||
|
"github.com/moby/buildkit/client"
|
||||||
"github.com/moby/buildkit/identity"
|
"github.com/moby/buildkit/identity"
|
||||||
"github.com/moby/buildkit/util/contentutil"
|
"github.com/moby/buildkit/util/contentutil"
|
||||||
"github.com/moby/buildkit/util/testutil"
|
"github.com/moby/buildkit/util/testutil"
|
||||||
@@ -25,6 +28,7 @@ import (
|
|||||||
var policyBuildTests = []func(t *testing.T, sb integration.Sandbox){
|
var policyBuildTests = []func(t *testing.T, sb integration.Sandbox){
|
||||||
testBuildPolicyAllow,
|
testBuildPolicyAllow,
|
||||||
testBuildPolicyDeny,
|
testBuildPolicyDeny,
|
||||||
|
testBuildPolicyDenyProgressStream,
|
||||||
testBuildPolicyImageName,
|
testBuildPolicyImageName,
|
||||||
testBuildPolicyEnv,
|
testBuildPolicyEnv,
|
||||||
testBuildPolicyHTTP,
|
testBuildPolicyHTTP,
|
||||||
@@ -103,6 +107,80 @@ decision := {"allow": allow, "deny_msg": deny_msg}
|
|||||||
require.Contains(t, string(out), "DENY")
|
require.Contains(t, string(out), "DENY")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testBuildPolicyDenyProgressStream(t *testing.T, sb integration.Sandbox) {
|
||||||
|
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
|
||||||
|
dockerfile := []byte(`
|
||||||
|
FROM busybox:latest
|
||||||
|
RUN echo policy-nope
|
||||||
|
`)
|
||||||
|
policyFile := []byte(`
|
||||||
|
package docker
|
||||||
|
|
||||||
|
default allow = false
|
||||||
|
|
||||||
|
allow if not input.image
|
||||||
|
|
||||||
|
decision := {"allow": allow}
|
||||||
|
`)
|
||||||
|
dir := tmpdir(
|
||||||
|
t,
|
||||||
|
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||||
|
fstest.CreateFile("policy.rego", policyFile, 0600),
|
||||||
|
)
|
||||||
|
policyPath := filepath.Join(dir, "policy.rego")
|
||||||
|
|
||||||
|
cmd := buildxCmd(sb, withDir(dir), withArgs(
|
||||||
|
"build",
|
||||||
|
"--progress=rawjson",
|
||||||
|
"--policy", "filename="+policyPath,
|
||||||
|
"--output=type=cacheonly",
|
||||||
|
dir,
|
||||||
|
))
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
outStr := string(out)
|
||||||
|
require.Error(t, err, outStr)
|
||||||
|
|
||||||
|
policyVertexName := "loading policies " + policyPath
|
||||||
|
sawPolicyVertex := false
|
||||||
|
sawPolicyCompleted := false
|
||||||
|
policyVertexDigest := ""
|
||||||
|
policyVertexError := ""
|
||||||
|
|
||||||
|
for line := range strings.SplitSeq(outStr, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if !strings.HasPrefix(line, "{") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var st client.SolveStatus
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(line), &st), line)
|
||||||
|
for _, v := range st.Vertexes {
|
||||||
|
if v == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v.Name == policyVertexName {
|
||||||
|
sawPolicyVertex = true
|
||||||
|
if v.Digest != "" {
|
||||||
|
policyVertexDigest = string(v.Digest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if policyVertexDigest == "" || string(v.Digest) != policyVertexDigest {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v.Completed != nil {
|
||||||
|
sawPolicyCompleted = true
|
||||||
|
}
|
||||||
|
if v.Error != "" {
|
||||||
|
policyVertexError = v.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require.True(t, sawPolicyVertex, outStr)
|
||||||
|
require.True(t, sawPolicyCompleted, outStr)
|
||||||
|
require.Contains(t, policyVertexError, "not allowed by policy", outStr)
|
||||||
|
require.Contains(t, outStr, "not allowed by policy")
|
||||||
|
}
|
||||||
|
|
||||||
func testBuildPolicyImageName(t *testing.T, sb integration.Sandbox) {
|
func testBuildPolicyImageName(t *testing.T, sb integration.Sandbox) {
|
||||||
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
|
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
|
||||||
registry, err := sb.NewRegistry()
|
registry, err := sb.NewRegistry()
|
||||||
|
|||||||
Reference in New Issue
Block a user