Kubernetes CRD Conversion Management
Kubernetes CRD Conversion Management ensures backward compatibility by automating schema updates across API versions, enabling smooth transitions in cluster configurations.
Kubernetes CRD Conversion Management is the practice of implementing and operating the mechanism that translates a custom resource object between its different served API versions, covering the choice between the None and Webhook conversion strategies, the hub-and-spoke pattern used to keep conversion logic tractable across many versions, and the round-trip correctness requirements a conversion implementation must satisfy.
Conversion Strategies
None Strategy
spec:
conversion:
strategy: None
The None strategy is only valid when a CRD serves exactly one version, or when multiple served versions are schema-identical aside from their name; the API server performs no field translation at all, which is trivially correct but inapplicable the moment two versions' schemas actually diverge.
Webhook Strategy
spec:
conversion:
strategy: Webhook
webhook:
clientConfig:
service:
name: postgrescluster-conversion
namespace: databases-system
path: "/convert"
conversionReviewVersions: ["v1"]
The Webhook strategy delegates conversion to an external service invoked by the API server on every read or write that requires translating between the storage version and a requested served version, receiving a ConversionReview request containing the objects to convert and returning them translated to the desired version.
The Hub-and-Spoke Conversion Pattern
Why Direct Pairwise Conversion Doesn't Scale
With three served versions, direct pairwise conversion logic would require six conversion functions (each version to each other version); the hub-and-spoke pattern designates one version, typically the current storage version, as the hub, and implements conversion only between each spoke version and the hub, reducing the required functions to two per additional version.
func (v1alpha1 *PostgresClusterV1Alpha1) ConvertTo(hub conversion.Hub) error {
dst := hub.(*v1.PostgresCluster)
dst.Spec.Replicas = v1alpha1.Spec.ReplicaCount
return nil
}
func (v1alpha1 *PostgresClusterV1Alpha1) ConvertFrom(hub conversion.Hub) error {
src := hub.(*v1.PostgresCluster)
v1alpha1.Spec.ReplicaCount = src.Spec.Replicas
return nil
}
The Hub Version's Special Status
Exactly one version must implement the Hub() marker method with no translation logic, since it is the reference shape every other version converts to and from; changing which version is the hub after conversion logic has been written across multiple spokes requires re-deriving every conversion function relative to the new hub.
Round-Trip Correctness
The Round-Trip Requirement
A correct conversion implementation must satisfy round-trip fidelity: converting an object from version A to the hub and back to version A must reproduce the original object exactly, for any valid object of version A, since the API server relies on this property when serving the same underlying stored object to clients requesting different versions repeatedly.
func TestRoundTrip(t *testing.T) {
original := &v1alpha1.PostgresCluster{Spec: v1alpha1.PostgresClusterSpec{ReplicaCount: 3}}
hub := &v1.PostgresCluster{}
original.ConvertTo(hub)
roundTripped := &v1alpha1.PostgresCluster{}
roundTripped.ConvertFrom(hub)
require.Equal(t, original, roundTripped)
}
Handling Fields with No Equivalent
When a newer version introduces a field with no counterpart in an older version, conversion to the older version necessarily drops it; if that dropped information later needs to survive a round trip (an older client reads, then writes back unmodified through the old version), the field must be preserved via an annotation on the object during ConvertTo, and restored from that annotation during ConvertFrom, since silently losing fields on a round trip that should have been transparent causes unexpected data loss for clients still using an older version.
func (v1alpha1 *PostgresClusterV1Alpha1) ConvertTo(hub conversion.Hub) error {
dst := hub.(*v1.PostgresCluster)
if v1alpha1.Annotations["conversion.example.com/backup-policy"] != "" {
dst.Spec.BackupPolicyRef = v1alpha1.Annotations["conversion.example.com/backup-policy"]
}
return nil
}
Operational Concerns
Conversion Webhook Availability
webhooks:
- failurePolicy: Fail
Because conversion is invoked synchronously on essentially every API operation touching a multi-version CRD, an unreachable conversion webhook makes the affected resource type entirely unreadable and unwritable across every version except the storage version itself, making conversion webhook deployment a high-availability requirement, typically run with multiple replicas and a PodDisruptionBudget, comparable in criticality to the API server itself for that resource type.
Testing Conversion Against Real API Server Behavior
kubectl get postgrescluster orders-db --output-version=databases.example.com/v1alpha1 -o yaml
Requesting an object explicitly through an older version, as above, is the standard way to verify a conversion webhook is correctly reachable and producing valid output before relying on it in production, since a schema-valid but semantically incorrect conversion will not surface as an error, only as silently wrong data delivered to an older client.
Relationship to CRD Version Management
Conversion management is the specific implementation mechanism underlying the version transitions described in CRD version management: where version management governs the timeline of introducing, promoting, deprecating, and retiring versions, conversion management is the technical machinery, hub-and-spoke design and round-trip correctness in particular, that makes multiple versions of the same resource simultaneously servable without any served version's clients or controllers observing incorrect or lossy data.