Merge pull request #298 from negz/barely-functional

Document the alpha Composition Functions feature
This commit is contained in:
Nic Cope 2023-01-31 16:17:28 -08:00 committed by GitHub
commit 90cbe784f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 994 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,994 @@
---
title: Composition Functions
state: alpha
---
Composition Functions allow you to supplement or replace your Compositions with
advanced logic. 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 compliment 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.
```yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: example
spec:
compositeTypeRef:
apiVersion: database.example.org/v1alpha1
kind: XPostgreSQLInstance
functions:
- name: my-cool-Function
type: Container
container:
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
To enable support for Composition Functions you must:
* Enable the alpha feature flag in Crossplane.
* Deploy a runner that's responsible for running Functions.
```shell
kubectl create namespace crossplane-system
helm install crossplane --namespace crossplane-system crossplane-stable/crossplane \
--set "args={--debug,--enable-composition-functions}" \
--set "xfn.enabled=true" \
--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 pod. Confirm Composition Functions were enabled by looking
for a log line:
```shell
$ kubectl -n crossplane-system logs -l app=crossplane
{"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.
```yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: example
spec:
compositeTypeRef:
apiVersion: database.example.org/v1alpha1
kind: XPostgreSQLInstance
resources:
- name: rds-instance
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
spec:
forProvider:
dbName: exmaple
instanceClass: db.t3.micro
region: us-west-2
skipFinalSnapshot: true
username: exampleuser
engine: postgres
engineVersion: "12"
patches:
- fromFieldPath: spec.parameters.storageGB
toFieldPath: spec.forProvider.allocatedStorage
connectionDetails:
- type: FromFieldPath
name: username
fromFieldPath: spec.forProvider.username
- type: FromConnectionSecretKey
name: password
fromConnectionSecretKey: attribute.password
functions:
- name: rds-instance-password
type: Container
container:
image: xpkg.upbound.io/provider-aws-xfns/random-rds-password:v0.1.0
- name: compose-dbsecuritygroup
type: Container
container:
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.
{{< expand "View Composition Function configuration options" >}}
```shell
$ kubectl explain composition.spec.functions
KIND: Composition
VERSION: apiextensions.crossplane.io/v1
RESOURCE: Functions <[]Object>
DESCRIPTION:
Functions is list of Composition Functions that will be used when a
composite resource referring to this composition is created. At least one
of resources and Functions must be specified. If both are specified the
resources will be rendered first, then passed to the Functions for further
processing. THIS IS AN ALPHA FIELD. Do not use it in production. It is not
honored unless the relevant Crossplane feature flag is enabled, and may be
changed or removed without notice.
A Function represents a Composition Function.
FIELDS:
config <>
Config is an optional, arbitrary Kubernetes resource (i.e. a resource with
an apiVersion and kind) that will be passed to the Composition Function as
the 'config' block of its FunctionIO.
container <Object>
Container configuration of this Function.
name <string> -required-
Name of this Function. Must be unique within its Composition.
type <string> -required-
Type of this Function.
```
{{< /expand >}}
{{< expand "An example of most Composition Function configuration options" >}}
```yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: example
spec:
compositeTypeRef:
apiVersion: database.example.org/v1alpha1
kind: XPostgreSQLInstance
functions:
- name: my-cool-Function
# Currently only Container is supported. Other types may be added in future.
type: Container
# Configuration specific to type: Container.
container:
# The OCI image to pull and run.
image: xkpg.io/my-cool-Function:0.1.0
# Whether to pull the Function image Never, Always, or IfNotPresent.
imagePullPolicy: IfNotPresent
# Note that only resource limits are supported - not requests.
# The Function will be run with the specified resource limits, specified
# in Kubernetes-style resource.Quantity form.
resources:
limits:
# Defaults to 128Mi
memory: 64Mi
# Defaults to 100m (a 10th of a core)
cpu: 250m
# Defaults to 'Isolated' - an isolated network namespace with no network
# access. Use 'Runner' to allow a Function access to the runner's (the xfn
# container's) network namespace.
network:
policy: Runner
# How long the Function may run before it's killed. Defaults to 20s.
# Keep in mind the Function pipeline is typically invoked once every
# 30 to 60 seconds - sometimes more frequently during error conditions.
timeout: 30s
# An arbitrary Kubernetes resource. Passed to the Function as the config
# block of its FunctionIO. Doesn't need to exist as a Custom Resource (CR),
# since this resource doesn't exist by itself in the API server but must be
# a valid Kubernetes resource (have an apiVersion and kind).
config:
apiVersion: database.example.org/v1alpha1
kind: Config
metadata:
name: cloudsql
spec:
version: POSTGRES_9_6
```
{{< /expand >}}
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 and any existing composed resources.
* The `desired` state of the XR and any composed resources.
* Optional `results` of the Function pipeline.
Here's a brief example of a `FunctionIO`:
```yaml
apiVersion: apiextensions.crossplane.io/v1alpha1
kind: FunctionIO
config:
apiVersion: database.example.org/v1alpha1
kind: Config
metadata:
name: cloudsql
spec:
version: POSTGRES_9_6
observed:
composite:
resource:
apiVersion: database.example.org/v1alpha1
kind: XPostgreSQLInstance
metadata:
name: platform-ref-gcp-db-p9wrj
connectionDetails:
- name: privateIP
value: 10.135.0.3
resources:
- name: db-instance
resource:
apiVersion: sql.gcp.upbound.io/v1beta1
kind: DatabaseInstance
metadata:
name: platform-ref-gcp-db-p9wrj-tvvtg
connectionDetails:
- name: privateIP
value: 10.135.0.3
desired:
composite:
resource:
apiVersion: database.example.org/v1alpha1
kind: XPostgreSQLInstance
metadata:
name: platform-ref-gcp-db-p9wrj
connectionDetails:
- name: privateIP
value: 10.135.0.3
resources:
- name: db-instance
resource:
apiVersion: sql.gcp.upbound.io/v1beta1
kind: DatabaseInstance
metadata:
name: platform-ref-gcp-db-p9wrj-tvvtg
- name: db-user
resource:
apiVersion: sql.gcp.upbound.io/v1beta1
kind: User
metadata:
name: platform-ref-gcp-db-p9wrj-z8lpz
connectionDetails:
- name: password
type: FromValue
value: very-secret
readinessChecks:
- type: None
results:
- severity: Normal
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.
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`.
{{< expand "A more detailed example" >}}
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.
```yaml
apiVersion: apiextensions.crossplane.io/v1alpha1
kind: FunctionIO
config:
apiVersion: database.example.org/v1alpha1
kind: Config
metadata:
name: cloudsql
spec:
version: POSTGRES_9_6
observed:
# The observed state of the Composite Resource.
composite:
resource:
apiVersion: database.example.org/v1alpha1
kind: XPostgreSQLInstance
metadata:
creationTimestamp: "2023-01-27T23:47:12Z"
finalizers:
- composite.apiextensions.crossplane.io
generateName: platform-ref-gcp-db-
generation: 5
labels:
crossplane.io/claim-name: platform-ref-gcp-db
crossplane.io/claim-namespace: default
crossplane.io/composite: platform-ref-gcp-db-p9wrj
name: platform-ref-gcp-db-p9wrj
resourceVersion: "6817"
uid: 96623f41-be2e-4eda-84d4-9668b48e284d
spec:
claimRef:
apiVersion: database.example.org/v1alpha1
kind: PostgreSQLInstance
name: platform-ref-gcp-db
namespace: default
compositionRef:
name: xpostgresqlinstances.database.example.org
compositionRevisionRef:
name: xpostgresqlinstances.database.example.org-eb6c684
compositionUpdatePolicy: Automatic
parameters:
storageGB: 10
resourceRefs:
- apiVersion: sql.gcp.upbound.io/v1beta1
kind: DatabaseInstance
name: platform-ref-gcp-db-p9wrj-tvvtg
writeConnectionSecretToRef:
name: 96623f41-be2e-4eda-84d4-9668b48e284d
namespace: upbound-system
status:
conditions:
- lastTransitionTime: "2023-01-27T23:47:12Z"
reason: ReconcileSuccess
status: "True"
type: Synced
- lastTransitionTime: "2023-01-28T00:09:12Z"
reason: Creating
status: "False"
type: Ready
connectionDetails:
lastPublishedTime: "2023-01-28T00:08:12Z"
# Any observed Composite Resource connection details.
connectionDetails:
- name: privateIP
value: 10.135.0.3
# The observed state of any existing Composed Resources.
resources:
- name: db-instance
resource:
apiVersion: sql.gcp.upbound.io/v1beta1
kind: DatabaseInstance
metadata:
annotations:
crossplane.io/composition-resource-name: db-instance
crossplane.io/external-name: platform-ref-gcp-db-p9wrj-tvvtg
creationTimestamp: "2023-01-27T23:47:12Z"
finalizers:
- finalizer.managedresource.crossplane.io
generateName: platform-ref-gcp-db-p9wrj-
generation: 80
labels:
crossplane.io/claim-name: platform-ref-gcp-db
crossplane.io/claim-namespace: default
crossplane.io/composite: platform-ref-gcp-db-p9wrj
name: platform-ref-gcp-db-p9wrj-tvvtg
ownerReferences:
- apiVersion: database.example.org/v1alpha1
blockOwnerDeletion: true
controller: true
kind: XPostgreSQLInstance
name: platform-ref-gcp-db-p9wrj
uid: 96623f41-be2e-4eda-84d4-9668b48e284d
resourceVersion: "7992"
uid: 43919834-fdce-427e-85d9-d03eab9501f1
spec:
forProvider:
databaseVersion: POSTGRES_13
deletionProtection: false
project: example
region: us-west2
settings:
- diskSize: 10
ipConfiguration:
- privateNetwork: projects/example/global/networks/platform-ref-gcp-cluster
privateNetworkRef:
name: platform-ref-gcp-cluster
tier: db-f1-micro
providerConfigRef:
name: default
writeConnectionSecretToRef:
name: 96623f41-be2e-4eda-84d4-9668b48e284d-gcp-postgresql
namespace: upbound-system
status:
atProvider:
connectionName: example:us-west2:platform-ref-gcp-db-p9wrj-tvvtg
firstIpAddress: 34.102.103.85
id: platform-ref-gcp-db-p9wrj-tvvtg
privateIpAddress: 10.135.0.3
publicIpAddress: 34.102.103.85
settings:
- version: 1
conditions:
- lastTransitionTime: "2023-01-28T00:07:30Z"
reason: Available
status: "True"
type: Ready
- lastTransitionTime: "2023-01-27T23:47:14Z"
reason: ReconcileSuccess
status: "True"
type: Synced
# Any observed composed resource connection details.
connectionDetails:
- name: privateIP
value: 10.135.0.3
desired:
# The observed state of the Composite Resource.
composite:
resource:
apiVersion: database.example.org/v1alpha1
kind: XPostgreSQLInstance
metadata:
creationTimestamp: "2023-01-27T23:47:12Z"
finalizers:
- composite.apiextensions.crossplane.io
generateName: platform-ref-gcp-db-
generation: 5
labels:
crossplane.io/claim-name: platform-ref-gcp-db
crossplane.io/claim-namespace: default
crossplane.io/composite: platform-ref-gcp-db-p9wrj
name: platform-ref-gcp-db-p9wrj
resourceVersion: "6817"
uid: 96623f41-be2e-4eda-84d4-9668b48e284d
spec:
claimRef:
e apiVersion: database.example.org/v1alpha1
kind: PostgreSQLInstance
name: platform-ref-gcp-db
namespace: default
compositionRef:
name: xpostgresqlinstances.database.example.org
compositionRevisionRef:
name: xpostgresqlinstances.database.example.org-eb6c684
compositionUpdatePolicy: Automatic
parameters:
storageGB: 10
resourceRefs:
- apiVersion: sql.gcp.upbound.io/v1beta1
kind: DatabaseInstance
name: platform-ref-gcp-db-p9wrj-tvvtg
writeConnectionSecretToRef:
name: 96623f41-be2e-4eda-84d4-9668b48e284d
namespace: upbound-system
status:
conditions:
- lastTransitionTime: "2023-01-27T23:47:12Z"
reason: ReconcileSuccess
status: "True"
type: Synced
- lastTransitionTime: "2023-01-28T00:09:12Z"
reason: Creating
status: "False"
type: Ready
connectionDetails:
lastPublishedTime: "2023-01-28T00:08:12Z"
# Any desired Composite Resource connection details. Your Composition
# Function can add new entries to this array and Crossplane will record them
# as the XR's connection details.
connectionDetails:
- name: privateIP
value: 10.135.0.3
# The desired composed resources.
resources:
# This db-instance matches the entry in observed. Functions must include any
# observed resources in their desired resources array. If you omit an observed
# resource from the desired resources array Crossplane will delete it.
# Crossplane will 'bootstrap' the desired state passed to the Function
# pipeline by copying all observed resources into the desired resources array.
- name: db-instance
resource:
apiVersion: sql.gcp.upbound.io/v1beta1
kind: DatabaseInstance
metadata:
annotations:
crossplane.io/composition-resource-name: DBInstance
crossplane.io/external-name: platform-ref-gcp-db-p9wrj-tvvtg
creationTimestamp: "2023-01-27T23:47:12Z"
finalizers:
- finalizer.managedresource.crossplane.io
generateName: platform-ref-gcp-db-p9wrj-
generation: 80
labels:
crossplane.io/claim-name: platform-ref-gcp-db
crossplane.io/claim-namespace: default
crossplane.io/composite: platform-ref-gcp-db-p9wrj
name: platform-ref-gcp-db-p9wrj-tvvtg
ownerReferences:
- apiVersion: database.example.org/v1alpha1
blockOwnerDeletion: true
controller: true
kind: XPostgreSQLInstance
name: platform-ref-gcp-db-p9wrj
uid: 96623f41-be2e-4eda-84d4-9668b48e284d
resourceVersion: "7992"
uid: 43919834-fdce-427e-85d9-d03eab9501f1
spec:
forProvider:
databaseVersion: POSTGRES_13
deletionProtection: false
project: example
region: us-west2
settings:
- diskSize: 10
ipConfiguration:
- privateNetwork: projects/example/global/networks/platform-ref-gcp-cluster
privateNetworkRef:
name: platform-ref-gcp-cluster
tier: db-f1-micro
providerConfigRef:
name: default
writeConnectionSecretToRef:
name: 96623f41-be2e-4eda-84d4-9668b48e284d-gcp-postgresql
namespace: upbound-system
status:
atProvider:
connectionName: example:us-west2:platform-ref-gcp-db-p9wrj-tvvtg
firstIpAddress: 34.102.103.85
id: platform-ref-gcp-db-p9wrj-tvvtg
privateIpAddress: 10.135.0.3
publicIpAddress: 34.102.103.85
settings:
- version: 1
conditions:
- lastTransitionTime: "2023-01-28T00:07:30Z"
reason: Available
status: "True"
type: Ready
- lastTransitionTime: "2023-01-27T23:47:14Z"
reason: ReconcileSuccess
status: "True"
type: Synced
# This db-user is a desired composed resource that doesn't yet exist. This
# Composition Function is requesting it be created.
- name: db-user
resource:
apiVersion: sql.gcp.upbound.io/v1beta1
kind: User
metadata:
annotations:
crossplane.io/composition-resource-name: db-user
crossplane.io/external-name: platform-ref-gcp-db-p9wrj-z8lpz
creationTimestamp: "2023-01-27T23:47:12Z"
finalizers:
- finalizer.managedresource.crossplane.io
generateName: platform-ref-gcp-db-p9wrj-
generation: 115
labels:
crossplane.io/claim-name: platform-ref-gcp-db
crossplane.io/claim-namespace: default
crossplane.io/composite: platform-ref-gcp-db-p9wrj
name: platform-ref-gcp-db-p9wrj-z8lpz
ownerReferences:
- apiVersion: database.example.org/v1alpha1
blockOwnerDeletion: true
controller: true
kind: XPostgreSQLInstance
name: platform-ref-gcp-db-p9wrj
uid: 96623f41-be2e-4eda-84d4-9668b48e284d
resourceVersion: "9951"
uid: ab5dafbe-2bc8-47ea-8b5b-9bcb40183e45
spec:
forProvider:
instance: platform-ref-gcp-db-p9wrj-tvvtg
project: example
providerConfigRef:
name: default
# Any desired connection details for the new db-user composed resource.
# Desired connection details can be FromValue, FromFieldPath, or
# FromConnectionSecretKey, just like their P&T Composition equivalents.
connectionDetails:
- name: password
type: FromValue
value: very-secret
# Any desired readiness checks for the new db-user composed resource.
# Desired readiness checks can be NonEmpty, MatchString, MatchInteger, or
# None, just like their P&T Composition equivalents.
readinessChecks:
- type: None
# An optional array of results.
results:
- severity: Normal
message: "Successfully composed GCP SQL user"
```
{{< /expand >}}
### 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.
```python
import sys
import requests
import yaml
ANNOTATION_KEY_AUTHOR = "quotable.io/author"
ANNOTATION_KEY_QUOTE = "quotable.io/quote"
def get_quote() -> tuple[str, str]:
"""Get a quote from quotable.io"""
rsp = requests.get("https://api.quotable.io/random")
rsp.raise_for_status()
j = rsp.json()
return (j["author"], j["content"])
def read_Functionio() -> dict:
"""Read the FunctionIO from stdin."""
return yaml.load(sys.stdin.read(), yaml.Loader)
def write_Functionio(Functionio: dict):
"""Write the FunctionIO to stdout and exit."""
sys.stdout.write(yaml.dump(Functionio))
sys.exit(0)
def result_warning(Functionio: dict, message: str):
"""Add a warning result to the supplied FunctionIO."""
if "results" not in Functionio:
Functionio["results"] = []
Functionio["results"].append({"severity": "Warning", "message": message})
def main():
"""Annotate all desired composed resources with a quote from quotable.io"""
try:
Functionio = read_Functionio()
except yaml.parser.ParserError as err:
sys.stdout.write("cannot parse FunctionIO: {}\n".format(err))
sys.exit(1)
# Return early if there are no desired resources to annotate.
if "desired" not in Functionio or "resources" not in Functionio["desired"]:
write_Functionio(Functionio)
# If we can't get our quote, add a warning and return early.
try:
quote, author = get_quote()
except requests.exceptions.RequestException as err:
result_warning(Functionio, "Cannot get quote: {}".format(err))
write_Functionio(Functionio)
# Annotate all desired resources with our quote.
for r in Functionio["desired"]["resources"]:
if "resource" not in r:
# This shouldn't happen - add a warning and continue.
result_warning(
Functionio,
"Desired resource {name} missing resource body".format(
name=r.get("name", "unknown")
),
)
continue
if "metadata" not in r["resource"]:
r["resource"]["metadata"] = {}
if "annotations" not in r["resource"]["metadata"]:
r["resource"]["metadata"]["annotations"] = {}
if ANNOTATION_KEY_QUOTE in r["resource"]["metadata"]["annotations"]:
continue
r["resource"]["metadata"]["annotations"][ANNOTATION_KEY_AUTHOR] = author
r["resource"]["metadata"]["annotations"][ANNOTATION_KEY_QUOTE] = quote
write_Functionio(Functionio)
if __name__ == "__main__":
main()
```
Building this function requires its `requirements.txt` and a `Dockerfile`:
{{< expand "The Function's requirements" >}}
```python
certifi==2022.12.7
charset-normalizer==3.0.1
click==8.1.3
idna==3.4
pathspec==0.10.3
platformdirs==2.6.2
PyYAML==6.0
requests==2.28.2
tomli==2.0.1
urllib3==1.26.14
```
{{< /expand >}}
{{< expand "The Function's Dockerfile" >}}
```Dockerfile
FROM debian:11-slim AS build
RUN apt-get update && \
apt-get install --no-install-suggests --no-install-recommends --yes python3-venv && \
python3 -m venv /venv && \
/venv/bin/pip install --upgrade pip setuptools wheel
FROM build AS build-venv
COPY requirements.txt /requirements.txt
RUN /venv/bin/pip install --disable-pip-version-check -r /requirements.txt
FROM gcr.io/distroless/python3-debian11
COPY --from=build-venv /venv /venv
COPY . /app
WORKDIR /app
ENTRYPOINT ["/venv/bin/python3", "main.py"]
```
{{< /expand >}}
Create and push the Function just like you would any Docker image:
```shell
# Build the Function.
$ docker build .
Sending build context to Docker daemon 38.99MB
Step 1/10 : FROM debian:11-slim AS build
---> 4810399f6c13
Step 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
---> Using cache
---> 9b34960c88d7
Step 3/10 : FROM build AS build-venv
---> 9b34960c88d7
Step 4/10 : COPY requirements.txt /requirements.txt
---> Using cache
---> fae19dad52af
Step 5/10 : RUN /venv/bin/pip install --disable-pip-version-check -r /requirements.txt
---> Using cache
---> f4b811c75812
Step 6/10 : FROM gcr.io/distroless/python3-debian11
---> 2a0e74a2b005
Step 7/10 : COPY --from=build-venv /venv /venv
---> Using cache
---> cf727d3f20d3
Step 8/10 : COPY . /app
---> a044aef45e32
Step 9/10 : WORKDIR /app
---> Running in d08a6144815b
Removing intermediate container d08a6144815b
---> 7250f5aa653e
Step 10/10 : ENTRYPOINT ["/venv/bin/python3", "main.py"]
---> Running in 3f4d9dc55bad
Removing intermediate container 3f4d9dc55bad
---> bfd2f920c591
Successfully built bfd2f920c591
# Tag the Function.
$ docker tag bfd2f920c591 example-org/xfn-quotable-simple:v0.1.0
# Push the Function.
$ docker push xpkg.upbound.io/example-org/xfn-quotable-simple:v0.1.0
The push refers to repository [xpkg.upbound.io/example-org/xfn-quotable-simple]
cf6d94b88843: Pushed
77646fd315d2: Mounted from example-org/xfn-quotable
50630ee42b6e: Mounted from example-org/xfn-quotable
7e2cf97ed8c4: Mounted from example-org/xfn-quotable
96e320b34b54: Mounted from example-org/xfn-quotable
fba4381f2bb7: Mounted from example-org/xfn-quotable
v0.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.
```yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: example
spec:
compositeTypeRef:
apiVersion: database.example.org/v1alpha1
kind: XPostgreSQLInstance
resources:
- name: rds-instance
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
spec:
forProvider:
dbName: example
instanceClass: db.t3.micro
region: us-west-2
skipFinalSnapshot: true
username: exampleuser
engine: postgres
engineVersion: "12"
patches:
- fromFieldPath: spec.parameters.storageGB
toFieldPath: spec.forProvider.allocatedStorage
connectionDetails:
- type: FromFieldPath
name: username
fromFieldPath: spec.forProvider.username
- type: FromConnectionSecretKey
name: password
fromConnectionSecretKey: attribute.password
functions:
- name: quotable
type: Container
container:
image: xpkg.upbound.io/example-org/xfn-quotable-simple:v0.1.0
network:
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`.
{{< hint "note" >}}
The default runner endpoint is `unix-abstract:crossplane/fn/default.sock`. It's
possible to run Functions using a different endpoint, for example:
```yaml
functions:
- name: my-cool-Function
type: Container
container:
image: xkpg.io/my-cool-Function:0.1.0
runner:
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.
{{< /hint >}}
`xfn` runs as a sidecar container within the Crossplane pod. It runs each
Composition Function as a nested [rootless container][rootless-containers].
{{< img src="master/guides/composition-functions-xfn-runner.png" alt="Crossplane running Functions using xfn via gRPC" size="tiny" >}}
The Crossplane Helm chart deploys `xfn` with:
* The [`Unconfined` seccomp profile][kubernetes-seccomp].
* The `CAP_SETUID` and `CAP_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][kubernetes-seccomp] 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)`].
[rootless-containers]: https://rootlesscontaine.rs
[kubernetes-seccomp]: https://kubernetes.io/docs/tutorials/security/seccomp/
[`user_namespaces(7)`]: https://man7.org/linux/man-pages/man7/user_namespaces.7.html