


Piotr Zaniewski
Upbound
Read time: 0 mins
Read time: 0 mins
Improve Crossplane Compositions Authoring with go-templating-function
Improve Crossplane Compositions Authoring with go-templating-function
With the addition of compositions functions in the Crossplane v.1.11 and their graduation to beta in Crossplane v.1.14 it is possible to use a programming language (for now Go) to enrich and transform the authoring of compositions.
Improve Crossplane Compositions Authoring with go-templating-function
Improve Crossplane Compositions Authoring with go-templating-function
With the addition of compositions functions in the Crossplane v.1.11 and their graduation to beta in Crossplane v.1.14 it is possible to use a programming language (for now Go) to enrich and transform the authoring of compositions.
Introduction
Crossplane Compositions are a powerful abstraction layer that enables platform teams to create a custom API for their internal customers to simplify and standardize the infrastructure management process. The Composition authors wrestle with the complexity of cloud infrastructure and at the same time need to ensure a stable and user-friendly API surface for the platform consumers.
Historically, Compositions were intended to support only very simple resources’ manipulation to allow the API calls to patch and transform the information passed from a Claim or Composite Resource (XR) to the Composition engine. The lack of touring complete language supporting the required transformations resulted in a very verbose YAML files with lots of repetitions, increasing the toil of authoring Compositions.
With the addition of Compositions Functions in the Crossplane v.1.11 it is possible to use a programming language, like Go or Python (more to come) or in our case a capability of Go called Go-templating to enrich and transform the authoring of Compositions.
This blog will guide you through the process of rewriting a simple internal platform Composition in the Go templating style using the go-templating-function.
Use Case: Collapsing two Compositions into one
Since traditional Composition's engine does not allow using conditionals, there is no way to render one Composition or another using a parameter from a Claim. To illustrate this example, let’s look at the two Compositions that render a GCP Bucket:
Standard bucket
This very simple composition renders a single GCP bucket
apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: xstoragebuckets.platform-composites.upbound.io labels: provider: gcp type: generic spec: patchSets: ... Omitted for brevity writeConnectionSecretsToNamespace: upbound-system compositeTypeRef: apiVersion: platform-composites.upbound.io/v1alpha1 kind: XStorageBucket resources: - name: storagebucket base: apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket spec: forProvider: location: us-west1 storageClass: STANDARD providerConfigRef: name: default patches: ...
apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: xstoragebuckets.platform-composites.upbound.io labels: provider: gcp type: generic spec: patchSets: ... Omitted for brevity writeConnectionSecretsToNamespace: upbound-system compositeTypeRef: apiVersion: platform-composites.upbound.io/v1alpha1 kind: XStorageBucket resources: - name: storagebucket base: apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket spec: forProvider: location: us-west1 storageClass: STANDARD providerConfigRef: name: default patches: ...
apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: xstoragebuckets.platform-composites.upbound.io labels: provider: gcp type: generic spec: patchSets: ... Omitted for brevity writeConnectionSecretsToNamespace: upbound-system compositeTypeRef: apiVersion: platform-composites.upbound.io/v1alpha1 kind: XStorageBucket resources: - name: storagebucket base: apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket spec: forProvider: location: us-west1 storageClass: STANDARD providerConfigRef: name: default patches: ...
Versioned Bucket
Another Composition needs to be created to accommodate for a versioning setting.
apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: xstoragebuckets.platform-composites.upbound.io labels: provider: gcp type: generic spec: patchSets: ... Omitted for brevity writeConnectionSecretsToNamespace: upbound-system compositeTypeRef: apiVersion: platform-composites.upbound.io/v1alpha1 kind: XStorageBucket resources: - name: storagebucket base: apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket spec: forProvider: location: us-west1 versioning: - enabled: true storageClass: STANDARD providerConfigRef: name: default patches: ...
apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: xstoragebuckets.platform-composites.upbound.io labels: provider: gcp type: generic spec: patchSets: ... Omitted for brevity writeConnectionSecretsToNamespace: upbound-system compositeTypeRef: apiVersion: platform-composites.upbound.io/v1alpha1 kind: XStorageBucket resources: - name: storagebucket base: apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket spec: forProvider: location: us-west1 versioning: - enabled: true storageClass: STANDARD providerConfigRef: name: default patches: ...
apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: xstoragebuckets.platform-composites.upbound.io labels: provider: gcp type: generic spec: patchSets: ... Omitted for brevity writeConnectionSecretsToNamespace: upbound-system compositeTypeRef: apiVersion: platform-composites.upbound.io/v1alpha1 kind: XStorageBucket resources: - name: storagebucket base: apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket spec: forProvider: location: us-west1 versioning: - enabled: true storageClass: STANDARD providerConfigRef: name: default patches: ...
In this case, we have to have two Compositions and need to use a Composition selector in a claim just for this one field.
Using go-templating-function
The two Compositions can be transformed into just one using the go-templating-function. Follow these steps
1. Install go-templating-function and function-auto-ready from the marketplace.
apiVersion: pkg.crossplane.io/v1beta1 kind: Function metadata: name: function-go-templating spec: package: xpkg.upbound.io/crossplane-contrib/function-go-templating:v0.2.2 --- apiVersion: pkg.crossplane.io/v1beta1 kind: Function metadata: name: function-auto-ready spec: package
apiVersion: pkg.crossplane.io/v1beta1 kind: Function metadata: name: function-go-templating spec: package: xpkg.upbound.io/crossplane-contrib/function-go-templating:v0.2.2 --- apiVersion: pkg.crossplane.io/v1beta1 kind: Function metadata: name: function-auto-ready spec: package
apiVersion: pkg.crossplane.io/v1beta1 kind: Function metadata: name: function-go-templating spec: package: xpkg.upbound.io/crossplane-contrib/function-go-templating:v0.2.2 --- apiVersion: pkg.crossplane.io/v1beta1 kind: Function metadata: name: function-auto-ready spec: package
2. Convert the Composition to use the go templating. The initial conversion gives us one to one translation between the standard “Patch and Transform” style Composition and the go-templating style. We are going to improve this design.
apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: xstoragebuckets.platform-composites.upbound.io labels: provider: gcp type: generic spec: writeConnectionSecretsToNamespace: upbound-system compositeTypeRef: apiVersion: platform-composites.upbound.io/v1alpha1 kind: XStorageBucket mode: Pipeline pipeline: - step: render-templates functionRef: name: function-go-templating input: apiVersion: gotemplate.fn.crossplane.io/v1beta1 kind: GoTemplate source: Inline inline: template: | --- apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket metadata: annotations: gotemplating.fn.crossplane.io/composition-resource-name: {{ .observed.composite.resource.metadata.name }} name: {{ .observed.composite.resource.metadata.name }} labels: owner: {{ .observed.composite.resource.spec.parameters.owner }} service: {{ .observed.composite.resource.spec.parameters.service }} spec: forProvider: location: {{ .observed.composite.resource.spec.parameters.location }} storageClass: {{ .observed.composite.resource.spec.parameters.class }} providerConfigRef: name: {{ .observed.composite.resource.spec.parameters.environment }} - step: ready functionRef: name
apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: xstoragebuckets.platform-composites.upbound.io labels: provider: gcp type: generic spec: writeConnectionSecretsToNamespace: upbound-system compositeTypeRef: apiVersion: platform-composites.upbound.io/v1alpha1 kind: XStorageBucket mode: Pipeline pipeline: - step: render-templates functionRef: name: function-go-templating input: apiVersion: gotemplate.fn.crossplane.io/v1beta1 kind: GoTemplate source: Inline inline: template: | --- apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket metadata: annotations: gotemplating.fn.crossplane.io/composition-resource-name: {{ .observed.composite.resource.metadata.name }} name: {{ .observed.composite.resource.metadata.name }} labels: owner: {{ .observed.composite.resource.spec.parameters.owner }} service: {{ .observed.composite.resource.spec.parameters.service }} spec: forProvider: location: {{ .observed.composite.resource.spec.parameters.location }} storageClass: {{ .observed.composite.resource.spec.parameters.class }} providerConfigRef: name: {{ .observed.composite.resource.spec.parameters.environment }} - step: ready functionRef: name
apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: xstoragebuckets.platform-composites.upbound.io labels: provider: gcp type: generic spec: writeConnectionSecretsToNamespace: upbound-system compositeTypeRef: apiVersion: platform-composites.upbound.io/v1alpha1 kind: XStorageBucket mode: Pipeline pipeline: - step: render-templates functionRef: name: function-go-templating input: apiVersion: gotemplate.fn.crossplane.io/v1beta1 kind: GoTemplate source: Inline inline: template: | --- apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket metadata: annotations: gotemplating.fn.crossplane.io/composition-resource-name: {{ .observed.composite.resource.metadata.name }} name: {{ .observed.composite.resource.metadata.name }} labels: owner: {{ .observed.composite.resource.spec.parameters.owner }} service: {{ .observed.composite.resource.spec.parameters.service }} spec: forProvider: location: {{ .observed.composite.resource.spec.parameters.location }} storageClass: {{ .observed.composite.resource.spec.parameters.class }} providerConfigRef: name: {{ .observed.composite.resource.spec.parameters.environment }} - step: ready functionRef: name
Using Composition Functions is very well documented in the Crossplane docs. Read the details to learn more about it.
The current implementation of our Composition renders the non-versioned Bucket. Next steps are to change the XRD and template to accommodate for the versioning field.
Using the new Crossplane beta trace command is helpful in checking the latest events on the composed resources from the Claim/XR:
Adding the versioning field
1. Modify the XRD to set the versioning field with a boolean flag:
apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition metadata: name: xstoragebuckets.platform-composites.upbound.io spec: group: platform-composites.upbound.io names: kind: XStorageBucket plural: xstoragebuckets claimNames: kind: StorageBucket plural: storagebuckets defaultCompositionRef: name: storagebuckets.platform-composites.upbound.io connectionSecretKeys: versions: - name: v1alpha1 served: true referenceable: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: parameters: type: object description: Generic XRD parameters properties: versioningEnabled: type: boolean description: specify if the bucket should be versioned owner: type: string description: Squad or individual who owns the cloud resource. service: type: string description: Service resource belogs to, like shimmer, api etc. location: type: string description: Passthrough location from cloud provider. Defaults to us-west1 for GCP. environment: type: string description: Playground, dev, staging, production this maps to ProviderConfig that points to a specific project in GCP. storageClass: type: string description: "Possible values: STANDARD, NEARLINE, COLDLINE. Defaults to STANDARD. The value is ingored for the secure bucket." required: - environment - owner - service - versioningEnabled required
apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition metadata: name: xstoragebuckets.platform-composites.upbound.io spec: group: platform-composites.upbound.io names: kind: XStorageBucket plural: xstoragebuckets claimNames: kind: StorageBucket plural: storagebuckets defaultCompositionRef: name: storagebuckets.platform-composites.upbound.io connectionSecretKeys: versions: - name: v1alpha1 served: true referenceable: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: parameters: type: object description: Generic XRD parameters properties: versioningEnabled: type: boolean description: specify if the bucket should be versioned owner: type: string description: Squad or individual who owns the cloud resource. service: type: string description: Service resource belogs to, like shimmer, api etc. location: type: string description: Passthrough location from cloud provider. Defaults to us-west1 for GCP. environment: type: string description: Playground, dev, staging, production this maps to ProviderConfig that points to a specific project in GCP. storageClass: type: string description: "Possible values: STANDARD, NEARLINE, COLDLINE. Defaults to STANDARD. The value is ingored for the secure bucket." required: - environment - owner - service - versioningEnabled required
apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition metadata: name: xstoragebuckets.platform-composites.upbound.io spec: group: platform-composites.upbound.io names: kind: XStorageBucket plural: xstoragebuckets claimNames: kind: StorageBucket plural: storagebuckets defaultCompositionRef: name: storagebuckets.platform-composites.upbound.io connectionSecretKeys: versions: - name: v1alpha1 served: true referenceable: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: parameters: type: object description: Generic XRD parameters properties: versioningEnabled: type: boolean description: specify if the bucket should be versioned owner: type: string description: Squad or individual who owns the cloud resource. service: type: string description: Service resource belogs to, like shimmer, api etc. location: type: string description: Passthrough location from cloud provider. Defaults to us-west1 for GCP. environment: type: string description: Playground, dev, staging, production this maps to ProviderConfig that points to a specific project in GCP. storageClass: type: string description: "Possible values: STANDARD, NEARLINE, COLDLINE. Defaults to STANDARD. The value is ingored for the secure bucket." required: - environment - owner - service - versioningEnabled required
2. Now let’s modify the Composition template to add the conditional for the versioning field.
apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: xstoragebuckets.platform-composites.upbound.io labels: provider: gcp type: generic spec: writeConnectionSecretsToNamespace: upbound-system compositeTypeRef: apiVersion: platform-composites.upbound.io/v1alpha1 kind: XStorageBucket mode: Pipeline pipeline: - step: render-templates functionRef: name: function-go-templating input: apiVersion: gotemplate.fn.crossplane.io/v1beta1 kind: GoTemplate source: Inline inline: template: | --- apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket metadata: annotations: gotemplating.fn.crossplane.io/composition-resource-name: {{ .observed.composite.resource.metadata.name }} name: {{ .observed.composite.resource.metadata.name }} labels: owner: {{ .observed.composite.resource.spec.parameters.owner }} service: {{ .observed.composite.resource.spec.parameters.service }} spec: forProvider: location: {{ .observed.composite.resource.spec.parameters.location }} storageClass: {{ .observed.composite.resource.spec.parameters.storageClass }} {{- if .observed.composite.resource.spec.parameters.versioningEnabled }} versioning: - enabled: true {{- else }} versioning: - enabled: false {{- end }} providerConfigRef: name: {{ .observed.composite.resource.spec.parameters.environment }} - step: ready functionRef: name
apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: xstoragebuckets.platform-composites.upbound.io labels: provider: gcp type: generic spec: writeConnectionSecretsToNamespace: upbound-system compositeTypeRef: apiVersion: platform-composites.upbound.io/v1alpha1 kind: XStorageBucket mode: Pipeline pipeline: - step: render-templates functionRef: name: function-go-templating input: apiVersion: gotemplate.fn.crossplane.io/v1beta1 kind: GoTemplate source: Inline inline: template: | --- apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket metadata: annotations: gotemplating.fn.crossplane.io/composition-resource-name: {{ .observed.composite.resource.metadata.name }} name: {{ .observed.composite.resource.metadata.name }} labels: owner: {{ .observed.composite.resource.spec.parameters.owner }} service: {{ .observed.composite.resource.spec.parameters.service }} spec: forProvider: location: {{ .observed.composite.resource.spec.parameters.location }} storageClass: {{ .observed.composite.resource.spec.parameters.storageClass }} {{- if .observed.composite.resource.spec.parameters.versioningEnabled }} versioning: - enabled: true {{- else }} versioning: - enabled: false {{- end }} providerConfigRef: name: {{ .observed.composite.resource.spec.parameters.environment }} - step: ready functionRef: name
apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: xstoragebuckets.platform-composites.upbound.io labels: provider: gcp type: generic spec: writeConnectionSecretsToNamespace: upbound-system compositeTypeRef: apiVersion: platform-composites.upbound.io/v1alpha1 kind: XStorageBucket mode: Pipeline pipeline: - step: render-templates functionRef: name: function-go-templating input: apiVersion: gotemplate.fn.crossplane.io/v1beta1 kind: GoTemplate source: Inline inline: template: | --- apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket metadata: annotations: gotemplating.fn.crossplane.io/composition-resource-name: {{ .observed.composite.resource.metadata.name }} name: {{ .observed.composite.resource.metadata.name }} labels: owner: {{ .observed.composite.resource.spec.parameters.owner }} service: {{ .observed.composite.resource.spec.parameters.service }} spec: forProvider: location: {{ .observed.composite.resource.spec.parameters.location }} storageClass: {{ .observed.composite.resource.spec.parameters.storageClass }} {{- if .observed.composite.resource.spec.parameters.versioningEnabled }} versioning: - enabled: true {{- else }} versioning: - enabled: false {{- end }} providerConfigRef: name: {{ .observed.composite.resource.spec.parameters.environment }} - step: ready functionRef: name
3. Now we are ready to apply the Claim of the versioning enabled Bucket to the preconfigured GCP project.
apiVersion: platform-composites.upbound.io/v1alpha1 kind: StorageBucket metadata: name: sample-storage-12345 namespace: default spec: compositionSelector: matchLabels: # Only provider GCP is available at the moment provider: gcp type: generic parameters: versioningEnabled: true owner: squad-platform service: platform-composites # Passthrough location from cloud provider # defaults to us-west1 for GCP location: us-west1 #Optional # This maps to ProviderConfig that points to a specific # project in GCP. Use default for local testing and playground for crossplane-playground environment: provider-gcp #Required # Possible values: STANDARD, NEARLINE, COLDLINE, ARCHIVE # Defaults to standard storageClass: STANDARD #Optional
apiVersion: platform-composites.upbound.io/v1alpha1 kind: StorageBucket metadata: name: sample-storage-12345 namespace: default spec: compositionSelector: matchLabels: # Only provider GCP is available at the moment provider: gcp type: generic parameters: versioningEnabled: true owner: squad-platform service: platform-composites # Passthrough location from cloud provider # defaults to us-west1 for GCP location: us-west1 #Optional # This maps to ProviderConfig that points to a specific # project in GCP. Use default for local testing and playground for crossplane-playground environment: provider-gcp #Required # Possible values: STANDARD, NEARLINE, COLDLINE, ARCHIVE # Defaults to standard storageClass: STANDARD #Optional
apiVersion: platform-composites.upbound.io/v1alpha1 kind: StorageBucket metadata: name: sample-storage-12345 namespace: default spec: compositionSelector: matchLabels: # Only provider GCP is available at the moment provider: gcp type: generic parameters: versioningEnabled: true owner: squad-platform service: platform-composites # Passthrough location from cloud provider # defaults to us-west1 for GCP location: us-west1 #Optional # This maps to ProviderConfig that points to a specific # project in GCP. Use default for local testing and playground for crossplane-playground environment: provider-gcp #Required # Possible values: STANDARD, NEARLINE, COLDLINE, ARCHIVE # Defaults to standard storageClass: STANDARD #Optional
The resource rendered correctly:
spec: deletionPolicy: Delete forProvider: location: us-west1 project: squad-platform-playground publicAccessPrevention: inherited storageClass: STANDARD versioning: - enabled: true initProvider: {} managementPolicies: - '*' providerConfigRef: name
spec: deletionPolicy: Delete forProvider: location: us-west1 project: squad-platform-playground publicAccessPrevention: inherited storageClass: STANDARD versioning: - enabled: true initProvider: {} managementPolicies: - '*' providerConfigRef: name
spec: deletionPolicy: Delete forProvider: location: us-west1 project: squad-platform-playground publicAccessPrevention: inherited storageClass: STANDARD versioning: - enabled: true initProvider: {} managementPolicies: - '*' providerConfigRef: name

Default values
Not all the schema fields are required, and for those the Composition provides default values. location and storageClass are not required in the schema, and the Composition provides default values for those fields when omitted. Let’s add them to the template. Now supplying a Claim without the location and storageClass fields will result in the default values being rendered like so.
... location: {{ default "us-west1" .observed.composite.resource.spec.parameters.location }} storageClass: {{ default "STANDARD" .observed.composite.resource.spec.parameters.storageClass }} ...
... location: {{ default "us-west1" .observed.composite.resource.spec.parameters.location }} storageClass: {{ default "STANDARD" .observed.composite.resource.spec.parameters.storageClass }} ...
... location: {{ default "us-west1" .observed.composite.resource.spec.parameters.location }} storageClass: {{ default "STANDARD" .observed.composite.resource.spec.parameters.storageClass }} ...
Bringing back PatchSets
This specific Composition is very simple, but there are multiple others where the same patch needs to be applied to multiple resources. In the original Composition we have the ownerAndServiceLabels that are applied to all resources that support labels.
- name: ownerAndServiceLabels patches: - type: FromCompositeFieldPath fromFieldPath: spec.parameters.owner toFieldPath: spec.forProvider.labels[owner] - type: FromCompositeFieldPath fromFieldPath: spec.parameters.service toFieldPath
- name: ownerAndServiceLabels patches: - type: FromCompositeFieldPath fromFieldPath: spec.parameters.owner toFieldPath: spec.forProvider.labels[owner] - type: FromCompositeFieldPath fromFieldPath: spec.parameters.service toFieldPath
- name: ownerAndServiceLabels patches: - type: FromCompositeFieldPath fromFieldPath: spec.parameters.owner toFieldPath: spec.forProvider.labels[owner] - type: FromCompositeFieldPath fromFieldPath: spec.parameters.service toFieldPath
With the go-templating-function, it’s easy to define the patchSet-like behavior
Here is the modified Composition template part with the labels:
template: | --- {{- define "ownerAndProjectLabels" }} labels: owner: {{ .observed.composite.resource.spec.parameters.owner }} service: {{ .observed.composite.resource.spec.parameters.service }} {{- end }} apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket metadata: name: {{ .observed.composite.resource.metadata.name }} annotations: gotemplating.fn.crossplane.io/composition-resource-name: {{ .observed.composite.resource.metadata.name }} {{ template "ownerAndProjectLabels" .
template: | --- {{- define "ownerAndProjectLabels" }} labels: owner: {{ .observed.composite.resource.spec.parameters.owner }} service: {{ .observed.composite.resource.spec.parameters.service }} {{- end }} apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket metadata: name: {{ .observed.composite.resource.metadata.name }} annotations: gotemplating.fn.crossplane.io/composition-resource-name: {{ .observed.composite.resource.metadata.name }} {{ template "ownerAndProjectLabels" .
template: | --- {{- define "ownerAndProjectLabels" }} labels: owner: {{ .observed.composite.resource.spec.parameters.owner }} service: {{ .observed.composite.resource.spec.parameters.service }} {{- end }} apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket metadata: name: {{ .observed.composite.resource.metadata.name }} annotations: gotemplating.fn.crossplane.io/composition-resource-name: {{ .observed.composite.resource.metadata.name }} {{ template "ownerAndProjectLabels" .
Template Variables
One small quality of life improvement is to define variables in the template so there is no need to type .observed.composite.resource.spec.parameters all the time. Here is the final version of the Composition:
apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: xstoragebuckets.platform-composites.upbound.io labels: provider: gcp type: generic spec: writeConnectionSecretsToNamespace: upbound-system compositeTypeRef: apiVersion: platform-composites.upbound.io/v1alpha1 kind: XStorageBucket mode: Pipeline pipeline: - step: render-templates functionRef: name: function-go-templating input: apiVersion: gotemplate.fn.crossplane.io/v1beta1 kind: GoTemplate source: Inline inline: template: | --- {{ $claim := .observed.composite.resource }} {{ $parameters := .observed.composite.resource.spec.parameters }} {{- define "ownerAndProjectLabels" }} labels: owner: {{ .observed.composite.resource.spec.parameters.owner }} service: {{ .observed.composite.resource.spec.parameters.service }} {{- end }} apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket metadata: name: {{ $claim.metadata.name }} annotations: gotemplating.fn.crossplane.io/composition-resource-name: {{ $claim.metadata.name }} {{ template "ownerAndProjectLabels" . }} spec: forProvider: location: {{ $parameters.location }} storageClass: {{ $parameters.storageClass }} {{- if $parameters.versioningEnabled }} versioning: - enabled: true {{ else }} versioning: - enabled: false {{- end }} providerConfigRef: name: {{ $parameters.environment }} - step: ready functionRef: name
apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: xstoragebuckets.platform-composites.upbound.io labels: provider: gcp type: generic spec: writeConnectionSecretsToNamespace: upbound-system compositeTypeRef: apiVersion: platform-composites.upbound.io/v1alpha1 kind: XStorageBucket mode: Pipeline pipeline: - step: render-templates functionRef: name: function-go-templating input: apiVersion: gotemplate.fn.crossplane.io/v1beta1 kind: GoTemplate source: Inline inline: template: | --- {{ $claim := .observed.composite.resource }} {{ $parameters := .observed.composite.resource.spec.parameters }} {{- define "ownerAndProjectLabels" }} labels: owner: {{ .observed.composite.resource.spec.parameters.owner }} service: {{ .observed.composite.resource.spec.parameters.service }} {{- end }} apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket metadata: name: {{ $claim.metadata.name }} annotations: gotemplating.fn.crossplane.io/composition-resource-name: {{ $claim.metadata.name }} {{ template "ownerAndProjectLabels" . }} spec: forProvider: location: {{ $parameters.location }} storageClass: {{ $parameters.storageClass }} {{- if $parameters.versioningEnabled }} versioning: - enabled: true {{ else }} versioning: - enabled: false {{- end }} providerConfigRef: name: {{ $parameters.environment }} - step: ready functionRef: name
apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: xstoragebuckets.platform-composites.upbound.io labels: provider: gcp type: generic spec: writeConnectionSecretsToNamespace: upbound-system compositeTypeRef: apiVersion: platform-composites.upbound.io/v1alpha1 kind: XStorageBucket mode: Pipeline pipeline: - step: render-templates functionRef: name: function-go-templating input: apiVersion: gotemplate.fn.crossplane.io/v1beta1 kind: GoTemplate source: Inline inline: template: | --- {{ $claim := .observed.composite.resource }} {{ $parameters := .observed.composite.resource.spec.parameters }} {{- define "ownerAndProjectLabels" }} labels: owner: {{ .observed.composite.resource.spec.parameters.owner }} service: {{ .observed.composite.resource.spec.parameters.service }} {{- end }} apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket metadata: name: {{ $claim.metadata.name }} annotations: gotemplating.fn.crossplane.io/composition-resource-name: {{ $claim.metadata.name }} {{ template "ownerAndProjectLabels" . }} spec: forProvider: location: {{ $parameters.location }} storageClass: {{ $parameters.storageClass }} {{- if $parameters.versioningEnabled }} versioning: - enabled: true {{ else }} versioning: - enabled: false {{- end }} providerConfigRef: name: {{ $parameters.environment }} - step: ready functionRef: name
Conclusion
In conclusion, the use of the go-templating-function in Compositions authoring significantly simplifies the process of creating and managing resources with varying configurations and makes it easier to manage complex Compositions.
It allows for the creation of Compositions with conditional fields, reducing the need for multiple compositions for each variation of a resource. The re-introduction of PatchSets-like behavior makes the portability from “Patch and Transform” style Compositions easier. Adding variables scoped to Composite Resource paths makes it easier to reason about the template.
Additionally, using the new Crossplane CLI functionality to render the resources based on the Claim, Composition and functions and also trace the state of resources is a very helpful development tool. Here’s a tip: running the commands with watch makes the development loop even better.

crossplane beta render examples/claim.yaml gcp-bucket/composite.yaml gcp-bucket/functions.yaml --- apiVersion: platform-composites.upbound.io/v1alpha1 kind: StorageBucket metadata: name: sample-storage-12345 --- apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket metadata: annotations: crossplane.io/composition-resource-name: sample-storage-12345 generateName: sample-storage-12345- labels: crossplane.io/composite: sample-storage-12345 owner: squad-platform service: platform-composites name: sample-storage-12345 ownerReferences: - apiVersion: platform-composites.upbound.io/v1alpha1 blockOwnerDeletion: true controller: true kind: StorageBucket name: sample-storage-12345 uid: "" spec: forProvider: location: us-west1 storageClass: STANDARD versioning: - enabled: true providerConfigRef: name
crossplane beta render examples/claim.yaml gcp-bucket/composite.yaml gcp-bucket/functions.yaml --- apiVersion: platform-composites.upbound.io/v1alpha1 kind: StorageBucket metadata: name: sample-storage-12345 --- apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket metadata: annotations: crossplane.io/composition-resource-name: sample-storage-12345 generateName: sample-storage-12345- labels: crossplane.io/composite: sample-storage-12345 owner: squad-platform service: platform-composites name: sample-storage-12345 ownerReferences: - apiVersion: platform-composites.upbound.io/v1alpha1 blockOwnerDeletion: true controller: true kind: StorageBucket name: sample-storage-12345 uid: "" spec: forProvider: location: us-west1 storageClass: STANDARD versioning: - enabled: true providerConfigRef: name
crossplane beta render examples/claim.yaml gcp-bucket/composite.yaml gcp-bucket/functions.yaml --- apiVersion: platform-composites.upbound.io/v1alpha1 kind: StorageBucket metadata: name: sample-storage-12345 --- apiVersion: storage.gcp.upbound.io/v1beta1 kind: Bucket metadata: annotations: crossplane.io/composition-resource-name: sample-storage-12345 generateName: sample-storage-12345- labels: crossplane.io/composite: sample-storage-12345 owner: squad-platform service: platform-composites name: sample-storage-12345 ownerReferences: - apiVersion: platform-composites.upbound.io/v1alpha1 blockOwnerDeletion: true controller: true kind: StorageBucket name: sample-storage-12345 uid: "" spec: forProvider: location: us-west1 storageClass: STANDARD versioning: - enabled: true providerConfigRef: name
If you are interested in technical details and design of the go-templating-function, check out the go-templating-function one-pager. Big thanks to @ezgidemirel for creating the function! It significantly improves the Compositions authoring process.
You can find various functions in our Marketplace and create your own to take the Crossplane Compositions to the next level.
About Authors

Piotr Zaniewski
Subscribe to the
Upbound Newsletter
Subscribe to the
Upbound Newsletter
Subscribe to the
Upbound Newsletter
Related
Related
Posts
Posts

Hunsung Lee

Hunsung Lee

Ana Margarita Medina

Ana Margarita Medina
Get Started with Upbound Crossplane 2.0
Trusted by 1,000+ organizations and downloaded over 100 million times.
Get Started with Upbound Crossplane 2.0
Trusted by 1,000+ organizations and downloaded over 100 million times.
Get Started with Upbound Crossplane 2.0
Trusted by 1,000+ organizations and downloaded over 100 million times.





