Handing Validation Off to the API Server
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.
- In
/root/crd/validate/no-image.yaml, write a WebService withmetadata.name: no-image(namespacecrd-lab) and noimageinspec, and try applying it. Save the failure output, including standard error, to/root/crd/validate/out/err-required.txt. The output must containspec.imageand wording about a required value, andno-imagemust not remain in the cluster. - In
/root/crd/validate/bad-type.yaml, writemetadata.name: bad-type,spec.image: nginx:1.27, andspec.replicas: "three", apply it, and save the failure output to/root/crd/validate/out/err-type.txt. - In
/root/crd/validate/bad-tier.yaml, writemetadata.name: bad-tier,spec.image: nginx:1.27, andspec.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. - In
/root/crd/validate/too-many.yaml, writemetadata.name: too-many,spec.image: nginx:1.27, andspec.replicas: 50, apply it, and save the failure output to/root/crd/validate/out/err-range.txt. - In
/root/crd/validate/defaulted.yaml, write onlymetadata.name: defaultedandspec.image. Never writespec.replicasandspec.tier. After applying, check that the stored object hasreplicas: 1andtier: devfilled in. - Add
properties.spec.properties.extrato the v1 schema in/root/crd/crd.yamlwithtype: objectandx-kubernetes-preserve-unknown-fields: true, and reapply the CRD. Then in/root/crd/validate/pruned.yaml, putmetadata.name: pruned,spec.image, andspec.bogus: anything, which is not in the schema, and apply it (bogusmust disappear from the stored object), and in/root/crd/validate/preserved.yaml, putmetadata.name: preserved,spec.image, andspec.extra.custom: kept, and apply it (this value must remain). - Add an
x-kubernetes-validationsarray directly underproperties.specin the v1 schema. Theruleof the first rule isself.tier != 'prod' || self.replicas >= 2, and themessageisprod 계층은 복제본이 2개 이상이어야 합니다(the Korean text, meaning "the prod tier must have at least 2 replicas"). After reapplying, in/root/crd/validate/cel-violation.yaml, putmetadata.name: cel-violation,spec.image,spec.tier: prod, andspec.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. - Create
/root/crd/validate/out/matrix.json. The top-level key iscases, and each element of the array has four keys:name,expected(rejectedoraccepted),actual, andrule. 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 caseexpectedandactualmust be the same.
Notes
- Lab Pods start fresh for every lab, so the cluster state from the previous lab is not there. Still, if you leave your declarations as files, you can rebuild the same state on any Pod — this is the practical advantage of the declarative approach.
- To save apply failure output to a file, you must capture standard error too, as in
kubectl apply -f 파일 > 출력파일 2>&1(where the first placeholder is the file to apply and the second is the output file). The rejection message is not on standard output. - If you copy
/opt/lab/fixtures/crd/sample-cr.yamland change only the values, you can build the cases quickly. - CEL rules are evaluated after defaults are filled in, so
self.tierandself.replicasalways exist. - Common mistake 1: writing
replicasin the manifest in step 5. Then you cannot tell whether the default was filled in or you wrote it yourself, and grading fails. - Common mistake 2: applying
x-kubernetes-preserve-unknown-fieldsto the whole spec in step 6. Then pruning is turned off across the entire spec andbogussurvives as well. Apply it only underextra. - Common mistake 3: copying the expected value into
actualin step 8. You must write the result you confirmed by actually querying the cluster, and the grader checks it against the cluster.
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.