Composition Functions
This feature was introduced in v1.11.
Crossplane may change or drop this feature at any time.
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:
- Find one or more Composition Functions, or write your own.
- Create a
Composition
that uses your Functions. - 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 aNormal
event associated with the XR.Warning
emits a debug log and aWarning
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
:
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 settingmetadata.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/ordesired
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 expectedFunctionIO
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
.
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.

The Crossplane Helm chart deploys xfn
with:
- The
Unconfined
seccomp profile. - The
CAP_SETUID
andCAP_SETGID
capabilities.
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)
.