github.com/pdmccormick/importable-docker-buildx@v0.0.0-20240426161518-e47091289030/commands/bake.go (about)

     1  package commands
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/containerd/console"
    12  	"github.com/containerd/containerd/platforms"
    13  	"github.com/docker/buildx/bake"
    14  	"github.com/docker/buildx/build"
    15  	"github.com/docker/buildx/builder"
    16  	"github.com/docker/buildx/localstate"
    17  	"github.com/docker/buildx/util/buildflags"
    18  	"github.com/docker/buildx/util/cobrautil/completion"
    19  	"github.com/docker/buildx/util/confutil"
    20  	"github.com/docker/buildx/util/desktop"
    21  	"github.com/docker/buildx/util/dockerutil"
    22  	"github.com/docker/buildx/util/progress"
    23  	"github.com/docker/buildx/util/tracing"
    24  	"github.com/docker/cli/cli/command"
    25  	"github.com/moby/buildkit/identity"
    26  	"github.com/moby/buildkit/util/progress/progressui"
    27  	"github.com/pkg/errors"
    28  	"github.com/spf13/cobra"
    29  )
    30  
    31  type bakeOptions struct {
    32  	files      []string
    33  	overrides  []string
    34  	printOnly  bool
    35  	sbom       string
    36  	provenance string
    37  
    38  	builder      string
    39  	metadataFile string
    40  	exportPush   bool
    41  	exportLoad   bool
    42  }
    43  
    44  func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in bakeOptions, cFlags commonFlags) (err error) {
    45  	ctx, end, err := tracing.TraceCurrentCommand(ctx, "bake")
    46  	if err != nil {
    47  		return err
    48  	}
    49  	defer func() {
    50  		end(err)
    51  	}()
    52  
    53  	var url string
    54  	cmdContext := "cwd://"
    55  
    56  	if len(targets) > 0 {
    57  		if build.IsRemoteURL(targets[0]) {
    58  			url = targets[0]
    59  			targets = targets[1:]
    60  			if len(targets) > 0 {
    61  				if build.IsRemoteURL(targets[0]) {
    62  					cmdContext = targets[0]
    63  					targets = targets[1:]
    64  				}
    65  			}
    66  		}
    67  	}
    68  
    69  	if len(targets) == 0 {
    70  		targets = []string{"default"}
    71  	}
    72  
    73  	overrides := in.overrides
    74  	if in.exportPush {
    75  		overrides = append(overrides, "*.push=true")
    76  	}
    77  	if in.exportLoad {
    78  		overrides = append(overrides, "*.load=true")
    79  	}
    80  	if cFlags.noCache != nil {
    81  		overrides = append(overrides, fmt.Sprintf("*.no-cache=%t", *cFlags.noCache))
    82  	}
    83  	if cFlags.pull != nil {
    84  		overrides = append(overrides, fmt.Sprintf("*.pull=%t", *cFlags.pull))
    85  	}
    86  	if in.sbom != "" {
    87  		overrides = append(overrides, fmt.Sprintf("*.attest=%s", buildflags.CanonicalizeAttest("sbom", in.sbom)))
    88  	}
    89  	if in.provenance != "" {
    90  		overrides = append(overrides, fmt.Sprintf("*.attest=%s", buildflags.CanonicalizeAttest("provenance", in.provenance)))
    91  	}
    92  	contextPathHash, _ := os.Getwd()
    93  
    94  	ctx2, cancel := context.WithCancel(context.TODO())
    95  	defer cancel()
    96  
    97  	var nodes []builder.Node
    98  	var progressConsoleDesc, progressTextDesc string
    99  
   100  	// instance only needed for reading remote bake files or building
   101  	if url != "" || !in.printOnly {
   102  		b, err := builder.New(dockerCli,
   103  			builder.WithName(in.builder),
   104  			builder.WithContextPathHash(contextPathHash),
   105  		)
   106  		if err != nil {
   107  			return err
   108  		}
   109  		if err = updateLastActivity(dockerCli, b.NodeGroup); err != nil {
   110  			return errors.Wrapf(err, "failed to update builder last activity time")
   111  		}
   112  		nodes, err = b.LoadNodes(ctx)
   113  		if err != nil {
   114  			return err
   115  		}
   116  		progressConsoleDesc = fmt.Sprintf("%s:%s", b.Driver, b.Name)
   117  		progressTextDesc = fmt.Sprintf("building with %q instance using %s driver", b.Name, b.Driver)
   118  	}
   119  
   120  	var term bool
   121  	if _, err := console.ConsoleFromFile(os.Stderr); err == nil {
   122  		term = true
   123  	}
   124  
   125  	progressMode := progressui.DisplayMode(cFlags.progress)
   126  	printer, err := progress.NewPrinter(ctx2, os.Stderr, progressMode,
   127  		progress.WithDesc(progressTextDesc, progressConsoleDesc),
   128  	)
   129  	if err != nil {
   130  		return err
   131  	}
   132  
   133  	defer func() {
   134  		if printer != nil {
   135  			err1 := printer.Wait()
   136  			if err == nil {
   137  				err = err1
   138  			}
   139  			if err == nil && progressMode != progressui.QuietMode && progressMode != progressui.RawJSONMode {
   140  				desktop.PrintBuildDetails(os.Stderr, printer.BuildRefs(), term)
   141  			}
   142  		}
   143  	}()
   144  
   145  	files, inp, err := readBakeFiles(ctx, nodes, url, in.files, dockerCli.In(), printer)
   146  	if err != nil {
   147  		return err
   148  	}
   149  
   150  	if len(files) == 0 {
   151  		return errors.New("couldn't find a bake definition")
   152  	}
   153  
   154  	tgts, grps, err := bake.ReadTargets(ctx, files, targets, overrides, map[string]string{
   155  		// don't forget to update documentation if you add a new
   156  		// built-in variable: docs/bake-reference.md#built-in-variables
   157  		"BAKE_CMD_CONTEXT":    cmdContext,
   158  		"BAKE_LOCAL_PLATFORM": platforms.DefaultString(),
   159  	})
   160  	if err != nil {
   161  		return err
   162  	}
   163  
   164  	if v := os.Getenv("SOURCE_DATE_EPOCH"); v != "" {
   165  		// TODO: extract env var parsing to a method easily usable by library consumers
   166  		for _, t := range tgts {
   167  			if _, ok := t.Args["SOURCE_DATE_EPOCH"]; ok {
   168  				continue
   169  			}
   170  			if t.Args == nil {
   171  				t.Args = map[string]*string{}
   172  			}
   173  			t.Args["SOURCE_DATE_EPOCH"] = &v
   174  		}
   175  	}
   176  
   177  	// this function can update target context string from the input so call before printOnly check
   178  	bo, err := bake.TargetsToBuildOpt(tgts, inp)
   179  	if err != nil {
   180  		return err
   181  	}
   182  
   183  	def := struct {
   184  		Group  map[string]*bake.Group  `json:"group,omitempty"`
   185  		Target map[string]*bake.Target `json:"target"`
   186  	}{
   187  		Group:  grps,
   188  		Target: tgts,
   189  	}
   190  
   191  	if in.printOnly {
   192  		dt, err := json.MarshalIndent(def, "", "  ")
   193  		if err != nil {
   194  			return err
   195  		}
   196  		err = printer.Wait()
   197  		printer = nil
   198  		if err != nil {
   199  			return err
   200  		}
   201  		fmt.Fprintln(dockerCli.Out(), string(dt))
   202  		return nil
   203  	}
   204  
   205  	groupRef := identity.NewID()
   206  	var refs []string
   207  	for k, b := range bo {
   208  		b.Ref = identity.NewID()
   209  		b.GroupRef = groupRef
   210  		b.WithProvenanceResponse = len(in.metadataFile) > 0
   211  		refs = append(refs, b.Ref)
   212  		bo[k] = b
   213  	}
   214  	dt, err := json.Marshal(def)
   215  	if err != nil {
   216  		return err
   217  	}
   218  	if err := saveLocalStateGroup(dockerCli, groupRef, localstate.StateGroup{
   219  		Definition: dt,
   220  		Targets:    targets,
   221  		Inputs:     overrides,
   222  		Refs:       refs,
   223  	}); err != nil {
   224  		return err
   225  	}
   226  
   227  	resp, err := build.Build(ctx, nodes, bo, dockerutil.NewClient(dockerCli), confutil.ConfigDir(dockerCli), printer)
   228  	if err != nil {
   229  		return wrapBuildError(err, true)
   230  	}
   231  
   232  	if len(in.metadataFile) > 0 {
   233  		dt := make(map[string]interface{})
   234  		for t, r := range resp {
   235  			dt[t] = decodeExporterResponse(r.ExporterResponse)
   236  		}
   237  		if err := writeMetadataFile(in.metadataFile, dt); err != nil {
   238  			return err
   239  		}
   240  	}
   241  
   242  	return err
   243  }
   244  
   245  func bakeCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command {
   246  	var options bakeOptions
   247  	var cFlags commonFlags
   248  
   249  	cmd := &cobra.Command{
   250  		Use:     "bake [OPTIONS] [TARGET...]",
   251  		Aliases: []string{"f"},
   252  		Short:   "Build from a file",
   253  		RunE: func(cmd *cobra.Command, args []string) error {
   254  			// reset to nil to avoid override is unset
   255  			if !cmd.Flags().Lookup("no-cache").Changed {
   256  				cFlags.noCache = nil
   257  			}
   258  			if !cmd.Flags().Lookup("pull").Changed {
   259  				cFlags.pull = nil
   260  			}
   261  			options.builder = rootOpts.builder
   262  			options.metadataFile = cFlags.metadataFile
   263  			// Other common flags (noCache, pull and progress) are processed in runBake function.
   264  			return runBake(cmd.Context(), dockerCli, args, options, cFlags)
   265  		},
   266  		ValidArgsFunction: completion.BakeTargets(options.files),
   267  	}
   268  
   269  	flags := cmd.Flags()
   270  
   271  	flags.StringArrayVarP(&options.files, "file", "f", []string{}, "Build definition file")
   272  	flags.BoolVar(&options.exportLoad, "load", false, `Shorthand for "--set=*.output=type=docker"`)
   273  	flags.BoolVar(&options.printOnly, "print", false, "Print the options without building")
   274  	flags.BoolVar(&options.exportPush, "push", false, `Shorthand for "--set=*.output=type=registry"`)
   275  	flags.StringVar(&options.sbom, "sbom", "", `Shorthand for "--set=*.attest=type=sbom"`)
   276  	flags.StringVar(&options.provenance, "provenance", "", `Shorthand for "--set=*.attest=type=provenance"`)
   277  	flags.StringArrayVar(&options.overrides, "set", nil, `Override target value (e.g., "targetpattern.key=value")`)
   278  
   279  	commonBuildFlags(&cFlags, flags)
   280  
   281  	return cmd
   282  }
   283  
   284  func saveLocalStateGroup(dockerCli command.Cli, ref string, lsg localstate.StateGroup) error {
   285  	l, err := localstate.New(confutil.ConfigDir(dockerCli))
   286  	if err != nil {
   287  		return err
   288  	}
   289  	return l.SaveGroup(ref, lsg)
   290  }
   291  
   292  func readBakeFiles(ctx context.Context, nodes []builder.Node, url string, names []string, stdin io.Reader, pw progress.Writer) (files []bake.File, inp *bake.Input, err error) {
   293  	var lnames []string // local
   294  	var rnames []string // remote
   295  	var anames []string // both
   296  	for _, v := range names {
   297  		if strings.HasPrefix(v, "cwd://") {
   298  			tname := strings.TrimPrefix(v, "cwd://")
   299  			lnames = append(lnames, tname)
   300  			anames = append(anames, tname)
   301  		} else {
   302  			rnames = append(rnames, v)
   303  			anames = append(anames, v)
   304  		}
   305  	}
   306  
   307  	if url != "" {
   308  		var rfiles []bake.File
   309  		rfiles, inp, err = bake.ReadRemoteFiles(ctx, nodes, url, rnames, pw)
   310  		if err != nil {
   311  			return nil, nil, err
   312  		}
   313  		files = append(files, rfiles...)
   314  	}
   315  
   316  	if len(lnames) > 0 || url == "" {
   317  		var lfiles []bake.File
   318  		progress.Wrap("[internal] load local bake definitions", pw.Write, func(sub progress.SubLogger) error {
   319  			if url != "" {
   320  				lfiles, err = bake.ReadLocalFiles(lnames, stdin, sub)
   321  			} else {
   322  				lfiles, err = bake.ReadLocalFiles(anames, stdin, sub)
   323  			}
   324  			return nil
   325  		})
   326  		if err != nil {
   327  			return nil, nil, err
   328  		}
   329  		files = append(files, lfiles...)
   330  	}
   331  
   332  	return
   333  }