Migrate Crossplane v1 Configurations to v2
This guide documents the migration from Crossplane v1 to v2 for configurations, including all breaking changes and lessons learned. It covers updating XRDs, compositions, KCL functions, and tests for the new v2 namespaced resource model.
Overview
Crossplane v2 introduces namespaced resources with the .m API group suffix
(for example, rds.aws.m.upbound.io/v1beta1) and removes the XR/Claim separation
pattern. This migration covers updating XRDs, compositions, KCL functions, and
tests.
Prerequisites
- Upgrade
upbound.yamlto reference v2 provider packages - Run
up project buildto generate v2 models in.up/kcl/models/ - Ensure Docker is running for composition rendering tests
Breaking changes
1. XRD API version and structure
Before (v1):
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xsqlinstances.aws.platform.upbound.io
spec:
claimNames:
kind: SQLInstance
plural: sqlinstances
connectionSecretKeys:
- username
- password
group: aws.platform.upbound.io
names:
kind: XSQLInstance
plural: xsqlinstances
After (v2):
apiVersion: apiextensions.crossplane.io/v2
kind: CompositeResourceDefinition
metadata:
name: sqlinstances.aws.platform.upbound.io
spec:
scope: Namespaced # NEW: Required in v2
group: aws.platform.upbound.io
names:
kind: SQLInstance # Removed "X" prefix
plural: sqlinstances
# claimNames removed - claims not supported in v2
# connectionSecretKeys moved or handled differently
Key changes:
- Update to
apiVersion: apiextensions.crossplane.io/v2 - Add
scope: Namespaced(orClusterif needed) - Remove
claimNames- v2 namespaced resources don't support claims - Remove "X" prefix from resource names (
XSQLInstance→SQLInstance) - Update metadata name to match new plural
2. Namespaced resource API groups
Before (v1 - cluster-scoped):
import models.io.upbound.aws.rds.v1beta1 as rdsv1beta1
After (v2 - namespaced):
import models.io.upbound.awsm.rds.v1beta1 as rdsv1beta1
// Note the "awsm" instead of "aws" - the .m indicates namespaced
API version changes:
- Cluster-scoped v1:
rds.aws.upbound.io/v1beta1 - Namespaced v2:
rds.aws.m.upbound.io/v1beta1
The .m suffix indicates managed/namespaced resources in v2.
3. deletionPolicy removed for namespaced resources
Before (v1):
defaultSpec = {
providerConfigRef.name = "default"
deletionPolicy = oxr.spec.parameters.deletionPolicy // "Delete" or "Orphan"
forProvider.region = "us-west-2"
}
After (v2):
defaultSpec = {
providerConfigRef = {
kind = "ProviderConfig" // Now required!
name = "default"
}
managementPolicies = ["*"] // Replaces deletionPolicy
forProvider.region = "us-west-2"
}
Migration path:
- Default behavior (
deletionPolicy: Delete) →managementPolicies: ["*"] - Orphan behavior (
deletionPolicy: Orphan) →managementPolicies: ["Create", "Observe", "Update", "LateInitialize"]
Available management policies:
*- All management policies (default)Create- Create the external resourceObserve- Observe the external resourceUpdate- Update the external resourceDelete- Delete the external resourceLateInitialize- Initialize unset fields from external resource values
XRD schema update:
parameters:
type: object
properties:
managementPolicies:
description: ManagementPolicies for the RDS resources. Defaults to ["*"] which includes all operations (Create, Observe, Update, Delete, LateInitialize). To orphan resources on deletion, use ["Create", "Observe", "Update", "LateInitialize"].
type: array
items:
type: string
enum:
- "*"
- Create
- Observe
- Update
- Delete
- LateInitialize
default: ["*"]
4. providerConfigRef requires kind field
Before (v1):
providerConfigRef.name = "default"
After (v2):
providerConfigRef = {
kind = "ProviderConfig" // REQUIRED in v2
name = "default"
}
Error if missing:
attribute 'kind' of RdsAwsmUpboundIoV1beta1SubnetGroupSpecProviderConfigRef is required and can't be None or Undefined
5. namespace field removed from secret references
Before (v1):
passwordSecretRef = {
name = "mariadbsecret"
namespace = "default" // Explicit namespace
key = "password"
}
writeConnectionSecretToRef = {
name = "{}-sql".format(oxr.metadata.uid)
namespace = oxr.spec.writeConnectionSecretToRef.namespace
}
After (v2):
passwordSecretRef = {
name = "mariadbsecret"
// namespace removed - inferred from resource namespace
key = "password"
}
writeConnectionSecretToRef = {
name = "{}-sql".format(oxr.metadata.uid)
// namespace removed - written to resource namespace
}
XRD schema update:
passwordSecretRef:
type: object
description: "A reference to the Secret object containing database password. In v2, namespace is inferred from the resource namespace."
properties:
namespace:
type: string
description: "Deprecated in v2 - namespace is now inferred from resource namespace"
name:
type: string
key:
type: string
required:
- name
- key
# namespace no longer required
6. compositionSelector moved to spec.crossplane
Before (v1):
apiVersion: aws.platform.upbound.io/v1alpha1
kind: XSQLInstance
metadata:
name: my-database
spec:
compositionSelector:
matchLabels:
type: rds-metrics
parameters:
region: us-west-2
After (v2):
apiVersion: aws.platform.upbound.io/v1alpha1
kind: SQLInstance
metadata:
name: my-database
namespace: default # Now required for namespaced resources
spec:
crossplane: # NEW: Crossplane-specific fields under this section
compositionSelector:
matchLabels:
type: rds-metrics
parameters:
region: us-west-2
All "Crossplane machinery" fields move under spec.crossplane to distinguish
Crossplane-specific configuration from user-facing parameters.
7. Connection secrets redesigned in v2
Crossplane v2 removes built-in connection secret support from XRs entirely. This represents an important architectural shift in how connection details are managed.
What changed
Before (v1):
apiVersion: aws.platform.upbound.io/v1alpha1
kind: XSQLInstance
spec:
parameters:
region: us-west-2
writeConnectionSecretToRef:
name: my-connection-secret
namespace: default
XRs had built-in spec.writeConnectionSecretToRef that automatically created
a Kubernetes Secret with aggregated connection details from all composed
resources.
After (v2):
apiVersion: aws.platform.upbound.io/v1alpha1
kind: SQLInstance
metadata:
namespace: default
spec:
parameters:
region: us-west-2
# writeConnectionSecretToRef completely removed - not supported in v2 XRs
The field is gone. V2 XRs don't support automatic connection secret creation.
The v2 philosophy
According to crossplane/crossplane#6440, Crossplane v2 shifts to representing higher-level abstractions (complete applications) rather than low-level infrastructure. Connection secrets are no longer a built-in feature because:
- Not all XRs need to expose connection details
- Different use cases need different secret structures
- Functions provide more flexibility for secret composition
Migration path: Manually compose secrets
The official guidance from the Crossplane maintainers:
"In v2 you can recreate it using functions - just have your XR compose a secret with the XR connection details in it."
This means your function must explicitly create a Kubernetes Secret resource containing the connection details for the XR.
Implementation example
Step 1: Managed resources still support connection secrets
rdsv1beta1.Instance{
metadata: _metadata("rds-instance")
spec: {
forProvider: {
engine = "postgres"
username = "masteruser"
# ... other config
}
# Managed resources can still write connection secrets
writeConnectionSecretToRef = {
name = "{}-rds-conn".format(oxr.metadata.name)
}
}
}
This creates a Secret with raw RDS credentials that the function can read via
ocds["rds-instance"].ConnectionDetails.
Step 2: Function composes a user-facing Secret
import models.io.k8s.api.core.v1 as corev1
# V2: Manually compose a Kubernetes Secret with connection details
corev1.Secret{
metadata: _metadata("connection-secret") | {
name: "{}-connection".format(oxr.metadata.name)
namespace: oxr.metadata.namespace
labels: {
"crossplane.io/composite": oxr.metadata.name
}
}
type: "connection.crossplane.io/v1alpha1"
if "rds-instance" in ocds:
data: {
# Base64-encode all values except password (already encoded)
endpoint: base64.encode(ocds["rds-instance"].Resource?.status?.atProvider?.endpoint or "")
host: base64.encode(ocds["rds-instance"].Resource?.status?.atProvider?.address or "")
port: base64.encode(str(ocds["rds-instance"].Resource?.status?.atProvider?.port or 3306))
username: base64.encode(ocds["rds-instance"].Resource?.spec?.forProvider?.username or "")
# Password comes from managed resource's connection secret
password: ocds["rds-instance"].ConnectionDetails?.password or ""
}
else:
data: {}
}
Add Kubernetes API dependency to upbound.yaml to generate Secret models:
apiVersion: meta.dev.upbound.io/v2alpha1
kind: Project
spec:
apiDependencies:
- k8s:
version: v1.33.0
type: k8s
Then run up project build to generate models in .up/kcl/models/io/k8s/api/core/v1/Secret.k.
Step 3: Users reference the composed Secret
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
env:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: my-database-connection # {sqlinstance-name}-connection
key: host
What about CompositeConnectionDetails
Don't use CompositeConnectionDetails for v2:
# This is NOT sufficient in v2!
{
apiVersion: "meta.krm.kcl.dev/v1alpha1"
kind: "CompositeConnectionDetails"
data: {
endpoint = ocds["rds-instance"].Resource?.status?.atProvider?.endpoint
}
}
CompositeConnectionDetails only exposes data in the XR
.status.connectionDetails field. It doesn't create a Kubernetes Secret that
applications can reference.
Architecture comparison
v1 built-in:
XR (writeConnectionSecretToRef)
└─> Crossplane creates Secret automatically
v2 function-based:
XR (no built-in support)
└─> Function generates:
├─> Managed Resource (writeConnectionSecretToRef)
│ └─> Creates Secret with raw credentials
└─> Kubernetes Secret resource (manually composed)
└─> Aggregates connection details for user consumption
XRD schema changes
Remove connection secret fields from the XRD schema:
# v1 XRD - REMOVE THESE
spec:
connectionSecretKeys:
- username
- password
- endpoint
- port
# In openAPIV3Schema
spec:
properties:
writeConnectionSecretToRef: # REMOVE
type: object
properties:
name:
type: string
namespace:
type: string
# v2 XRD - No connection secret fields
spec:
scope: Namespaced
# connectionSecretKeys removed
# No writeConnectionSecretToRef in schema
Documentation note
This connection secret architecture change represents a significant shift in Crossplane v2. As noted in crossplane/docs#1001, migration guidance is actively evolving:
- Official migration patterns are being developed
- Community feedback from GitHub discussions informs best practices
- This guide captures production migration experience
Upbound is working with the Crossplane community to contribute these findings back to upstream documentation after field validation.
Migration checklist
Step 1: Update upbound.yaml dependencies
dependsOn:
- apiVersion: pkg.crossplane.io/v1
kind: Provider
package: xpkg.upbound.io/upbound/provider-aws-rds
version: v2 # Update to v2
Step 2: Migrate the XRD
- Update
apiVersion: apiextensions.crossplane.io/v2 - Add
scope: Namespaced - Remove
claimNamessection - Remove "X" prefix from
kindand updateplural - Update
metadata.nameto match new plural - Add
managementPoliciesparameter (removedeletionPolicy) - Make
passwordSecretRef.namespaceoptional with deprecation note
Step 3: Update function imports
// Change all provider imports from .aws. to .awsm.
import models.io.upbound.awsm.rds.v1beta1 as rdsv1beta1
import models.io.upbound.awsm.v1beta1 as awsv1beta1
// Update XR type
oxr = platformawsv1alpha1.SQLInstance{**option("params").oxr}
Step 4: Update function code
- Update
providerConfigRefto includekindfield - Replace
deletionPolicywithmanagementPolicies - Remove
namespacefrompasswordSecretRef - Remove
namespacefromwriteConnectionSecretToRef(on managed resources) - Update all
XSQLInstancereferences toSQLInstance - CRITICAL: Replace
CompositeConnectionDetailswith manually composed Kubernetes Secret- Add
import base64to function - Create a
v1/Secretresource in function output - Populate
datafields with base64-encoded connection details fromocds - Use
ocds["resource-name"].ConnectionDetails.passwordfor password - Use
ocds["resource-name"].Resource?.status?.atProvider?.fieldfor other fields
- Add
Step 5: Update compositions
spec:
compositeTypeRef:
apiVersion: aws.platform.upbound.io/v1alpha1
kind: SQLInstance # Remove "X" prefix
Step 6: Update examples
- Change kind from
XSQLInstancetoSQLInstance - Add
namespace: defaultto metadata - Move
compositionSelectortospec.crossplane.compositionSelector - Remove
namespacefrompasswordSecretRef - Remove
writeConnectionSecretToReffrom spec
Step 7: Update tests
- Update imports to use
awsmmodels - Change all
XSQLInstancetoSQLInstance - Add
providerConfigRef.kindto expected resources - Add
managementPoliciesto expected resources - Remove
namespacefrompasswordSecretRefin assertions - Remove
writeConnectionSecretToReffrom XR assertions
Step 8: Build and test
# Rebuild to generate v2 models
up project build
# Test composition rendering (requires Docker)
up composition render \
--xrd=apis/definition.yaml \
apis/composition-rds-metrics.yaml \
examples/mariadb-xr-rds-metrics.yaml
# Run composition tests
up test run tests/test-xsqlinstance/
Common errors and solutions
Error: "Cannot add member 'deletionPolicy'"
Cause: v2 namespaced resources don't have the deletionPolicy field.
Solution: Replace with the managementPolicies field:
managementPolicies = oxr.spec.parameters.managementPolicies or ["*"]
Error: "Cannot add member 'namespace' to schema"
Cause: v2 removes explicit namespace from secret references.
Solution: Remove namespace field:
passwordSecretRef = {
name = "secret-name"
key = "password"
// namespace removed
}
Error: "attribute 'kind' of ProviderConfigRef is required"
Cause: v2 requires explicit kind in provider config references.
Solution:
providerConfigRef = {
kind = "ProviderConfig"
name = "default"
}
Error: "Schema doesn't contain attribute compositionSelector"
Cause: v2 moves Crossplane fields under spec.crossplane.
Solution:
spec:
crossplane:
compositionSelector:
matchLabels:
type: rds-metrics
Error: "Cannot add member 'writeConnectionSecretToRef'" in XR spec
Cause: v2 XRD schema removes writeConnectionSecretToRef from XR spec
entirely.
Solution:
- Remove from XR examples and test assertions
- Create manual Secret composition in your function (see Section 7)
- Keep
writeConnectionSecretToRefon managed resources - that's still supported
Error: "Connection secrets not being created"
Cause: Migrated from v1 but didn't implement manual Secret composition.
Symptoms:
- No Secret created for the XR
- Applications can't find connection details
- Only managed resources have connection secrets
Solution: Create function-based Secret composition:
import base64
_items = [
# ... your managed resources with writeConnectionSecretToRef ...
# Add this: Manually compose connection Secret
{
apiVersion: "v1"
kind: "Secret"
metadata: _metadata("connection-secret") | {
name: "{}-connection".format(oxr.metadata.name)
namespace: oxr.metadata.namespace
}
type: "connection.crossplane.io/v1alpha1"
if "your-managed-resource-name" in ocds:
data: {
endpoint: base64.encode(ocds["resource-name"].Resource?.status?.atProvider?.endpoint or "")
password: ocds["resource-name"].ConnectionDetails?.password or ""
# ... other fields
}
else:
data: {}
}
]
Error: CompositeConnectionDetails not creating a Secret
Cause: CompositeConnectionDetails only updates XR status, it doesn't
create a Kubernetes Secret.
Symptoms:
- Data appears in
kubectl get xr -o yamlunderstatus.connectionDetails - No Secret resource created
- Applications can't reference the data
Solution: Replace CompositeConnectionDetails with a manually composed
v1/Secret resource (see Section 7).
Testing strategy
Unit tests (composition tests)
Create credentials file for external functions:
# tests/test-credentials.yaml
apiVersion: v1
kind: Secret
metadata:
name: aws-creds
namespace: crossplane-system
type: Opaque
data:
credentials: <base64-encoded-aws-credentials>
Run tests with credentials:
up test run tests/test-xsqlinstance/ \
--function-credentials=tests/test-credentials.yaml
End-to-end tests
Ensure your end-to-end tests:
- Include proper ProviderConfig with v2 structure
- Use namespaced resources
- Reference v2 provider packages
Best practices
-
Incremental migration: Test each component one at a time
- XRD changes first
- Function code second
- Tests last
-
Model generation: Always run
up project buildafter dependency updates to regenerate models -
Namespace handling: Be explicit about resource namespaces in v2
metadata:
name: my-resource
namespace: default # Always specify for namespaced resources -
Composition selection: Use
spec.crossplanefor all Crossplane-specific fieldsspec:
crossplane:
compositionSelector:
matchLabels:
provider: aws
parameters:
# User-facing parameters here -
Provider package names: Always use fully qualified package names
package: xpkg.upbound.io/upbound/provider-aws-rds
# NOT: provider-aws-rds
Troubleshooting
Models not updating
Problem: Changes to provider version not reflected in generated models.
Solution:
rm -rf .up/
up project build
Function compilation errors
Problem: KCL schema validation errors after migration.
Solution: Check .up/kcl/models/ for the actual generated schemas. The v2
schemas are structurally different.
Test failures
Problem: Tests pass locally but fail in CI.
Solution: Ensure Docker is available in CI environment for composition rendering.
Conclusion
Crossplane v2 simplifies resource management by:
- Removing the XR/Claim abstraction
- Using namespaced resources for better multi-tenancy
- Consolidating Crossplane-specific fields under
spec.crossplane - Inferring namespaces for secret references
The migration requires careful attention to breaking changes but results in cleaner, more intuitive APIs.
Next steps
- Migrate live clusters to v2 - Operational guide for migrating running clusters without recreating resources
- Upgrade to UXP - Upgrade from Crossplane v2 OSS to Upbound Crossplane
- Provider migration guide - Migrate from monolithic to family providers