[Operational] Pilot — the Cluster CRD, then an unrelated one
Status: draft for review. The checker's output below is deterministic and reproducible. The triage — whether each finding is a real defect, and what to change — was drafted by the ruleset's author with AI assistance and has not been reviewed by either owning team. Nothing has been routed yet; see Routing. Scope: IPCEICIS-11214.
What was run
| CRD | Why this one | |
|---|---|---|
| Pilot | KubernetesCluster in platform.edgeconnect.eu | Taken to be the Cluster CRD of the first IPCEICIS-10884 feature proof: the user-facing cluster kind. An assumption, to confirm against 10884 and the Cluster exercise of IPCEICIS-11223. Its provider-facing twin in provider.cluster.edgeconnect.eu is, by upstream's own example, created by the controller rather than by a caller |
| Unrelated | Tenant in tenancy.edgeconnect.eu | Another domain, repository and team — and the most-measured kind in this repository, so the checker can be held against behavioural evidence |
| Calibration | all 27 pinned CRDs | The same implementation over every substrate this repository holds |
Ruleset crd-readiness 2.0.0 (sha256:5d407f88…), checker 1.1.0 (sha256:4b6ad3ae…), Python 3.13.0,
PyYAML 6.0.3, jsonschema 4.26.0 — as recorded in each report. Ruleset 2.1.0 adds only an authority tag per rule and
changes no status, so these reports stand; a re-run shows the tags.
Inputs
| Input | Source | Fidelity |
|---|---|---|
| Cluster CRD | crds/platform.edgeconnect.eu_kubernetesclusters.yaml, pinned from cluster-controller-manger @ 899dc4f4 (Source B) | Upstream main was still at 899dc4f4 on 2026-09-23; byte-identical |
| Cluster example | inputs/cluster-controller-manger/examples/kubernetescluster.yaml | Upstream examples/kubernetescluster.yaml @ 899dc4f4, verbatim (file sha256 d8bae0be…). Authentic, but not owner-confirmed: re-run with the owner-confirmed known-good example from contract intake (IPCEICIS-11159) when it exists |
| Cluster RBAC | inputs/cluster-controller-manger/rbac/clusterrole.yaml | Rendered from upstream deploy/charts/cluster-controller @ 899dc4f4 with Helm v3.16.4, command below |
| Tenant CRD | crds/tenancy.edgeconnect.eu_tenants.yaml, chart edge-connect-crds 0.2.30 (Source A) | Held at 0.2.30 on purpose; upstream has moved |
| Tenant example | examples/tenant-acme.yaml | Authentic, from poc-core-deploy |
| Tenant RBAC | not supplied | tenant-controller RBAC is not pinned here at the CRD's version, so rbac.evidence reports not evaluated |
| API server | a local kube-apiserver v1.30.3 | The envtest binaries, SHA-512 verified, with the 27 pinned CRDs installed; no webhooks and no controllers. Set up as in the readiness README |
The chart will not render without a kcp kubeconfig Secret name. The value supplied names a Secret the ClusterRole never references, and the release name is fixed so the rendered labels are reproducible:
git clone https://edp.buildth.ing/DevFW-CICD/cluster-controller-manger ccm
git -C ccm checkout 899dc4f40855bdccd58e36beca3abf981fd42d22
helm template cluster-controller ccm/deploy/charts/cluster-controller \
--set kcp.connection.kubeconfigSecret.name=kcp-kubeconfig --show-only templates/clusterrole.yaml
Reproduce
uv run tools/readiness.py crds --crd kubernetesclusters.platform.edgeconnect.eu \
--examples readiness/pilot/inputs/cluster-controller-manger/examples \
--rbac readiness/pilot/inputs/cluster-controller-manger/rbac \
--context readiness-envtest --report readiness/pilot/kubernetescluster
uv run tools/readiness.py crds --crd tenants.tenancy.edgeconnect.eu \
--examples examples/tenant-acme.yaml --context readiness-envtest --report readiness/pilot/tenant
Without --context the same commands reproduce every offline result, and the three dry-run rules report not
evaluated.
Reports: kubernetescluster/readiness.md and
tenant/readiness.md, each beside the JSON it was rendered from.
KubernetesCluster — findings and triage
1 major, 4 minor, 2 info; 21 decided checks pass, the three dry-run checks among them; 12 manual, behavioural and live rules not evaluated.
| Severity | Rule | Finding | Triage | Proposed change, for the owners |
|---|---|---|---|---|
| major | naming.cross-group | also served by provider.cluster.edgeconnect.eu | True, known since 2026-08-24 (decision register). ADR-0011 moves both groups and leaves the collision | A distinct name for the provider-facing kind, which callers do not author; failing that, a first-line disambiguation in both descriptions |
| minor | naming.vocabulary | head word cluster reads as Cluster API's Cluster | Plausible, low priority. The modifier narrows it and no run has measured it | None on its own; revisit only if the rename above happens anyway. A suppression candidate |
| minor | docs.enum-values | status.status: Ready, NotReady, Pending, Error, none explained | True. What separates NotReady from Pending or Error cannot be inferred | One clause per value in the Go comment on the field |
| minor | status.condition-shape | conditions are not a list-map keyed by type | True | +listType=map and +listMapKey=type on the conditions field |
| minor | lifecycle.deletion | nothing says what deleting one does; the controller's ClusterRole may set its finalizers | True, and the most consequential. This kind provisions real infrastructure, and agents in this repository deleted and recreated objects to get around a rule (IPCEICIS-10005, IPCEICIS-10595) | One or two sentences in the kind description: does deletion deprovision the cluster, wait on cleanup, or orphan it? Whether that is then true is live.deletion-cleanup |
| info | discoverability.categories | no categories | True, cosmetic | One category shared by the four cluster-provisioning kinds |
| info | discoverability.printer-columns | a listing shows NAME and AGE only | True | Columns for status.status and spec.kubernetes.version |
What passed is worth as much: the upstream example conforms offline; the controller's ClusterRole grants the kind,
its status and its finalizers; status is served through the subresource and carries conditions and
observedGeneration; all 12 authored fields are described; the schema is structural. Against the API server, the
manifest is accepted, discovery serves it as declared, and the upstream example survives a strict dry-run create.
Manual observations — not checker output, for whoever holds the manual review:
spec.nodeCountsays "min == max == desired in the PoC" — prototype wording in a published contract.spec.kubernetes.versionis free text with one example and no constraint. Which versions a provider accepts is not in the contract; that may be deliberate, and only the owners can say.
Tenant — the same checker, unchanged
2 major, 14 minor, 1 info. The question worth asking of an unrelated CRD is whether the checker agrees with what this repository measured by running agents against it:
Measured behaviourally (analysis/crd-design.md) | Decidable by a machine? (CRD_AUTHORING.md) | This checker |
|---|---|---|
| The name competes with namespace-as-tenant: 56% wrong kind | partly | naming.vocabulary flags it |
spec.displayName has no description; every arm wrote a label instead | yes, the absence | docs.field-descriptions flags it |
| Agents read the slug immutability rule, then deleted and recreated the tenant | partly, the wording | validation.cel-alternative flags "slug is immutable" |
| Sibling kinds' "platform-internal CRD reconciled by X-controller" | no (rule 5) | manual.kind-purpose: not evaluated |
Provider and Tenant collide across the consumer/supplier boundary | no | not evaluated — behavioural |
claimMappings is a bare map with no key vocabulary (on OrgWorkspace) | yes | docs.freeform-map, in the calibration run |
Every measured defect that the authoring guideline calls mechanically decidable is flagged, and every one it calls
undecidable is reported as not evaluated rather than passed. Three findings are new to the behavioural record:
status.children.tenantPolicyRef names no kind (the TenantPolicy it points at exists only on the kcp tier),
spec.owners[].kind leaves User and Group unexplained, and conditions are not a list-map.
The two levels are easiest to tell apart on this kind. The authentic example is accepted by the API server. The
same example with metadata.name changed to differ from spec.slug still conforms offline, where CEL cannot be
evaluated, and the server rejects it with the CRD's own message: metadata.name must equal spec.slug.
Calibration — all 27 pinned CRDs
The same implementation over every pinned CRD, with every authentic example and no RBAC — one repository's RBAC applied to all 27 would report "no role" for the other 26. Number of CRDs each rule flags:
| Rule | Severity | Basis | CRDs |
|---|---|---|---|
validation.cel-alternative | major | heuristic | 5 |
docs.freeform-map | major | heuristic | 3 |
docs.kind-boilerplate | major | heuristic | 2 — exactly the two workload kinds IPCEICIS-10595 measured agents confusing |
discoverability.reference-target | major | heuristic | 2 |
naming.cross-group | major | decided | 2 |
docs.kind-description | major | decided | 1 |
naming.builtin-kind | major | decided | 1 |
examples.supplied | minor | decided | 22 — this repository holds examples for five kinds only |
lifecycle.deletion | minor | heuristic | 17 of the 19 reconciled kinds |
status.condition-shape | minor | decided | 16 |
docs.field-descriptions | minor | decided | 14 |
naming.vocabulary, naming.near-synonym | minor | heuristic | 11, 10 |
docs.enum-values | minor | heuristic | 10 |
docs.pattern | minor | heuristic | 4 |
status.conditions, status.observed-generation, validation.immutability-stated | minor | mixed | 1 each |
discoverability.categories | info | decided | 26 |
docs.dangling-reference | info | heuristic | 8 |
discoverability.printer-columns, validation.spec-unconstrained, docs.restates-name | info | mixed | 5, 3, 2 |
Reproduce with uv run tools/readiness.py crds --examples examples/*.yaml readiness/pilot/inputs/cluster-controller-manger/examples --report <dir>. The report is not committed: it is
reproducible from pinned inputs, and 750 KB of generated text would go stale with the first checker change.
The likeliest false positives among the major findings are the two path-traversal messages on the workload source kinds: "path must not traverse outside the source root" implies its alternative without stating it.
What the pilot changed in the ruleset
The first pass over this substrate produced three kinds of false positive, all fixed before 1.0.0:
apiVersionandkindcounted as undescribed fields on the two CRDs that declare them bare. They are the object envelope — the same for every kind, not the author's to describe — and are now excluded.validation.required-statedread "whether GPU capability is required" on a boolean flag as a claim that the field is required. Booleans and whether/if/when clauses no longer count.discoverability.reference-targetflagged six*SecretReffields and a reference described as "the Zitadel org". A name ending in a built-in kind, a multi-word kind written as words, and a capitalised type name mid-sentence now state a target. Only a built-in may be matched by a name's tail, sotenantPolicyRefis not read as pointing at an unrelated kind that happens to share its last word.
Each shape now sits in the clean fixture, so none can come back unnoticed. The substrate figures above are
therefore calibration, not validation: the heuristics were tuned against the set they were then run on. The
first CRD set the checker has not seen is the real test, which is why enforcement needs one
(../README.md).
What the real API server changed (ruleset 2.0.0)
The first run against kube-apiserver v1.30.3 found three defects no fixture could have:
- The checker decoded
kubectloutput with the platform code page. On Windows that garbled the dashes inTenant's descriptions, so the installed CRD looked different from the identical manifest and the dry-run level silently reported not evaluated. It now decodes UTF-8. - 1.0.0 misfiled server-enforced names as conventions. The server refuses an uppercase
singularoutright; 1.0.0 called it a minor convention that makes nothing fail. 2.0.0 addsnaming.validfor exactly what the server refuses, checked name by name against it, and narrowsnaming.conventionsto what it accepts either way. - Server messages were cut at 600 characters, which hid the fourth of the four errors the server gave for the broken fixture. They are now kept whole.
With those fixed, every error the server reports for the broken fixture has its offline counterpart:
naming.crd-name, naming.valid, and two schema.structural findings. The run also corrected a claim: a CRD
whose required names an undeclared field is accepted by the server, which then refuses every object.
What the pilot does not show
- The platform's own API surface. The dry-run level ran against a bare kube-apiserver v1.30.3 — no admission webhooks, no controllers, no kcp. It shows what the schema, CEL and defaulting accept, not what the platform's admission chain would.
- Anything live. No custom resource was persisted and no controller ran.
- RBAC beyond one repository. The Tenant run had none.
- Owner review. Every verdict in the triage columns above is a draft until the owning teams answer.
Routing
| Findings | Owner | Status |
|---|---|---|
KubernetesCluster: deletion semantics, enum values, conditions list-map, printer columns, categories | cluster-controller-manger owners | Not yet routed — pending review of this page |
KubernetesCluster served by two groups | cluster-controller-manger owners; the cross-domain owners if the provider-facing contract changes | Known since 2026-08-24; not yet routed as a readiness finding |
Tenant findings | tenant-controller owners | To join the IPCEICIS-10388 hand-off, after a re-run against upstream — the pinned copy is held at 0.2.30 |