OSUS Operator Disconnected OpenShift
Deploy the OpenShift Update Service (OSUS) operator for disconnected clusters. Local Cincinnati graph, graph-data image mirroring, and upgrade path serving.
💡 Quick Answer: The OpenShift Update Service (OSUS) operator deploys a local Cincinnati-compatible graph server inside your disconnected cluster. It serves the same upgrade path graph that connected clusters get from
api.openshift.com, but using locally mirrored graph-data and release images. Install the operator, mirror the graph-data container image, create anUpdateServiceCR, and point yourClusterVersionupstream to the local service.
The Problem
Connected OpenShift clusters query api.openshift.com to discover safe upgrade paths. Disconnected (air-gapped) clusters can’t reach this endpoint, which means:
oc adm upgradeshows no available updates- The ClusterVersion operator can’t determine valid upgrade paths
- Administrators have no visibility into which versions are reachable
- Blind upgrades risk hitting blocked paths or missing required intermediate hops
- No conditional update warnings — you might hit known bugs
The Solution
Architecture
graph TD
subgraph Internet Side
RH[registry.redhat.io] --> |mirror| MR[Mirror Registry]
API[api.openshift.com<br/>graph data] --> |oc-mirror| MR
end
subgraph Disconnected Cluster
OSUS[OSUS Operator] --> |deploys| GS[Graph Server Pod]
OSUS --> |reads| GDI[graph-data init container]
GDI --> |loads from| MR
GS --> |serves graph| CV[ClusterVersion Operator]
CV --> |queries| GS
CV --> |pulls releases| MR
end
style GS fill:#4CAF50,color:white
style CV fill:#2196F3,color:white
style MR fill:#FF9800,color:whiteStep 1: Mirror the Graph-Data Image
The graph-data image contains the entire upgrade graph (all channels, all architectures). It’s updated by Red Hat whenever new releases or edges are added.
# On a connected bastion host with access to both registries
# Mirror graph-data image to your internal registry
oc image mirror \
registry.redhat.io/openshift-update-service/graph-data:latest \
quay.example.com/openshift-update-service/graph-data:latest
# Verify the mirror
skopeo inspect docker://quay.example.com/openshift-update-service/graph-data:latest \
| jq '.Created'Automate this — the graph-data image is updated frequently. Set up a cron job:
#!/bin/bash
# mirror-graph-data.sh — run weekly or before planned upgrades
set -euo pipefail
SRC="registry.redhat.io/openshift-update-service/graph-data:latest"
DST="quay.example.com/openshift-update-service/graph-data:latest"
echo "[$(date)] Mirroring graph-data..."
oc image mirror "$SRC" "$DST" --keep-manifest-list=true
# Tag with date for rollback
DATE_TAG=$(date +%Y%m%d)
oc image mirror "$SRC" "${DST%:*}:${DATE_TAG}"
echo "[$(date)] Done. Latest graph-data mirrored."Step 2: Mirror Release Images
You need the actual OCP release images for every version in your planned upgrade path:
# Mirror a specific release
oc adm release mirror \
--from=quay.io/openshift-release-dev/ocp-release:4.14.42-x86_64 \
--to=quay.example.com/openshift-release-dev/ocp-release \
--to-release-image=quay.example.com/openshift-release-dev/ocp-release:4.14.42-x86_64
# Or use oc-mirror for batch mirroring
cat > imageset-config.yaml << 'EOF'
apiVersion: mirror.openshift.io/v1alpha2
kind: ImageSetConfiguration
mirror:
platform:
channels:
- name: stable-4.14
minVersion: 4.14.38
maxVersion: 4.14.42
- name: stable-4.15
minVersion: 4.15.30
maxVersion: 4.15.35
- name: eus-4.16
minVersion: 4.16.10
maxVersion: 4.16.15
graph: true # Include graph-data image
EOF
oc mirror --config=imageset-config.yaml \
docker://quay.example.comStep 3: Install the OSUS Operator
In disconnected clusters, OLM cannot access Red Hat’s remote OperatorHub sources. You need a mirrored or pruned catalog containing the OpenShift Update Service Operator. Include cincinnati-operator in your oc-mirror ImageSetConfiguration operators list.
# Namespace
apiVersion: v1
kind: Namespace
metadata:
name: openshift-update-service
annotations:
openshift.io/node-selector: ""
labels:
openshift.io/cluster-monitoring: "true"
---
# OperatorGroup
apiVersion: operators.coreos.com/v1
kind: OperatorGroup
metadata:
name: update-service-operator
namespace: openshift-update-service
spec:
targetNamespaces:
- openshift-update-service
---
# Subscription
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
name: cincinnati-operator
namespace: openshift-update-service
spec:
channel: v1
installPlanApproval: Manual
name: cincinnati-operator
source: redhat-operators
sourceNamespace: openshift-marketplacekubectl apply -f osus-operator.yaml
# Approve the install plan (Manual approval for production)
kubectl get installplan -n openshift-update-service
kubectl patch installplan <plan-name> -n openshift-update-service \
--type merge -p '{"spec":{"approved":true}}'
# Wait for operator
kubectl get pods -n openshift-update-service -w
# NAME READY STATUS
# update-service-operator-xxxxxxx 1/1 RunningStep 4: Create the UpdateService Instance
apiVersion: updateservice.operator.openshift.io/v1
kind: UpdateService
metadata:
name: update-service
namespace: openshift-update-service
spec:
replicas: 2 # HA for production
releases: quay.example.com/openshift-release-dev/ocp-release
graphDataImage: quay.example.com/openshift-update-service/graph-data:latestkubectl apply -f update-service-cr.yaml
# Wait for the graph server pods
kubectl get pods -n openshift-update-service
# NAME READY STATUS
# update-service-operator-xxxxxxx 1/1 Running
# update-service-xxxxxxx-xxxxx 1/1 Running
# update-service-xxxxxxx-xxxxx 1/1 Running
# Get the service URL
kubectl get route -n openshift-update-service
# NAME HOST/PORT
# update-service update-service-openshift-update-service.apps.cluster.example.comStep 5: Configure ClusterVersion to Use Local OSUS
# Get the OSUS policy engine URI from the UpdateService status
NAMESPACE=openshift-update-service
NAME=update-service
POLICY_ENGINE_GRAPH_URI="$(
oc -n "${NAMESPACE}" get updateservice "${NAME}" \
-o jsonpath='{.status.policyEngineURI}/api/upgrades_info/v1/graph{"\n"}'
)"
# Point ClusterVersion to local OSUS
oc patch clusterversion version --type merge -p "{
\"spec\": {
\"upstream\": \"${POLICY_ENGINE_GRAPH_URI}\"
}
}"
# Verify — should now show available updates
oc adm upgradeIf using a custom CA for the internal registry:
# Create a ConfigMap with the CA bundle
# IMPORTANT: The key MUST be named "updateservice-registry"
# If your registry URL includes a port, replace ":" with ".."
# e.g., registry.example.com:5000 → key: "registry.example.com..5000"
oc create configmap custom-ca \
--from-file=updateservice-registry=ca-bundle.crt \
-n openshift-config
# Reference it in the cluster proxy
oc patch proxy/cluster --type merge -p '{
"spec": {
"trustedCA": {
"name": "custom-ca"
}
}
}'
# Also ensure the cluster trusts the ingress/router CA
# CVO communicates with OSUS over the ingress route — if using
# self-signed ingress certificates, CVO must trust that CA tooStep 6: Verify the Graph is Serving
# Query the local graph endpoint
OSUS_ROUTE=$(kubectl get route update-service -n openshift-update-service -o jsonpath='{.spec.host}')
# Get graph for your channel
curl -sk "https://${OSUS_ROUTE}/api/upgrades_info/v1/graph?channel=stable-4.14&arch=amd64" \
| jq '.nodes | length'
# 45 (number of versions in the graph)
# Find your current version in the graph
CURRENT=$(oc get clusterversion -o jsonpath='{.items[0].status.desired.version}')
curl -sk "https://${OSUS_ROUTE}/api/upgrades_info/v1/graph?channel=stable-4.14&arch=amd64" \
| jq --arg v "$CURRENT" '[.nodes[] | select(.version == $v)] | length'
# 1 (your version exists in the graph)
# List all reachable versions from current
curl -sk "https://${OSUS_ROUTE}/api/upgrades_info/v1/graph?channel=stable-4.14&arch=amd64" \
| jq --arg v "$CURRENT" '
(.nodes | to_entries | map({key: .key, value: .value.version})) as $nodes |
($nodes | map(select(.value == $v)) | .[0].key) as $idx |
[.edges[] | select(.[0] == ($idx | tonumber)) | .[1]] as $targets |
[$nodes[] | select(.key as $k | $targets | map(tostring) | index($k))] |
map(.value) | sort
'Monitoring OSUS Health
# ServiceMonitor for OSUS metrics
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: update-service-monitor
namespace: openshift-update-service
spec:
selector:
matchLabels:
app: update-service
endpoints:
- port: metrics
interval: 30s# Quick health check script
#!/bin/bash
echo "=== OSUS Pods ==="
kubectl get pods -n openshift-update-service -o wide
echo ""
echo "=== Graph Freshness ==="
OSUS_ROUTE=$(kubectl get route update-service -n openshift-update-service -o jsonpath='{.spec.host}')
GRAPH_DATE=$(skopeo inspect docker://quay.example.com/openshift-update-service/graph-data:latest 2>/dev/null | jq -r '.Created')
echo "Graph-data image created: $GRAPH_DATE"
echo ""
echo "=== ClusterVersion Upstream ==="
oc get clusterversion -o jsonpath='{.items[0].spec.upstream}'
echo ""
echo ""
echo "=== Available Updates ==="
oc adm upgrade 2>&1 | head -15Refreshing the Graph
When Red Hat publishes new releases or upgrade edges:
# 1. Re-mirror graph-data on bastion
oc image mirror \
registry.redhat.io/openshift-update-service/graph-data:latest \
quay.example.com/openshift-update-service/graph-data:latest
# 2. Restart OSUS pods to pick up new graph-data
kubectl rollout restart deployment -n openshift-update-service -l app=update-service
# 3. Verify new versions appear
oc adm upgradeCommon Issues
“No updates available” after setting upstream
The graph-data image may be stale or the release images aren’t mirrored. Verify: (1) graph-data was recently mirrored, (2) your channel is set correctly, (3) the release images for target versions exist in your mirror registry.
OSUS pod CrashLooping
Check if the graphDataImage is accessible from within the cluster. Run kubectl logs -n openshift-update-service <pod> — common cause is image pull failure due to missing pull secret or untrusted CA.
Certificate errors querying OSUS route
The OSUS route uses the cluster’s default ingress certificate. If using self-signed ingress certificates, the CVO must trust the router CA to communicate with OSUS. This is the #1 missed step in air-gapped deployments — Red Hat’s guidance specifically calls out that the cluster must trust the router CA for CVO→OSUS communication over ingress.
Registry CA ConfigMap key naming
The ConfigMap key for the registry CA must be exactly updateservice-registry. If your registry URL includes a port (e.g., registry.example.com:5000), replace : with .. in the key name: registry.example.com..5000. Wrong key names cause silent trust failures.
Graph shows versions but release pull fails
You mirrored the graph but not the actual release images. Use oc adm release mirror for every version in your planned path. The graph tells you which versions exist — you still need the images.
Best Practices
- Mirror graph-data weekly — new edges and conditional updates appear frequently
- Use
oc-mirrorwithgraph: true— mirrors releases and graph-data in one operation - Set
replicas: 2for production OSUS — availability matters during upgrades - Use
installPlanApproval: Manual— control when the OSUS operator itself upgrades - Tag graph-data with dates — enables rollback to a known-good graph if issues arise
- Test the full upgrade path in staging — mirror, OSUS, and upgrade before production
- Document your mirror cron schedule — stale mirrors are the #1 cause of “no updates”
Key Takeaways
- OSUS is the official way to serve upgrade graphs in disconnected OpenShift environments
- Three things to mirror: graph-data image, release images, and operator catalog
- The graph-data image changes frequently — automate mirroring with cron
- ClusterVersion
spec.upstreampoints to the local OSUS route - Always verify the graph is serving and your version is present before starting upgrades
- Combine with
oc-mirror+ImageSetConfigurationfor a fully automated mirror pipeline

Recommended
Kubernetes Recipes — The Complete Book100+ production-ready patterns with detailed explanations, best practices, and copy-paste YAML. Everything in one place.
Get the Book →Learn by Doing
CopyPasteLearn — Hands-on Cloud & DevOps CoursesMaster Kubernetes, Ansible, Terraform, and MLOps with interactive, copy-paste-run lessons. Start free.
Browse Courses →🎓 Deepen Your Skills — Hands-on Courses
Courses by CopyPasteLearn.com — Learn IT by Doing
