diff --git a/README.md b/README.md index 3e3fcfb..7f126ca 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,8 @@ spec: This function can load templates from three sources: `Inline`, `FileSystem` and `Environment`. Use the `Inline` source to specify a simple template inline in your Composition. -Multiple YAML manifests can be specified using the `---` document separator. +Multiple YAML manifests can be specified using the `templates` field, which may contain a slice of +strings, or by using the `---` document separator in the `template` field. Use the `FileSystem` source to specify a directory of templates. The `FileSystem` source treats all files under the specified directory as templates. diff --git a/example/inline-templates/composition.yaml b/example/inline-templates/composition.yaml new file mode 100644 index 0000000..8222939 --- /dev/null +++ b/example/inline-templates/composition.yaml @@ -0,0 +1,19 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: example-inline +spec: + mode: Pipeline + compositeTypeRef: + apiVersion: example.crossplane.io/v1beta1 + kind: XR + pipeline: + - step: render-templates + functionRef: + name: function-go-templating + input: + apiVersion: gotemplating.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + templates: [] # Kustomize will add the templates here \ No newline at end of file diff --git a/example/inline-templates/kustomization.yaml b/example/inline-templates/kustomization.yaml new file mode 100644 index 0000000..845c2f4 --- /dev/null +++ b/example/inline-templates/kustomization.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - composition.yaml +patches: + - path: templates/user.patch.yaml + target: { kind: Composition, name: example-inline } + - path: templates/access-key.patch.yaml + target: { kind: Composition, name: example-inline } \ No newline at end of file diff --git a/example/inline-templates/templates/access-key.patch.yaml b/example/inline-templates/templates/access-key.patch.yaml new file mode 100644 index 0000000..03db6e5 --- /dev/null +++ b/example/inline-templates/templates/access-key.patch.yaml @@ -0,0 +1,10 @@ +- op: add + path: /spec/pipeline/0/input/inline/templates/- + value: | + apiVersion: iam.aws.upbound.io/v1beta1 + kind: AccessKey + metadata: + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: access-key + spec: + forProvider: {} \ No newline at end of file diff --git a/example/inline-templates/templates/user.patch.yaml b/example/inline-templates/templates/user.patch.yaml new file mode 100644 index 0000000..439c3f1 --- /dev/null +++ b/example/inline-templates/templates/user.patch.yaml @@ -0,0 +1,10 @@ +- op: add + path: /spec/pipeline/0/input/inline/templates/- + value: | + apiVersion: iam.aws.upbound.io/v1beta1 + kind: User + metadata: + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: user + spec: + forProvider: {} \ No newline at end of file diff --git a/fn_test.go b/fn_test.go index 9beb62a..91c5339 100644 --- a/fn_test.go +++ b/fn_test.go @@ -139,7 +139,7 @@ func TestRunFunction(t *testing.T) { Results: []*fnv1.Result{ { Severity: fnv1.Severity_SEVERITY_FATAL, - Message: "invalid function input: inline.template should be provided", + Message: "invalid function input: inline.template or inline.templates should be provided", Target: fnv1.Target_TARGET_COMPOSITE.Enum(), }, }, @@ -359,6 +359,44 @@ func TestRunFunction(t *testing.T) { }, }, }, + "ResponseIsReturnedWithTemplatingUsingTheTemplatesField": { + reason: "The Function should return the desired composite resource and the templated composed resources.", + args: args{ + req: &fnv1.RunFunctionRequest{ + Meta: &fnv1.RequestMeta{Tag: "templates"}, + Input: resource.MustStructObject( + &v1beta1.GoTemplate{ + Source: v1beta1.InlineSource, + Inline: &v1beta1.TemplateSourceInline{Templates: []string{cdTmpl}}, + }), + Observed: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + Desired: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + want: want{ + rsp: &fnv1.RunFunctionResponse{ + Meta: &fnv1.ResponseMeta{Tag: "templates", Ttl: durationpb.New(response.DefaultTTL)}, + Desired: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*fnv1.Resource{ + "cool-cd": { + Resource: resource.MustStructJSON(`{"apiVersion": "example.org/v1","kind":"CD","metadata":{"annotations":{},"name":"cool-cd","labels":{"belongsTo":"cool-xr"}}}`), + }, + }, + }, + }, + }, + }, "UpdateDesiredCompositeStatus": { reason: "The Function should update the desired composite resource status.", args: args{ diff --git a/input/v1beta1/input.go b/input/v1beta1/input.go index 0952708..6ef2494 100644 --- a/input/v1beta1/input.go +++ b/input/v1beta1/input.go @@ -49,9 +49,11 @@ const ( EnvironmentSource TemplateSource = "Environment" ) -// TemplateSourceInline defines the structure of the inline source. +// TemplateSourceInline defines the structure of the inline source. Allows specifying either a single inline template or multiple templates, but not both. +// +kubebuilder:validation:XValidation:rule="(has(self.template) ? 1 : 0) + (has(self.templates) ? 1 : 0) == 1",message="Exactly one of 'template' or 'templates' must be set" type TemplateSourceInline struct { - Template string `json:"template,omitempty"` + Template string `json:"template,omitempty"` + Templates []string `json:"templates,omitempty"` } // TemplateSourceFileSystem defines the structure of the filesystem source. diff --git a/input/v1beta1/zz_generated.deepcopy.go b/input/v1beta1/zz_generated.deepcopy.go index 9a48408..6307288 100644 --- a/input/v1beta1/zz_generated.deepcopy.go +++ b/input/v1beta1/zz_generated.deepcopy.go @@ -46,7 +46,7 @@ func (in *GoTemplate) DeepCopyInto(out *GoTemplate) { if in.Inline != nil { in, out := &in.Inline, &out.Inline *out = new(TemplateSourceInline) - **out = **in + (*in).DeepCopyInto(*out) } if in.FileSystem != nil { in, out := &in.FileSystem, &out.FileSystem @@ -120,6 +120,11 @@ func (in *TemplateSourceFileSystem) DeepCopy() *TemplateSourceFileSystem { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TemplateSourceInline) DeepCopyInto(out *TemplateSourceInline) { *out = *in + if in.Templates != nil { + in, out := &in.Templates, &out.Templates + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateSourceInline. diff --git a/package/input/gotemplating.fn.crossplane.io_gotemplates.yaml b/package/input/gotemplating.fn.crossplane.io_gotemplates.yaml index 2354529..69ea9a4 100644 --- a/package/input/gotemplating.fn.crossplane.io_gotemplates.yaml +++ b/package/input/gotemplating.fn.crossplane.io_gotemplates.yaml @@ -58,7 +58,15 @@ spec: properties: template: type: string + templates: + items: + type: string + type: array type: object + x-kubernetes-validations: + - message: Exactly one of 'template' or 'templates' must be set + rule: '(has(self.template) ? 1 : 0) + (has(self.templates) ? 1 : 0) + == 1' kind: description: |- Kind is a string value representing the REST resource this object represents. diff --git a/template.go b/template.go index 4a62536..db80402 100644 --- a/template.go +++ b/template.go @@ -3,6 +3,7 @@ package main import ( "io/fs" "path/filepath" + "strings" "github.com/crossplane-contrib/function-go-templating/input/v1beta1" "google.golang.org/protobuf/types/known/structpb" @@ -57,12 +58,18 @@ func (is *InlineSource) GetTemplates() string { } func newInlineSource(in *v1beta1.GoTemplate) (*InlineSource, error) { - if in.Inline == nil || in.Inline.Template == "" { - return nil, errors.New("inline.template should be provided") + if in.Inline == nil || (in.Inline.Template == "" && len(in.Inline.Templates) == 0) { + return nil, errors.New("inline.template or inline.templates should be provided") + } + + template := strings.Join(in.Inline.Templates, "\n---\n") + + if in.Inline.Template != "" { + template = in.Inline.Template } return &InlineSource{ - Template: in.Inline.Template, + Template: template, }, nil }