Composition Functions

This is an alpha feature. Crossplane may change or drop this feature at any time.

This feature was introduced in v1.11.

For more information read the Crossplane feature lifecycle.

Composition Functions allow you to supplement or replace your Compositions with advanced logic not implementable through available patching strategies.

You can build a Function using general-purpose programming languages such as Go or Python, or relevant tools such as Helm, Kustomize, or CUE.

Functions complement contemporary “Patch and Transform” (P&T) style Composition. It’s possible to use only P&T, only Functions, or a mix of both in the same Composition.

 1apiVersion: apiextensions.crossplane.io/v1
 2kind: Composition
 3metadata:
 4  name: example
 5spec:
 6  compositeTypeRef:
 7    apiVersion: database.example.org/v1alpha1
 8    kind: XPostgreSQLInstance
 9  functions:
10  - name: my-cool-Function
11    type: Container
12    container:
13      image: xpkg.upbound.io/my-cool-Function:0.1.0

A Composition Function is a short-lived OCI container that tells Crossplane how to reconcile a Composite Resource (XR). The preceding example shows a minimal Composition that uses a Composition Function. Note that it has a functions array rather than the typical P&T style array of resources.

Enabling functions

Enable support for Composition Functions by enabling the alpha feature flag in Crossplane with helm install --args.

1helm install crossplane --namespace crossplane-system crossplane-stable/crossplane \
2    --create-namespace \
3    --set "args='{--debug,--enable-composition-functions}'" \
4    --set "xfn.enabled=true" \
5    --set "xfn.args='{--debug}'"

The preceding Helm command installs Crossplane with the Composition Functions feature flag enabled, and with the reference xfn Composition Function runner deployed as a sidecar container. Confirm Composition Functions were enabled by looking for a log line:

1 kubectl -n crossplane-system logs -l app=crossplane
2{"level":"info","ts":1674535093.36186,"logger":"crossplane","msg":"Alpha feature enabled","flag":"EnableAlphaCompositionFunctions"}

You should see the log line emitted shortly after Crossplane starts.

Using functions

To use Composition Functions you must:

  1. Find one or more Composition Functions, or write your own.
  2. Create a Composition that uses your Functions.
  3. Create an XR that uses your Composition.

Your XRs, claims, and providers don’t need to be updated or otherwise aware of Composition Functions to use them. They need only use a Composition that includes one or more entries in its spec.functions array.

Composition Functions are designed to be run in a pipeline, so you can ‘stack’ several of them together. Each Function is passed the output of the previous Function as its input. Functions can also be used in conjunction with P&T Composition (a spec.resources array).

In the following example P&T Composition composes an RDS instance. A pipeline of (hypothetical) Composition Functions then mutates the desired RDS instance by adding a randomly generated password, and composes an RDS security group.

 1apiVersion: apiextensions.crossplane.io/v1
 2kind: Composition
 3metadata:
 4  name: example
 5spec:
 6  compositeTypeRef:
 7    apiVersion: database.example.org/v1alpha1
 8    kind: XPostgreSQLInstance
 9  resources:
10    - name: rds-instance
11      base:
12        apiVersion: rds.aws.upbound.io/v1beta1
13        kind: Instance
14        spec:
15          forProvider:
16            dbName: exmaple
17            instanceClass: db.t3.micro
18            region: us-west-2
19            skipFinalSnapshot: true
20            username: exampleuser
21            engine: postgres
22            engineVersion: "12"
23      patches:
24        - fromFieldPath: spec.parameters.storageGB
25          toFieldPath: spec.forProvider.allocatedStorage
26      connectionDetails:
27        - type: FromFieldPath
28          name: username
29          fromFieldPath: spec.forProvider.username
30        - type: FromConnectionSecretKey
31          name: password
32          fromConnectionSecretKey: attribute.password
33  functions:
34  - name: rds-instance-password
35    type: Container
36    container:
37      image: xpkg.upbound.io/provider-aws-xfns/random-rds-password:v0.1.0
38  - name: compose-dbsecuritygroup
39    type: Container
40    container:
41      image: xpkg.upbound.io/example-org/compose-rds-securitygroup:v0.9.0

Use kubectl explain to explore the configuration options available when using Composition Functions, or take a look at the following example.

 1kubectl explain composition.spec.functions
 2KIND:     Composition
 3VERSION:  apiextensions.crossplane.io/v1
 4
 5RESOURCE: Functions <[]Object>
 6
 7DESCRIPTION:
 8     Functions is list of Composition Functions that will be used when a
 9     composite resource referring to this composition is created. At least one
10     of resources and Functions must be specified. If both are specified the
11     resources will be rendered first, then passed to the Functions for further
12     processing. THIS IS AN ALPHA FIELD. Do not use it in production. It is not
13     honored unless the relevant Crossplane feature flag is enabled, and may be
14     changed or removed without notice.
15
16     A Function represents a Composition Function.
17
18FIELDS:
19   config       <>
20     Config is an optional, arbitrary Kubernetes resource (i.e. a resource with
21     an apiVersion and kind) that will be passed to the Composition Function as
22     the 'config' block of its FunctionIO.
23
24   container    <Object>
25     Container configuration of this Function.
26
27   name <string> -required-
28     Name of this Function. Must be unique within its Composition.
29
30   type <string> -required-
31     Type of this Function.

 1apiVersion: apiextensions.crossplane.io/v1
 2kind: Composition
 3metadata:
 4  name: example
 5spec:
 6  compositeTypeRef:
 7    apiVersion: database.example.org/v1alpha1
 8    kind: XPostgreSQLInstance
 9  functions:
10  - name: my-cool-Function
11    # Currently only Container is supported. Other types may be added in future.
12    type: Container
13    # Configuration specific to type: Container.
14    container:
15      # The OCI image to pull and run.
16      image: xkpg.io/my-cool-Function:0.1.0
17      # Whether to pull the Function image Never, Always, or IfNotPresent.
18      imagePullPolicy: IfNotPresent
19      # Note that only resource limits are supported - not requests.
20      # The Function will be run with the specified resource limits, specified
21      # in Kubernetes-style resource.Quantity form.
22      resources:
23        limits:
24          # Defaults to 128Mi
25          memory: 64Mi
26          # Defaults to 100m (a 10th of a core)
27          cpu: 250m
28      # Defaults to 'Isolated' - an isolated network namespace with no network
29      # access. Use 'Runner' to allow a Function access to the runner's (the xfn
30      # container's) network namespace.
31      network:
32        policy: Runner
33      # How long the Function may run before it's killed. Defaults to 20s.
34      # Keep in mind the Function pipeline is typically invoked once every
35      # 30 to 60 seconds - sometimes more frequently during error conditions.
36      timeout: 30s
37    # An arbitrary Kubernetes resource. Passed to the Function as the config
38    # block of its FunctionIO. Doesn't need to exist as a Custom Resource (CR),
39    # since this resource doesn't exist by itself in the API server but must be
40    # a valid Kubernetes resource (have an apiVersion and kind).
41    config:
42      apiVersion: database.example.org/v1alpha1
43      kind: Config
44      metadata:
45        name: cloudsql
46      spec:
47        version: POSTGRES_9_6

Use kubectl describe <xr-kind> <xr-name> to debug Composition Functions. Look for status conditions and events. Most Functions will emit events associated with the XR if they experience issues.

Building a function

Crossplane doesn’t have opinions about how a Composition Function is implemented. Functions must:

  • Be packaged as an OCI image, where the ENTRYPOINT is the Function.
  • Accept input in the form of a FunctionIO document on stdin.
  • Return the FunctionIO they were passed, optionally mutated, on stdout.
  • Run within the constraints specified by the Composition that includes them, such as timeouts, compute, network access.

This means Functions may be written using a general-purpose programming language like Python, Go, or TypeScript. They may also be implemented using a shell script, or an existing tool like Helm or Kustomize.

FunctionIO

When a Composition Function runner like xfn runs your Function it will write FunctionIO to its stdin. A FunctionIO is a Kubernetes style YAML manifest. It’s not a custom resource (it never gets created in the API server) but it follows Kubernetes conventions.

A FunctionIO consists of:

  • An optional, arbitrary config object.
  • The observed state of the XR, any existing composed resources, and their connection details.
  • The desired state of the XR and any composed resources.
  • Optional results of the Function pipeline.

Here’s a brief example of a FunctionIO:

 1apiVersion: apiextensions.crossplane.io/v1alpha1
 2kind: FunctionIO
 3config:
 4  apiVersion: database.example.org/v1alpha1
 5  kind: Config
 6  metadata:
 7    name: cloudsql
 8  spec:
 9    version: POSTGRES_9_6
10observed:
11  composite:
12    resource:
13      apiVersion: database.example.org/v1alpha1
14      kind: XPostgreSQLInstance
15      metadata:
16        name: platform-ref-gcp-db-p9wrj
17    connectionDetails:
18    - name: privateIP
19      value: 10.135.0.3
20  resources:
21  - name: db-instance
22    resource:
23      apiVersion: sql.gcp.upbound.io/v1beta1
24      kind: DatabaseInstance
25      metadata:
26        name: platform-ref-gcp-db-p9wrj-tvvtg
27    connectionDetails:
28    - name: privateIP
29      value: 10.135.0.3
30desired:
31  composite:
32    resource:
33      apiVersion: database.example.org/v1alpha1
34      kind: XPostgreSQLInstance
35      metadata:
36        name: platform-ref-gcp-db-p9wrj
37    connectionDetails:
38    - name: privateIP
39      value: 10.135.0.3
40  resources:
41  - name: db-instance
42    resource:
43      apiVersion: sql.gcp.upbound.io/v1beta1
44      kind: DatabaseInstance
45      metadata:
46        name: platform-ref-gcp-db-p9wrj-tvvtg
47  - name: db-user
48    resource:
49      apiVersion: sql.gcp.upbound.io/v1beta1
50      kind: User
51      metadata:
52        name: platform-ref-gcp-db-p9wrj-z8lpz
53    connectionDetails:
54    - name: password
55      type: FromValue
56      value: very-secret
57    readinessChecks:
58    - type: None
59results:
60- severity: Normal
61  message: "Successfully composed GCP SQL user"

The config object is copied from the Composition. It will match what’s passed as your Function’s config in the Functions array. It must be a valid Kubernetes object - have an apiVersion and kind.

The observed state of the XR and any existing composed resources reflects the observed state at the beginning of a reconcile, before any Composition happens. Your Function will only see composite and composed resources that actually exist in the API server in the observed state. The observed state also includes any observed connection details. Initial function invocations might see empty connection details, but once external resources are created, connection details will be passed to the functions. Access to the connection details enables us to implement quite sophisticated tweaks on composed resources.

For example, if a composition is declared on two or more resources, it is possible to use one resource’s connection details to update another. This ability is not available with any of the available patch types available.

The desired state of the XR and composed resources is how your Function tells Crossplane what it should do. Crossplane ‘bootstraps’ the initial desired state passed to a Function pipeline with:

  • A copy of the observed state of the XR.
  • A copy of the observed state of any existing composed resources.
  • Any new composed resources or modifications to observed resources produced from the resources array.

When adding a new desired resource to the desired.resources array you don’t need to:

  • Update the XR’s resource references.
  • Add any composition annotations like crossplane.io/composite-resource-name.
  • Set the XR as a controller/owner reference of the desired resource.

Crossplane will take care of all of these for you. It won’t do anything else, including setting a sensible metadata.name for the new composed resource - this is up to your Function.

Finally, the results array allows your Function to surface events and debug logs on the XR. Results support the following severities:

  • Normal emits a debug log and a Normal event associated with the XR.
  • Warning emits a debug log and a Warning event associated with the XR.
  • Fatal stops the Composition process before applying any changes.

When Crossplane encounters a Fatal result it will finish running the Composition Function pipeline. Crossplane will then return an error without applying any changes to the API server. Crossplane surfaces this error as a Warning event, a debug log, and by setting the Synced status condition of the XR to “False”.

The preceding example is heavily edited for brevity. Expand the following example for a more detailed, realistic, and commented example of a FunctionIO.

In this example a XPostgreSQLInstance XR has one existing composed resource - db-instance. The composition Function returns a desired object with one new composed resource, a db-user, to tell Crossplane it should also create a database user.

  1apiVersion: apiextensions.crossplane.io/v1alpha1
  2kind: FunctionIO
  3config:
  4  apiVersion: database.example.org/v1alpha1
  5  kind: Config
  6  metadata:
  7    name: cloudsql
  8  spec:
  9    version: POSTGRES_9_6
 10observed:
 11  # The observed state of the Composite Resource.
 12  composite:
 13    resource:
 14      apiVersion: database.example.org/v1alpha1
 15      kind: XPostgreSQLInstance
 16      metadata:
 17        creationTimestamp: "2023-01-27T23:47:12Z"
 18        finalizers:
 19        - composite.apiextensions.crossplane.io
 20        generateName: platform-ref-gcp-db-
 21        generation: 5
 22        labels:
 23          crossplane.io/claim-name: platform-ref-gcp-db
 24          crossplane.io/claim-namespace: default
 25          crossplane.io/composite: platform-ref-gcp-db-p9wrj
 26        name: platform-ref-gcp-db-p9wrj
 27        resourceVersion: "6817"
 28        uid: 96623f41-be2e-4eda-84d4-9668b48e284d
 29      spec:
 30        claimRef:
 31          apiVersion: database.example.org/v1alpha1
 32          kind: PostgreSQLInstance
 33          name: platform-ref-gcp-db
 34          namespace: default
 35        compositionRef:
 36          name: xpostgresqlinstances.database.example.org
 37        compositionRevisionRef:
 38          name: xpostgresqlinstances.database.example.org-eb6c684
 39        compositionUpdatePolicy: Automatic
 40        parameters:
 41          storageGB: 10
 42        resourceRefs:
 43        - apiVersion: sql.gcp.upbound.io/v1beta1
 44          kind: DatabaseInstance
 45          name: platform-ref-gcp-db-p9wrj-tvvtg
 46        writeConnectionSecretToRef:
 47          name: 96623f41-be2e-4eda-84d4-9668b48e284d
 48          namespace: upbound-system
 49      status:
 50        conditions:
 51        - lastTransitionTime: "2023-01-27T23:47:12Z"
 52          reason: ReconcileSuccess
 53          status: "True"
 54          type: Synced
 55        - lastTransitionTime: "2023-01-28T00:09:12Z"
 56          reason: Creating
 57          status: "False"
 58          type: Ready
 59        connectionDetails:
 60          lastPublishedTime: "2023-01-28T00:08:12Z"
 61    # Any observed Composite Resource connection details.
 62    connectionDetails:
 63    - name: privateIP
 64      value: 10.135.0.3
 65  # The observed state of any existing Composed Resources.
 66  resources:
 67  - name: db-instance
 68    resource:
 69      apiVersion: sql.gcp.upbound.io/v1beta1
 70      kind: DatabaseInstance
 71      metadata:
 72        annotations:
 73          crossplane.io/composition-resource-name: db-instance
 74          crossplane.io/external-name: platform-ref-gcp-db-p9wrj-tvvtg
 75        creationTimestamp: "2023-01-27T23:47:12Z"
 76        finalizers:
 77        - finalizer.managedresource.crossplane.io
 78        generateName: platform-ref-gcp-db-p9wrj-
 79        generation: 80
 80        labels:
 81          crossplane.io/claim-name: platform-ref-gcp-db
 82          crossplane.io/claim-namespace: default
 83          crossplane.io/composite: platform-ref-gcp-db-p9wrj
 84        name: platform-ref-gcp-db-p9wrj-tvvtg
 85        ownerReferences:
 86        - apiVersion: database.example.org/v1alpha1
 87          blockOwnerDeletion: true
 88          controller: true
 89          kind: XPostgreSQLInstance
 90          name: platform-ref-gcp-db-p9wrj
 91          uid: 96623f41-be2e-4eda-84d4-9668b48e284d
 92        resourceVersion: "7992"
 93        uid: 43919834-fdce-427e-85d9-d03eab9501f1
 94      spec:
 95        forProvider:
 96          databaseVersion: POSTGRES_13
 97          deletionProtection: false
 98          project: example
 99          region: us-west2
100          settings:
101          - diskSize: 10
102            ipConfiguration:
103            - privateNetwork: projects/example/global/networks/platform-ref-gcp-cluster
104              privateNetworkRef:
105                name: platform-ref-gcp-cluster
106            tier: db-f1-micro
107        providerConfigRef:
108          name: default
109        writeConnectionSecretToRef:
110          name: 96623f41-be2e-4eda-84d4-9668b48e284d-gcp-postgresql
111          namespace: upbound-system
112      status:
113        atProvider:
114          connectionName: example:us-west2:platform-ref-gcp-db-p9wrj-tvvtg
115          firstIpAddress: 34.102.103.85
116          id: platform-ref-gcp-db-p9wrj-tvvtg
117          privateIpAddress: 10.135.0.3
118          publicIpAddress: 34.102.103.85
119          settings:
120          - version: 1
121        conditions:
122        - lastTransitionTime: "2023-01-28T00:07:30Z"
123          reason: Available
124          status: "True"
125          type: Ready
126        - lastTransitionTime: "2023-01-27T23:47:14Z"
127          reason: ReconcileSuccess
128          status: "True"
129          type: Synced
130    # Any observed composed resource connection details.
131    connectionDetails:
132    - name: privateIP
133      value: 10.135.0.3
134desired:
135  # The observed state of the Composite Resource.
136  composite:
137    resource:
138      apiVersion: database.example.org/v1alpha1
139      kind: XPostgreSQLInstance
140      metadata:
141        creationTimestamp: "2023-01-27T23:47:12Z"
142        finalizers:
143        - composite.apiextensions.crossplane.io
144        generateName: platform-ref-gcp-db-
145        generation: 5
146        labels:
147          crossplane.io/claim-name: platform-ref-gcp-db
148          crossplane.io/claim-namespace: default
149          crossplane.io/composite: platform-ref-gcp-db-p9wrj
150        name: platform-ref-gcp-db-p9wrj
151        resourceVersion: "6817"
152        uid: 96623f41-be2e-4eda-84d4-9668b48e284d
153      spec:
154        claimRef:
155         e apiVersion: database.example.org/v1alpha1
156          kind: PostgreSQLInstance
157          name: platform-ref-gcp-db
158          namespace: default
159        compositionRef:
160          name: xpostgresqlinstances.database.example.org
161        compositionRevisionRef:
162          name: xpostgresqlinstances.database.example.org-eb6c684
163        compositionUpdatePolicy: Automatic
164        parameters:
165          storageGB: 10
166        resourceRefs:
167        - apiVersion: sql.gcp.upbound.io/v1beta1
168          kind: DatabaseInstance
169          name: platform-ref-gcp-db-p9wrj-tvvtg
170        writeConnectionSecretToRef:
171          name: 96623f41-be2e-4eda-84d4-9668b48e284d
172          namespace: upbound-system
173      status:
174        conditions:
175        - lastTransitionTime: "2023-01-27T23:47:12Z"
176          reason: ReconcileSuccess
177          status: "True"
178          type: Synced
179        - lastTransitionTime: "2023-01-28T00:09:12Z"
180          reason: Creating
181          status: "False"
182          type: Ready
183        connectionDetails:
184          lastPublishedTime: "2023-01-28T00:08:12Z"
185    # Any desired Composite Resource connection details. Your Composition
186    # Function can add new entries to this array and Crossplane will record them
187    # as the XR's connection details.
188    connectionDetails:
189    - name: privateIP
190      value: 10.135.0.3
191  # The desired composed resources.
192  resources:
193  # This db-instance matches the entry in observed. Functions must include any
194  # observed resources in their desired resources array. If you omit an observed
195  # resource from the desired resources array Crossplane will delete it.
196  # Crossplane will 'bootstrap' the desired state passed to the Function
197  # pipeline by copying all observed resources into the desired resources array.
198  - name: db-instance
199    resource:
200      apiVersion: sql.gcp.upbound.io/v1beta1
201      kind: DatabaseInstance
202      metadata:
203        annotations:
204          crossplane.io/composition-resource-name: DBInstance
205          crossplane.io/external-name: platform-ref-gcp-db-p9wrj-tvvtg
206        creationTimestamp: "2023-01-27T23:47:12Z"
207        finalizers:
208        - finalizer.managedresource.crossplane.io
209        generateName: platform-ref-gcp-db-p9wrj-
210        generation: 80
211        labels:
212          crossplane.io/claim-name: platform-ref-gcp-db
213          crossplane.io/claim-namespace: default
214          crossplane.io/composite: platform-ref-gcp-db-p9wrj
215        name: platform-ref-gcp-db-p9wrj-tvvtg
216        ownerReferences:
217        - apiVersion: database.example.org/v1alpha1
218          blockOwnerDeletion: true
219          controller: true
220          kind: XPostgreSQLInstance
221          name: platform-ref-gcp-db-p9wrj
222          uid: 96623f41-be2e-4eda-84d4-9668b48e284d
223        resourceVersion: "7992"
224        uid: 43919834-fdce-427e-85d9-d03eab9501f1
225      spec:
226        forProvider:
227          databaseVersion: POSTGRES_13
228          deletionProtection: false
229          project: example
230          region: us-west2
231          settings:
232          - diskSize: 10
233            ipConfiguration:
234            - privateNetwork: projects/example/global/networks/platform-ref-gcp-cluster
235              privateNetworkRef:
236                name: platform-ref-gcp-cluster
237            tier: db-f1-micro
238        providerConfigRef:
239          name: default
240        writeConnectionSecretToRef:
241          name: 96623f41-be2e-4eda-84d4-9668b48e284d-gcp-postgresql
242          namespace: upbound-system
243      status:
244        atProvider:
245          connectionName: example:us-west2:platform-ref-gcp-db-p9wrj-tvvtg
246          firstIpAddress: 34.102.103.85
247          id: platform-ref-gcp-db-p9wrj-tvvtg
248          privateIpAddress: 10.135.0.3
249          publicIpAddress: 34.102.103.85
250          settings:
251          - version: 1
252        conditions:
253        - lastTransitionTime: "2023-01-28T00:07:30Z"
254          reason: Available
255          status: "True"
256          type: Ready
257        - lastTransitionTime: "2023-01-27T23:47:14Z"
258          reason: ReconcileSuccess
259          status: "True"
260          type: Synced
261  # This db-user is a desired composed resource that doesn't yet exist. This
262  # Composition Function is requesting it be created.
263  - name: db-user
264    resource:
265      apiVersion: sql.gcp.upbound.io/v1beta1
266      kind: User
267      metadata:
268        annotations:
269          crossplane.io/composition-resource-name: db-user
270          crossplane.io/external-name: platform-ref-gcp-db-p9wrj-z8lpz
271        creationTimestamp: "2023-01-27T23:47:12Z"
272        finalizers:
273        - finalizer.managedresource.crossplane.io
274        generateName: platform-ref-gcp-db-p9wrj-
275        generation: 115
276        labels:
277          crossplane.io/claim-name: platform-ref-gcp-db
278          crossplane.io/claim-namespace: default
279          crossplane.io/composite: platform-ref-gcp-db-p9wrj
280        name: platform-ref-gcp-db-p9wrj-z8lpz
281        ownerReferences:
282        - apiVersion: database.example.org/v1alpha1
283          blockOwnerDeletion: true
284          controller: true
285          kind: XPostgreSQLInstance
286          name: platform-ref-gcp-db-p9wrj
287          uid: 96623f41-be2e-4eda-84d4-9668b48e284d
288        resourceVersion: "9951"
289        uid: ab5dafbe-2bc8-47ea-8b5b-9bcb40183e45
290      spec:
291        forProvider:
292          instance: platform-ref-gcp-db-p9wrj-tvvtg
293          project: example
294        providerConfigRef:
295          name: default
296    # Any desired connection details for the new db-user composed resource.
297    # Desired connection details can be FromValue, FromFieldPath, or
298    # FromConnectionSecretKey, just like their P&T Composition equivalents.
299    connectionDetails:
300    - name: password
301      type: FromValue
302      value: very-secret
303    # Any desired readiness checks for the new db-user composed resource.
304    # Desired readiness checks can be NonEmpty, MatchString, MatchInteger, or
305    # None, just like their P&T Composition equivalents.    
306    readinessChecks:
307    - type: None
308# An optional array of results.
309results:
310- severity: Normal
311  message: "Successfully composed GCP SQL user"

An example Function

You can write a Composition Function using any programming language that can be containerized, or existing tools like Helm or Kustomize.

Here’s a Python Composition Function that doesn’t create any new desired resources, but instead annotates any existing desired resources with a quote. Because this function accesses the internet it needs to be run with the Runner network policy.

 1import sys
 2
 3import requests
 4import yaml
 5
 6ANNOTATION_KEY_AUTHOR = "quotable.io/author"
 7ANNOTATION_KEY_QUOTE = "quotable.io/quote"
 8
 9
10def get_quote() -> tuple[str, str]:
11    """Get a quote from quotable.io"""
12    rsp = requests.get("https://api.quotable.io/random")
13    rsp.raise_for_status()
14    j = rsp.json()
15    return (j["author"], j["content"])
16
17
18def read_Functionio() -> dict:
19    """Read the FunctionIO from stdin."""
20    return yaml.load(sys.stdin.read(), yaml.Loader)
21
22
23def write_Functionio(Functionio: dict):
24    """Write the FunctionIO to stdout and exit."""
25    sys.stdout.write(yaml.dump(Functionio))
26    sys.exit(0)
27
28
29def result_warning(Functionio: dict, message: str):
30    """Add a warning result to the supplied FunctionIO."""
31    if "results" not in Functionio:
32        Functionio["results"] = []
33    Functionio["results"].append({"severity": "Warning", "message": message})
34
35
36def main():
37    """Annotate all desired composed resources with a quote from quotable.io"""
38    try:
39        Functionio = read_Functionio()
40    except yaml.parser.ParserError as err:
41        sys.stdout.write("cannot parse FunctionIO: {}\n".format(err))
42        sys.exit(1)
43
44    # Return early if there are no desired resources to annotate.
45    if "desired" not in Functionio or "resources" not in Functionio["desired"]:
46        write_Functionio(Functionio)
47
48    # If we can't get our quote, add a warning and return early.
49    try:
50        quote, author = get_quote()
51    except requests.exceptions.RequestException as err:
52        result_warning(Functionio, "Cannot get quote: {}".format(err))
53        write_Functionio(Functionio)
54
55    # Annotate all desired resources with our quote.
56    for r in Functionio["desired"]["resources"]:
57        if "resource" not in r:
58            # This shouldn't happen - add a warning and continue.
59            result_warning(
60                Functionio,
61                "Desired resource {name} missing resource body".format(
62                    name=r.get("name", "unknown")
63                ),
64            )
65            continue
66
67        if "metadata" not in r["resource"]:
68            r["resource"]["metadata"] = {}
69
70        if "annotations" not in r["resource"]["metadata"]:
71            r["resource"]["metadata"]["annotations"] = {}
72
73        if ANNOTATION_KEY_QUOTE in r["resource"]["metadata"]["annotations"]:
74            continue
75
76        r["resource"]["metadata"]["annotations"][ANNOTATION_KEY_AUTHOR] = author
77        r["resource"]["metadata"]["annotations"][ANNOTATION_KEY_QUOTE] = quote
78
79    write_Functionio(Functionio)
80
81
82if __name__ == "__main__":
83    main()

Building this function requires its requirements.txt and a Dockerfile:

 1certifi==2022.12.7
 2charset-normalizer==3.0.1
 3click==8.1.3
 4idna==3.4
 5pathspec==0.10.3
 6platformdirs==2.6.2
 7PyYAML==6.0
 8requests==2.28.2
 9tomli==2.0.1
10urllib3==1.26.14

 1FROM debian:11-slim AS build
 2RUN apt-get update && \
 3    apt-get install --no-install-suggests --no-install-recommends --yes python3-venv && \
 4    python3 -m venv /venv && \
 5    /venv/bin/pip install --upgrade pip setuptools wheel
 6
 7FROM build AS build-venv
 8COPY requirements.txt /requirements.txt
 9RUN /venv/bin/pip install --disable-pip-version-check -r /requirements.txt
10
11FROM gcr.io/distroless/python3-debian11
12COPY --from=build-venv /venv /venv
13COPY . /app
14WORKDIR /app
15ENTRYPOINT ["/venv/bin/python3", "main.py"]

Create and push the Function just like you would any Docker image.

Build the function.

 1docker build .
 2Sending build context to Docker daemon  38.99MB
 3Step 1/10 : FROM debian:11-slim AS build
 4 ---> 4810399f6c13
 5Step 2/10 : RUN apt-get update &&     apt-get install --no-install-suggests --no-install-recommends --yes python3-venv gcc && python3 -m venv /venv &&     /venv/bin/pip install --upgrade pip setuptools wheel
 6 ---> Using cache
 7 ---> 9b34960c88d7
 8Step 3/10 : FROM build AS build-venv
 9 ---> 9b34960c88d7
10Step 4/10 : COPY requirements.txt /requirements.txt
11 ---> Using cache
12 ---> fae19dad52af
13Step 5/10 : RUN /venv/bin/pip install --disable-pip-version-check -r /requirements.txt
14 ---> Using cache
15 ---> f4b811c75812
16Step 6/10 : FROM gcr.io/distroless/python3-debian11
17 ---> 2a0e74a2b005
18Step 7/10 : COPY --from=build-venv /venv /venv
19 ---> Using cache
20 ---> cf727d3f20d3
21Step 8/10 : COPY . /app
22 ---> a044aef45e32
23Step 9/10 : WORKDIR /app
24 ---> Running in d08a6144815b
25Removing intermediate container d08a6144815b
26 ---> 7250f5aa653e
27Step 10/10 : ENTRYPOINT ["/venv/bin/python3", "main.py"]
28 ---> Running in 3f4d9dc55bad
29Removing intermediate container 3f4d9dc55bad
30 ---> bfd2f920c591
31Successfully built bfd2f920c591

Tag the function.

1docker tag bfd2f920c591 example-org/xfn-quotable-simple:v0.1.0

Push the function.

1docker push xpkg.upbound.io/example-org/xfn-quotable-simple:v0.1.0
2The push refers to repository [xpkg.upbound.io/example-org/xfn-quotable-simple]
3cf6d94b88843: Pushed
477646fd315d2: Mounted from example-org/xfn-quotable
550630ee42b6e: Mounted from example-org/xfn-quotable
67e2cf97ed8c4: Mounted from example-org/xfn-quotable
796e320b34b54: Mounted from example-org/xfn-quotable
8fba4381f2bb7: Mounted from example-org/xfn-quotable
9v0.1.0: digest: sha256:d8a6404e5fe38936aa8dadd861fea35ede0aded6168d501052f91cdabab0135e size: 1584

You can now use this Function in your Composition. The following example will create an RDSInstance using P&T Composition, then run the Function to annotate it with a quote.

 1apiVersion: apiextensions.crossplane.io/v1
 2kind: Composition
 3metadata:
 4  name: example
 5spec:
 6  compositeTypeRef:
 7    apiVersion: database.example.org/v1alpha1
 8    kind: XPostgreSQLInstance
 9  resources:
10    - name: rds-instance
11      base:
12        apiVersion: rds.aws.upbound.io/v1beta1
13        kind: Instance
14        spec:
15          forProvider:
16            dbName: example
17            instanceClass: db.t3.micro
18            region: us-west-2
19            skipFinalSnapshot: true
20            username: exampleuser
21            engine: postgres
22            engineVersion: "12"
23      patches:
24        - fromFieldPath: spec.parameters.storageGB
25          toFieldPath: spec.forProvider.allocatedStorage
26      connectionDetails:
27        - type: FromFieldPath
28          name: username
29          fromFieldPath: spec.forProvider.username
30        - type: FromConnectionSecretKey
31          name: password
32          fromConnectionSecretKey: attribute.password
33  functions:
34  - name: quotable
35    type: Container
36    container:
37      image: xpkg.upbound.io/example-org/xfn-quotable-simple:v0.1.0
38      network:
39        policy: Runner

Tips for new functions

Here are some things to keep in mind when building a Composition Function:

  • Your Function may be running as part of a pipeline. This means your Function must pass through any desired state that it’s unconcerned with. If your Function is passed a desired composed resource and doesn’t return that composed resource in its output, it will be deleted. Crossplane considers the desired state of the XR and any composed resources to be whatever FunctionIO is returned by the last Function in the pipeline.
  • Crossplane won’t set a metadata.name for your desired resources resources. It’s a good practice to match P&T Composition’s behavior by setting metadata.generateName: "name-of-the-xr-" for any new desired resources.
  • Don’t add new entries to the desired resources array every time your function is invoked. Remember to check whether your desired resource is already in the observed and/or desired objects. You may need to update it rather than create it.
  • Don’t bypass providers. Composition Functions are designed to tell Crossplane how to orchestrate managed resources - not to directly orchestrate external systems.
  • Include your function name and version in any results you return to aid in debugging.
  • Write tests for your function. Pass it a FunctionIO on stdin in and ensure it returns the expected FunctionIO on stdout.
  • Keep your Functions fast and lightweight. Remember that Crossplane runs them approximately once every 30-60 seconds.

The xfn runner

Composition Function runners are designed to be pluggable. Each time Crossplane needs to invoke a Composition Function it makes a gRPC call to a configurable endpoint. The default, reference Composition Function runner is named xfn.

Note

The default runner endpoint is unix-abstract:crossplane/fn/default.sock. It’s possible to run Functions using a different endpoint, for example:

1  functions:
2  - name: my-cool-Function
3    type: Container
4    container:
5      image: xkpg.io/my-cool-Function:0.1.0
6      runner:
7        endpoint: unix-abstract:/your/custom/runner.sock

Currently Crossplane uses unauthenticated, unencrypted gRPC requests to run Functions, so requests shouldn’t be sent over the network. Encryption and authentication will be added in a future release.

xfn runs as a sidecar container within the Crossplane pod. It runs each Composition Function as a nested rootless container.

Crossplane running Functions using xfn via gRPC

The Crossplane Helm chart deploys xfn with:

The Unconfined seccomp profile allows Crossplane to make required syscalls such as unshare and mount that are not allowed by most RuntimeDefault profiles. It’s possible to run xfn with nearly the same restrictions as most RuntimeDefault profiles by authoring a custom Localhost profile. Refer to the seccomp documentation for information on how to do so.

Granting CAP_SETUID and CAP_SETGID allows xfn to create Function containers that support up to 65,536 UIDs and GIDs. If xfn is run without these capabilities it will be restricted to creating Function containers that support only UID and GID 0.

Regardless of capabilities xfn always runs each Composition Function as an unprivileged user. That user will appear to be root inside the Composition Function container thanks to user_namespaces(7).