Avoids the call to `os.Getenv` when it is unnecessary because it would be overwritten anyway. Removes the comments about moving environment variable parsing to a method for use by library consumers. That method exists in containerd and this set of code doesn't actually perform any parsing since the parsing of this time is done within buildkit and not on the client. Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
931 lines
24 KiB
Go
931 lines
24 KiB
Go
package commands
|
|
|
|
import (
|
|
"bytes"
|
|
"cmp"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"text/tabwriter"
|
|
|
|
"github.com/containerd/console"
|
|
"github.com/containerd/containerd/v2/pkg/epoch"
|
|
"github.com/containerd/platforms"
|
|
"github.com/docker/buildx/bake"
|
|
"github.com/docker/buildx/bake/hclparser"
|
|
"github.com/docker/buildx/build"
|
|
"github.com/docker/buildx/builder"
|
|
"github.com/docker/buildx/localstate"
|
|
"github.com/docker/buildx/util/buildflags"
|
|
"github.com/docker/buildx/util/cobrautil"
|
|
"github.com/docker/buildx/util/cobrautil/completion"
|
|
"github.com/docker/buildx/util/confutil"
|
|
"github.com/docker/buildx/util/desktop"
|
|
"github.com/docker/buildx/util/dockerutil"
|
|
"github.com/docker/buildx/util/dockerutil/dockerconfig"
|
|
"github.com/docker/buildx/util/osutil"
|
|
"github.com/docker/buildx/util/progress"
|
|
"github.com/docker/buildx/util/tracing"
|
|
"github.com/docker/buildx/util/urlutil"
|
|
"github.com/docker/cli/cli/command"
|
|
"github.com/moby/buildkit/identity"
|
|
"github.com/moby/buildkit/session/auth/authprovider"
|
|
"github.com/moby/buildkit/util/progress/progressui"
|
|
"github.com/pkg/errors"
|
|
"github.com/spf13/cobra"
|
|
"github.com/tonistiigi/go-csvvalue"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
)
|
|
|
|
const (
|
|
bakeEnvFileSeparator = "BUILDX_BAKE_PATH_SEPARATOR"
|
|
bakeEnvFilePath = "BUILDX_BAKE_FILE"
|
|
)
|
|
|
|
type bakeOptions struct {
|
|
files []string
|
|
overrides []string
|
|
vars []string
|
|
|
|
sbom string
|
|
provenance string
|
|
allow []string
|
|
|
|
builder string
|
|
metadataFile string
|
|
exportPush bool
|
|
exportLoad bool
|
|
callFunc string
|
|
|
|
print bool
|
|
list string
|
|
|
|
// TODO: remove deprecated flags
|
|
listTargets bool
|
|
listVars bool
|
|
}
|
|
|
|
func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in bakeOptions, cFlags commonFlags, filesFromEnv bool) (err error) {
|
|
mp := dockerCli.MeterProvider()
|
|
|
|
ctx, end, err := tracing.TraceCurrentCommand(ctx, append([]string{"bake"}, targets...),
|
|
attribute.String("builder", in.builder),
|
|
attribute.StringSlice("targets", targets),
|
|
attribute.StringSlice("files", in.files),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
end(err)
|
|
}()
|
|
|
|
url, cmdContext, targets := bakeArgs(targets)
|
|
if len(targets) == 0 {
|
|
targets = []string{"default"}
|
|
}
|
|
|
|
callFunc, err := buildflags.ParseCallFunc(in.callFunc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
overrides := in.overrides
|
|
if in.exportPush {
|
|
overrides = append(overrides, "*.push=true")
|
|
}
|
|
if in.exportLoad {
|
|
overrides = append(overrides, "*.load=true")
|
|
}
|
|
if callFunc != nil {
|
|
overrides = append(overrides, fmt.Sprintf("*.call=%s", callFunc.Name))
|
|
}
|
|
if cFlags.noCache != nil {
|
|
overrides = append(overrides, fmt.Sprintf("*.no-cache=%t", *cFlags.noCache))
|
|
}
|
|
if cFlags.pull != nil {
|
|
overrides = append(overrides, fmt.Sprintf("*.pull=%t", *cFlags.pull))
|
|
}
|
|
if in.sbom != "" {
|
|
overrides = append(overrides, fmt.Sprintf("*.attest=%s", buildflags.CanonicalizeAttest("sbom", in.sbom)))
|
|
}
|
|
if in.provenance != "" {
|
|
overrides = append(overrides, fmt.Sprintf("*.attest=%s", buildflags.CanonicalizeAttest("provenance", in.provenance)))
|
|
}
|
|
contextPathHash, _ := os.Getwd()
|
|
|
|
ent, err := bake.ParseEntitlements(in.allow)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to get current working directory")
|
|
}
|
|
// filesystem access under the current working directory is allowed by default
|
|
ent.FSRead = append(ent.FSRead, wd)
|
|
ent.FSWrite = append(ent.FSWrite, wd)
|
|
|
|
ctx2, cancel := context.WithCancelCause(context.TODO())
|
|
defer cancel(errors.WithStack(context.Canceled))
|
|
|
|
var nodes []builder.Node
|
|
var progressConsoleDesc, progressTextDesc string
|
|
|
|
if in.print && in.list != "" {
|
|
return errors.New("--print and --list are mutually exclusive")
|
|
}
|
|
|
|
// instance only needed for reading remote bake files or building
|
|
var driverType string
|
|
if url != "" || (!in.print && in.list == "") {
|
|
b, err := builder.New(dockerCli,
|
|
builder.WithName(in.builder),
|
|
builder.WithContextPathHash(contextPathHash),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = updateLastActivity(dockerCli, b.NodeGroup); err != nil {
|
|
return errors.Wrapf(err, "failed to update builder last activity time")
|
|
}
|
|
nodes, err = b.LoadNodes(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
progressConsoleDesc = fmt.Sprintf("%s:%s", b.Driver, b.Name)
|
|
progressTextDesc = fmt.Sprintf("building with %q instance using %s driver", b.Name, b.Driver)
|
|
driverType = b.Driver
|
|
}
|
|
|
|
var term bool
|
|
if _, err := console.ConsoleFromFile(os.Stderr); err == nil {
|
|
term = true
|
|
}
|
|
attributes := bakeMetricAttributes(dockerCli, driverType, url, cmdContext, targets, &in)
|
|
|
|
progressMode := progressui.DisplayMode(cFlags.progress)
|
|
|
|
var printer *progress.Printer
|
|
defer func() {
|
|
if printer != nil {
|
|
printer.Wait()
|
|
}
|
|
}()
|
|
|
|
makePrinter := func() error {
|
|
var err error
|
|
printer, err = progress.NewPrinter(ctx2, os.Stderr, progressMode,
|
|
progress.WithDesc(progressTextDesc, progressConsoleDesc),
|
|
progress.WithMetrics(mp, attributes),
|
|
progress.WithOnClose(func() {
|
|
printWarnings(os.Stderr, printer.Warnings(), progressMode)
|
|
}),
|
|
)
|
|
return err
|
|
}
|
|
|
|
if err := makePrinter(); err != nil {
|
|
return err
|
|
}
|
|
|
|
files, inp, err := readBakeFiles(ctx, nodes, url, in.files, dockerCli.In(), printer, filesFromEnv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(files) == 0 {
|
|
return errors.New("couldn't find a bake definition")
|
|
}
|
|
|
|
defaults := map[string]string{
|
|
// don't forget to update documentation if you add a new
|
|
// built-in variable: docs/bake-reference.md#built-in-variables
|
|
"BAKE_CMD_CONTEXT": cmdContext,
|
|
"BAKE_LOCAL_PLATFORM": platforms.Format(platforms.DefaultSpec()),
|
|
}
|
|
vars, err := parseBakeVars(in.vars)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if in.list != "" {
|
|
cfg, pm, err := bake.ParseFiles(files, defaults, vars)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = printer.Wait(); err != nil {
|
|
return err
|
|
}
|
|
list, err := parseList(in.list)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch list.Type {
|
|
case "targets":
|
|
return printTargetList(dockerCli.Out(), list.Format, cfg)
|
|
case "variables":
|
|
return printVars(dockerCli.Out(), list.Format, pm.AllVariables)
|
|
}
|
|
}
|
|
|
|
tgts, grps, err := bake.ReadTargets(ctx, files, targets, overrides, defaults, vars, &ent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var sourceDateEpoch *string
|
|
for _, t := range tgts {
|
|
if _, ok := t.Args[epoch.SourceDateEpochEnv]; ok {
|
|
continue
|
|
}
|
|
|
|
v := os.Getenv(epoch.SourceDateEpochEnv)
|
|
sourceDateEpoch = &v
|
|
|
|
if *sourceDateEpoch != "" {
|
|
if t.Args == nil {
|
|
t.Args = map[string]*string{}
|
|
}
|
|
t.Args[epoch.SourceDateEpochEnv] = sourceDateEpoch
|
|
}
|
|
}
|
|
|
|
// this function can update target context string from the input so call before printOnly check
|
|
bo, err := bake.TargetsToBuildOpt(tgts, inp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// make sure local credentials aren't loaded multiple times for different targets
|
|
authProvider := authprovider.NewDockerAuthProvider(authprovider.DockerAuthProviderConfig{
|
|
AuthConfigProvider: dockerconfig.LoadAuthConfig(dockerCli),
|
|
})
|
|
|
|
for k, opt := range bo {
|
|
opt.Session = append(opt.Session, authProvider)
|
|
bo[k] = opt
|
|
}
|
|
|
|
def := struct {
|
|
Group map[string]*bake.Group `json:"group,omitempty"`
|
|
Target map[string]*bake.Target `json:"target"`
|
|
}{
|
|
Group: grps,
|
|
Target: tgts,
|
|
}
|
|
|
|
if in.print {
|
|
if err = printer.Wait(); err != nil {
|
|
return err
|
|
}
|
|
dtdef, err := json.MarshalIndent(def, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = fmt.Fprintln(dockerCli.Out(), string(dtdef))
|
|
return err
|
|
}
|
|
|
|
for k, opt := range bo {
|
|
if opt.CallFunc != nil {
|
|
cf, err := buildflags.ParseCallFunc(opt.CallFunc.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cf == nil {
|
|
opt.CallFunc = nil
|
|
bo[k] = opt
|
|
} else {
|
|
opt.CallFunc.Name = cf.Name
|
|
}
|
|
}
|
|
}
|
|
|
|
exp, err := ent.Validate(bo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if progressMode != progressui.RawJSONMode {
|
|
if err := exp.Prompt(ctx, url != "", &syncWriter{w: dockerCli.Err(), wait: printer.Wait}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if printer.IsDone() {
|
|
// init new printer as old one was stopped to show the prompt
|
|
if err := makePrinter(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := saveLocalStateGroup(dockerCli, in, targets, bo); err != nil {
|
|
return err
|
|
}
|
|
|
|
done := timeBuildCommand(mp, attributes)
|
|
resp, retErr := build.Build(ctx, nodes, bo, dockerutil.NewClient(dockerCli), confutil.NewConfig(dockerCli), printer)
|
|
if err := printer.Wait(); retErr == nil {
|
|
retErr = err
|
|
}
|
|
if retErr != nil {
|
|
err = wrapBuildError(retErr, true)
|
|
}
|
|
done(err)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if progressMode != progressui.QuietMode && progressMode != progressui.RawJSONMode {
|
|
desktop.PrintBuildDetails(os.Stderr, printer.BuildRefs(), term)
|
|
}
|
|
if len(in.metadataFile) > 0 {
|
|
dt := make(map[string]any)
|
|
for t, r := range resp {
|
|
dt[t] = decodeExporterResponse(r.ExporterResponse)
|
|
}
|
|
if callFunc == nil {
|
|
if warnings := printer.Warnings(); len(warnings) > 0 && confutil.MetadataWarningsEnabled() {
|
|
dt["buildx.build.warnings"] = warnings
|
|
}
|
|
}
|
|
if err := writeMetadataFile(in.metadataFile, dt); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var callFormatJSON bool
|
|
jsonResults := map[string]map[string]any{}
|
|
if callFunc != nil {
|
|
callFormatJSON = callFunc.Format == "json"
|
|
}
|
|
var sep bool
|
|
var exitCode int
|
|
|
|
names := make([]string, 0, len(bo))
|
|
for name := range bo {
|
|
names = append(names, name)
|
|
}
|
|
slices.Sort(names)
|
|
|
|
for _, name := range names {
|
|
req := bo[name]
|
|
if req.CallFunc == nil {
|
|
continue
|
|
}
|
|
|
|
pf := &buildflags.CallFunc{
|
|
Name: req.CallFunc.Name,
|
|
Format: req.CallFunc.Format,
|
|
IgnoreStatus: req.CallFunc.IgnoreStatus,
|
|
}
|
|
|
|
if callFunc != nil {
|
|
pf.Format = callFunc.Format
|
|
pf.IgnoreStatus = callFunc.IgnoreStatus
|
|
}
|
|
|
|
var res map[string]string
|
|
if sp, ok := resp[name]; ok {
|
|
res = sp.ExporterResponse
|
|
}
|
|
|
|
if callFormatJSON {
|
|
jsonResults[name] = map[string]any{}
|
|
buf := &bytes.Buffer{}
|
|
if code, err := printResult(buf, pf, res, name, &req.Inputs); err != nil {
|
|
jsonResults[name]["error"] = err.Error()
|
|
exitCode = 1
|
|
} else if code != 0 && exitCode == 0 {
|
|
exitCode = code
|
|
}
|
|
m := map[string]*json.RawMessage{}
|
|
if err := json.Unmarshal(buf.Bytes(), &m); err == nil {
|
|
for k, v := range m {
|
|
jsonResults[name][k] = v
|
|
}
|
|
} else {
|
|
jsonResults[name][pf.Name] = json.RawMessage(buf.Bytes())
|
|
}
|
|
} else {
|
|
if sep {
|
|
fmt.Fprintln(dockerCli.Out())
|
|
} else {
|
|
sep = true
|
|
}
|
|
fmt.Fprintf(dockerCli.Out(), "%s\n", name)
|
|
if descr := tgts[name].Description; descr != "" {
|
|
fmt.Fprintf(dockerCli.Out(), "%s\n", descr)
|
|
}
|
|
|
|
fmt.Fprintln(dockerCli.Out())
|
|
if code, err := printResult(dockerCli.Out(), pf, res, name, &req.Inputs); err != nil {
|
|
fmt.Fprintf(dockerCli.Out(), "error: %v\n", err)
|
|
exitCode = 1
|
|
} else if code != 0 && exitCode == 0 {
|
|
exitCode = code
|
|
}
|
|
}
|
|
}
|
|
if callFormatJSON {
|
|
out := struct {
|
|
Group map[string]*bake.Group `json:"group,omitempty"`
|
|
Target map[string]map[string]any `json:"target"`
|
|
}{
|
|
Group: grps,
|
|
Target: map[string]map[string]any{},
|
|
}
|
|
|
|
for name, def := range tgts {
|
|
out.Target[name] = map[string]any{
|
|
"build": def,
|
|
}
|
|
if res, ok := jsonResults[name]; ok {
|
|
printName := bo[name].CallFunc.Name
|
|
if printName == "lint" {
|
|
printName = "check"
|
|
}
|
|
out.Target[name][printName] = res
|
|
}
|
|
}
|
|
dt, err := json.MarshalIndent(out, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintln(dockerCli.Out(), string(dt))
|
|
}
|
|
|
|
for _, name := range names {
|
|
if sp, ok := resp[name]; ok {
|
|
if v, ok := sp.ExporterResponse["frontend.result.inlinemessage"]; ok {
|
|
fmt.Fprintf(dockerCli.Out(), "\n# %s\n%s\n", name, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
if exitCode != 0 {
|
|
return cobrautil.ExitCodeError(exitCode)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func bakeCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command {
|
|
var options bakeOptions
|
|
var cFlags commonFlags
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "bake [OPTIONS] [TARGET...]",
|
|
Aliases: []string{"f"},
|
|
Short: "Build from a file",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
filesFromEnv := false
|
|
if len(options.files) == 0 {
|
|
if envFiles, err := bakeEnvFiles(os.LookupEnv); err != nil {
|
|
return err
|
|
} else if len(envFiles) > 0 {
|
|
options.files = envFiles
|
|
filesFromEnv = true
|
|
}
|
|
}
|
|
// reset to nil to avoid override is unset
|
|
if !cmd.Flags().Lookup("no-cache").Changed {
|
|
cFlags.noCache = nil
|
|
}
|
|
if !cmd.Flags().Lookup("pull").Changed {
|
|
cFlags.pull = nil
|
|
}
|
|
if options.list == "" {
|
|
if options.listTargets {
|
|
options.list = "targets"
|
|
} else if options.listVars {
|
|
options.list = "variables"
|
|
}
|
|
}
|
|
options.builder = rootOpts.builder
|
|
options.metadataFile = cFlags.metadataFile
|
|
// Other common flags (noCache, pull and progress) are processed in runBake function.
|
|
return runBake(cmd.Context(), dockerCli, args, options, cFlags, filesFromEnv)
|
|
},
|
|
ValidArgsFunction: completion.BakeTargets(options.files),
|
|
DisableFlagsInUseLine: true,
|
|
}
|
|
|
|
flags := cmd.Flags()
|
|
|
|
flags.StringArrayVarP(&options.files, "file", "f", []string{}, "Build definition file")
|
|
flags.BoolVar(&options.exportLoad, "load", false, `Shorthand for "--set=*.output=type=docker". Conditional.`)
|
|
flags.BoolVar(&options.exportPush, "push", false, `Shorthand for "--set=*.output=type=registry". Conditional.`)
|
|
flags.StringVar(&options.sbom, "sbom", "", `Shorthand for "--set=*.attest=type=sbom"`)
|
|
flags.StringVar(&options.provenance, "provenance", "", `Shorthand for "--set=*.attest=type=provenance"`)
|
|
flags.StringArrayVar(&options.overrides, "set", nil, `Override target value (e.g., "targetpattern.key=value")`)
|
|
flags.StringArrayVar(&options.vars, "var", nil, `Set a variable value (e.g., "name=value")`)
|
|
flags.StringVar(&options.callFunc, "call", "build", `Set method for evaluating build ("check", "outline", "targets")`)
|
|
flags.StringArrayVar(&options.allow, "allow", nil, "Allow build to access specified resources")
|
|
|
|
flags.VarPF(callAlias(&options.callFunc, "check"), "check", "", `Shorthand for "--call=check"`)
|
|
flags.Lookup("check").NoOptDefVal = "true"
|
|
|
|
flags.BoolVar(&options.print, "print", false, "Print the options without building")
|
|
flags.StringVar(&options.list, "list", "", "List targets or variables")
|
|
|
|
// TODO: remove deprecated flags
|
|
flags.BoolVar(&options.listTargets, "list-targets", false, "List available targets")
|
|
flags.MarkHidden("list-targets")
|
|
flags.MarkDeprecated("list-targets", "list-targets is deprecated, use list=targets instead")
|
|
flags.BoolVar(&options.listVars, "list-variables", false, "List defined variables")
|
|
flags.MarkHidden("list-variables")
|
|
flags.MarkDeprecated("list-variables", "list-variables is deprecated, use list=variables instead")
|
|
|
|
commonBuildFlags(&cFlags, flags)
|
|
|
|
return cmd
|
|
}
|
|
|
|
func bakeEnvFiles(lookup func(string string) (string, bool)) ([]string, error) {
|
|
sep, _ := lookup(bakeEnvFileSeparator)
|
|
if sep == "" {
|
|
sep = string(os.PathListSeparator)
|
|
}
|
|
f, ok := lookup(bakeEnvFilePath)
|
|
if ok {
|
|
return cleanPaths(strings.Split(f, sep))
|
|
}
|
|
return []string{}, nil
|
|
}
|
|
|
|
func cleanPaths(p []string) ([]string, error) {
|
|
var paths []string
|
|
for _, f := range p {
|
|
f = strings.TrimSpace(f)
|
|
if f == "" {
|
|
continue
|
|
}
|
|
if f == "-" {
|
|
paths = append(paths, f)
|
|
continue
|
|
}
|
|
if _, err := os.Stat(f); err != nil {
|
|
return nil, err
|
|
}
|
|
paths = append(paths, f)
|
|
}
|
|
return paths, nil
|
|
}
|
|
|
|
func saveLocalStateGroup(dockerCli command.Cli, in bakeOptions, targets []string, bo map[string]build.Options) error {
|
|
l, err := localstate.New(confutil.NewConfig(dockerCli))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer l.MigrateIfNeeded()
|
|
|
|
prm := confutil.MetadataProvenance()
|
|
if len(in.metadataFile) == 0 {
|
|
prm = confutil.MetadataProvenanceModeDisabled
|
|
}
|
|
groupRef := identity.NewID()
|
|
refs := make([]string, 0, len(bo))
|
|
for k, b := range bo {
|
|
if b.CallFunc != nil {
|
|
continue
|
|
}
|
|
b.Ref = identity.NewID()
|
|
b.GroupRef = groupRef
|
|
b.ProvenanceResponseMode = prm
|
|
refs = append(refs, b.Ref)
|
|
bo[k] = b
|
|
}
|
|
if len(refs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return l.SaveGroup(groupRef, localstate.StateGroup{
|
|
Refs: refs,
|
|
Targets: targets,
|
|
})
|
|
}
|
|
|
|
// bakeArgs will retrieve the remote url, command context, and targets
|
|
// from the command line arguments.
|
|
func bakeArgs(args []string) (url, cmdContext string, targets []string) {
|
|
cmdContext, targets = "cwd://", args
|
|
if len(targets) == 0 || !urlutil.IsRemoteURL(targets[0]) {
|
|
return url, cmdContext, targets
|
|
}
|
|
url, targets = targets[0], targets[1:]
|
|
if len(targets) == 0 || !urlutil.IsRemoteURL(targets[0]) {
|
|
return url, cmdContext, targets
|
|
}
|
|
cmdContext, targets = targets[0], targets[1:]
|
|
return url, cmdContext, targets
|
|
}
|
|
|
|
func readBakeFiles(ctx context.Context, nodes []builder.Node, url string, names []string, stdin io.Reader, pw progress.Writer, filesFromEnv bool) (files []bake.File, inp *bake.Input, err error) {
|
|
var lnames []string // local
|
|
var rnames []string // remote
|
|
var anames []string // both
|
|
for _, v := range names {
|
|
if tname, ok := strings.CutPrefix(v, "cwd://"); ok {
|
|
lnames = append(lnames, tname)
|
|
anames = append(anames, tname)
|
|
} else {
|
|
rnames = append(rnames, v)
|
|
anames = append(anames, v)
|
|
}
|
|
}
|
|
|
|
if url != "" {
|
|
var rfiles []bake.File
|
|
rfiles, inp, err = bake.ReadRemoteFiles(ctx, nodes, url, rnames, pw)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
files = append(files, rfiles...)
|
|
}
|
|
|
|
if len(lnames) > 0 || url == "" {
|
|
var lfiles []bake.File
|
|
where := ""
|
|
if filesFromEnv {
|
|
where = " from " + bakeEnvFilePath + " env"
|
|
}
|
|
progress.Wrap("[internal] load local bake definitions"+where, pw.Write, func(sub progress.SubLogger) error {
|
|
if url != "" {
|
|
lfiles, err = bake.ReadLocalFiles(lnames, stdin, sub)
|
|
} else {
|
|
lfiles, err = bake.ReadLocalFiles(anames, stdin, sub)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
files = append(files, lfiles...)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
type listEntry struct {
|
|
Type string
|
|
Format string
|
|
}
|
|
|
|
func parseList(input string) (listEntry, error) {
|
|
res := listEntry{}
|
|
|
|
fields, err := csvvalue.Fields(input, nil)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
|
|
if len(fields) == 1 && fields[0] == input && !strings.HasPrefix(input, "type=") {
|
|
res.Type = input
|
|
}
|
|
|
|
if res.Type == "" {
|
|
for _, field := range fields {
|
|
key, value, ok := strings.Cut(field, "=")
|
|
if !ok {
|
|
return res, errors.Errorf("invalid value %s", field)
|
|
}
|
|
key = strings.TrimSpace(strings.ToLower(key))
|
|
switch key {
|
|
case "type":
|
|
res.Type = value
|
|
case "format":
|
|
res.Format = value
|
|
default:
|
|
return res, errors.Errorf("unexpected key '%s' in '%s'", key, field)
|
|
}
|
|
}
|
|
}
|
|
if res.Format == "" {
|
|
res.Format = "table"
|
|
}
|
|
|
|
switch res.Type {
|
|
case "targets", "variables":
|
|
default:
|
|
return res, errors.Errorf("invalid list type %q", res.Type)
|
|
}
|
|
|
|
switch res.Format {
|
|
case "table", "json":
|
|
default:
|
|
return res, errors.Errorf("invalid list format %q", res.Format)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func parseBakeVars(values []string) (map[string]string, error) {
|
|
if len(values) == 0 {
|
|
return nil, nil
|
|
}
|
|
vars := make(map[string]string, len(values))
|
|
for _, value := range values {
|
|
if value == "" {
|
|
continue
|
|
}
|
|
key, val, ok := strings.Cut(value, "=")
|
|
if !ok {
|
|
return nil, errors.Errorf("invalid variable %q, expected key=value", value)
|
|
}
|
|
if key == "" {
|
|
return nil, errors.Errorf("invalid variable %q, key is empty", value)
|
|
}
|
|
vars[key] = val
|
|
}
|
|
return vars, nil
|
|
}
|
|
|
|
func printVars(w io.Writer, format string, vars []*hclparser.Variable) error {
|
|
slices.SortFunc(vars, func(a, b *hclparser.Variable) int {
|
|
return cmp.Compare(a.Name, b.Name)
|
|
})
|
|
|
|
if format == "json" {
|
|
enc := json.NewEncoder(w)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(vars)
|
|
}
|
|
|
|
tw := tabwriter.NewWriter(w, 1, 8, 1, '\t', 0)
|
|
defer tw.Flush()
|
|
|
|
tw.Write([]byte("VARIABLE\tTYPE\tVALUE\tDESCRIPTION\n"))
|
|
|
|
for _, v := range vars {
|
|
var value string
|
|
if v.Value != nil {
|
|
value = *v.Value
|
|
} else {
|
|
value = "<null>"
|
|
}
|
|
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", v.Name, v.Type, value, v.Description)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func printTargetList(w io.Writer, format string, cfg *bake.Config) error {
|
|
type targetOrGroup struct {
|
|
name string
|
|
target *bake.Target
|
|
group *bake.Group
|
|
}
|
|
|
|
list := make([]targetOrGroup, 0, len(cfg.Targets)+len(cfg.Groups))
|
|
for _, tgt := range cfg.Targets {
|
|
list = append(list, targetOrGroup{name: tgt.Name, target: tgt})
|
|
}
|
|
for _, grp := range cfg.Groups {
|
|
list = append(list, targetOrGroup{name: grp.Name, group: grp})
|
|
}
|
|
|
|
slices.SortFunc(list, func(a, b targetOrGroup) int {
|
|
return cmp.Compare(a.name, b.name)
|
|
})
|
|
|
|
var tw *tabwriter.Writer
|
|
if format == "table" {
|
|
tw = tabwriter.NewWriter(w, 1, 8, 1, '\t', 0)
|
|
defer tw.Flush()
|
|
tw.Write([]byte("TARGET\tDESCRIPTION\n"))
|
|
}
|
|
|
|
type targetList struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
Group bool `json:"group,omitempty"`
|
|
}
|
|
var targetsList []targetList
|
|
|
|
for _, tgt := range list {
|
|
if strings.HasPrefix(tgt.name, "_") {
|
|
// convention for a private target
|
|
continue
|
|
}
|
|
var descr string
|
|
if tgt.target != nil {
|
|
descr = tgt.target.Description
|
|
targetsList = append(targetsList, targetList{Name: tgt.name, Description: descr})
|
|
} else if tgt.group != nil {
|
|
descr = tgt.group.Description
|
|
if len(tgt.group.Targets) > 0 {
|
|
slices.Sort(tgt.group.Targets)
|
|
names := strings.Join(tgt.group.Targets, ", ")
|
|
if descr != "" {
|
|
descr += " (" + names + ")"
|
|
} else {
|
|
descr = names
|
|
}
|
|
}
|
|
targetsList = append(targetsList, targetList{Name: tgt.name, Description: descr, Group: true})
|
|
}
|
|
if format == "table" {
|
|
fmt.Fprintf(tw, "%s\t%s\n", tgt.name, descr)
|
|
}
|
|
}
|
|
|
|
if format == "json" {
|
|
enc := json.NewEncoder(w)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(targetsList)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func bakeMetricAttributes(dockerCli command.Cli, driverType, url, cmdContext string, targets []string, options *bakeOptions) attribute.Set {
|
|
return attribute.NewSet(
|
|
commandNameAttribute.String("bake"),
|
|
attribute.Stringer(string(commandOptionsHash), &bakeOptionsHash{
|
|
bakeOptions: options,
|
|
cfg: confutil.NewConfig(dockerCli),
|
|
url: url,
|
|
cmdContext: cmdContext,
|
|
targets: targets,
|
|
}),
|
|
driverNameAttribute.String(options.builder),
|
|
driverTypeAttribute.String(driverType),
|
|
)
|
|
}
|
|
|
|
type bakeOptionsHash struct {
|
|
*bakeOptions
|
|
cfg *confutil.Config
|
|
url string
|
|
cmdContext string
|
|
targets []string
|
|
result string
|
|
resultOnce sync.Once
|
|
}
|
|
|
|
func (o *bakeOptionsHash) String() string {
|
|
o.resultOnce.Do(func() {
|
|
url := o.url
|
|
cmdContext := o.cmdContext
|
|
if cmdContext == "cwd://" {
|
|
// Resolve the directory if the cmdContext is the current working directory.
|
|
cmdContext = osutil.GetWd()
|
|
}
|
|
|
|
// Sort the inputs for files and targets since the ordering
|
|
// doesn't matter, but avoid modifying the original slice.
|
|
files := immutableSort(o.files)
|
|
targets := immutableSort(o.targets)
|
|
|
|
joinedFiles := strings.Join(files, ",")
|
|
joinedTargets := strings.Join(targets, ",")
|
|
salt := o.cfg.TryNodeIdentifier()
|
|
|
|
h := sha256.New()
|
|
for _, s := range []string{url, cmdContext, joinedFiles, joinedTargets, salt} {
|
|
_, _ = io.WriteString(h, s)
|
|
h.Write([]byte{0})
|
|
}
|
|
o.result = hex.EncodeToString(h.Sum(nil))
|
|
})
|
|
return o.result
|
|
}
|
|
|
|
// immutableSort will sort the entries in s without modifying the original slice.
|
|
func immutableSort(s []string) []string {
|
|
if !sort.StringsAreSorted(s) {
|
|
cpy := make([]string, len(s))
|
|
copy(cpy, s)
|
|
sort.Strings(cpy)
|
|
return cpy
|
|
}
|
|
return s
|
|
}
|
|
|
|
type syncWriter struct {
|
|
w io.Writer
|
|
once sync.Once
|
|
wait func() error
|
|
}
|
|
|
|
func (w *syncWriter) Write(p []byte) (n int, err error) {
|
|
w.once.Do(func() {
|
|
if w.wait != nil {
|
|
err = w.wait()
|
|
}
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return w.w.Write(p)
|
|
}
|