Create a composition with Go Templates
Upbound Crossplane allows you to choose how you want to write your composition logic based on your preferred language.
You can choose:
Go - High performance. IDE support with full type safety.
Go Templating (this guide) - Good for YAML-like configurations. IDE support with YAML language server.
KCL - Concise. Good for transitioning from another configuration language like HCL. IDE support with language server.
Python - Highly accessible, supports complex logic. Provides type hints and autocompletion in your IDE.
Overview
This guide explains how to create compositions that turn your XRs into actual cloud resources. Compositions allow you to implement the business logic that powers your control plane.
Use this guide after you define your API schema and need to write the logic that creates and manages the underlying resources.
Upbound supports defining your control plane APIs using YAML with Go templates. Go templating functions can make use of all built-in Go templating features and anything supported by Crossplane's function-go-templating.
You can organize templates into multiple files with an arbitrary directory structure, as long as all files have the .gotmpl
file extension. Templates are read in lexical order and concatenated with YAML separators (---
) between them.
Prerequisites
Before you begin, make sure:
- You designed your XRD
- You've added provider dependencies
- understand your XRD schema and what resources you need to create
- YAML Visual Studio Code extension is installed
- Modelines extension is installed (optional)
Create your composition scaffold
Use the XRD you created in the previous step to generate a new composition:
up composition generate apis/<your_resource_name>/definition.yaml
This command creates apis/<your_resource_name>/composition.yaml
which references the XRD.
Generate your function
Use your chosen programming language to generate a new function:
up function generate --language=go-templating compose-resources
apis/<your_resource_name>/composition.yaml
This command creates a functions/compose-resources
directory with your function code and updates your composition file to reference it.
Your function file in functions/compose-resources/01-compose.yaml.gotmpl
should be similar to:
# code: language=yaml
# yaml-language-server: $schema=../../.up/json/models/index.schema.json
---
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
annotations:
{{ setResourceNameAnnotation "bucket" }}
spec:
forProvider:
region: "{{ $xr.spec.parameters.region }}"
Create your function logic
The following example function composes an S3 bucket based on a simplified bucket XRD:
# code: language=yaml
# yaml-language-server: $schema=../../.up/json/models/index.schema.json
---
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
annotations:
{{ setResourceNameAnnotation "bucket" }}
spec:
forProvider:
region: "{{ $xr.spec.parameters.region }}"
A more advanced Go templating function
The files below take a composite resource (XR) as input and produces managed resources (MRs) from the S3 provider based on its parameters. The templates are split across multiple files for readability.
The function always composes an S3 bucket. When the S3 bucket exists, it also composes a bucket access control list (ACL). The ACL references the bucket by name.
If the composite resource's spec.versioning
field is true
, the function enables versioning by composing a bucket versioning configuration. Like the ACL, the versioning configuration references the bucket by name.
As a best practice, place any Go templating flow control (if
statements, loops, variable assignments, etc.) in YAML comments, as demonstrated below. While not required, this avoids confusing the YAML language server with non-YAML syntax, leading to a better editor experience.
00-prelude.yaml.gotmpl
This file contains generated boilerplate to place the observed composite resource and its parameters in Go templating variables, for use by subsequent templates.
#{{ $xr := getCompositeResource . }}
#{{ $params := $xr.spec.parameters }}
01-bucket.yaml.gotmpl
This file creates the Bucket
MR. It then checks whether Crossplane has assigned an external name for the bucket and places the name in a variable.
# code: language=yaml
# yaml-language-server: $schema=../../.up/json/models/index.schema.json
---
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
annotations:
{{ setResourceNameAnnotation "bucket" }}
spec:
forProvider:
region: "{{ $params.region }}"
# The desired ACL, encryption, and versioning resources all need to refer to the
# bucket by its external name, which is stored in its external name
# annotation. Fetch the external name into a variable so subsequent templates
# can use it.
#
#{{ $bucket := getComposedResource . "bucket" }}
#{{ $bucket_external_name := "" }}
#{{ if $bucket }}
#{{ $bucket_external_name = get $bucket.metadata.annotations "crossplane.io/external-name" }}
#{{ end }}
02-acl.yaml.gotmpl
This file creates the BucketACL
MR. Since this MR requires the bucket's external name, the template produces the MR only when the external name is available.
# code: language=yaml
# yaml-language-server: $schema=../../.up/json/models/index.schema.json
# Don't create the ACL until the bucket name is available.
#{{ if $bucket_external_name }}
---
apiVersion: s3.aws.upbound.io/v1beta1
kind: BucketACL
metadata:
annotations:
{{ setResourceNameAnnotation "acl" }}
spec:
forProvider:
region: "{{ $params.region }}"
bucket: "{{ $bucket_external_name }}"
acl: "{{ $params.acl }}"
#{{ end }}
03-versioning.yaml.gotmpl
This file creates the BucketVersioning
MR when the XR has versioning enabled. Like the BucketACL
, this MR requires the bucket's external name, so the template produces the MR only after the external name is available.
# code: language=yaml
# yaml-language-server: $schema=../../.up/json/models/index.schema.json
#{{ if $params.versioning }}
# Don't create the BucketVersioning until the bucket name is available.
#{{ if $bucket_external_name }}
---
apiVersion: s3.aws.upbound.io/v1beta1
kind: BucketVersioning
metadata:
annotations:
{{ setResourceNameAnnotation "versioning" }}
spec:
forProvider:
region: "{{ $params.region }}"
bucket: "{{ $bucket_external_name }}"
versioningConfiguration:
- status: Enabled
#{{ end }}
#{{ end }}
Work with schemas
Upbound Official Providers and some other packages include JSON Schemas for their resources. The up
tooling generates a "meta-schema" that references all the individual schemas. Editors, via the YAML language server, consume these schemas and provide in-line documentation, linting, autocompletion, and other features when writing embedded Go templating functions.
Make schemas available to a function
Use up dependency add
to make schemas from a dependency available to a function. Dependencies are most often Crossplane providers, but they can also be configurations that include XRDs.
up dependency add xpkg.upbound.io/upbound/provider-aws-s3:v1.20.0
Use up project build
to make schemas available for XRDs defined by your project.
up project build
Use schemas in a Go templating file
The YAML language server reads a special comment at the top of the file to determine the file's schema. This comment indicates the location of the schema file built by up
based on all the schemas available in the project:
# yaml-language-server: $schema=../../.up/json/models/index.schema.json
All Upbound Official Providers include JSON Schemas.
When you build your project with up project build
, the generated artifact contains the generated schemas for your XRDs. You can build a project and then import that project as a dependency for the resources you define. Your own project's schemas are also available when editing your functions as described above.
Handle inputs and outputs
Functions require inputs and outputs to process requests and return values to your control plane.
See the Crossplane function-go-templating documentation for the full set of inputs, outputs, and other features supported by Go templating.
Inputs
Compositions execute in a pipeline of one or more sequential functions. A function updates desired resource state and returns it to Crossplane. Function requests and values rely on three pieces of information:
- The observed state of the composite resource, and any composed resources.
- The desired state of the composite resource, and any composed resources.
- The function pipeline's context.
Each composition pipeline provides this information as inputs into the function.
Function inputs are available to Go templates in the following template inputs, which templates can access directly or with helper functions:
- Observed state:
.observed
- The
getCompositeResource
helper function fetches the observed composite resource. - The
getComposedResource
helper function looks up an observed composed resource.
- The
- Desired state:
.desired
- Pipeline context:
.context
Outputs
To add resources to the pipeline's desired composed resources, define them in the template. Use the gotemplating.fn.crossplane.io/composition-resource-name
annotation to define unique names for each resource. This allows you to update a resource rather than create a new one on subsequent functions runs. The setResourceNameAnnotation
helper function sets this annotation:
# code: language=yaml
# yaml-language-server: $schema=../../.up/json/models/index.schema.json
---
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
annotations:
{{ setResourceNameAnnotation "bucket" }}
spec:
forProvider:
region: "{{ $xr.spec.parameters.region }}"
To update the composite resource's status, have your templates output a resource of the composite's type without the composition-resource-name
annotation:
# code: language=yaml
# yaml-language-server: $schema=../../.up/json/models/index.schema.json
---
apiVersion: devexdemo.upbound.io/v1alpha1
kind: XBucket
status:
someInformation: cool-status
To set conditions on the claim and composite, you can add a ClaimConditions
resource to your templates:
apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1
kind: ClaimConditions
conditions:
- type: BucketReady
status: "True"
reason: Ready
message: Bucket is ready
target: CompositeAndClaim
See also
- Go Templates Documentation - Official Go template documentation
- function-go-templating Documentation - Complete function reference
- JSON Schema - Understanding schema validation