Skip to content

Commit 5d89b4e

Browse files
perdasilvaPer G. da Silva
andauthored
Add ClusterObjectSet documentation (#2615)
Signed-off-by: Per G. da Silva <pegoncal@redhat.com> Co-authored-by: Per G. da Silva <pegoncal@redhat.com>
1 parent f7f0352 commit 5d89b4e

1 file changed

Lines changed: 393 additions & 0 deletions

File tree

Lines changed: 393 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,393 @@
1+
# ClusterObjectSets
2+
3+
This document explains what ClusterObjectSets are, how they enable safe phased rollouts of Kubernetes resources, and how operator-controller uses them to manage ClusterExtensions.
4+
5+
## What is a ClusterObjectSet?
6+
7+
A `ClusterObjectSet` is a cluster-scoped Kubernetes API that represents a versioned set of Kubernetes resources organized into ordered phases. It provides a declarative way to roll out a group of related resources sequentially, with built-in readiness checks between phases.
8+
9+
The revision content — the `revision` number, `collisionProtection` strategy, and `phases` (including all objects within them) — is immutable once set. This guarantees that the record of what was deployed at a given revision cannot change after creation. Other fields like `lifecycleState` can change over the object's lifecycle (e.g. transitioning from `Active` to `Archived`), and optional fields like `progressDeadlineMinutes` and `progressionProbes` can be configured independently.
10+
11+
Each ClusterObjectSet has:
12+
13+
- A **revision number** — an immutable, sequential integer that identifies the version
14+
- A **lifecycle state** — either `Active` (being reconciled) or `Archived` (inactive); transitions from Active to Archived but not back
15+
- A list of **phases** — immutable, ordered groups of Kubernetes objects that are applied sequentially
16+
- A **collision protection** strategy — immutable, controlling how pre-existing objects are handled
17+
- **Status conditions** — reporting rollout progress, availability, and success
18+
19+
ClusterObjectSets can be used by any controller or system that needs to manage the rollout of a set of Kubernetes resources in a controlled, phased manner. Within OLM, the operator-controller uses ClusterObjectSets as the mechanism to deploy and upgrade ClusterExtensions.
20+
21+
## Why ClusterObjectSets?
22+
23+
ClusterObjectSets solve several problems that arise when managing sets of related Kubernetes resources:
24+
25+
- **Immutable revision content** — The revision number, phases, objects, and collision protection strategy are immutable once set, providing a clear, auditable record of exactly what was deployed at each revision. There is no ambiguity about what resources belong to a given version.
26+
- **Phased rollout** — Resources are grouped into phases (e.g. CRDs before Deployments) and applied sequentially. A phase only progresses after all its objects pass readiness probes, preventing partially-applied states.
27+
- **Safe transitions** — During a revision change, both the old and new revisions remain active until the new revision fully rolls out. Object ownership transitions from one revision to the next, ensuring no resource is left unmanaged.
28+
- **Single ownership** — Each Kubernetes resource can only be managed by one ClusterObjectSet at a time. This prevents conflicts between revisions fighting over the same object.
29+
- **Large resource support** — Object manifests can be stored inline or externalized into Secrets via references, allowing ClusterObjectSets to manage bundles that would otherwise exceed etcd's 1.5 MiB object size limit.
30+
31+
## Lifecycle
32+
33+
### Active state
34+
35+
When a ClusterObjectSet is `Active`, the controller actively reconciles it using the [boxcutter](https://github.com/package-operator/boxcutter) library:
36+
37+
- Phases are applied sequentially, each waiting for readiness before proceeding
38+
- Objects are managed via server-side apply
39+
- Status conditions are updated to reflect rollout progress
40+
41+
### Revision transitions
42+
43+
When transitioning from one revision to the next:
44+
45+
1. A new ClusterObjectSet is created with the next sequential revision number
46+
2. Both old and new revisions remain `Active` during the transition
47+
3. Objects transition ownership from the old revision to the new one
48+
4. Once the new revision reports `Succeeded`, the old revision can be archived
49+
50+
### Archival
51+
52+
When a revision's lifecycle state is set to `Archived`:
53+
54+
- It is removed from the owner list of all previously managed objects
55+
- Objects that did not transition to a succeeding revision are deleted
56+
- The revision cannot be un-archived
57+
- It remains in the cluster for historical reference until garbage collected
58+
59+
## Phases
60+
61+
Objects within a ClusterObjectSet are organized into phases. Each phase groups related resources, and phases are applied sequentially. Within a phase, all objects are applied simultaneously in no particular order.
62+
63+
A phase only progresses to the next after all of its objects pass their readiness probes. This ensures dependencies are satisfied before dependents are created — for example, CRDs are established before Deployments that use custom resources.
64+
65+
Phase names follow the DNS label standard ([RFC 1123](https://tools.ietf.org/html/rfc1123)) and must be unique within a ClusterObjectSet. A ClusterObjectSet supports up to 20 phases, with up to 50 objects per phase.
66+
67+
### Phase ordering in operator-controller
68+
69+
When operator-controller creates a ClusterObjectSet for a ClusterExtension, it automatically assigns objects to phases based on their GroupKind:
70+
71+
| Order | Phase | Resource Kinds |
72+
| --- | --- | --- |
73+
| 1 | `namespaces` | Namespace |
74+
| 2 | `policies` | NetworkPolicy, PodDisruptionBudget, PriorityClass |
75+
| 3 | `identity` | ServiceAccount |
76+
| 4 | `configuration` | Secret, ConfigMap |
77+
| 5 | `storage` | PersistentVolume, PersistentVolumeClaim, StorageClass |
78+
| 6 | `crds` | CustomResourceDefinition |
79+
| 7 | `roles` | ClusterRole, Role |
80+
| 8 | `bindings` | ClusterRoleBinding, RoleBinding |
81+
| 9 | `infrastructure` | Service, Issuer (cert-manager) |
82+
| 10 | `deploy` | Certificate (cert-manager), Deployment |
83+
| 11 | `scaling` | VerticalPodAutoscaler |
84+
| 12 | `publish` | PrometheusRule, ServiceMonitor, PodMonitor, Ingress, Route, ConsoleYAMLSample, ConsoleQuickStart, ConsoleCLIDownload, ConsoleLink, ConsolePlugin |
85+
| 13 | `admission` | ValidatingWebhookConfiguration, MutatingWebhookConfiguration |
86+
87+
Any resource kind not listed above defaults to the `deploy` phase.
88+
89+
!!! note
90+
This phase ordering is specific to how operator-controller creates ClusterObjectSets. The API itself does not enforce any particular phase ordering — phases are applied in the order they appear in the `spec.phases` list.
91+
92+
## Readiness probes
93+
94+
A phase only progresses to the next after all of its objects pass readiness probes. Several resource kinds have built-in probes:
95+
96+
| Resource Kind | Readiness Criteria |
97+
| --- | --- |
98+
| CustomResourceDefinition | Condition `Established` = True |
99+
| Namespace | `status.phase` = "Active" |
100+
| PersistentVolumeClaim | `status.phase` = "Bound" |
101+
| Deployment | `status.updatedReplicas` == `status.replicas` and condition `Available` = True |
102+
| StatefulSet | `status.updatedReplicas` == `status.replicas` and condition `Available` = True |
103+
| Certificate (cert-manager) | Condition `Ready` = True |
104+
| Issuer (cert-manager) | Condition `Ready` = True |
105+
106+
For custom resources or other objects that need tailored checks, you can define custom progression probes in the `spec.progressionProbes` field. These are experimental and support three assertion types:
107+
108+
`ConditionEqual`
109+
: Checks that an object has a condition of the specified type and status (e.g. `Ready` = `True`).
110+
111+
`FieldsEqual`
112+
: Checks that the values at two field paths match (e.g. `spec.replicas` == `status.readyReplicas`).
113+
114+
`FieldValue`
115+
: Checks that a field has a specific value (e.g. `status.phase` = `"Bound"`).
116+
117+
Probes use selectors to target objects by GroupKind or by label. A probe only runs against objects matching its selector — if no objects in a phase match, the probe is considered to have passed.
118+
119+
## Collision protection
120+
121+
Collision protection controls whether a ClusterObjectSet can adopt pre-existing objects on the cluster. This is configured at three levels, with the most specific taking precedence:
122+
123+
**object > phase > spec**
124+
125+
The available strategies are:
126+
127+
`Prevent`
128+
: Only manages objects the revision created itself. This is the safest option and prevents ownership collisions entirely.
129+
130+
`IfNoController`
131+
: Can adopt pre-existing objects that are not owned by another controller. Useful when taking over management of manually-created resources.
132+
133+
`None`
134+
: Can adopt any pre-existing object, even if owned by another controller. Use with extreme caution — this can cause multiple controllers to fight over the same resource.
135+
136+
## Object storage: inline vs. references
137+
138+
Each object in a phase can be stored in one of two ways (exactly one must be set):
139+
140+
`object`
141+
: The full Kubernetes manifest is embedded inline in the ClusterObjectSet. Simple and self-contained, but contributes to the overall size of the ClusterObjectSet resource in etcd.
142+
143+
`ref`
144+
: A reference to a Secret that holds the serialized object manifest. The `ref` specifies the Secret name, namespace, and data key containing either a JSON-encoded manifest or gzip-compressed JSON bytes. This allows ClusterObjectSets to manage bundles that would otherwise exceed etcd's 1.5 MiB object size limit.
145+
146+
When operator-controller creates ClusterObjectSets for ClusterExtensions, it automatically externalizes objects into immutable Secrets:
147+
148+
- Secrets are packed up to 900 KiB each, leaving headroom below the etcd limit
149+
- Objects larger than 900 KiB are gzip-compressed before storage (ref resolution auto-detects and transparently decompresses gzip-compressed values so consumers see the original JSON manifest)
150+
- Content-addressable naming (based on SHA-256 hashes of the data) ensures that Secret names and data keys are deterministic, making creation idempotent and safe to retry
151+
152+
For a detailed design discussion, see [Large Bundle Support](../../concepts/large-bundle-support.md).
153+
154+
## Status conditions
155+
156+
ClusterObjectSets report three conditions that describe their current state:
157+
158+
### Progressing
159+
160+
Indicates whether the revision is actively rolling out.
161+
162+
| Status | Reason | Meaning |
163+
| --- | --- | --- |
164+
| True | `RollingOut` | Actively making progress |
165+
| True | `Retrying` | Encountered a retryable error |
166+
| True | `Succeeded` | Reached the desired state |
167+
| False | `Blocked` | Error requiring manual intervention |
168+
| False | `Archived` | No longer actively reconciled |
169+
170+
### Available
171+
172+
Indicates whether all objects have been successfully rolled out and pass readiness probes.
173+
174+
| Status | Reason | Meaning |
175+
| --- | --- | --- |
176+
| True | `ProbesSucceeded` | All objects pass readiness probes |
177+
| False | `ProbeFailure` | One or more probes failing |
178+
| Unknown | `Reconciling` | Error prevented probe observation |
179+
| Unknown | `Archived` | Objects torn down after archival |
180+
| Unknown | `Migrated` | Migrated from existing release; probes not yet observed |
181+
182+
### Succeeded
183+
184+
A terminal condition set once the rollout completes. It persists even if the revision later becomes unavailable, marking that this version was successfully deployed at least once.
185+
186+
## How operator-controller uses ClusterObjectSets
187+
188+
Within OLM, ClusterObjectSets serve as the deployment mechanism for `ClusterExtensions`. The operator-controller is one consumer of the ClusterObjectSet API.
189+
190+
### Installation
191+
192+
When a ClusterExtension is installed:
193+
194+
1. The operator-controller resolves a bundle from the catalog
195+
2. The bundle's Kubernetes manifests are sorted into phases by resource kind
196+
3. Objects are packed into immutable Secrets
197+
4. A ClusterObjectSet is created with `revision: 1` in `Active` lifecycle state
198+
5. Phases roll out sequentially — each phase waits for all its objects to become ready before the next phase begins
199+
200+
### Upgrades
201+
202+
When a ClusterExtension is upgraded to a new version:
203+
204+
1. A new ClusterObjectSet is created with `revision: N+1`
205+
2. Both old and new revisions are `Active` during the transition
206+
3. Objects transition ownership from the old revision to the new one
207+
4. Once the new revision reports `Succeeded`, the old revision is archived
208+
5. Older archived revisions are garbage collected, keeping the last 5 for auditing
209+
210+
### Relationship with ClusterExtension
211+
212+
A `ClusterExtension` owns one or more `ClusterObjectSet` resources. The ClusterExtension's `.status.activeRevisions` field lists all currently active (non-archived) revisions along with their status conditions.
213+
214+
When created by operator-controller, ClusterObjectSets carry labels that identify the parent:
215+
216+
```yaml
217+
metadata:
218+
labels:
219+
olm.operatorframework.io/owner-kind: ClusterExtension
220+
olm.operatorframework.io/owner-name: my-extension
221+
olm.operatorframework.io/package-name: my-operator
222+
olm.operatorframework.io/bundle-version: 1.2.3
223+
```
224+
225+
## Examples
226+
227+
### Inline objects
228+
229+
This example shows a ClusterObjectSet with objects embedded directly in the spec. This is the simplest approach and works well when the total size of all objects stays within etcd's 1.5 MiB limit.
230+
231+
```yaml
232+
apiVersion: olm.operatorframework.io/v1
233+
kind: ClusterObjectSet
234+
metadata:
235+
name: my-app-rev1
236+
spec:
237+
revision: 1
238+
lifecycleState: Active
239+
collisionProtection: Prevent
240+
phases:
241+
- name: crds
242+
objects:
243+
- object:
244+
apiVersion: apiextensions.k8s.io/v1
245+
kind: CustomResourceDefinition
246+
metadata:
247+
name: widgets.example.com
248+
spec:
249+
group: example.com
250+
names:
251+
kind: Widget
252+
plural: widgets
253+
scope: Namespaced
254+
versions:
255+
- name: v1
256+
served: true
257+
storage: true
258+
schema:
259+
openAPIV3Schema:
260+
type: object
261+
properties:
262+
spec:
263+
type: object
264+
- name: rbac
265+
objects:
266+
- object:
267+
apiVersion: v1
268+
kind: ServiceAccount
269+
metadata:
270+
name: my-app-controller
271+
namespace: my-app-system
272+
- object:
273+
apiVersion: rbac.authorization.k8s.io/v1
274+
kind: ClusterRole
275+
metadata:
276+
name: my-app-manager-role
277+
rules:
278+
- apiGroups: ["example.com"]
279+
resources: ["widgets"]
280+
verbs: ["get", "list", "watch", "create", "update", "delete"]
281+
- name: deploy
282+
objects:
283+
- object:
284+
apiVersion: apps/v1
285+
kind: Deployment
286+
metadata:
287+
name: my-app-controller
288+
namespace: my-app-system
289+
spec:
290+
replicas: 1
291+
selector:
292+
matchLabels:
293+
app: my-app-controller
294+
template:
295+
metadata:
296+
labels:
297+
app: my-app-controller
298+
spec:
299+
serviceAccountName: my-app-controller
300+
containers:
301+
- name: manager
302+
image: example.com/my-app-controller:v1.0.0
303+
```
304+
305+
### Secret references
306+
307+
This example shows a ClusterObjectSet that references objects stored in Secrets. This is how operator-controller creates ClusterObjectSets, and is necessary when managing bundles with large resources like CRDs with extensive schemas.
308+
309+
Object manifests are stored in immutable Secrets. Secret names are content-addressable, computed as `<revisionName>-<16 hex chars>` where the suffix is the first 8 bytes of a SHA-256 digest over the Secret's sorted keys and values. Each data key is a 43-character base64url-encoded (no padding) SHA-256 hash of the stored object bytes.
310+
311+
```yaml
312+
apiVersion: v1
313+
kind: Secret
314+
metadata:
315+
name: my-app-rev1-3a7f1b9c0e2d4f68
316+
namespace: olmv1-system
317+
immutable: true
318+
data:
319+
# key: 43-char base64url SHA-256 of the object JSON bytes
320+
# value: base64-encoded JSON-serialized Kubernetes manifest
321+
K8sTl2xQa0vNpR7mW4dYcJfHbE9gUiAoZX1sDj6wFCy: <base64-encoded CRD JSON>
322+
---
323+
apiVersion: v1
324+
kind: Secret
325+
metadata:
326+
name: my-app-rev1-8b2e4a6c1d3f5097
327+
namespace: olmv1-system
328+
immutable: true
329+
data:
330+
# Multiple objects can be packed into the same Secret
331+
Pm5tRqLwXv8sCj3kFdNhYe7bUiA0oZG1x2W9acDfHJy: <base64-encoded ServiceAccount JSON>
332+
Vn4mTsLwXp8rCk3jFdNhYe7bUiA0oZG1x2W9acDfHQy: <base64-encoded ClusterRole JSON>
333+
Bq9nRtLxYw2sDk4lGePhZf8cVjB1pUiA0oZH3x5WaCdy: <base64-encoded Deployment JSON>
334+
```
335+
336+
The ClusterObjectSet references these Secrets:
337+
338+
```yaml
339+
apiVersion: olm.operatorframework.io/v1
340+
kind: ClusterObjectSet
341+
metadata:
342+
name: my-app-rev1
343+
spec:
344+
revision: 1
345+
lifecycleState: Active
346+
collisionProtection: Prevent
347+
phases:
348+
- name: crds
349+
objects:
350+
- ref:
351+
name: my-app-rev1-3a7f1b9c0e2d4f68
352+
namespace: olmv1-system
353+
key: "K8sTl2xQa0vNpR7mW4dYcJfHbE9gUiAoZX1sDj6wFCy"
354+
- name: rbac
355+
objects:
356+
- ref:
357+
name: my-app-rev1-8b2e4a6c1d3f5097
358+
namespace: olmv1-system
359+
key: "Pm5tRqLwXv8sCj3kFdNhYe7bUiA0oZG1x2W9acDfHJy"
360+
- ref:
361+
name: my-app-rev1-8b2e4a6c1d3f5097
362+
namespace: olmv1-system
363+
key: "Vn4mTsLwXp8rCk3jFdNhYe7bUiA0oZG1x2W9acDfHQy"
364+
- name: deploy
365+
objects:
366+
- ref:
367+
name: my-app-rev1-8b2e4a6c1d3f5097
368+
namespace: olmv1-system
369+
key: "Bq9nRtLxYw2sDk4lGePhZf8cVjB1pUiA0oZH3x5WaCdy"
370+
```
371+
372+
Multiple objects can reference different keys within the same Secret, allowing efficient packing of smaller objects.
373+
374+
## Inspecting ClusterObjectSets
375+
376+
```bash
377+
# List all ClusterObjectSets
378+
kubectl get clusterobjectsets
379+
380+
# List revisions for a specific extension
381+
kubectl get clusterobjectsets -l olm.operatorframework.io/owner-name=my-extension
382+
383+
# View full details for a specific revision
384+
kubectl get clusterobjectset <name> -o yaml
385+
```
386+
387+
Example output:
388+
389+
```
390+
NAME AVAILABLE PROGRESSING AGE
391+
my-extension-abc12 Unknown False 2d
392+
my-extension-def34 True True 1h
393+
```

0 commit comments

Comments
 (0)