mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-05-18 00:47:48 +08:00

Strongly typing the API allows us to perform all command line parsing fully on the client-side, where we have access to the client local directory and all the client environment variables, which may not be available on the remote server. Additionally, the controller api starts to look a lot like build.Options, so at some point in the future there may be an oppportunity to merge the two, which would allow both build and bake to execute through the controller, instead of needing to maintain multiple code paths. Signed-off-by: Justin Chadwell <me@jedevc.com>
402 lines
11 KiB
Go
402 lines
11 KiB
Go
package build
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/csv"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/docker/buildx/build"
|
|
"github.com/docker/buildx/builder"
|
|
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"
|
|
"github.com/docker/cli/cli/config"
|
|
dockeropts "github.com/docker/cli/opts"
|
|
"github.com/docker/docker/pkg/ioutils"
|
|
"github.com/docker/go-units"
|
|
"github.com/moby/buildkit/client"
|
|
"github.com/moby/buildkit/session/auth/authprovider"
|
|
"github.com/moby/buildkit/solver/errdefs"
|
|
"github.com/moby/buildkit/util/grpcerrors"
|
|
"github.com/moby/buildkit/util/progress/progressui"
|
|
"github.com/morikuni/aec"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
"google.golang.org/grpc/codes"
|
|
)
|
|
|
|
const defaultTargetName = "default"
|
|
|
|
func RunBuild(ctx context.Context, dockerCli command.Cli, in controllerapi.BuildOptions, inStream io.Reader, progressMode string, statusChan chan *client.SolveStatus) (res *build.ResultContext, err error) {
|
|
if in.Opts.NoCache && len(in.NoCacheFilter) > 0 {
|
|
return nil, errors.Errorf("--no-cache and --no-cache-filter cannot currently be used together")
|
|
}
|
|
|
|
if in.Quiet && progressMode != progress.PrinterModeAuto && progressMode != progress.PrinterModeQuiet {
|
|
return nil, errors.Errorf("progress=%s and quiet cannot be used together", progressMode)
|
|
} else if in.Quiet {
|
|
progressMode = "quiet"
|
|
}
|
|
|
|
contexts := map[string]build.NamedContext{}
|
|
for name, path := range in.NamedContexts {
|
|
contexts[name] = build.NamedContext{Path: path}
|
|
}
|
|
|
|
printFunc, err := parsePrintFunc(in.PrintFunc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
opts := build.Options{
|
|
Inputs: build.Inputs{
|
|
ContextPath: in.ContextPath,
|
|
DockerfilePath: in.DockerfileName,
|
|
InStream: inStream,
|
|
NamedContexts: contexts,
|
|
},
|
|
BuildArgs: in.BuildArgs,
|
|
ExtraHosts: in.ExtraHosts,
|
|
ImageIDFile: in.ImageIDFile,
|
|
Labels: in.Labels,
|
|
NetworkMode: in.NetworkMode,
|
|
NoCache: in.Opts.NoCache,
|
|
NoCacheFilter: in.NoCacheFilter,
|
|
Pull: in.Opts.Pull,
|
|
ShmSize: dockeropts.MemBytes(in.ShmSize),
|
|
Tags: in.Tags,
|
|
Target: in.Target,
|
|
Ulimits: controllerUlimitOpt2DockerUlimit(in.Ulimits),
|
|
PrintFunc: printFunc,
|
|
}
|
|
|
|
platforms, err := platformutil.Parse(in.Platforms)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opts.Platforms = platforms
|
|
|
|
dockerConfig := config.LoadDefaultConfigFile(os.Stderr)
|
|
opts.Session = append(opts.Session, authprovider.NewDockerAuthProvider(dockerConfig))
|
|
|
|
secrets, err := controllerapi.CreateSecrets(in.Secrets)
|
|
if err != nil {
|
|
return 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, err
|
|
}
|
|
opts.Session = append(opts.Session, ssh)
|
|
|
|
outputs, err := controllerapi.CreateExports(in.Exports)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if in.Opts.ExportPush {
|
|
if in.Opts.ExportLoad {
|
|
return nil, errors.Errorf("push and load may not be set together at the moment")
|
|
}
|
|
if len(outputs) == 0 {
|
|
outputs = []client.ExportEntry{{
|
|
Type: "image",
|
|
Attrs: map[string]string{
|
|
"push": "true",
|
|
},
|
|
}}
|
|
} else {
|
|
switch outputs[0].Type {
|
|
case "image":
|
|
outputs[0].Attrs["push"] = "true"
|
|
default:
|
|
return nil, errors.Errorf("push and %q output can't be used together", outputs[0].Type)
|
|
}
|
|
}
|
|
}
|
|
if in.Opts.ExportLoad {
|
|
if len(outputs) == 0 {
|
|
outputs = []client.ExportEntry{{
|
|
Type: "docker",
|
|
Attrs: map[string]string{},
|
|
}}
|
|
} else {
|
|
switch outputs[0].Type {
|
|
case "docker":
|
|
default:
|
|
return nil, errors.Errorf("load and %q output can't be used together", outputs[0].Type)
|
|
}
|
|
}
|
|
}
|
|
opts.Exports = outputs
|
|
|
|
inAttests := append([]string{}, in.Attests...)
|
|
if in.Opts.Provenance != "" {
|
|
inAttests = append(inAttests, buildflags.CanonicalizeAttest("provenance", in.Opts.Provenance))
|
|
}
|
|
if in.Opts.SBOM != "" {
|
|
inAttests = append(inAttests, buildflags.CanonicalizeAttest("sbom", in.Opts.SBOM))
|
|
}
|
|
opts.Attests, err = buildflags.ParseAttests(inAttests)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
opts.CacheFrom = controllerapi.CreateCaches(in.CacheFrom)
|
|
opts.CacheTo = controllerapi.CreateCaches(in.CacheTo)
|
|
|
|
allow, err := buildflags.ParseEntitlements(in.Allow)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opts.Allow = allow
|
|
|
|
// key string used for kubernetes "sticky" mode
|
|
contextPathHash, err := filepath.Abs(in.ContextPath)
|
|
if err != nil {
|
|
contextPathHash = in.ContextPath
|
|
}
|
|
|
|
b, err := builder.New(dockerCli,
|
|
builder.WithName(in.Opts.Builder),
|
|
builder.WithContextPathHash(contextPathHash),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err = updateLastActivity(dockerCli, b.NodeGroup); err != nil {
|
|
return nil, errors.Wrapf(err, "failed to update builder last activity time")
|
|
}
|
|
nodes, err := b.LoadNodes(ctx, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
imageID, res, err := buildTargets(ctx, dockerCli, b.NodeGroup, nodes, map[string]build.Options{defaultTargetName: opts}, progressMode, in.Opts.MetadataFile, statusChan)
|
|
err = wrapBuildError(err, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if in.Quiet {
|
|
fmt.Println(imageID)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func buildTargets(ctx context.Context, dockerCli command.Cli, ng *store.NodeGroup, nodes []builder.Node, opts map[string]build.Options, progressMode string, metadataFile string, statusChan chan *client.SolveStatus) (imageID string, res *build.ResultContext, err error) {
|
|
ctx2, cancel := context.WithCancel(context.TODO())
|
|
defer cancel()
|
|
|
|
printer, err := progress.NewPrinter(ctx2, os.Stderr, os.Stderr, progressMode, progressui.WithDesc(
|
|
fmt.Sprintf("building with %q instance using %s driver", ng.Name, ng.Driver),
|
|
fmt.Sprintf("%s:%s", ng.Driver, ng.Name),
|
|
))
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
var mu sync.Mutex
|
|
var idx int
|
|
resp, err := build.BuildWithResultHandler(ctx, nodes, opts, dockerutil.NewClient(dockerCli), confutil.ConfigDir(dockerCli), progress.Tee(printer, statusChan), func(driverIndex int, gotRes *build.ResultContext) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if res == nil || driverIndex < idx {
|
|
idx, res = driverIndex, gotRes
|
|
}
|
|
})
|
|
err1 := printer.Wait()
|
|
if err == nil {
|
|
err = err1
|
|
}
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
if len(metadataFile) > 0 && resp != nil {
|
|
if err := writeMetadataFile(metadataFile, decodeExporterResponse(resp[defaultTargetName].ExporterResponse)); err != nil {
|
|
return "", nil, err
|
|
}
|
|
}
|
|
|
|
printWarnings(os.Stderr, printer.Warnings(), progressMode)
|
|
|
|
for k := range resp {
|
|
if opts[k].PrintFunc != nil {
|
|
if err := printResult(opts[k].PrintFunc, resp[k].ExporterResponse); err != nil {
|
|
return "", nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return resp[defaultTargetName].ExporterResponse["containerimage.digest"], res, err
|
|
}
|
|
|
|
func printWarnings(w io.Writer, warnings []client.VertexWarning, mode string) {
|
|
if len(warnings) == 0 || mode == progress.PrinterModeQuiet {
|
|
return
|
|
}
|
|
fmt.Fprintf(w, "\n ")
|
|
sb := &bytes.Buffer{}
|
|
if len(warnings) == 1 {
|
|
fmt.Fprintf(sb, "1 warning found")
|
|
} else {
|
|
fmt.Fprintf(sb, "%d warnings found", len(warnings))
|
|
}
|
|
if logrus.GetLevel() < logrus.DebugLevel {
|
|
fmt.Fprintf(sb, " (use --debug to expand)")
|
|
}
|
|
fmt.Fprintf(sb, ":\n")
|
|
fmt.Fprint(w, aec.Apply(sb.String(), aec.YellowF))
|
|
|
|
for _, warn := range warnings {
|
|
fmt.Fprintf(w, " - %s\n", warn.Short)
|
|
if logrus.GetLevel() < logrus.DebugLevel {
|
|
continue
|
|
}
|
|
for _, d := range warn.Detail {
|
|
fmt.Fprintf(w, "%s\n", d)
|
|
}
|
|
if warn.URL != "" {
|
|
fmt.Fprintf(w, "More info: %s\n", warn.URL)
|
|
}
|
|
if warn.SourceInfo != nil && warn.Range != nil {
|
|
src := errdefs.Source{
|
|
Info: warn.SourceInfo,
|
|
Ranges: warn.Range,
|
|
}
|
|
src.Print(w)
|
|
}
|
|
fmt.Fprintf(w, "\n")
|
|
|
|
}
|
|
}
|
|
|
|
func parsePrintFunc(str string) (*build.PrintFunc, error) {
|
|
if str == "" {
|
|
return nil, nil
|
|
}
|
|
csvReader := csv.NewReader(strings.NewReader(str))
|
|
fields, err := csvReader.Read()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := &build.PrintFunc{}
|
|
for _, field := range fields {
|
|
parts := strings.SplitN(field, "=", 2)
|
|
if len(parts) == 2 {
|
|
if parts[0] == "format" {
|
|
f.Format = parts[1]
|
|
} else {
|
|
return nil, errors.Errorf("invalid print field: %s", field)
|
|
}
|
|
} else {
|
|
if f.Name != "" {
|
|
return nil, errors.Errorf("invalid print value: %s", str)
|
|
}
|
|
f.Name = field
|
|
}
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
func writeMetadataFile(filename string, dt interface{}) error {
|
|
b, err := json.MarshalIndent(dt, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ioutils.AtomicWriteFile(filename, b, 0644)
|
|
}
|
|
|
|
func decodeExporterResponse(exporterResponse map[string]string) map[string]interface{} {
|
|
out := make(map[string]interface{})
|
|
for k, v := range exporterResponse {
|
|
dt, err := base64.StdEncoding.DecodeString(v)
|
|
if err != nil {
|
|
out[k] = v
|
|
continue
|
|
}
|
|
var raw map[string]interface{}
|
|
if err = json.Unmarshal(dt, &raw); err != nil || len(raw) == 0 {
|
|
out[k] = v
|
|
continue
|
|
}
|
|
out[k] = json.RawMessage(dt)
|
|
}
|
|
return out
|
|
}
|
|
|
|
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]*units.Ulimit)
|
|
for k, v := range u.Values {
|
|
values[k] = &units.Ulimit{
|
|
Name: v.Name,
|
|
Hard: v.Hard,
|
|
Soft: v.Soft,
|
|
}
|
|
}
|
|
return dockeropts.NewUlimitOpt(&values)
|
|
}
|