Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions cmd/frontend/main.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package main

import (
"context"
_ "embed"
"flag"
"fmt"
"os"

gwclient "github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/frontend/gateway/grpcclient"
"github.com/moby/buildkit/util/appcontext"
"github.com/moby/buildkit/util/bklog"
Expand Down Expand Up @@ -65,12 +67,14 @@ func main() {

func dalecMain() {
ctx := appcontext.Context()
mux, err := frontendapi.NewBuildRouter(ctx)
if err != nil {
bklog.L.WithError(err).Fatal("error creating frontend router")
}

if err := grpcclient.RunFromEnvironment(ctx, mux.Handler(frontend.WithTargetForwardingHandler)); err != nil {
if err := grpcclient.RunFromEnvironment(ctx, func(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) {
r, err := frontendapi.NewRouter(ctx, client)
if err != nil {
return nil, err
}
return r.Handler(frontend.WithTargetForwardingHandler)(ctx, client)
}); err != nil {
bklog.L.WithError(err).Fatal("error running frontend")
os.Exit(70) // 70 is EX_SOFTWARE, meaning internal software error occurred
}
Expand Down
37 changes: 33 additions & 4 deletions cmd/gen-doc-targets/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package main

import (
"bytes"
"context"
"flag"
"fmt"
"io"
"os"

"github.com/moby/buildkit/client/llb"
gwclient "github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/frontend/subrequests/targets"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/appcontext"
"github.com/moby/buildkit/util/bklog"
"github.com/project-dalec/dalec/internal/frontendapi"
Expand All @@ -22,16 +25,16 @@ func main() {
bklog.L.Logger.SetLevel(logrus.ErrorLevel)
ctx := appcontext.Context()

mux, err := frontendapi.NewBuildRouter(ctx)
client := &targetPrintClient{}
r, err := frontendapi.NewRouter(ctx, client)
if err != nil {
bklog.L.Fatal(err)
}

var client gwclient.Client = &targetPrintClient{}
res, err := mux.Handle(ctx, client)
res, err := r.Handle(ctx, client)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
os.Exit(2)
}

dt, ok := res.Metadata["result.txt"]
Expand Down Expand Up @@ -63,6 +66,8 @@ func main() {
}
}

// targetPrintClient embeds gwclient.Client and overrides methods needed
// to load an empty spec during route setup and trigger the targets subrequest.
type targetPrintClient struct {
gwclient.Client
}
Expand All @@ -72,5 +77,29 @@ func (t *targetPrintClient) BuildOpts() gwclient.BuildOpts {
Opts: map[string]string{
"requestid": targets.SubrequestsTargetsDefinition.Name,
},
LLBCaps: pb.Caps.CapSet(pb.Caps.All()),
Caps: pb.Caps.CapSet(pb.Caps.All()),
}
}

func (t *targetPrintClient) Inputs(_ context.Context) (map[string]llb.State, error) {
return map[string]llb.State{
"dockerfile": llb.Scratch(),
}, nil
}

func (t *targetPrintClient) Solve(_ context.Context, _ gwclient.SolveRequest) (*gwclient.Result, error) {
res := gwclient.NewResult()
res.SetRef(&emptySpecRef{})
return res, nil
}

// emptySpecRef embeds gwclient.Reference and overrides ReadFile to return
// an empty YAML object, which LoadSpec interprets as an empty dalec.Spec.
type emptySpecRef struct {
gwclient.Reference
}

func (r *emptySpecRef) ReadFile(_ context.Context, _ gwclient.ReadRequest) ([]byte, error) {
return []byte("{}"), nil
}
42 changes: 35 additions & 7 deletions cmd/worker-img-matrix/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
package main

import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"path"
"path/filepath"
"strings"

"github.com/containerd/plugin"
"github.com/project-dalec/dalec"
"github.com/project-dalec/dalec/internal/plugins"
_ "github.com/project-dalec/dalec/targets/plugin"
)
Expand All @@ -29,7 +33,7 @@ func main() {
outF := os.Stdout
if outPath := flag.Arg(0); outPath != "" {
var err error
if err := os.MkdirAll(path.Dir(outPath), 0755); err != nil {
if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
panic(fmt.Errorf("failed to create output directory: %w", err))
}
outF, err = os.Create(outPath)
Expand All @@ -40,14 +44,38 @@ func main() {
}

filter := func(r *plugins.Registration) bool {
return r.Type != plugins.TypeBuildTarget
return r.Type != plugins.TypeRouteProvider
}

ctx := context.Background()
spec := &dalec.Spec{}
set := plugin.NewPluginSet()

var out []Info
for _, r := range plugins.Graph(filter) {
var i Info
i.Target = path.Join(r.ID, "worker")
out = append(out, i)
for _, reg := range plugins.Graph(filter) {
cfg := plugin.NewContext(ctx, set, nil)

p := reg.Init(cfg)
if err := set.Add(p); err != nil {
panic(fmt.Errorf("failed to add plugin %s: %w", reg.ID, err))
}

v, err := p.Instance()
if err != nil {
panic(fmt.Errorf("failed to get instance for plugin %s: %w", reg.ID, err))
}

provider := v.(plugins.RouteProvider)
routes, err := provider.Routes(ctx, spec)
if err != nil {
panic(fmt.Errorf("failed to get routes for plugin %s: %w", reg.ID, err))
}
for _, route := range routes {
_, suffix, ok := strings.Cut(route.FullPath, "/")
if ok && suffix == "worker" {
out = append(out, Info{Target: route.FullPath})
}
}
}

m := Matrix{
Expand Down
12 changes: 5 additions & 7 deletions frontend/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@ type LoadConfig struct {

type LoadOpt func(*LoadConfig)

type frontendClient interface {
CurrentFrontend() (*llb.State, error)
}

func WithAllowArgs(args ...string) LoadOpt {
return func(cfg *LoadConfig) {
set := make(map[string]struct{}, len(args))
Expand Down Expand Up @@ -199,14 +195,16 @@ func WithDefaultPlatform(platform ocispecs.Platform, build gwclient.BuildFunc) g
if client.BuildOpts().Opts["platform"] != "" {
return build(ctx, client)
}
client = &clientWithPlatform{
client = withCurrentFrontend(client, &clientWithPlatform{
Client: client,
platform: &platform,
}
})
return build(ctx, client)
}
}

var _ gwclient.Client = (*clientWithPlatform)(nil)

type clientWithPlatform struct {
gwclient.Client
platform *ocispecs.Platform
Expand All @@ -219,7 +217,7 @@ func (c *clientWithPlatform) BuildOpts() gwclient.BuildOpts {
}

func GetCurrentFrontend(client gwclient.Client) (llb.State, error) {
f, err := client.(frontendClient).CurrentFrontend()
f, err := client.(currentFrontend).CurrentFrontend()
if err != nil {
return llb.Scratch(), err
}
Expand Down
146 changes: 146 additions & 0 deletions frontend/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package frontend

import (
"context"
"encoding/json"
"fmt"
"strings"

"github.com/containerd/platforms"
"github.com/goccy/go-yaml"
"github.com/moby/buildkit/frontend/dockerui"
gwclient "github.com/moby/buildkit/frontend/gateway/client"
"github.com/project-dalec/dalec"
)

const (
keyResolveSpec = "frontend.dalec.resolve"

// KeyDefaultPlatform is the subrequest id for returning the default platform
// for the builder.
KeyDefaultPlatform = "frontend.dalec.defaultPlatform"
KeyJSONSchema = "frontend.dalec.schema"
)

const keyTarget = "target"

// noSuchHandlerError is returned when no route matches the requested target.
type noSuchHandlerError struct {
Target string
Available []string
}

func handlerNotFound(target string, available []string) error {
return &noSuchHandlerError{Target: target, Available: available}
}

func (err *noSuchHandlerError) Error() string {
return fmt.Sprintf("no such handler for target %q: available targets: %s", err.Target, strings.Join(err.Available, ", "))
}

var (
_ gwclient.Client = (*clientWithCustomOpts)(nil)
)

type clientWithCustomOpts struct {
opts gwclient.BuildOpts
gwclient.Client
}

func (d *clientWithCustomOpts) BuildOpts() gwclient.BuildOpts {
return d.opts
}

func setClientOptOption(client gwclient.Client, extraOpts map[string]string) gwclient.Client {
opts := client.BuildOpts()

for key, value := range extraOpts {
opts.Opts[key] = value
}
return withCurrentFrontend(client, &clientWithCustomOpts{
Client: client,
opts: opts,
})
}

func maybeSetDalecTargetKey(client gwclient.Client, key string) gwclient.Client {
opts := client.BuildOpts()
if opts.Opts[keyTopLevelTarget] != "" {
// do nothing since this is already set
return client
}

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

func handleResolveSpec(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) {
dc, err := dockerui.NewClient(client)
if err != nil {
return nil, err
}

targets := dc.TargetPlatforms
if len(targets) == 0 {
targets = append(targets, platforms.DefaultSpec())
}

out := make([]*dalec.Spec, 0, len(targets))
for _, p := range targets {
spec, err := LoadSpec(ctx, dc, &p)
if err != nil {
return nil, err
}
out = append(out, spec)
}

dtYaml, err := yaml.Marshal(out)
if err != nil {
return nil, err
}

dtJSON, err := json.Marshal(out)
if err != nil {
return nil, err
}

res := gwclient.NewResult()
res.AddMeta("result.json", dtJSON)
// result.txt here so that `docker buildx build --print` will output it directly.
// Otherwise it prints a go object.
res.AddMeta("result.txt", dtYaml)

return res, nil
}

// handleDefaultPlatform returns the default platform
func handleDefaultPlatform() (*gwclient.Result, error) {
res := gwclient.NewResult()

p := platforms.DefaultSpec()
dt, err := json.Marshal(p)
if err != nil {
return nil, err
}

res.AddMeta("result.json", dt)
res.AddMeta("result.txt", []byte(platforms.Format(p)))

return res, nil
}

func handleJSONSchema() (*gwclient.Result, error) {
res := gwclient.NewResult()
jsonSchema := dalec.GetJSONSchema()
res.AddMeta("result.json", jsonSchema)
res.AddMeta("result.txt", jsonSchema)

return res, nil
}
Loading
Loading