Versions in CustomResourceDefinition

Piotr Stroz
DevBulls

--

How does Kubernetes handle multiple CustomResource versions? What happens when clients create objects in v1beta1, and you release v1beta2 with breaking changes?

Let’s explore this using a simple Widget CRD:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: widgets.example.com
spec:
group: example.com
names:
kind: Widget
plural: widgets
singular: widget
scope: Namespaced
versions:
- name: v1beta1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
name:
type: boolean
- name: v1beta2
served: true
storage: false
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
name:
type: string

Different API Maturity Levels

Kubernetes defines three API maturity levels:

  • Alpha: Experimental and unstable. Use only for short-term testing. High risk of bugs and breaking changes.
  • Beta: More stable than alpha. Suitable for testing but not recommended for production. Changes may still occur.
  • Stable(vX): Reliable and safe for production. Long-term support with no expected breaking changes.

Read more on Kubernetes API versioning.

Multiple API Versions

CRDs can support multiple versions. In our example, we have v1beta1 and v1beta2:

versions:
- name: v1beta1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
name:
type: boolean
- name: v1beta2
served: true
storage: false
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
name:
type: string

Both versions are served (served: true), so you can create objects in either version:

v1beta1:

apiVersion: example.com/v1beta1
kind: Widget
metadata:
name: widget-v1beta1
spec:
name: true

v1beta2:

apiVersion: example.com/v1beta2
kind: Widget
metadata:
name: widget-v1beta2
spec:
name: "Piotr"

Applying both:

kubectl apply -f widget-v1beta1.yaml -f widget-v1beta2.yaml
widget.example.com/widget-v1beta1 created
widget.example.com/widget-v1beta2 created

However, when retrieving widget-v1beta1:

kubectl get widget.example.com/widget-v1beta1 -o yaml

You’ll notice:

apiVersion: example.com/v1beta2
kind: Widget
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"example.com/v1beta1","kind":"Widget","metadata":{"annotations":{},"name":"widget-v1beta1","namespace":"default"},"spec":{"name":true}}
creationTimestamp: "2024-08-13T13:41:14Z"
generation: 1
name: widget-v1beta1
namespace: default
resourceVersion: "790652"
uid: 47dcb472-7990-4ad8-8b64-45ec70c8ac2a
spec:
name: true

The apiVersion returned is v1beta2, despite the object being created with v1beta1.

Preferred Version

Kubernetes defaults to the newest available version when multiple versions are supported:

kubectl get --raw /apis/example.com | jq .
{
"kind": "APIGroup",
"apiVersion": "v1",
"name": "example.com",
"versions": [
{
"groupVersion": "example.com/v1beta2",
"version": "v1beta2"
},
{
"groupVersion": "example.com/v1beta1",
"version": "v1beta1"
}
],
"preferredVersion": {
"groupVersion": "example.com/v1beta2",
"version": "v1beta2"
}
}

“The newest available resource version is preferred, but strong consistency is not required.”
Learn more about resource versions.

Additionally, the version with the highest priority is used by kubectl as the default version to access objects:

“The version with the highest priority is used by kubectl as the default version to access objects.”
Learn more about version priority.

This behaviour means that even if an object was created with one version, Kubernetes may return it in the preferred(highest priority) version.

Storage Field

The storage field dictates which version is stored in etcd:

versions:
- name: v1beta1
served: true
storage: true

Creating a Widget using v1beta2:

apiVersion: example.com/v1beta2
kind: Widget
metadata:
name: widget1
spec:
name: "Piotr"

When checking etcd, the object is stored as v1beta1:

root@master:~/etcd-v3.4.12-linux-amd64# ./etcdctl --endpoints=https://localhost:2379   --cacert=/etc/kubernetes/pki/etcd/ca.crt   --cert=/etc/kubernetes/pki/apiserver-etcd-client.crt   --key=/etc/kubernetes/pki/apiserver-etcd-client.key get /registry/example.com/widgets/default/widget1
/registry/example.com/widgets/default/widget1
{"apiVersion":"example.com/v1beta1","kind":"Widget","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"example.com/v1beta2\",\"kind\":\"Widget\",\"metadata\":{\"annotations\":{},\"name\":\"widget1\",\"namespace\":\"default\"},\"spec\":{\"name\":true}}\n"},"creationTimestamp":"2024-07-17T09:07:03Z","generation":2,"name":"widget1","namespace":"default","uid":"96d187bc-958d-4c95-8a59-8a2d33899d90"},"spec":{"name":true}}

Only one version is stored in etcd, and other versions are just different views of the same data.

Conversion Strategies

What happens if you create an object in one version but request it in another? Kubernetes tries to convert the object to match the requested version. This conversion can happen in one of two ways:

  • None: Kubernetes simply updates the apiVersion field to the requested version without altering the object’s schema. This approach assumes the schemas are compatible, so only the version field changes, while the data remains the same.
  • Webhook: If the schemas differ significantly, you can use a custom conversion webhook. The webhook handles the transformation, ensuring the object fits the requested version’s schema, maintaining data consistency across versions.

Learn more about conversion strategies.

Bonus

This Kubecon 2018 talk is the best one on the topic I could find.

--

--