Operator Lifecycle Manager

Operator Lifecycle Manager (OLM) installs, manages, upgrades, and removes Kubernetes Operators themselves — it is an operator-for-operators (olm.operatorframework.io). An operator manages a stateful application; OLM manages the operators. It is the runtime half of the Operator Framework (the authoring half being Operator SDK). The problem OLM solves is unglamorous but real: at scale, hand-installing operator YAML is unmanageable. An operator ships as a CRD, a Deployment, a ServiceAccount, RBAC, and sometimes webhooks — and operators have dependencies on other operators and on specific CRD versions. Applying that by hand, tracking which version is installed, coordinating upgrades, and resolving “operator A needs operator B’s CRD at version ≥ X” does not scale past a handful of operators. OLM turns operator installation into a declarative, catalog-driven, dependency-resolved, channel-based process — kubectl apply a one-line Subscription and OLM finds the operator in a catalog, resolves its dependencies, installs it, and keeps it upgraded. OLM is installed by default in OpenShift, where it powers the in-console OperatorHub; on vanilla Kubernetes it is an opt-in install. OLM is part of the Operator Framework, a CNCF Incubating project (accepted July 9 2020 at the Incubating level — CNCF project page).

Two generations of OLM coexist as of 2026

Everything in the body below describes OLM (Classic) — the original Subscription/CSV/InstallPlan/OperatorGroup/CatalogSource model. A ground-up rewrite, OLM v1 (the operator-controller + catalogd + ClusterExtension model), reached general availability in OpenShift 4.17, and from OpenShift 4.18 (early 2025) is enabled by default alongside OLM (Classic) (OpenShift 4.18 release notes). The name OLM (Classic) is itself a retronym Red Hat introduced when v1 went GA. The two are separate controllers with incompatible object models and run side by side; there is no in-place migration. See the OLM v1 — the next-generation rewrite section for the new model and its current limits. Verify which generation a given cluster runs before applying any object shape from this note.

Mental Model

OLM is itself a control loop — several of them — operating over a set of CRDs that describe “what operators should exist and stay current.” The five objects you need to hold in your head, and how they relate:

flowchart TD
    CATALOG["CatalogSource<br/>a repository of operators<br/>(OperatorHub.io, Red Hat catalog)<br/>= a catalog-server Pod + Service"]
    SUB["Subscription<br/>'install operator X from catalog Y,<br/>track channel Z, auto/manual upgrades'<br/>= the user's INTENT"]
    IP["InstallPlan<br/>the concrete list of resources<br/>to apply for one install/upgrade<br/>= may need manual approval"]
    CSV["ClusterServiceVersion (CSV)<br/>one VERSION of an operator:<br/>metadata, RBAC, Deployment,<br/>owned CRDs, install modes"]
    OG["OperatorGroup<br/>which namespaces the<br/>installed operator WATCHES"]
    OPERATOR["The operator's<br/>Deployment + CRDs<br/>running in the cluster"]

    SUB -- "references" --> CATALOG
    SUB -- "OLM resolver generates" --> IP
    IP -- "once approved, applies" --> CSV
    CSV -- "OLM creates the operator's<br/>Deployment + RBAC" --> OPERATOR
    OG -- "scopes the RBAC OLM<br/>generates for the operator" --> OPERATOR
    CATALOG -- "supplies the CSVs +<br/>the upgrade graph" --> IP

The diagram shows the OLM object graph. The insight to extract: there is a clean separation between intent and mechanism. The Subscription is pure intent (“I want operator X, current, from channel Z”) and is the only object the user normally writes. Everything else — the InstallPlan (the concrete change set), the CSV (the resolved operator version), the operator’s Deployment — is generated and reconciled by OLM. The OperatorGroup is the orthogonal “tenancy” axis: it does not say which operator, it says where the operator may act.

Mechanical Walk-through

The five resources

CatalogSource — a repository of available operators. Concretely it is a Pod running a catalog server (a gRPC service) plus a Service, populated from a catalog image (a container image bundling many operators’ metadata and their upgrade graphs). OperatorHub.io and the Red Hat Certified/Community catalogs are CatalogSources. Adding a CatalogSource makes its operators discoverable cluster-wide.

Subscription — the user’s declared intent to install and maintain an operator. It names the operator package, the CatalogSource to install from, the channel to track (e.g. stable, alpha, candidate), and the installPlanApproval mode (Automatic or Manual) (olm.operatorframework.io — Subscription). This is normally the only object the user authors.

InstallPlan — the concrete change set: the explicit list of resources (a specific CSV, specific CRDs, etc.) that OLM has resolved must be applied to satisfy a Subscription. OLM’s resolver generates an InstallPlan whenever the Subscription is created or when a newer version becomes available on the tracked channel. If installPlanApproval: Automatic, OLM applies it immediately; if Manual, the InstallPlan sits with approved: false until a human (or a pipeline) flips it — the controlled-upgrade gate.

ClusterServiceVersion (CSV) — describes one version of one operator: its metadata (name, version, description, icon, maintainers), the CRDs it owns and requires, its install strategy (the Deployment and the RBAC permissions/clusterPermissions OLM must create to run it), and its install modes — the namespace topologies it supports: OwnNamespace, SingleNamespace, MultiNamespace, AllNamespaces. The CSV is the deployable unit; OLM reads it and materializes the operator’s Deployment, ServiceAccount, Roles, and RoleBindings. The CSV is produced by Operator SDK’s make bundle and shipped inside an operator bundle image.

OperatorGroup — defines which namespaces an installed operator watches and acts in. It is OLM’s multi-tenancy primitive. An OperatorGroup in a namespace selects target namespaces; OLM then generates the operator’s RBAC scoped to exactly those namespaces. The target namespaces must be compatible with the operator’s CSV installModes — an operator that declares only OwnNamespace cannot be placed under an OperatorGroup that selects many namespaces, and OLM will refuse the install.

The install sequence

  1. Admin adds a CatalogSource (or uses one shipped with the cluster). OLM’s catalog-operator starts the catalog-server Pod and indexes its operators.
  2. Admin creates an OperatorGroup in the target namespace (defines watch scope).
  3. Admin creates a Subscription naming the operator, the catalog, and the channel.
  4. OLM’s resolver computes what is needed: the head CSV of the requested channel, any dependency operators it requires (transitively), and any required CRDs. It emits an InstallPlan enumerating every resource.
  5. If approval is Automatic, OLM applies the InstallPlan; if Manual, it waits for approval.
  6. On apply, OLM creates the CRDs, the operator’s ServiceAccount/RBAC (scoped by the OperatorGroup), and the operator’s Deployment. The operator pod starts and begins reconciling its own custom resources.

The upgrade graph — channels, replaces, skips

An operator’s catalog metadata defines an upgrade graph: a directed graph over CSV versions saying “from version A you may upgrade to version B.” Each CSV declares spec.replaces: <older-CSV> (“this version supersedes that one”) and optionally spec.skips: [<CSV>, ...] (“these intermediate versions may be skipped”). A channel (stable, fast, candidate, etc.) is a named pointer to a head CSV and the chain of replaces edges leading to it — it is a release stream. A Subscription tracks one channel; when the channel’s head advances, OLM walks the replaces/skips edges to compute the upgrade path and generates an InstallPlan for it. This is what gives OLM controlled, ordered upgrades: you cannot jump arbitrarily between versions, only along edges the operator author sanctioned. With installPlanApproval: Manual, every hop is gated; with Automatic, OLM keeps the operator at the channel head continuously.

Configuration / API Surface — an annotated Subscription

apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
  name: my-postgres-operator
  namespace: operators                 # the namespace the operator is installed into
spec:
  channel: stable                      # track the 'stable' release stream
  name: postgres-operator              # the operator package name (as it appears in the catalog)
  source: operatorhubio-catalog        # the CatalogSource to install from
  sourceNamespace: olm                 # the namespace the CatalogSource lives in
  installPlanApproval: Manual          # OLM generates an InstallPlan but waits for human approval
  startingCSV: postgres-operator.v1.2.0  # (optional) pin the initial version; omit for channel head

Line-by-line: channel: stable selects the release stream — OLM will only ever move along the replaces/skips edges within stable. name + source + sourceNamespace triangulate the operator within a specific catalog. installPlanApproval: Manual is the production-safe setting: OLM still detects new versions and generates an InstallPlan, but the InstallPlan stays approved: false — visible via kubectl get installplan — until an operator (human or pipeline) approves it. Set Automatic for low-stakes operators you want always-current. startingCSV pins the first installed version; without it OLM installs the channel head.

Approving a pending plan and inspecting the chain:

kubectl get subscription my-postgres-operator -n operators -o yaml   # see currentCSV / installedCSV / state
kubectl get installplan -n operators                                  # find the pending plan
kubectl patch installplan <name> -n operators --type merge \
  -p '{"spec":{"approved":true}}'                                      # approve the upgrade hop
kubectl get csv -n operators                                           # the resolved operator version(s)

Failure Modes

  • installModes vs OperatorGroup mismatch. The most common first-install failure: the CSV declares only OwnNamespace/SingleNamespace but the OperatorGroup targets multiple namespaces. OLM refuses; the Subscription’s status reports the mismatch. Fix: align the OperatorGroup’s target namespaces with what the CSV supports.
  • Dependency resolution dead-ends. The resolver cannot satisfy a required CRD/operator because no enabled CatalogSource provides it, or two operators require conflicting versions. The Subscription stalls with a resolution error in status.
  • Stuck pending InstallPlan. With Manual approval, an unapproved InstallPlan blocks all further upgrades on that Subscription indefinitely — easy to forget. Symptom: operator silently stays old. Fix: approve or switch to Automatic.
  • Upgrade-graph gaps. If a CSV’s replaces/skips edges do not connect the installed version to the channel head, OLM cannot compute an upgrade path and the operator is stranded on an old version. This is an operator-author packaging bug, surfaced as a resolution error.
  • Multiple OperatorGroups in one namespace. Only one OperatorGroup per namespace is valid; a second one puts operators in the namespace into an error state. A frequent multi-team collision.
  • CatalogSource Pod unhealthy. If the catalog-server Pod crashloops (bad catalog image, image-pull failure), every Subscription pointing at it stalls — resolution depends on the catalog being reachable.

Alternatives and When to Choose Them

  • Plain kubectl apply of operator YAML. The baseline. Fine for one or two operators with no inter-dependencies and a human who remembers to upgrade them. Loses dependency resolution, channel-based upgrades, and the catalog. This is what OLM exists to replace at scale.
  • Helm to install operators. Many operators also ship a Helm chart. Installing an operator via helm install (often through ArgoCD/Flux GitOps) is a popular non-OLM path: you get versioning and rollback from Helm, but no cross-operator dependency resolution and no OperatorHub catalog. On non-OpenShift clusters this is arguably the more common choice.
  • GitOps (ArgoCD / Flux) for operators. Treat operator manifests as just more YAML in Git. Auditable and reproducible, but, like Helm, lacks OLM’s dependency resolver and upgrade graph. Many teams combine the two: GitOps to install OLM and the Subscriptions, OLM to resolve and upgrade the operators.
  • OLM v1 — the same project’s own next generation, described in detail below. On a cluster where OLM v1 is available, it is the forward-looking choice for any operator that fits its current constraints (registry+v1 bundle, AllNamespaces, no webhooks).

OLM v1 — the next-generation rewrite

OLM v1 is not a new minor version of the controller described above; it is a separate, ground-up redesign with its own controllers and an incompatible object model, built by the Operator Framework team to fix the structural problems of OLM (Classic) (Red Hat — Announcing OLM v1). It reached general availability in OpenShift 4.17 and, from OpenShift 4.18, is enabled by default side by side with OLM (Classic) (OpenShift 4.18 OLM v1 docs). Upstream it ships as the operator-framework/operator-controller project.

What changed and why

The Classic model spreads one install across five objects (CatalogSource, Subscription, InstallPlan, OperatorGroup, CSV), with OLM both resolving dependencies and deciding the install when, and generating RBAC on the operator’s behalf. Three properties of that design caused chronic pain: (1) the implicit, OLM-driven dependency resolver could pick versions or pull in dependencies the admin never asked for, making upgrades unpredictable; (2) OLM granting itself broad permissions to create the operator’s RBAC violated least-privilege; and (3) the per-CatalogSource gRPC catalog Pod was a heavyweight, stateful component. OLM v1 collapses the object graph and inverts the privilege model:

  • ClusterExtension is a single cluster-scoped resource that consolidates what Subscription + InstallPlan + CSV + OperatorGroup expressed in Classic (Red Hat Developer — Manage operators as ClusterExtensions). One kubectl apply of a ClusterExtension names the package, the version or version range, and — critically — a user-supplied ServiceAccount whose RBAC the admin defines. OLM v1 installs the extension as that ServiceAccount: it can do nothing the admin did not explicitly grant. This is the least-privilege fix — OLM no longer fabricates RBAC for the operator; the admin owns it.
  • ClusterCatalog replaces CatalogSource. Catalogs are published in File-Based Catalog (FBC) format (a directory of declarative JSON/YAML blobs describing packages, channels, and the upgrade graph) and served by the catalogd component over an HTTP(S) catalog API, rather than the Classic per-source gRPC Pod. This makes catalog content cacheable, queryable, and far lighter to run.
  • operator-controller is the central reconciler. It watches ClusterExtension objects, queries catalogd for the resolved bundle, and applies it using the supplied ServiceAccount, reconciling continuously rather than as a one-shot InstallPlan apply.
flowchart TD
    CC["ClusterCatalog<br/>FBC content, served by catalogd<br/>over an HTTP catalog API"]
    CE["ClusterExtension<br/>package + version range +<br/>user-supplied ServiceAccount<br/>(consolidates Sub+IP+CSV+OG)"]
    OC["operator-controller<br/>resolves + applies the bundle<br/>AS the supplied ServiceAccount"]
    SA["ServiceAccount + RBAC<br/>defined by the ADMIN<br/>(least-privilege)"]
    OP["The extension's<br/>Deployment + CRDs"]

    CE -- "references catalog content via" --> CC
    CE -- "names" --> SA
    OC -- "watches" --> CE
    OC -- "fetches resolved bundle from" --> CC
    OC -- "applies bundle using" --> SA
    SA -- "bounds what can be created" --> OP

The diagram shows the OLM v1 object graph. The insight to extract: compared with the five-object Classic graph, v1 has two user-facing objects (ClusterCatalog, ClusterExtension) and folds privilege into an explicit, admin-owned ServiceAccount. The single most consequential change is that the admin, not OLM, supplies the RBAC — installs become least-privilege and auditable instead of OLM granting itself sweeping rights.

A ClusterExtension and ClusterCatalog

apiVersion: olm.operatorframework.io/v1
kind: ClusterCatalog
metadata:
  name: operatorhubio
spec:
  source:
    type: Image
    image:
      ref: quay.io/operatorhubio/catalog:latest   # FBC catalog image, served by catalogd over HTTP
      pollIntervalMinutes: 10                      # how often catalogd re-pulls for new content
---
apiVersion: olm.operatorframework.io/v1
kind: ClusterExtension
metadata:
  name: argocd
spec:
  namespace: argocd                                # where the operand/operator is installed
  serviceAccount:
    name: argocd-installer                         # ADMIN-defined SA; install runs AS this identity
  source:
    sourceType: Catalog
    catalog:
      packageName: argocd-operator
      version: ">=0.6.0 <0.7.0"                     # SemVer range; controller picks the best match

Line-by-line: the ClusterCatalog registers FBC content for catalogd to serve — note there is no gRPC Pod-per-source as in Classic. In the ClusterExtension, spec.namespace is the install target; spec.serviceAccount.name is the identity the install executes as — if it lacks a permission the bundle needs, the install fails visibly rather than OLM silently widening rights; version is a SemVer constraint the controller resolves against the catalog’s upgrade graph. There is no separate Subscription, no InstallPlan to approve, no OperatorGroup, and no hand-edited CSV — those concepts are absorbed into this one object.

Current GA limitations

The initial GA is deliberately a subset of Classic’s capabilities. As of the OpenShift 4.18 GA, OLM v1 installs only extensions that (OLM v1 docs; Red Hat Developer):

  • use the registry+v1 bundle format (the same bundle format Classic introduced);
  • support the AllNamespaces install mode — OwnNamespace/SingleNamespace/MultiNamespace scoping is not yet supported, a direct consequence of dropping OperatorGroup;
  • do not use admission/conversion webhooks; and
  • do not declare dependencies via FBC properties — there is no automatic transitive dependency resolver as in Classic, by design (the admin installs dependencies explicitly).

Operators that need namespace-scoped install, ship webhooks, or rely on automatic dependency resolution must still use OLM (Classic) for now. There is no automated migration between the two — the conceptual models differ too much, so a move from Classic to v1 is a re-install.

Production Notes

  • OpenShift is the OLM heartland. OLM is installed by default in OpenShift/OKD; the in-console “OperatorHub” UI is an OLM front end over the Red Hat Certified, Red Hat, Community, and Marketplace CatalogSources. Most production OLM experience is OpenShift experience.
  • On vanilla Kubernetes, OLM is an explicit install (operator-sdk olm install or applying the upstream manifests). Adoption outside OpenShift is lighter — many non-OpenShift shops install operators via Helm/GitOps instead, because they do not need the cross-operator dependency resolver.
  • Manual approval is the production default for critical operators. It turns every operator upgrade into a reviewable, gated event rather than an automatic one — important for operators managing databases or other stateful systems where an unexpected operator upgrade can trigger an unexpected operand change.
  • Catalog images are themselves versioned artifacts. A CatalogSource pinned to a mutable latest catalog image will silently gain new operator versions; pin catalog images for reproducibility.
  • The capability-level interplay. OLM manages any operator regardless of its Operator SDK flavor or capability level — but OLM’s own “seamless upgrades” capability is exactly what lifts a packaged operator to Level 2. OLM is how an operator graduates from “some YAML” to “a lifecycle-managed product.”
  • Classic is not deprecated, but it is now legacy. With v1 GA and on-by-default in OpenShift 4.18, OLM (Classic) is in maintenance: it keeps working and still handles the cases v1 cannot (webhooks, namespace-scoped installs, automatic dependency resolution), but new investment is going into v1. As of 2026, Operator SDK tooling still targets OLM (Classic) — its bundle/CSV output and operator-sdk olm install install the Classic controller, and the SDK overview lists support for OLM v0 minor versions (0.26–0.28 at time of writing) with no v1 integration yet. Plan new packaging against Classic but watch the v1 roadmap.

See Also