From a41fc817967ff15e81eef08ef6fdd19ad97d2858 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 9 Jan 2025 10:36:49 +0100 Subject: [PATCH 1/3] bake: replace list-targets and list-variables flags with list= also put this flag out of experimental Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- commands/bake.go | 54 +++++++++++++++++++++++------------ docs/reference/buildx_bake.md | 1 + tests/bake.go | 4 +-- 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/commands/bake.go b/commands/bake.go index 12befc84..b5e4e80d 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -25,7 +25,6 @@ import ( "github.com/docker/buildx/controller/pb" "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" @@ -42,20 +41,25 @@ import ( ) type bakeOptions struct { - files []string - overrides []string - printOnly bool - listTargets bool - listVars bool - sbom string - provenance string - allow []string + files []string + overrides []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) (err error) { @@ -123,7 +127,7 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba // instance only needed for reading remote bake files or building var driverType string - if url != "" || !(in.printOnly || in.listTargets || in.listVars) { + if url != "" || !(in.print || in.list != "") { b, err := builder.New(dockerCli, builder.WithName(in.builder), builder.WithContextPathHash(contextPathHash), @@ -184,7 +188,7 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba "BAKE_LOCAL_PLATFORM": platforms.Format(platforms.DefaultSpec()), } - if in.listTargets || in.listVars { + if in.list != "" { cfg, pm, err := bake.ParseFiles(files, defaults) if err != nil { return err @@ -192,10 +196,13 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba if err = printer.Wait(); err != nil { return err } - if in.listTargets { + switch in.list { + case "targets": return printTargetList(dockerCli.Out(), cfg) - } else if in.listVars { + case "variables": return printVars(dockerCli.Out(), pm.AllVariables) + default: + return errors.Errorf("invalid list mode %q", in.list) } } @@ -231,7 +238,7 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba Target: tgts, } - if in.printOnly { + if in.print { if err = printer.Wait(); err != nil { return err } @@ -427,6 +434,13 @@ func bakeCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { 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. @@ -439,7 +453,6 @@ func bakeCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { flags.StringArrayVarP(&options.files, "file", "f", []string{}, "Build definition file") flags.BoolVar(&options.exportLoad, "load", false, `Shorthand for "--set=*.output=type=docker"`) - flags.BoolVar(&options.printOnly, "print", false, "Print the options without building") flags.BoolVar(&options.exportPush, "push", false, `Shorthand for "--set=*.output=type=registry"`) flags.StringVar(&options.sbom, "sbom", "", `Shorthand for "--set=*.attest=type=sbom"`) flags.StringVar(&options.provenance, "provenance", "", `Shorthand for "--set=*.attest=type=provenance"`) @@ -450,13 +463,16 @@ func bakeCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { flags.VarPF(callAlias(&options.callFunc, "check"), "check", "", `Shorthand for "--call=check"`) flags.Lookup("check").NoOptDefVal = "true" - flags.BoolVar(&options.listTargets, "list-targets", false, "List available targets") - cobrautil.MarkFlagsExperimental(flags, "list-targets") - flags.MarkHidden("list-targets") + 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") - cobrautil.MarkFlagsExperimental(flags, "list-variables") flags.MarkHidden("list-variables") + flags.MarkDeprecated("list-variables", "list-variables is deprecated, use list=variables instead") commonBuildFlags(&cFlags, flags) diff --git a/docs/reference/buildx_bake.md b/docs/reference/buildx_bake.md index 3e02b859..e4a81460 100644 --- a/docs/reference/buildx_bake.md +++ b/docs/reference/buildx_bake.md @@ -21,6 +21,7 @@ Build from a file | [`--check`](#check) | `bool` | | Shorthand for `--call=check` | | `-D`, `--debug` | `bool` | | Enable debug logging | | [`-f`](#file), [`--file`](#file) | `stringArray` | | Build definition file | +| `--list` | `string` | | List targets or variables | | `--load` | `bool` | | Shorthand for `--set=*.output=type=docker` | | [`--metadata-file`](#metadata-file) | `string` | | Write build result metadata to a file | | [`--no-cache`](#no-cache) | `bool` | | Do not use cache when building the image | diff --git a/tests/bake.go b/tests/bake.go index 70238e43..8c6b724a 100644 --- a/tests/bake.go +++ b/tests/bake.go @@ -1504,7 +1504,7 @@ target "abc" { out, err := bakeCmd( sb, withDir(dir), - withArgs("--list-targets"), + withArgs("--list=targets"), ) require.NoError(t, err, out) @@ -1533,7 +1533,7 @@ target "default" { out, err := bakeCmd( sb, withDir(dir), - withArgs("--list-variables"), + withArgs("--list=variables"), ) require.NoError(t, err, out) From 41215835cf9cd9dcb81c31d59fb2f7972be45d18 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 9 Jan 2025 10:40:01 +0100 Subject: [PATCH 2/3] bake: print and list flag mutually exclusive Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- commands/bake.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/commands/bake.go b/commands/bake.go index b5e4e80d..3f61beb2 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -125,6 +125,10 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba 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 != "") { From 11c85b236928b87033417d0a6c5f440e5f660db3 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 9 Jan 2025 11:39:22 +0100 Subject: [PATCH 3/3] bake: list flag json format support Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- bake/hclparser/hclparser.go | 6 +- commands/bake.go | 109 +++++++++++++++++++++++++++++++----- 2 files changed, 98 insertions(+), 17 deletions(-) diff --git a/bake/hclparser/hclparser.go b/bake/hclparser/hclparser.go index 6933ad71..fc820e4f 100644 --- a/bake/hclparser/hclparser.go +++ b/bake/hclparser/hclparser.go @@ -579,9 +579,9 @@ func (p *parser) validateVariables(vars map[string]*variable, ectx *hcl.EvalCont } type Variable struct { - Name string - Description string - Value *string + Name string `json:"name"` + Description string `json:"description,omitempty"` + Value *string `json:"value,omitempty"` } type ParseMeta struct { diff --git a/commands/bake.go b/commands/bake.go index 3f61beb2..dcb5de5e 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -37,6 +37,7 @@ import ( "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" ) @@ -200,13 +201,15 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba if err = printer.Wait(); err != nil { return err } - switch in.list { + list, err := parseList(in.list) + if err != nil { + return err + } + switch list.Type { case "targets": - return printTargetList(dockerCli.Out(), cfg) + return printTargetList(dockerCli.Out(), list.Format, cfg) case "variables": - return printVars(dockerCli.Out(), pm.AllVariables) - default: - return errors.Errorf("invalid list mode %q", in.list) + return printVars(dockerCli.Out(), list.Format, pm.AllVariables) } } @@ -577,10 +580,70 @@ func readBakeFiles(ctx context.Context, nodes []builder.Node, url string, names return } -func printVars(w io.Writer, vars []*hclparser.Variable) error { +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 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() @@ -598,12 +661,7 @@ func printVars(w io.Writer, vars []*hclparser.Variable) error { return nil } -func printTargetList(w io.Writer, cfg *bake.Config) error { - tw := tabwriter.NewWriter(w, 1, 8, 1, '\t', 0) - defer tw.Flush() - - tw.Write([]byte("TARGET\tDESCRIPTION\n")) - +func printTargetList(w io.Writer, format string, cfg *bake.Config) error { type targetOrGroup struct { name string target *bake.Target @@ -622,6 +680,20 @@ func printTargetList(w io.Writer, cfg *bake.Config) error { 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 @@ -630,9 +702,9 @@ func printTargetList(w io.Writer, cfg *bake.Config) error { 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, ", ") @@ -642,8 +714,17 @@ func printTargetList(w io.Writer, cfg *bake.Config) error { descr = names } } + targetsList = append(targetsList, targetList{Name: tgt.name, Description: descr, Group: true}) } - fmt.Fprintf(tw, "%s\t%s\n", tgt.name, descr) + 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