Skip to content

Commit 8c20682

Browse files
committed
policysession: dynamic source policy support
Add support for dynamic source policies via client session. Client session can allow or deny specific source or ask additional metadata information via sourcemetaresolver if that is needed to make the decision. Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
1 parent fe65d5f commit 8c20682

File tree

18 files changed

+2284
-14
lines changed

18 files changed

+2284
-14
lines changed

api/services/control/control.pb.go

Lines changed: 11 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/services/control/control.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ message SolveRequest {
7676
moby.buildkit.v1.sourcepolicy.Policy SourcePolicy = 12;
7777
repeated Exporter Exporters = 13;
7878
bool EnableSessionExporter = 14;
79+
string SourcePolicySession = 15;
7980
}
8081

8182
message CacheOptions {

api/services/control/control_vtproto.pb.go

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/client_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
237237
testRunValidExitCodes,
238238
testFileOpSymlink,
239239
testMetadataOnlyLocal,
240+
testSourcePolicySession,
240241
}
241242

242243
func TestIntegration(t *testing.T) {

client/policy_test.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"io"
7+
"runtime"
8+
"testing"
9+
10+
"github.com/moby/buildkit/client/llb"
11+
pb "github.com/moby/buildkit/frontend/gateway/pb"
12+
sourcepolicpb "github.com/moby/buildkit/sourcepolicy/pb"
13+
"github.com/moby/buildkit/sourcepolicy/policysession"
14+
"github.com/moby/buildkit/util/testutil/integration"
15+
"github.com/moby/buildkit/util/testutil/workers"
16+
digest "github.com/opencontainers/go-digest"
17+
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
18+
"github.com/pkg/errors"
19+
"github.com/stretchr/testify/require"
20+
)
21+
22+
func testSourcePolicySession(t *testing.T, sb integration.Sandbox) {
23+
requiresLinux(t)
24+
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter)
25+
26+
ctx := sb.Context()
27+
28+
c, err := New(ctx, sb.Address())
29+
require.NoError(t, err)
30+
defer c.Close()
31+
32+
type tcase struct {
33+
name string
34+
state func() llb.State
35+
callbacks []policysession.PolicyCallback
36+
expectedError string
37+
}
38+
39+
tcases := []tcase{
40+
{
41+
name: "basic alpine",
42+
state: func() llb.State { return llb.Image("alpine") },
43+
callbacks: []policysession.PolicyCallback{
44+
func(ctx context.Context, req *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *pb.ResolveSourceMetaRequest, error) {
45+
require.Equal(t, runtime.GOOS, req.Platform.OS)
46+
require.Equal(t, runtime.GOARCH, req.Platform.Architecture)
47+
48+
require.Equal(t, "docker-image://docker.io/library/alpine:latest", req.Source.Source.Identifier)
49+
return &policysession.DecisionResponse{
50+
Action: sourcepolicpb.PolicyAction_ALLOW,
51+
}, nil, nil
52+
},
53+
},
54+
},
55+
{
56+
name: "alpine with attrs",
57+
state: func() llb.State { return llb.Image("alpine", llb.WithLayerLimit(1)) },
58+
callbacks: []policysession.PolicyCallback{
59+
func(ctx context.Context, req *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *pb.ResolveSourceMetaRequest, error) {
60+
require.Equal(t, "docker-image://docker.io/library/alpine:latest", req.Source.Source.Identifier)
61+
require.Equal(t, map[string]string{
62+
"image.layerlimit": "1",
63+
}, req.Source.Source.Attrs)
64+
return &policysession.DecisionResponse{
65+
Action: sourcepolicpb.PolicyAction_ALLOW,
66+
}, nil, nil
67+
},
68+
},
69+
},
70+
{
71+
name: "deny alpine",
72+
state: func() llb.State { return llb.Image("alpine") },
73+
callbacks: []policysession.PolicyCallback{
74+
func(ctx context.Context, req *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *pb.ResolveSourceMetaRequest, error) {
75+
require.Equal(t, "docker-image://docker.io/library/alpine:latest", req.Source.Source.Identifier)
76+
return nil, nil, errors.New("policy denied")
77+
},
78+
},
79+
expectedError: "policy denied",
80+
},
81+
{
82+
name: "alpine with digest policy",
83+
state: func() llb.State { return llb.Image("alpine") },
84+
callbacks: []policysession.PolicyCallback{
85+
func(ctx context.Context, req *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *pb.ResolveSourceMetaRequest, error) {
86+
require.Equal(t, "docker-image://docker.io/library/alpine:latest", req.Source.Source.Identifier)
87+
require.Nil(t, req.Source.Image)
88+
return nil, &pb.ResolveSourceMetaRequest{
89+
Source: req.Source.Source,
90+
Platform: req.Platform,
91+
// TODO: resolveMode
92+
}, nil
93+
},
94+
func(ctx context.Context, req *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *pb.ResolveSourceMetaRequest, error) {
95+
require.Equal(t, "docker-image://docker.io/library/alpine:latest", req.Source.Source.Identifier)
96+
require.NotEmpty(t, req.Source.Image.Digest)
97+
_, err := digest.Parse(req.Source.Image.Digest)
98+
require.NoError(t, err)
99+
require.NotEmpty(t, req.Source.Image.Config)
100+
var cfg ocispecs.Image
101+
err = json.Unmarshal(req.Source.Image.Config, &cfg)
102+
require.NoError(t, err)
103+
require.NotEmpty(t, cfg.RootFS)
104+
return &policysession.DecisionResponse{
105+
Action: sourcepolicpb.PolicyAction_ALLOW,
106+
}, nil, nil
107+
},
108+
},
109+
},
110+
}
111+
112+
for _, tc := range tcases {
113+
t.Run(tc.name, func(t *testing.T) {
114+
st := tc.state()
115+
def, err := st.Marshal(ctx)
116+
require.NoError(t, err)
117+
118+
callCounter := 0
119+
120+
p := policysession.NewPolicyProvider(func(ctx context.Context, req *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *pb.ResolveSourceMetaRequest, error) {
121+
if callCounter >= len(tc.callbacks) {
122+
return nil, nil, errors.Errorf("too many calls to policy callback %d", callCounter)
123+
}
124+
cb := tc.callbacks[callCounter]
125+
callCounter++
126+
return cb(ctx, req)
127+
})
128+
129+
_, err = c.Solve(ctx, def, SolveOpt{
130+
SourcePolicyProvider: p,
131+
Exports: []ExportEntry{
132+
{
133+
Type: ExporterOCI,
134+
Output: fixedWriteCloser(nopWriteCloser{io.Discard}),
135+
},
136+
},
137+
}, nil)
138+
if tc.expectedError != "" {
139+
require.Error(t, err)
140+
require.Contains(t, err.Error(), tc.expectedError)
141+
return
142+
}
143+
require.NoError(t, err)
144+
145+
require.Equal(t, len(tc.callbacks), callCounter, "not all policy callbacks were called")
146+
})
147+
}
148+
}

client/solve.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ type SolveOpt struct {
5151
SessionPreInitialized bool // TODO: refactor to better session syncing
5252
Internal bool
5353
SourcePolicy *spb.Policy
54+
SourcePolicyProvider session.Attachable
5455
Ref string
5556
}
5657

@@ -219,6 +220,10 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG
219220
s.Allow(filesync.NewFSSyncTarget(syncTargets...))
220221
}
221222

223+
if opt.SourcePolicyProvider != nil {
224+
s.Allow(opt.SourcePolicyProvider)
225+
}
226+
222227
eg.Go(func() error {
223228
sd := c.sessionDialer
224229
if sd == nil {
@@ -275,7 +280,7 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG
275280
})
276281
}
277282

278-
resp, err := c.ControlClient().Solve(ctx, &controlapi.SolveRequest{
283+
sopt := &controlapi.SolveRequest{
279284
Ref: ref,
280285
Definition: pbd,
281286
Exporters: exports,
@@ -290,7 +295,12 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG
290295
Entitlements: slices.Clone(opt.AllowedEntitlements),
291296
Internal: opt.Internal,
292297
SourcePolicy: opt.SourcePolicy,
293-
})
298+
}
299+
if opt.SourcePolicyProvider != nil {
300+
sopt.SourcePolicySession = s.ID()
301+
}
302+
303+
resp, err := c.ControlClient().Solve(ctx, sopt)
294304
if err != nil {
295305
return errors.Wrap(err, "failed to solve")
296306
}

control/control.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ func (c *Controller) Solve(ctx context.Context, req *controlapi.SolveRequest) (*
539539
Exporters: expis,
540540
CacheExporters: cacheExporters,
541541
EnableSessionExporter: req.EnableSessionExporter,
542-
}, entitlementsFromPB(req.Entitlements), procs, req.Internal, req.SourcePolicy)
542+
}, entitlementsFromPB(req.Entitlements), procs, req.Internal, req.SourcePolicy, req.SourcePolicySession)
543543
if err != nil {
544544
return nil, err
545545
}

0 commit comments

Comments
 (0)