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) }