diff --git a/bake/hcl_test.go b/bake/hcl_test.go index 31abc43e..f33722c7 100644 --- a/bake/hcl_test.go +++ b/bake/hcl_test.go @@ -1445,6 +1445,39 @@ func TestVarUnsupportedType(t *testing.T) { require.Error(t, err) } +func TestHCLIndexOfFunc(t *testing.T) { + dt := []byte(` + variable "APP_VERSIONS" { + default = [ + "1.42.4", + "1.42.3" + ] + } + target "default" { + args = { + APP_VERSION = app_version + } + matrix = { + app_version = APP_VERSIONS + } + name="app-${replace(app_version, ".", "-")}" + tags = [ + "app:${app_version}", + indexof(APP_VERSIONS, app_version) == 0 ? "app:latest" : "", + ] + } + `) + + c, err := ParseFile(dt, "docker-bake.hcl") + require.NoError(t, err) + + require.Equal(t, 2, len(c.Targets)) + require.Equal(t, "app-1-42-4", c.Targets[0].Name) + require.Equal(t, "app:latest", c.Targets[0].Tags[1]) + require.Equal(t, "app-1-42-3", c.Targets[1].Name) + require.Empty(t, c.Targets[1].Tags[1]) +} + func ptrstr(s interface{}) *string { var n *string if reflect.ValueOf(s).Kind() == reflect.String { diff --git a/bake/hclparser/stdlib.go b/bake/hclparser/stdlib.go index 660fef14..e0cd9b5f 100644 --- a/bake/hclparser/stdlib.go +++ b/bake/hclparser/stdlib.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/go-cty-funcs/uuid" "github.com/hashicorp/hcl/v2/ext/tryfunc" "github.com/hashicorp/hcl/v2/ext/typeexpr" + "github.com/pkg/errors" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" "github.com/zclconf/go-cty/cty/function/stdlib" @@ -52,6 +53,7 @@ var stdlibFunctions = map[string]function.Function{ "hasindex": stdlib.HasIndexFunc, "indent": stdlib.IndentFunc, "index": stdlib.IndexFunc, + "indexof": indexOfFunc, "int": stdlib.IntFunc, "join": stdlib.JoinFunc, "jsondecode": stdlib.JSONDecodeFunc, @@ -115,6 +117,51 @@ var stdlibFunctions = map[string]function.Function{ "zipmap": stdlib.ZipmapFunc, } +// indexOfFunc constructs a function that finds the element index for a given +// value in a list. +var indexOfFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "list", + Type: cty.DynamicPseudoType, + }, + { + Name: "value", + Type: cty.DynamicPseudoType, + }, + }, + Type: function.StaticReturnType(cty.Number), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + if !(args[0].Type().IsListType() || args[0].Type().IsTupleType()) { + return cty.NilVal, errors.New("argument must be a list or tuple") + } + + if !args[0].IsKnown() { + return cty.UnknownVal(cty.Number), nil + } + + if args[0].LengthInt() == 0 { // Easy path + return cty.NilVal, errors.New("cannot search an empty list") + } + + for it := args[0].ElementIterator(); it.Next(); { + i, v := it.Element() + eq, err := stdlib.Equal(v, args[1]) + if err != nil { + return cty.NilVal, err + } + if !eq.IsKnown() { + return cty.UnknownVal(cty.Number), nil + } + if eq.True() { + return i, nil + } + } + return cty.NilVal, errors.New("item not found") + + }, +}) + // timestampFunc constructs a function that returns a string representation of the current date and time. // // This function was imported from terraform's datetime utilities. diff --git a/bake/hclparser/stdlib_test.go b/bake/hclparser/stdlib_test.go new file mode 100644 index 00000000..df2336ce --- /dev/null +++ b/bake/hclparser/stdlib_test.go @@ -0,0 +1,49 @@ +package hclparser + +import ( + "testing" + + "github.com/zclconf/go-cty/cty" +) + +func TestIndexOf(t *testing.T) { + type testCase struct { + input cty.Value + key cty.Value + want cty.Value + wantErr bool + } + tests := map[string]testCase{ + "index 0": { + input: cty.TupleVal([]cty.Value{cty.StringVal("one"), cty.NumberIntVal(2.0), cty.NumberIntVal(3), cty.StringVal("four")}), + key: cty.StringVal("one"), + want: cty.NumberIntVal(0), + }, + "index 3": { + input: cty.TupleVal([]cty.Value{cty.StringVal("one"), cty.NumberIntVal(2.0), cty.NumberIntVal(3), cty.StringVal("four")}), + key: cty.StringVal("four"), + want: cty.NumberIntVal(3), + }, + "index -1": { + input: cty.TupleVal([]cty.Value{cty.StringVal("one"), cty.NumberIntVal(2.0), cty.NumberIntVal(3), cty.StringVal("four")}), + key: cty.StringVal("3"), + wantErr: true, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + got, err := indexOfFunc.Call([]cty.Value{test.input, test.key}) + if err != nil { + if test.wantErr { + return + } + t.Fatalf("unexpected error: %s", err) + } + if !got.RawEquals(test.want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want) + } + }) + } +}