mirror of https://github.com/crossplane/docs.git
867 lines
28 KiB
Markdown
867 lines
28 KiB
Markdown
---
|
|
title: Write a Composition Function in Go
|
|
weight: 80
|
|
description: "Composition functions allow you to template resources using Go"
|
|
---
|
|
|
|
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]({{<ref "../concepts/compositions" >}})
|
|
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](https://go.dev).
|
|
|
|
{{< hint "important" >}}
|
|
It helps to be familiar with
|
|
[how composition functions work]({{<ref "../concepts/compositions#how-composition-functions-work" >}})
|
|
before following this guide.
|
|
{{< /hint >}}
|
|
|
|
## Understand the steps
|
|
|
|
This guide covers writing a composition function for an
|
|
{{<hover label="xr" line="2">}}XBuckets{{</hover>}} composite resource (XR).
|
|
|
|
```yaml {label="xr"}
|
|
apiVersion: example.crossplane.io/v1
|
|
kind: XBuckets
|
|
metadata:
|
|
name: example-buckets
|
|
spec:
|
|
region: us-east-2
|
|
names:
|
|
- crossplane-functions-example-a
|
|
- crossplane-functions-example-b
|
|
- crossplane-functions-example-c
|
|
```
|
|
|
|
<!-- vale gitlab.FutureTense = NO -->
|
|
<!--
|
|
This section is setting the stage for future sections. It doesn't make sense to
|
|
refer to the function in the present tense, because it doesn't exist yet.
|
|
-->
|
|
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.
|
|
<!-- vale gitlab.FutureTense = YES -->
|
|
|
|
To write a function in Go:
|
|
|
|
1. [Install the tools you need to write the function](#install-the-tools-you-need-to-write-the-function)
|
|
1. [Initialize the function from a template](#initialize-the-function-from-a-template)
|
|
1. [Edit the template to add the function's logic](#edit-the-template-to-add-the-functions-logic)
|
|
1. [Test the function end-to-end](#test-the-function-end-to-end)
|
|
1. [Build and push the function to a package repository](#build-and-push-the-function-to-a-package-registry)
|
|
|
|
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](https://go.dev/dl/) v1.23 or newer. The guide uses Go v1.23.
|
|
* [Docker Engine](https://docs.docker.com/engine/). This guide uses Engine v24.
|
|
* The [Crossplane CLI]({{<ref "../cli" >}}) v1.17 or newer. This guide uses Crossplane
|
|
CLI v1.17.
|
|
|
|
{{<hint "note">}}
|
|
You don't need access to a Kubernetes cluster or a Crossplane control plane to
|
|
build or test a composition function.
|
|
{{</hint>}}
|
|
|
|
## 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](https://github.com/crossplane/function-template-go)
|
|
as a template.
|
|
|
|
```shell {copy-lines=1}
|
|
crossplane xpkg init function-xbuckets function-template-go -d function-xbuckets
|
|
Initialized package "function-xbuckets" in directory "/home/negz/control/negz/function-xbuckets" from https://github.com/crossplane/function-template-go/tree/91a1a5eed21964ff98966d72cc6db6f089ad63f4 (main)
|
|
|
|
To get started:
|
|
|
|
1. Replace `function-template-go` with your function in `go.mod`,
|
|
`package/crossplane.yaml`, and any Go imports. (You can also do this
|
|
automatically by running the `./init.sh <function-name>` script.)
|
|
2. Update `input/v1beta1/` to reflect your desired input (and run `go generate`)
|
|
3. Add your logic to `RunFunction` in `fn.go`
|
|
4. Add tests for your logic in `fn_test.go`
|
|
5. Update `README.md`, to be about your function!
|
|
|
|
Found init.sh script!
|
|
Do you want to run it? [y]es/[n]o/[v]iew: y
|
|
Function 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:
|
|
|
|
```shell {copy-lines=1}
|
|
ls function-xbuckets
|
|
Dockerfile 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.
|
|
|
|
{{<hint "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.
|
|
{{</hint>}}
|
|
|
|
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]({{<ref "../concepts/compositions/#function-input" >}})
|
|
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.
|
|
|
|
{{<hint "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.
|
|
|
|
```shell
|
|
go generate ./...
|
|
```
|
|
{{</hint>}}
|
|
|
|
## Edit the template to add the function's logic
|
|
|
|
You add your function's logic to the
|
|
{{<hover label="hello-world" line="1">}}RunFunction{{</hover>}}
|
|
method in `fn.go`. When you first open the file it contains a "hello world"
|
|
function.
|
|
|
|
```go {label="hello-world"}
|
|
func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) {
|
|
f.log.Info("Running Function", "tag", req.GetMeta().GetTag())
|
|
|
|
rsp := response.To(req, response.DefaultTTL)
|
|
|
|
in := &v1beta1.Input{}
|
|
if err := request.GetInput(req, in); err != nil {
|
|
response.Fatal(rsp, errors.Wrapf(err, "cannot get Function input from %T", req))
|
|
return rsp, nil
|
|
}
|
|
|
|
response.Normalf(rsp, "I was run with input %q", in.Example)
|
|
return rsp, nil
|
|
}
|
|
```
|
|
|
|
All Go composition functions have a `RunFunction` method. Crossplane passes
|
|
everything the function needs to run in a
|
|
{{<hover label="hello-world" line="1">}}RunFunctionRequest{{</hover>}} struct.
|
|
|
|
The function tells Crossplane what resources it should compose by returning a
|
|
{{<hover label="hello-world" line="13">}}RunFunctionResponse{{</hover>}} struct.
|
|
|
|
{{<hint "tip">}}
|
|
Crossplane generates the `RunFunctionRequest` and `RunFunctionResponse` structs
|
|
using [Protocol Buffers](http://protobuf.dev). You can find detailed schemas for
|
|
`RunFunctionRequest` and `RunFunctionResponse` in the
|
|
[Buf Schema Registry](https://buf.build/crossplane/crossplane/docs/main:apiextensions.fn.proto.v1).
|
|
{{</hint>}}
|
|
|
|
Edit the `RunFunction` method to replace it with this code.
|
|
|
|
```go {hl_lines="4-56"}
|
|
func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) {
|
|
rsp := response.To(req, response.DefaultTTL)
|
|
|
|
xr, err := request.GetObservedCompositeResource(req)
|
|
if err != nil {
|
|
response.Fatal(rsp, errors.Wrapf(err, "cannot get observed composite resource from %T", req))
|
|
return rsp, nil
|
|
}
|
|
|
|
region, err := xr.Resource.GetString("spec.region")
|
|
if err != nil {
|
|
response.Fatal(rsp, errors.Wrapf(err, "cannot read spec.region field of %s", xr.Resource.GetKind()))
|
|
return rsp, nil
|
|
}
|
|
|
|
names, err := xr.Resource.GetStringArray("spec.names")
|
|
if err != nil {
|
|
response.Fatal(rsp, errors.Wrapf(err, "cannot read spec.names field of %s", xr.Resource.GetKind()))
|
|
return rsp, nil
|
|
}
|
|
|
|
desired, err := request.GetDesiredComposedResources(req)
|
|
if err != nil {
|
|
response.Fatal(rsp, errors.Wrapf(err, "cannot get desired resources from %T", req))
|
|
return rsp, nil
|
|
}
|
|
|
|
_ = v1beta1.AddToScheme(composed.Scheme)
|
|
|
|
for _, name := range names {
|
|
b := &v1beta1.Bucket{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Annotations: map[string]string{
|
|
"crossplane.io/external-name": name,
|
|
},
|
|
},
|
|
Spec: v1beta1.BucketSpec{
|
|
ForProvider: v1beta1.BucketParameters{
|
|
Region: ptr.To[string](region),
|
|
},
|
|
},
|
|
}
|
|
|
|
cd, err := composed.From(b)
|
|
if err != nil {
|
|
response.Fatal(rsp, errors.Wrapf(err, "cannot convert %T to %T", b, &composed.Unstructured{}))
|
|
return rsp, nil
|
|
}
|
|
|
|
desired[resource.Name("xbuckets-"+name)] = &resource.DesiredComposed{Resource: cd}
|
|
}
|
|
|
|
if err := response.SetDesiredComposedResources(rsp, desired); err != nil {
|
|
response.Fatal(rsp, errors.Wrapf(err, "cannot set desired composed resources in %T", rsp))
|
|
return rsp, nil
|
|
}
|
|
|
|
return rsp, nil
|
|
}
|
|
```
|
|
|
|
Expand the below block to view the full `fn.go`, including imports and
|
|
commentary explaining the function's logic.
|
|
|
|
{{<expand "The full fn.go file" >}}
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/utils/ptr"
|
|
|
|
"github.com/upbound/provider-aws/apis/s3/v1beta1"
|
|
|
|
"github.com/crossplane/function-sdk-go/errors"
|
|
"github.com/crossplane/function-sdk-go/logging"
|
|
fnv1 "github.com/crossplane/function-sdk-go/proto/v1"
|
|
"github.com/crossplane/function-sdk-go/request"
|
|
"github.com/crossplane/function-sdk-go/resource"
|
|
"github.com/crossplane/function-sdk-go/resource/composed"
|
|
"github.com/crossplane/function-sdk-go/response"
|
|
)
|
|
|
|
// Function returns whatever response you ask it to.
|
|
type Function struct {
|
|
fnv1.UnimplementedFunctionRunnerServiceServer
|
|
|
|
log logging.Logger
|
|
}
|
|
|
|
// RunFunction observes an XBuckets composite resource (XR). It adds an S3
|
|
// bucket to the desired state for every entry in the XR's spec.names array.
|
|
func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) {
|
|
f.log.Info("Running Function", "tag", req.GetMeta().GetTag())
|
|
|
|
// Create a response to the request. This copies the desired state and
|
|
// pipeline context from the request to the response.
|
|
rsp := response.To(req, response.DefaultTTL)
|
|
|
|
// Read the observed XR from the request. Most functions use the observed XR
|
|
// to add desired managed resources.
|
|
xr, err := request.GetObservedCompositeResource(req)
|
|
if err != nil {
|
|
// You can set a custom status condition on the claim. This
|
|
// allows you to communicate with the user.
|
|
response.ConditionFalse(rsp, "FunctionSuccess", "InternalError").
|
|
WithMessage("Something went wrong.").
|
|
TargetCompositeAndClaim()
|
|
|
|
// You can emit an event regarding the claim. This allows you to
|
|
// communicate with the user. Note that events should be used
|
|
// sparingly and are subject to throttling
|
|
response.Warning(rsp, errors.New("something went wrong")).
|
|
TargetCompositeAndClaim()
|
|
|
|
// If the function can't read the XR, the request is malformed. This
|
|
// should never happen. The function returns a fatal result. This tells
|
|
// Crossplane to stop running functions and return an error.
|
|
response.Fatal(rsp, errors.Wrapf(err, "cannot get observed composite resource from %T", req))
|
|
return rsp, nil
|
|
}
|
|
|
|
// Create an updated logger with useful information about the XR.
|
|
log := f.log.WithValues(
|
|
"xr-version", xr.Resource.GetAPIVersion(),
|
|
"xr-kind", xr.Resource.GetKind(),
|
|
"xr-name", xr.Resource.GetName(),
|
|
)
|
|
|
|
// Get the region from the XR. The XR has getter methods like GetString,
|
|
// GetBool, etc. You can use them to get values by their field path.
|
|
region, err := xr.Resource.GetString("spec.region")
|
|
if err != nil {
|
|
response.Fatal(rsp, errors.Wrapf(err, "cannot read spec.region field of %s", xr.Resource.GetKind()))
|
|
return rsp, nil
|
|
}
|
|
|
|
// Get the array of bucket names from the XR.
|
|
names, err := xr.Resource.GetStringArray("spec.names")
|
|
if err != nil {
|
|
response.Fatal(rsp, errors.Wrapf(err, "cannot read spec.names field of %s", xr.Resource.GetKind()))
|
|
return rsp, nil
|
|
}
|
|
|
|
// Get all desired composed resources from the request. The function will
|
|
// update this map of resources, then save it. This get, update, set pattern
|
|
// ensures the function keeps any resources added by other functions.
|
|
desired, err := request.GetDesiredComposedResources(req)
|
|
if err != nil {
|
|
response.Fatal(rsp, errors.Wrapf(err, "cannot get desired resources from %T", req))
|
|
return rsp, nil
|
|
}
|
|
|
|
// Add v1beta1 types (including Bucket) to the composed resource scheme.
|
|
// composed.From uses this to automatically set apiVersion and kind.
|
|
_ = v1beta1.AddToScheme(composed.Scheme)
|
|
|
|
// Add a desired S3 bucket for each name.
|
|
for _, name := range names {
|
|
// One advantage of writing a function in Go is strong typing. The
|
|
// function can import and use managed resource types from the provider.
|
|
b := &v1beta1.Bucket{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
// Set the external name annotation to the desired bucket name.
|
|
// This controls what the bucket will be named in AWS.
|
|
Annotations: map[string]string{
|
|
"crossplane.io/external-name": name,
|
|
},
|
|
},
|
|
Spec: v1beta1.BucketSpec{
|
|
ForProvider: v1beta1.BucketParameters{
|
|
// Set the bucket's region to the value read from the XR.
|
|
Region: ptr.To[string](region),
|
|
},
|
|
},
|
|
}
|
|
|
|
// Convert the bucket to the unstructured resource data format the SDK
|
|
// uses to store desired composed resources.
|
|
cd, err := composed.From(b)
|
|
if err != nil {
|
|
response.Fatal(rsp, errors.Wrapf(err, "cannot convert %T to %T", b, &composed.Unstructured{}))
|
|
return rsp, nil
|
|
}
|
|
|
|
// Add the bucket to the map of desired composed resources. It's
|
|
// important that the function adds the same bucket every time it's
|
|
// called. It's also important that the bucket is added with the same
|
|
// resource.Name every time it's called. The function prefixes the name
|
|
// with "xbuckets-" to avoid collisions with any other composed
|
|
// resources that might be in the desired resources map.
|
|
desired[resource.Name("xbuckets-"+name)] = &resource.DesiredComposed{Resource: cd}
|
|
}
|
|
|
|
// Finally, save the updated desired composed resources to the response.
|
|
if err := response.SetDesiredComposedResources(rsp, desired); err != nil {
|
|
response.Fatal(rsp, errors.Wrapf(err, "cannot set desired composed resources in %T", rsp))
|
|
return rsp, nil
|
|
}
|
|
|
|
// Log what the function did. This will only appear in the function's pod
|
|
// logs. A function can use response.Normal and response.Warning to emit
|
|
// Kubernetes events associated with the XR it's operating on.
|
|
log.Info("Added desired buckets", "region", region, "count", len(names))
|
|
|
|
// You can set a custom status condition on the claim. This allows you
|
|
// to communicate with the user.
|
|
response.ConditionTrue(rsp, "FunctionSuccess", "Success").
|
|
TargetCompositeAndClaim()
|
|
|
|
return rsp, nil
|
|
}
|
|
```
|
|
{{</expand>}}
|
|
|
|
This code:
|
|
|
|
1. Gets the observed composite resource from the `RunFunctionRequest`.
|
|
1. Gets the region and bucket names from the observed composite resource.
|
|
1. Adds one desired S3 bucket for each bucket name.
|
|
1. Returns the desired S3 buckets in a `RunFunctionResponse`.
|
|
|
|
The code uses the `v1beta1.Bucket` type from the [AWS S3 provider](https://github.com/crossplane-contrib/provider-upjet-aws).
|
|
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:
|
|
|
|
```shell
|
|
go get github.com/upbound/provider-aws@v1.14.0
|
|
```
|
|
|
|
Crossplane provides a
|
|
[software development kit](https://github.com/crossplane/function-sdk-go) (SDK)
|
|
for writing composition functions in [Go](https://go.dev). This function uses
|
|
utilities from the SDK. In particular the `request` and `response` packages make
|
|
working with the `RunFunctionRequest` and `RunFunctionResponse` types easier.
|
|
|
|
{{<hint "tip">}}
|
|
Read the
|
|
[Go package documentation](https://pkg.go.dev/github.com/crossplane/function-sdk-go)
|
|
for the SDK.
|
|
{{</hint>}}
|
|
|
|
## 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](https://go.dev/wiki/TestComments). They use only
|
|
[`pkg/testing`](https://pkg.go.dev/testing) from the Go standard library and
|
|
[`google/go-cmp`](https://pkg.go.dev/github.com/google/go-cmp/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.
|
|
|
|
{{<expand "The full fn_test.go file" >}}
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"google.golang.org/protobuf/testing/protocmp"
|
|
"google.golang.org/protobuf/types/known/durationpb"
|
|
|
|
"github.com/crossplane/crossplane-runtime/pkg/logging"
|
|
|
|
fnv1 "github.com/crossplane/function-sdk-go/proto/v1"
|
|
"github.com/crossplane/function-sdk-go/resource"
|
|
)
|
|
|
|
func TestRunFunction(t *testing.T) {
|
|
type args struct {
|
|
ctx context.Context
|
|
req *fnv1.RunFunctionRequest
|
|
}
|
|
type want struct {
|
|
rsp *fnv1.RunFunctionResponse
|
|
err error
|
|
}
|
|
|
|
cases := map[string]struct {
|
|
reason string
|
|
args args
|
|
want want
|
|
}{
|
|
"AddTwoBuckets": {
|
|
reason: "The Function should add two buckets to the desired composed resources",
|
|
args: args{
|
|
req: &fnv1.RunFunctionRequest{
|
|
Observed: &fnv1.State{
|
|
Composite: &fnv1.Resource{
|
|
// MustStructJSON is a handy way to provide mock
|
|
// resources.
|
|
Resource: resource.MustStructJSON(`{
|
|
"apiVersion": "example.crossplane.io/v1alpha1",
|
|
"kind": "XBuckets",
|
|
"metadata": {
|
|
"name": "test"
|
|
},
|
|
"spec": {
|
|
"region": "us-east-2",
|
|
"names": [
|
|
"test-bucket-a",
|
|
"test-bucket-b"
|
|
]
|
|
}
|
|
}`),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: want{
|
|
rsp: &fnv1.RunFunctionResponse{
|
|
Meta: &fnv1.ResponseMeta{Ttl: durationpb.New(60 * time.Second)},
|
|
Desired: &fnv1.State{
|
|
Resources: map[string]*fnv1.Resource{
|
|
"xbuckets-test-bucket-a": {Resource: resource.MustStructJSON(`{
|
|
"apiVersion": "s3.aws.upbound.io/v1beta1",
|
|
"kind": "Bucket",
|
|
"metadata": {
|
|
"annotations": {
|
|
"crossplane.io/external-name": "test-bucket-a"
|
|
}
|
|
},
|
|
"spec": {
|
|
"forProvider": {
|
|
"region": "us-east-2"
|
|
}
|
|
},
|
|
"status": {
|
|
"observedGeneration": 0
|
|
}
|
|
}`)},
|
|
"xbuckets-test-bucket-b": {Resource: resource.MustStructJSON(`{
|
|
"apiVersion": "s3.aws.upbound.io/v1beta1",
|
|
"kind": "Bucket",
|
|
"metadata": {
|
|
"annotations": {
|
|
"crossplane.io/external-name": "test-bucket-b"
|
|
}
|
|
},
|
|
"spec": {
|
|
"forProvider": {
|
|
"region": "us-east-2"
|
|
}
|
|
},
|
|
"status": {
|
|
"observedGeneration": 0
|
|
}
|
|
}`)},
|
|
},
|
|
},
|
|
Conditions: []*fnv1.Condition{
|
|
{
|
|
Type: "FunctionSuccess",
|
|
Status: fnv1.Status_STATUS_CONDITION_TRUE,
|
|
Reason: "Success",
|
|
Target: fnv1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
f := &Function{log: logging.NewNopLogger()}
|
|
rsp, err := f.RunFunction(tc.args.ctx, tc.args.req)
|
|
|
|
if diff := cmp.Diff(tc.want.rsp, rsp, protocmp.Transform()); diff != "" {
|
|
t.Errorf("%s\nf.RunFunction(...): -want rsp, +got rsp:\n%s", tc.reason, diff)
|
|
}
|
|
|
|
if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" {
|
|
t.Errorf("%s\nf.RunFunction(...): -want err, +got err:\n%s", tc.reason, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
```
|
|
{{</expand>}}
|
|
|
|
Run the unit tests using the `go test` command:
|
|
|
|
```shell {copy-lines=1}
|
|
go test -v -cover .
|
|
=== RUN TestRunFunction
|
|
=== RUN TestRunFunction/AddTwoBuckets
|
|
--- PASS: TestRunFunction (0.00s)
|
|
--- PASS: TestRunFunction/AddTwoBuckets (0.00s)
|
|
PASS
|
|
coverage: 52.6% of statements
|
|
ok 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.
|
|
|
|
{{<expand "The xr.yaml, composition.yaml and function.yaml 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:
|
|
|
|
```yaml
|
|
apiVersion: example.crossplane.io/v1
|
|
kind: XBuckets
|
|
metadata:
|
|
name: example-buckets
|
|
spec:
|
|
region: us-east-2
|
|
names:
|
|
- crossplane-functions-example-a
|
|
- crossplane-functions-example-b
|
|
- crossplane-functions-example-c
|
|
```
|
|
|
|
<br />
|
|
|
|
The `composition.yaml` file contains the Composition to use to render the
|
|
composite resource:
|
|
|
|
```yaml
|
|
apiVersion: apiextensions.crossplane.io/v1
|
|
kind: Composition
|
|
metadata:
|
|
name: create-buckets
|
|
spec:
|
|
compositeTypeRef:
|
|
apiVersion: example.crossplane.io/v1
|
|
kind: XBuckets
|
|
mode: Pipeline
|
|
pipeline:
|
|
- step: create-buckets
|
|
functionRef:
|
|
name: function-xbuckets
|
|
```
|
|
|
|
<br />
|
|
|
|
The `functions.yaml` file contains the Functions the Composition references in
|
|
its pipeline steps:
|
|
|
|
```yaml
|
|
apiVersion: pkg.crossplane.io/v1
|
|
kind: Function
|
|
metadata:
|
|
name: function-xbuckets
|
|
annotations:
|
|
render.crossplane.io/runtime: Development
|
|
spec:
|
|
# The CLI ignores this package when using the Development runtime.
|
|
# You can set it to any value.
|
|
package: xpkg.crossplane.io/negz/function-xbuckets:v0.1.0
|
|
```
|
|
{{</expand>}}
|
|
|
|
The Function in `functions.yaml` uses the
|
|
{{<hover label="development" line="6">}}Development{{</hover>}}
|
|
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.
|
|
|
|
```yaml {label="development"}
|
|
apiVersion: pkg.crossplane.io/v1
|
|
kind: Function
|
|
metadata:
|
|
name: function-xbuckets
|
|
annotations:
|
|
render.crossplane.io/runtime: Development
|
|
```
|
|
|
|
Use `go run` to run your function locally.
|
|
|
|
```shell {label="run"}
|
|
go run . --insecure --debug
|
|
```
|
|
|
|
{{<hint "warning">}}
|
|
The {{<hover label="run" line="1">}}insecure{{</hover>}} flag tells the function
|
|
to run without encryption or authentication. Only use it during testing and
|
|
development.
|
|
{{</hint>}}
|
|
|
|
In a separate terminal, run `crossplane render`.
|
|
|
|
```shell
|
|
crossplane 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:
|
|
|
|
```shell
|
|
go run . --insecure --debug
|
|
2023-10-31T16:17:32.158-0700 INFO function-xbuckets/fn.go:29 Running Function {"tag": ""}
|
|
2023-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.
|
|
|
|
```yaml
|
|
---
|
|
apiVersion: example.crossplane.io/v1
|
|
kind: XBuckets
|
|
metadata:
|
|
name: example-buckets
|
|
---
|
|
apiVersion: s3.aws.upbound.io/v1beta1
|
|
kind: Bucket
|
|
metadata:
|
|
annotations:
|
|
crossplane.io/composition-resource-name: xbuckets-crossplane-functions-example-b
|
|
crossplane.io/external-name: crossplane-functions-example-b
|
|
generateName: example-buckets-
|
|
labels:
|
|
crossplane.io/composite: example-buckets
|
|
ownerReferences:
|
|
# Omitted for brevity
|
|
spec:
|
|
forProvider:
|
|
region: us-east-2
|
|
---
|
|
apiVersion: s3.aws.upbound.io/v1beta1
|
|
kind: Bucket
|
|
metadata:
|
|
annotations:
|
|
crossplane.io/composition-resource-name: xbuckets-crossplane-functions-example-c
|
|
crossplane.io/external-name: crossplane-functions-example-c
|
|
generateName: example-buckets-
|
|
labels:
|
|
crossplane.io/composite: example-buckets
|
|
ownerReferences:
|
|
# Omitted for brevity
|
|
spec:
|
|
forProvider:
|
|
region: us-east-2
|
|
---
|
|
apiVersion: s3.aws.upbound.io/v1beta1
|
|
kind: Bucket
|
|
metadata:
|
|
annotations:
|
|
crossplane.io/composition-resource-name: xbuckets-crossplane-functions-example-a
|
|
crossplane.io/external-name: crossplane-functions-example-a
|
|
generateName: example-buckets-
|
|
labels:
|
|
crossplane.io/composite: example-buckets
|
|
ownerReferences:
|
|
# Omitted for brevity
|
|
spec:
|
|
forProvider:
|
|
region: us-east-2
|
|
```
|
|
|
|
{{<hint "tip">}}
|
|
Read the composition functions documentation to learn more about
|
|
[testing composition functions]({{< ref "../concepts/compositions#test-a-composition" >}}).
|
|
{{</hint>}}
|
|
|
|
## 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]({{<ref "../concepts/compositions" >}})
|
|
to learn how to use a function in a control plane.
|
|
|
|
Use Docker to build a runtime for each platform.
|
|
|
|
```shell {copy-lines="1"}
|
|
docker build . --quiet --platform=linux/amd64 --tag runtime-amd64
|
|
sha256:fdf40374cc6f0b46191499fbc1dbbb05ddb76aca854f69f2912e580cfe624b4b
|
|
```
|
|
|
|
```shell {copy-lines="1"}
|
|
docker build . --quiet --platform=linux/arm64 --tag runtime-arm64
|
|
sha256:cb015ceabf46d2a55ccaeebb11db5659a2fb5e93de36713364efcf6d699069af
|
|
```
|
|
|
|
{{<hint "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.
|
|
{{</hint>}}
|
|
|
|
Use the Crossplane CLI to build a package for each platform. Each package embeds
|
|
a runtime image.
|
|
|
|
The {{<hover label="build" line="2">}}--package-root{{</hover>}} flag specifies
|
|
the `package` directory, which contains `crossplane.yaml`. This includes
|
|
metadata about the package.
|
|
|
|
The {{<hover label="build" line="3">}}--embed-runtime-image{{</hover>}} flag
|
|
specifies the runtime image tag built using Docker.
|
|
|
|
The {{<hover label="build" line="4">}}--package-file{{</hover>}} flag specifies
|
|
specifies where to write the package file to disk. Crossplane package files use
|
|
the extension `.xpkg`.
|
|
|
|
```shell {label="build"}
|
|
crossplane xpkg build \
|
|
--package-root=package \
|
|
--embed-runtime-image=runtime-amd64 \
|
|
--package-file=function-amd64.xpkg
|
|
```
|
|
|
|
```shell
|
|
crossplane xpkg build \
|
|
--package-root=package \
|
|
--embed-runtime-image=runtime-arm64 \
|
|
--package-file=function-arm64.xpkg
|
|
```
|
|
|
|
{{<hint "tip">}}
|
|
Crossplane packages are special OCI images. Read more about packages in the
|
|
[packages documentation]({{< ref "../concepts/packages" >}}).
|
|
{{</hint>}}
|
|
|
|
Push both package files to a registry. Pushing both files to one tag in the
|
|
registry creates a
|
|
[multi-platform](https://docs.docker.com/build/building/multi-platform/)
|
|
package that runs on both `linux/arm64` and `linux/amd64` hosts.
|
|
|
|
```shell
|
|
crossplane xpkg push \
|
|
--package-files=function-amd64.xpkg,function-arm64.xpkg \
|
|
negz/function-xbuckets:v0.1.0
|
|
```
|
|
|
|
{{<hint "tip">}}
|
|
If you push the function to a GitHub repository the template automatically sets
|
|
up continuous integration (CI) using
|
|
[GitHub Actions](https://github.com/features/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`.
|
|
{{</hint>}}
|