diff --git a/config/interpolate_funcs.go b/config/interpolate_funcs.go index b94fca8896..d1755fe327 100644 --- a/config/interpolate_funcs.go +++ b/config/interpolate_funcs.go @@ -47,6 +47,20 @@ func stringSliceToVariableValue(values []string) []ast.Variable { return output } +// listVariableSliceToVariableValue converts a list of lists into the value +// required to be returned from interpolation functions which return TypeList. +func listVariableSliceToVariableValue(values [][]ast.Variable) []ast.Variable { + output := make([]ast.Variable, len(values)) + + for index, value := range values { + output[index] = ast.Variable{ + Type: ast.TypeList, + Value: value, + } + } + return output +} + func listVariableValueToStringSlice(values []ast.Variable) ([]string, error) { output := make([]string, len(values)) for index, value := range values { @@ -104,6 +118,7 @@ func Funcs() map[string]ast.Function { "min": interpolationFuncMin(), "pathexpand": interpolationFuncPathExpand(), "pow": interpolationFuncPow(), + "product": interpolationFuncProduct(), "uuid": interpolationFuncUUID(), "replace": interpolationFuncReplace(), "rsadecrypt": interpolationFuncRsaDecrypt(), @@ -1725,3 +1740,55 @@ func interpolationFuncRsaDecrypt() ast.Function { }, } } + +// interpolationFuncProduct implements the "product" function +// that returns the cartesian product of two or more lists +func interpolationFuncProduct() ast.Function { + return ast.Function{ + ArgTypes: []ast.Type{ast.TypeList}, + ReturnType: ast.TypeList, + Variadic: true, + VariadicType: ast.TypeList, + Callback: func(args []interface{}) (interface{}, error) { + if len(args) < 2 { + return nil, fmt.Errorf("must provide at least two arguments") + } + + total := 1 + for _, arg := range args { + total *= len(arg.([]ast.Variable)) + } + + if total == 0 { + return nil, fmt.Errorf("empty list provided") + } + + product := make([][]ast.Variable, total) + + b := make([]ast.Variable, total*len(args)) + n := make([]int, len(args)) + s := 0 + + for i := range product { + e := s + len(args) + pi := b[s:e] + product[i] = pi + s = e + + for j, n := range n { + pi[j] = args[j].([]ast.Variable)[n] + } + + for j := len(n) - 1; j >= 0; j-- { + n[j]++ + if n[j] < len(args[j].([]ast.Variable)) { + break + } + n[j] = 0 + } + } + + return listVariableSliceToVariableValue(product), nil + }, + } +} diff --git a/config/interpolate_funcs_test.go b/config/interpolate_funcs_test.go index f63d81c30b..1035c020ef 100644 --- a/config/interpolate_funcs_test.go +++ b/config/interpolate_funcs_test.go @@ -2960,3 +2960,55 @@ H7CurtMwALQ/n/6LUKFmjRZjqbKX9SO2QSaC3grd6sY9Tu+bZjLe }, }) } + +func TestInterpolateFuncProduct(t *testing.T) { + testFunction(t, testFunctionConfig{ + Cases: []testFunctionCase{ + { + `${product(list("dev", "qas", "prd"), list("applicationA", "applicationB", "applicationC"))}`, + []interface{}{ + []interface{}{"dev", "applicationA"}, + []interface{}{"dev", "applicationB"}, + []interface{}{"dev", "applicationC"}, + []interface{}{"qas", "applicationA"}, + []interface{}{"qas", "applicationB"}, + []interface{}{"qas", "applicationC"}, + []interface{}{"prd", "applicationA"}, + []interface{}{"prd", "applicationB"}, + []interface{}{"prd", "applicationC"}}, + false, + }, + { + `${product(list("A", "B"), list("C", "D"), list("E", "F"))}`, + []interface{}{ + []interface{}{"A", "C", "E"}, + []interface{}{"A", "C", "F"}, + []interface{}{"A", "D", "E"}, + []interface{}{"A", "D", "F"}, + []interface{}{"B", "C", "E"}, + []interface{}{"B", "C", "F"}, + []interface{}{"B", "D", "E"}, + []interface{}{"B", "D", "F"}, + }, + false, + }, + { + `${product(list(), list(), list())}`, + nil, + true, + }, + { + `${product(list("foo"),list("bar"))}`, + []interface{}{ + []interface{}{"foo", "bar"}, + }, + false, + }, + { + `${product(list("foo"))}`, + nil, + true, + }, + }, + }) +}