PATTERN Cited by 1 source
Reuse runtime logic on admission path¶
Problem¶
A control-plane resource (Kubernetes Ingress / RouteGroup,
a CRD, a config-map) carries a domain-specific DSL in an
annotation, string field, or opaque payload. The API server
can validate the shape of the resource (is this an object,
are the required fields present, is the type correct) but has
no knowledge of the DSL itself. So a manifest can be
syntactically valid YAML containing a reference to a
predicate, filter, function, or identifier that does not
exist — or exists but has wrong argument types — and the API
server will happily write it to etcd.
The broken object then propagates to whichever controller
reads it at runtime. The failure surfaces there: in request
logs, in a rejected request, in an alert — far from the
kubectl apply that introduced the mistake, often to a
different team's inbox. Fixing it requires bisecting back to
the original change.
A naïve solution is to write a second validator — a standalone schema checker, an OPA policy, a CI lint — that encodes the DSL's rules. That validator will slowly diverge from the runtime's rules: features land in the runtime, fixes land in the runtime, edge-cases land in the runtime; the secondary validator lags. Eventually the two disagree: the CI/admission validator rejects something the runtime would accept, or vice versa. Both outcomes are worse than no validator at all.
Solution¶
Treat the runtime's validator as a library and call it from
the admission path. When a
validating admission
webhook receives an AdmissionReview, have it extract the
domain-specific payload and pass it to the same
validation code the production binary uses at request time.
The answer the webhook returns is by construction the same
answer the runtime would give — they cannot drift because
they are literally the same code.
Operational shape:
- The production code organises its validator as a callable module (filter registry lookup, predicate spec check, route parser, backend parser), not tangled into request- handling.
- A thin admission-webhook adapter takes the
AdmissionReviewJSON, extracts the domain payload (e.g. thezalando.org/skipper-predicateannotation from anIngress), calls the validator, and turns its result into anAdmissionReviewallow/deny response. - The same binary can run in either mode (runtime or webhook) or the validator lives in a shared library that both binaries import.
- A feature flag scoping the new strictness (so baseline validation keeps working if the new rules over-reject) is kept on the webhook side.
The contract the pattern guarantees: if the admission path accepts an object, the runtime path will accept the same object; if the admission path rejects it, the runtime path would also reject it. Drift is structurally impossible.
Why "just write a second validator" loses¶
Two validators means two places to fix bugs, two places to add features, two places for tests to cover. In practice:
- The runtime validator is the one that must be correct — it's between real traffic and real user impact. So that's where engineering attention goes.
- The admission-side validator becomes the lagging copy. It accumulates false positives (rejects things the runtime accepts) until teams start filing tickets, and false negatives (accepts things the runtime rejects, so the original problem surfaces again).
- Eventually someone gives up on the admission-side validator, rolls it back, and the system is back to the original no-admission-validation state — but now with more scar tissue.
The pattern avoids this by making the admission-side validator not a copy. There is only one validator; it just has two call sites.
When not to use¶
- The runtime's validator requires cluster-wide state to decide. E.g. "is this key unique across the keyspace" — a runtime validator that does that has to query etcd or the datastore; at admission-time scale that is too slow or introduces cross-object serialization issues a webhook is poorly suited for.
- The runtime's validator is side-effectful. A validator that mutates state while validating cannot be called at admission time without consequences. Separation of pure validation from side-effects is a prerequisite for using this pattern.
- The runtime is in a language the API server's webhook infrastructure can't conveniently embed. If the runtime is in language X and the webhook host is constrained to language Y, the library-sharing model breaks; you may have to run the validator as a separate service the webhook calls over RPC, which is still one implementation and qualifies, but adds latency.
When to prefer policy engines (OPA/Kyverno) instead¶
Use a policy engine when the rules you want to enforce are cross-cutting, cluster-wide, not domain-DSL-specific — things like "pods must declare resource limits," "no privileged containers in namespace X," "images must be signed." These rules are not in any runtime binary's validator; the policy engine is their canonical home. This pattern is specifically for the case where the runtime already knows how to validate the DSL, and the question is just who else should call that code.
Seen in¶
- sources/2026-04-08-zalando-rejecting-invalid-ingress-routes-at-apply-time
— canonical instance. Zalando's
Skipper HTTP router already has a
full validator: its filter registry (list of callable
filters), predicate specifications (allowed predicate
names and argument types), route parser (eskip DSL), and
backend parser (URL and service reference validation). The
ingress-admitter.teapot.zalan.dowebhook calls the exact same validator on theAdmissionReviewfor anyIngressorRouteGroup. Zavodskikh's framing: "The webhook no longer answers: 'Does this string parse?' — instead it answers: 'Would Skipper accept this route?' That is the difference between basic admission checks and admission- time route validation." At 250+ clusters × ~200k routes × 15k Ingresses, the pattern is what makes the admission-time validator practically maintainable: every time Skipper gains a new filter or predicate, the admission-time check updates automatically; there is nothing to sync. Paired with-enable-advanced-validationfor fast rollback andskipper_route_invalid{route_id, reason}as the rollout gate. Shipped in open-source Skipper v0.24.18, so every Skipper operator gets the same architectural shape.
Related¶
- systems/skipper-proxy · systems/kubernetes · companies/zalando
- concepts/validating-admission-webhook · concepts/shift-left-validation · concepts/control-plane-data-plane-separation · concepts/feature-flag-rollback-for-validator
- patterns/invisible-rollout-via-default-on-validation · patterns/three-mode-rollout-off-shadow-exec · patterns/feature-flagged-dual-implementation