diff --git a/content/master/concepts/composition-functions.md b/content/master/concepts/composition-functions.md index 6cb2215a..c9fa4a3f 100644 --- a/content/master/concepts/composition-functions.md +++ b/content/master/concepts/composition-functions.md @@ -303,6 +303,315 @@ See the [Crossplane CLI docs](https://github.com/crossplane/docs/pull/584) to learn how to install and use the Crossplane CLI. {{< /hint >}} +## Write a composition function + +Composition functions let you replace complicated Compositions with code written +in your programming language of choice. Crossplane has tools, software +development kits (SDKs) and templates to help you write a function. + + + +Here's an example of a tiny, hello world function. This example is written in +Go. + + +```go +func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequest) (*fnv1beta1.RunFunctionResponse, error) { + rsp := response.To(req, response.DefaultTTL) + response.Normal(rsp, "Hello world!") + return rsp, nil +} +``` + +Some people design composition functions for you to use them with any kind of +composite resource. Function Patch and Transform and Function Auto Ready work +with any kind of composite resource. + +Another common pattern is to write a composition function specific to one kind +of composite resource. The function contains all the logic needed to tell +Crossplane what resources to create when you create a composite resource. When +you write a composition function like this, your Composition can be small. It +just tells Crossplane what function to run when you create, update, or delete a +composite resource. + +This Composition tells Crossplane to call {{}}function-xr-xbucket{{}} whenever you create, update, or +delete an {{}}XBucket{{}} composite +resource. `function-xr-xbucket` is hard coded to handle `XBucket` composite +resources. + +```yaml {label="dedicated"} +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: example-bucket-function +spec: + compositeTypeRef: + apiVersion: example.crossplane.io/v1 + kind: XBucket + mode: Pipeline + pipeline: + - step: handle-xbucket-xr + functionRef: + name: function-xr-xbucket +``` + +To write a composition function, you: + +1. Create the function from a template. +1. Edit the template to add your function's logic. +1. Test your function. +1. Build your function, and push it to a package registry. + +You use the [Crossplane CLI](https://github.com/crossplane/docs/pull/584) to +create, test, build, and push a function. For example, + +```shell {copy-lines=none} +# Create the function from a template. +crossplane beta xpkg init function-example function-template-go +Initialized package "function-example" in directory "/home/negz/control/negz/function-example" from https://github.com/crossplane/function-template-go/tree/91a1a5eed21964ff98966d72cc6db6f089ad63f4 (main) + +$ ls +Dockerfile fn.go fn_test.go go.mod go.sum input LICENSE main.go package README.md renovate.json + +# Edit the template to add your function's logic +$ vim fn.go + +# Build the function. +$ docker build . --quiet --tag runtime +sha256:2c31b0f7a34b34ba5b0b2dacc94c360d18aca1b99f56ca4f40a1f26535a7c1c4 + +# Package the function. +$ crossplane xpkg build -f package --embed-runtime-image=runtime + +# Push the function package to xpkg.upbound.io. +$ crossplane xpkg push -f package/*.xpkg crossplane-contrib/function-example:v0.1.0 +``` + +{{}} +Crossplane has +[language specific guides]({{}}) to writing +a composition function. Refer to the guide for your preferred language for a +more detailed guide to writing a function. +{{}} + +When you're writing a composition function it's useful to know how composition +functions work. Read the next section to learn +[how composition functions work](#how-composition-functions-work). + +## How composition functions work + +Each composition function is actually a [gRPC](https://grpc.io) server. gRPC is +a high performance, open source remote procedure call (RPC) framework. When you +[install a function](#install-a-composition-function) Crossplane deploys the +function as a gRPC server. Crossplane encrypts and authenticates all gRPC +communication. + +You don't have to be a gRPC expert to write a function. Crossplane's function +SDKs setup gRPC for you. It's useful to understand how Crossplane calls your +function though, and how your function should respond. + +```mermaid +sequenceDiagram + User->>+API Server: Create composite resource + Crossplane Pod->>+API Server: Observe composite resource + Crossplane Pod->>+Function Pod: gRPC RunFunctionRequest + Function Pod->>+Crossplane Pod: gRPC RunFunctionResponse + Crossplane Pod->>+API Server: Apply desired composed resources +``` + +When you create, update, or delete a composite resource that uses composition +functions Crossplane calls each function in the order they appear in the +Composition's pipeline. Crossplane calls each function by sending it a gRPC +RunFunctionRequest. The function must respond with a gRPC RunFunctionResponse. + +{{}} +You can find detailed schemas for the RunFunctionRequest and RunFunctionResponse +RPCs in the [Buf Schema Registry](https://buf.build/crossplane/crossplane/docs/main:apiextensions.fn.proto.v1beta1). +{{}} + +When Crossplane calls a function it includes four important things in the +RunFunctionRequest. + +1. The __observed state__ of the composite resource, and any composed resources. +1. The __desired state__ of the composite resource, and any composed resources. +1. The function's __input__. +1. The function pipeline's __context__. + +A function's main job is to update the __desired state__ and return it to +Crossplane. It does this by returning a RunFunctionResponse. + +Most composition functions read the observed state of the composite resource, +and use it to add composed resources to the desired state. This tells Crossplane +which composed resources it should create or update. + +{{}} + + +A _composed_ resource is a resource created by a composite resource. Composed +resources are usually Crossplane managed resources (MRs), but they can be any +kind of Crossplane resource. For example a composite resource could also create +a ProviderConfig, or another kind of composite resource. + +{{}} + +### Observed state + +When you create a composite resource like this one, Crossplane _observes_ it and +sends it to the composition function as part of the observed state. + +```yaml +apiVersion: example.crossplane.io/v1 +kind: XBucket +metadata: + name: example-render +spec: + bucketRegion: us-east-2 +``` + +If any composed resources already exist, Crossplane observes them and sends them +to your function to as part of the observed state. + +Crossplane also observes the connection details of your composite resource and +any composed resources. It sends them to your function as part of the observed +state. + +Crossplane observes the composite resource and any composed resources once, +right before it starts calling the functions in the pipeline. This means that +Crossplane sends every function in the pipeline the same observed state. + +### Desired state + +Desired state is the set of the changes the function pipeline wants to make to +the composite resource and any composed resources. When a function adds composed +resources to the desired state Crossplane creates them. + +A function can change: + +* The `status` of the composite resource. +* The `metadata` and `spec` of any composed resource. + +A function can also change the connection details and readiness of the composite +resource. A function indicates that the composite resource is ready by telling +Crossplane whether its composed resources are ready. When the function pipeline +tells Crossplane that all composed resources are ready, Crossplane marks the +composite resource as ready. + +A function can't change: + +* The `metadata` or `spec` of the composite resource. +* The `status` of any composed resource. +* The connection details of any composed resource. + +A pipeline of functions _accumulates_ desired state. This means that each +function builds upon the desired state of previous functions in the pipeline. +Crossplane sends a function the desired state accumulated by all previous +functions in the pipeline. The function adds to or updates the desired state and +then passes it on. When the last function in the pipeline has run, Crossplane +applies the desired state it returns. + +{{}} +A function __must__ copy all desired state from its RunFunctionRequest to its +RunFunctionResponse. If a function adds a resource to its desired state the next +function must copy it to its desired state. If it doesn't, Crossplane doesn't +apply the resource. If the resource exists, Crossplane deletes it. + +A function can _intentionally_ choose not to copy parts of the desired state. +For example a function may choose not to copy a desired resource to prevent that +resource from existing. + +Most function SDKs handle copying desired state automatically. +{{}} + +A function should only add the fields it cares about to the desired state. It +should add these fields every time Crossplane calls it. If a function adds a +field to the desired state once, but doesn't add it the next time it's called, +Crossplane deletes the field. The same is true for composed resources. If a +function adds a composed resource to the desired state, but doesn't add it the +next time it's called, Crossplane deletes the composed resource. + +{{}} +Crossplane uses +[server side apply](https://kubernetes.io/docs/reference/using-api/apply/) +to apply the desired state returned by a function pipeline. In server side apply +terminology, the desired state is a _fully specified intent_. +{{}} + +For example, if all a function wants is to make sure an S3 bucket in region +`us-east-2` exists, it should add this resource to its desired composed +resources. + +```yaml +apiVersion: s3.aws.upbound.io/v1beta1 +kind: Bucket +spec: + forProvider: + region: us-east-2 +``` + +Even if the Bucket already exists and has other `spec` fields, or a `status`, +`name`, `labels`, etc the function should omit them. The function should only +include the fields it has an opinion about. Crossplane takes care of applying +the fields the function cares about, merging them with the existing Bucket. + +{{}} +Composition functions don't actually use YAML for desired and observed +resources. This example uses YAML for illustration purposes only. +{{}} + +### Function input + +If a Composition includes {{}}input{{}} +Crossplane sends it to the function. Input is a useful way to provide extra +configuration to a function. Supporting input is optional. Not all functions +support input. + +```yaml {label="input",copy-lines="none"} +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: example-render +spec: + compositeTypeRef: + apiVersion: example.crossplane.io/v1 + kind: XBucket + mode: Pipeline + pipeline: + - step: patch-and-transform + functionRef: + name: function-patch-and-transform + input: + apiVersion: pt.fn.crossplane.io/v1beta1 + kind: Resources + resources: + - name: storage-bucket + base: + apiVersion: s3.aws.upbound.io/v1beta1 + kind: Bucket + patches: + - type: FromCompositeFieldPath + fromFieldPath: spec.bucketRegion + toFieldPath: spec.forProvider.region +``` + +{{}} +Crossplane doesn't validate function input. It's a good idea for a function to +validate its own input. +{{}} + +### Function pipeline context + +Sometimes two functions in a pipeline want to share information with each other +that isn't desired state. Functions can use context for this. Any function can +write to the pipeline context. Crossplane passes the context to all following +functions. When Crossplane has called all functions it discards the pipeline +context. + +Crossplane can write context too. If you enable the alpha +[composition environment]({{}}) feature Crossplane +writes the environment to the top-level context field +`apiextensions.crossplane.io/environment`. + ## Disable composition functions Crossplane enables composition functions by default. Disable support for