Skip to content

Commit b985889

Browse files
authored
Add RFC: Instrumentation v1beta1 (#5079)
* Add RFC: Instrumentation v1beta1 Signed-off-by: Pavol Loffay <p.loffay@gmail.com> * Some review comments Signed-off-by: Pavol Loffay <p.loffay@gmail.com> * Add CRD versioning explanation Signed-off-by: Pavol Loffay <p.loffay@gmail.com> * Add CRD versioning explanation Signed-off-by: Pavol Loffay <p.loffay@gmail.com> * Add supporting doc Signed-off-by: Pavol Loffay <p.loffay@gmail.com> * Add section about migration Signed-off-by: Pavol Loffay <p.loffay@gmail.com> * Add labels support Signed-off-by: Pavol Loffay <p.loffay@gmail.com> * Review comments Signed-off-by: Pavol Loffay <p.loffay@gmail.com> * Review comments Signed-off-by: Pavol Loffay <p.loffay@gmail.com> * Fix review comments Signed-off-by: Pavol Loffay <p.loffay@gmail.com> --------- Signed-off-by: Pavol Loffay <p.loffay@gmail.com>
1 parent e31ed7d commit b985889

2 files changed

Lines changed: 847 additions & 0 deletions

File tree

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
# CRD Version Graduation Strategies
2+
3+
This is a supporting document for the [Instrumentation v1beta1 RFC](instrumentation-v1beta1.md). It outlines how Kubernetes handles multiple CRD versions, strategies for operator maintainers, and lessons learned from other projects.
4+
5+
## Background
6+
7+
When graduating a CRD from `v1alpha1` to `v1beta1` (or `v1beta1` to `v1`), operators face a choice: how to handle the transition for existing users? Kubernetes supports serving multiple versions of the same CRD simultaneously, but this comes with complexity.
8+
9+
## Kubernetes CRD Versioning Basics
10+
11+
### Storage Version
12+
13+
Only one version can be the **storage version** - the version persisted in etcd. All other versions are converted to/from this version.
14+
15+
```yaml
16+
apiVersion: apiextensions.k8s.io/v1
17+
kind: CustomResourceDefinition
18+
spec:
19+
versions:
20+
- name: v1alpha1
21+
served: true
22+
storage: false # Not stored, converted from v1beta1
23+
- name: v1beta1
24+
served: true
25+
storage: true # Stored in etcd
26+
```
27+
28+
### Served Versions
29+
30+
The `served` field controls whether the API server accepts requests for that version.
31+
32+
**When `served: true`:**
33+
- Clients can create, read, update, and delete resources using that version (e.g. `instrumentations.v1alpha1.opentelemetry.io`)
34+
- Resources are auto-converted to/from the storage version
35+
36+
**When `served: false`:**
37+
- API server returns 404 for that version's endpoint
38+
- `kubectl get instrumentations.v1alpha1.opentelemetry.io` fails
39+
- Existing resources in etcd are still accessible via served versions - with `strategy: None`, the API server just swaps the `apiVersion` field (requires identical schemas)
40+
- New resources cannot be created using that version
41+
42+
### Conversion Strategies
43+
44+
| Strategy | When to Use |
45+
|----------|-------------|
46+
| `None` | Schemas are identical (only apiVersion differs) |
47+
| `Webhook` | Schemas differ (field renames, restructuring, removals) |
48+
49+
If schemas differ and you use `None`, data won't map correctly between versions:
50+
- **Renamed fields**: `foo` in `v1alpha1` won't appear in `bar` in `v1beta1` — appears empty
51+
- **Restructured fields**: `spec.exporter.endpoint` won't map to `spec.envConfig.exporter.endpoint`
52+
- **Removed fields**: Data preserved in etcd but invisible in new schema
53+
54+
#### CRD conversions examples
55+
56+
**Example: No conversion (identical schemas)**
57+
58+
```yaml
59+
apiVersion: apiextensions.k8s.io/v1
60+
kind: CustomResourceDefinition
61+
spec:
62+
conversion:
63+
strategy: None
64+
```
65+
66+
**Example: Webhook conversion**
67+
68+
```yaml
69+
apiVersion: apiextensions.k8s.io/v1
70+
kind: CustomResourceDefinition
71+
spec:
72+
conversion:
73+
strategy: Webhook
74+
webhook:
75+
conversionReviewVersions: ["v1"] # ConversionReview API versions the webhook accepts (not CRD versions)
76+
clientConfig:
77+
service:
78+
namespace: opentelemetry-operator-system
79+
name: opentelemetry-operator-webhook
80+
path: /convert
81+
```
82+
83+
## Strategy 1: Conversion Webhook
84+
85+
Implement a webhook that converts between versions automatically.
86+
87+
### Pros
88+
89+
- **Seamless migration**: Users can continue using old version, resources auto-convert
90+
- **No forced migration**: Users upgrade at their own pace
91+
- **Backwards compatible**: Old tools/scripts continue working
92+
93+
### Cons
94+
95+
- **Deployment complexity**: Webhook requires TLS certificates, secrets, and firewall rules on GKE private clusters (default firewall only allows ports 443/10250 from control plane to nodes — webhooks on other ports require custom firewall rules)
96+
- **Maintenance burden**: Must maintain conversion logic
97+
- **Helm complexity**: Webhooks need complex orchestration in Helm charts (OpenTelemetry operator solved this with templated CRDs — see [UPGRADING.md](https://github.com/open-telemetry/opentelemetry-helm-charts/blob/main/charts/opentelemetry-operator/UPGRADING.md))
98+
99+
### OpenTelemetry Collector `v1alpha1` → `v1beta1` Experience
100+
101+
The OpenTelemetry Operator implemented a conversion webhook for OpenTelemetryCollector v1alpha1 → v1beta1. Key issues encountered:
102+
103+
**Helm chart complications:**
104+
- Webhook service name must be templated for custom Helm release names ([helm-charts#1167](https://github.com/open-telemetry/opentelemetry-helm-charts/issues/1167))
105+
- Users get "service opentelemetry-operator-webhook not found" errors ([helm-charts#1199](https://github.com/open-telemetry/opentelemetry-helm-charts/issues/1199))
106+
107+
*Why this happens:* CRDs are cluster-scoped with hardcoded webhook service references, but Helm prefixes resource names with the release name (e.g., `helm install my-otel ...` creates `my-otel-opentelemetry-operator-webhook`). The CRD references `opentelemetry-operator-webhook`, but the actual service has a different name.
108+
109+
**OLM install mode restriction:**
110+
- Only `AllNamespaces` install mode supported (operator watches all namespaces) — CRDs are cluster-scoped, so conversion webhooks must handle resources from all namespaces, incompatible with `OwnNamespace` mode. OLM v1 is moving away from install modes entirely, but the fundamental constraint remains: conversion webhooks are cluster-scoped.
111+
112+
## Strategy 2: Identical Schemas
113+
114+
Make all breaking changes while still in alpha, then graduate with identical schemas.
115+
116+
Prometheus Operator chose this approach for ScrapeConfig graduation after experiencing pain with conversion webhooks for AlertmanagerConfig. See [ScrapeConfig Graduation Proposal](https://prometheus-operator.dev/docs/proposals/accepted/scrapeconfig-graduation/#path-for-graduation).
117+
118+
When using `strategy: None`, no separate controllers are needed per version:
119+
120+
1. User creates resource using any served version (e.g., `v1alpha1`)
121+
2. API server converts to storage version by changing `apiVersion` field
122+
3. Resource is persisted in etcd as storage version (e.g., `v1beta1`)
123+
4. Controller watches only the storage version using a single Go struct
124+
5. When user reads with old version, API server converts back on the fly
125+
126+
The operator code remains unchanged — it reconciles only the storage version. The API server handles all version transformations transparently.
127+
128+
### Approach
129+
130+
1. Make all breaking changes in `v1alpha1` while it's still alpha (breaking changes are expected)
131+
2. When schema is finalized, graduate to `v1beta1` with identical schema
132+
3. Use conversion strategy `None` — only `apiVersion` changes
133+
4. No conversion webhook needed
134+
135+
### Pros
136+
137+
- No conversion webhook complexity
138+
- No maintenance burden for conversion logic
139+
- Clear expectations — both versions behave identically
140+
- Simple Helm/deployment — no webhook TLS/firewall concerns
141+
142+
### Cons
143+
144+
- **Breaking changes in alpha** — users on v1alpha1 must update their manifests
145+
- **No automatic migration** — users must manually update `apiVersion`
146+
147+
## Cert-Manager `cmctl` Approach
148+
149+
Cert-manager used conversion webhooks for their core CRDs (`Certificate`, `Issuer`, `ClusterIssuer`, `CertificateRequest`) during the transition period while multiple versions were served. They had breaking changes between versions:
150+
151+
- **API group rename**: `certmanager.k8s.io` → `cert-manager.io`
152+
- **Field removals**: `certificate.spec.acme`, `issuer.spec.http01`, `issuer.spec.dns01`
153+
- **Field restructuring**: challenge solver configuration moved to new location
154+
155+
In addition to the runtime conversion webhook, they provide `cmctl convert` — an offline CLI tool for migrating stored manifests before upgrading.
156+
157+
**Version progression:** `v1alpha2` → `v1alpha3` → `v1beta1` → `v1`
158+
159+
| cert-manager | Storage | Served | Notes |
160+
|--------------|---------|--------|-------|
161+
| v1.0 - v1.3 | `v1` | `v1`, `v1beta1`, `v1alpha3`, `v1alpha2` | All versions served |
162+
| v1.4 - v1.5 | `v1` | `v1`, `v1beta1`, `v1alpha3`, `v1alpha2` | Old versions deprecated |
163+
| v1.6 | `v1` | `v1` only | Old versions no longer served |
164+
| v1.7+ | `v1` | `v1` only | Old versions removed from CRD |
165+
166+
### How It Works
167+
168+
```bash
169+
# Convert a single file
170+
cmctl convert -f old-certificate.yaml > new-certificate.yaml
171+
172+
# Convert and apply directly
173+
cmctl convert -f old-certificate.yaml | kubectl apply -f -
174+
175+
# Convert entire directory
176+
cmctl convert -f ./manifests/ --output-dir ./converted/
177+
```
178+
179+
The tool:
180+
1. Parses input YAML with old API version
181+
2. Maps old fields to new field names/locations
182+
3. Applies defaults for new required fields
183+
4. Outputs valid YAML for the new API version
184+
185+
### References
186+
187+
- [Migrating Deprecated API Resources](https://cert-manager.io/docs/releases/upgrading/remove-deprecated-apis/) — official migration guide
188+
- [Upgrading from v0.16 to v1.0](https://cert-manager.io/docs/installation/upgrading/upgrading-0.16-1.0/) — major version upgrade guide
189+
- [Issue #4686: Make cmctl upgrade old API versions](https://github.com/cert-manager/cert-manager/issues/4686) — discussion on migration tooling
190+
191+
## Storage Version Migration
192+
193+
When the storage version changes, existing resources in etcd remain in the old format until updated. The CRD's `status.storedVersions` tracks which versions still have objects in etcd:
194+
195+
```bash
196+
kubectl get crd instrumentations.opentelemetry.io -o jsonpath='{.status.storedVersions}'
197+
# Output: ["v1alpha1","v1beta1"]
198+
```
199+
200+
You cannot remove a version from the CRD while it still appears in `storedVersions`.
201+
202+
### Migration Options
203+
204+
**Manual migration:**
205+
```bash
206+
# Empty patch forces read→convert→write cycle
207+
kubectl get instrumentations -A -o name | xargs -I {} kubectl patch {} -p '{}'
208+
209+
# Or use get + apply
210+
kubectl get instrumentations -A -o yaml | kubectl apply -f -
211+
```
212+
213+
**Automated options:**
214+
215+
| Approach | Description |
216+
|----------|-------------|
217+
| [cmctl upgrade migrate-api-version](https://cert-manager.io/docs/reference/cmctl/) | Cert-manager CLI command; some Helm charts run this in CRD install jobs |
218+
| [kube-storage-version-migrator](https://github.com/kubernetes-sigs/kube-storage-version-migrator) | Kubernetes SIG project; auto-detects storage version changes and migrates |
219+
| [OpenShift migrator operator](https://github.com/openshift/cluster-kube-storage-version-migrator-operator) | Built into OpenShift; requires manual migration request creation |
220+
221+
See [Kubernetes docs: Upgrade existing objects to a new stored version](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/#upgrade-existing-objects-to-a-new-stored-version).
222+
223+
## Removing an Old CRD Version
224+
225+
Removing an old API version from a CRD is a multi-step process that requires careful coordination. Premature removal can make stored objects unreadable.
226+
227+
### Prerequisites
228+
229+
Before removing a version, ensure:
230+
231+
1. **Storage migration complete** — no objects stored in the old version
232+
2. **No clients using the old version** — all tools, controllers, and users migrated
233+
3. **ManagedFields cleaned up** — old version references removed from field ownership tracking
234+
235+
### Step-by-Step Process
236+
237+
#### 1. Stop serving the old version
238+
239+
Set `served: false` for the version being removed. The API server will return 404 for requests to that version:
240+
241+
```yaml
242+
spec:
243+
versions:
244+
- name: v1alpha1
245+
served: false # Stop accepting requests
246+
storage: false
247+
- name: v1beta1
248+
served: true
249+
storage: true
250+
```
251+
252+
At this point:
253+
- New requests to `v1alpha1` fail with 404
254+
- Existing objects are still accessible via `v1beta1`
255+
- Objects stored as `v1alpha1` in etcd are converted on read (requires conversion webhook if schemas differ)
256+
257+
#### 2. Communicate deprecation and provide migration time
258+
259+
For OSS operators, maintainers don't have access to users' clusters. Instead:
260+
261+
- **Deprecation warnings** — log warnings when the operator processes resources using deprecated versions
262+
- **Documentation** — clearly document the deprecation timeline and migration steps
263+
- **Release cadence** — keep deprecated versions served for multiple releases (e.g., 2-3 minor versions)
264+
- **Migration guides** — provide kubectl commands users can run to check their own clusters:
265+
266+
```bash
267+
# Users can check if any manifests reference the old version
268+
kubectl get instrumentations -A -o jsonpath='{range .items[*]}{.apiVersion}{"\n"}{end}' | sort | uniq -c
269+
270+
# Check if old version is still in storedVersions
271+
kubectl get crd instrumentations.opentelemetry.io -o jsonpath='{.status.storedVersions}'
272+
```
273+
274+
Users are responsible for migrating their own resources before upgrading to a release that removes the old version.
275+
276+
#### 3. Run storage version migration
277+
278+
Ensure all objects are re-written in the new storage version:
279+
280+
```bash
281+
# Option A: Manual migration
282+
kubectl get instrumentations -A -o name | xargs -I {} kubectl patch {} -p '{}'
283+
284+
# Option B: StorageVersionMigration resource (Kubernetes 1.30+, beta in 1.35)
285+
kubectl apply -f - <<EOF
286+
apiVersion: storagemigration.k8s.io/v1beta1
287+
kind: StorageVersionMigration
288+
metadata:
289+
name: instrumentation-migration
290+
spec:
291+
resource:
292+
group: opentelemetry.io
293+
resource: instrumentations
294+
EOF
295+
296+
kubectl wait --for=condition=Succeeded storageversionmigration/instrumentation-migration
297+
```
298+
299+
#### 4. Verify storedVersions
300+
301+
Confirm the old version is no longer in `status.storedVersions`:
302+
303+
```bash
304+
kubectl get crd instrumentations.opentelemetry.io -o jsonpath='{.status.storedVersions}'
305+
# Should show: ["v1beta1"]
306+
```
307+
308+
If the old version still appears, patch the status subresource:
309+
310+
```bash
311+
kubectl patch crd instrumentations.opentelemetry.io \
312+
--subresource='status' \
313+
--type='merge' \
314+
-p '{"status":{"storedVersions":["v1beta1"]}}'
315+
```
316+
317+
**Warning:** Only patch `storedVersions` after confirming all objects have been migrated. Incorrectly removing a version from `storedVersions` can make objects unreadable.
318+
319+
#### 6. Remove the version from the CRD
320+
321+
Once storage migration is complete:
322+
323+
```yaml
324+
spec:
325+
versions:
326+
- name: v1beta1 # Only the new version remains
327+
served: true
328+
storage: true
329+
```
330+
331+
#### 7. Remove conversion webhook support
332+
333+
If using a conversion webhook, remove the code handling the old version. This reduces maintenance burden and potential bugs.
334+
335+
## References
336+
337+
- [Kubernetes CRD Versioning](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/)
338+
- [Kubernetes Storage Version Migration](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/storage-version-migration/)
339+
- [Cluster API: Removal of old apiVersions in CRDs](https://github.com/kubernetes-sigs/cluster-api/issues/11894)
340+
- [Prometheus Operator ScrapeConfig Graduation](https://prometheus-operator.dev/docs/proposals/accepted/scrapeconfig-graduation/)
341+
- [Prometheus Operator AlertmanagerConfig v1beta1 Issue](https://github.com/prometheus-operator/prometheus-operator/issues/4677)
342+
- [Helm Charts v1beta1 Missing Issue](https://github.com/prometheus-community/helm-charts/issues/5168)
343+
- [Kubernetes Bug: Conversion for Unserved Versions](https://github.com/kubernetes/kubernetes/issues/129979)

0 commit comments

Comments
 (0)