diff --git a/applicationset/utils/template_functions.go b/applicationset/utils/template_functions.go new file mode 100644 index 0000000000000..84ab870404f1a --- /dev/null +++ b/applicationset/utils/template_functions.go @@ -0,0 +1,71 @@ +package utils + +import ( + "regexp" + "strings" + + "sigs.k8s.io/yaml" +) + +// SanitizeName sanitizes the name in accordance with the below rules +// 1. contain no more than 253 characters +// 2. contain only lowercase alphanumeric characters, '-' or '.' +// 3. start and end with an alphanumeric character +func SanitizeName(name string) string { + invalidDNSNameChars := regexp.MustCompile("[^-a-z0-9.]") + maxDNSNameLength := 253 + + name = strings.ToLower(name) + name = invalidDNSNameChars.ReplaceAllString(name, "-") + if len(name) > maxDNSNameLength { + name = name[:maxDNSNameLength] + } + + return strings.Trim(name, "-.") +} + +// This has been copied from helm and may be removed as soon as it is retrofited in sprig +// toYAML takes an interface, marshals it to yaml, and returns a string. It will +// always return a string, even on marshal error (empty string). +// +// This is designed to be called from a template. +func toYAML(v interface{}) (string, error) { + data, err := yaml.Marshal(v) + if err != nil { + // Swallow errors inside of a template. + return "", err + } + return strings.TrimSuffix(string(data), "\n"), nil +} + +// This has been copied from helm and may be removed as soon as it is retrofited in sprig +// fromYAML converts a YAML document into a map[string]interface{}. +// +// This is not a general-purpose YAML parser, and will not parse all valid +// YAML documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string into +// m["Error"] in the returned map. +func fromYAML(str string) (map[string]interface{}, error) { + m := map[string]interface{}{} + + if err := yaml.Unmarshal([]byte(str), &m); err != nil { + return nil, err + } + return m, nil +} + +// This has been copied from helm and may be removed as soon as it is retrofited in sprig +// fromYAMLArray converts a YAML array into a []interface{}. +// +// This is not a general-purpose YAML parser, and will not parse all valid +// YAML documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string as +// the first and only item in the returned array. +func fromYAMLArray(str string) ([]interface{}, error) { + a := []interface{}{} + + if err := yaml.Unmarshal([]byte(str), &a); err != nil { + return nil, err + } + return a, nil +} diff --git a/applicationset/utils/utils.go b/applicationset/utils/utils.go index 264c18cda9811..089a6ff103100 100644 --- a/applicationset/utils/utils.go +++ b/applicationset/utils/utils.go @@ -32,6 +32,9 @@ func init() { delete(sprigFuncMap, "expandenv") delete(sprigFuncMap, "getHostByName") sprigFuncMap["normalize"] = SanitizeName + sprigFuncMap["toYaml"] = toYAML + sprigFuncMap["fromYaml"] = fromYAML + sprigFuncMap["fromYamlArray"] = fromYAMLArray } type Renderer interface { @@ -431,23 +434,6 @@ func NormalizeBitbucketBasePath(basePath string) string { return basePath } -// SanitizeName sanitizes the name in accordance with the below rules -// 1. contain no more than 253 characters -// 2. contain only lowercase alphanumeric characters, '-' or '.' -// 3. start and end with an alphanumeric character -func SanitizeName(name string) string { - invalidDNSNameChars := regexp.MustCompile("[^-a-z0-9.]") - maxDNSNameLength := 253 - - name = strings.ToLower(name) - name = invalidDNSNameChars.ReplaceAllString(name, "-") - if len(name) > maxDNSNameLength { - name = name[:maxDNSNameLength] - } - - return strings.Trim(name, "-.") -} - func getTlsConfigWithCACert(scmRootCAPath string) *tls.Config { tlsConfig := &tls.Config{} diff --git a/applicationset/utils/utils_test.go b/applicationset/utils/utils_test.go index cfabd9ce088c0..a1c58769160cc 100644 --- a/applicationset/utils/utils_test.go +++ b/applicationset/utils/utils_test.go @@ -555,6 +555,64 @@ func TestRenderTemplateParamsGoTemplate(t *testing.T) { templateOptions: []string{"missingkey=error"}, errorMessage: `failed to execute go template --> {{.doesnotexist}} <--: template: :1:6: executing "" at <.doesnotexist>: map has no entry for key "doesnotexist"`, }, + { + name: "toYaml", + fieldVal: `{{ toYaml . | indent 2 }}`, + expectedVal: " foo:\n bar:\n bool: true\n number: 2\n str: Hello world", + params: map[string]interface{}{ + "foo": map[string]interface{}{ + "bar": map[string]interface{}{ + "bool": true, + "number": 2, + "str": "Hello world", + }, + }, + }, + }, + { + name: "toYaml Error", + fieldVal: `{{ toYaml . | indent 2 }}`, + expectedVal: " foo:\n bar:\n bool: true\n number: 2\n str: Hello world", + errorMessage: "failed to execute go template {{ toYaml . | indent 2 }}: template: :1:3: executing \"\" at : error calling toYaml: error marshaling into JSON: json: unsupported type: func(*string)", + params: map[string]interface{}{ + "foo": func(test *string) { + }, + }, + }, + { + name: "fromYaml", + fieldVal: `{{ get (fromYaml .value) "hello" }}`, + expectedVal: "world", + params: map[string]interface{}{ + "value": "hello: world", + }, + }, + { + name: "fromYaml error", + fieldVal: `{{ get (fromYaml .value) "hello" }}`, + expectedVal: "world", + errorMessage: "failed to execute go template {{ get (fromYaml .value) \"hello\" }}: template: :1:8: executing \"\" at : error calling fromYaml: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type map[string]interface {}", + params: map[string]interface{}{ + "value": "non\n compliant\n yaml", + }, + }, + { + name: "fromYamlArray", + fieldVal: `{{ fromYamlArray .value | last }}`, + expectedVal: "bonjour tout le monde", + params: map[string]interface{}{ + "value": "- hello world\n- bonjour tout le monde", + }, + }, + { + name: "fromYamlArray error", + fieldVal: `{{ fromYamlArray .value | last }}`, + expectedVal: "bonjour tout le monde", + errorMessage: "failed to execute go template {{ fromYamlArray .value | last }}: template: :1:3: executing \"\" at : error calling fromYamlArray: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type []interface {}", + params: map[string]interface{}{ + "value": "non\n compliant\n yaml", + }, + }, } for _, test := range tests { diff --git a/docs/operator-manual/applicationset/GoTemplate.md b/docs/operator-manual/applicationset/GoTemplate.md index 358c176c35272..08c1f3feb035a 100644 --- a/docs/operator-manual/applicationset/GoTemplate.md +++ b/docs/operator-manual/applicationset/GoTemplate.md @@ -174,6 +174,18 @@ It is also possible to use Sprig functions to construct the path variables manua | `{{path.filenameNormalized}}` | `{{.path.filenameNormalized}}` | `{{normalize .path.filename}}` | | `{{path[N]}}` | `-` | `{{index .path.segments N}}` | +## Available template functions + +ApplicationSet controller provides: + +- all [sprig](http://masterminds.github.io/sprig/) Go templates function except `env`, `expandenv` and `getHostByName` +- `normalize`: sanitizes the input so that it complies with the following rules: + 1. contains no more than 253 characters + 2. contains only lowercase alphanumeric characters, '-' or '.' + 3. starts and ends with an alphanumeric character +- `toYaml` / `fromYaml` / `fromYamlArray` helm like functions + + ## Examples ### Basic Go template usage