Crossplane’s conversion webhooks are the unsung heroes of seamless Custom Resource Definition (XRD) version upgrades, ensuring your composite resources don’t break when you update the underlying schema.
Let’s see this in action. Imagine you have an RDSInstance composite resource defined by an XRD. Initially, it might look like this:
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: rdsinstances.rds.example.com
spec:
group: rds.example.com
names:
kind: RDSInstance
plural: rdsinstances
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
engine:
type: string
storageGB:
type: integer
# ... other properties
Now, you want to introduce a new version, v1beta1, with some schema changes, perhaps adding a dbSubnetGroupName field. You’d update your XRD:
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: rdsinstances.rds.example.com
spec:
group: rds.example.com
names:
kind: RDSInstance
plural: rdsinstances
versions:
- name: v1alpha1 # The old version
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
engine:
type: string
storageGB:
type: integer
# ... other properties
- name: v1beta1 # The new version
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
engine:
type: string
storageGB:
type: integer
dbSubnetGroupName: # New field
type: string
# ... other properties
After applying this updated XRD, Kubernetes needs to know how to convert between v1alpha1 and v1beta1 representations of your RDSInstance. This is where the conversion webhook comes in. When you create a new RDSInstance in v1beta1, and then later decide to revert to v1alpha1 (or vice-versa), the webhook intercepts the API request and performs the necessary transformation.
The problem this solves is maintaining data integrity and backward compatibility during schema evolution. Without a conversion mechanism, updating an XRD could lead to unrecoverable data loss or make existing resources incompatible with the new schema. Crossplane leverages Kubernetes’ admission webhooks to achieve this.
Internally, when you modify an XRD to include new versions, Crossplane automatically registers admission webhooks for that XRD. These webhooks are configured to intercept requests for the specific kind and group defined in the XRD. When a Kubernetes API server receives a request to create, update, or even just get a resource whose apiVersion doesn’t match the preferred version of the XRD, it consults these webhooks.
The webhook’s logic is defined by the conversion block within the XRD.spec. This block specifies how to convert between different versions. If you don’t explicitly define conversion logic, Kubernetes defaults to a basic strategy. However, for complex transformations or to handle specific field mappings, you’ll often define a webhook within the conversion block.
The crucial part is understanding the webhook configuration within the conversion section of your XRD. This tells Kubernetes where to send the conversion requests and how to handle them.
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: rdsinstances.rds.example.com
spec:
group: rds.example.com
names:
kind: RDSInstance
plural: rdsinstances
versions:
- name: v1alpha1
# ... schema ...
- name: v1beta1
# ... schema ...
conversion:
strategy: Webhook
webhook:
clientConfig:
service:
name: crossplane-webhook-svc
namespace: crossplane-system
path: /convert
conversionReviewVersions:
- v1
The clientConfig points to the Kubernetes service that hosts your conversion webhook. In Crossplane’s case, this is typically a service running within the crossplane-system namespace. The path is the specific endpoint on that service that handles conversion requests. conversionReviewVersions indicates the API versions of the ConversionReview object that the webhook supports.
The most surprising true thing about Crossplane’s conversion webhooks is that they don’t just convert data when you change the apiVersion of a resource. They are also invoked when the Kubernetes API server needs to serialize or deserialize resource objects for storage in etcd, ensuring that the stored version is consistent and can be accurately retrieved in any of the XRD’s defined versions. This means even if you never manually change the apiVersion of an existing RDSInstance, the webhook is silently working in the background to maintain schema compatibility.
When a ConversionReview request arrives at the webhook, it contains the object in its current version. The webhook’s handler then inspects the spec.convertedObjects field. If the request is to convert from v1alpha1 to v1beta1, the handler would populate spec.convertedObjects with the v1beta1 representation of the input object. Conversely, if converting from v1beta1 to v1alpha1, it would provide the v1alpha1 version. The webhook must be able to handle conversions in both directions between all listed versions.
A subtle but critical detail is that the webhook implementation needs to be robust enough to handle missing fields gracefully. If your v1beta1 schema introduces dbSubnetGroupName, and you’re converting an existing v1alpha1 resource that doesn’t have this field, the conversion logic should simply omit it in the v1alpha1 output, rather than erroring out. The apiextensions.crossplane.io/v1 webhook server within Crossplane handles this by default for simple schema differences, but custom conversion logic might require explicit checks.
The next concept you’ll likely encounter is handling more complex schema transformations, such as renaming fields or changing data types, which often require custom conversion logic beyond simple field mapping.