diff --git a/bake/hclparser/hclparser.go b/bake/hclparser/hclparser.go index d8f134590ebd..0cf38e142aa8 100644 --- a/bake/hclparser/hclparser.go +++ b/bake/hclparser/hclparser.go @@ -38,6 +38,9 @@ type variable struct { Validations []*variableValidation `json:"validation,omitempty" hcl:"validation,block"` Body hcl.Body `json:"-" hcl:",body"` Remain hcl.Body `json:"-" hcl:",remain"` + + // the type described by Type if it was specified + constraint *cty.Type } type variableValidation struct { @@ -296,6 +299,9 @@ func (p *parser) resolveValue(ectx *hcl.EvalContext, name string) (err error) { return diags } typeSpecified = !varType.Equals(cty.DynamicPseudoType) || hcl.ExprAsKeyword(vr.Type) == "any" + if typeSpecified { + vr.constraint = &varType + } } if def == nil { @@ -674,6 +680,7 @@ func (p *parser) validateVariables(vars map[string]*variable, ectx *hcl.EvalCont type Variable struct { Name string `json:"name"` Description string `json:"description,omitempty"` + Type string `json:"type,omitempty"` Value *string `json:"value,omitempty"` } @@ -803,13 +810,31 @@ func Parse(b hcl.Body, opt Opt, val any) (*ParseMeta, hcl.Diagnostics) { Name: p.vars[k].Name, Description: p.vars[k].Description, } + tc := p.vars[k].constraint + if tc != nil { + v.Type = tc.FriendlyNameForConstraint() + } 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()) + switch { + case tc != nil: + if bs, err := ctyjson.Marshal(vv, *tc); err == nil { + s = string(bs) + // untyped strings were always unquoted, so be consistent with typed strings as well + if tc.Equals(cty.String) { + s = strings.Trim(s, "\"") + } + } + case vv.Type().IsPrimitiveType(): + // all primitives can convert to string, so error should never occur + if val, err := convert.Convert(vv, cty.String); err == nil { + s = val.AsString() + } + default: + // must be an (inferred) tuple or object + if bs, err := ctyjson.Marshal(vv, vv.Type()); err == nil { + s = string(bs) + } } v.Value = &s } diff --git a/commands/bake.go b/commands/bake.go index e9636e2fef38..24e777612d44 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -663,7 +663,7 @@ func printVars(w io.Writer, format string, vars []*hclparser.Variable) error { tw := tabwriter.NewWriter(w, 1, 8, 1, '\t', 0) defer tw.Flush() - tw.Write([]byte("VARIABLE\tVALUE\tDESCRIPTION\n")) + tw.Write([]byte("VARIABLE\tTYPE\tVALUE\tDESCRIPTION\n")) for _, v := range vars { var value string @@ -672,7 +672,7 @@ func printVars(w io.Writer, format string, vars []*hclparser.Variable) error { } else { value = "" } - fmt.Fprintf(tw, "%s\t%s\t%s\n", v.Name, value, v.Description) + fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", v.Name, v.Type, value, v.Description) } return nil } diff --git a/docs/reference/buildx_bake.md b/docs/reference/buildx_bake.md index 733a3eae1231..189866b2bc1f 100644 --- a/docs/reference/buildx_bake.md +++ b/docs/reference/buildx_bake.md @@ -198,12 +198,15 @@ To list variables: ```console $ docker buildx bake --list=variables -VARIABLE VALUE DESCRIPTION -REGISTRY docker.io/username Registry and namespace -IMAGE_NAME my-app Image name -GO_VERSION +VARIABLE TYPE VALUE DESCRIPTION +REGISTRY string docker.io/username Registry and namespace +IMAGE_NAME string my-app Image name +GO_VERSION +DEBUG bool false Add debug symbols ``` +Variable types will be shown when set using the `type` property in the Bake file. + By default, the output of `docker buildx bake --list` is presented in a table format. Alternatively, you can use a long-form CSV syntax and specify a `format` attribute to output the list in JSON. diff --git a/tests/bake.go b/tests/bake.go index 7065726a02bb..ca9902a47bef 100644 --- a/tests/bake.go +++ b/tests/bake.go @@ -74,6 +74,7 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){ testBakeLoadPush, testListTargets, testListVariables, + testListTypedVariables, testBakeCallCheck, testBakeCallCheckFlag, testBakeCallMetadata, @@ -1737,7 +1738,93 @@ target "default" { ) require.NoError(t, err, out) - require.Equal(t, "VARIABLE\tVALUE\tDESCRIPTION\nabc\t\t\t\ndef\t\t\t\nfoo\t\tbar\tThis is foo", strings.TrimSpace(out)) + require.Equal(t, "VARIABLE\tTYPE\tVALUE\tDESCRIPTION\nabc\t\t\t\t\ndef\t\t\t\t\nfoo\t\t\tbar\tThis is foo", strings.TrimSpace(out)) +} + +func testListTypedVariables(t *testing.T, sb integration.Sandbox) { + bakefile := []byte(` +variable "abc" { + type = string + default = "bar" + description = "This is abc" +} +variable "def" { + type = string + description = "simple type, no default" +} +variable "ghi" { + type = number + default = 99 + description = "simple type w/ default" +} +variable "jkl" { + type = list(string) + default = ["hello"] + description = "collection with quoted strings" +} +variable "mno" { + type = list(number) + description = "collection, no default" +} +variable "pqr" { + type = tuple([number, string, bool]) + default = [99, "99", true] +} +variable "stu" { + type = map(string) + default = {"foo": "bar"} +} +variable "vwx" { + type = set(bool) + default = null + description = "collection, null default" +} + +// untyped, but previously didn't have its value output +variable "wxy" { + default = ["foo"] + description = "inferred tuple" +} +// untyped, but previously didn't have its value output +variable "xyz" { + default = {"foo": "bar"} + description = "inferred object" +} +// untyped, but previously didn't have its value output +variable "yza" { + default = true + description = "inferred bool" +} +target "default" { +} +`) + dir := tmpdir( + t, + fstest.CreateFile("docker-bake.hcl", bakefile, 0600), + ) + + out, err := bakeCmd( + sb, + withDir(dir), + withArgs("--list=variables"), + ) + require.NoError(t, err, out) + require.Equal(t, + "VARIABLE\tTYPE\t\tVALUE\t\tDESCRIPTION\n"+ + "abc\t\tstring\t\tbar\t\tThis is abc\n"+ + "def\t\tstring\t\t\t\tsimple type, no default\n"+ + "ghi\t\tnumber\t\t99\t\tsimple type w/ default\n"+ + "jkl\t\tlist of string\t[\"hello\"]\tcollection with quoted strings\n"+ + "mno\t\tlist of number\t\t\tcollection, no default\n"+ + // the implementation for tuple's 'friendly name' is very basic + // and marked as TODO, so this may change/break at some point + "pqr\t\ttuple\t\t[99,\"99\",true]\t\n"+ + "stu\t\tmap of string\t{\"foo\":\"bar\"}\t\n"+ + "vwx\t\tset of bool\t\t\tcollection, null default\n"+ + "wxy\t\t\t\t[\"foo\"]\t\tinferred tuple\n"+ + "xyz\t\t\t\t{\"foo\":\"bar\"}\tinferred object\n"+ + "yza\t\t\t\ttrue\t\tinferred bool", + strings.TrimSpace(out)) } func testBakeCallCheck(t *testing.T, sb integration.Sandbox) {