API Versioning and Graduation Policy¶
This document defines the rules for evolving the fleetmanagement.grafana.com API group: how versions graduate from v1alpha1, how breaking changes are introduced, and how long deprecated versions remain served.
Current state¶
- All CRDs ship at
v1alpha1and are the only served, stored version. - No conversion webhook is wired up yet.
- Seven CRDs in scope:
Pipeline,PipelineDiscovery,Collector,RemoteAttributePolicy,ExternalAttributeSync,CollectorDiscovery,TenantPolicy.
Graduation criteria — v1alpha1 to v1¶
A CRD is considered ready to graduate to v1 only when all of the following hold:
- Known design gaps closed. Every gap documented in the repository's
CLAUDE.md("V1 gaps"), tenant-policy.md, and the internal production-readiness audit is either resolved or re-classified as v2 work with a documented rationale. - Schema validation parity in CEL. Every webhook validation rule that is structurally expressible (length caps, prefix bans, simple shape checks) is mirrored as either an OpenAPI structural rule or a CEL
XValidation. The Go webhook stays in place as defence-in-depth, but the API server itself rejects malformed CRs even with the webhook offline. - Status discipline. Every CRD has a status subresource, an
observedGenerationfield, and at least one documented condition type per the registry indocs/conditions.md. - Production soak. At least one production deployment has been running on the candidate spec for ≥3 months without an incident that would have required a CRD-shape change.
- Printer columns finalised. All printer columns expose a typed field (no string-array-as-integer coercion). Standard columns present:
Age,Ready. - Documentation alignment. User-facing samples (
config/samples/*.yaml) are valid against the v1 schema; the chart, RBAC, and webhook configurations install cleanly.
Path to v1 — hub-and-spoke conversion¶
When the gates above are met:
- Add
v1as a served, non-storage version alongsidev1alpha1. v1alpha1remains the storage version through the deprecation window — no etcd rewrites, no migration job.- Stand up a conversion webhook before flipping any defaults. Round-trip every supported field; envtest the conversion before the served-version change merges.
- After ≥6 months of
v1being available and at least one minor release exposing it, switch the storage version tov1. - After a further ≥6 months, mark
v1alpha1as not served (still stored where applicable until an explicit storage migration). The deprecation timer forv1alpha1removal starts when "not served" ships and is recorded in this document.
Deprecation policy¶
- Notice window: ≥6 months between announcement and removal for any served-version drop, field rename, type change, or default change visible to users.
- Announcements appear in:
- The release notes for the version that introduces the deprecation.
- This document, in a "Deprecations in flight" section (added when needed; absent today).
- A
Deprecatedcondition on affected CRs where applicable (e.g. when a single field is being removed but the CR is otherwise valid). - Field renames go through additive-then-remove:
- Introduce the new field; both fields are accepted; the new field wins on read.
- Mark the old field deprecated in godoc + release notes.
- After the notice window, remove the old field in the next served version (not in
v1alpha1patches). - Type changes are not allowed in served versions — they require a new field name and the deprecate-then-remove dance above.
- Default changes count as breaking. Same window, same notice.
Known v1 blockers¶
These items must be resolved (or explicitly punted) before any CRD graduates to v1. They are sourced from the production-readiness audit (2026-04-28) and CLAUDE.md.
- TenantPolicy
selector.collectorIDsbypass. Required-matcher enforcement does not apply when a CR usesspec.selector.collectorIDs. Either close the gap or add a documentedallowedCollectorIDsfield. - TenantPolicy required-matcher semantics. No reasoning about negation or regex.
team!=billingandteam=~.*both pass ateam=billingrequirement. A "strict mode" with subset semantics would be a CRD-compatible upgrade. - TenantPolicy coverage.
CollectorandCollectorDiscoveryare not subject to TenantPolicy enforcement. status.matchedCollectorIDsunbounded onRemoteAttributePolicyandExternalAttributeSync— at 30k collectors a single status field could grow large enough to bloat etcd. Decide on a cap or move to a count-only summary byv1. (Production-readiness audit findingPERF-01.)- CRD condition vocabulary finalised — see
docs/conditions.md. Any rename here is a breaking change.
Scale subresource (audit finding API-09)¶
The scale subresource is deferred to v1+ for every CRD in this group. Pipeline, Collector, RemoteAttributePolicy, PipelineDiscovery, ExternalAttributeSync, CollectorDiscovery, and TenantPolicy are not workload CRDs — kubectl scale semantics ("set replicas") do not map cleanly onto "make this Pipeline match more collectors", "import more Fleet pipelines", or "bind this Policy to more subjects". We have not identified a user workflow that needs it. Revisit only if a concrete request surfaces.
Conversion webhook — out of scope today¶
Until at least one v1 version is on the roadmap, no conversion webhook is added. The single served version makes conversion a no-op and we deliberately avoid setting up infrastructure that has nothing to convert. When the first v1 type lands, the webhook is added in the same change.
Conversion webhook scaffolding¶
api/v1alpha1/hub.go declares the Hub() method on every type, satisfying the controller-runtime/pkg/conversion.Hub interface. This is the prerequisite for kubebuilder's conversion generation.
When v1beta1 is ready: 1. kubebuilder create api --group fleetmanagement --version v1beta1 --kind <Kind> 2. make generate — produces zz_generated.conversion.go stubs 3. Implement ConvertTo(dst *v1beta1.Kind) / ConvertFrom(src *v1beta1.Kind) 4. Register the conversion webhook in cmd/main.go 5. Set storage: true on v1beta1 in CRD; storage: false on v1alpha1
Allowed and forbidden schema changes in v1alpha1¶
| Change class | Allowed? | Notes |
|---|---|---|
| Add optional field (no default) | Yes | Older clients omit it; ignored by cache |
| Add required field | No | Rejects all existing stored CRs on next touch |
| Remove field | No | Breaks deserialization of existing stored objects |
| Rename field | No | Equivalent to remove+add; needs conversion webhook |
| Tighten validation (stricter) | No | Would reject currently-valid stored CRs |
| Loosen validation | Yes | Old CRs remain valid |
| Add new condition Type | Yes | Additive; watchers ignore unknown types |
| Add new condition Reason for existing Type | Yes | Additive |
| Rename condition Reason | No | Breaking for dashboards/alerts; additive-then-deprecate window (>=1 release) |
| Add new status field | Yes | Controllers ignore unknown fields from older CRDs |
| Remove status field | No | Needs conversion webhook |
| Add new CRD to the group | Yes | Does not affect existing CRDs |