Skip to content

Commit

Permalink
Add developer-focused documentation for functions
Browse files Browse the repository at this point in the history
This consists of two sections in the existing page:

* How to write a function
* How functions work

The former is somewhat light, because I intend to add detailed guides
for each language we support. It's hard to go too deep in this general
documentation without using language-specific examples.

Signed-off-by: Nic Cope <nicc@rk0n.org>
  • Loading branch information
negz committed Oct 28, 2023
1 parent cb1f9d2 commit 8c7231e
Showing 1 changed file with 309 additions and 0 deletions.
309 changes: 309 additions & 0 deletions content/master/concepts/composition-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.


<!-- vale write-good.Passive = NO -->
Here's an example of a tiny, hello world function. This example is written in
Go.
<!-- vale write-good.Passive = YES -->

```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 {{<hover label="dedicated"
line="13">}}function-xr-xbucket{{</hover>}} whenever you create, update, or
delete an {{<hover label="dedicated" line="8">}}XBucket{{</hover>}} 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
```

{{<hint "tip">}}
Crossplane has
[language specific guides]({{<ref "../../knowledge-base/guides">}}) to writing
a composition function. Refer to the guide for your preferred language for a
more detailed guide to writing a function.
{{</hint>}}

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.

{{<hint "tip">}}
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).
{{</hint>}}

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.

{{<hint "tip">}}
<!-- vale write-good.Weasel = NO -->
<!-- Disable Weasel to say "usually", which is correct in this context. -->
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.
<!-- vale write-good.Weasel = YES -->
{{</hint>}}

### 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.

{{<hint "important">}}
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.
{{</hint>}}

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.

{{<hint "tip">}}
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_.
{{</hint>}}

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.

{{<hint "tip">}}
Composition functions don't actually use YAML for desired and observed
resources. This example uses YAML for illustration purposes only.
{{</hint>}}

### Function input

If a Composition includes {{<hover label="input" line="14">}}input{{</hover>}}
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
```

{{<hint "important">}}
Crossplane doesn't validate function input. It's a good idea for a function to
validate its own input.
{{</hint>}}

### 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]({{<ref "environment-configs">}}) 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
Expand Down

0 comments on commit 8c7231e

Please sign in to comment.