Models

Upbound Official Providers and some other packages include Pydantic models for their resources. These models enable in-line documentation, linting, autocompletion, and other features when working with Crossplane resources in embedded Python functions.

Make models available to a function

Use up dependency add to make models from a dependency available to a function. Dependencies are most often Crossplane providers, but they can also be configurations that include XRDs.

up dependency add xpkg.upbound.io/upbound/provider-aws-s3:v1.16.0

Use up project build to make models available for XRDs defined by your project.

up project build
Tip
up caches Python models in the .up/python directory, at the root of your project. You shouldn’t commit the .up directory to source control.

Import models into a function

Each provider’s models are available in their own package, named after the package’s resource names. Import models to your main.py function file with the following syntax:

from .model.io.upbound.aws.s3.bucket import v1beta1 as bucketv1beta1
Tip
The period prefix on .model is important. It tells Python to look for the model package in the same directory as main.py.

Use model in a function

Once you import the model, you can convert import resources to model types and construct output resources using model types.

Crossplane passes resources to your function as generic, Python dictionary-like objects. Convert them to model types to take advantage of type checking, linting, and autocompletion:

from crossplane.function.proto.v1 import run_function_pb2 as fnv1

from .model.com.example.platform.xmytype import v1alpha1
from .model.io.upbound.aws.s3.bucket import v1beta1 as bucketv1beta1


def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse):
    observed_xr = v1alpha1.XMyType(**req.observed.composite.resource)
    observed_bucket = bucketv1beta1.Bucket(**req.observed.resource["bucket"].resource)

Use resource.update to add composed resources to the function’s response:

from crossplane.function import resource
from crossplane.function.proto.v1 import run_function_pb2 as fnv1

from .model.io.upbound.aws.s3.bucket import v1beta1


def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse):
    bucket = v1beta1.Bucket(
        apiVersion="s3.aws.upbound.io/v1beta1",
        kind="Bucket",
        spec=v1beta1.Spec(
            forProvider=v1beta1.ForProvider(
                region="us-west-1",
            ),
        ),
    )
    resource.update(rsp.desired.resources["bucket"], bucket)

You can also use resource.update to update the desired XR:

from crossplane.function import resource
from crossplane.function.proto.v1 import run_function_pb2 as fnv1

from .model.com.example.platform.xmytype import v1alpha1


def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse):
    desired_xr = v1alpha1.XMyType(**req.desired.composite.resource)
    desired_xr.status.replicas = 3
    resource.update(rsp.desired.composite.resource, desired_xr)
Tip

If you don’t want to use a model, you can also pass resource.update a Python dictionary.

from crossplane.function import resource
from crossplane.function.proto.v1 import run_function_pb2 as fnv1


def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse):
    resource.update(rsp.desired.composite.resource, {
        "status: {
            "replicas": 3,
        },
    })

Supported packages

All Upbound Official Providers include Python models.

When you build your project with up project build, the generated artifact contains the generated models for your XRDs. You can build a project and then import that project as a dependency for the resources you define. You can also use your own project’s models in your functions as described above.

Optional and required fields

Upbound’s Python models know which resource fields Crossplane requires and which are optional.

Required fields have a specific type, like str - a string.

Python raises an exception if you create a model without supplying a required field. This can be a problem when updating the desired composite resource (XR).

You should only include the fields your function has an opinion about when you update the desired XR. This can be a problem if for example Crossplane requires an XR spec field, but your function only wants to update a status field.

When updating the desired XR, you can avoid issues due to required fields by using the resource’s status model directly.

from crossplane.function import resource
from crossplane.function.proto.v1 import run_function_pb2 as fnv1

from .model.com.example.platform.xstoragebucket import v1alpha1


def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse):
    # Create a model of the XR's status.
    desired_xr_status = v1alpha1.Status()

    # Include any desired status from previous functions in the pipeline.
    if "status" in req.desired.composite.resource:
        desired_xr_status = v1alpha1.Status(**req.desired.composite.resource["status"])

    # Update only the status field your function is concerned with.
    desired_xr_status.replicas = 3

    # Dump the model as a Python dictionary.
    resource.update(rsp.desired.composite, {"status": desired_xr_status.model_dump()})

Optional fields have a union type with None, like str | None. This means the field can be a string, or None - Python’s null value.

Pydantic warns you when you copy a required field to an optional field.

For example, Pydantic warns you if you try to copy an optional spec.region field from an XR to a required spec.forProvider.region field of an MR:

from crossplane.function import resource
from crossplane.function.proto.v1 import run_function_pb2 as fnv1

from .model.io.upbound.aws.s3.bucket import v1beta1 as bucketv1beta1
from .model.org.example.xstoragebucket import v1alpha1


def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse):
    observed_xr = v1alpha1.XStorageBucket(**req.observed.composite.resource)

    desired_bucket = bucketv1beta1.Bucket(
        apiVersion="s3.aws.upbound.io/v1beta1",
        kind="Bucket",
        spec=bucketv1beta1.Spec(
            forProvider=bucketv1beta1.ForProvider(
                region=observed_xr.spec.region,  # Warning: Argument of type "str | None" cannot be assigned to parameter "region" of type "str"
            ),
        ),
    )
    resource.update(rsp.desired.resources["bucket"], desired_bucket)

You can address this warning two ways.

If the optional field could be None in practice, handle that case by specifying a default value.

from crossplane.function import resource
from crossplane.function.proto.v1 import run_function_pb2 as fnv1

from .model.io.upbound.aws.s3.bucket import v1beta1 as bucketv1beta1
from .model.org.example.xstoragebucket import v1alpha1


def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse):
    observed_xr = v1alpha1.XStorageBucket(**req.observed.composite.resource)

    region = "us-west-2"
    if observed_xr.spec.region is not None:
        region = observed_xr.spec.

    desired_bucket = bucketv1beta1.Bucket(
        apiVersion="s3.aws.upbound.io/v1beta1",
        kind="Bucket",
        spec=bucketv1beta1.Spec(
            forProvider=bucketv1beta1.ForProvider(
                region=observed_xr.spec.region or "us-west-2",  # Default to "us-west-2" if region is None.
            ),
        ),
    )
    resource.update(rsp.desired.resources["bucket"], desired_bucket)

If the optional field can’t be None in practice, use a type: ignore comment to silence the warning.

from crossplane.function import resource
from crossplane.function.proto.v1 import run_function_pb2 as fnv1

from .model.io.k8s.apimachinery.pkg.apis.meta import v1 as metav1
from .model.io.upbound.aws.s3.bucket import v1beta1 as bucketv1beta1
from .model.org.example.xstoragebucket import v1alpha1


def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse):
    observed_xr = v1alpha1.XStorageBucket(**req.observed.composite.resource)

    desired_bucket = bucketv1beta1.Bucket(
        apiVersion="s3.aws.upbound.io/v1beta1",
        kind="Bucket",
        from .model.io.k8s.apimachinery.pkg.apis.meta import v1 as metav1
        metadata=metav1.ObjectMeta(
            name=observed_xr.metadata.name + "-bucket", # type: ignore  # The observed XR will always have a name.
        ),
        spec=bucketv1beta1.Spec(
            forProvider=bucketv1beta1.ForProvider(
                region="us-west-2",
            ),
        ),
    )
    resource.update(rsp.desired.resources["bucket"], desired_bucket)