mirror of https://github.com/crossplane/docs.git
736 lines
25 KiB
Markdown
736 lines
25 KiB
Markdown
---
|
|
title: Write a Composition Function in Python
|
|
weight: 81
|
|
description: "Composition functions allow you to template resources using Python"
|
|
---
|
|
|
|
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
|
|
[Python](https://python.org).
|
|
|
|
{{< 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 Python:
|
|
|
|
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 Python you need:
|
|
|
|
* [Python](https://www.python.org/downloads/) v3.11.
|
|
* [Hatch](https://hatch.pypa.io/), a Python build tool. This guide uses v1.7.
|
|
* [Docker Engine](https://docs.docker.com/engine/). This guide uses Engine v24.
|
|
* The [Crossplane CLI]({{<ref "../cli" >}}) v1.14 or newer. This guide uses Crossplane
|
|
CLI v1.14.
|
|
|
|
{{<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-python)
|
|
as a template.
|
|
|
|
```shell {copy-lines=1}
|
|
crossplane xpkg init function-xbuckets https://github.com/crossplane/function-template-python -d function-xbuckets
|
|
Initialized package "function-xbuckets" in directory "/home/negz/control/negz/function-xbuckets" from https://github.com/crossplane/function-template-python/tree/bfed6923ab4c8e7adeed70f41138645fc7d38111 (main)
|
|
```
|
|
|
|
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 example/ function/ LICENSE package/ pyproject.toml README.md renovate.json tests/
|
|
```
|
|
|
|
Your function's code lives in the `function` directory:
|
|
|
|
```shell {copy-lines=1}
|
|
ls function/
|
|
__version__.py fn.py main.py
|
|
```
|
|
|
|
The `function/fn.py` file is where you add the function's code. It's useful to
|
|
know about some other files in the template:
|
|
|
|
* `function/main.py` runs the function. You don't need to edit `main.py`.
|
|
* `Dockerfile` builds the function runtime. You don't need to edit `Dockerfile`.
|
|
* The `package` directory contains metadata used to build the function package.
|
|
|
|
{{<hint "tip">}}
|
|
<!-- vale gitlab.FutureTense = NO -->
|
|
<!--
|
|
This tip talks about future plans for Crossplane.
|
|
-->
|
|
In v1.14 of the Crossplane CLI `crossplane xpkg init` just clones a
|
|
template GitHub repository. A future CLI release will automate tasks like
|
|
replacing the template name with the new function's name. See Crossplane issue
|
|
[#4941](https://github.com/crossplane/crossplane/issues/4941) for details.
|
|
<!-- vale gitlab.FutureTense = YES -->
|
|
{{</hint>}}
|
|
|
|
Edit `package/crossplane.yaml` to change the package's name before you start
|
|
adding code. Name your package `function-xbuckets`.
|
|
|
|
The `package/input` directory defines the OpenAPI schema for the a function's
|
|
input. The function in this guide doesn't accept an input. Delete the
|
|
`package/input` directory.
|
|
|
|
The [composition functions]({{<ref "../concepts/compositions" >}})
|
|
documentation explains composition function inputs.
|
|
|
|
{{<hint "tip">}}
|
|
If you're writing a function that uses an input, edit the input YAML file 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.
|
|
{{</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 `function/fn.py`. When you first open the file it contains a "hello
|
|
world" function.
|
|
|
|
```python {label="hello-world"}
|
|
async def RunFunction(self, req: fnv1.RunFunctionRequest, _: grpc.aio.ServicerContext) -> fnv1.RunFunctionResponse:
|
|
log = self.log.bind(tag=req.meta.tag)
|
|
log.info("Running function")
|
|
|
|
rsp = response.to(req)
|
|
|
|
example = ""
|
|
if "example" in req.input:
|
|
example = req.input["example"]
|
|
|
|
# TODO: Add your function logic here!
|
|
response.normal(rsp, f"I was run with input {example}!")
|
|
log.info("I was run!", input=example)
|
|
|
|
return rsp
|
|
```
|
|
|
|
All Python composition functions have a `RunFunction` method. Crossplane passes
|
|
everything the function needs to run in a
|
|
{{<hover label="hello-world" line="1">}}RunFunctionRequest{{</hover>}} object.
|
|
|
|
The function tells Crossplane what resources it should compose by returning a
|
|
{{<hover label="hello-world" line="15">}}RunFunctionResponse{{</hover>}} object.
|
|
|
|
Edit the `RunFunction` method to replace it with this code.
|
|
|
|
```python {hl_lines="7-28"}
|
|
async def RunFunction(self, req: fnv1.RunFunctionRequest, _: grpc.aio.ServicerContext) -> fnv1.RunFunctionResponse:
|
|
log = self.log.bind(tag=req.meta.tag)
|
|
log.info("Running function")
|
|
|
|
rsp = response.to(req)
|
|
|
|
region = req.observed.composite.resource["spec"]["region"]
|
|
names = req.observed.composite.resource["spec"]["names"]
|
|
|
|
for name in names:
|
|
rsp.desired.resources[f"xbuckets-{name}"].resource.update(
|
|
{
|
|
"apiVersion": "s3.aws.upbound.io/v1beta1",
|
|
"kind": "Bucket",
|
|
"metadata": {
|
|
"annotations": {
|
|
"crossplane.io/external-name": name,
|
|
},
|
|
},
|
|
"spec": {
|
|
"forProvider": {
|
|
"region": region,
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
log.info("Added desired buckets", region=region, count=len(names))
|
|
|
|
return rsp
|
|
```
|
|
|
|
Expand the below block to view the full `fn.py`, including imports and
|
|
commentary explaining the function's logic.
|
|
|
|
{{<expand "The full fn.py file" >}}
|
|
```python
|
|
"""A Crossplane composition function."""
|
|
|
|
import grpc
|
|
from crossplane.function import logging, response
|
|
from crossplane.function.proto.v1 import run_function_pb2 as fnv1
|
|
from crossplane.function.proto.v1 import run_function_pb2_grpc as grpcv1
|
|
|
|
|
|
class FunctionRunner(grpcv1.FunctionRunnerService):
|
|
"""A FunctionRunner handles gRPC RunFunctionRequests."""
|
|
|
|
def __init__(self):
|
|
"""Create a new FunctionRunner."""
|
|
self.log = logging.get_logger()
|
|
|
|
async def RunFunction(
|
|
self, req: fnv1.RunFunctionRequest, _: grpc.aio.ServicerContext
|
|
) -> fnv1.RunFunctionResponse:
|
|
"""Run the function."""
|
|
# Create a logger for this request.
|
|
log = self.log.bind(tag=req.meta.tag)
|
|
log.info("Running function")
|
|
|
|
# Create a response to the request. This copies the desired state and
|
|
# pipeline context from the request to the response.
|
|
rsp = response.to(req)
|
|
|
|
# Get the region and a list of bucket names from the observed composite
|
|
# resource (XR). Crossplane represents resources using the Struct
|
|
# well-known protobuf type. The Struct Python object can be accessed
|
|
# like a dictionary.
|
|
region = req.observed.composite.resource["spec"]["region"]
|
|
names = req.observed.composite.resource["spec"]["names"]
|
|
|
|
# Add a desired S3 bucket for each name.
|
|
for name in names:
|
|
# Crossplane represents desired composed resources using a protobuf
|
|
# map of messages. This works a little like a Python defaultdict.
|
|
# Instead of assigning to a new key in the dict-like map, you access
|
|
# the key and mutate its value as if it did exist.
|
|
#
|
|
# The below code works because accessing the xbuckets-{name} key
|
|
# automatically creates a new, empty fnv1.Resource message. The
|
|
# Resource message has a resource field containing an empty Struct
|
|
# object that can be populated from a dictionary by calling update.
|
|
#
|
|
# https://protobuf.dev/reference/python/python-generated/#map-fields
|
|
rsp.desired.resources[f"xbuckets-{name}"].resource.update(
|
|
{
|
|
"apiVersion": "s3.aws.upbound.io/v1beta1",
|
|
"kind": "Bucket",
|
|
"metadata": {
|
|
"annotations": {
|
|
"crossplane.io/external-name": name,
|
|
},
|
|
},
|
|
"spec": {
|
|
"forProvider": {
|
|
"region": region,
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
# 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))
|
|
|
|
return rsp
|
|
```
|
|
{{</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`.
|
|
|
|
Crossplane provides a
|
|
[software development kit](https://github.com/crossplane/function-sdk-python)
|
|
(SDK) for writing composition functions in Python. This function uses utilities
|
|
from the SDK.
|
|
|
|
{{<hint "tip">}}
|
|
Read [the Python Function SDK documentation](https://crossplane.github.io/function-sdk-python).
|
|
{{</hint>}}
|
|
|
|
{{<hint "important">}}
|
|
The Python SDK automatically generates the `RunFunctionRequest` and
|
|
`RunFunctionResponse` Python objects from a
|
|
[Protocol Buffers](https://protobuf.dev) schema. You can see the schema in the
|
|
[Buf Schema Registry](https://buf.build/crossplane/crossplane/docs/main:apiextensions.fn.proto.v1).
|
|
|
|
The fields of the generated Python objects behave similarly to builtin Python
|
|
types like dictionaries and lists. Be aware that there are some differences.
|
|
|
|
Notably, you access the map of observed and desired resources like a dictionary
|
|
but you can't add a new desired resource by assigning to a map key. Instead,
|
|
access and mutate the map key as if it already exists.
|
|
|
|
Instead of adding a new resource like this:
|
|
|
|
```python
|
|
resource = {"apiVersion": "example.org/v1", "kind": "Composed", ...}
|
|
rsp.desired.resources["new-resource"] = fnv1.Resource(resource=resource)
|
|
```
|
|
|
|
Pretend it already exists and mutate it, like this:
|
|
|
|
```python
|
|
resource = {"apiVersion": "example.org/v1", "kind": "Composed", ...}
|
|
rsp.desired.resources["new-resource"].resource.update(resource)
|
|
```
|
|
|
|
Refer to the Protocol Buffers
|
|
[Python Generated Code Guide](https://protobuf.dev/reference/python/python-generated/#fields)
|
|
for further details.
|
|
{{</hint>}}
|
|
|
|
## Test the function end-to-end
|
|
|
|
Test your function by adding unit tests, and by using the `crossplane render`
|
|
command.
|
|
|
|
When you initialize a function from the
|
|
template it adds some unit tests to `tests/test_fn.py`. These tests use the
|
|
[`unittest`](https://docs.python.org/3/library/unittest.html) module from the
|
|
Python standard library.
|
|
|
|
To add test cases, update the `cases` list in `test_run_function`. Expand the
|
|
below block to view the full `tests/test_fn.py` file for the function.
|
|
|
|
{{<expand "The full test_fn.py file" >}}
|
|
```python
|
|
import dataclasses
|
|
import unittest
|
|
|
|
from crossplane.function import logging, resource
|
|
from crossplane.function.proto.v1 import run_function_pb2 as fnv1
|
|
from google.protobuf import duration_pb2 as durationpb
|
|
from google.protobuf import json_format
|
|
from google.protobuf import struct_pb2 as structpb
|
|
|
|
from function import fn
|
|
|
|
|
|
class TestFunctionRunner(unittest.IsolatedAsyncioTestCase):
|
|
def setUp(self) -> None:
|
|
logging.configure(level=logging.Level.DISABLED)
|
|
self.maxDiff = 2000
|
|
|
|
async def test_run_function(self) -> None:
|
|
@dataclasses.dataclass
|
|
class TestCase:
|
|
reason: str
|
|
req: fnv1.RunFunctionRequest
|
|
want: fnv1.RunFunctionResponse
|
|
|
|
cases = [
|
|
TestCase(
|
|
reason="The function should compose two S3 buckets.",
|
|
req=fnv1.RunFunctionRequest(
|
|
observed=fnv1.State(
|
|
composite=fnv1.Resource(
|
|
resource=resource.dict_to_struct(
|
|
{
|
|
"apiVersion": "example.crossplane.io/v1alpha1",
|
|
"kind": "XBuckets",
|
|
"metadata": {"name": "test"},
|
|
"spec": {
|
|
"region": "us-east-2",
|
|
"names": ["test-bucket-a", "test-bucket-b"],
|
|
},
|
|
}
|
|
)
|
|
)
|
|
)
|
|
),
|
|
want=fnv1.RunFunctionResponse(
|
|
meta=fnv1.ResponseMeta(ttl=durationpb.Duration(seconds=60)),
|
|
desired=fnv1.State(
|
|
resources={
|
|
"xbuckets-test-bucket-a": fnv1.Resource(
|
|
resource=resource.dict_to_struct(
|
|
{
|
|
"apiVersion": "s3.aws.upbound.io/v1beta1",
|
|
"kind": "Bucket",
|
|
"metadata": {
|
|
"annotations": {
|
|
"crossplane.io/external-name": "test-bucket-a"
|
|
},
|
|
},
|
|
"spec": {
|
|
"forProvider": {"region": "us-east-2"}
|
|
},
|
|
}
|
|
)
|
|
),
|
|
"xbuckets-test-bucket-b": fnv1.Resource(
|
|
resource=resource.dict_to_struct(
|
|
{
|
|
"apiVersion": "s3.aws.upbound.io/v1beta1",
|
|
"kind": "Bucket",
|
|
"metadata": {
|
|
"annotations": {
|
|
"crossplane.io/external-name": "test-bucket-b"
|
|
},
|
|
},
|
|
"spec": {
|
|
"forProvider": {"region": "us-east-2"}
|
|
},
|
|
}
|
|
)
|
|
),
|
|
},
|
|
),
|
|
context=structpb.Struct(),
|
|
),
|
|
),
|
|
]
|
|
|
|
runner = fn.FunctionRunner()
|
|
|
|
for case in cases:
|
|
got = await runner.RunFunction(case.req, None)
|
|
self.assertEqual(
|
|
json_format.MessageToDict(got),
|
|
json_format.MessageToDict(case.want),
|
|
"-want, +got",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|
|
```
|
|
{{</expand>}}
|
|
|
|
Run the unit tests using `hatch run`:
|
|
|
|
```shell {copy-lines="1"}
|
|
hatch run test:unit
|
|
.
|
|
----------------------------------------------------------------------
|
|
Ran 1 test in 0.003s
|
|
|
|
OK
|
|
```
|
|
|
|
{{<hint "tip">}}
|
|
[Hatch](https://hatch.pypa.io/) is a Python build tool. It builds Python
|
|
artifacts like wheels. It also manages virtual environments, similar
|
|
to `virtualenv` or `venv`. The `hatch run` command creates a virtual environment
|
|
and runs a command in that environment.
|
|
{{</hint>}}
|
|
|
|
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.
|
|
|
|
Create a directory under `function-xbuckets` named `example` and create
|
|
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 `hatch run development` to run your function locally.
|
|
|
|
```shell {label="run"}
|
|
hatch run development
|
|
```
|
|
|
|
{{<hint "warning">}}
|
|
`hatch run development` runs the function 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
|
|
hatch run development
|
|
2024-01-11T22:12:58.153572Z [info ] Running function filename=fn.py lineno=22 tag=
|
|
2024-01-11T22:12:58.153792Z [info ] Added desired buckets count=3 filename=fn.py lineno=68 region=us-east-2 tag=
|
|
```
|
|
|
|
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>}}
|
|
|
|
{{<hint "important">}}
|
|
Docker uses emulation to create images for different platforms. If building an
|
|
image for a different platform fails, make sure you have installed `binfmt`. See
|
|
the
|
|
[Docker documentation](https://docs.docker.com/build/building/multi-platform/#qemu)
|
|
for instructions.
|
|
{{</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>}}
|