diff --git a/bake/bake.go b/bake/bake.go index 264fa7a39572..3d3f5a51b5e1 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -177,7 +177,7 @@ func readWithProgress(r io.Reader, setStatus func(st *client.VertexStatus)) (dt } func ListTargets(files []File) ([]string, error) { - c, err := ParseFiles(files, nil) + c, _, err := ParseFiles(files, nil) if err != nil { return nil, err } @@ -192,7 +192,7 @@ func ListTargets(files []File) ([]string, error) { } func ReadTargets(ctx context.Context, files []File, targets, overrides []string, defaults map[string]string) (map[string]*Target, map[string]*Group, error) { - c, err := ParseFiles(files, defaults) + c, _, err := ParseFiles(files, defaults) if err != nil { return nil, nil, err } @@ -298,7 +298,7 @@ func sliceToMap(env []string) (res map[string]string) { return } -func ParseFiles(files []File, defaults map[string]string) (_ *Config, err error) { +func ParseFiles(files []File, defaults map[string]string) (_ *Config, _ *hclparser.ParseMeta, err error) { defer func() { err = formatHCLError(err, files) }() @@ -310,7 +310,7 @@ func ParseFiles(files []File, defaults map[string]string) (_ *Config, err error) isCompose, composeErr := validateComposeFile(f.Data, f.Name) if isCompose { if composeErr != nil { - return nil, composeErr + return nil, nil, composeErr } composeFiles = append(composeFiles, f) } @@ -318,13 +318,13 @@ func ParseFiles(files []File, defaults map[string]string) (_ *Config, err error) hf, isHCL, err := ParseHCLFile(f.Data, f.Name) if isHCL { if err != nil { - return nil, err + return nil, nil, err } hclFiles = append(hclFiles, hf) } else if composeErr != nil { - return nil, errors.Wrapf(err, "failed to parse %s: parsing yaml: %v, parsing hcl", f.Name, composeErr) + return nil, nil, errors.Wrapf(err, "failed to parse %s: parsing yaml: %v, parsing hcl", f.Name, composeErr) } else { - return nil, err + return nil, nil, err } } } @@ -332,23 +332,24 @@ func ParseFiles(files []File, defaults map[string]string) (_ *Config, err error) if len(composeFiles) > 0 { cfg, cmperr := ParseComposeFiles(composeFiles) if cmperr != nil { - return nil, errors.Wrap(cmperr, "failed to parse compose file") + return nil, nil, errors.Wrap(cmperr, "failed to parse compose file") } c = mergeConfig(c, *cfg) c = dedupeConfig(c) } + var pm hclparser.ParseMeta if len(hclFiles) > 0 { - renamed, err := hclparser.Parse(hclparser.MergeFiles(hclFiles), hclparser.Opt{ + res, err := hclparser.Parse(hclparser.MergeFiles(hclFiles), hclparser.Opt{ LookupVar: os.LookupEnv, Vars: defaults, ValidateLabel: validateTargetName, }, &c) if err.HasErrors() { - return nil, err + return nil, nil, err } - for _, renamed := range renamed { + for _, renamed := range res.Renamed { for oldName, newNames := range renamed { newNames = dedupSlice(newNames) if len(newNames) == 1 && oldName == newNames[0] { @@ -361,9 +362,10 @@ func ParseFiles(files []File, defaults map[string]string) (_ *Config, err error) } } c = dedupeConfig(c) + pm = *res } - return &c, nil + return &c, &pm, nil } func dedupeConfig(c Config) Config { @@ -388,7 +390,8 @@ func dedupeConfig(c Config) Config { } func ParseFile(dt []byte, fn string) (*Config, error) { - return ParseFiles([]File{{Data: dt, Name: fn}}, nil) + c, _, err := ParseFiles([]File{{Data: dt, Name: fn}}, nil) + return c, err } type Config struct { diff --git a/bake/bake_test.go b/bake/bake_test.go index fe21d2b508ae..cc2bed87246b 100644 --- a/bake/bake_test.go +++ b/bake/bake_test.go @@ -1528,7 +1528,7 @@ services: v2: "bar" `) - c, err := ParseFiles([]File{ + c, _, err := ParseFiles([]File{ {Data: dt, Name: "c1.foo"}, {Data: dt2, Name: "c2.bar"}, }, nil) diff --git a/bake/hcl_test.go b/bake/hcl_test.go index f33722c72acc..ac5e61eb6951 100644 --- a/bake/hcl_test.go +++ b/bake/hcl_test.go @@ -273,7 +273,7 @@ func TestHCLMultiFileSharedVariables(t *testing.T) { } `) - c, err := ParseFiles([]File{ + c, _, err := ParseFiles([]File{ {Data: dt, Name: "c1.hcl"}, {Data: dt2, Name: "c2.hcl"}, }, nil) @@ -285,7 +285,7 @@ func TestHCLMultiFileSharedVariables(t *testing.T) { t.Setenv("FOO", "def") - c, err = ParseFiles([]File{ + c, _, err = ParseFiles([]File{ {Data: dt, Name: "c1.hcl"}, {Data: dt2, Name: "c2.hcl"}, }, nil) @@ -322,7 +322,7 @@ func TestHCLVarsWithVars(t *testing.T) { } `) - c, err := ParseFiles([]File{ + c, _, err := ParseFiles([]File{ {Data: dt, Name: "c1.hcl"}, {Data: dt2, Name: "c2.hcl"}, }, nil) @@ -334,7 +334,7 @@ func TestHCLVarsWithVars(t *testing.T) { t.Setenv("BASE", "new") - c, err = ParseFiles([]File{ + c, _, err = ParseFiles([]File{ {Data: dt, Name: "c1.hcl"}, {Data: dt2, Name: "c2.hcl"}, }, nil) @@ -612,7 +612,7 @@ func TestHCLMultiFileAttrs(t *testing.T) { FOO="def" `) - c, err := ParseFiles([]File{ + c, _, err := ParseFiles([]File{ {Data: dt, Name: "c1.hcl"}, {Data: dt2, Name: "c2.hcl"}, }, nil) @@ -623,7 +623,7 @@ func TestHCLMultiFileAttrs(t *testing.T) { t.Setenv("FOO", "ghi") - c, err = ParseFiles([]File{ + c, _, err = ParseFiles([]File{ {Data: dt, Name: "c1.hcl"}, {Data: dt2, Name: "c2.hcl"}, }, nil) @@ -647,7 +647,7 @@ func TestHCLMultiFileGlobalAttrs(t *testing.T) { FOO = "def" `) - c, err := ParseFiles([]File{ + c, _, err := ParseFiles([]File{ {Data: dt, Name: "c1.hcl"}, {Data: dt2, Name: "c2.hcl"}, }, nil) @@ -830,7 +830,7 @@ func TestHCLRenameMultiFile(t *testing.T) { } `) - c, err := ParseFiles([]File{ + c, _, err := ParseFiles([]File{ {Data: dt, Name: "c1.hcl"}, {Data: dt2, Name: "c2.hcl"}, {Data: dt3, Name: "c3.hcl"}, @@ -1050,7 +1050,7 @@ func TestHCLMatrixArgsOverride(t *testing.T) { } `) - c, err := ParseFiles([]File{ + c, _, err := ParseFiles([]File{ {Data: dt, Name: "docker-bake.hcl"}, }, map[string]string{"ABC": "11,22,33"}) require.NoError(t, err) @@ -1236,7 +1236,7 @@ services: v2: "bar" `) - c, err := ParseFiles([]File{ + c, _, err := ParseFiles([]File{ {Data: dt, Name: "c1.hcl"}, {Data: dt2, Name: "c2.yml"}, }, nil) @@ -1258,7 +1258,7 @@ func TestHCLBuiltinVars(t *testing.T) { } `) - c, err := ParseFiles([]File{ + c, _, err := ParseFiles([]File{ {Data: dt, Name: "c1.hcl"}, }, map[string]string{ "BAKE_CMD_CONTEXT": "foo", @@ -1272,7 +1272,7 @@ func TestHCLBuiltinVars(t *testing.T) { } func TestCombineHCLAndJSONTargets(t *testing.T) { - c, err := ParseFiles([]File{ + c, _, err := ParseFiles([]File{ { Name: "docker-bake.hcl", Data: []byte(` @@ -1348,7 +1348,7 @@ target "b" { } func TestCombineHCLAndJSONVars(t *testing.T) { - c, err := ParseFiles([]File{ + c, _, err := ParseFiles([]File{ { Name: "docker-bake.hcl", Data: []byte(` diff --git a/bake/hclparser/hclparser.go b/bake/hclparser/hclparser.go index 7efdc9f6b3e4..eee3a67017ce 100644 --- a/bake/hclparser/hclparser.go +++ b/bake/hclparser/hclparser.go @@ -25,9 +25,11 @@ type Opt struct { } type variable struct { - Name string `json:"-" hcl:"name,label"` - Default *hcl.Attribute `json:"default,omitempty" hcl:"default,optional"` - Body hcl.Body `json:"-" hcl:",body"` + Name string `json:"-" hcl:"name,label"` + Default *hcl.Attribute `json:"default,omitempty" hcl:"default,optional"` + Description string `json:"description,omitempty" hcl:"description,optional"` + Body hcl.Body `json:"-" hcl:",body"` + Remain hcl.Body `json:"-" hcl:",remain"` } type functionDef struct { @@ -534,7 +536,18 @@ func (p *parser) resolveBlockNames(block *hcl.Block) ([]string, error) { return names, nil } -func Parse(b hcl.Body, opt Opt, val interface{}) (map[string]map[string][]string, hcl.Diagnostics) { +type Variable struct { + Name string + Description string + Value *string +} + +type ParseMeta struct { + Renamed map[string]map[string][]string + AllVariables []*Variable +} + +func Parse(b hcl.Body, opt Opt, val interface{}) (*ParseMeta, hcl.Diagnostics) { reserved := map[string]struct{}{} schema, _ := gohcl.ImpliedBodySchema(val) @@ -643,6 +656,7 @@ func Parse(b hcl.Body, opt Opt, val interface{}) (map[string]map[string][]string } } + vars := make([]*Variable, 0, len(p.vars)) for k := range p.vars { if err := p.resolveValue(p.ectx, k); err != nil { if diags, ok := err.(hcl.Diagnostics); ok { @@ -651,6 +665,21 @@ func Parse(b hcl.Body, opt Opt, val interface{}) (map[string]map[string][]string r := p.vars[k].Body.MissingItemRange() return nil, wrapErrorDiagnostic("Invalid value", err, &r, &r) } + v := &Variable{ + Name: p.vars[k].Name, + Description: p.vars[k].Description, + } + if vv := p.ectx.Variables[k]; !vv.IsNull() { + var s string + switch vv.Type() { + case cty.String: + s = vv.AsString() + case cty.Bool: + s = strconv.FormatBool(vv.True()) + } + v.Value = &s + } + vars = append(vars, v) } for k := range p.funcs { @@ -795,7 +824,10 @@ func Parse(b hcl.Body, opt Opt, val interface{}) (map[string]map[string][]string } } - return renamed, nil + return &ParseMeta{ + Renamed: renamed, + AllVariables: vars, + }, nil } // wrapErrorDiagnostic wraps an error into a hcl.Diagnostics object. diff --git a/bake/hclparser/merged.go b/bake/hclparser/merged.go index 6faf6aceccbb..7fdf12347823 100644 --- a/bake/hclparser/merged.go +++ b/bake/hclparser/merged.go @@ -111,21 +111,19 @@ func (mb mergedBodies) JustAttributes() (hcl.Attributes, hcl.Diagnostics) { diags = append(diags, thisDiags...) } - if thisAttrs != nil { - for name, attr := range thisAttrs { - if existing := attrs[name]; existing != nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Duplicate argument", - Detail: fmt.Sprintf( - "Argument %q was already set at %s", - name, existing.NameRange.String(), - ), - Subject: thisAttrs[name].NameRange.Ptr(), - }) - } - attrs[name] = attr + for name, attr := range thisAttrs { + if existing := attrs[name]; existing != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate argument", + Detail: fmt.Sprintf( + "Argument %q was already set at %s", + name, existing.NameRange.String(), + ), + Subject: thisAttrs[name].NameRange.Ptr(), + }) } + attrs[name] = attr } } diff --git a/commands/bake.go b/commands/bake.go index 889a5535f738..9abe2e0e6912 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -15,6 +15,7 @@ import ( "github.com/containerd/console" "github.com/containerd/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/controller/pb" @@ -39,6 +40,7 @@ type bakeOptions struct { overrides []string printOnly bool listTargets bool + listVars bool sbom string provenance string @@ -157,19 +159,19 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba if err != nil { return } - } - if progressMode != progressui.QuietMode && progressMode != progressui.RawJSONMode { - desktop.PrintBuildDetails(os.Stderr, printer.BuildRefs(), term) - } - if resp != nil && len(in.metadataFile) > 0 { - dt := make(map[string]interface{}) - for t, r := range resp { - dt[t] = decodeExporterResponse(r.ExporterResponse) + if progressMode != progressui.QuietMode && progressMode != progressui.RawJSONMode { + desktop.PrintBuildDetails(os.Stderr, printer.BuildRefs(), term) } - if warnings := printer.Warnings(); len(warnings) > 0 && confutil.MetadataWarningsEnabled() { - dt["buildx.build.warnings"] = warnings + if resp != nil && len(in.metadataFile) > 0 { + dt := make(map[string]interface{}) + for t, r := range resp { + dt[t] = decodeExporterResponse(r.ExporterResponse) + } + if warnings := printer.Warnings(); len(warnings) > 0 && confutil.MetadataWarningsEnabled() { + dt["buildx.build.warnings"] = warnings + } + err = writeMetadataFile(in.metadataFile, dt) } - err = writeMetadataFile(in.metadataFile, dt) } }() @@ -189,8 +191,8 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba "BAKE_LOCAL_PLATFORM": platforms.DefaultString(), } - if in.listTargets { - cfg, err := bake.ParseFiles(files, defaults) + if in.listTargets || in.listVars { + cfg, pm, err := bake.ParseFiles(files, defaults) if err != nil { return err } @@ -200,7 +202,11 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba if err != nil { return err } - return printTargetList(dockerCli.Out(), cfg) + if in.listTargets { + return printTargetList(dockerCli.Out(), cfg) + } else if in.listVars { + return printVars(dockerCli.Out(), pm.AllVariables) + } } tgts, grps, err := bake.ReadTargets(ctx, files, targets, overrides, defaults) @@ -435,6 +441,7 @@ func bakeCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { 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.listTargets, "list-targets", false, "List available targets") + flags.BoolVar(&options.listVars, "list-variables", false, "List defined variables") 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"`) @@ -499,6 +506,27 @@ func readBakeFiles(ctx context.Context, nodes []builder.Node, url string, names return } +func printVars(w io.Writer, vars []*hclparser.Variable) error { + slices.SortFunc(vars, func(a, b *hclparser.Variable) int { + return cmp.Compare(a.Name, b.Name) + }) + tw := tabwriter.NewWriter(w, 1, 8, 1, '\t', 0) + defer tw.Flush() + + tw.Write([]byte("VARIABLE\tVALUE\tDESCRIPTION\n")) + + for _, v := range vars { + var value string + if v.Value != nil { + value = *v.Value + } else { + value = "" + } + fmt.Fprintf(tw, "%s\t%s\t%s\n", v.Name, value, v.Description) + } + return nil +} + func printTargetList(w io.Writer, cfg *bake.Config) error { tw := tabwriter.NewWriter(w, 1, 8, 1, '\t', 0) defer tw.Flush()