TT Lab
Get started
Learn Learning paths Courses

CRDs and Operators

Handing Validation Off to the API Server

Continue in TT Lab

Goal

Throw resources that deliberately break the rules at the API server, collect for yourself what it rejects and how, and confirm that default injection, pruning, and CEL validation are all handled by a single schema.

Why it matters

The real benefit of moving validation into the schema is not "it rejects" but "it rejects and tells the user what is wrong and why." When you add an enum, the rejection message includes the allowed list, and when you add maximum, the upper limit goes straight into the wording. Users can fix things just by reading the error, without digging through a wiki. Pruning is disorienting at first — a typo'd field quietly disappears without even an error. But this is the mechanism that enforces "the schema is the contract," which is why a spot that must accept arbitrary keys should be opened only as an exception, and only that spot. Finally, CEL changed the game. Previously, for a cross-field constraint like "at least 2 replicas if prod," you had to run a validating webhook server, manage certificates, and accept the risk that if that webhook died, the cluster would be paralyzed. Now it is one line in the schema.

Steps

Before you start: lab Pods start fresh for every lab, so the cluster state you created in the previous lab is not there. If kubectl get crd webservices.apps.labhub.io returns nothing, rewrite the CRD from the previous lab to /root/crd/crd.yaml, apply it, and also run kubectl create ns crd-lab. The schema this lab requires is spec.required: [image], replicas (integer, minimum 1, maximum 10, default 1), and tier (string, enum [dev, stage, prod], default dev), and in steps 6 and 7 you will add spec.extra and a CEL rule to it.

  1. In /root/crd/validate/no-image.yaml, write a WebService with metadata.name: no-image (namespace crd-lab) and no image in spec, and try applying it. Save the failure output, including standard error, to /root/crd/validate/out/err-required.txt. The output must contain spec.image and wording about a required value, and no-image must not remain in the cluster.
  2. In /root/crd/validate/bad-type.yaml, write metadata.name: bad-type, spec.image: nginx:1.27, and spec.replicas: "three", apply it, and save the failure output to /root/crd/validate/out/err-type.txt.
  3. In /root/crd/validate/bad-tier.yaml, write metadata.name: bad-tier, spec.image: nginx:1.27, and spec.tier: qa, apply it, and save the failure output to /root/crd/validate/out/err-enum.txt. The output must also show the allowed values.
  4. In /root/crd/validate/too-many.yaml, write metadata.name: too-many, spec.image: nginx:1.27, and spec.replicas: 50, apply it, and save the failure output to /root/crd/validate/out/err-range.txt.
  5. In /root/crd/validate/defaulted.yaml, write only metadata.name: defaulted and spec.image. Never write spec.replicas and spec.tier. After applying, check that the stored object has replicas: 1 and tier: dev filled in.
  6. Add properties.spec.properties.extra to the v1 schema in /root/crd/crd.yaml with type: object and x-kubernetes-preserve-unknown-fields: true, and reapply the CRD. Then in /root/crd/validate/pruned.yaml, put metadata.name: pruned, spec.image, and spec.bogus: anything, which is not in the schema, and apply it (bogus must disappear from the stored object), and in /root/crd/validate/preserved.yaml, put metadata.name: preserved, spec.image, and spec.extra.custom: kept, and apply it (this value must remain).
  7. Add an x-kubernetes-validations array directly under properties.spec in the v1 schema. The rule of the first rule is self.tier != 'prod' || self.replicas >= 2, and the message is prod 계층은 복제본이 2개 이상이어야 합니다 (the Korean text, meaning "the prod tier must have at least 2 replicas"). After reapplying, in /root/crd/validate/cel-violation.yaml, put metadata.name: cel-violation, spec.image, spec.tier: prod, and spec.replicas: 1, apply it, and save the failure output to /root/crd/validate/out/err-cel.txt. That output must contain the message above exactly.
  8. Create /root/crd/validate/out/matrix.json. The top-level key is cases, and each element of the array has four keys: name, expected (rejected or accepted), actual, and rule. Include all five rejections (no-image/required, bad-type/type, bad-tier/enum, too-many/maximum, cel-violation/cel) and three acceptances (defaulted/default, pruned/pruning, preserved/preserve-unknown-fields), and in every case expected and actual must be the same.

Notes

See a missing required field get rejected

In /root/crd/validate/no-image.yaml, write a WebService with metadata.name: no-image (namespace crd-lab) and no image in spec, and try applying it. Save the failure output, including standard error, to /root/crd/validate/out/err-required.txt. The output must contain spec.image and wording about a required value, and no-image must not remain in the cluster.

This is a step where you fail on purpose. The rejection message comes out on standard error, so to save it to a file you must capture standard error as well. The rejected resource must not remain in the cluster.

See a type mismatch get rejected

In /root/crd/validate/bad-type.yaml, write metadata.name: bad-type, spec.image: nginx:1.27, and spec.replicas: "three", apply it, and save the failure output to /root/crd/validate/out/err-type.txt.

In YAML, wrapping a number in quotes makes it a string. Save the wording that appears when the schema expects an integer.

See an enum violation and the allowed-list guidance

In /root/crd/validate/bad-tier.yaml, write metadata.name: bad-tier, spec.image: nginx:1.27, and spec.tier: qa, apply it, and save the failure output to /root/crd/validate/out/err-enum.txt. The output must also show the allowed values.

An enum violation message does not just reject; it also tells you what is possible. That list must be in the output.

See an out-of-range value get rejected

In /root/crd/validate/too-many.yaml, write metadata.name: too-many, spec.image: nginx:1.27, and spec.replicas: 50, apply it, and save the failure output to /root/crd/validate/out/err-range.txt.

Give a value that exceeds maximum. The message mentions the upper limit as it is.

Confirm that defaults fill in fields you did not write

In /root/crd/validate/defaulted.yaml, write only metadata.name: defaulted and spec.image. Never write spec.replicas and spec.tier. After applying, check that the stored object has replicas: 1 and tier: dev filled in.

If you write the values in the manifest, you cannot tell whether the defaults were filled in. Leave both fields empty, read the stored object back, and compare.

Unknown fields get pruned, with an exceptional preserve

Add properties.spec.properties.extra to the v1 schema in /root/crd/crd.yaml with type: object and x-kubernetes-preserve-unknown-fields: true, and reapply the CRD. Then in /root/crd/validate/pruned.yaml, put metadata.name: pruned, spec.image, and spec.bogus: anything, which is not in the schema, and apply it (bogus must disappear from the stored object), and in /root/crd/validate/preserved.yaml, put metadata.name: preserved, spec.image, and spec.extra.custom: kept, and apply it (this value must remain).

Fields not in the schema are pruned before storage. A spot that must accept arbitrary keys has to be defined in the schema as an object and then marked to preserve unknown fields. That marker is an extension key that starts with x-.

Express a cross-field constraint with CEL

Add an x-kubernetes-validations array directly under properties.spec in the v1 schema. The rule of the first rule is self.tier != 'prod' || self.replicas >= 2, and the message is prod 계층은 복제본이 2개 이상이어야 합니다 (the Korean text, meaning "the prod tier must have at least 2 replicas"). After reapplying, in /root/crd/validate/cel-violation.yaml, put metadata.name: cel-violation, spec.image, spec.tier: prod, and spec.replicas: 1, apply it, and save the failure output to /root/crd/validate/out/err-cel.txt. That output must contain the message above exactly.

This is a rule you cannot judge by looking at a single field. Put the rule array at the spec object level, and inside a rule, self refers to the current object. The message is the wording users will see, so it appears in the error as it is.

Build a validation matrix and check it against reality

Create /root/crd/validate/out/matrix.json. The top-level key is cases, and each element of the array has four keys: name, expected (rejected or accepted), actual, and rule. Include all five rejections (no-image/required, bad-type/type, bad-tier/enum, too-many/maximum, cel-violation/cel) and three acceptances (defaulted/default, pruned/pruning, preserved/preserve-unknown-fields), and in every case expected and actual must be the same.

Organize the cases you made in the earlier steps into a table. For each case, write the name, expected result, actual result, and the rule that triggered, and not a single expected result may differ from its actual result.