frontend/gateway.go (205 lines of code) (raw):
package frontend
import (
"context"
"fmt"
"sync"
"sync/atomic"
"github.com/Azure/dalec"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/frontend/dockerui"
gwclient "github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/bklog"
"github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
const (
requestIDKey = "requestid"
dalecSubrequstForwardBuild = "dalec.forward.build"
gatewayFrontend = "gateway.v0"
)
func getDockerfile(ctx context.Context, client gwclient.Client, build *dalec.SourceBuild, defPb *pb.Definition) ([]byte, error) {
dockerfilePath := dockerui.DefaultDockerfileName
if build.DockerfilePath != "" {
dockerfilePath = build.DockerfilePath
}
// First we need to read the dockerfile to determine what frontend to forward to
res, err := client.Solve(ctx, gwclient.SolveRequest{
Definition: defPb,
})
if err != nil {
return nil, errors.Wrap(err, "error getting build context")
}
ref, err := res.SingleRef()
if err != nil {
return nil, err
}
dt, err := ref.ReadFile(ctx, gwclient.ReadRequest{
Filename: dockerfilePath,
})
if err != nil {
return nil, errors.Wrap(err, "error reading dockerfile")
}
return dt, nil
}
// ForwarderFromClient creates a [dalec.ForwarderFunc] from a gateway client.
// This is used for forwarding builds to other frontends in [dalec.Source2LLBGetter]
func ForwarderFromClient(ctx context.Context, client gwclient.Client) dalec.ForwarderFunc {
return func(st llb.State, spec *dalec.SourceBuild, opts ...llb.ConstraintsOpt) (llb.State, error) {
if spec == nil {
spec = &dalec.SourceBuild{}
}
def, err := st.Marshal(ctx, opts...)
if err != nil {
return llb.Scratch(), err
}
defPb := def.ToPB()
dockerfileDt, err := getDockerfile(ctx, client, spec, defPb)
if err != nil {
return llb.Scratch(), err
}
req, err := newSolveRequest(
toDockerfile(ctx, st, dockerfileDt, spec, dalec.ProgressGroup("prepare dockerfile to forward to frontend")),
copyForForward(ctx, client),
)
if err != nil {
return llb.Scratch(), err
}
res, err := client.Solve(ctx, req)
if err != nil {
return llb.Scratch(), err
}
ref, err := res.SingleRef()
if err != nil {
return llb.Scratch(), err
}
return ref.ToState()
}
}
func GetBuildArg(client gwclient.Client, k string) (string, bool) {
opts := client.BuildOpts().Opts
if opts != nil {
if v, ok := opts["build-arg:"+k]; ok {
return v, true
}
}
return "", false
}
func SourceOptFromUIClient(ctx context.Context, c gwclient.Client, dc *dockerui.Client, platform *ocispecs.Platform) dalec.SourceOpts {
return dalec.SourceOpts{
TargetPlatform: platform,
Resolver: c,
Forward: ForwarderFromClient(ctx, c),
GetContext: func(ref string, opts ...llb.LocalOption) (*llb.State, error) {
if ref == dockerui.DefaultLocalNameContext {
return dc.MainContext(ctx, opts...)
}
nc, err := dc.NamedContext(ref, dockerui.ContextOpt{
ResolveMode: dc.ImageResolveMode.String(),
AsyncLocalOpts: func() []llb.LocalOption {
return opts
},
Platform: platform,
})
if err != nil {
return nil, err
}
if nc == nil {
return nil, nil
}
st, _, err := nc.Load(ctx)
return st, err
},
}
}
func SourceOptFromClient(ctx context.Context, c gwclient.Client, platform *ocispecs.Platform) (dalec.SourceOpts, error) {
dc, err := dockerui.NewClient(c)
if err != nil {
return dalec.SourceOpts{}, err
}
return SourceOptFromUIClient(ctx, c, dc, platform), nil
}
var (
supportsDiffMergeOnce sync.Once
supportsDiffMerge atomic.Bool
)
// SupportsDiffMerge checks if the given client supports the diff and merge operations.
func SupportsDiffMerge(client gwclient.Client) bool {
supportsDiffMergeOnce.Do(func() {
if client.BuildOpts().Opts["build-arg:DALEC_DISABLE_DIFF_MERGE"] == "1" {
supportsDiffMerge.Store(false)
return
}
supportsDiffMerge.Store(checkDiffMerge(client))
})
return supportsDiffMerge.Load()
}
func checkDiffMerge(client gwclient.Client) bool {
caps := client.BuildOpts().LLBCaps
if caps.Supports(pb.CapMergeOp) != nil {
return false
}
if caps.Supports(pb.CapDiffOp) != nil {
return false
}
return true
}
// copyForForward copies all the inputs and build opts from the initial request in order to forward to another frontend.
func copyForForward(ctx context.Context, client gwclient.Client) solveRequestOpt {
return func(req *gwclient.SolveRequest) error {
// Inputs are any additional build contexts or really any llb that the client sent along.
inputs, err := client.Inputs(ctx)
if err != nil {
return err
}
if req.FrontendInputs == nil {
req.FrontendInputs = make(map[string]*pb.Definition, len(inputs))
}
for k, v := range inputs {
if _, ok := req.FrontendInputs[k]; ok {
// Do not overwrite existing inputs
continue
}
def, err := v.Marshal(ctx)
if err != nil {
return errors.Wrap(err, "error marshaling frontend input")
}
req.FrontendInputs[k] = def.ToPB()
}
opts := client.BuildOpts().Opts
if req.FrontendOpt == nil {
req.FrontendOpt = make(map[string]string, len(opts))
}
for k, v := range opts {
if k == "filename" || k == "dockerfilekey" || k == "target" {
// These are some well-known keys that the dockerfile frontend uses
// which we'll be overriding with our own values (as needed) in the
// caller.
// Basically there should not be a need, nor is it desirable, to forward these along.
continue
}
if _, ok := req.FrontendOpt[k]; ok {
// Do not overwrite existing opts
continue
}
req.FrontendOpt[k] = v
}
return nil
}
}
const keyTopLevelTarget = "dalec.target"
type BuildOpstGetter interface {
BuildOpts() gwclient.BuildOpts
}
// GetTargetKey returns the key that should be used to select the [dalec.Target] from the [dalec.Spec]
func GetTargetKey(client BuildOpstGetter) string {
return client.BuildOpts().Opts[keyTopLevelTarget]
}
// Warn sends a warning to the client for the provided state.
func Warn(ctx context.Context, client gwclient.Client, st llb.State, msg string) {
// Note: This will attempt to marshal the state to get its digest for metadata
// on the warning message, but it is not required to actually write the message.
// For this reason we can continue on error.
def, err := st.Marshal(ctx)
if err != nil {
bklog.G(ctx).WithError(err).WithField("warn", msg).Warn("Error marshalling state for outputting warning message")
}
var dgst digest.Digest
if def != nil {
dgst, err = def.Head()
if err != nil {
bklog.G(ctx).WithError(err).WithField("warn", msg).Warn("Could not get state digest for outputting warning message")
}
}
if err := client.Warn(ctx, dgst, msg, gwclient.WarnOpts{}); err != nil {
bklog.G(ctx).WithError(err).WithField("warn", msg).Warn("Error writing warning message")
}
}
func Warnf(ctx context.Context, client gwclient.Client, st llb.State, format string, args ...any) {
msg := fmt.Sprintf(format, args...)
Warn(ctx, client, st, msg)
}