πŸ“šBook Signing at KubeCon EU 2026Meet us at Booking.com HQ (Mon 18:30-21:00) & vCluster booth #521 (Tue 24 Mar, 12:30-1:30pm) β€” free book giveaway!RSVP Booking.com Event
Deployments intermediate ⏱ 20 minutes K8s 1.26+

Manage OperatorGroups with ArgoCD

Deploy and manage OLM OperatorGroup resources via ArgoCD for GitOps-driven operator lifecycle management in OpenShift namespaces.

By Luca Berton β€’ β€’ πŸ“– 5 min read

πŸ’‘ Quick Answer: Create OperatorGroup β†’ Subscription β†’ wait for CSV in ArgoCD using sync waves. OperatorGroup must exist in the namespace BEFORE the Subscription, or OLM will refuse to install the operator.

The Problem

Deploying OLM operators via ArgoCD requires careful ordering:

  1. Namespace must exist first
  2. OperatorGroup must be created in the namespace
  3. Subscription triggers the operator install
  4. CSV (ClusterServiceVersion) is created by OLM automatically

Without proper sync wave ordering, ArgoCD creates resources simultaneously and OLM rejects Subscriptions that lack an OperatorGroup. ArgoCD also fights OLM over resource ownership, causing sync loops.

The Solution

Basic OperatorGroup + Subscription via ArgoCD

# 01-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: openshift-logging
  annotations:
    argocd.argoproj.io/sync-wave: "0"
---
# 02-operatorgroup.yaml
apiVersion: operators.coreos.com/v1
kind: OperatorGroup
metadata:
  name: openshift-logging
  namespace: openshift-logging
  annotations:
    argocd.argoproj.io/sync-wave: "1"
spec:
  targetNamespaces:
    - openshift-logging
---
# 03-subscription.yaml
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
  name: cluster-logging
  namespace: openshift-logging
  annotations:
    argocd.argoproj.io/sync-wave: "2"
spec:
  channel: stable-6.0
  name: cluster-logging
  source: redhat-operators
  sourceNamespace: openshift-marketplace
  installPlanApproval: Automatic

Handle ArgoCD Sync Loops with OLM

OLM modifies Subscription and CSV resources after creation, causing ArgoCD to detect drift. Fix with resource customizations:

# ArgoCD ConfigMap β€” argocd-cm
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: openshift-gitops
data:
  resource.customizations.ignoreDifferences.operators.coreos.com_Subscription: |
    jsonPointers:
      - /spec/startingCSV
      - /status
  resource.customizations.ignoreDifferences.operators.coreos.com_ClusterServiceVersion: |
    jsonPointers:
      - /metadata/annotations
      - /metadata/labels
      - /spec/install
      - /status
  resource.customizations.ignoreDifferences.operators.coreos.com_OperatorGroup: |
    jsonPointers:
      - /metadata/annotations/olm.providedAPIs
      - /status

Or in the Application resource:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: logging-operator
  namespace: openshift-gitops
spec:
  project: platform
  source:
    repoURL: https://git.example.com/platform/operators.git
    path: logging
    targetRevision: main
  destination:
    server: https://kubernetes.default.svc
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
  ignoreDifferences:
    - group: operators.coreos.com
      kind: Subscription
      jsonPointers:
        - /status
        - /spec/startingCSV
    - group: operators.coreos.com
      kind: OperatorGroup
      jsonPointers:
        - /metadata/annotations/olm.providedAPIs

AllNamespaces OperatorGroup (Cluster-Wide Operators)

apiVersion: operators.coreos.com/v1
kind: OperatorGroup
metadata:
  name: global-operators
  namespace: openshift-operators
  annotations:
    argocd.argoproj.io/sync-wave: "1"
spec: {}
  # Empty spec = AllNamespaces mode
  # OLM watches all namespaces for this operator

⚠️ Note: openshift-operators namespace already has a global OperatorGroup by default. Don’t create a second one β€” OLM allows only one OperatorGroup per namespace.

Multi-Operator Namespace Pattern

# Single OperatorGroup for multiple operators in same namespace
apiVersion: operators.coreos.com/v1
kind: OperatorGroup
metadata:
  name: monitoring-operators
  namespace: monitoring
  annotations:
    argocd.argoproj.io/sync-wave: "1"
spec:
  targetNamespaces:
    - monitoring
---
# Multiple Subscriptions share the same OperatorGroup
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
  name: prometheus-operator
  namespace: monitoring
  annotations:
    argocd.argoproj.io/sync-wave: "2"
spec:
  channel: stable
  name: prometheus-operator
  source: community-operators
  sourceNamespace: openshift-marketplace
---
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
  name: grafana-operator
  namespace: monitoring
  annotations:
    argocd.argoproj.io/sync-wave: "2"
spec:
  channel: v5
  name: grafana-operator
  source: community-operators
  sourceNamespace: openshift-marketplace

Wait for Operator Ready with Health Checks

# ArgoCD custom health check for Subscriptions
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: openshift-gitops
data:
  resource.customizations.health.operators.coreos.com_Subscription: |
    hs = {}
    if obj.status ~= nil then
      if obj.status.currentCSV ~= nil and obj.status.state == "AtLatestKnown" then
        hs.status = "Healthy"
        hs.message = obj.status.currentCSV
      elseif obj.status.state == "UpgradePending" then
        hs.status = "Progressing"
        hs.message = "Upgrade pending"
      else
        hs.status = "Progressing"
        hs.message = "Waiting for operator install"
      end
    else
      hs.status = "Progressing"
      hs.message = "Waiting for status"
    end
    return hs

App-of-Apps Pattern for Operators

# parent-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: platform-operators
  namespace: openshift-gitops
  annotations:
    argocd.argoproj.io/sync-wave: "0"
spec:
  project: platform
  source:
    repoURL: https://git.example.com/platform/operators.git
    path: apps
    targetRevision: main
  destination:
    server: https://kubernetes.default.svc
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
operators/
β”œβ”€β”€ apps/
β”‚   β”œβ”€β”€ logging-app.yaml      # ArgoCD Application
β”‚   β”œβ”€β”€ compliance-app.yaml
β”‚   └── monitoring-app.yaml
β”œβ”€β”€ logging/
β”‚   β”œβ”€β”€ namespace.yaml         # wave 0
β”‚   β”œβ”€β”€ operatorgroup.yaml     # wave 1
β”‚   └── subscription.yaml     # wave 2
β”œβ”€β”€ compliance/
β”‚   β”œβ”€β”€ namespace.yaml
β”‚   β”œβ”€β”€ operatorgroup.yaml
β”‚   └── subscription.yaml
└── monitoring/
    β”œβ”€β”€ namespace.yaml
    β”œβ”€β”€ operatorgroup.yaml
    └── subscription.yaml
graph TD
    A[ArgoCD App-of-Apps] --> B[Logging App]
    A --> C[Compliance App]
    A --> D[Monitoring App]
    
    B --> B1["wave 0: Namespace"]
    B1 --> B2["wave 1: OperatorGroup"]
    B2 --> B3["wave 2: Subscription"]
    B3 --> B4["OLM: InstallPlan + CSV"]
    
    C --> C1["wave 0: Namespace"]
    C1 --> C2["wave 1: OperatorGroup"]
    C2 --> C3["wave 2: Subscription"]
    
    D --> D1["wave 0: Namespace"]
    D1 --> D2["wave 1: OperatorGroup"]
    D2 --> D3["wave 2: Subscription"]

Common Issues

”Multiple OperatorGroups Found” Error

# OLM only allows ONE OperatorGroup per namespace
oc get operatorgroup -n <namespace>

# If you see multiple, delete the extra one
oc delete operatorgroup <extra-og> -n <namespace>

# In ArgoCD: ensure only one OperatorGroup per namespace in your git repo

ArgoCD Shows β€œOutOfSync” Constantly

# OLM adds annotations like olm.providedAPIs β€” add to ignoreDifferences
# Check what's drifting:
argocd app diff <app-name> --local /path/to/manifests

# Most common drifts:
# - metadata.annotations.olm.providedAPIs (added by OLM)
# - spec.startingCSV (set by OLM after install)
# - status fields (managed by OLM)

Operator Installs in Wrong Namespace

# OperatorGroup targetNamespaces controls where the operator watches
# AllNamespaces (empty spec) vs OwnNamespace vs MultiNamespace

# Check current scope
oc get operatorgroup -n <namespace> -o jsonpath='{.items[0].status.namespaces}'

# For OwnNamespace: targetNamespaces must list only the OG's namespace
# For specific namespaces: list all target namespaces explicitly

Best Practices

  • Always use sync waves: Namespace (0) β†’ OperatorGroup (1) β†’ Subscription (2)
  • One OperatorGroup per namespace β€” OLM enforces this; violating it blocks all installs
  • Configure ignoreDifferences for all OLM resources to prevent sync loops
  • Add health checks for Subscriptions so ArgoCD waits for operator readiness
  • Use App-of-Apps for managing multiple operators across namespaces
  • Pin operator channels (stable-6.0 not stable) for predictable upgrades
  • Don’t manage CSVs in Git β€” they’re created by OLM, not by you

Key Takeaways

  • OperatorGroup is required before Subscription β€” sync waves enforce ordering
  • ArgoCD and OLM both want to manage operator resources β€” ignoreDifferences prevents fights
  • One OperatorGroup per namespace is an OLM hard rule
  • Custom health checks let ArgoCD wait for operator readiness before deploying dependent resources
  • The App-of-Apps pattern scales operator management across dozens of namespaces
#operatorgroup #olm #argocd #gitops #openshift
Luca Berton
Written by Luca Berton

Principal Solutions Architect specializing in Kubernetes, AI/GPU infrastructure, and cloud-native platforms. Author of Kubernetes Recipes and creator of CopyPasteLearn courses.

Kubernetes Recipes book cover

Want More Kubernetes Recipes?

This recipe is from Kubernetes Recipes, our 750-page practical guide with hundreds of production-ready patterns.

Luca Berton Ansible Pilot Ansible by Example Open Empower K8s Recipes Terraform Pilot CopyPasteLearn ProteinLens