Skip to content

crossplane-contrib/function-go-templating

Repository files navigation

function-go-templating

CI GitHub release (latest SemVer)

This composition function allows you to compose Crossplane resources using Go templates. If you've written a Helm chart before, using this function will be a familiar experience.

Here's an example:

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: example
spec:
  compositeTypeRef:
    apiVersion: example.crossplane.io/v1beta1
    kind: XR
  mode: Pipeline
  pipeline:
    - step: create-a-bucket
      functionRef:
        name: function-go-templating
      input:
        apiVersion: gotemplating.fn.crossplane.io/v1beta1
        kind: GoTemplate
        source: Inline
        inline:
          template: |
            apiVersion: s3.aws.upbound.io/v1beta1
            kind: Bucket
            metadata:
              annotations:
                gotemplating.fn.crossplane.io/composition-resource-name: bucket
            spec:
              forProvider:
                region: {{ .observed.composite.resource.spec.region }}
    - step: automatically-detect-ready-composed-resources
      functionRef:
        name: function-auto-ready

Using this function

This function can load templates from two sources: Inline and FileSystem.

Use the Inline source to specify a simple template inline in your Composition. Multiple YAML manifests can be specified using the --- document separator.

Use the FileSystem source to specify a directory of templates. The FileSystem source treats all files under the specified directory as templates.

The templates are passed a RunFunctionRequest as data. This means that you can access the composite resource, any composed resources, and the function pipeline context using notation like:

  • {{ .observed.composite.resource.metadata.name }}
  • {{ .desired.composite.resource.status.widgets }}
  • {{ (index .desired.composed "resource-name").resource.spec.widgets }}
  • {{ index .context "apiextensions.crossplane.io/environment" }}
  • {{ index .extraResources "some-bucket-by-name" }}

This function supports all of Go's built-in template functions. The above examples use the index function to access keys like resource-name that contain periods, hyphens and other special characters. Like Helm, this function also supports Sprig template functions as well as additional functions.

To return desired composite resource connection details, include a template that produces the special CompositeConnectionDetails resource:

apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1
kind: CompositeConnectionDetails
data:
  connection-secret-key: connection-secret-value

Note: The value of the connection secret value must be base64 encoded. This is already the case if you are referencing a key from a mananged resource's connectionDetails field. However, if you want to include a connection secret value from somewhere else, you will need to use the b64enc Sprig function:

apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1
kind: CompositeConnectionDetails
data:
  server-endpoint: {{ (index $.observed.resources "my-server").resource.status.atProvider.endpoint | b64enc }}

To mark a desired composed resource as ready, use the gotemplating.fn.crossplane.io/ready annotation:

apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
  annotations:
    gotemplating.fn.crossplane.io/composition-resource-name: bucket
    gotemplating.fn.crossplane.io/ready: "True"
spec: {}

See the example directory for examples that you can run locally using the Crossplane CLI:

$ crossplane beta render xr.yaml composition.yaml functions.yaml

See the composition functions documentation to learn more about crossplane beta render.

ExtraResources

By defining one or more special ExtraResources, you can ask Crossplane to retrieve additional resources from the local cluster and make them available to your templates. See the docs for more information.

apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1
kind: ExtraResources
requirements:
  some-foo-by-name:
    # Resources can be requested either by name
    apiVersion: example.com/v1beta1
    kind: Foo
    matchName: "some-extra-foo"
  some-foo-by-labels:
    # Or by label.
    apiVersion: example.com/v1beta1
    kind: Foo
    matchLabels:
      app: my-app
  some-bar-by-a-computed-label:
    # But you can also generate them dynamically using the template, for example:
    apiVersion: example.com/v1beta1
    kind: Bar
    matchLabels:
      foo: {{ .observed.composite.resource.name }}

This will result in Crossplane retrieving the requested resources and making them available to your templates under the extraResources key, with the following format:

{
  "extraResources": {
    "some-foo-by-name": [
      // ... the requested bucket if found, empty otherwise ...
    ],
    "some-foo-by-labels": [
      // ... the requested buckets if found, empty otherwise ...
    ],
    // ... any other requested extra resources ...
  }
}

So, you can access the retrieved resources in your templates like this, for example:

{{- $someExtraResources := index .extraResources "some-extra-resources-key" }}
{{- range $i, $extraResource := $someExtraResources.items }}
#
# Do something for each retrieved extraResource
#
{{- end }}

Writing to the Context

This function can write to the Composition Context. Subsequent pipeline steps will be able to access the data.

---
apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1
kind: Context
data:
  region: {{ $spec.region }}
  id: field
  array:
  - "1" 
  - "2"

To update Context data, match an existing key. For example, function-environment-configs stores data under the key apiextensions.crossplane.io/environment.

In this case, Environment fields update and nestedEnvUpdate.hello would be updated with new values.

---
apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1
kind: Context
data:
  "apiextensions.crossplane.io/environment":
      kind: Environment
      apiVersion: internal.crossplane.io/v1alpha1
      update: environment
      nestedEnvUpdate:
        hello: world
  otherContextData:
    test: field

For more information, see the example in context.

Updating status or creating composed resources with the composite resource's type

This function applies special logic if a resource with the composite resource's type is found in the template.

If the resource name is not set (the gotemplating.fn.crossplane.io/composition-resource-name meta annotation is not present), then the function does not create composed resources with the composite resource's type. In this case only the composite resource's status is updated.

For example, the following composition does not create composed resources. Rather, it updates the composite resource's status to include dummy: cool-status.

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: example-update-status
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: |
            apiVersion: example.crossplane.io/v1beta1
            kind: XR
            status:
              dummy: cool-status

On the other hand, if the resource name is set (using the gotemplating.fn.crossplane.io/composition-resource-name meta annotation), then the function creates composed resources with the composite resource's type.

For example, the following composition will create a composed resource:

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: example-allow-recursion
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: |
            apiVersion: example.crossplane.io/v1beta1
            kind: XR
            metadata:
              annotations:
                {{ setResourceNameAnnotation "recursive-xr" }}
            spec:
              compositionRef:
                name: example-other # make sure to avoid infinite recursion

Warning

This can lead to infinite recursion. Make sure to terminate the recursion by specifying a different compositionRef at some point.

For more information, see the example in recursive.

Additional functions

Name Description
randomChoice Randomly selects one of a given strings
toYaml Marshals any object into a YAML string
fromYaml Unmarshals a YAML string into an object
getResourceCondition Helper function to retreive conditions of resources
getComposedResource Helper function to retrieve observed composed resources
getCompositeResource Helper function to retreive the observed composite resource
setResourceNameAnnotation Returns the special resource-name annotation with given name
include Outputs template as a string

Developing this function

This function uses Go, Docker, and the Crossplane CLI to build functions.

# Run code generation - see input/generate.go
$ go generate ./...

# Run tests - see fn_test.go
$ go test ./...

# Build the function's runtime image - see Dockerfile
$ docker build . --tag=runtime

# Build a function package - see package/crossplane.yaml
$ crossplane xpkg build -f package --embed-runtime-image=runtime