TT Lab
Get started
Learn Learning paths Courses

CRDs and Operators

Defining a WebService Type and Registering It With the API

Continue in TT Lab

Goal

Define a new resource type called WebService in the apps.labhub.io/v1 group and register it with the API server, completing a CRD that is actually usable, with a schema, custom columns, subresources, and multiple versions.

Why it matters

Writing a CRD is not "listing fields"; it means deciding where to place the responsibility for validation. If you write minimum: 1 in the schema, the API server enforces that rule at apply time, and the error message goes straight to the user. If you put it in the controller code instead, you discover the problem in the logs only after the bad object has already been stored. Subresources are not just a convenience either. When you split status into a separate path, writing status does not raise metadata.generation, so the controller can tell "the user changed the spec" apart from "I just wrote status." Without this distinction, the controller falls into an infinite loop, reacting again to its own status writes. Finally, it is good to start with two versions from the beginning. If you learn firsthand that there must be exactly one storage version and that status.storedVersions exists, you can avoid the incident where, later, deleting an old version leaves the data unreadable.

Steps

  1. Create /root/crd/crd.yaml. It must have apiVersion: apiextensions.k8s.io/v1, kind: CustomResourceDefinition, metadata.name: webservices.apps.labhub.io, and spec.group: apps.labhub.io.
  2. In spec.names of the same file, put plural: webservices, singular: webservice, kind: WebService, listKind: WebServiceList, shortNames: [ws], and categories: [labhub], and set spec.scope: Namespaced.
  3. Put name: v1 in spec.versions and write schema.openAPIV3Schema. The top level has type: object, properties.spec.type: object, and properties.spec.required: [image], and under properties.spec.properties define three fields: image (type string), replicas (type integer, minimum: 1, maximum: 10, default: 1), and tier (type string, enum: [dev, stage, prod], default: dev).
  4. Apply it with kubectl apply -f /root/crd/crd.yaml, check that the Established condition is True, then save the output of kubectl api-resources --api-group=apps.labhub.io to /root/crd/out/api-resources.txt.
  5. Add additionalPrinterColumns to version v1. There are four columns: Image (type string, jsonPath .spec.image), Replicas (type integer, jsonPath .spec.replicas), Tier (type string, jsonPath .spec.tier), and Age (type date, jsonPath .metadata.creationTimestamp).
  6. Enable subresources.status: {} and subresources.scale on version v1. For scale, use specReplicasPath: .spec.replicas, statusReplicasPath: .status.replicas, and labelSelectorPath: .status.selector. At the same time, define properties.status in the schema as type: object and put under it replicas (integer), selector (string), observedGeneration (integer), and conditions (type array, where items is type object, type, status, reason, and message are strings, and lastTransitionTime is a string). Status fields not in the schema are pruned and not stored.
  7. Add name: v1alpha1 to spec.versions. v1alpha1 has served: true and storage: false, and v1 has served: true and storage: true. v1alpha1 must also have a schema, so copy the v1 schema as it is. After reapplying, check that v1 is present in the output of kubectl get crd webservices.apps.labhub.io -o jsonpath='{.status.storedVersions}'.
  8. Create a namespace with kubectl create ns crd-lab, then apply /opt/lab/fixtures/crd/sample-cr.yaml to create sample (spec.image must include the tag). Then save the output of kubectl get webservice -n crd-lab to /root/crd/out/get-ws.txt, and the output of kubectl get --raw /apis/apps.labhub.io/v1/namespaces/crd-lab/webservices/sample/status to /root/crd/out/status.json.

Notes

Set up the CRD skeleton and follow the naming rule

Create /root/crd/crd.yaml. It must have apiVersion: apiextensions.k8s.io/v1, kind: CustomResourceDefinition, metadata.name: webservices.apps.labhub.io, and spec.group: apps.labhub.io.

A CRD is an object in the apiextensions.k8s.io/v1 group. You cannot name metadata.name freely; it must be the plural name and the group joined with a dot. If you apply the broken CRD in the fixtures, you can see how the API server words the rejection.

Decide the names, scope, and short names

In spec.names of the same file, put plural: webservices, singular: webservice, kind: WebService, listKind: WebServiceList, shortNames: [ws], and categories: [labhub], and set spec.scope: Namespaced.

spec.names takes the plural, singular, kind, and listKind separately. kind is in Pascal case, and listKind is kind followed by List. shortNames and categories are arrays.

Pin down the fields with an OpenAPI v3 schema

Put name: v1 in spec.versions and write schema.openAPIV3Schema. The top level has type: object, properties.spec.type: object, and properties.spec.required: [image], and under properties.spec.properties define three fields: image (type string), replicas (type integer, minimum: 1, maximum: 10, default: 1), and tier (type string, enum: [dev, stage, prod], default: dev).

required goes inside the spec object as an array. Numeric fields can use minimum, maximum, and default, and string fields can use enum. Note that a default value must be one that passes validation.

Apply it and confirm it appears in the API list

Apply it with kubectl apply -f /root/crd/crd.yaml, check that the Established condition is True, then save the output of kubectl api-resources --api-group=apps.labhub.io to /root/crd/out/api-resources.txt.

It is not usable immediately after applying. One of the CRD's status conditions must become True before the API server accepts the type. You confirm that the new type is actually registered from the API resource list.

Add columns to show in kubectl get

Add additionalPrinterColumns to version v1. There are four columns: Image (type string, jsonPath .spec.image), Replicas (type integer, jsonPath .spec.replicas), Tier (type string, jsonPath .spec.tier), and Age (type date, jsonPath .metadata.creationTimestamp).

You attach additionalPrinterColumns per version. Each column needs three things: name, type, and jsonPath, and a time column must have type date to be shown as relative time.

Enable the status and scale subresources

Enable subresources.status: {} and subresources.scale on version v1. For scale, use specReplicasPath: .spec.replicas, statusReplicasPath: .status.replicas, and labelSelectorPath: .status.selector. At the same time, define properties.status in the schema as type: object and put under it replicas (integer), selector (string), observedGeneration (integer), and conditions (type array, where items is type object, type, status, reason, and message are strings, and lastTransitionTime is a string). Status fields not in the schema are pruned and not stored.

Enabling the subresource is not enough. If you do not define the status fields in the schema, they are not stored even when you write them, because of pruning. For scale you must give three paths, and one of them is what the HPA uses to count Pods.

Serve two versions and keep exactly one storage version

Add name: v1alpha1 to spec.versions. v1alpha1 has served: true and storage: false, and v1 has served: true and storage: true. v1alpha1 must also have a schema, so copy the v1 schema as it is. After reapplying, check that v1 is present in the output of kubectl get crd webservices.apps.labhub.io -o jsonpath='{.status.storedVersions}'.

In apiextensions/v1, every version must have its own schema. served and storage mean different things, and exactly one version must have storage set to true. After applying, look at whether the storage version is recorded in the CRD's status.

Create and query your first custom resource

Create a namespace with kubectl create ns crd-lab, then apply /opt/lab/fixtures/crd/sample-cr.yaml to create sample (spec.image must include the tag). Then save the output of kubectl get webservice -n crd-lab to /root/crd/out/get-ws.txt, and the output of kubectl get --raw /apis/apps.labhub.io/v1/namespaces/crd-lab/webservices/sample/status to /root/crd/out/status.json.

You must create the namespace first. To check that the custom columns actually show, save the ordinary query output, and read status separately through the subresource path rather than a regular query. There is a way to hit the raw API path directly.