frontend/build.go (171 lines of code) (raw):
package frontend
import (
"bytes"
"context"
stderrors "errors"
"fmt"
"runtime"
"time"
"github.com/Azure/dalec"
"github.com/containerd/platforms"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/frontend/dockerui"
gwclient "github.com/moby/buildkit/frontend/gateway/client"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
type LoadConfig struct {
SubstituteOpts []dalec.SubstituteOpt
}
type LoadOpt func(*LoadConfig)
func WithAllowArgs(args ...string) LoadOpt {
return func(cfg *LoadConfig) {
set := make(map[string]struct{}, len(args))
for _, arg := range args {
set[arg] = struct{}{}
}
cfg.SubstituteOpts = append(cfg.SubstituteOpts, func(cfg *dalec.SubstituteConfig) {
orig := cfg.AllowArg
cfg.AllowArg = func(key string) bool {
if orig != nil && orig(key) {
return true
}
_, ok := set[key]
return ok
}
})
}
}
func LoadSpec(ctx context.Context, client *dockerui.Client, platform *ocispecs.Platform, opts ...LoadOpt) (*dalec.Spec, error) {
cfg := LoadConfig{}
for _, o := range opts {
o(&cfg)
}
src, err := client.ReadEntrypoint(ctx, "Dockerfile")
if err != nil {
return nil, fmt.Errorf("could not read spec file: %w", err)
}
spec, err := dalec.LoadSpec(bytes.TrimSpace(src.Data))
if err != nil {
return nil, fmt.Errorf("error loading spec: %w", err)
}
args := dalec.DuplicateMap(client.BuildArgs)
if platform == nil {
p := platforms.DefaultSpec()
platform = &p
}
fillPlatformArgs("TARGET", args, *platform)
fillPlatformArgs("BUILD", args, client.BuildPlatforms[0])
if err := spec.SubstituteArgs(args, cfg.SubstituteOpts...); err != nil {
return nil, errors.Wrap(err, "error resolving build args")
}
return spec, nil
}
func getOS(platform ocispecs.Platform) string {
return platform.OS
}
func getArch(platform ocispecs.Platform) string {
return platform.Architecture
}
func getVariant(platform ocispecs.Platform) string {
return platform.Variant
}
func getPlatformFormat(platform ocispecs.Platform) string {
return platforms.Format(platform)
}
var passthroughGetters = map[string]func(ocispecs.Platform) string{
"OS": getOS,
"ARCH": getArch,
"VARIANT": getVariant,
"PLATFORM": getPlatformFormat,
}
func fillPlatformArgs(prefix string, args map[string]string, platform ocispecs.Platform) {
for attr, getter := range passthroughGetters {
args[prefix+attr] = getter(platform)
}
}
type PlatformBuildFunc func(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (gwclient.Reference, *dalec.DockerImageSpec, error)
// BuildWithPlatform is a helper function to build a spec with a given platform
// It takes care of looping through each target platform and executing the build with the platform args substituted in the spec.
// This also deals with the docker-style multi-platform output.
func BuildWithPlatform(ctx context.Context, client gwclient.Client, f PlatformBuildFunc) (*gwclient.Result, error) {
dc, err := dockerui.NewClient(client)
if err != nil {
return nil, err
}
return BuildWithPlatformFromUIClient(ctx, client, dc, f)
}
func getPanicStack() error {
stackBuf := make([]uintptr, 32)
n := runtime.Callers(4, stackBuf) // Skip 4 frames to exclude runtime.Callers, the current function, and defer internals
stackBuf = stackBuf[:n]
frames := runtime.CallersFrames(stackBuf)
var stackTrace string
for {
frame, more := frames.Next()
stackTrace += fmt.Sprintf("%s\n\t%s:%d\n", frame.Function, frame.File, frame.Line)
if !more {
break
}
}
return stderrors.New(stackTrace)
}
// Like [BuildWithPlatform] but with a pre-initialized dockerui.Client
func BuildWithPlatformFromUIClient(ctx context.Context, client gwclient.Client, dc *dockerui.Client, f PlatformBuildFunc) (*gwclient.Result, error) {
rb, err := dc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (_ gwclient.Reference, _ *dalec.DockerImageSpec, _ *dalec.DockerImageSpec, retErr error) {
defer func() {
if r := recover(); r != nil {
trace := getPanicStack()
recErr := fmt.Errorf("recovered from panic in build: %+v", r)
retErr = stderrors.Join(recErr, trace)
}
}()
spec, err := LoadSpec(ctx, dc, platform)
if err != nil {
return nil, nil, nil, err
}
targetKey := GetTargetKey(dc)
ref, cfg, err := f(ctx, client, platform, spec, targetKey)
if cfg != nil {
now := time.Now()
cfg.Created = &now
}
return ref, cfg, nil, err
})
if err != nil {
return nil, err
}
return rb.Finalize()
}
// GetBaseImage returns an image that first checks if the client provided the
// image in the build context matching the image ref.
//
// This follows the behavior of of the dockerfile frontend.
func GetBaseImage(sOpt dalec.SourceOpts, ref string, opts ...llb.ConstraintsOpt) llb.State {
return llb.Scratch().Async(func(ctx context.Context, _ llb.State, c *llb.Constraints) (llb.State, error) {
for _, o := range opts {
o.SetConstraintsOption(c)
}
fromClient, err := sOpt.GetContext(ref, dalec.WithConstraint(c))
if err != nil {
return llb.Scratch(), err
}
if fromClient != nil {
return *fromClient, nil
}
return llb.Image(ref, dalec.WithConstraint(c), llb.WithMetaResolver(sOpt.Resolver)), nil
})
}
// WithDefaultPlatform is a helper function to set a default platform for a build
// if the client does not provide one.
func WithDefaultPlatform(platform ocispecs.Platform, build gwclient.BuildFunc) gwclient.BuildFunc {
return func(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) {
if client.BuildOpts().Opts["platform"] != "" {
return build(ctx, client)
}
client = &clientWithPlatform{
Client: client,
platform: &platform,
}
return build(ctx, client)
}
}
type clientWithPlatform struct {
gwclient.Client
platform *ocispecs.Platform
}
func (c *clientWithPlatform) BuildOpts() gwclient.BuildOpts {
opts := c.Client.BuildOpts()
opts.Opts["platform"] = platforms.Format(*c.platform)
return opts
}