Crossplane providers and compositions are essentially just Kubernetes Custom Resources, and testing them end-to-end means running your actual Crossplane control plane against a real Kubernetes cluster and watching it provision and manage cloud resources.

Here’s a breakdown of how to set up those tests, focusing on a practical, repeatable approach.


The Core Idea: Treat Crossplane Like Any Other Application

At its heart, Crossplane is a Kubernetes operator. Your providers are also operators, and your Compositions are just CRDs that define how Crossplane should behave. Therefore, testing them end-to-end means deploying Crossplane, its providers, and your custom resources to a Kubernetes cluster and verifying that the intended cloud resources are created and managed as expected.

Setting Up Your Test Environment

You need a Kubernetes cluster. For local development, kind (Kubernetes in Docker) is excellent. For CI/CD, a managed Kubernetes service like GKE, EKS, or AKS is more representative.

  1. Provision a Kubernetes Cluster:

    • kind (Local):
      kind create cluster --name crossplane-test --config - <<EOF
      kind: Cluster
      apiVersion: kind.x-k8s.io/v1alpha4
      nodes:
      - role: control-plane
      EOF
      
      This spins up a lightweight Kubernetes cluster entirely within Docker containers. It’s fast and isolated.
    • Managed Kubernetes (CI/CD): Use your cloud provider’s CLI (e.g., gcloud container clusters create, aws eks create-cluster, az aks create) to provision a cluster. Ensure it has enough resources for Crossplane, your providers, and the resources they manage.
  2. Install kubectl and helm: These are essential for interacting with your cluster. Make sure they are in your PATH.

  3. Install Crossplane: You can install Crossplane using Helm. This is the most common and recommended way.

    helm install crossplane crossplane-stable/crossplane \
      --namespace crossplane-system \
      --create-namespace \
      --version 1.15.0 # Use a specific, stable version
    

    This deploys the Crossplane core components into the crossplane-system namespace.

  4. Install Your Provider(s): Providers are also installed as Custom Resources. You can apply their YAML manifests directly or use Helm charts if available.

    For example, to install the AWS provider:

    kubectl apply -f https://raw.githubusercontent.com/crossplane/provider-aws/v0.55.0/package/crossplane.yaml
    

    (Note: Always check the latest provider version and its installation method on its respective GitHub repository.)

  5. Configure Provider Credentials: This is critical. Your provider needs credentials to talk to the cloud API. This is done by creating a ProviderConfig custom resource. The exact configuration depends on the provider.

    For AWS, you’d typically create a Kubernetes Secret containing your AWS credentials and then reference it in the ProviderConfig:

    # Example AWS ProviderConfig
    apiVersion: aws.crossplane.io/v1beta1
    kind: ProviderConfig
    metadata:
      name: default # or any name you prefer
    spec:
      credentials:
        source: Secret
        secretRef:
          namespace: crossplane-system
          name: aws-credentials # This secret must exist and contain AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
    

    You’d create the aws-credentials secret like this:

    kubectl create secret generic aws-credentials \
      --from-literal=AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY \
      --from-literal=AWS_SECRET_ACCESS_KEY=YOUR_SECRET_KEY \
      --namespace crossplane-system
    

    Crucially, ensure these credentials have the necessary permissions to create and manage the cloud resources your Compositions will define.

Writing and Applying Your Compositions and Claims

Now you write your Compositions and Claims, just as you would for any Crossplane deployment.

  1. Define Your Composition: This is a Kubernetes Custom Resource that tells Crossplane how to provision cloud resources from a set of managed resources.

    # Example Composition for a managed PostgreSQL instance
    apiVersion: apiextensions.crossplane.io/v1
    kind: Composition
    metadata:
      name: managedpostgresqls.mycompany.com
      labels:
        testing: true # Label to easily select this composition
    spec:
      compositeTypeRef:
        apiVersion: example.mycompany.com/v1alpha1
        kind: ManagedPostgreSQL
      resources:
        - name: postgresql-instance
          base:
            apiVersion: rds.aws.upbound.io/v1beta1 # Using upbound provider for simplicity here
            kind: DBInstance
            spec:
              forProvider:
                region: us-east-1
                dbInstanceClass: db.t3.micro
                engine: postgres
                engineVersion: "14.5"
              # Note: The ProviderConfig reference is implicitly handled by Crossplane
              # if there's a single ProviderConfig that matches the provider.
              # Explicitly, it would look like:
              # providerConfigRef:
              #   name: default
          patches:
            - fromFieldPath: spec.parameters.storageGB
              toFieldPath: spec.forProvider.allocatedStorage
            - fromFieldPath: spec.parameters.version
              toFieldPath: spec.forProvider.engineVersion
    
  2. Define Your Composite Resource Definition (XRD): This defines the schema for your composite resource (e.g., ManagedPostgreSQL).

    apiVersion: apiextensions.crossplane.io/v1
    kind: CompositeResourceDefinition
    metadata:
      name: managedpostgresqls.example.mycompany.com
    spec:
      group: example.mycompany.com
      names:
        kind: ManagedPostgreSQL
        plural: managedpostgresqls
      claimNames:
        kind: PostgreSQL
        plural: postgresqls
      versions:
        - name: v1alpha1
          served: true
          referenceable: true
          schema:
            openAPIV3Schema:
              type: object
              properties:
                spec:
                  type: object
                  properties:
                    parameters:
                      type: object
                      properties:
                        storageGB:
                          type: integer
                        version:
                          type: string
                  required:
                    - parameters
    
  3. Apply Your XRD and Composition: Apply these to your cluster.

    kubectl apply -f your-xrd.yaml
    kubectl apply -f your-composition.yaml
    
  4. Create a Claim: This is what your application developers would create. It references the claim kind defined in your XRD and specifies the desired parameters.

    # Example Claim for a PostgreSQL instance
    apiVersion: example.mycompany.com/v1alpha1
    kind: PostgreSQL
    metadata:
      name: my-app-db
    spec:
      parameters:
        storageGB: 20
        version: "14.5"
      compositionSelector: # Optional, but good for testing
        matchLabels:
          testing: true
    

    Apply this claim:

    kubectl apply -f your-claim.yaml
    

Verification and Assertions

This is where the "end-to-end" part truly shines. You’re not just checking if Crossplane thinks it did something; you’re checking if the cloud resource actually exists.

  1. Check Kubernetes Status: First, verify that Crossplane has created the managed resources and that they are in a Ready state.

    # Check the claim
    kubectl get managedpostgresqls my-app-db -o yaml
    
    # Check the composite resource (created by Crossplane from the claim)
    kubectl get postgresqls.example.mycompany.com my-app-db -o yaml
    
    # Check the actual managed resource (e.g., AWS RDS DBInstance)
    kubectl get dbinstances.rds.aws.upbound.io my-app-db-postgresql-instance -n crossplane-system -o yaml
    

    Look for status.atProvider.dbInstanceStatus: available or similar indicators of success in the managed resource’s status.

  2. Check Cloud Provider Console/CLI: This is the ultimate validation. Log into your AWS, GCP, or Azure console and verify that the actual database instance, storage bucket, or Kubernetes cluster has been provisioned with the correct settings.

    For AWS RDS:

    aws rds describe-db-instances --db-instance-identifier <your-db-instance-id> --query "DBInstances[0].DBInstanceStatus"
    

    The output should be available.

  3. Testing Updates and Deletions:

    • Updates: Modify your Claim (e.g., change storageGB) and re-apply. Verify that the Kubernetes status updates and that the cloud resource reflects the changes.
    • Deletions: Delete the Claim. Verify that the Kubernetes managed resources are cleaned up and that the cloud resource is also deleted.
    kubectl delete managedpostgresqls my-app-db
    # Then check that the DBInstance CR and the actual RDS instance are gone.
    

The Secret Sauce: Dynamic Resource Management

What’s often overlooked is how Crossplane’s reconciliation loop continuously monitors the desired state (defined in your Claims and Compositions) against the actual state in the cloud. When you apply a Claim, Crossplane:

  1. Finds a matching Composition (based on compositeTypeRef and compositionSelector).
  2. Uses the Composition’s resources and patches to generate the necessary Kubernetes Custom Resources for managed resources (e.g., DBInstance).
  3. The Provider (e.g., provider-aws) watches these managed resource CRs.
  4. The Provider interacts with the cloud API (AWS RDS API in this case) to create, update, or delete the actual cloud resource.
  5. It then watches the cloud resource’s state and updates the Kubernetes managed resource’s status field.
  6. Crossplane propagates this status back up to your Composite resource (and ultimately, your Claim).

This continuous loop means your tests should not just check for immediate creation but also for eventual consistency and responsiveness to changes.

The next step in your testing journey is likely to explore testing Crossplane’s built-in policies for compliance and governance.

Want structured learning?

Take the full Crossplane course →