Skip to content

Commit 7434965

Browse files
committed
frontend: fix gwclient.Client wrappers losing CurrentFrontend interface
Add internal/gwutil package with WithCurrentFrontend helper that preserves CurrentFrontend and SpecLoader interfaces when wrapping a gwclient.Client. This replaces ad-hoc manual CurrentFrontend() forwarding methods that were fragile and easy to forget when adding new wrappers. Update all wrapping sites (setClientOptOption, maybeSetDalecTargetKey, WithDefaultPlatform, and test wrappers) to use gwutil.WithCurrentFrontend. Signed-off-by: Brian Goff <cpuguy83@gmail.com>
1 parent 2c2d065 commit 7434965

File tree

4 files changed

+100
-28
lines changed

4 files changed

+100
-28
lines changed

frontend/build.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
1616
"github.com/pkg/errors"
1717
"github.com/project-dalec/dalec"
18+
"github.com/project-dalec/dalec/internal/gwutil"
1819
)
1920

2021
type LoadConfig struct {
@@ -23,10 +24,6 @@ type LoadConfig struct {
2324

2425
type LoadOpt func(*LoadConfig)
2526

26-
type frontendClient interface {
27-
CurrentFrontend() (*llb.State, error)
28-
}
29-
3027
func WithAllowArgs(args ...string) LoadOpt {
3128
return func(cfg *LoadConfig) {
3229
set := make(map[string]struct{}, len(args))
@@ -199,14 +196,16 @@ func WithDefaultPlatform(platform ocispecs.Platform, build gwclient.BuildFunc) g
199196
if client.BuildOpts().Opts["platform"] != "" {
200197
return build(ctx, client)
201198
}
202-
client = &clientWithPlatform{
199+
client = gwutil.WithCurrentFrontend(client, &clientWithPlatform{
203200
Client: client,
204201
platform: &platform,
205-
}
202+
})
206203
return build(ctx, client)
207204
}
208205
}
209206

207+
var _ gwclient.Client = (*clientWithPlatform)(nil)
208+
210209
type clientWithPlatform struct {
211210
gwclient.Client
212211
platform *ocispecs.Platform
@@ -219,7 +218,7 @@ func (c *clientWithPlatform) BuildOpts() gwclient.BuildOpts {
219218
}
220219

221220
func GetCurrentFrontend(client gwclient.Client) (llb.State, error) {
222-
f, err := client.(frontendClient).CurrentFrontend()
221+
f, err := client.(gwutil.CurrentFrontend).CurrentFrontend()
223222
if err != nil {
224223
return llb.Scratch(), err
225224
}

frontend/client.go

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import (
88

99
"github.com/containerd/platforms"
1010
"github.com/goccy/go-yaml"
11-
"github.com/moby/buildkit/client/llb"
1211
"github.com/moby/buildkit/frontend/dockerui"
1312
gwclient "github.com/moby/buildkit/frontend/gateway/client"
1413
"github.com/project-dalec/dalec"
14+
"github.com/project-dalec/dalec/internal/gwutil"
1515
)
1616

1717
const (
@@ -39,15 +39,8 @@ func (err *noSuchHandlerError) Error() string {
3939
return fmt.Sprintf("no such handler for target %q: available targets: %s", err.Target, strings.Join(err.Available, ", "))
4040
}
4141

42-
// CurrentFrontend is an interface typically implemented by a [gwclient.Client].
43-
// This is used to get the rootfs of the current frontend.
44-
type CurrentFrontend interface {
45-
CurrentFrontend() (*llb.State, error)
46-
}
47-
4842
var (
4943
_ gwclient.Client = (*clientWithCustomOpts)(nil)
50-
_ CurrentFrontend = (*clientWithCustomOpts)(nil)
5144
)
5245

5346
type clientWithCustomOpts struct {
@@ -59,20 +52,16 @@ func (d *clientWithCustomOpts) BuildOpts() gwclient.BuildOpts {
5952
return d.opts
6053
}
6154

62-
func (d *clientWithCustomOpts) CurrentFrontend() (*llb.State, error) {
63-
return d.Client.(CurrentFrontend).CurrentFrontend()
64-
}
65-
66-
func setClientOptOption(client gwclient.Client, extraOpts map[string]string) *clientWithCustomOpts {
55+
func setClientOptOption(client gwclient.Client, extraOpts map[string]string) gwclient.Client {
6756
opts := client.BuildOpts()
6857

6958
for key, value := range extraOpts {
7059
opts.Opts[key] = value
7160
}
72-
return &clientWithCustomOpts{
61+
return gwutil.WithCurrentFrontend(client, &clientWithCustomOpts{
7362
Client: client,
7463
opts: opts,
75-
}
64+
})
7665
}
7766

7867
func maybeSetDalecTargetKey(client gwclient.Client, key string) gwclient.Client {
@@ -82,13 +71,13 @@ func maybeSetDalecTargetKey(client gwclient.Client, key string) gwclient.Client
8271
return client
8372
}
8473

85-
// optimization to help prevent unnecessary grpc requests
74+
// Optimization to help prevent unnecessary grpc requests.
8675
// The gateway client will make a grpc request to get the build opts from the gateway.
8776
// This just caches those opts locally.
8877
// If the client is already a clientWithCustomOpts, then the opts are already cached.
8978
if _, ok := client.(*clientWithCustomOpts); !ok {
9079
// this forces the client to use our cached opts from above
91-
client = &clientWithCustomOpts{opts: opts, Client: client}
80+
client = gwutil.WithCurrentFrontend(client, &clientWithCustomOpts{opts: opts, Client: client})
9281
}
9382
return setClientOptOption(client, map[string]string{keyTopLevelTarget: key, "build-arg:" + dalec.KeyDalecTarget: key})
9483
}

internal/gwutil/gwutil.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package gwutil
2+
3+
import (
4+
"context"
5+
6+
"github.com/moby/buildkit/client/llb"
7+
gwclient "github.com/moby/buildkit/frontend/gateway/client"
8+
"github.com/project-dalec/dalec"
9+
)
10+
11+
// CurrentFrontend is an interface typically implemented by a [gwclient.Client].
12+
// This is used to get the rootfs of the current frontend.
13+
type CurrentFrontend interface {
14+
CurrentFrontend() (*llb.State, error)
15+
}
16+
17+
// SpecLoader is an optional interface that a [gwclient.Client] wrapper can
18+
// implement to provide a cached or pre-loaded dalec spec. This avoids
19+
// redundant loads when many callers need the spec from the same client.
20+
type SpecLoader interface {
21+
LoadSpec(context.Context) (*dalec.Spec, error)
22+
}
23+
24+
// WithCurrentFrontend wraps a [gwclient.Client] to ensure that [CurrentFrontend]
25+
// is not lost when the client is wrapped.
26+
//
27+
// If inner does not implement [CurrentFrontend], wrapper is returned as-is.
28+
// Otherwise, if either the wrapper or the inner client implements [SpecLoader],
29+
// that interface is also preserved on the returned client (wrapper is preferred
30+
// over inner).
31+
func WithCurrentFrontend(inner gwclient.Client, wrapper gwclient.Client) gwclient.Client {
32+
cf, ok := inner.(CurrentFrontend)
33+
if !ok {
34+
return wrapper
35+
}
36+
37+
w := &clientWithCurrentFrontend{Client: wrapper, cf: cf}
38+
39+
// Preserve SpecLoader if present on either wrapper or inner.
40+
// Prefer wrapper (it's the more specific layer).
41+
switch {
42+
case isSpecLoader(wrapper):
43+
return &clientWithCurrentFrontendAndSpecLoader{clientWithCurrentFrontend: w, sl: wrapper.(SpecLoader)}
44+
case isSpecLoader(inner):
45+
return &clientWithCurrentFrontendAndSpecLoader{clientWithCurrentFrontend: w, sl: inner.(SpecLoader)}
46+
}
47+
48+
return w
49+
}
50+
51+
func isSpecLoader(c gwclient.Client) bool {
52+
_, ok := c.(SpecLoader)
53+
return ok
54+
}
55+
56+
// clientWithCurrentFrontend wraps a gwclient.Client and forwards
57+
// CurrentFrontend calls to the inner client.
58+
type clientWithCurrentFrontend struct {
59+
gwclient.Client
60+
cf CurrentFrontend
61+
}
62+
63+
var _ gwclient.Client = (*clientWithCurrentFrontend)(nil)
64+
65+
func (c *clientWithCurrentFrontend) CurrentFrontend() (*llb.State, error) {
66+
return c.cf.CurrentFrontend()
67+
}
68+
69+
// clientWithCurrentFrontendAndSpecLoader extends clientWithCurrentFrontend
70+
// with SpecLoader forwarding.
71+
type clientWithCurrentFrontendAndSpecLoader struct {
72+
*clientWithCurrentFrontend
73+
sl SpecLoader
74+
}
75+
76+
var (
77+
_ gwclient.Client = (*clientWithCurrentFrontendAndSpecLoader)(nil)
78+
_ SpecLoader = (*clientWithCurrentFrontendAndSpecLoader)(nil)
79+
)
80+
81+
func (c *clientWithCurrentFrontendAndSpecLoader) LoadSpec(ctx context.Context) (*dalec.Spec, error) {
82+
return c.sl.LoadSpec(ctx)
83+
}

test/testenv/buildx.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/moby/buildkit/solver/pb"
2626
spb "github.com/moby/buildkit/sourcepolicy/pb"
2727
pkgerrors "github.com/pkg/errors"
28+
"github.com/project-dalec/dalec/internal/gwutil"
2829
"github.com/project-dalec/dalec/sessionutil/socketprovider"
2930
"google.golang.org/grpc"
3031
"google.golang.org/grpc/codes"
@@ -398,7 +399,7 @@ func (b *BuildxEnv) RunTest(ctx context.Context, t *testing.T, f TestFunc, opts
398399
}
399400

400401
_, err = c.Build(ctx, so, "", func(ctx context.Context, gwc gwclient.Client) (*gwclient.Result, error) {
401-
gwc = &clientForceDalecWithInput{gwc}
402+
gwc = gwutil.WithCurrentFrontend(gwc, &clientForceDalecWithInput{gwc})
402403

403404
b.mu.Lock()
404405
for id, f := range b.refs {
@@ -441,12 +442,12 @@ type gwClientInputInject struct {
441442
f gwclient.BuildFunc
442443
}
443444

444-
func wrapWithInput(c gwclient.Client, id string, f gwclient.BuildFunc) *gwClientInputInject {
445-
return &gwClientInputInject{
445+
func wrapWithInput(c gwclient.Client, id string, f gwclient.BuildFunc) gwclient.Client {
446+
return gwutil.WithCurrentFrontend(c, &gwClientInputInject{
446447
Client: c,
447448
id: id,
448449
f: f,
449-
}
450+
})
450451
}
451452

452453
func (c *gwClientInputInject) Solve(ctx context.Context, req gwclient.SolveRequest) (*gwclient.Result, error) {

0 commit comments

Comments
 (0)