Merge pull request #3653 from tonistiigi/util-sourcemeta-resolver

policy: separate sourcemetaresolver utility to reusable pkg
This commit is contained in:
CrazyMax
2026-02-17 10:11:36 +01:00
committed by GitHub
6 changed files with 350 additions and 132 deletions
+5 -16
View File
@@ -15,6 +15,7 @@ import (
"github.com/docker/buildx/builder" "github.com/docker/buildx/builder"
"github.com/docker/buildx/policy" "github.com/docker/buildx/policy"
"github.com/docker/buildx/util/confutil" "github.com/docker/buildx/util/confutil"
"github.com/docker/buildx/util/sourcemeta"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/moby/buildkit/client/llb/sourceresolver" "github.com/moby/buildkit/client/llb/sourceresolver"
"github.com/moby/buildkit/frontend/dockerui" "github.com/moby/buildkit/frontend/dockerui"
@@ -96,11 +97,8 @@ func runEval(ctx context.Context, dockerCli command.Cli, source string, opts eva
OS: defaultPlatform.OS, OS: defaultPlatform.OS,
Variant: defaultPlatform.Variant, Variant: defaultPlatform.Variant,
} }
openClient, release, err := gatewayClientFactory(c) metaResolver := sourcemeta.NewResolver(c)
if err != nil { defer metaResolver.Close()
return err
}
defer release()
platform := &pb.Platform{ platform := &pb.Platform{
Architecture: p.Architecture, Architecture: p.Architecture,
@@ -148,13 +146,8 @@ func runEval(ctx context.Context, dockerCli command.Cli, source string, opts eva
if err := policy.AddUnknowns(req, toReload); err != nil { if err := policy.AddUnknowns(req, toReload); err != nil {
return err return err
} }
gwClient, err := openClient(ctx)
if err != nil {
return err
}
opt := sourceResolverOpt(req, &p) opt := sourceResolverOpt(req, &p)
resp, err := gwClient.ResolveSourceMetadata(ctx, src, opt) resp, err := metaResolver.ResolveSourceMetadata(ctx, src, opt)
if err != nil { if err != nil {
return err return err
} }
@@ -240,12 +233,8 @@ func runEval(ctx context.Context, dockerCli command.Cli, source string, opts eva
return evalDecisionError(decision) return evalDecisionError(decision)
} }
gwClient, err := openClient(ctx)
if err != nil {
return err
}
opt := sourceResolverOpt(next, &p) opt := sourceResolverOpt(next, &p)
resp, err := gwClient.ResolveSourceMetadata(ctx, src, opt) resp, err := metaResolver.ResolveSourceMetadata(ctx, src, opt)
if err != nil { if err != nil {
return err return err
} }
-80
View File
@@ -1,80 +0,0 @@
package policy
import (
"context"
"errors"
"sync"
"sync/atomic"
"github.com/moby/buildkit/client"
gwclient "github.com/moby/buildkit/frontend/gateway/client"
)
type gatewayClientOpener func(context.Context) (gwclient.Client, error)
func gatewayClientFactory(c *client.Client) (gatewayClientOpener, func() error, error) {
var (
once sync.Once
releaseOnce sync.Once
started atomic.Bool
ready = make(chan gwclient.Client, 1)
done = make(chan error, 1)
openErr error
releaseErr error
gwClient gwclient.Client
cancel context.CancelCauseFunc
)
open := func(ctx context.Context) (gwclient.Client, error) {
once.Do(func() {
started.Store(true)
buildCtx, cancelFn := context.WithCancelCause(ctx)
cancel = cancelFn
go func() {
_, err := c.Build(buildCtx, client.SolveOpt{Internal: true}, "buildx", func(ctx context.Context, c gwclient.Client) (*gwclient.Result, error) {
ready <- c
<-buildCtx.Done()
return nil, context.Cause(buildCtx)
}, nil)
done <- err
}()
select {
case gwClient = <-ready:
case err := <-done:
if err == nil {
err = errors.New("gateway build finished without a client")
}
openErr = err
case <-ctx.Done():
openErr = context.Cause(ctx)
cancelFn(openErr)
}
})
if openErr != nil {
return nil, openErr
}
return gwClient, nil
}
release := func() error {
releaseOnce.Do(func() {
if !started.Load() {
return
}
if cancel != nil {
cancel(context.Canceled)
}
err := <-done
if err == nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return
}
releaseErr = err
})
return releaseErr
}
return open, release, nil
}
+21 -31
View File
@@ -14,6 +14,7 @@ import (
"github.com/docker/buildx/policy" "github.com/docker/buildx/policy"
"github.com/docker/buildx/util/cobrautil" "github.com/docker/buildx/util/cobrautil"
"github.com/docker/buildx/util/confutil" "github.com/docker/buildx/util/confutil"
"github.com/docker/buildx/util/sourcemeta"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
gwpb "github.com/moby/buildkit/frontend/gateway/pb" gwpb "github.com/moby/buildkit/frontend/gateway/pb"
"github.com/moby/buildkit/solver/pb" "github.com/moby/buildkit/solver/pb"
@@ -30,9 +31,9 @@ func testCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
resolver := newPolicyTestResolver(dockerCli, rootOpts.Builder) optionsProvider := newPolicyTestOptionsProvider(dockerCli, rootOpts.Builder)
opts.Resolver = resolver.Options() opts.Provider = optionsProvider.TestOptionsProvider()
defer resolver.Close() defer optionsProvider.Close()
return runTest(cmd.Context(), cmd.OutOrStdout(), args[0], opts) return runTest(cmd.Context(), cmd.OutOrStdout(), args[0], opts)
}, },
} }
@@ -114,63 +115,58 @@ func withInputPrefix(keys []string) []string {
return out return out
} }
type policyTestResolver struct { type policyTestOptionsProvider struct {
dockerCli command.Cli dockerCli command.Cli
builderName *string builderName *string
once sync.Once once sync.Once
platform *ocispecs.Platform platform *ocispecs.Platform
openClient gatewayClientOpener metaResolver *sourcemeta.Resolver
release func() error err error
err error
} }
func newPolicyTestResolver(dockerCli command.Cli, builderName *string) *policyTestResolver { func newPolicyTestOptionsProvider(dockerCli command.Cli, builderName *string) *policyTestOptionsProvider {
return &policyTestResolver{ return &policyTestOptionsProvider{
dockerCli: dockerCli, dockerCli: dockerCli,
builderName: builderName, builderName: builderName,
} }
} }
func (r *policyTestResolver) Options() *policy.TestResolver { func (r *policyTestOptionsProvider) TestOptionsProvider() *policy.TestOptionsProvider {
return &policy.TestResolver{ return &policy.TestOptionsProvider{
Resolve: r.Resolve, Resolve: r.Resolve,
Platform: r.Platform, Platform: r.Platform,
VerifierProvider: policy.SignatureVerifier(confutil.NewConfig(r.dockerCli)), VerifierProvider: policy.SignatureVerifier(confutil.NewConfig(r.dockerCli)),
} }
} }
func (r *policyTestResolver) Close() error { func (r *policyTestOptionsProvider) Close() error {
if r.release == nil { if r.metaResolver == nil {
return nil return nil
} }
return r.release() return r.metaResolver.Close()
} }
func (r *policyTestResolver) Platform(ctx context.Context) (*ocispecs.Platform, error) { func (r *policyTestOptionsProvider) Platform(ctx context.Context) (*ocispecs.Platform, error) {
if err := r.init(ctx); err != nil { if err := r.init(ctx); err != nil {
return nil, err return nil, err
} }
return r.platform, nil return r.platform, nil
} }
func (r *policyTestResolver) Resolve(ctx context.Context, source *pb.SourceOp, req *gwpb.ResolveSourceMetaRequest) (*gwpb.ResolveSourceMetaResponse, error) { func (r *policyTestOptionsProvider) Resolve(ctx context.Context, source *pb.SourceOp, req *gwpb.ResolveSourceMetaRequest) (*gwpb.ResolveSourceMetaResponse, error) {
if err := r.init(ctx); err != nil { if err := r.init(ctx); err != nil {
return nil, err return nil, err
} }
gwClient, err := r.openClient(ctx)
if err != nil {
return nil, err
}
opt := sourceResolverOpt(req, r.platform) opt := sourceResolverOpt(req, r.platform)
resp, err := gwClient.ResolveSourceMetadata(ctx, source, opt) resp, err := r.metaResolver.ResolveSourceMetadata(ctx, source, opt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return buildSourceMetaResponse(resp), nil return buildSourceMetaResponse(resp), nil
} }
func (r *policyTestResolver) init(ctx context.Context) error { func (r *policyTestOptionsProvider) init(ctx context.Context) error {
r.once.Do(func() { r.once.Do(func() {
bopts := []builder.Option{} bopts := []builder.Option{}
if r.builderName != nil { if r.builderName != nil {
@@ -208,13 +204,7 @@ func (r *policyTestResolver) init(ctx context.Context) error {
OS: defaultPlatform.OS, OS: defaultPlatform.OS,
Variant: defaultPlatform.Variant, Variant: defaultPlatform.Variant,
} }
openClient, release, err := gatewayClientFactory(c) r.metaResolver = sourcemeta.NewResolver(c)
if err != nil {
r.err = err
return
}
r.openClient = openClient
r.release = release
}) })
return r.err return r.err
} }
+5 -5
View File
@@ -25,7 +25,7 @@ type TestOptions struct {
Run string Run string
Filename string Filename string
Root fs.StatFS Root fs.StatFS
Resolver *TestResolver Provider *TestOptionsProvider
} }
type TestSummary struct { type TestSummary struct {
@@ -50,7 +50,7 @@ type testDef struct {
PkgPath string PkgPath string
} }
type TestResolver struct { type TestOptionsProvider struct {
Resolve func(context.Context, *pb.SourceOp, *gwpb.ResolveSourceMetaRequest) (*gwpb.ResolveSourceMetaResponse, error) Resolve func(context.Context, *pb.SourceOp, *gwpb.ResolveSourceMetaRequest) (*gwpb.ResolveSourceMetaResponse, error)
Platform func(context.Context) (*ocispecs.Platform, error) Platform func(context.Context) (*ocispecs.Platform, error)
VerifierProvider PolicyVerifierProvider VerifierProvider PolicyVerifierProvider
@@ -286,8 +286,8 @@ func runPolicyTest(ctx context.Context, policyModules map[string]*ast.Module, te
return result, err return result, err
} }
effectiveInput := input effectiveInput := input
if opts.Resolver != nil { if opts.Provider != nil {
resolvedInput, ok, err := resolveTestInput(ctx, policyFiles, opts.Resolver, policyPackageModules, input, fsProvider) resolvedInput, ok, err := resolveTestInput(ctx, policyFiles, opts.Provider, policyPackageModules, input, fsProvider)
if err != nil { if err != nil {
return result, err return result, err
} }
@@ -397,7 +397,7 @@ func stateFromInput(input *Input) *state {
return st return st
} }
func resolveTestInput(ctx context.Context, files []File, resolver *TestResolver, policyModules []*ast.Module, input *Input, fsProvider func() (fs.StatFS, func() error, error)) (*Input, bool, error) { func resolveTestInput(ctx context.Context, files []File, resolver *TestOptionsProvider, policyModules []*ast.Module, input *Input, fsProvider func() (fs.StatFS, func() error, error)) (*Input, bool, error) {
if resolver == nil { if resolver == nil {
return nil, false, nil return nil, false, nil
} }
+126
View File
@@ -0,0 +1,126 @@
package sourcemeta
import (
"context"
"errors"
"sync"
"sync/atomic"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/llb/sourceresolver"
gwclient "github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/solver/pb"
)
var _ sourceresolver.MetaResolver = &Resolver{}
type Resolver struct {
startOnce sync.Once
closeOnce sync.Once
started atomic.Bool
mu sync.Mutex
ready chan sourceresolver.MetaResolver
done chan struct{}
openErr error
doneErr error
cancel context.CancelCauseFunc
metaResolver sourceresolver.MetaResolver
run func(context.Context, chan<- sourceresolver.MetaResolver) error
closeErr error
}
func NewResolver(c *client.Client) *Resolver {
return newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
_, err := c.Build(ctx, client.SolveOpt{Internal: true}, "buildx", func(ctx context.Context, gw gwclient.Client) (*gwclient.Result, error) {
ready <- gw
<-ctx.Done()
return nil, context.Cause(ctx)
}, nil)
return err
})
}
func newWithRun(run func(context.Context, chan<- sourceresolver.MetaResolver) error) *Resolver {
return &Resolver{
ready: make(chan sourceresolver.MetaResolver, 1),
done: make(chan struct{}),
run: run,
}
}
func (r *Resolver) ResolveSourceMetadata(ctx context.Context, op *pb.SourceOp, opt sourceresolver.Opt) (*sourceresolver.MetaResponse, error) {
mr, err := r.open(ctx)
if err != nil {
return nil, err
}
return mr.ResolveSourceMetadata(ctx, op, opt)
}
func (r *Resolver) Close() error {
r.closeOnce.Do(func() {
if !r.started.Load() {
return
}
if r.cancel != nil {
r.cancel(context.Canceled)
}
<-r.done
err := r.doneErr
if err == nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return
}
r.closeErr = err
})
return r.closeErr
}
func (r *Resolver) open(ctx context.Context) (sourceresolver.MetaResolver, error) {
r.startOnce.Do(func() {
r.started.Store(true)
buildCtx, cancel := context.WithCancelCause(context.Background())
r.cancel = cancel
go func() {
r.doneErr = r.run(buildCtx, r.ready)
close(r.done)
}()
})
for {
r.mu.Lock()
if r.metaResolver != nil {
mr := r.metaResolver
r.mu.Unlock()
return mr, nil
}
if r.openErr != nil {
err := r.openErr
r.mu.Unlock()
return nil, err
}
r.mu.Unlock()
select {
case mr := <-r.ready:
r.mu.Lock()
if r.metaResolver == nil {
r.metaResolver = mr
}
r.mu.Unlock()
case <-r.done:
r.mu.Lock()
if r.metaResolver == nil && r.openErr == nil {
err := r.doneErr
if err == nil {
err = errors.New("gateway build finished without a source metadata resolver")
}
r.openErr = err
}
r.mu.Unlock()
case <-ctx.Done():
return nil, context.Cause(ctx)
}
}
}
+193
View File
@@ -0,0 +1,193 @@
package sourcemeta
import (
"context"
"errors"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/moby/buildkit/client/llb/sourceresolver"
"github.com/moby/buildkit/solver/pb"
"github.com/stretchr/testify/require"
)
type fakeMetaResolver struct {
calls atomic.Int32
resp *sourceresolver.MetaResponse
err error
}
func (f *fakeMetaResolver) ResolveSourceMetadata(ctx context.Context, op *pb.SourceOp, opt sourceresolver.Opt) (*sourceresolver.MetaResponse, error) {
f.calls.Add(1)
return f.resp, f.err
}
func TestResolverCloseNoopBeforeResolve(t *testing.T) {
t.Parallel()
var called atomic.Int32
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
called.Add(1)
return nil
})
require.NoError(t, r.Close())
require.EqualValues(t, 0, called.Load())
}
func TestResolverResolveOpensOnce(t *testing.T) {
t.Parallel()
var runs atomic.Int32
mr := &fakeMetaResolver{resp: &sourceresolver.MetaResponse{}}
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
runs.Add(1)
ready <- mr
<-ctx.Done()
return context.Cause(ctx)
})
op := &pb.SourceOp{}
_, err := r.ResolveSourceMetadata(t.Context(), op, sourceresolver.Opt{})
require.NoError(t, err)
_, err = r.ResolveSourceMetadata(t.Context(), op, sourceresolver.Opt{})
require.NoError(t, err)
require.EqualValues(t, 1, runs.Load())
require.EqualValues(t, 2, mr.calls.Load())
require.NoError(t, r.Close())
}
func TestResolverCloseAfterOpenCancelsBuild(t *testing.T) {
t.Parallel()
var canceled atomic.Bool
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
ready <- &fakeMetaResolver{resp: &sourceresolver.MetaResponse{}}
<-ctx.Done()
canceled.Store(true)
return context.Cause(ctx)
})
_, err := r.ResolveSourceMetadata(t.Context(), &pb.SourceOp{}, sourceresolver.Opt{})
require.NoError(t, err)
require.NoError(t, r.Close())
require.True(t, canceled.Load())
}
func TestResolverOpenFailureIsSticky(t *testing.T) {
t.Parallel()
expected := errors.New("boom")
var runs atomic.Int32
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
runs.Add(1)
return expected
})
_, err := r.ResolveSourceMetadata(t.Context(), &pb.SourceOp{}, sourceresolver.Opt{})
require.ErrorIs(t, err, expected)
_, err = r.ResolveSourceMetadata(t.Context(), &pb.SourceOp{}, sourceresolver.Opt{})
require.ErrorIs(t, err, expected)
require.EqualValues(t, 1, runs.Load())
require.ErrorIs(t, r.Close(), expected)
}
func TestResolverCloseIgnoresTerminalContextErrors(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
}{
{name: "canceled", err: context.Canceled},
{name: "deadline", err: context.DeadlineExceeded},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
return tc.err
})
_, err := r.ResolveSourceMetadata(t.Context(), &pb.SourceOp{}, sourceresolver.Opt{})
require.ErrorIs(t, err, tc.err)
require.NoError(t, r.Close())
})
}
}
func TestResolverConcurrentResolveUsesSingleOpen(t *testing.T) {
t.Parallel()
var runs atomic.Int32
mr := &fakeMetaResolver{resp: &sourceresolver.MetaResponse{}}
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
runs.Add(1)
ready <- mr
<-ctx.Done()
return context.Cause(ctx)
})
const n = 16
errCh := make(chan error, n)
var wg sync.WaitGroup
wg.Add(n)
for range n {
go func() {
defer wg.Done()
_, err := r.ResolveSourceMetadata(t.Context(), &pb.SourceOp{}, sourceresolver.Opt{})
errCh <- err
}()
}
wg.Wait()
close(errCh)
for err := range errCh {
require.NoError(t, err)
}
require.EqualValues(t, 1, runs.Load())
require.EqualValues(t, n, mr.calls.Load())
done := make(chan struct{})
closeErr := make(chan error, 1)
go func() {
defer close(done)
closeErr <- r.Close()
}()
select {
case <-done:
require.NoError(t, <-closeErr)
case <-time.After(2 * time.Second):
t.Fatal("close timed out")
}
}
func TestResolverFirstCanceledContextDoesNotPoisonFutureCalls(t *testing.T) {
t.Parallel()
mr := &fakeMetaResolver{resp: &sourceresolver.MetaResponse{}}
started := make(chan struct{})
release := make(chan struct{})
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
close(started)
<-release
ready <- mr
<-ctx.Done()
return context.Cause(ctx)
})
canceledCtx, cancel := context.WithCancelCause(t.Context())
cancel(context.Canceled)
_, err := r.ResolveSourceMetadata(canceledCtx, &pb.SourceOp{}, sourceresolver.Opt{})
require.ErrorIs(t, err, context.Canceled)
<-started
close(release)
_, err = r.ResolveSourceMetadata(t.Context(), &pb.SourceOp{}, sourceresolver.Opt{})
require.NoError(t, err)
require.NoError(t, r.Close())
}