A systematic approach to testing Crossplane infrastructure-as-code using the Fast Food Restaurant metaphor
This repository is part of a learning series:
- learning-crossplane - Core Crossplane concepts and fundamentals
- learning-test-driven-development - TDD principles and practices
- This Repository - Applying TDD to Crossplane infrastructure
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.
| 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 |
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
- 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)
# 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# 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# 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 --watchWhat: 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 -strictWhat: 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.regoWhat: 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 smallWhat: 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# 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# 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# 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#!/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"#!/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"# 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# 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])
}# 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# 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- Set up Minikube with Crossplane
- Understand XRD β Composition β Claim flow
- Write first schema validation test
- Deploy your first Developer Combo
- Learn conftest and OPA policies
- Write 5 policy tests
- Implement YQ-based generation scripts
- Practice Red-Green-Refactor cycle
- Set up chainsaw testing framework
- Write integration tests for all combo sizes
- Test resource naming conventions
- Verify status condition propagation
- Build full lifecycle tests
- Test multi-environment scenarios
- Implement failure recovery tests
- Create CI/CD pipeline
# 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# 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- conftest - Policy testing
- chainsaw - E2E testing
- kuttl - Kubernetes operator testing
- kubeconform - Schema validation
- Fast Infrastructure: Understanding Crossplane like a Fast Food Restaurant
- Testing Crossplane Compositions
- TDD for Infrastructure
This is a learning repository. Feel free to:
- Open issues for questions
- Submit PRs with improvements
- Share your own test cases
- Suggest new examples
MIT License - See file for details
Willem van Heemstra
- Cloud Engineer & Security Domain Expert
- GitHub: @vanHeemstraSystems
- LinkedIn: Willem van Heemstra
Happy Testing! π― May your infrastructure be as reliable as a fast food combo meal!