Skip to content

Commit 2c2d065

Browse files
committed
frontend: make route registration spec-aware
Change RouteProvider.Routes() to accept (prefix, ctx, client) so that providers can load the spec at registration time. This enables: - SpecDefined annotation on each route (whether the target key appears in spec.Targets), used by the target list API - Per-request NewRouter(ctx, client) in the BuildFunc, since route registration now needs the gateway client - LoadSpecFromClient() helper in frontend/router.go for providers - Each distro registers as a separate RouteProvider via RegisterRouteProvider() - Bare distro prefix routes (e.g. "mariner2") as hidden aliases for the default container handler - Hidden field on frontend.Target to exclude alias routes from the target list while keeping them dispatchable - DebugRoutes now registers individual sub-routes (buildroot, sources, spec) instead of a single prefix handler - gen-doc-targets uses a stub gwclient.Client to set up routes for documentation generation Signed-off-by: Brian Goff <cpuguy83@gmail.com>
1 parent 9651a2e commit 2c2d065

File tree

14 files changed

+230
-110
lines changed

14 files changed

+230
-110
lines changed

cmd/frontend/main.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package main
22

33
import (
4+
"context"
45
_ "embed"
56
"flag"
67
"fmt"
78
"os"
89

10+
gwclient "github.com/moby/buildkit/frontend/gateway/client"
911
"github.com/moby/buildkit/frontend/gateway/grpcclient"
1012
"github.com/moby/buildkit/util/appcontext"
1113
"github.com/moby/buildkit/util/bklog"
@@ -65,12 +67,14 @@ func main() {
6567

6668
func dalecMain() {
6769
ctx := appcontext.Context()
68-
r, err := frontendapi.NewRouter(ctx)
69-
if err != nil {
70-
bklog.L.WithError(err).Fatal("error creating frontend router")
71-
}
7270

73-
if err := grpcclient.RunFromEnvironment(ctx, r.Handler(frontend.WithTargetForwardingHandler)); err != nil {
71+
if err := grpcclient.RunFromEnvironment(ctx, func(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) {
72+
r, err := frontendapi.NewRouter(ctx, client)
73+
if err != nil {
74+
return nil, err
75+
}
76+
return r.Handler(frontend.WithTargetForwardingHandler)(ctx, client)
77+
}); err != nil {
7478
bklog.L.WithError(err).Fatal("error running frontend")
7579
os.Exit(70) // 70 is EX_SOFTWARE, meaning internal software error occurred
7680
}

cmd/gen-doc-targets/main.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,16 @@ func main() {
2525
bklog.L.Logger.SetLevel(logrus.ErrorLevel)
2626
ctx := appcontext.Context()
2727

28-
r, err := frontendapi.NewRouter(ctx)
28+
client := &targetPrintClient{}
29+
r, err := frontendapi.NewRouter(ctx, client)
2930
if err != nil {
3031
bklog.L.Fatal(err)
3132
}
3233

33-
var client gwclient.Client = &targetPrintClient{}
3434
res, err := r.Handle(ctx, client)
3535
if err != nil {
3636
fmt.Fprintln(os.Stderr, err)
37-
os.Exit(1)
37+
os.Exit(2)
3838
}
3939

4040
dt, ok := res.Metadata["result.txt"]
@@ -66,6 +66,8 @@ func main() {
6666
}
6767
}
6868

69+
// targetPrintClient embeds gwclient.Client and overrides methods needed
70+
// to load an empty spec during route setup and trigger the targets subrequest.
6971
type targetPrintClient struct {
7072
gwclient.Client
7173
}
@@ -92,8 +94,8 @@ func (t *targetPrintClient) Solve(_ context.Context, _ gwclient.SolveRequest) (*
9294
return res, nil
9395
}
9496

95-
// emptySpecRef implements gwclient.Reference and returns an empty YAML object
96-
// from ReadFile, which LoadSpec interprets as an empty dalec.Spec.
97+
// emptySpecRef embeds gwclient.Reference and overrides ReadFile to return
98+
// an empty YAML object, which LoadSpec interprets as an empty dalec.Spec.
9799
type emptySpecRef struct {
98100
gwclient.Reference
99101
}

cmd/worker-img-matrix/main.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import (
1111
"strings"
1212

1313
"github.com/containerd/plugin"
14+
"github.com/moby/buildkit/client/llb"
15+
gwclient "github.com/moby/buildkit/frontend/gateway/client"
16+
"github.com/moby/buildkit/frontend/subrequests/targets"
17+
"github.com/moby/buildkit/solver/pb"
1418
"github.com/project-dalec/dalec/internal/plugins"
1519
_ "github.com/project-dalec/dalec/targets/plugin"
1620
)
@@ -47,6 +51,7 @@ func main() {
4751
}
4852

4953
ctx := context.Background()
54+
client := &stubClient{}
5055
set := plugin.NewPluginSet()
5156

5257
var out []Info
@@ -64,7 +69,11 @@ func main() {
6469
}
6570

6671
provider := v.(plugins.RouteProvider)
67-
for _, route := range provider.Routes() {
72+
routes, err := provider.Routes(ctx, client)
73+
if err != nil {
74+
panic(fmt.Errorf("failed to get routes for plugin %s: %w", reg.ID, err))
75+
}
76+
for _, route := range routes {
6877
_, suffix, ok := strings.Cut(route.FullPath, "/")
6978
if ok && suffix == "worker" {
7079
out = append(out, Info{Target: route.FullPath})
@@ -83,3 +92,35 @@ func main() {
8392
panic(fmt.Errorf("failed to write output: %w", err))
8493
}
8594
}
95+
96+
// stubClient provides just enough gwclient.Client implementation for
97+
// spec-aware route registration to load an empty spec.
98+
type stubClient struct {
99+
gwclient.Client
100+
}
101+
102+
func (s *stubClient) BuildOpts() gwclient.BuildOpts {
103+
return gwclient.BuildOpts{
104+
Opts: map[string]string{"requestid": targets.SubrequestsTargetsDefinition.Name},
105+
LLBCaps: pb.Caps.CapSet(pb.Caps.All()),
106+
Caps: pb.Caps.CapSet(pb.Caps.All()),
107+
}
108+
}
109+
110+
func (s *stubClient) Inputs(_ context.Context) (map[string]llb.State, error) {
111+
return map[string]llb.State{"dockerfile": llb.Scratch()}, nil
112+
}
113+
114+
func (s *stubClient) Solve(_ context.Context, _ gwclient.SolveRequest) (*gwclient.Result, error) {
115+
res := gwclient.NewResult()
116+
res.SetRef(&emptySpecRef{})
117+
return res, nil
118+
}
119+
120+
type emptySpecRef struct {
121+
gwclient.Reference
122+
}
123+
124+
func (r *emptySpecRef) ReadFile(_ context.Context, _ gwclient.ReadRequest) ([]byte, error) {
125+
return []byte("{}"), nil
126+
}

frontend/debug/handler.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
const DebugRoute = "debug"
99

1010
// Routes returns the flat routes for the debug handler, prefixed with the given prefix.
11+
// Debug routes are never spec-defined since no spec would have a "debug" target key.
1112
func Routes(prefix string) []frontend.Route {
1213
return []frontend.Route{
1314
{

frontend/router.go

Lines changed: 26 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ type Target struct {
3131
// appears in spec.Targets. When spec.Targets is empty (no explicit
3232
// target filtering), this field is false for all targets.
3333
SpecDefined bool `json:"specDefined,omitempty"`
34+
35+
// Hidden indicates that this route should not appear in the target
36+
// list but is still dispatchable. This is used for routes like bare
37+
// distro names (e.g. "mariner2") that act as aliases for a default
38+
// sub-route (e.g. "mariner2/container").
39+
Hidden bool `json:"hidden,omitempty"`
3440
}
3541

3642
// TargetList is the dalec-extended target list response.
@@ -54,9 +60,6 @@ type Route struct {
5460
// It replaces the hierarchical BuildMux with a simpler dispatch model.
5561
type Router struct {
5662
routes map[string]Route
57-
58-
// cached spec so we don't have to load it every time its needed
59-
spec *dalec.Spec
6063
}
6164

6265
// Add registers a route. If a route with the same FullPath already exists
@@ -105,11 +108,6 @@ func (r *Router) Handle(ctx context.Context, client gwclient.Client) (_ *gwclien
105108
if retErr != nil {
106109
if _, ok := opts[keyTopLevelTarget]; !ok {
107110
retErr = errors.Wrapf(retErr, "error handling requested build target %q", target)
108-
109-
spec, _ := r.loadSpec(ctx, client)
110-
if spec != nil && spec.Name != "" {
111-
retErr = errors.Wrapf(retErr, "spec: %s", spec.Name)
112-
}
113111
}
114112
}
115113
}()
@@ -201,17 +199,9 @@ func (r *Router) describe() (*gwclient.Result, error) {
201199
}
202200

203201
// list returns the target list. All route metadata is available directly —
204-
// no handler invocation needed. Targets are annotated with SpecDefined
205-
// based on the loaded spec.
202+
// no handler invocation needed. SpecDefined is set by providers at
203+
// route registration time.
206204
func (r *Router) list(ctx context.Context, client gwclient.Client, target string) (*gwclient.Result, error) {
207-
spec, err := r.loadSpec(ctx, client)
208-
if err != nil {
209-
bklog.G(ctx).WithError(err).Warn("Could not load spec for target list annotation")
210-
// Continue without spec — targets just won't have SpecDefined set.
211-
}
212-
213-
hasSpecTargets := spec != nil && len(spec.Targets) > 0
214-
215205
var ls TargetList
216206

217207
keys := maps.Keys(r.routes)
@@ -225,15 +215,12 @@ func (r *Router) list(ctx context.Context, client gwclient.Client, target string
225215
continue
226216
}
227217

228-
dt := route.Info
229-
if hasSpecTargets {
230-
tlk := topLevelKey(key)
231-
if _, ok := spec.Targets[tlk]; ok {
232-
dt.SpecDefined = true
233-
}
218+
// Hidden routes are dispatchable but excluded from the target list.
219+
if route.Info.Hidden {
220+
continue
234221
}
235222

236-
ls.Targets = append(ls.Targets, dt)
223+
ls.Targets = append(ls.Targets, route.Info)
237224
}
238225

239226
return dalecTargetListToResult(ls)
@@ -320,32 +307,27 @@ func (r *Router) lookupTarget(ctx context.Context, target string) (matchedPath s
320307
return "", nil, handlerNotFound(target, maps.Keys(r.routes))
321308
}
322309

323-
func (r *Router) loadSpec(ctx context.Context, client gwclient.Client) (*dalec.Spec, error) {
324-
if r.spec != nil {
325-
return r.spec, nil
310+
// topLevelKey returns the first path segment of a route path.
311+
// e.g. "azlinux3/container/depsonly" → "azlinux3"
312+
func topLevelKey(routePath string) string {
313+
if i := strings.IndexByte(routePath, '/'); i >= 0 {
314+
return routePath[:i]
326315
}
316+
return routePath
317+
}
318+
319+
// LoadSpecFromClient loads a dalec spec from the gateway client's build
320+
// context. Build args are substituted with WithAllowAnyArg so that
321+
// unresolved args don't cause errors during route setup.
322+
func LoadSpecFromClient(ctx context.Context, client gwclient.Client) (*dalec.Spec, error) {
327323
dc, err := dockerui.NewClient(client)
328324
if err != nil {
329325
return nil, err
330326
}
331327

332-
spec, err := LoadSpec(ctx, dc, nil, func(cfg *LoadConfig) {
328+
return LoadSpec(ctx, dc, nil, func(cfg *LoadConfig) {
333329
cfg.SubstituteOpts = append(cfg.SubstituteOpts, dalec.WithAllowAnyArg)
334330
})
335-
if err != nil {
336-
return nil, err
337-
}
338-
r.spec = spec
339-
return spec, nil
340-
}
341-
342-
// topLevelKey returns the first path segment of a route path.
343-
// e.g. "azlinux3/container/depsonly" → "azlinux3"
344-
func topLevelKey(routePath string) string {
345-
if i := strings.IndexByte(routePath, '/'); i >= 0 {
346-
return routePath[:i]
347-
}
348-
return routePath
349331
}
350332

351333
// WithTargetForwardingHandler registers a forwarding handler for each
@@ -355,7 +337,7 @@ func WithTargetForwardingHandler(ctx context.Context, client gwclient.Client, r
355337
if k := GetTargetKey(client); k != "" {
356338
return fmt.Errorf("target forwarding requested but target is already forwarded: this is a bug in the frontend for %q", k)
357339
}
358-
spec, err := r.loadSpec(ctx, client)
340+
spec, err := LoadSpecFromClient(ctx, client)
359341
if err != nil {
360342
return err
361343
}

internal/frontendapi/router.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import (
55
"fmt"
66

77
"github.com/containerd/plugin"
8+
gwclient "github.com/moby/buildkit/frontend/gateway/client"
89
"github.com/project-dalec/dalec/frontend"
910
"github.com/project-dalec/dalec/frontend/debug"
1011
"github.com/project-dalec/dalec/internal/plugins"
1112
_ "github.com/project-dalec/dalec/targets/plugin"
1213
)
1314

1415
// NewRouter creates a flat Router with all routes registered eagerly.
15-
func NewRouter(ctx context.Context) (*frontend.Router, error) {
16+
func NewRouter(ctx context.Context, client gwclient.Client) (*frontend.Router, error) {
1617
r := &frontend.Router{}
1718

1819
// Register debug routes.
@@ -21,14 +22,14 @@ func NewRouter(ctx context.Context) (*frontend.Router, error) {
2122
}
2223

2324
// Load route providers from the plugin registry.
24-
if err := loadRouteProviders(ctx, r); err != nil {
25+
if err := loadRouteProviders(ctx, client, r); err != nil {
2526
return nil, err
2627
}
2728

2829
return r, nil
2930
}
3031

31-
func loadRouteProviders(ctx context.Context, r *frontend.Router) error {
32+
func loadRouteProviders(ctx context.Context, client gwclient.Client, r *frontend.Router) error {
3233
set := plugin.NewPluginSet()
3334

3435
filter := func(reg *plugins.Registration) bool {
@@ -55,7 +56,11 @@ func loadRouteProviders(ctx context.Context, r *frontend.Router) error {
5556
if !ok {
5657
return fmt.Errorf("plugin %T does not implement RouteProvider", v)
5758
}
58-
for _, route := range provider.Routes() {
59+
routes, err := provider.Routes(ctx, client)
60+
if err != nil {
61+
return err
62+
}
63+
for _, route := range routes {
5964
r.Add(ctx, route)
6065
}
6166
}

internal/plugins/types.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package plugins
22

33
import (
4+
"context"
5+
6+
gwclient "github.com/moby/buildkit/frontend/gateway/client"
47
"github.com/project-dalec/dalec/frontend"
58
)
69

@@ -12,12 +15,12 @@ const (
1215

1316
// RouteProvider is implemented by plugins that supply flat routes for the Router.
1417
type RouteProvider interface {
15-
Routes() []frontend.Route
18+
Routes(ctx context.Context, client gwclient.Client) ([]frontend.Route, error)
1619
}
1720

1821
// RouteProviderFunc is a convenience adapter for RouteProvider.
19-
type RouteProviderFunc func() []frontend.Route
22+
type RouteProviderFunc func(ctx context.Context, client gwclient.Client) ([]frontend.Route, error)
2023

21-
func (f RouteProviderFunc) Routes() []frontend.Route {
22-
return f()
24+
func (f RouteProviderFunc) Routes(ctx context.Context, client gwclient.Client) ([]frontend.Route, error) {
25+
return f(ctx, client)
2326
}

0 commit comments

Comments
 (0)