This creates a `Monitor` type that keeps the global state between monitor invocations and allows the monitor to exist during the build so it can be utilized for callbacks. The result handler is now registered with the monitor during the build and `Run` will use the result if it is present and the configuration intends the monitor to be invoked with the given result. Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
273 lines
7.8 KiB
Go
273 lines
7.8 KiB
Go
package build
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/docker/buildx/build"
|
|
"github.com/docker/buildx/builder"
|
|
"github.com/docker/buildx/controller/errdefs"
|
|
controllerapi "github.com/docker/buildx/controller/pb"
|
|
"github.com/docker/buildx/store"
|
|
"github.com/docker/buildx/store/storeutil"
|
|
"github.com/docker/buildx/util/buildflags"
|
|
"github.com/docker/buildx/util/confutil"
|
|
"github.com/docker/buildx/util/dockerutil"
|
|
"github.com/docker/buildx/util/platformutil"
|
|
"github.com/docker/buildx/util/progress"
|
|
"github.com/docker/cli/cli/command"
|
|
dockeropts "github.com/docker/cli/opts"
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/moby/buildkit/client"
|
|
"github.com/moby/buildkit/session/auth/authprovider"
|
|
"github.com/moby/buildkit/util/grpcerrors"
|
|
"github.com/pkg/errors"
|
|
"google.golang.org/grpc/codes"
|
|
)
|
|
|
|
const defaultTargetName = "default"
|
|
|
|
// RunBuild runs the specified build and returns the result.
|
|
//
|
|
// NOTE: When an error happens during the build and this function acquires the debuggable *build.ResultHandle,
|
|
// this function returns it in addition to the error (i.e. it does "return nil, res, err"). The caller can
|
|
// inspect the result and debug the cause of that error.
|
|
func RunBuild(ctx context.Context, dockerCli command.Cli, in *Options, inStream io.Reader, progress progress.Writer, bh *build.Handler) (*client.SolveResponse, *build.Inputs, error) {
|
|
if in.NoCache && len(in.NoCacheFilter) > 0 {
|
|
return nil, nil, errors.Errorf("--no-cache and --no-cache-filter cannot currently be used together")
|
|
}
|
|
|
|
contexts := map[string]build.NamedContext{}
|
|
for name, path := range in.NamedContexts {
|
|
contexts[name] = build.NamedContext{Path: path}
|
|
}
|
|
|
|
opts := build.Options{
|
|
Inputs: build.Inputs{
|
|
ContextPath: in.ContextPath,
|
|
DockerfilePath: in.DockerfileName,
|
|
InStream: build.NewSyncMultiReader(inStream),
|
|
NamedContexts: contexts,
|
|
},
|
|
Ref: in.Ref,
|
|
BuildArgs: in.BuildArgs,
|
|
CgroupParent: in.CgroupParent,
|
|
ExtraHosts: in.ExtraHosts,
|
|
Labels: in.Labels,
|
|
NetworkMode: in.NetworkMode,
|
|
NoCache: in.NoCache,
|
|
NoCacheFilter: in.NoCacheFilter,
|
|
Pull: in.Pull,
|
|
ShmSize: dockeropts.MemBytes(in.ShmSize),
|
|
Tags: in.Tags,
|
|
Target: in.Target,
|
|
Ulimits: controllerUlimitOpt2DockerUlimit(in.Ulimits),
|
|
GroupRef: in.GroupRef,
|
|
ProvenanceResponseMode: confutil.ParseMetadataProvenance(in.ProvenanceResponseMode),
|
|
}
|
|
|
|
platforms, err := platformutil.Parse(in.Platforms)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
opts.Platforms = platforms
|
|
|
|
dockerConfig := dockerCli.ConfigFile()
|
|
opts.Session = append(opts.Session, authprovider.NewDockerAuthProvider(authprovider.DockerAuthProviderConfig{
|
|
ConfigFile: dockerConfig,
|
|
}))
|
|
|
|
secrets, err := controllerapi.CreateSecrets(in.Secrets)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
opts.Session = append(opts.Session, secrets)
|
|
|
|
sshSpecs := in.SSH
|
|
if len(sshSpecs) == 0 && buildflags.IsGitSSH(in.ContextPath) {
|
|
sshSpecs = append(sshSpecs, &controllerapi.SSH{ID: "default"})
|
|
}
|
|
ssh, err := controllerapi.CreateSSH(sshSpecs)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
opts.Session = append(opts.Session, ssh)
|
|
|
|
outputs, _, err := controllerapi.CreateExports(in.Exports)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if in.ExportPush {
|
|
var pushUsed bool
|
|
for i := range outputs {
|
|
if outputs[i].Type == client.ExporterImage {
|
|
outputs[i].Attrs["push"] = "true"
|
|
pushUsed = true
|
|
}
|
|
}
|
|
if !pushUsed {
|
|
outputs = append(outputs, client.ExportEntry{
|
|
Type: client.ExporterImage,
|
|
Attrs: map[string]string{
|
|
"push": "true",
|
|
},
|
|
})
|
|
}
|
|
}
|
|
if in.ExportLoad {
|
|
var loadUsed bool
|
|
for i := range outputs {
|
|
if outputs[i].Type == client.ExporterDocker {
|
|
if _, ok := outputs[i].Attrs["dest"]; !ok {
|
|
loadUsed = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !loadUsed {
|
|
outputs = append(outputs, client.ExportEntry{
|
|
Type: client.ExporterDocker,
|
|
Attrs: map[string]string{},
|
|
})
|
|
}
|
|
}
|
|
|
|
annotations, err := buildflags.ParseAnnotations(in.Annotations)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "parse annotations")
|
|
}
|
|
|
|
for _, o := range outputs {
|
|
for k, v := range annotations {
|
|
o.Attrs[k.String()] = v
|
|
}
|
|
}
|
|
|
|
opts.Exports = outputs
|
|
|
|
opts.CacheFrom = controllerapi.CreateCaches(in.CacheFrom)
|
|
opts.CacheTo = controllerapi.CreateCaches(in.CacheTo)
|
|
|
|
opts.Attests = controllerapi.CreateAttestations(in.Attests)
|
|
|
|
opts.SourcePolicy = in.SourcePolicy
|
|
|
|
allow, err := buildflags.ParseEntitlements(in.Allow)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
opts.Allow = allow
|
|
|
|
if in.CallFunc != nil {
|
|
opts.CallFunc = &build.CallFunc{
|
|
Name: in.CallFunc.Name,
|
|
Format: in.CallFunc.Format,
|
|
IgnoreStatus: in.CallFunc.IgnoreStatus,
|
|
}
|
|
}
|
|
|
|
// key string used for kubernetes "sticky" mode
|
|
contextPathHash, err := filepath.Abs(in.ContextPath)
|
|
if err != nil {
|
|
contextPathHash = in.ContextPath
|
|
}
|
|
|
|
// TODO: this should not be loaded this side of the controller api
|
|
b, err := builder.New(dockerCli,
|
|
builder.WithName(in.Builder),
|
|
builder.WithContextPathHash(contextPathHash),
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if err = updateLastActivity(dockerCli, b.NodeGroup); err != nil {
|
|
return nil, nil, errors.Wrapf(err, "failed to update builder last activity time")
|
|
}
|
|
nodes, err := b.LoadNodes(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
var inputs *build.Inputs
|
|
buildOptions := map[string]build.Options{defaultTargetName: opts}
|
|
resp, err := buildTargets(ctx, dockerCli, nodes, buildOptions, progress, bh)
|
|
err = wrapBuildError(err, false)
|
|
if err != nil {
|
|
return nil, nil, errdefs.WrapBuild(err)
|
|
}
|
|
if i, ok := buildOptions[defaultTargetName]; ok {
|
|
inputs = &i.Inputs
|
|
}
|
|
return resp, inputs, nil
|
|
}
|
|
|
|
// buildTargets runs the specified build and returns the result.
|
|
//
|
|
// NOTE: When an error happens during the build and this function acquires the debuggable *build.ResultHandle,
|
|
// this function returns it in addition to the error (i.e. it does "return nil, res, err"). The caller can
|
|
// inspect the result and debug the cause of that error.
|
|
func buildTargets(ctx context.Context, dockerCli command.Cli, nodes []builder.Node, opts map[string]build.Options, progress progress.Writer, bh *build.Handler) (*client.SolveResponse, error) {
|
|
resp, err := build.BuildWithResultHandler(ctx, nodes, opts, dockerutil.NewClient(dockerCli), confutil.NewConfig(dockerCli), progress, bh)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp[defaultTargetName], err
|
|
}
|
|
|
|
func wrapBuildError(err error, bake bool) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
st, ok := grpcerrors.AsGRPCStatus(err)
|
|
if ok {
|
|
if st.Code() == codes.Unimplemented && strings.Contains(st.Message(), "unsupported frontend capability moby.buildkit.frontend.contexts") {
|
|
msg := "current frontend does not support --build-context."
|
|
if bake {
|
|
msg = "current frontend does not support defining additional contexts for targets."
|
|
}
|
|
msg += " Named contexts are supported since Dockerfile v1.4. Use #syntax directive in Dockerfile or update to latest BuildKit."
|
|
return &wrapped{err, msg}
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
type wrapped struct {
|
|
err error
|
|
msg string
|
|
}
|
|
|
|
func (w *wrapped) Error() string {
|
|
return w.msg
|
|
}
|
|
|
|
func (w *wrapped) Unwrap() error {
|
|
return w.err
|
|
}
|
|
|
|
func updateLastActivity(dockerCli command.Cli, ng *store.NodeGroup) error {
|
|
txn, release, err := storeutil.GetStore(dockerCli)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
return txn.UpdateLastActivity(ng)
|
|
}
|
|
|
|
func controllerUlimitOpt2DockerUlimit(u *controllerapi.UlimitOpt) *dockeropts.UlimitOpt {
|
|
if u == nil {
|
|
return nil
|
|
}
|
|
values := make(map[string]*container.Ulimit)
|
|
for k, v := range u.Values {
|
|
values[k] = &container.Ulimit{
|
|
Name: v.Name,
|
|
Hard: v.Hard,
|
|
Soft: v.Soft,
|
|
}
|
|
}
|
|
return dockeropts.NewUlimitOpt(&values)
|
|
}
|