Skip to content

Creating Solutions#

This guide walks through building a complete solution — from defining the API schema to making it available in consumer project workspaces.


Prerequisites#

  • infrastructure.stakater.com API available
  • Crossplane installed with kubernetes-provider, kcp-provider, and helm-provider configured
  • Target operator installed on the management cluster (e.g., CloudNativePG for a PostgreSQL solution)

Overview#

Creating a solution involves four steps:

  1. Define the XRD — the API schema consumers interact with
  2. Write the Composition — the implementation that fulfils claims
  3. Test locally — validate with a test claim before publishing
  4. Publish — expose the API to consumer project workspaces

Step 1: Define the XRD#

The CompositeResourceDefinition declares the API your solution exposes. It defines what fields consumers configure, what validation rules apply, and what defaults are set.

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xpostgresqldatabases.databases.cloud.stakater.com
spec:
  scope: LegacyCluster
  group: databases.cloud.stakater.com
  names:
    kind: XPostgreSQLDatabase
    plural: xpostgresqldatabases
  claimNames:
    kind: PostgreSQLDatabase
    plural: postgresqldatabases
  versions:
    - name: v1
      served: true
      referenceable: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                providerConfigsRef:
                  type: object
                  properties:
                    kubernetes:
                      type: string
                      default: kubernetes-provider
                parameters:
                  type: object
                  required: [dbName]
                  properties:
                    dbName:
                      type: string
                      description: Name of the database instance
                    version:
                      type: string
                      default: "16"
                      description: PostgreSQL major version
                      enum: ["14", "15", "16"]
                    storageGb:
                      type: integer
                      default: 20
                      description: Storage size in GiB
                    instances:
                      type: integer
                      default: 1
                      description: Number of PostgreSQL instances (1 = standalone, 3 = HA)
            status:
              type: object
              properties:
                endpoint:
                  type: string
                  description: Connection endpoint for the database
                port:
                  type: integer

Apply the XRD to the management cluster:

kubectl apply -f xrd.yaml

Crossplane validates the XRD and makes the claim type available.


Step 2: Write the Composition#

The Composition implements the XRD — it defines what infrastructure to create when a consumer applies a PostgreSQLDatabase claim.

SCO supports any Crossplane pipeline function — use whichever language you are most comfortable with. The same composition is shown below in three languages. See Crossplane Compositions for patterns and examples.

Using function-kcl (KCL)#

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  labels:
    crossplane.io/xrd: xpostgresqldatabases.databases.cloud.stakater.com
  name: postgresql-database
spec:
  compositeTypeRef:
    apiVersion: databases.cloud.stakater.com/v1
    kind: XPostgreSQLDatabase
  mode: Pipeline
  pipeline:
    - step: kcl
      functionRef:
        name: function-kcl
      input:
        apiVersion: krm.kcl.dev/v1alpha1
        kind: KCLInput
        spec:
          source: |
            oxr = option("params").oxr

            spec = oxr.spec
            parameters = spec.parameters
            kubernetesProvider = spec?.providerConfigsRef?.kubernetes or "kubernetes-provider"

            dbName = parameters.dbName
            instances = parameters?.instances or 1
            storageGb = parameters?.storageGb or 20
            targetNamespace = spec.claimRef.namespace

            items = [
                {
                    apiVersion: "kubernetes.crossplane.io/v1alpha2"
                    kind: "Object"
                    metadata: {
                        name: dbName
                        annotations: {
                            "krm.kcl.dev/composition-resource-name": "postgresql-cluster"
                        }
                    }
                    spec: {
                        providerConfigRef: {name: kubernetesProvider}
                        forProvider: {
                            manifest: {
                                apiVersion: "postgresql.cnpg.io/v1"
                                kind: "Cluster"
                                metadata: {name: dbName, namespace: targetNamespace}
                                spec: {
                                    instances: instances
                                    postgresql: {parameters: {max_connections: "200"}}
                                    bootstrap: {initdb: {database: dbName, owner: dbName}}
                                    storage: {size: "{}Gi".format(storageGb)}
                                }
                            }
                        }
                    }
                }
            ]

    - step: automatically-detect-ready-composed-resources
      functionRef:
        name: function-auto-ready

Using function-go-templating (Go templates)#

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  labels:
    crossplane.io/xrd: xpostgresqldatabases.databases.cloud.stakater.com
  name: postgresql-database
spec:
  compositeTypeRef:
    apiVersion: databases.cloud.stakater.com/v1
    kind: XPostgreSQLDatabase
  mode: Pipeline
  pipeline:
    - step: go-templates
      functionRef:
        name: function-go-templating
      input:
        apiVersion: gotemplating.fn.crossplane.io/v1beta1
        kind: GoTemplate
        source: Inline
        inline:
          template: |
            {{- $params := .observed.composite.resource.spec.parameters }}
            {{- $spec := .observed.composite.resource.spec }}
            {{- $dbName := $params.dbName }}
            {{- $instances := default 1 $params.instances }}
            {{- $storageGb := default 20 $params.storageGb }}
            ---
            apiVersion: kubernetes.crossplane.io/v1alpha2
            kind: Object
            metadata:
              name: {{ $dbName }}
              annotations:
                gotemplating.fn.crossplane.io/composition-resource-name: postgresql-cluster
            spec:
              providerConfigRef:
                name: {{ default "kubernetes-provider" $spec.providerConfigsRef.kubernetes }}
              forProvider:
                manifest:
                  apiVersion: postgresql.cnpg.io/v1
                  kind: Cluster
                  metadata:
                    name: {{ $dbName }}
                    namespace: {{ $spec.claimRef.namespace }}
                  spec:
                    instances: {{ $instances }}
                    postgresql:
                      parameters:
                        max_connections: "200"
                    bootstrap:
                      initdb:
                        database: {{ $dbName }}
                        owner: {{ $dbName }}
                    storage:
                      size: {{ $storageGb }}Gi

    - step: automatically-detect-ready-composed-resources
      functionRef:
        name: function-auto-ready

Using function-pythonic (Python)#

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  labels:
    crossplane.io/xrd: xpostgresqldatabases.databases.cloud.stakater.com
  name: postgresql-database
spec:
  compositeTypeRef:
    apiVersion: databases.cloud.stakater.com/v1
    kind: XPostgreSQLDatabase
  mode: Pipeline
  pipeline:
    - step: python
      functionRef:
        name: function-pythonic
      input:
        apiVersion: pythonic.fn.crossplane.io/v1beta1
        kind: PythonInput
        spec:
          inline: |
            from crossplane.function import resource
            from crossplane.function.proto.v1 import run_function_pb2 as fnv1

            def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse):
                params = req.observed.composite.resource["spec"]["parameters"]
                spec = req.observed.composite.resource["spec"]

                db_name = params["dbName"]
                instances = params.get("instances", 1)
                storage_gb = params.get("storageGb", 20)
                target_namespace = spec["claimRef"]["namespace"]
                k8s_provider = spec.get("providerConfigsRef", {}).get(
                    "kubernetes", "kubernetes-provider"
                )

                resource.update(rsp.desired.resources["postgresql-cluster"], {
                    "apiVersion": "kubernetes.crossplane.io/v1alpha2",
                    "kind": "Object",
                    "metadata": {"name": db_name},
                    "spec": {
                        "providerConfigRef": {"name": k8s_provider},
                        "forProvider": {
                            "manifest": {
                                "apiVersion": "postgresql.cnpg.io/v1",
                                "kind": "Cluster",
                                "metadata": {
                                    "name": db_name,
                                    "namespace": target_namespace,
                                },
                                "spec": {
                                    "instances": instances,
                                    "postgresql": {
                                        "parameters": {"max_connections": "200"}
                                    },
                                    "bootstrap": {
                                        "initdb": {
                                            "database": db_name,
                                            "owner": db_name,
                                        }
                                    },
                                    "storage": {"size": f"{storage_gb}Gi"},
                                },
                            }
                        },
                    },
                })

    - step: automatically-detect-ready-composed-resources
      functionRef:
        name: function-auto-ready

Apply the Composition:

kubectl apply -f composition.yaml

Step 3: Test Locally#

Before publishing, validate the solution with a test claim on the management cluster:

apiVersion: databases.cloud.stakater.com/v1
kind: PostgreSQLDatabase
metadata:
  name: test-db
spec:
  parameters:
    dbName: myapp
    version: "16"
    storageGb: 10
    instances: 1
kubectl apply -f test-claim.yaml
kubectl get postgresqldatabase test-db
kubectl describe postgresqldatabase test-db

Watch for Ready: True in the status conditions. If the claim gets stuck, inspect the composed resources:

kubectl get managed -l crossplane.io/claim-name=test-db

Remove the test claim before publishing:

kubectl delete postgresqldatabase test-db

Step 4: Publish to Consumer Projects#

Once the XRD and Composition are verified, publish the API to consumer project workspaces using a PublishedOffering claim:

apiVersion: infrastructure.stakater.com/v1alpha1
kind: PublishedOffering
metadata:
  name: databases-service
spec:
  providerConfigRef:
    name: kubernetes-provider
  kcpProviderConfigRef:
    name: kcp-provider
  helmProviderConfigRef:
    name: helm-provider
  parameters:
    apiSyncagent:
      namespace: kcp-system
    exports:
      - apiGroup: databases.cloud.stakater.com
        resources:
          - resource:
              kind: PostgreSQLDatabase
              plural: postgresqldatabases
              version: v1
            namespaceSuffix: postgresql
kubectl apply -f published-offering.yaml

The PostgreSQLDatabase claim type is now available in all consumer project workspaces. See Publishing APIs for the full publishing guide.


What's Next?#