Save Hours with DevEx for Crossplane

Take a look at this.

I’ll just say “let there be an XRD” and…

Do not try to run the commands in this section. They are only a preview of what we’ll explore in this post. Full instructions are available starting from the Setup section.

up xrd generate examples/sql/my-db.yaml

code apis/xsqls/definition.yaml

The output is as follows.

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xsqls.devopstoolkit.live
spec:
  claimNames:
    kind: SQL
    plural: sqls
  group: devopstoolkit.live
  names:
    categories:
      - crossplane
    kind: XSQL
    plural: xsqls
  versions:
    - name: v1alpha1
      referenceable: true
      schema:
        openAPIV3Schema:
          description: SQL is the Schema for the SQL API.
          properties:
            spec:
              description: SQLSpec defines the desired state of SQL.
              properties:
                id:
                  type: string
                parameters:
                  properties:
                    databases:
                      items:
                        type: string
                      type: array
                    region:
                      type: string
                    size:
                      type: string
                    version:
                      type: string
                  type: object
              type: object
            status:
              description: SQLStatus defines the observed state of SQL.
              type: object
          required:
            - spec
          type: object
      served: true

Boom! Done!

I just saved you half an hour of work.

Is that not enough?

How about saving you much more time by enabling you to define Compositions as code with IntellIsense support in VSCode?

Think about those hundreds or even thousands of lines of YAML you wrote before when you were writing Compositions. Try to imagine how much time would be saved if you wrote code instead of YAML, if you had intellisense support in VSCode which allowed you to use auto-complete based on schemas from dependencies you chose? Think about the possibility to develop Crossplane resources in a language of choice, with full support from your favorite IDE, with commands to build, push, test, deploy the results of your work, with CI/CD pipelines, and all the other goodies we normally associate with development lifecycle.

Would that be something interesting?

If you answered “no” to that last question, I can only assume that you are not familiar with Crossplane, since I cannot imagine why would someone who is using it reject such an offer. If that’s you, if you are new to Crossplane, stop watching this video and go through the Crossplane Tutorial instead. You’ll thank me later.

On the other hand, if you did say “yes”, keep watching. I’ll show you the beginning of something very exciting.

That something is… DevEx or developer experience for Crossplane users, or a way how to implement developer lifecycle for Crossplane, or a way develop Crossplane Compositions instead of doing whatever we were doing before or…

You’ll see. It’s exciting, and I want to jump straight into it.

Before we continue, there is something very important you should be aware of. What I’m about to show you is in very early stage. As a matter of fact, I’m writing this post while what I’m showing is not even public. It’s alpha, or pre-alpha, or pre-pre-alpha. By the time you read this, it probably changed a lot. Commands might have changed. New features have almost certainly been added. It might have been turned upside down. Keep that in mind while reading. I’m showing you very early version of an idea, or a proof of concept. This is the worst version of it you’ll ever see. Your experience should be very different and much better. Heck, if you try to follow my instructions they are likely not going to even work because the solution likely changed drastically in the meantime, hopefully for better. I’m probably making a mistake by sharing this with you, but I was too excited to wait any longer.

Here we go!

Setup

git clone https://github.com/vfarcic/upbound-devex-demo

cd upbound-devex-demo

Watch Nix for Everyone: Unleash Devbox for Simplified Development if you are not familiar with Devbox. Alternatively, you can skip Devbox and install all the tools listed in devbox.json yourself.

devbox shell

Install up CLI by following the instructions.

Replace [...] in the command that follows with your name (e.g., Viktor Farcic)

export NAME="[...]"

Replace [...] in the command that follows with your email

export EMAIL="[...]"

export GIT_URL=$(git config --get remote.origin.url)

Register at Upbound Console if you haven’t already.

Replace [...] with your Upbound account.

export UP_ACCOUNT=[...]

Install KCL Language Server.

Install Visual Studio Code (VSCode) and make sure that code command is working.

Install VSCode plugins KCL and Upbound.

Crossplane (Upbound) Project

This will be a story that starts slow. It’s one of those that the more we progress the more interesting it gets.

It all starts with a project.

If we would like to develop Crossplane Configurations with Composite Definitions, Compositions, and everything else, the first step is to initiate a new project,…

up project init silly-demo

…and enter into it.

cd silly-demo/

Let’s see what we got.

ls

The output is as follows.

-I apis
-I examples
-I functions
-I LICENSE
-I upbound.yaml

We got the apis directory where CompositeDefinitions and Compositions will reside. Then there is functions where the actual code of Compositions will be. Finally, there is upbound.yaml which is similar to Configuration we were defining in crossplane.yaml, but, this time, it is a different kind of resource which we’ll explore soon.

The first major benefit is that now we have a standard. Instead of each of us trying to figure out how to organize Crossplane projects, we have a “standard” way to do so. Standards are good since they help us navigate other people’s work more easily.

Another big improvement is that now everything is integrated with VSCode. For example, we’ll get IntelliSense with code completion which helps tremendously, especially when working with potentially big Compositions.

We’ll see that in action soon. For now, let’s just open VSCode.

code .

Let’s take a look at upbound.yaml that was auto-generated for us when we initialized the project.

code upbound.yaml

The output is as follows.

apiVersion: meta.dev.upbound.io/v1alpha1
kind: Project
metadata:
  creationTimestamp: null
  name: silly-demo
spec:
  description: This is where you can describe your project.
  license: Apache-2.0
  maintainer: Upbound User <user@example.com>
  readme: |
    This is where you can add a readme for your project.    
  repository: xpkg.upbound.io/<YOUR ORGANIZATION>/silly-demo
  source: github.com/upbound/project-template

That file, for now, contains mostly the information that will help us later. That will change, very soon. For now, feel free to update it to match your data like the maintainer, the repository which should point to your Upbound organization, and the source which should point to the Git repository where you might want to keep the project we created.

Open a terminal session in VSCode to execute the commands that follow as well as any other command throughout the rest of this post. Alternatively, when commands are modifying the files (like those that follow), you can make the changes directly in the VSCode editor.

yq --inplace ".spec.maintainer = \"$NAME <$EMAIL>\"" upbound.yaml

yq --inplace \
    ".spec.repository = \"xpkg.upbound.io/$UP_ACCOUNT/silly-demo\"" \
    upbound.yaml

yq --inplace ".spec.source = \"$GIT_URL\"" upbound.yaml

Next, we should probably start thinking about dependencies that are, most of the time, providers and functions that we’ll use in our Compositions. Today, we’ll be building a Composition that should manage PostgreSQL servers in AWS, Azure, and Google Cloud. To do that, we’ll need ec2,…

up dependency add \
    "xpkg.upbound.io/upboundcare/provider-aws-ec2@>=v1.16.0"

… and rds providers for AWS.

up dependency add \
    "xpkg.upbound.io/upboundcare/provider-aws-rds@>=v1.16.0"

Then there is dbforpostgresql for Azure.

up dependency add \
    "xpkg.upbound.io/upbound/provider-azure-dbforpostgresql@>=v1.7.0"

Finally, there is also sql provider for Google Cloud.

up dependency add \
    "xpkg.upbound.io/upbound/provider-gcp-sql@>=v0.33.0"

We’ll also add the generic sql provider that we can use for generic resources that are the same in all three hyperscalers.

up dependency add \
    "xpkg.upbound.io/crossplane-contrib/provider-sql@>=v0.9.0"

We should probably add the kubernetes provider if we would like to have custom-made Kubernetes secrets.

up dependency add \
    "xpkg.upbound.io/crossplane-contrib/provider-kubernetes@>=v0.15.0"

Finally, we’ll add the status-transformer function that might help us to propagate relevant statuses to claims.

up dependency add \
    "xpkg.upbound.io/crossplane-contrib/function-status-transformer@v0.4.0"

Those commands should have simplified management of dependencies and we can see the outcome by taking another look at upbound.yaml.

code upbound.yaml

The output is as follows.

apiVersion: meta.dev.upbound.io/v1alpha1
kind: Project
metadata:
  name: silly-demo
spec:
  dependsOn:
  - provider: xpkg.upbound.io/upboundcare/provider-aws-ec2
    version: '>=v1.16.0'
  - provider: xpkg.upbound.io/upboundcare/provider-aws-rds
    version: '>=v1.16.0'
  - provider: xpkg.upbound.io/upbound/provider-azure-dbforpostgresql
    version: '>=v1.7.0'
  - provider: xpkg.upbound.io/upbound/provider-gcp-sql
    version: '>=v0.33.0'
  - provider: xpkg.upbound.io/crossplane-contrib/provider-sql
    version: '>=v0.9.0'
  - provider: xpkg.upbound.io/crossplane-contrib/provider-kubernetes
    version: '>=v0.15.0'
  - function: xpkg.upbound.io/crossplane-contrib/function-status-transformer
    version: '>=v0.4.0'
  description: This is where you can describe your project.
  license: Apache-2.0
  maintainer: Viktor Farcic <viktor@farcic.com>
  readme: |
    This is where you can add a readme for your project.    
  repository: xpkg.upbound.io/devops-toolkit/silly-demo
  source: https://github.com/vfarcic/upbound-devex-demo

We can see that all the dependencies have been added as dependsOn entries. Some of them are providers (provider) while others are functions (function).

The next chapter in this story will spice it up. As I already mentioned, it gets better with time.

Crossplane Composite Resource Definitions (XRDs)

There are two main types of resources we need to define when trying to build services with Crossplane. There are Composite Resource Definitions and Compositions themselves. Right now we are going to focus on the former.

Typically, we would start the work by writing the CompositeResourceDefinition or XRD. That’s what ultimately becomes Kubernetes Custom Resource Definition. That’s what creates new interfaces the consumers of your service will interact with. That’s what allows people to define claims.

Now, to be honest, writing XRDs is tedious.

Here’s an example.

Please watch https://youtu.be/BII6ZY2Rnlc if you are not familiar with GitHub CLI.

gh browse --repo vfarcic/crossplane-sql package/definition.yaml

Now, I won’t go into details of that XRD. As I already mentioned, today I’m assuming that you are familiar with Crossplane and, more importantly, that you experienced the pain of writing XRDs.

Today, we’ll take a very different approach. Instead of writing XRDs directly, we’ll approach the problem from a different angle. We’ll define examples of what we feel our users should be defining as claims. In other words, we’ll define the end-user experience first, and let the CLI do the heavy lifting of defining the XRD based on those.

We can define baseline claims or composite resources by executing up example generate.

up example generate

The output is as follows.

What do you want to create? [type to search]: 
> Composite Resource Claim (XRC)
  Composite Resource (XR)

That command walks us through a series of questions and will use the answers to generate the initial claim or a composite resource.

While that is great, I’m personally not very fond of wizards so, instead, we’ll execute that same command with the information provided as arguments.

Press ctrl+c to cancel the “wizard”.

up example generate --name my-db \
    --namespace default --type xrc --kind SQL \
    --api-group devopstoolkit.live \
    --api-version v1alpha1

The output is as follows.

Successfully created resource and saved to examples/sql/my-db.yaml

We can see, from the output, that it generated my-db.yaml file, so let’s take a look at what we got.

code examples/sql/my-db.yaml

The output is as follows.

apiVersion: devopstoolkit.live/v1alpha1
kind: SQL
metadata:
  name: my-db
  namespace: default
spec: {}

That’s the base definition of a claim that we expect our users to define.

Replace spec: {} with the YAML that follows in examples/sql/my-db.yaml.

spec:
  id: my-db
  compositionSelector:
    matchLabels:
      provider: aws
      db: postgresql
  parameters:
    version: "16.2"
    size: medium
    region: us-east-1

Let’s spice it up a bit by saying that they should be able to set the id. Since that database server should work in AWS, Azure, and Google Cloud, the user might want to specify which one should be used through matchLabels and, in this case, we’ll say that the provider should be aws and that the db should be postgresql since, at least in theory, there might be an option to use MySQL or some other database server as well.

You’ll notice that, as we type, IntelliSense is showing us what’s wrong by giving us warnings, errors, and suggestions. Nevertheless, that’s only the tip of the iceberg. You’ll see IntelliSense in its true glory later.

We’ll also define a few parameters like the version, set the size to medium, and the region to us-east-1.

What we did is fictitious. There is no kind SQL in our clusters. No one created that CRD. All we did was to define what the users might expect to have as the interface when creating databases.

Think of the phase we just went through as design. Now that we’re done, we should convert all those examples into the actual XRD that will, eventually, become a Kubernetes CRD.

So, we’ll execute up xrd generate with the path to our example, and press the enter key.

up xrd generate examples/sql/my-db.yaml

The output is as follows.

Successfully created CompositeResourceDefinition and saved to apis/xsqls/definition.yaml

Let’s take a look at what we got.

code apis/xsqls/definition.yaml

The output is as follows.

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xsqls.devopstoolkit.live
spec:
  claimNames:
    kind: SQL
    plural: sqls
  group: devopstoolkit.live
  names:
    categories:
    - crossplane
    kind: XSQL
    plural: xsqls
  versions:
  - name: v1alpha1
    referenceable: true
    schema:
      openAPIV3Schema:
        description: SQL is the Schema for the SQL API.
        properties:
          spec:
            description: SQLSpec defines the desired state of SQL.
            properties:
              id:
                type: string
              parameters:
                properties:
                  region:
                    type: string
                  size:
                    type: string
                  version:
                    type: string
                type: object
            type: object
          status:
            description: SQLStatus defines the observed state of SQL.
            type: object
        required:
        - spec
        type: object
    served: true

That is a full CompositeResourceDefinition that we would normally define ourselves. That’s the tedious part we just avoided by creating it based on the example we expect users to write when defining the claim. We inverted the order in which we do things. The goal was to focus on the end-user experience and let the CLI generate the XRD that should support it.

From now on, we could fine tune it if there are special cases that were not covered. That won’t be necessary for today’s demo, so let’s move on and see how we can write Compositions in a better way. If what we did so far generated even a mild excitement, what’s coming next will be huge.

Crossplane Compositions

There are two important parts to be aware of when defining Compositions. There is the Composition itself or, to be more precise, the boiler-plate code that is necessary for any Composition. Then there is code that defines which managed resources will be handled by a Composition. Let’s see how we can do both, starting with the boiler-plate code by executing up composition generate, pass the path to the XRD we created earlier, and finishing with the --path where the Composition should be created.

up composition generate apis/xsqls/definition.yaml \
    --path apis/xsqls/aws.yaml

The output is as follows.

successfully created Composition and saved to apis/xsqls/google.yaml

Let’s see what we got.

code apis/xsqls/aws.yaml

The output is as follows.

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: xsqls.devopstoolkit.live
spec:
  compositeTypeRef:
    apiVersion: devopstoolkit.live/v1alpha1
    kind: XSQL
  mode: Pipeline
  pipeline:
  - functionRef:
      name: crossplane-contrib-function-status-transformer
    input:
      apiVersion: function-status-transformer.fn.crossplane.io/v1beta1
      kind: StatusTransformation
      metadata: {}
    step: crossplane-contrib-function-status-transformer
  - functionRef:
      name: crossplane-contrib-function-auto-ready
    step: crossplane-contrib-function-auto-ready

That was the boring part that has to be done, yet we did not have to spend any time getting it.

The interesting part is that the Composition we created contains not only the standard parts we always need to have, but it also figured out that, in this case, we want to use the status-transformer and auto-ready functions, so it added those as well.

That was the boring part. The truly interesting stuff comes up next.

We are about to start working on the function that will assemble all managed resources we might want to include into the Composition. We could do that by writing a ton of YAML, but we won’t. That would be silly since using functions is much more efficient and more flexible way to define what we need.

Right now, by the time I’m writing this post, we could use KCL or Python. By the time you’re reading this, additional languages might be supported and, eventually, a ton of them should be available.

Since I prefer KCL and you don’t have a say in it, that’s what we’ll use today, and you should know that’s not the only option if KCL is not your thing.

Here it goes.

We want a function to be generated with the name aws and for the Composition aws.yaml.

up function generate aws apis/xsqls/aws.yaml

The output is as follows.

  ✓   Checking dependencies                                     
  ✓   Generating Function Folder                                
  ✓   Adding Pipeline Step in Composition                       
successfully created Function and saved to /Users/viktorfarcic/code/upbound-devex-demo/silly-demo/functions/aws

That command did quite a few things.

It inspected the dependencies in the project and, based on what we have there, downloaded schemas that will provide us with IntelliSense in VSCode. Through them, we’ll get auto-complete and other nice things we’re used to have when developing something.

On top of that, it skaffolded the files we might need to develop the function that will define all the managed resources we want that Composition to manage.

Let’s see what we got.

ls functions/aws

The output is as follows.

-I kcl.mod
-I kcl.mod.lock
-I main.k
-I model -> ../../../../.up/kcl/models

Since, in this case, I chose to use KCL, it created, among other things, the main.k file which we should modify to suit our needs. Let’s take a look at it.

code functions/aws/main.k

The output is as follows.

import models.v1beta1 as v1beta1
import models.v1beta2 as v1beta2
import models.v1beta3 as v1beta3
import models.k8s.apimachinery.pkg.apis.meta.v1 as metav1

oxr = option("params").oxr # observed composite resource
_ocds = option("params").ocds # observed composed resources
_dxr = option("params").dxr # desired composite resource
dcds = option("params").dcds # desired composed resources

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

_items = [

]
items = _items

We can see that it imported (import) the schemas for all the resources available in the dependencies we added earlier.

Further on, it defined variables like oxr that contains the observed composite resource, _ocds with observed composed resources, as well as _dxr and dcds with desired composite and composed resources. Those might look confusing if you’re not used to working with the KCL function. We’ll see them in action very soon.

The important part is the _items array. That’s where we should add managed resources that should be managed by that Composition. We’ll see how to do that in a moment. For now, let’s see how we can use one of the predefined variables.

Let’s say that we would like to extract the region we said we’ll allow users to specify when creating Claims.

We can do that by defining the variable _region and, for now, setting it to us-east-1.

Next, we’ll create a conditional (if) that checks whether spec, and, inside it, parameters, and, inside it, region is set. Those and all other params are available in oxr as the observed composite resource.

If the region is indeed set, we’re putting it as the value of the _region variable.

The final version should look like the snippet that follows.

...
}

_region = "us-east-1"
if oxr.spec?.parameters?.region:
    _region = oxr.spec.parameters.region

_items = [
...

All that was pretty boring. What comes next is what makes a true difference and we can see it if we add the first resource to the _items array.

Let’s say that we would like to add AWS Internet Gateway. In the past, we would need to find out the spec and start typing or copying and pasting parameters. That’s not the case any more. Now we have IntelliSense and, given that it is version v1beta1, we can type just that followed with dot (.) and… Lo and behold. We got the list of all the resources that belong to v1beta1 and we can just start typing Inter, select InternetGateway, and press the enter key.

Next, we should add metadata following the same process. Type meta, select metadata, press the enter key.

We’ll assign it the value of the _metadata lambda.

Let’s do a few more inside the spec.

Type sp, select spec, type . followed with for, select forProvider and assign it to… It autocompleted it for us.

Inside the provider, we should add the region with whatever the value of the _region variable we defined earlier is.

Let’s also set the vpcIdSelector.matchControlelrRef to True.

The final version should look like the snippet that follows.

If you tried to define a managed resource in the past, you must admit that IntelliSense alone is a massive help.

Let’s do an experiment.

I’ll write the code for all the resources and time it. Let’s see how long that will take.

This is how the final version looks like.

code ../aws.k

The output is as follows.

import models.v1beta1 as v1beta1
import models.v1beta2 as v1beta2
import models.v1beta3 as v1beta3
import models.k8s.apimachinery.pkg.apis.meta.v1 as metav1

oxr = option("params").oxr # observed composite resource
_ocds = option("params").ocds # observed composed resources
_dxr = option("params").dxr # desired composite resource
dcds = option("params").dcds # desired composed resources

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

_region = "us-east-1"
if oxr.spec?.parameters?.region:
    _region = oxr.spec.parameters.region

_items = [
    v1beta1.InternetGateway {
        metadata = _metadata("gateway")
        spec.forProvider = v1beta1.Ec2AwsUpboundIoV1beta1InternetGatewaySpecForProvider{
            region = _region
            vpcIdSelector.matchControllerRef = True
        }
    },
    v1beta1.MainRouteTableAssociation {
        metadata = _metadata("mainRouteTableAssociation")
        spec.forProvider = v1beta1.Ec2AwsUpboundIoV1beta1MainRouteTableAssociationSpecForProvider{
            region = _region
            routeTableIdSelector.matchControllerRef = True
            vpcIdSelector.matchControllerRef = True
        }
    },
    v1beta1.RouteTable {
        metadata = _metadata("routeTable")
        spec.forProvider = v1beta1.Ec2AwsUpboundIoV1beta1RouteTableSpecForProvider{
            region = _region
            vpcIdSelector.matchControllerRef = True
        }
    },
    v1beta1.Route {
        metadata = _metadata("route")
        spec.forProvider = v1beta1.Ec2AwsUpboundIoV1beta1RouteSpecForProvider{
            region = _region
            routeTableIdSelector.matchControllerRef = True
            destinationCidrBlock = "0.0.0.0/0"
            gatewayIdSelector.matchControllerRef = True
        }
    },
    v1beta1.SecurityGroupRule {
        metadata = _metadata("securityGroupRule")
        spec.forProvider = v1beta1.Ec2AwsUpboundIoV1beta1SecurityGroupRuleSpecForProvider{
            region = _region
            description = "I am too lazy to write descriptions"
            type = "ingress"
            fromPort = 5432
            toPort = 5432
            protocol = "tcp"
            cidrBlocks = ["0.0.0.0/0"]
            securityGroupIdSelector.matchControllerRef = True
        }
    },
    v1beta1.SecurityGroup {
        metadata = _metadata("securityGroup")
        spec.forProvider = v1beta1.Ec2AwsUpboundIoV1beta1SecurityGroupSpecForProvider{
            region = _region
            description = "I am too lazy to write descriptions"
            vpcIdSelector.matchControllerRef = True
        }
    },
    v1beta1.VPC {
        metadata = _metadata("vpc")
        spec.forProvider = v1beta1.Ec2AwsUpboundIoV1beta1VPCSpecForProvider{
            region = _region
            cidrBlock = "11.0.0.0/16"
            enableDnsSupport = True
            enableDnsHostnames = True
        }
    },
    v1beta1.SubnetGroup {
        metadata = _metadata("subnetgroup")
        spec.forProvider = v1beta1.RdsAwsUpboundIoV1beta1SubnetGroupSpecForProvider{
            region = _region
            description = "I'm too lazy to write a good description"
            subnetIdSelector.matchControllerRef = True
        }
    },
    v1beta2.Instance {
        metadata = _metadata("rdsinstance")
        spec.forProvider = v1beta2.RdsAwsUpboundIoV1beta2InstanceSpecForProvider{
            region = _region
            dbSubnetGroupNameSelector.matchControllerRef = True
            vpcSecurityGroupIdSelector.matchControllerRef = True
            username = "masteruser"
            engine = "postgres"
            skipFinalSnapshot = True
            publiclyAccessible = True
            allocatedStorage = 200
            passwordSecretRef = v1beta2.RdsAwsUpboundIoV1beta2InstanceSpecForProviderPasswordSecretRef {
                name = oxr.spec.id + "-password"
                namespace = oxr.spec.claimRef.namespace
                key = "password"
            }
            identifier = oxr.spec.id
            if oxr.spec.parameters.size == "small":
                instanceClass = "db.m5.large"
            elif oxr.spec.parameters.size == "medium":
                instanceClass = "db.m5.2xlarge"
            else:
                instanceClass = "db.m5.8xlarge"
            engineVersion = oxr.spec.parameters.version
        }
    },
    {
        **oxr
        if "rdsinstance" in _ocds:
            status.address = _ocds["rdsinstance"].Resource.status.atProvider.address
    }
]

_zoneList = [
    { zone = "a", cidrBlock = "11.0.0.0/24" },
    { zone = "b", cidrBlock = "11.0.1.0/24"  },
    { zone = "c", cidrBlock = "11.0.2.0/24"  }
]

_routeTableAssociations = [
    v1beta1.RouteTableAssociation {
        metadata = {
            name = oxr.spec.id + "-1" + _data.zone
            annotations = {
                "krm.kcl.dev/composition-resource-name" = "routeTableAssociation1" + _data.zone
            }
        }
        spec.forProvider = v1beta1.Ec2AwsUpboundIoV1beta1RouteTableAssociationSpecForProvider{
            region = _region
            routeTableIdSelector.matchControllerRef = True
            subnetIdSelector = v1beta1.Ec2AwsUpboundIoV1beta1RouteTableAssociationSpecForProviderSubnetIDSelector{
                matchControllerRef = True
                matchLabels = { zone = _region + _data.zone }
            }
        }
    } for _data in _zoneList
]

_subnets = [
    v1beta1.Subnet {
        metadata = {
            name = oxr.spec.id + "-" + _data.zone
            annotations = {
                "krm.kcl.dev/composition-resource-name" = "subnet-" + _data.zone
            }
            labels = { zone = _region + _data.zone }
        }
        spec.forProvider = v1beta1.Ec2AwsUpboundIoV1beta1SubnetSpecForProvider{
            region = _region
            availabilityZone = _region + _data.zone
            cidrBlock = _data.cidrBlock
            vpcIdSelector.matchControllerRef = True
        }
    } for _data in _zoneList
]

_objects = [{
    apiVersion = "kubernetes.crossplane.io/v1alpha2"
    kind = "Object"
    metadata = {
        name = oxr.spec.id + "-secret"
        annotations = {
            "krm.kcl.dev/ready": "True"
            "krm.kcl.dev/composition-resource-name" = "sql-secret"
        }
    }
    spec = {
        references = [{
            patchesFrom = {
                apiVersion = "rds.aws.upbound.io/v1beta1"
                kind = "Instance"
                name = oxr.spec.id
                namespace = "crossplane-system"
                fieldPath = "spec.forProvider.username"
            }
            toFieldPath = "stringData.username"
        }, {
            patchesFrom = {
                apiVersion = "v1"
                kind = "Secret"
                name = oxr.spec.id + "-password"
                namespace = oxr.spec.claimRef.namespace
                fieldPath = "data.password"
            }
            toFieldPath = "data.password"
        }, {
            patchesFrom = {
                apiVersion = "rds.aws.upbound.io/v1beta1"
                kind = "Instance"
                name = oxr.spec.id
                namespace = "crossplane-system"
                fieldPath = "status.atProvider.address"
            }
            toFieldPath = "stringData.endpoint"
        }]
        forProvider.manifest = {
            apiVersion = "v1"
            kind = "Secret"
            metadata = {
                name = oxr.spec.id
                namespace = oxr.spec.claimRef.namespace
            }
            data.port = "NTQzMg=="
        }
        providerConfigRef.name = oxr.spec.id + "-sql"
    }
}]

items = _items + _routeTableAssociations + _subnets + _objects

I wrote all that in more or less, 15 minutes. In the past I would probably spend a few hours to accomplish the same result. Isn’t that awesome?

Now, let’s say that we’re finished developing the Composition. The next thing we should do is build and publish the whole project.

Build and Push Projects

All that’s left is to build,…

up project build

…and push it to the registry as version v0.0.1.

up project push --tag v0.0.1

That’s it. The whole project is now published as an OCI image that contains everything we might need to run it in a control plane, and this was the shortest section I ever put into a video.

This Is NOT The End

There are many other goodies coming or, by the time you read this, already available. We should be able to create ephemeral control planes where we could test our Compositions before publishing them. We could have created a permanent production-ready control plane where we would run the Composition we built and allow our users to create claims. We should be able to see the analysis of the changes before applying them. We could have automated the whole process with Workflows like, for example, GitHub Actions or Argo Workflows or Jenkins or whatever we might be using. There are many other things we could have done, and even more that’s coming soon under the umbrella of developer experience.

However, we’re done for today. I’ll explore more in upcoming posts. In the meantime, visit Upbound and check out the new features yourself.

Destroy

up repository delete silly-demo --force

up repository delete silly-demo_aws --force

exit