Manage standalone ClusterObjectDeployments
A ClusterObjectDeployment (COD) manages mutable desired content by creating
immutable ClusterObjectSet (COS) revisions. This tutorial installs the standalone
experimental object-controller, deploys a ConfigMap, and follows an A → B → A
template change through three forward revisions.
The COD owns its COS revisions through controller owner references. Each COS owns the Kubernetes objects it manages:
flowchart LR
COD[ClusterObjectDeployment] --> COS[ClusterObjectSet revision]
COS --> CM[ConfigMap]
Demo: revisions and ownership
This recording follows A → B → A, showing the new COS revision and ConfigMap owner at each update, the unchanged ConfigMap UID, archived history, and cascading cleanup when the COD is deleted.
The demo uses direct COD resources in the experimental E2E environment, which also runs operator-controller and catalogd. It does not create a ClusterExtension or use a catalog. The installation instructions below show how to run only object-controller.
Prerequisites
- A Kubernetes test cluster where you can install CRDs and cluster-scoped RBAC.
- A schedulable Linux node labeled
node-role.kubernetes.io/control-plane, matching the chart's default node selector. If your cluster lacks this label, customizedeployments.templateSpec.nodeSelectorwhen rendering the chart. - The
cod-examplenamespace must not already exist. The sample creates it; cleanup deletes it and all resources in it. kubectland Helm, withkubectlconfigured for the test cluster.- A checkout of this repository. Run the commands below from its root directory.
- An object-controller image built from the checkout and available to the cluster. See Controller deployment for build and image configuration details. For KIND, load the image into the cluster before installing the controller.
The example uses the new COD name and COS group cod-example. Use a fresh group
for the walkthrough: group membership identifies the revision family.
Install the standalone controller
Set the image reference and install only object-controller and its experimental APIs:
OBJECT_CONTROLLER_IMAGE='<your-built-image>'
helm template object-controller helm/olmv1 \
--set options.featureSet=experimental \
--set options.objectController.enabled=true \
--set options.operatorController.enabled=false \
--set options.catalogd.enabled=false \
--set-string options.objectController.deployment.image="$OBJECT_CONTROLLER_IMAGE" \
> /tmp/standalone-object-controller.yaml
kubectl apply -f /tmp/standalone-object-controller.yaml
kubectl -n olmv1-system rollout status deployment/object-controller-controller-manager --timeout=180s
This installation uses the chart's existing cluster-admin service account binding for managing arbitrary Kubernetes resources. ClusterExtension and catalog controllers are unnecessary for standalone COD use.
Create the first revision
The sample in config/samples/olm_v1_clusterobjectdeployment.yaml creates a
namespace and a COD whose template contains one inline ConfigMap:
apiVersion: v1
kind: Namespace
metadata:
name: cod-example
---
apiVersion: olm.operatorframework.io/v1
kind: ClusterObjectDeployment
metadata:
name: cod-example
spec:
revisionHistoryLimit: 2
progressDeadlineMinutes: 10
template:
metadata:
labels:
app.kubernetes.io/name: cod-example
annotations:
example.com/description: Configuration managed by a standalone COD
spec:
collisionProtection: Prevent
phases:
- name: configuration
objects:
- type: InlineManifest
inlineManifest:
apiVersion: v1
kind: ConfigMap
metadata:
name: cod-example-settings
namespace: cod-example
data:
message: Hello from revision A
kubectl apply -f config/samples/olm_v1_clusterobjectdeployment.yaml
kubectl wait --for=condition=Available clusterobjectdeployment/cod-example --timeout=180s
kubectl get clusterobjectdeployment cod-example
kubectl get clusterobjectsets --field-selector spec.group=cod-example
kubectl -n cod-example get configmap cod-example-settings -o jsonpath='{.data.message}{"\n"}'
The message is Hello from revision A. The COD creates cod-example-1 with
spec.group: cod-example, spec.revision: 1, and spec.lifecycleState: Active.
Its Progressing condition becomes False with reason
NewClusterObjectSetProgressed after rollout completes, and Available becomes
True.
The namespace in the sample is created separately from the COD's managed content. The ConfigMap is owned by the COS. Inspect that ownership and record the ConfigMap's UID for comparison after the update:
kubectl -n cod-example get configmap cod-example-settings \
-o jsonpath='{.metadata.ownerReferences[?(@.controller==true)].name}{"\n"}{.metadata.uid}{"\n"}'
Change the template
Change the ConfigMap message to B:
kubectl patch clusterobjectdeployment cod-example --type=json \
-p='[{"op":"replace","path":"/spec/template/spec/phases/0/objects/0/inlineManifest/data/message","value":"Hello from revision B"}]'
kubectl wait --for=create clusterobjectset/cod-example-2 --timeout=180s
kubectl wait --for=condition=Ready clusterobjectset/cod-example-2 --timeout=180s
kubectl -n cod-example get configmap cod-example-settings \
-o jsonpath='{.data.message}{"\n"}{.metadata.ownerReferences[?(@.controller==true)].name}{"\n"}{.metadata.uid}{"\n"}'
The message becomes Hello from revision B, ownership moves to cod-example-2,
and the ConfigMap keeps its UID. During the handoff, more than one COS can be
Active. An older Ready revision can keep the COD Available=True while its
successor is still progressing. Waiting for the new COS to become Ready observes
that specific rollout.
Reapply the original sample to restore A:
kubectl apply -f config/samples/olm_v1_clusterobjectdeployment.yaml
kubectl wait --for=create clusterobjectset/cod-example-3 --timeout=180s
kubectl wait --for=condition=Ready clusterobjectset/cod-example-3 --timeout=180s
kubectl -n cod-example get configmap cod-example-settings -o jsonpath='{.data.message}{"\n"}'
kubectl get clusterobjectsets --field-selector spec.group=cod-example
The message returns to Hello from revision A through revision 3. Restoring an
earlier template creates a new, higher revision; an archived revision cannot
become Active again. Reapplying an unchanged template keeps the current revision.
Changes to spec.template, including its labels and annotations, create a new
revision. Changes to rollout policy, such as spec.progressDeadlineMinutes or
spec.revisionHistoryLimit, do not. See
Which edits create revisions?
for the complete rules.
Read status and diagnose failures
The COD exposes Available and Progressing. A child COS exposes Ready with
the details of its resource reconciliation.
For revision-specific failures, the COD's condition message names the COS to inspect. See COD status conditions for the available reasons and Revision ownership for conflicts that prevent a new rollout.
Inspect the parent, the current child, and the controller logs:
kubectl describe clusterobjectdeployment cod-example
kubectl get clusterobjectset cod-example-3 -o yaml
kubectl -n olmv1-system logs deployment/object-controller-controller-manager
A COS can report Ready=False with PreconditionsNotMet for a mutable
referenced Secret; before the deadline, the COD reports
Progressing=True/RevisionPreconditionsNotMet. A COS reports ObjectsNotReady
for objects still waiting on probes. A missing referenced Secret causes a
retryable read error in controller logs and leaves the COS's existing Ready
condition unchanged. A newly created COS can remain Ready=Unknown with reason
Pending until the Secret is available.
Supply the expected immutable Secret content or resolve the readiness failure and let reconciliation retry. Change the COD template when the desired object content needs to change. See COS status conditions for the child reasons.
Adjust the rollout deadline
The sample configures a ten-minute rollout budget with
spec.progressDeadlineMinutes. To change that budget without creating another
revision, patch the COD directly:
kubectl patch clusterobjectdeployment cod-example --type=merge \
-p='{"spec":{"progressDeadlineMinutes":20}}'
The new budget is measured from the current COS's original creation timestamp, not from the time of the edit. See Progress deadlines and recovery for timing, timeout precedence, and recovery behavior.
Inspect history and referenced content
The COS controller archives superseded revisions after handoff and cleans up objects that were removed from the desired content. COD prunes excess archived history after teardown completes. Active revisions are excluded from the limit.
The sample retains two archived revisions with spec.revisionHistoryLimit: 2.
See Revision history and deletion
for retention rules. After A → B → A, revisions 1 and 2 can remain as archived
history while revision 3 is Active:
kubectl get clusterobjectsets --field-selector spec.group=cod-example \
-o custom-columns=NAME:.metadata.name,REVISION:.spec.revision,STATE:.spec.lifecycleState
For larger content, each template object can use the COS secretRef mechanism
to read a JSON manifest from an immutable Secret. Create the referenced Secret
before applying the COD; it must remain available for every retained revision
that references it. Changing the intended content requires a new Secret reference
in the template. See Object storage.
Standalone callers manage the lifetime of these content Secrets.
With BoxcutterRuntime enabled, operator-controller manages CODs for
ClusterExtensions. This standalone workflow uses object-controller without bundle
resolution or catalog components. See
Relationship with ClusterExtension
for the integration.
For all fields, see the COD API reference.
Clean up
Deleting the COD cascades to its owned COSs and managed objects. Wait for the
ConfigMap to disappear before removing the separately created namespace. Only
remove cod-example if it was created for this walkthrough and contains no
unrelated resources:
kubectl delete clusterobjectdeployment cod-example
kubectl -n cod-example wait --for=delete configmap/cod-example-settings --timeout=180s
kubectl delete namespace cod-example
After cleaning up the example resources, remove the standalone installation only
if every resource in the rendered manifest is dedicated to this walkthrough. The
manifest includes the olmv1-system namespace and the COD/COS CRDs; deleting them
also deletes the namespace's contents and all COD/COS instances. If any resources
are shared, leave the installation in place: