Skip to main content

2. Build your composition logic

In the previous guide, you created a brand new project and reviewed the foundational components of your project. This guide walks through how to update those components to create real resources in your Upbound organization.

Prerequisites​

Make sure you've completed the previous guide and have:

If you missed any of the previous steps, go to the project foundations guide to get started.

Generate a function​

Composition functions allow you to write the logic for creating cloud resources with programming languages like KCL, Python, or Go.

With composition functions you can:

  • Write code with familiar programming concepts like variables, loops, and conditions
  • Catch errors with type safety before deployment
  • Keep related logic together in an easier to parse format
  • Test your infrastructure logic like any other code

Your composition function is a program you write that translates your user's requests as the inputs and returns specific cloud resources as the outputs.

Generate the composition function scaffolding and choose your preferred language:

up function generate example-function apis/storagebuckets/composition.yaml --language=kcl

This command creates a function directory and creates a new file based on your chosen language.

Create your function logic​

Next, create the actual program logic that builds your cloud resources.

Paste the following into main.k:

upbound-hello-world/functions/example-function/main.k
import models.io.upbound.awsm.s3.v1beta1 as awsms3v1beta1

oxr = option("params").oxr # observed composite resource
params = oxr.spec.parameters # extract parameter values from XR

_metadata = lambda name: str -> any {
{
generateName = name # due to global S3 naming restrictions we'll have
# Crossplane generate a name to garauntee uniqueness
annotations = {
"krm.kcl.dev/composition-resource-name" = name
}
}
}

_items: [any] = [
# Create S3 Bucket
awsms3v1beta1.Bucket {
metadata: _metadata("{}-bucket".format(oxr.metadata.name))
spec = {
forProvider = {
region = params.region
}
}
},

# Bucket BOC
awsms3v1beta1.BucketOwnershipControls {
metadata: _metadata("{}-boc".format(oxr.metadata.name))
spec = {
forProvider = {
bucketSelector: {
matchControllerRef: True
}
region = params.region
rule = {
objectOwnership = "BucketOwnerPreferred"
}
}
}
},

# Bucket PAB
awsms3v1beta1.BucketPublicAccessBlock {
metadata: _metadata("{}-pab".format(oxr.metadata.name))
spec = {
forProvider = {
bucketSelector: {
matchControllerRef: True
}
region = params.region
blockPublicAcls: False
ignorePublicAcls: False
restrictPublicBuckets: False
blockPublicPolicy: False
}
}
},

# Bucket ACL
awsms3v1beta1.BucketACL {
metadata: _metadata("{}-acl".format(oxr.metadata.name))
spec = {
forProvider = {
bucketSelector: {
matchControllerRef: True
}
region = params.region
acl = params.acl
}
}
},

# Default encryption for the bucket
awsms3v1beta1.BucketServerSideEncryptionConfiguration {
metadata: _metadata("{}-encryption".format(oxr.metadata.name))
spec = {
forProvider = {
region = params.region
bucketSelector: {
matchControllerRef: True
}
rule = [
{
applyServerSideEncryptionByDefault = {
sseAlgorithm = "AES256"
}
bucketKeyEnabled = True
}
]
}
}
}
]

# Set up versioning for the bucket if desired
if params.versioning:
_items += [
awsms3v1beta1.BucketVersioning{
metadata: _metadata("{}-versioning".format(oxr.metadata.name))
spec = {
forProvider = {
region = params.region
bucketSelector: {
matchControllerRef: True
}
versioningConfiguration = {
status = "Enabled"
}
}
}
}
]

items = _items

Save your composition file.

Review your function​

Resource imports and user inputs​

upbound-hello-world/functions/example-function/main.k
import models.io.upbound.awsm.s3.v1beta1 as awsms3v1beta1

oxr = option("params").oxr # observed composite resource
params = oxr.spec.parameters # extract parameter values from XR

This section:

  • Imports the cloud resource definitions required to create the resource
  • Extracts the user-specified values in the claim (params = oxr.spec.parameters)

Metadata and helper functions​

upbound-hello-world/functions/example-function/main.k

_metadata = lambda name: str -> any {
{
generateName = name # due to global S3 naming restrictions we'll have
# Crossplane generate a name to garauntee uniqueness
annotations = {
"krm.kcl.dev/composition-resource-name" = name
}
}
}

This section:

  • Standardizes resource naming
  • Adds required labels and annotations where needed
  • Leverages helper functions to sanitize naming where applicable
  • Reduces metadata duplication

Cloud resource definition​

upbound-hello-world/functions/example-function/main.k
# Create S3 Bucket
awsms3v1beta1.Bucket {
metadata: _metadata("{}-bucket".format(oxr.metadata.name))
spec = {
forProvider = {
region = params.region
}
}
},

This section:

  • Creates the primary resource for your chosen cloud provider
  • Inserts your claim parameters as required fields
  • Applies the metadata function to the resource

Security configuration​

upbound-hello-world/functions/example-function/main.k
# Bucket BOC
awsms3v1beta1.BucketOwnershipControls {
metadata: _metadata("{}-boc".format(oxr.metadata.name))
spec = {
forProvider = {
bucketSelector: {
matchControllerRef: True
}
region = params.region
rule = {
objectOwnership = "BucketOwnerPreferred"
}
}
}
},

# Bucket PAB
awsms3v1beta1.BucketPublicAccessBlock {
metadata: _metadata("{}-pab".format(oxr.metadata.name))
spec = {
forProvider = {
bucketSelector: {
matchControllerRef: True
}
region = params.region
blockPublicAcls: False
ignorePublicAcls: False
restrictPublicBuckets: False
blockPublicPolicy: False
}
}
},

This section:

  • Applies object ownership to the bucket
  • Allows for public access to the bucket

Access control and encryption​

upbound-hello-world/functions/example-function/main.k
# Bucket ACL
awsms3v1beta1.BucketACL {
metadata: _metadata("{}-acl".format(oxr.metadata.name))
spec = {
forProvider = {
bucketSelector: {
matchControllerRef: True
}
region = params.region
acl = params.acl
}
}
},

# Default encryption for the bucket
awsms3v1beta1.BucketServerSideEncryptionConfiguration {
metadata: _metadata("{}-encryption".format(oxr.metadata.name))
spec = {
forProvider = {
region = params.region
bucketSelector: {
matchControllerRef: True
}
rule = [
{
applyServerSideEncryptionByDefault = {
sseAlgorithm = "AES256"
}
bucketKeyEnabled = True
}
]
}
}
}

This section:

  • Sets the access level using the user's ACL parameter
  • Automatically enables encryption for all objects

Bucket versioning​

upbound-hello-world/functions/example-function/main.k
# Set up versioning for the bucket if desired
if params.versioning:
_items += [
awsms3v1beta1.BucketVersioning{
metadata: _metadata("{}-versioning".format(oxr.metadata.name))
spec = {
forProvider = {
region = params.region
bucketSelector: {
matchControllerRef: True
}
versioningConfiguration = {
status = "Enabled"
}
}
}
}
]

This section:

  • Only creates versioning if requested in the claim
  • Captures the items defined in the function as a single variable.

Save your changes.

Render your composition locally​

The up composition render command allows you to review your desired composed resources.

Render the composition against your XR file:

up composition render apis/storagebuckets/composition.yaml examples/storagebucket/example.yaml

This process ensures the build, configuration, and orchestration runs as expected before you deploy to a development control plane.

Errors in the render command can indicate a malformed function or other issues within the composition itself.

Next steps​

You constructed a new embedded function that allows user input from your claim file. This function uses your composition to create a fully configured storage resources to your specifications.

The next guide walks through how to test your composition function logic with the built-in test suite.