01: Extending Crossplane with Composition Functions¶
Patch-and-transform is a powerful way to create Compositions, but it has its limits. What if you need to add complex logic, conditional resource creation, or loop over a list to generate resources? For this, we use Composition Functions.
What are Composition Functions?¶
A Composition Function is a custom program that you write, package as a container image, and reference in your Composition. When Crossplane reconciles your CompositeResource, it sends the Observed and Desired state to your function. Your function then returns a new Desired state, which Crossplane applies.
This allows you to inject any logic you can imagine into the composition process.
Why Use Them?¶
- Conditional Logic: Only create a resource if a certain field in the claim is set (e.g.,
if claim.spec.ha == true, create a read replica). - Looping: Create a variable number of resources based on a list in the claim (e.g.,
for each port in claim.spec.firewallPorts, create a FirewallRule). - External Lookups: Call an external API to fetch data and use it to enrich your resources.
- Complex Validation: Implement validation logic that is too complex for OpenAPI schema validation.
How They Work¶
Your Composition is modified to run in Pipeline mode, which consists of a series of steps. One of these steps can be your function.
Example Composition with a Function:
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: my-function-composition
spec:
compositeTypeRef: # ...
mode: Pipeline
pipeline:
- step: patch-and-transform
functionRef:
name: function-patch-and-transform
- step: my-custom-logic
functionRef:
name: my-awesome-function
input:
apiVersion: my-fn.example.org/v1alpha1
kind: MyFunctionInput
spec:
someValue: "hello world"
In this example:
- The
modeis set toPipeline. - The first step is the standard
patch-and-transformfunction. - The second step calls our custom function,
my-awesome-function. - We can pass custom
inputto our function.
Writing a Composition Function¶
Composition Functions can be written in any language that can be packaged in a container. Go is a popular choice due to its strong typing and Kubernetes ecosystem tooling.
Crossplane provides libraries (crossplane-runtime/pkg/function and crossplane-runtime/pkg/function/proto) to simplify the process.
The FunctionRunner¶
Your function's main.go will typically use a FunctionRunner.
package main
import (
"context"
"fmt"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/function"
fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1"
)
func main() {
function.Run(function.HandlerFunc(func(ctx context.Context, req *fnv1beta1.RunFunctionRequest) (*fnv1beta1.RunFunctionResponse, error) {
// 1. Get the observed and desired state from the request.
observed, err := function.ParseObservedResources(req)
if err != nil {
return nil, errors.Wrap(err, "cannot parse observed resources")
}
desired, err := function.ParseDesiredResources(req)
if err != nil {
return nil, errors.Wrap(err, "cannot parse desired resources")
}
// 2. Add your custom logic here.
// For example, loop over a field in the claim and add new resources to the `desired` map.
// 3. Return the modified desired state.
if err := function.ComposeDesiredResources(req, desired); err != nil {
return nil, errors.Wrap(err, "cannot compose desired resources")
}
return req, nil
}))
}
Exercise: A Looping Function¶
Objective: Create a Composition Function that creates a set of Azure Firewall rules based on a list in the claim.
Tasks:
- Define the XRD: Create a
CompositeFirewallwith a claim,Firewall, that has a fieldallowedPortswhich is an array of strings. - Write the Function:
- Create a new Go project for your function.
- The function should get the
allowedPortsarray from the claim. - It should loop through the array.
- For each port, it should create a new Azure
FirewallRuleresource and add it to the desired state.
- Package and Push: Build the function's Docker image and push it to a registry.
- Create the
Functionresource: Define aFunctionresource in yourplatformrepository that points to your new image. - Create the
Composition: Create aCompositionthat uses your new function in its pipeline. - Claim a
Firewall: Create aFirewallclaim with a list of ports. - Verify: Check in the Azure portal that a firewall rule was created for each port in your claim.
Composition Functions are an advanced topic, but they unlock the full potential of Crossplane, allowing you to build a truly powerful and customized Internal Developer Platform.