Crossplane Composition Versioning

Crossplane Composition Versioning

The Problem

I needed to run multiple versions of a Crossplane composition simultaneously — wdb-v1 (stable, used by production databases) alongside wdb-v2 (new features I’m testing). The official Crossplane approach uses OCI packages, but the workflow felt heavy:

  1. Build package with crossplane xpkg build
  2. Push to Harbor with version tag
  3. Update every consumer manifest to reference new version
  4. Hope the version selector actually works (spoiler: it has bugs)

I tried it. The build-push-pull dance added friction to every composition change. Testing a small tweak meant rebuilding and republishing. Running two major versions required managing multiple packages. It felt like solving a simple problem with a complex solution.

The Decision

GitOps-synced manifests with versioned compositions (-v1, -v2 suffixes) instead of OCI package distribution. Originally deployed via ArgoCD, now managed by Flux CD.

Why Not OCI Packages?

Crossplane’s version selection is more complex than it needs to be:

  • No simple version: v1.2.3 field — requires labels + selectors
  • Known bugs with selectors not updating refs
  • Managing multiple MAJOR versions is still painful
  • Every version bump requires updating consumer manifests
FactorOCI PackagesGitOps + Versioned Manifests
ComplexityBuild → Push → Pull danceDirect GitOps sync
Version selectionPackage reference updatescompositionSelector.matchLabels
Multiple versionsRequires multiple packagesCoexist naturally (wdb-v1, wdb-v2)
TestingComplex CI setupSymlink composition.yaml → latest
Registry dependencyYesNo

How It Works

Compositions live as versioned files in Git:

manifests/
├── composition-v1.yaml    # Stable, used by production
├── composition-v2.yaml    # New features, testing
└── composition.yaml       # Symlink → latest (for CI tests)

Flux syncs them directly to the cluster. Both versions exist simultaneously. Claims select which version they want via labels:

apiVersion: wxs.io/v1alpha1
kind: Wdb
metadata:
  name: my-database
spec:
  compositionSelector:
    matchLabels:
      wxs.io/composition-version: "v2"  # Use the new version

No rebuilding, no republishing, no registry. Change a file, push, Flux syncs.

The Result

Before (OCI packages):

  • Change composition → build → push → wait for sync → test → repeat
  • Version bumps required touching every consumer manifest
  • Running v1 and v2 simultaneously meant managing two separate packages

After (versioned manifests):

  • Change composition → push → Flux syncs → test
  • Consumers choose version via label selector (no manifest changes needed)
  • v1 and v2 coexist naturally in the same directory

I’ve been using this pattern for months across harbor, wdb, wapp, and wsecret compositions. Zero regrets.

Trade-offs

Accepted:

  • Not using “official” Crossplane packaging approach
  • Version management is manual (file naming convention)
  • No automatic dependency resolution like OCI packages provide

Gained:

  • Faster iteration — seconds instead of minutes
  • Easier testing — symlink to latest for CI
  • Simpler rollback — revert a file, not a package
  • Less infrastructure — no package registry complexity
Last updated on