Crossplane logo
Crossplane logo
  • Why Control Planes?
  • Documentation
  • Community
  • Blog
  • Crossplane GitHub
  • Crossplane Slack
Crossplane Documentation - v
Welcome
What's Crossplane?
What's New in v2?
Get Started
Install Crossplane
Get Started With Composition
Get Started With Managed Resources
Get Started With Operations
Composition
Composite Resources
Composite Resource Definitions
Compositions
Composition Revisions
Environment Configs
Managed Resources
Managed Resources
Managed Resource Definitions
Managed Resource Activation Policies
Usages
Operations
Operations
Cron Operations
Watch Operations
Packages
Providers
Functions
Configurations
Image Configs
Guides
Crossplane Pods
Metrics
Function Patch and Transform
Releasing Crossplane Extensions
Write a Composition Function in Go
Write a Composition Function in Python
Disabling Unused Managed Resources
Implementing safe-start in Providers
Troubleshoot Crossplane
Upgrade Crossplane
Upgrade to Crossplane v2
Uninstall Crossplane
CLI Reference
Command Reference
API Reference
Learn More
Release Cycle
Feature Lifecycle
Contributing Guide
Crossplane Roadmap
v2.0
Latest
master v2.0-preview v2.0
Latest
v1.20 v1.19

Write a Composition Function in Go

On this page
  • Understand the steps
  • Install the tools you need to write the function
  • Initialize the function from a template
  • Edit the template to add the function’s logic
  • Test the function end-to-end
  • Build and push the function to a package registry
Report a problem
View page source

Composition functions (or just functions, for short) are custom programs that template Crossplane resources. Crossplane calls composition functions to determine what resources it should create when you create a composite resource (XR). Read the concepts page to learn more about composition functions.

You can write a function to template resources using a general purpose programming language. Using a general purpose programming language allows a function to use advanced logic to template resources, like loops and conditionals. This guide explains how to write a composition function in Go.

Important
It helps to be familiar with how composition functions work before following this guide.

Understand the steps

This guide covers writing a composition function for an XBuckets composite resource (XR).

 1apiVersion: example.crossplane.io/v1  2kind: XBuckets  3metadata:  4  name: example-buckets  5spec:  6  region: us-east-2  7  names:  8  - crossplane-functions-example-a  9  - crossplane-functions-example-b 10  - crossplane-functions-example-c 

An XBuckets XR has a region and an array of bucket names. The function will create an Amazon Web Services (AWS) S3 bucket for each entry in the names array.

To write a function in Go:

  1. Install the tools you need to write the function
  2. Initialize the function from a template
  3. Edit the template to add the function’s logic
  4. Test the function end-to-end
  5. Build and push the function to a package repository

This guide covers each of these steps in detail.

Install the tools you need to write the function

To write a function in Go you need:

  • Go v1.23 or newer. The guide uses Go v1.23.
  • Docker Engine. This guide uses Engine v24.
  • The Crossplane CLI v1.17 or newer. This guide uses Crossplane CLI v1.17.
Note
You don’t need access to a Kubernetes cluster or a Crossplane control plane to build or test a composition function.

Initialize the function from a template

Use the crossplane xpkg init command to initialize a new function. When you run this command it initializes your function using a GitHub repository as a template.

 1crossplane xpkg init function-xbuckets function-template-go -d function-xbuckets   2Initialized package "function-xbuckets" in directory "/home/negz/control/negz/function-xbuckets" from https://github.com/crossplane/function-template-go/tree/91a1a5eed21964ff98966d72cc6db6f089ad63f4 (main)  3  4To get started:  5  61. Replace `function-template-go` with your function in `go.mod`,  7   `package/crossplane.yaml`, and any Go imports. (You can also do this  8   automatically by running the `./init.sh <function-name>` script.)  92. Update `input/v1beta1/` to reflect your desired input (and run `go generate`) 103. Add your logic to `RunFunction` in `fn.go` 114. Add tests for your logic in `fn_test.go` 125. Update `README.md`, to be about your function! 13 14Found init.sh script! 15Do you want to run it? [y]es/[n]o/[v]iew: y 16Function function-xbuckets has been initialised successfully 

The crossplane xpkg init command creates a directory named function-xbuckets. When you run the command the new directory should look like this:

1ls function-xbuckets 2Dockerfile    LICENSE       NOTES.txt     README.md     example       fn.go         fn_test.go    go.mod        go.sum        init.sh       input         main.go       package       renovate.json 

The fn.go file is where you add the function’s code. It’s useful to know about some other files in the template:

  • main.go runs the function. You don’t need to edit main.go.
  • Dockerfile builds the function runtime. You don’t need to edit Dockerfile.
  • The input directory defines the function’s input type.
  • The package directory contains metadata used to build the function package.
Tip
Starting with v1.15 of the Crossplane CLI, crossplane xpkg init gives you the option of running an initialization script to automate tasks like replacing the template name with the new function’s name.

You must make some changes before you start adding code:

  • Edit package/crossplane.yaml to change the package’s name.
  • Edit go.mod to change the Go module’s name.

Name your package function-xbuckets.

The name of your module depends on where you want to keep your function code. If you push Go code to GitHub, you can use your GitHub username. For example module github.com/negz/function-xbuckets.

The function in this guide doesn’t use an input type. For this function you should delete the input and package/input directories.

The input directory defines a Go struct that a function can use to take input, using the input field from a Composition. The composition functions documentation explains how to pass an input to a composition function.

The package/input directory contains an OpenAPI schema generated from the structs in the input directory.

Tip

If you’re writing a function that uses an input, edit the input to meet your function’s requirements.

Change the input’s kind and API group. Don’t use Input and template.fn.crossplane.io. Instead use something meaningful to your function.

When you edit files under the input directory you must update some generated files by running go generate. See input/generate.go for details.

1go generate ./... 

Edit the template to add the function’s logic

You add your function’s logic to the RunFunction method in fn.go. When you first open the file it contains a “hello world” function.

 1func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) {  2	f.log.Info("Running Function", "tag", req.GetMeta().GetTag())  3  4	rsp := response.To(req, response.DefaultTTL)  5  6	in := &v1beta1.Input{}  7	if err := request.GetInput(req, in); err != nil {  8		response.Fatal(rsp, errors.Wrapf(err, "cannot get Function input from %T", req))  9		return rsp, nil 10	} 11 12	response.Normalf(rsp, "I was run with input %q", in.Example) 13	return rsp, nil 14} 

All Go composition functions have a RunFunction method. Crossplane passes everything the function needs to run in a RunFunctionRequest struct.

The function tells Crossplane what resources it should compose by returning a RunFunctionResponse struct.

Tip
Crossplane generates the RunFunctionRequest and RunFunctionResponse structs using Protocol Buffers. You can find detailed schemas for RunFunctionRequest and RunFunctionResponse in the Buf Schema Registry.

Edit the RunFunction method to replace it with this code.

 1func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) {  2	rsp := response.To(req, response.DefaultTTL)  3  4	xr, err := request.GetObservedCompositeResource(req)  5	if err != nil {  6		response.Fatal(rsp, errors.Wrapf(err, "cannot get observed composite resource from %T", req))  7		return rsp, nil  8	}  9 10	region, err := xr.Resource.GetString("spec.region") 11	if err != nil { 12		response.Fatal(rsp, errors.Wrapf(err, "cannot read spec.region field of %s", xr.Resource.GetKind())) 13		return rsp, nil 14	} 15 16	names, err := xr.Resource.GetStringArray("spec.names") 17	if err != nil { 18		response.Fatal(rsp, errors.Wrapf(err, "cannot read spec.names field of %s", xr.Resource.GetKind())) 19		return rsp, nil 20	} 21 22	desired, err := request.GetDesiredComposedResources(req) 23	if err != nil { 24		response.Fatal(rsp, errors.Wrapf(err, "cannot get desired resources from %T", req)) 25		return rsp, nil 26	} 27 28	_ = v1beta1.AddToScheme(composed.Scheme) 29 30	for _, name := range names { 31		b := &v1beta1.Bucket{ 32			ObjectMeta: metav1.ObjectMeta{ 33				Annotations: map[string]string{ 34					"crossplane.io/external-name": name, 35				}, 36			}, 37			Spec: v1beta1.BucketSpec{ 38				ForProvider: v1beta1.BucketParameters{ 39					Region: ptr.To[string](region), 40				}, 41			}, 42		} 43 44		cd, err := composed.From(b) 45		if err != nil { 46			response.Fatal(rsp, errors.Wrapf(err, "cannot convert %T to %T", b, &composed.Unstructured{})) 47			return rsp, nil 48		} 49 50		desired[resource.Name("xbuckets-"+name)] = &resource.DesiredComposed{Resource: cd} 51	} 52 53	if err := response.SetDesiredComposedResources(rsp, desired); err != nil { 54		response.Fatal(rsp, errors.Wrapf(err, "cannot set desired composed resources in %T", rsp)) 55		return rsp, nil 56	} 57 58	return rsp, nil 59} 

Expand the below block to view the full fn.go, including imports and commentary explaining the function’s logic.

  1package main   2   3import (   4	"context"   5   6	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"   7	"k8s.io/utils/ptr"   8   9	"github.com/crossplane-contrib/provider-upjet-aws/apis/s3/v1beta1"  10  11	"github.com/crossplane/function-sdk-go/errors"  12	"github.com/crossplane/function-sdk-go/logging"  13	fnv1 "github.com/crossplane/function-sdk-go/proto/v1"  14	"github.com/crossplane/function-sdk-go/request"  15	"github.com/crossplane/function-sdk-go/resource"  16	"github.com/crossplane/function-sdk-go/resource/composed"  17	"github.com/crossplane/function-sdk-go/response"  18)  19  20// Function returns whatever response you ask it to.  21type Function struct {  22	fnv1.UnimplementedFunctionRunnerServiceServer  23  24	log logging.Logger  25}  26  27// RunFunction observes an XBuckets composite resource (XR). It adds an S3  28// bucket to the desired state for every entry in the XR's spec.names array.  29func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) {  30	f.log.Info("Running Function", "tag", req.GetMeta().GetTag())  31  32	// Create a response to the request. This copies the desired state and  33	// pipeline context from the request to the response.  34	rsp := response.To(req, response.DefaultTTL)  35  36	// Read the observed XR from the request. Most functions use the observed XR  37	// to add desired managed resources.  38	xr, err := request.GetObservedCompositeResource(req)  39	if err != nil {  40		// You can set a custom status condition on the XR. This  41		// allows you to communicate with the user.  42		response.ConditionFalse(rsp, "FunctionSuccess", "InternalError").  43			WithMessage("Something went wrong.").  44			TargetComposite()  45  46		// You can emit an event regarding the XR. This allows you to  47		// communicate with the user. Note that events should be used   48		// sparingly and are subject to throttling  49		response.Warning(rsp, errors.New("something went wrong")).  50			TargetComposite()  51  52		// If the function can't read the XR, the request is malformed. This  53		// should never happen. The function returns a fatal result. This tells  54		// Crossplane to stop running functions and return an error.  55		response.Fatal(rsp, errors.Wrapf(err, "cannot get observed composite resource from %T", req))  56		return rsp, nil  57	}  58  59	// Create an updated logger with useful information about the XR.  60	log := f.log.WithValues(  61		"xr-version", xr.Resource.GetAPIVersion(),  62		"xr-kind", xr.Resource.GetKind(),  63		"xr-name", xr.Resource.GetName(),  64	)  65  66	// Get the region from the XR. The XR has getter methods like GetString,  67	// GetBool, etc. You can use them to get values by their field path.  68	region, err := xr.Resource.GetString("spec.region")  69	if err != nil {  70		response.Fatal(rsp, errors.Wrapf(err, "cannot read spec.region field of %s", xr.Resource.GetKind()))  71		return rsp, nil  72	}  73  74	// Get the array of bucket names from the XR.  75	names, err := xr.Resource.GetStringArray("spec.names")  76	if err != nil {  77		response.Fatal(rsp, errors.Wrapf(err, "cannot read spec.names field of %s", xr.Resource.GetKind()))  78		return rsp, nil  79	}  80  81	// Get all desired composed resources from the request. The function will  82	// update this map of resources, then save it. This get, update, set pattern  83	// ensures the function keeps any resources added by other functions.  84	desired, err := request.GetDesiredComposedResources(req)  85	if err != nil {  86		response.Fatal(rsp, errors.Wrapf(err, "cannot get desired resources from %T", req))  87		return rsp, nil  88	}  89  90	// Add v1beta1 types (including Bucket) to the composed resource scheme.  91	// composed.From uses this to automatically set apiVersion and kind.  92	_ = v1beta1.AddToScheme(composed.Scheme)  93  94	// Add a desired S3 bucket for each name.  95	for _, name := range names {  96		// One advantage of writing a function in Go is strong typing. The  97		// function can import and use managed resource types from the provider.  98		b := &v1beta1.Bucket{  99			ObjectMeta: metav1.ObjectMeta{ 100				// Set the external name annotation to the desired bucket name. 101				// This controls what the bucket will be named in AWS. 102				Annotations: map[string]string{ 103					"crossplane.io/external-name": name, 104				}, 105			}, 106			Spec: v1beta1.BucketSpec{ 107				ForProvider: v1beta1.BucketParameters{ 108					// Set the bucket's region to the value read from the XR. 109					Region: ptr.To[string](region), 110				}, 111			}, 112		} 113 114		// Convert the bucket to the unstructured resource data format the SDK 115		// uses to store desired composed resources. 116		cd, err := composed.From(b) 117		if err != nil { 118			response.Fatal(rsp, errors.Wrapf(err, "cannot convert %T to %T", b, &composed.Unstructured{})) 119			return rsp, nil 120		} 121 122		// Add the bucket to the map of desired composed resources. It's 123		// important that the function adds the same bucket every time it's 124		// called. It's also important that the bucket is added with the same 125		// resource.Name every time it's called. The function prefixes the name 126		// with "xbuckets-" to avoid collisions with any other composed 127		// resources that might be in the desired resources map. 128		desired[resource.Name("xbuckets-"+name)] = &resource.DesiredComposed{Resource: cd} 129	} 130 131	// Finally, save the updated desired composed resources to the response. 132	if err := response.SetDesiredComposedResources(rsp, desired); err != nil { 133		response.Fatal(rsp, errors.Wrapf(err, "cannot set desired composed resources in %T", rsp)) 134		return rsp, nil 135	} 136 137	// Log what the function did. This will only appear in the function's pod 138	// logs. A function can use response.Normal and response.Warning to emit 139	// Kubernetes events associated with the XR it's operating on. 140	log.Info("Added desired buckets", "region", region, "count", len(names)) 141 142	// You can set a custom status condition on the XR. This allows you 143	// to communicate with the user. 144	response.ConditionTrue(rsp, "FunctionSuccess", "Success"). 145		TargetComposite() 146 147	return rsp, nil 148} 

This code:

  1. Gets the observed composite resource from the RunFunctionRequest.
  2. Gets the region and bucket names from the observed composite resource.
  3. Adds one desired S3 bucket for each bucket name.
  4. Returns the desired S3 buckets in a RunFunctionResponse.

The code uses the v1beta1.Bucket type from the AWS S3 provider. One advantage of writing a function in Go is that you can compose resources using the same strongly typed structs Crossplane uses in its providers.

You must get the AWS Provider Go module to use this type:

1go get github.com/crossplane-contrib/[email protected] 

Crossplane provides a software development kit (SDK) for writing composition functions in Go. This function uses utilities from the SDK. In particular the request and response packages make working with the RunFunctionRequest and RunFunctionResponse types easier.

Tip
Read the Go package documentation for the SDK.

Test the function end-to-end

Test your function by adding unit tests, and by using the crossplane render command.

Go has rich support for unit testing. When you initialize a function from the template it adds some unit tests to fn_test.go. These tests follow Go’s recommendations. They use only pkg/testing from the Go standard library and google/go-cmp.

To add test cases, update the cases map in TestRunFunction. Expand the below block to view the full fn_test.go file for the function.

  1package main   2   3import (   4	"context"   5	"testing"   6	"time"   7   8	"github.com/google/go-cmp/cmp"   9	"github.com/google/go-cmp/cmp/cmpopts"  10	"google.golang.org/protobuf/testing/protocmp"  11	"google.golang.org/protobuf/types/known/durationpb"  12  13	"github.com/crossplane/crossplane-runtime/pkg/logging"  14  15	fnv1 "github.com/crossplane/function-sdk-go/proto/v1"  16	"github.com/crossplane/function-sdk-go/resource"  17)  18  19func TestRunFunction(t *testing.T) {  20	type args struct {  21		ctx context.Context  22		req *fnv1.RunFunctionRequest  23	}  24	type want struct {  25		rsp *fnv1.RunFunctionResponse  26		err error  27	}  28  29	cases := map[string]struct {  30		reason string  31		args   args  32		want   want  33	}{  34		"AddTwoBuckets": {  35			reason: "The Function should add two buckets to the desired composed resources",  36			args: args{  37				req: &fnv1.RunFunctionRequest{  38					Observed: &fnv1.State{  39						Composite: &fnv1.Resource{  40							// MustStructJSON is a handy way to provide mock  41							// resources.  42							Resource: resource.MustStructJSON(`{  43								"apiVersion": "example.crossplane.io/v1alpha1",  44								"kind": "XBuckets",  45								"metadata": {  46									"name": "test"  47								},  48								"spec": {  49									"region": "us-east-2",  50									"names": [  51										"test-bucket-a",  52										"test-bucket-b"  53									]  54								}  55							}`),  56						},  57					},  58				},  59			},  60			want: want{  61				rsp: &fnv1.RunFunctionResponse{  62					Meta: &fnv1.ResponseMeta{Ttl: durationpb.New(60 * time.Second)},  63					Desired: &fnv1.State{  64						Resources: map[string]*fnv1.Resource{  65							"xbuckets-test-bucket-a": {Resource: resource.MustStructJSON(`{  66								"apiVersion": "s3.aws.m.upbound.io/v1beta1",  67								"kind": "Bucket",  68								"metadata": {  69									"annotations": {  70										"crossplane.io/external-name": "test-bucket-a"  71									}  72								},  73								"spec": {  74									"forProvider": {  75										"region": "us-east-2"  76									}  77								},  78								"status": {  79									"observedGeneration": 0  80								}  81							}`)},  82							"xbuckets-test-bucket-b": {Resource: resource.MustStructJSON(`{  83								"apiVersion": "s3.aws.m.upbound.io/v1beta1",  84								"kind": "Bucket",  85								"metadata": {  86									"annotations": {  87										"crossplane.io/external-name": "test-bucket-b"  88									}  89								},  90								"spec": {  91									"forProvider": {  92										"region": "us-east-2"  93									}  94								},  95								"status": {  96									"observedGeneration": 0  97								}  98							}`)},  99						}, 100					}, 101					Conditions: []*fnv1.Condition{ 102						{ 103							Type:   "FunctionSuccess", 104							Status: fnv1.Status_STATUS_CONDITION_TRUE, 105							Reason: "Success", 106							Target: fnv1.Target_TARGET_COMPOSITE.Enum(), 107						}, 108					}, 109				}, 110			}, 111		}, 112	} 113 114	for name, tc := range cases { 115		t.Run(name, func(t *testing.T) { 116			f := &Function{log: logging.NewNopLogger()} 117			rsp, err := f.RunFunction(tc.args.ctx, tc.args.req) 118 119			if diff := cmp.Diff(tc.want.rsp, rsp, protocmp.Transform()); diff != "" { 120				t.Errorf("%s\nf.RunFunction(...): -want rsp, +got rsp:\n%s", tc.reason, diff) 121			} 122 123			if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" { 124				t.Errorf("%s\nf.RunFunction(...): -want err, +got err:\n%s", tc.reason, diff) 125			} 126		}) 127	} 128} 

Run the unit tests using the go test command:

1go test -v -cover . 2=== RUN   TestRunFunction 3=== RUN   TestRunFunction/AddTwoBuckets 4--- PASS: TestRunFunction (0.00s) 5    --- PASS: TestRunFunction/AddTwoBuckets (0.00s) 6PASS 7coverage: 52.6% of statements 8ok      github.com/negz/function-xbuckets       0.016s  coverage: 52.6% of statements 

You can preview the output of a Composition that uses this function using the Crossplane CLI. You don’t need a Crossplane control plane to do this.

Under function-xbuckets, there is a directory named example with Composite Resource, Composition and Function YAML files.

Expand the following block to see example files.

You can recreate the output below using by running crossplane render with these files.

The xr.yaml file contains the composite resource to render:

 1apiVersion: example.crossplane.io/v1  2kind: XBuckets  3metadata:  4  name: example-buckets  5spec:  6  region: us-east-2  7  names:  8  - crossplane-functions-example-a  9  - crossplane-functions-example-b 10  - crossplane-functions-example-c 

The composition.yaml file contains the Composition to use to render the composite resource:

 1apiVersion: apiextensions.crossplane.io/v1  2kind: Composition  3metadata:  4  name: create-buckets  5spec:  6  compositeTypeRef:  7    apiVersion: example.crossplane.io/v1  8    kind: XBuckets  9  mode: Pipeline 10  pipeline: 11  - step: create-buckets 12    functionRef: 13      name: function-xbuckets 

The functions.yaml file contains the Functions the Composition references in its pipeline steps:

 1apiVersion: pkg.crossplane.io/v1  2kind: Function  3metadata:  4  name: function-xbuckets  5  annotations:  6    render.crossplane.io/runtime: Development  7spec:  8  # The CLI ignores this package when using the Development runtime.  9  # You can set it to any value. 10  package: xpkg.crossplane.io/negz/function-xbuckets:v0.1.0 

The Function in functions.yaml uses the Development runtime. This tells crossplane render that your function is running locally. It connects to your locally running function instead of using Docker to pull and run the function.

1apiVersion: pkg.crossplane.io/v1 2kind: Function 3metadata: 4  name: function-xbuckets 5  annotations: 6    render.crossplane.io/runtime: Development 

Use go run to run your function locally.

1go run . --insecure --debug 
Warning
The insecure flag tells the function to run without encryption or authentication. Only use it during testing and development.

In a separate terminal, run crossplane render.

1crossplane render xr.yaml composition.yaml functions.yaml 

This command calls your function. In the terminal where your function is running you should now see log output:

1go run . --insecure --debug 22023-10-31T16:17:32.158-0700    INFO    function-xbuckets/fn.go:29      Running Function        {"tag": ""} 32023-10-31T16:17:32.159-0700    INFO    function-xbuckets/fn.go:125     Added desired buckets   {"xr-version": "example.crossplane.io/v1", "xr-kind": "XBuckets", "xr-name": "example-buckets", "region": "us-east-2", "count": 3} 

The crossplane render command prints the desired resources the function returns.

 1---  2apiVersion: example.crossplane.io/v1  3kind: XBuckets  4metadata:  5  name: example-buckets  6---  7apiVersion: s3.aws.m.upbound.io/v1beta1  8kind: Bucket  9metadata: 10  annotations: 11    crossplane.io/composition-resource-name: xbuckets-crossplane-functions-example-b 12    crossplane.io/external-name: crossplane-functions-example-b 13  generateName: example-buckets- 14  labels: 15    crossplane.io/composite: example-buckets 16  ownerReferences: 17    # Omitted for brevity 18spec: 19  forProvider: 20    region: us-east-2 21--- 22apiVersion: s3.aws.m.upbound.io/v1beta1 23kind: Bucket 24metadata: 25  annotations: 26    crossplane.io/composition-resource-name: xbuckets-crossplane-functions-example-c 27    crossplane.io/external-name: crossplane-functions-example-c 28  generateName: example-buckets- 29  labels: 30    crossplane.io/composite: example-buckets 31  ownerReferences: 32    # Omitted for brevity 33spec: 34  forProvider: 35    region: us-east-2 36--- 37apiVersion: s3.aws.m.upbound.io/v1beta1 38kind: Bucket 39metadata: 40  annotations: 41    crossplane.io/composition-resource-name: xbuckets-crossplane-functions-example-a 42    crossplane.io/external-name: crossplane-functions-example-a 43  generateName: example-buckets- 44  labels: 45    crossplane.io/composite: example-buckets 46  ownerReferences: 47    # Omitted for brevity 48spec: 49  forProvider: 50    region: us-east-2 
Tip
Read the composition functions documentation to learn more about testing composition functions.

Build and push the function to a package registry

You build a function in two stages. First you build the function’s runtime. This is the Open Container Initiative (OCI) image Crossplane uses to run your function. You then embed that runtime in a package, and push it to a package registry. The Crossplane CLI uses xpkg.crossplane.io as its default package registry.

A function supports a single platform, like linux/amd64, by default. You can support multiple platforms by building a runtime and package for each platform, then pushing all the packages to a single tag in the registry.

Pushing your function to a registry allows you to use your function in a Crossplane control plane. See the composition functions documentation to learn how to use a function in a control plane.

Use Docker to build a runtime for each platform.

1docker build . --quiet --platform=linux/amd64 --tag runtime-amd64 2sha256:fdf40374cc6f0b46191499fbc1dbbb05ddb76aca854f69f2912e580cfe624b4b 
1docker build . --quiet --platform=linux/arm64 --tag runtime-arm64 2sha256:cb015ceabf46d2a55ccaeebb11db5659a2fb5e93de36713364efcf6d699069af 
Tip
You can use whatever tag you want. There’s no need to push the runtime images to a registry. The tag is only used to tell crossplane xpkg build what runtime to embed.

Use the Crossplane CLI to build a package for each platform. Each package embeds a runtime image.

The --package-root flag specifies the package directory, which contains crossplane.yaml. This includes metadata about the package.

The --embed-runtime-image flag specifies the runtime image tag built using Docker.

The --package-file flag specifies where to write the package file to disk. Crossplane package files use the extension .xpkg.

1crossplane xpkg build \ 2    --package-root=package \ 3    --embed-runtime-image=runtime-amd64 \ 4    --package-file=function-amd64.xpkg 
1crossplane xpkg build \ 2    --package-root=package \ 3    --embed-runtime-image=runtime-arm64 \ 4    --package-file=function-arm64.xpkg 
Tip
Crossplane packages are special OCI images. Read more about packages in the packages documentation.

Push both package files to a registry. Pushing both files to one tag in the registry creates a multi-platform package that runs on both linux/arm64 and linux/amd64 hosts.

1crossplane xpkg push \ 2  --package-files=function-amd64.xpkg,function-arm64.xpkg \ 3  negz/function-xbuckets:v0.1.0 
Tip
If you push the function to a GitHub repository the template automatically sets up continuous integration (CI) using GitHub Actions. The CI workflow will lint, test, and build your function. You can see how the template configures CI by reading .github/workflows/ci.yaml.
Crossplane logo
Twitter
Youtube
Podcast
Forum

© Crossplane Authors 2025. Documentation distributed under CC-BY-4.0.

© 2025 The Linux Foundation. All rights reserved. The Linux Foundation has registered trademarks and uses trademarks. For a list of trademarks of The Linux Foundation, please see our Trademark Usage page.

cncfLogo

We are a Cloud Native Computing Foundation incubating project.