diff --git a/example/functions/tpl/README.md b/example/functions/tpl/README.md new file mode 100644 index 0000000..1836310 --- /dev/null +++ b/example/functions/tpl/README.md @@ -0,0 +1,24 @@ +# tpl + +## Usage + +```golang +{{ tpl $template $context }} +``` + +Examples: + +```golang +//context +test: + user: "travis" + +//template +{{- $testuser := .test.user }} +{{ tpl "Welcome, {{$testuser}}" . }} + +//output +Welcome, travis +``` + +See example composition for more usage examples \ No newline at end of file diff --git a/example/functions/tpl/composition.yaml b/example/functions/tpl/composition.yaml new file mode 100644 index 0000000..6f6f4ce --- /dev/null +++ b/example/functions/tpl/composition.yaml @@ -0,0 +1,43 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: example-function-tpl +spec: + compositeTypeRef: + apiVersion: example.crossplane.io/v1beta1 + kind: XR + mode: Pipeline + pipeline: + - step: render-templates + functionRef: + name: function-go-templating + input: + apiVersion: gotemplating.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + {{$vals:= .observed.composite.resource.spec}} + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx-deployment + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: test1 + labels: + {{- toYaml (tpl $vals.labels $vals) | nindent 4}} + spec: + replicas: 3 + selector: + matchLabels: + {{- toYaml (tpl $vals.labels $vals) | nindent 6}} + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 diff --git a/example/functions/tpl/functions.yaml b/example/functions/tpl/functions.yaml new file mode 100644 index 0000000..c4a446f --- /dev/null +++ b/example/functions/tpl/functions.yaml @@ -0,0 +1,6 @@ +apiVersion: pkg.crossplane.io/v1beta1 +kind: Function +metadata: + name: function-go-templating +spec: + package: xpkg.upbound.io/crossplane-contrib/function-go-templating:latest \ No newline at end of file diff --git a/example/functions/tpl/xr.yaml b/example/functions/tpl/xr.yaml new file mode 100644 index 0000000..aa9281b --- /dev/null +++ b/example/functions/tpl/xr.yaml @@ -0,0 +1,9 @@ +apiVersion: example.crossplane.io/v1beta1 +kind: XR +metadata: + name: example +spec: + labels: + key1: value1 + key2: "{{ .val2 }}" + val2: value2 diff --git a/function_maps.go b/function_maps.go index 4dff6ea..a61a9d4 100644 --- a/function_maps.go +++ b/function_maps.go @@ -31,6 +31,7 @@ var funcMaps = []template.FuncMap{ func GetNewTemplateWithFunctionMaps(delims *v1beta1.Delims) *template.Template { tpl := template.New("manifests") + includedNames := make(map[string]int) if delims != nil { if delims.Left != nil && delims.Right != nil { @@ -42,7 +43,8 @@ func GetNewTemplateWithFunctionMaps(delims *v1beta1.Delims) *template.Template { tpl.Funcs(f) } tpl.Funcs(template.FuncMap{ - "include": initInclude(tpl), + "include": initInclude(tpl, includedNames), + "tpl": initTpl(tpl, includedNames), }) // Sprig's env and expandenv can lead to information leakage (injected tokens/passwords). // Both Helm and ArgoCD remove these due to security implications. @@ -91,9 +93,35 @@ func setResourceNameAnnotation(name string) string { return fmt.Sprintf("gotemplating.fn.crossplane.io/composition-resource-name: %s", name) } -func initInclude(t *template.Template) func(string, interface{}) (string, error) { +func initTpl(parent *template.Template, includedNames map[string]int) func(string, interface{}) (string, error) { + //see https://github.com/helm/helm/blob/261233caec499c18602c61ac32507fa4656ebc9b/pkg/engine/engine.go#L148 + return func(tpl string, vals interface{}) (string, error) { + t, err := parent.Clone() + t.Option("missingkey=zero") + if err != nil { + return "", errors.Wrapf(err, "cannot clone template") + } - includedNames := make(map[string]int) + t.Funcs(template.FuncMap{ + "include": initInclude(t, includedNames), + "tpl": initTpl(t, includedNames), + }) + + t, err = t.New(parent.Name()).Parse(tpl) + if err != nil { + return "", errors.Wrapf(err, "cannot parse template %q", tpl) + } + + var buf strings.Builder + if err := t.Execute(&buf, vals); err != nil { + return "", errors.Wrapf(err, "error during tpl function execution for %q", tpl) + } + + return strings.ReplaceAll(buf.String(), "", ""), nil + } +} + +func initInclude(t *template.Template, includedNames map[string]int) func(string, interface{}) (string, error) { return func(name string, data interface{}) (string, error) { var buf strings.Builder diff --git a/function_maps_test.go b/function_maps_test.go index 8767755..63b3540 100644 --- a/function_maps_test.go +++ b/function_maps_test.go @@ -308,9 +308,10 @@ Must capture output: {{$var}} }, } + includedNames := make(map[string]int) tpl := template.New("") tpl.Funcs(template.FuncMap{ - "include": initInclude(tpl), + "include": initInclude(tpl, includedNames), }) for name, tc := range cases { @@ -459,3 +460,60 @@ func Test_getCompositeResource(t *testing.T) { }) } } + +func Test_tpl(t *testing.T) { + type args struct { + val string + } + type want struct { + rsp string + err error + } + cases := map[string]struct { + reason string + args args + want want + }{ + "ExecTemplate": { + reason: "Should return the executed template", + args: args{ + val: `Must capture output: {{(tpl "should interpolate {{.}}" "test" )}}`, + }, + want: want{ + rsp: `Must capture output: should interpolate test`, + }, + }, + "TemplateErrorCtxNotSet": { + reason: "Should return error if ctx not set", + args: args{ + val: `Must capture output: {{ tpl "should interpolate {{.}}" }}`, + }, + want: want{ + rsp: `Must capture output: `, + err: cmpopts.AnyError, + }, + }, + } + + includedNames := make(map[string]int) + tpl := template.New("") + tpl.Funcs(template.FuncMap{ + "include": initInclude(tpl, includedNames), + "tpl": initTpl(tpl, includedNames), + }) + + for name, tc := range cases { + _tpl := template.Must(tpl.Parse(tc.args.val)) + t.Run(name, func(t *testing.T) { + rsp := &bytes.Buffer{} + err := _tpl.Execute(rsp, nil) + if diff := cmp.Diff(tc.want.rsp, rsp.String(), protocmp.Transform()); diff != "" { + t.Errorf("%s\ntpl(...): -want rsp, +got rsp:\n%s", tc.reason, diff) + } + + if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" { + t.Errorf("%s\ntpl(...): -want err, +got err:\n%s", tc.reason, diff) + } + }) + } +}