Evaluate source policy caps before solve requests so policies can enable
BuildKit proxy networking. Policy can return caps {"exec.proxy": true}
during the caps request to enable proxy network
support for the solve.
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
389 lines
9.3 KiB
Go
389 lines
9.3 KiB
Go
package build
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/docker/buildx/policy"
|
|
"github.com/docker/buildx/util/buildflags"
|
|
"github.com/docker/buildx/util/ocilayout"
|
|
"github.com/docker/buildx/util/progress"
|
|
"github.com/moby/buildkit/client"
|
|
"github.com/moby/buildkit/client/ociindex"
|
|
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
|
"github.com/moby/buildkit/solver/pb"
|
|
"github.com/moby/buildkit/util/apicaps"
|
|
"github.com/opencontainers/go-digest"
|
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/pkg/errors"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestCacheOptions_DerivedVars(t *testing.T) {
|
|
t.Setenv("ACTIONS_RUNTIME_TOKEN", "sensitive_token")
|
|
t.Setenv("ACTIONS_CACHE_URL", "https://cache.github.com")
|
|
t.Setenv("AWS_ACCESS_KEY_ID", "definitely_dont_look_here")
|
|
t.Setenv("AWS_SECRET_ACCESS_KEY", "hackers_please_dont_steal")
|
|
t.Setenv("AWS_SESSION_TOKEN", "not_a_mitm_attack")
|
|
|
|
cacheFrom, err := buildflags.ParseCacheEntry([]string{"type=gha", "type=s3,region=us-west-2,bucket=my_bucket,name=my_image"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, []client.CacheOptionsEntry{
|
|
{
|
|
Type: "gha",
|
|
Attrs: map[string]string{
|
|
"token": "sensitive_token",
|
|
"url": "https://cache.github.com",
|
|
},
|
|
},
|
|
{
|
|
Type: "s3",
|
|
Attrs: map[string]string{
|
|
"region": "us-west-2",
|
|
"bucket": "my_bucket",
|
|
"name": "my_image",
|
|
"access_key_id": "definitely_dont_look_here",
|
|
"secret_access_key": "hackers_please_dont_steal",
|
|
"session_token": "not_a_mitm_attack",
|
|
},
|
|
},
|
|
}, CreateCaches(cacheFrom))
|
|
}
|
|
|
|
func TestCreateExports_RegistryUnpack(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
entries []*buildflags.ExportEntry
|
|
wantType string
|
|
wantPush string
|
|
wantUnpack string
|
|
}{
|
|
{
|
|
name: "registry type sets unpack=false",
|
|
entries: []*buildflags.ExportEntry{
|
|
{
|
|
Type: "registry",
|
|
Attrs: map[string]string{},
|
|
},
|
|
},
|
|
wantType: "image",
|
|
wantPush: "true",
|
|
wantUnpack: "false",
|
|
},
|
|
{
|
|
name: "registry type respects explicit unpack=true",
|
|
entries: []*buildflags.ExportEntry{
|
|
{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"unpack": "true",
|
|
},
|
|
},
|
|
},
|
|
wantType: "image",
|
|
wantPush: "true",
|
|
wantUnpack: "true",
|
|
},
|
|
{
|
|
name: "registry type respects explicit unpack=false",
|
|
entries: []*buildflags.ExportEntry{
|
|
{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"unpack": "false",
|
|
},
|
|
},
|
|
},
|
|
wantType: "image",
|
|
wantPush: "true",
|
|
wantUnpack: "false",
|
|
},
|
|
{
|
|
name: "image type without push does not set unpack",
|
|
entries: []*buildflags.ExportEntry{
|
|
{
|
|
Type: "image",
|
|
Attrs: map[string]string{},
|
|
},
|
|
},
|
|
wantType: "image",
|
|
wantPush: "",
|
|
wantUnpack: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
exports, _, err := CreateExports(tt.entries)
|
|
require.NoError(t, err)
|
|
require.Len(t, exports, 1)
|
|
|
|
require.Equal(t, tt.wantType, exports[0].Type)
|
|
require.Equal(t, tt.wantPush, exports[0].Attrs["push"])
|
|
require.Equal(t, tt.wantUnpack, exports[0].Attrs["unpack"])
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProxyArgKeyExists(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
proxyArgs map[string]string
|
|
key string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "exact match",
|
|
proxyArgs: map[string]string{"NO_PROXY": "cli"},
|
|
key: "NO_PROXY",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "case insensitive match",
|
|
proxyArgs: map[string]string{"no_proxy": "cli"},
|
|
key: "NO_PROXY",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "no match",
|
|
proxyArgs: map[string]string{"HTTP_PROXY": "cli"},
|
|
key: "NO_PROXY",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "nil map",
|
|
proxyArgs: nil,
|
|
key: "NO_PROXY",
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.want, proxyArgKeyExists(tt.proxyArgs, tt.key))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplyPolicyCapsEnablesProxyNetwork(t *testing.T) {
|
|
p := policyWithDecision(`
|
|
package docker
|
|
|
|
decision := {
|
|
"allow": false,
|
|
"caps": {"exec.proxy": true},
|
|
}
|
|
`)
|
|
var so client.SolveOpt
|
|
|
|
err := applyPolicyCaps(context.Background(), p, buildOptsWithCaps(pb.CapExecMetaNetworkProxy), &so)
|
|
require.NoError(t, err)
|
|
require.True(t, so.ProxyNetwork)
|
|
}
|
|
|
|
func TestApplyPolicyCapsOrsProxyNetwork(t *testing.T) {
|
|
falsePolicy := policyWithDecision(`
|
|
package docker
|
|
|
|
decision := {
|
|
"allow": true,
|
|
"caps": {"exec.proxy": false},
|
|
}
|
|
`)
|
|
truePolicy := policyWithDecision(`
|
|
package docker
|
|
|
|
decision := {
|
|
"allow": true,
|
|
"caps": {"exec.proxy": true},
|
|
}
|
|
`)
|
|
var so client.SolveOpt
|
|
|
|
err := applyPolicyCaps(context.Background(), falsePolicy, buildOptsWithCaps(pb.CapExecMetaNetworkProxy), &so)
|
|
require.NoError(t, err)
|
|
require.False(t, so.ProxyNetwork)
|
|
|
|
err = applyPolicyCaps(context.Background(), truePolicy, buildOptsWithCaps(pb.CapExecMetaNetworkProxy), &so)
|
|
require.NoError(t, err)
|
|
require.True(t, so.ProxyNetwork)
|
|
}
|
|
|
|
func TestApplyPolicyCapsRequiresBuildKitCap(t *testing.T) {
|
|
p := policyWithDecision(`
|
|
package docker
|
|
|
|
decision := {
|
|
"allow": true,
|
|
"caps": {"exec.proxy": true},
|
|
}
|
|
`)
|
|
var so client.SolveOpt
|
|
|
|
err := applyPolicyCaps(context.Background(), p, buildOptsWithCaps(), &so)
|
|
require.ErrorContains(t, err, "network proxy requested by policy is not supported by the current BuildKit daemon")
|
|
require.ErrorContains(t, err, "please upgrade to version v0.31+")
|
|
require.False(t, so.ProxyNetwork)
|
|
}
|
|
|
|
func TestLoadInputsOCILayoutNamedContext(t *testing.T) {
|
|
layoutPath := t.TempDir()
|
|
|
|
idx := ociindex.NewStoreIndex(layoutPath)
|
|
manifestDigest := digest.FromString("manifest")
|
|
err := idx.Put(ocispecs.Descriptor{
|
|
MediaType: ocispecs.MediaTypeImageManifest,
|
|
Digest: manifestDigest,
|
|
Size: 1,
|
|
}, ociindex.Tag("latest"))
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
name string
|
|
ref string
|
|
wantRef ocilayout.Ref
|
|
}{
|
|
{
|
|
name: "digest only",
|
|
ref: "oci-layout://" + layoutPath + "@" + manifestDigest.String(),
|
|
wantRef: ocilayout.Ref{
|
|
Digest: manifestDigest,
|
|
},
|
|
},
|
|
{
|
|
name: "tag only",
|
|
ref: "oci-layout://" + layoutPath + ":latest",
|
|
wantRef: ocilayout.Ref{
|
|
Tag: "latest",
|
|
Digest: manifestDigest,
|
|
},
|
|
},
|
|
{
|
|
name: "tag and digest",
|
|
ref: "oci-layout://" + layoutPath + ":latest@" + manifestDigest.String(),
|
|
wantRef: ocilayout.Ref{
|
|
Tag: "latest",
|
|
Digest: manifestDigest,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
target := &client.SolveOpt{
|
|
FrontendAttrs: map[string]string{},
|
|
}
|
|
inp := &Inputs{
|
|
ContextPath: "https://example.com/context.tar.gz",
|
|
NamedContexts: map[string]NamedContext{
|
|
"proxy": {
|
|
Path: tt.ref,
|
|
},
|
|
},
|
|
}
|
|
|
|
release, err := loadInputs(context.Background(), nil, inp, testProgressWriter{}, target)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, release)
|
|
t.Cleanup(release)
|
|
|
|
attr, ok := target.FrontendAttrs["context:proxy"]
|
|
require.True(t, ok)
|
|
require.Len(t, target.OCIStores, 1)
|
|
|
|
parsed, ok, err := ocilayout.Parse(attr)
|
|
require.True(t, ok)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, parsed.Path)
|
|
assert.Equal(t, tt.wantRef.Tag, parsed.Tag)
|
|
assert.Equal(t, tt.wantRef.Digest, parsed.Digest)
|
|
target.OCIStores = nil
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPolicyProgressLoggerCloseWithErrorAfterCompletedWindow(t *testing.T) {
|
|
pw := &captureProgressWriter{}
|
|
logger := newPolicyProgressLogger(pw, "loading policies policy.rego")
|
|
|
|
logger.Log("policy decision: DENY")
|
|
logger.completeWindow(1, nil)
|
|
logger.Close(errors.New("source not allowed by policy"))
|
|
|
|
var completedErrors []string
|
|
for _, st := range pw.Statuses() {
|
|
for _, v := range st.Vertexes {
|
|
if v == nil || v.Completed == nil {
|
|
continue
|
|
}
|
|
if v.Error == "" {
|
|
continue
|
|
}
|
|
completedErrors = append(completedErrors, v.Error)
|
|
}
|
|
}
|
|
|
|
require.Equal(t, []string{"source not allowed by policy"}, completedErrors)
|
|
}
|
|
|
|
type testProgressWriter struct{}
|
|
|
|
func (testProgressWriter) Write(*client.SolveStatus) {}
|
|
|
|
func (testProgressWriter) WriteBuildRef(string, string) {}
|
|
|
|
func (testProgressWriter) ValidateLogSource(digest.Digest, any) bool { return true }
|
|
|
|
func (testProgressWriter) ClearLogSource(any) {}
|
|
|
|
var _ progress.Writer = testProgressWriter{}
|
|
|
|
type captureProgressWriter struct {
|
|
mu sync.Mutex
|
|
statuses []*client.SolveStatus
|
|
}
|
|
|
|
func (w *captureProgressWriter) Write(st *client.SolveStatus) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
w.statuses = append(w.statuses, st)
|
|
}
|
|
|
|
func (w *captureProgressWriter) Statuses() []*client.SolveStatus {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
return append([]*client.SolveStatus(nil), w.statuses...)
|
|
}
|
|
|
|
func (w *captureProgressWriter) WriteBuildRef(string, string) {}
|
|
|
|
func (w *captureProgressWriter) ValidateLogSource(digest.Digest, any) bool { return true }
|
|
|
|
func (w *captureProgressWriter) ClearLogSource(any) {}
|
|
|
|
var _ progress.Writer = (*captureProgressWriter)(nil)
|
|
|
|
func policyWithDecision(decision string) *policy.Policy {
|
|
return policy.NewPolicy(policy.Opt{
|
|
Files: []policy.File{{
|
|
Filename: "policy.rego",
|
|
Data: []byte(decision),
|
|
}},
|
|
})
|
|
}
|
|
|
|
func buildOptsWithCaps(caps ...apicaps.CapID) gateway.BuildOpts {
|
|
out := make([]*apicaps.PBCap, 0, len(caps))
|
|
for _, c := range caps {
|
|
out = append(out, &apicaps.PBCap{
|
|
ID: string(c),
|
|
Enabled: true,
|
|
})
|
|
}
|
|
return gateway.BuildOpts{
|
|
LLBCaps: pb.Caps.CapSet(out),
|
|
}
|
|
}
|