Skip to content

vanHeemstraSystems/learning-crossplane-test-driven-development

Repository files navigation

Learning Crossplane Test-Driven Development

A systematic approach to testing Crossplane infrastructure-as-code using the Fast Food Restaurant metaphor

.github/workflows/ci.yml Crossplane Kubernetes License

πŸ“š Related Repositories

This repository is part of a learning series:

🎯 Purpose

Learn how to apply Test-Driven Development (TDD) principles to Crossplane infrastructure provisioning, ensuring reliable, maintainable, and well-tested infrastructure-as-code. We use the Fast Food Restaurant metaphor from this article to make concepts easier to understand.

πŸ” The Fast Food Metaphor

Crossplane Concept Fast Food Metaphor What We Test
XRD Menu Board Schema validation, API contract
Composition Recipe Card Resource creation, patching logic
Claim Customer Order Input validation, user experience
Composite Resource (XR) Completed Meal End-to-end integration
Provider Kitchen Station Provider configuration, credentials
Managed Resource Food Items (burger, fries) Individual resource properties

πŸ—οΈ Repository Structure

learning-crossplane-test-driven-development/
β”œβ”€β”€ README.md                           # This file
β”œβ”€β”€ docs/
β”‚   β”œβ”€β”€ 01-tdd-principles.md           # TDD fundamentals for infrastructure
β”‚   β”œβ”€β”€ 02-testing-strategy.md         # Overall testing approach
β”‚   β”œβ”€β”€ 03-tooling.md                  # Tools and their usage
β”‚   └── 04-best-practices.md           # Patterns and anti-patterns
β”‚
β”œβ”€β”€ environments/
β”‚   β”œβ”€β”€ minikube/
β”‚   β”‚   β”œβ”€β”€ setup.sh                   # Minikube cluster setup
β”‚   β”‚   β”œβ”€β”€ crossplane-install.sh      # Crossplane installation
β”‚   β”‚   └── provider-install.sh        # Azure providers
β”‚   └── kind/                          # Alternative: kind cluster
β”‚       └── setup.sh
β”‚
β”œβ”€β”€ scripts/
β”‚   β”œβ”€β”€ generate/                      # YQ-based YAML generators
β”‚   β”‚   β”œβ”€β”€ generate-xrd.sh
β”‚   β”‚   β”œβ”€β”€ generate-composition.sh
β”‚   β”‚   └── generate-claim.sh
β”‚   β”œβ”€β”€ validate/                      # Pre-deployment validation
β”‚   β”‚   β”œβ”€β”€ validate-schema.sh
β”‚   β”‚   β”œβ”€β”€ validate-policies.sh
β”‚   β”‚   └── validate-with-conftest.sh
β”‚   └── test/                          # Test execution scripts
β”‚       β”œβ”€β”€ run-unit-tests.sh
β”‚       β”œβ”€β”€ run-integration-tests.sh
β”‚       └── run-e2e-tests.sh
β”‚
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ unit/                          # Unit-level tests
β”‚   β”‚   β”œβ”€β”€ xrd/
β”‚   β”‚   β”‚   β”œβ”€β”€ test-schema-validation.yaml
β”‚   β”‚   β”‚   └── test-required-fields.yaml
β”‚   β”‚   β”œβ”€β”€ composition/
β”‚   β”‚   β”‚   β”œβ”€β”€ test-patching-logic.yaml
β”‚   β”‚   β”‚   └── test-resource-naming.yaml
β”‚   β”‚   └── policies/
β”‚   β”‚       β”œβ”€β”€ deletion-policy.rego
β”‚   β”‚       β”œβ”€β”€ naming-convention.rego
β”‚   β”‚       └── security-baseline.rego
β”‚   β”‚
β”‚   β”œβ”€β”€ integration/                   # Integration tests
β”‚   β”‚   β”œβ”€β”€ developer-combo/
β”‚   β”‚   β”‚   β”œβ”€β”€ test-small-combo.yaml
β”‚   β”‚   β”‚   β”œβ”€β”€ test-medium-combo.yaml
β”‚   β”‚   β”‚   └── test-large-combo.yaml
β”‚   β”‚   └── assertions/
β”‚   β”‚       β”œβ”€β”€ assert-resources-created.yaml
β”‚   β”‚       └── assert-status-conditions.yaml
β”‚   β”‚
β”‚   └── e2e/                           # End-to-end tests
β”‚       β”œβ”€β”€ scenarios/
β”‚       β”‚   β”œβ”€β”€ full-lifecycle.yaml    # Create β†’ Update β†’ Delete
β”‚       β”‚   β”œβ”€β”€ multi-environment.yaml # Dev β†’ Staging β†’ Prod
β”‚       β”‚   └── failure-recovery.yaml  # Error handling
β”‚       └── chainsaw/
β”‚           └── developer-combo-test.yaml
β”‚
β”œβ”€β”€ crossplane/
β”‚   β”œβ”€β”€ providers/                     # Provider configurations
β”‚   β”‚   β”œβ”€β”€ provider-azure-storage.yaml
β”‚   β”‚   β”œβ”€β”€ provider-azure-sql.yaml
β”‚   β”‚   β”œβ”€β”€ provider-azure-network.yaml
β”‚   β”‚   └── providerconfig-azure.yaml
β”‚   β”‚
β”‚   β”œβ”€β”€ xrds/                          # Composite Resource Definitions (The Menu)
β”‚   β”‚   β”œβ”€β”€ developer-combo/
β”‚   β”‚   β”‚   β”œβ”€β”€ xrd.yaml
β”‚   β”‚   β”‚   └── examples/
β”‚   β”‚   β”‚       β”œβ”€β”€ small-claim.yaml
β”‚   β”‚   β”‚       β”œβ”€β”€ medium-claim.yaml
β”‚   β”‚   β”‚       └── large-claim.yaml
β”‚   β”‚   └── application-stack/
β”‚   β”‚       └── xrd.yaml
β”‚   β”‚
β”‚   β”œβ”€β”€ compositions/                  # Compositions (The Recipes)
β”‚   β”‚   β”œβ”€β”€ developer-combo/
β”‚   β”‚   β”‚   β”œβ”€β”€ azure-composition.yaml
β”‚   β”‚   β”‚   └── README.md
β”‚   β”‚   └── application-stack/
β”‚   β”‚       └── composition.yaml
β”‚   β”‚
β”‚   └── claims/                        # Example claims (Customer Orders)
β”‚       β”œβ”€β”€ dev/
β”‚       β”‚   └── myapp-dev-combo.yaml
β”‚       β”œβ”€β”€ staging/
β”‚       β”‚   └── myapp-staging-combo.yaml
β”‚       └── prod/
β”‚           └── myapp-prod-combo.yaml
β”‚
β”œβ”€β”€ templates/                         # YQ templates for generation
β”‚   β”œβ”€β”€ xrd-template.yaml
β”‚   β”œβ”€β”€ composition-template.yaml
β”‚   └── claim-template.yaml
β”‚
└── tools/
    β”œβ”€β”€ conftest/
    β”‚   └── policy/
    β”‚       └── crossplane.rego
    β”œβ”€β”€ kyverno/
    β”‚   └── policies/
    β”‚       └── crossplane-policies.yaml
    └── chainsaw/
        └── config.yaml

πŸš€ Quick Start

Prerequisites

  • Minikube v1.32+ (or kind/k3d)
  • kubectl v1.28+
  • yq v4.35+
  • helm v3.12+
  • conftest (for policy testing)
  • crossplane CLI (optional but recommended)
  • Azure CLI (for Azure provider)

1. Setup Environment

# Clone this repository
git clone https://github.com/vanHeemstraSystems/learning-crossplane-test-driven-development.git
cd learning-crossplane-test-driven-development

# Start Minikube cluster
./environments/minikube/setup.sh

# Install Crossplane
./environments/minikube/crossplane-install.sh

# Install Azure providers
./environments/minikube/provider-install.sh

2. Configure Azure Credentials

# Create Azure Service Principal
az ad sp create-for-rbac \
  --name crossplane-sp \
  --role Contributor \
  --scopes /subscriptions/YOUR_SUBSCRIPTION_ID

# Create Kubernetes secret
kubectl create secret generic azure-credentials \
  -n crossplane-system \
  --from-literal=credentials='{"clientId": "your-client-id",
  "clientSecret": "your-client-secret",
  "subscriptionId": "your-subscription-id",
  "tenantId": "your-tenant-id"}'

# Apply ProviderConfig
kubectl apply -f crossplane/providers/providerconfig-azure.yaml

3. Run Your First Test

# Validate XRD schema
./scripts/validate/validate-schema.sh crossplane/xrds/developer-combo/xrd.yaml

# Run policy tests
./scripts/validate/validate-policies.sh

# Deploy and test a Developer Combo
kubectl apply -f crossplane/xrds/developer-combo/xrd.yaml
kubectl apply -f crossplane/compositions/developer-combo/azure-composition.yaml
kubectl apply -f crossplane/xrds/developer-combo/examples/small-claim.yaml

# Watch the reconciliation
kubectl get developercombo --watch

πŸ§ͺ Testing Strategy

Level 1: Schema Validation (Pre-deployment)

What: Validate YAML structure and OpenAPI schemas When: Before committing to Git Tools: yq, kubectl --dry-run, kubeconform

# Example: Validate XRD schema
yq eval '.spec.versions[0].schema.openAPIV3Schema' \
  crossplane/xrds/developer-combo/xrd.yaml | \
  kubeconform -schema-location default -strict

Level 2: Policy Testing (Pre-deployment)

What: Enforce organizational standards and best practices When: In CI/CD pipeline before deployment Tools: conftest (OPA), kyverno

# Example: Test deletion policy
conftest test crossplane/compositions/developer-combo/azure-composition.yaml \
  -p tools/conftest/policy/crossplane.rego

Level 3: Integration Testing (Live cluster)

What: Deploy to test cluster and verify resource creation When: In ephemeral test environment Tools: chainsaw, kuttl, custom scripts

# Example: Run integration test
./scripts/test/run-integration-tests.sh developer-combo small

Level 4: End-to-End Testing (Live cluster)

What: Full lifecycle testing (create β†’ update β†’ delete) When: Before promoting to production Tools: chainsaw, custom test scenarios

# Example: Run E2E test
./scripts/test/run-e2e-tests.sh tests/e2e/scenarios/full-lifecycle.yaml

πŸ› οΈ TDD Workflow

Red β†’ Green β†’ Refactor

1. RED: Write a Failing Test

# Create a policy test that should fail
cat > tests/unit/policies/require-deletion-policy.rego <<EOF
package crossplane.composition

deny[msg] {
  input.kind == "Composition"
  resource := input.spec.resources[_]
  not resource.base.spec.deletionPolicy
  msg := sprintf("Resource %s missing deletionPolicy", [resource.name])
}
EOF

# Run test (should fail)
conftest test crossplane/compositions/developer-combo/azure-composition.yaml \
  -p tests/unit/policies/
# Output: FAIL - Resource database missing deletionPolicy

2. GREEN: Make the Test Pass

# Update composition using yq
yq eval '.spec.pipeline[0].input.resources[] |= 
  .base.spec.deletionPolicy = "Delete"' \
  -i crossplane/compositions/developer-combo/azure-composition.yaml

# Run test again (should pass)
conftest test crossplane/compositions/developer-combo/azure-composition.yaml \
  -p tests/unit/policies/
# Output: PASS

3. REFACTOR: Improve the Implementation

# Extract to reusable yq function
cat > scripts/generate/lib/add-deletion-policy.yq <<EOF
.spec.pipeline[0].input.resources[] |= 
  .base.spec.deletionPolicy = "Delete"
EOF

# Use in generation script
yq eval -f scripts/generate/lib/add-deletion-policy.yq \
  templates/composition-template.yaml

πŸ“ Using YQ for YAML Generation

Generate XRD

#!/bin/bash
# scripts/generate/generate-xrd.sh

COMBO_NAME=$1
API_GROUP="example.com"

yq eval '
  .metadata.name = "x'${COMBO_NAME}'.${API_GROUP}" |
  .spec.group = "${API_GROUP}" |
  .spec.names.kind = "X'${COMBO_NAME^}'" |
  .spec.claimNames.kind = "'${COMBO_NAME^}'"
' templates/xrd-template.yaml > "crossplane/xrds/${COMBO_NAME}/xrd.yaml"

echo "βœ… Generated XRD: crossplane/xrds/${COMBO_NAME}/xrd.yaml"

Generate Composition with Validation

#!/bin/bash
# scripts/generate/generate-composition.sh

COMBO_NAME=$1
PROVIDER=$2

# Generate base composition
yq eval '
  .metadata.name = "'${COMBO_NAME}'.'${PROVIDER}'.example.com" |
  .metadata.labels.provider = "'${PROVIDER}'" |
  .spec.compositeTypeRef.kind = "X'${COMBO_NAME^}'"
' templates/composition-template.yaml > /tmp/composition.yaml

# Add deletion policies
yq eval -f scripts/generate/lib/add-deletion-policy.yq \
  -i /tmp/composition.yaml

# Validate before saving
kubectl apply --dry-run=server -f /tmp/composition.yaml && \
  mv /tmp/composition.yaml "crossplane/compositions/${COMBO_NAME}/${PROVIDER}-composition.yaml"

echo "βœ… Generated and validated composition"

πŸ” Example: Developer Combo Test Suite

Unit Test: Schema Validation

# tests/unit/xrd/test-required-fields.yaml
apiVersion: example.com/v1alpha1
kind: DeveloperCombo
metadata:
  name: test-missing-size
spec:
  # Missing required field: size
  includeDatabase: true

# Expected: Validation error
# kubectl apply fails with: spec.size is required

Policy Test: Deletion Policy

# tests/unit/policies/deletion-policy.rego
package crossplane.composition

deny[msg] {
  input.kind == "Composition"
  resource := input.spec.pipeline[0].input.resources[_]
  not resource.base.spec.deletionPolicy
  msg := sprintf("Resource '%s' missing deletionPolicy", [resource.name])
}

deny[msg] {
  input.kind == "Composition"
  resource := input.spec.pipeline[0].input.resources[_]
  resource.base.spec.deletionPolicy == "Orphan"
  msg := sprintf("Resource '%s' uses Orphan policy (should be Delete)", [resource.name])
}

Integration Test: Small Developer Combo

# tests/integration/developer-combo/test-small-combo.yaml
apiVersion: chainsaw.kyverno.io/v1alpha1
kind: Test
metadata:
  name: developer-combo-small
spec:
  steps:
  - name: create-claim
    try:
    - apply:
        file: ../../../crossplane/xrds/developer-combo/examples/small-claim.yaml
    - assert:
        file: assertions/claim-accepted.yaml

  - name: verify-resources
    try:
    - assert:
        file: assertions/resourcegroup-created.yaml
    - assert:
        file: assertions/database-created.yaml
    - assert:
        file: assertions/storage-created.yaml
    - assert:
        file: assertions/network-created.yaml

  - name: verify-ready
    try:
    - assert:
        file: assertions/claim-ready.yaml
        timeout: 10m

  - name: cleanup
    try:
    - delete:
        file: ../../../crossplane/xrds/developer-combo/examples/small-claim.yaml
    - assert:
        file: assertions/resources-deleted.yaml
        timeout: 5m

E2E Test: Full Lifecycle

# tests/e2e/scenarios/full-lifecycle.yaml
apiVersion: chainsaw.kyverno.io/v1alpha1
kind: Test
metadata:
  name: developer-combo-lifecycle
spec:
  steps:
  # Phase 1: Create
  - name: create-small
    try:
    - apply:
        file: ../../crossplane/claims/dev/myapp-dev-combo.yaml
    - assert:
        resource:
          apiVersion: example.com/v1alpha1
          kind: DeveloperCombo
          metadata:
            name: myapp-dev
            namespace: development
          status:
            conditions:
            - type: Ready
              status: "True"
        timeout: 10m

  # Phase 2: Update (scale up)
  - name: update-to-medium
    try:
    - patch:
        resource:
          apiVersion: example.com/v1alpha1
          kind: DeveloperCombo
          metadata:
            name: myapp-dev
            namespace: development
          spec:
            size: medium  # Changed from small
    - assert:
        resource:
          apiVersion: dbforpostgresql.azure.upbound.io/v1beta1
          kind: FlexibleServer
          status:
            atProvider:
              skuName: GP_Standard_D2s_v3
        timeout: 15m

  # Phase 3: Delete
  - name: delete-combo
    try:
    - delete:
        file: ../../crossplane/claims/dev/myapp-dev-combo.yaml
    - script:
        content: |
          # Verify Azure resources are deleted
          kubectl wait --for=delete resourcegroup/rg-combo-myapp-dev \
            --timeout=10m

πŸŽ“ Learning Path

Week 1: Foundation

  • Set up Minikube with Crossplane
  • Understand XRD β†’ Composition β†’ Claim flow
  • Write first schema validation test
  • Deploy your first Developer Combo

Week 2: Unit Testing

  • Learn conftest and OPA policies
  • Write 5 policy tests
  • Implement YQ-based generation scripts
  • Practice Red-Green-Refactor cycle

Week 3: Integration Testing

  • Set up chainsaw testing framework
  • Write integration tests for all combo sizes
  • Test resource naming conventions
  • Verify status condition propagation

Week 4: E2E Testing

  • Build full lifecycle tests
  • Test multi-environment scenarios
  • Implement failure recovery tests
  • Create CI/CD pipeline

πŸ”§ Useful Commands

Development Workflow

# Generate new XRD
./scripts/generate/generate-xrd.sh mycombo

# Validate YAML syntax
yq eval 'explode(.)' crossplane/xrds/mycombo/xrd.yaml > /dev/null && echo "βœ… Valid YAML"

# Dry-run apply
kubectl apply --dry-run=server -f crossplane/xrds/mycombo/xrd.yaml

# Run all policy tests
conftest test crossplane/compositions/ -p tests/unit/policies/ --all-namespaces

# Watch claim status
kubectl get developercombo -w

# Debug composition
kubectl describe xdevelopercombo <name>

# Check managed resources
kubectl get managed

Troubleshooting

# Check Crossplane logs
kubectl logs -n crossplane-system deployment/crossplane -f

# Check provider logs
kubectl logs -n crossplane-system deployment/provider-azure-storage -f

# View events
kubectl get events --sort-by='.lastTimestamp'

# Check provider health
kubectl get providers

# Verify ProviderConfig
kubectl describe providerconfig default

πŸ“š Additional Resources

Official Documentation

Testing Tools

Articles & Tutorials

🀝 Contributing

This is a learning repository. Feel free to:

  • Open issues for questions
  • Submit PRs with improvements
  • Share your own test cases
  • Suggest new examples

πŸ“„ License

MIT License - See file for details

πŸ‘€ Author

Willem van Heemstra


Happy Testing! 🎯 May your infrastructure be as reliable as a fast food combo meal!

About

Learning Crossplane Test Driven Development

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published