Skip to main content

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 test-function apis/xstoragebuckets/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/test-function/main.k
import models.io.upbound.aws.s3.v1beta1 as s3v1beta1

oxr = option("params").oxr # observed composite resource
params = oxr.spec.parameters

bucketName = "{}-bucket".format(oxr.metadata.name)

_metadata = lambda name: str -> any {
{
name = name
annotations = {
"krm.kcl.dev/composition-resource-name" = name
}
}
}

_items: [any] = [
# Bucket in the desired region
s3v1beta1.Bucket{
metadata: _metadata(bucketName)
spec = {
forProvider = {
region = params.region
}
}
},
s3v1beta1.BucketOwnershipControls{
metadata: _metadata("{}-boc".format(oxr.metadata.name))
spec = {
forProvider = {
bucketRef = {
name = bucketName
}
region = params.region
rule:[{
objectOwnership:"BucketOwnerPreferred"
}]
}
}
},
s3v1beta1.BucketPublicAccessBlock{
metadata: _metadata("{}-pab".format(oxr.metadata.name))
spec = {
forProvider = {
bucketRef = {
name = bucketName
}
region = params.region
blockPublicAcls: False
ignorePublicAcls: False
restrictPublicBuckets: False
blockPublicPolicy: False
}
}
},
# ACL for the bucket
s3v1beta1.BucketACL{
metadata: _metadata("{}-acl".format(oxr.metadata.name))
spec = {
forProvider = {
bucketRef = {
name = bucketName
}
region = params.region
acl = params.acl
}
}
},
# Default encryption for the bucket
s3v1beta1.BucketServerSideEncryptionConfiguration{
metadata: _metadata("{}-encryption".format(oxr.metadata.name))
spec = {
forProvider = {
region = params.region
bucketRef = {
name = bucketName
}
rule = [
{
applyServerSideEncryptionByDefault = [
{
sseAlgorithm = "AES256"
}
]
bucketKeyEnabled = True
}
]
}
}
}
]

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

items = _items

Save your composition file.

Review your function

Resource imports and user inputs

upbound-hello-world/functions/test-function/main.k
import models.io.upbound.aws.s3.v1beta1 as s3v1beta1

oxr = option("params").oxr # observed composite resource
params = oxr.spec.parameters

bucketName = "{}-bucket".format(oxr.metadata.name)

This section:

  • Imports the cloud resource definitions required to create the resource
  • Extracts the user-specified values in the claim (params = oxr.spec.parameters)
  • Creates a resource name with the claim name and appropriate suffix.

Metadata helper function

upbound-hello-world/functions/test-function/main.k
_metadata = lambda name: str -> any {
{
name = name
annotations = {
"krm.kcl.dev/composition-resource-name" = name
}
}
}

This section:

  • Standardizes resource naming
  • Adds required annotations
  • Reduces metadata duplication

Cloud resource definition

upbound-hello-world/functions/test-function/main.k
_items: [any] = [
# Main S3 bucket
s3v1beta1.Bucket{
metadata: _metadata(bucketName)
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/test-function/main.k
s3v1beta1.BucketOwnershipControls{
metadata: _metadata("{}-boc".format(oxr.metadata.name))
spec = {
forProvider = {
bucketRef = {
name = bucketName
}
region = params.region
rule:[{
objectOwnership:"BucketOwnerPreferred"
}]
}
}
},
s3v1beta1.BucketPublicAccessBlock{
metadata: _metadata("{}-pab".format(oxr.metadata.name))
spec = {
forProvider = {
bucketRef = {
name = bucketName
}
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/test-function/main.k
# ACL for the bucket
s3v1beta1.BucketACL{
metadata: _metadata("{}-acl".format(oxr.metadata.name))
spec = {
forProvider = {
bucketRef = {
name = bucketName
}
region = params.region
acl = params.acl
}
}
},
# Default encryption for the bucket
s3v1beta1.BucketServerSideEncryptionConfiguration{
metadata: _metadata("{}-encryption".format(oxr.metadata.name))
spec = {
forProvider = {
region = params.region
bucketRef = {
name = bucketName
}
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/test-function/main.k
# Set up versioning for the bucket if desired
if params.versioning:
_items += [
s3v1beta1.BucketVersioning{
metadata: _metadata("{}-versioning".format(oxr.metadata.name))
spec = {
forProvider = {
region = params.region
bucketRef = {
name = bucketName
}
versioningConfiguration = [
{
status = "Enabled"
}
]
}
}
}
]

items = _items

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. To render a composition, create a Composite Resource (XR) file.

Create a new file called examples/storagebucket/xr.yaml:

examples/storagebucket/xr.yaml
apiVersion: platform.example.com/v1alpha1
kind: XStorageBucket
metadata:
name: example
spec:
parameters:
region: us-west-1
versioning: true
acl: public-read

Render the composition against your XR file:

up composition render apis/xstoragebuckets/composition.yaml examples/storagebucket/xr.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.