Skip to content
15 changes: 15 additions & 0 deletions install/0000_50_cluster-update-console-plugin_10_namespace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Namespace
metadata:
name: openshift-cluster-update-console-plugin
annotations:
kubernetes.io/description: The OpenShift cluster-update console plugin provides a web-console interface for managing ClusterVersion updates.
capability.openshift.io/name: Console
release.openshift.io/feature-set: TechPreviewNoUpgrade
exclude.release.openshift.io/internal-openshift-hosted: "true"
include.release.openshift.io/self-managed-high-availability: "true"
labels:
openshift.io/cluster-monitoring: "true"
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/warn: restricted
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: openshift-cluster-update-console-plugin
annotations:
kubernetes.io/description: This NetworkPolicy is used to deny all ingress and egress traffic by default in this namespace, matching all Pods, and serving as a baseline.
capability.openshift.io/name: Console
release.openshift.io/feature-set: TechPreviewNoUpgrade
exclude.release.openshift.io/internal-openshift-hosted: "true"
include.release.openshift.io/self-managed-high-availability: "true"
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
Comment on lines +13 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Default-deny policy blocks plugin traffic without companion allow policies.

podSelector: {} + Ingress/Egress with no rules denies all traffic. As-is, backend access to the plugin service (and monitoring ingress) will be blocked unless additional allow policies are added in this namespace.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@install/0000_50_cluster-update-console-plugin_20_networkpolicy.yaml` around
lines 13 - 16, The NetworkPolicy currently uses podSelector: {} with
policyTypes: - Ingress and - Egress which creates a default-deny for all pods;
update the policy to either target specific pods instead of podSelector: {}
(e.g., use a labelSelector matching the console-plugin pods) or add explicit
allow rules for required traffic (ingress rules to permit backend/monitoring
sources and egress rules to permit upstream destinations) so the console plugin
service and monitoring can communicate; reference the podSelector and
policyTypes (Ingress, Egress) in the NetworkPolicy being modified and ensure the
new rules explicitly allow the necessary CIDRs/namespaceSelectors/port/protocols
for plugin traffic.

67 changes: 67 additions & 0 deletions install/0000_50_cluster-update-console-plugin_50_deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: cluster-update-console-plugin
namespace: openshift-cluster-update-console-plugin
annotations:
kubernetes.io/description: The OpenShift cluster-update console plugin provides a web-console interface for managing ClusterVersion updates.
capability.openshift.io/name: Console
release.openshift.io/feature-set: TechPreviewNoUpgrade
exclude.release.openshift.io/internal-openshift-hosted: "true"
include.release.openshift.io/self-managed-high-availability: "true"
spec:
selector:
matchLabels:
app: cluster-update-console-plugin
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 1
type: RollingUpdate
template:
metadata:
annotations:
target.workload.openshift.io/management: '{"effect": "PreferredDuringScheduling"}'
openshift.io/required-scc: restricted-v3
labels:
app: cluster-update-console-plugin
spec:
automountServiceAccountToken: false
containers:
- name: plugin
image: '{{index .Images "cluster-update-console-plugin"}}'
imagePullPolicy: IfNotPresent
ports:
- name: https
containerPort: 9001
resources:
Comment on lines +30 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add readiness/liveness probes for the plugin container.

The container currently has no probes, so rollout/health gating is weaker than required.

Suggested patch
       - name: plugin
         image: placeholder.url.oc.will.replace.this.example.org:cluster-update-console-plugin
         imagePullPolicy: IfNotPresent
         ports:
         - name: https
           containerPort: 9001
+        readinessProbe:
+          httpGet:
+            path: /
+            port: https
+            scheme: HTTPS
+        livenessProbe:
+          httpGet:
+            path: /
+            port: https
+            scheme: HTTPS

As per coding guidelines, **/*.{yaml,yml} requires Liveness + readiness probes defined.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@install/0000_50_cluster-update-console-plugin_50_deployment.yaml` around
lines 30 - 37, The pod spec for the container named "plugin" is missing liveness
and readiness probes; add both probes under the container "plugin" that use the
existing port/name (https / containerPort 9001) — for example configure an
httpGet (or tcpSocket) probe hitting a health endpoint (e.g. /health or /ready)
on port 9001, set sensible initialDelaySeconds, periodSeconds and
failureThreshold values, and include identical or appropriately different probe
settings for liveness (to restart unhealthy containers) and readiness (to gate
service traffic) so the deployment meets the Liveness + readiness probes
requirement.

requests:
cpu: 20m
memory: 50Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
terminationMessagePolicy: FallbackToLogsOnError
Comment on lines +37 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add container limits and read-only root filesystem.

Container plugin has requests but no limits, and readOnlyRootFilesystem is not set.

Suggested patch
       resources:
         requests:
           cpu: 20m
           memory: 50Mi
+        limits:
+          cpu: 100m
+          memory: 200Mi
       securityContext:
         allowPrivilegeEscalation: false
+        readOnlyRootFilesystem: true
         capabilities:
           drop:
           - ALL

As per coding guidelines, **/*.{yaml,yml} requires readOnlyRootFilesystem and Resource limits (cpu, memory) on every container.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
resources:
requests:
cpu: 20m
memory: 50Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
terminationMessagePolicy: FallbackToLogsOnError
resources:
requests:
cpu: 20m
memory: 50Mi
limits:
cpu: 100m
memory: 200Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
terminationMessagePolicy: FallbackToLogsOnError
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@install/0000_50_cluster-update-console-plugin_50_deployment.yaml` around
lines 37 - 46, The plugin container currently defines resources.requests but no
resources.limits and its securityContext lacks readOnlyRootFilesystem; update
the container spec for the container named "plugin" to add a resources.limits
block (set cpu and memory limits equal to or higher than the existing requests,
e.g., cpu and memory) and set securityContext.readOnlyRootFilesystem: true;
ensure you modify the container entry that contains resources.requests and
securityContext so both limits and readOnlyRootFilesystem are present and
compliant.

volumeMounts:
- mountPath: /var/cert
name: cluster-update-console-plugin-cert
readOnly: true
dnsPolicy: Default
hostUsers: false
priorityClassName: system-cluster-critical
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
terminationGracePeriodSeconds: 30
tolerations:
- effect: NoSchedule
key: node-role.kubernetes.io/infra
operator: Exists
volumes:
- name: cluster-update-console-plugin-cert
secret:
defaultMode: 420
secretName: cluster-update-console-plugin-cert
20 changes: 20 additions & 0 deletions install/0000_50_cluster-update-console-plugin_60_service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
apiVersion: v1
kind: Service
metadata:
name: openshift-cluster-update-console-plugin
namespace: openshift-cluster-update-console-plugin
annotations:
kubernetes.io/description: The OpenShift cluster-update console plugin provides a web-console interface for managing ClusterVersion updates.
service.beta.openshift.io/serving-cert-secret-name: cluster-update-console-plugin-cert
capability.openshift.io/name: Console
release.openshift.io/feature-set: TechPreviewNoUpgrade
exclude.release.openshift.io/internal-openshift-hosted: "true"
include.release.openshift.io/self-managed-high-availability: "true"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
spec:
type: ClusterIP
selector:
app: cluster-update-console-plugin
ports:
- name: https
port: 9001
targetPort: https
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
k8s-app: cluster-version-operator
name: cluster-version-operator
namespace: openshift-cluster-version
annotations:
kubernetes.io/description: Configure Prometheus to monitor cluster-version operator metrics.
capability.openshift.io/name: Console
release.openshift.io/feature-set: TechPreviewNoUpgrade
exclude.release.openshift.io/internal-openshift-hosted: "true"
include.release.openshift.io/self-managed-high-availability: "true"
spec:
endpoints:
- interval: 30s
port: metrics
scheme: https
tlsConfig:
serverName: cluster-version-operator.openshift-cluster-version.svc
scrapeClass: tls-client-certificate-auth
namespaceSelector:
matchNames:
- openshift-cluster-version
selector:
matchLabels:
k8s-app: cluster-version-operator
Comment on lines +1 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for ServiceMonitor resources with the same name/namespace
echo "=== ServiceMonitors named 'cluster-version-operator' ==="
rg -l "name: cluster-version-operator" --glob '*servicemonitor*.yaml' install/

echo ""
echo "=== Feature-set annotations in matching files ==="
for f in $(fd -e yaml servicemonitor install/); do
  if rg -q "name: cluster-version-operator" "$f"; then
    echo "--- $f ---"
    rg "release.openshift.io/feature-set|name:|namespace:" "$f" | head -10
  fi
done

Repository: openshift/cluster-version-operator

Length of output: 1174


🏁 Script executed:

#!/bin/bash
set -euo pipefail

files=(
  install/0000_50_cluster-update-console-plugin_80_servicemonitor.yaml
  install/0000_90_cluster-version-operator_02_servicemonitor.yaml
)

for f in "${files[@]}"; do
  echo "===== $f ====="
  # Print only metadata/annotations area (first ~80 lines around 'annotations:')
  if rg -n "annotations:" "$f" >/dev/null; then
    start=$(rg -n "annotations:" "$f" | head -1 | cut -d: -f1)
    # show 40 lines starting at annotations
    sed -n "$start,$((start+60))p" "$f" | cat -n
  else
    echo "(no annotations key found)"
  fi
  echo
done

Repository: openshift/cluster-version-operator

Length of output: 5204


🏁 Script executed:

#!/bin/bash
set -euo pipefail

files=(
  install/0000_50_cluster-update-console-plugin_80_servicemonitor.yaml
  install/0000_90_cluster-version-operator_02_servicemonitor.yaml
)

for f in "${files[@]}"; do
  echo "===== $f ====="
  if rg -n "annotations:" "$f" >/dev/null; then
    start=$(rg -n "annotations:" "$f" | head -1 | cut -d: -f1)
    sed -n "$start,$((start+80))p" "$f" | cat -n
  else
    echo "(no annotations key found)"
  fi
  echo
done

Repository: openshift/cluster-version-operator

Length of output: 6778


Fix duplicate ServiceMonitor name/namespace to avoid annotation overwrite

install/0000_50_cluster-update-console-plugin_80_servicemonitor.yaml and install/0000_90_cluster-version-operator_02_servicemonitor.yaml both define a ServiceMonitor named cluster-version-operator in openshift-cluster-version. Since 0000_90 is applied after 0000_50, it will replace the resource and drop capability.openshift.io/name: Console and release.openshift.io/feature-set: TechPreviewNoUpgrade from the console plugin manifest. Use a unique ServiceMonitor name for the console plugin (or merge the required annotations into the later manifest).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@install/0000_50_cluster-update-console-plugin_80_servicemonitor.yaml` around
lines 1 - 27, The ServiceMonitor in this manifest uses the same metadata.name
and namespace as the cluster-version-operator ServiceMonitor, causing the later
0000_90 manifest to overwrite annotations; fix by giving this console plugin
ServiceMonitor a unique metadata.name (e.g., append "-console" or similar) and
ensure any related references (labels/selectors like metadata.labels k8s-app and
selector.matchLabels) remain correct, or alternatively merge the annotations
(capability.openshift.io/name and release.openshift.io/feature-set) into the
later ServiceMonitor so they are not lost when 0000_90 is applied.

122 changes: 122 additions & 0 deletions install/0000_50_cluster-update-console-plugin_81_prometheusrule.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
labels:
k8s-app: cluster-version-operator
name: cluster-version-operator
namespace: openshift-cluster-version
annotations:
kubernetes.io/description: Alerting rules for when cluster-version operator metrics call for administrator attention.
capability.openshift.io/name: Console
release.openshift.io/feature-set: TechPreviewNoUpgrade
exclude.release.openshift.io/internal-openshift-hosted: "true"
include.release.openshift.io/self-managed-high-availability: "true"
Comment on lines +1 to +13

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for PrometheusRule resources with the same name/namespace
echo "=== PrometheusRules named 'cluster-version-operator' ==="
rg -l "kind: PrometheusRule" install/ | xargs -I{} sh -c 'rg -q "name: cluster-version-operator" {} && echo {}'

echo ""
echo "=== Alert definitions in 0000_90 file for comparison ==="
rg "alert:" install/0000_90_cluster-version-operator_02_servicemonitor.yaml 2>/dev/null || echo "File not found or no alerts"

Repository: openshift/cluster-version-operator

Length of output: 736


🏁 Script executed:

#!/bin/bash
set -euo pipefail

files=(
  install/0000_90_cluster-version-operator_02_servicemonitor.yaml
  install/0000_90_cluster-version-operator_02_prometheusrule_servicemonitor.yaml
  install/0000_50_cluster-update-console-plugin_81_prometheusrule.yaml
)

for f in "${files[@]}"; do
  echo "=== $f (metadata) ==="
  rg -n "^(kind:|name:|namespace:)" "$f" || true
  echo
done

echo "=== alerts present in 0000_50_cluster-update-console-plugin_81_prometheusrule.yaml ==="
rg -n "^[[:space:]]*- alert:" install/0000_50_cluster-update-console-plugin_81_prometheusrule.yaml || true

echo
echo "=== alerts present in 0000_90_cluster-version-operator_02_prometheusrule_servicemonitor.yaml ==="
rg -n "^[[:space:]]*- alert:" install/0000_90_cluster-version-operator_02_prometheusrule_servicemonitor.yaml || true

Repository: openshift/cluster-version-operator

Length of output: 1034


🏁 Script executed:

#!/bin/bash
set -euo pipefail

files=(
  install/0000_90_cluster-version-operator_02_servicemonitor.yaml
  install/0000_90_cluster-version-operator_02_prometheusrule_servicemonitor.yaml
  install/0000_50_cluster-update-console-plugin_81_prometheusrule.yaml
)

for f in "${files[@]}"; do
  echo "=== $f (PrometheusRule metadata.name/namespace blocks) ==="
  awk '
    $0 ~ /kind:[[:space:]]*PrometheusRule/ {inpr=1}
    inpr && $0 ~ /^[[:space:]]*name:[[:space:]]*/ {print "name:", $0}
    inpr && $0 ~ /^[[:space:]]*namespace:[[:space:]]*/ {print "namespace:", $0}
    $0 ~ /^---/ {inpr=0}
  ' "$f" || true
  echo
done

echo "=== $PWD / sanity: show exact metadata snippet from 0000_50 file ==="
rg -n "kind: PrometheusRule|name: |namespace: |annotations:|labels:" install/0000_50_cluster-update-console-plugin_81_prometheusrule.yaml

Repository: openshift/cluster-version-operator

Length of output: 1572


🏁 Script executed:

#!/bin/bash
set -euo pipefail

f="install/0000_50_cluster-update-console-plugin_81_prometheusrule.yaml"

echo "=== Check last_over_time usage ==="
rg -n "last_over_time" "$f" || true

echo
echo "=== Check for '== 0' vs '!= 0' in alert expressions ==="
rg -n "expr:.*(==[[:space:]]*0|!=[[:space:]]*0)" "$f" || true

echo
echo "=== Extract UpdateAvailable block (up to next alert) ==="
python3 - <<'PY'
import re, itertools, sys
path="install/0000_50_cluster-update-console-plugin_81_prometheusrule.yaml"
lines=open(path,'r',encoding='utf-8').read().splitlines()
start=None
for i,l in enumerate(lines):
    if re.match(r'^\s*-\s*alert:\s*UpdateAvailable\s*$', l):
        start=i
        break
if start is None:
    print("UpdateAvailable not found")
    sys.exit(0)
# print until next "- alert:" line or EOF
for l in lines[start:]:
    if l!=lines[start] and re.match(r'^\s*-\s*alert:\s*', l):
        break
    print(l)
PY

echo
echo "=== Check runbookUrl and description/summary annotations for each alert ==="
python3 - <<'PY'
import re, sys
path="install/0000_50_cluster-update-console-plugin_81_prometheusrule.yaml"
text=open(path,'r',encoding='utf-8').read().splitlines()
alerts=[]
current=None
buf=[]
for l in text:
    m=re.match(r'^\s*-\s*alert:\s*(.+)\s*$', l)
    if m:
        if current:
            alerts.append((current, buf))
        current=m.group(1).strip()
        buf=[l]
    else:
        if current:
            buf.append(l)
if current:
    alerts.append((current, buf))

for name,blk in alerts:
    # capture first occurrences
    run=None; desc=None; summ=None
    for l in blk:
        if 'runbookUrl:' in l:
            run=l.strip()
        if re.match(r'^\s*description:\s*', l):
            desc=l.strip()
        if re.match(r'^\s*summary:\s*', l):
            summ=l.strip()
    print(f"--- {name} ---")
    if run: print(run)
    if summ: print(summ)
    if desc: print(desc)
    # for presence
    for_present=any(re.match(r'^\s*for:\s*', l) for l in blk)
    print("for:", for_present)
PY

Repository: openshift/cluster-version-operator

Length of output: 6708


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) ServiceMonitor + PrometheusRule names/namespaces inside the combined servicemonitor file
f1="install/0000_90_cluster-version-operator_02_servicemonitor.yaml"
echo "=== $f1: all name/namespace blocks by resource kind ==="
python3 - <<'PY'
import re
path="install/0000_90_cluster-version-operator_02_servicemonitor.yaml"
lines=open(path,'r',encoding='utf-8').read().splitlines()
kind=None
out=[]
for i,l in enumerate(lines):
    m=re.match(r'^\s*kind:\s*(.+)\s*$', l)
    if m:
        kind=m.group(1)
    if kind and re.match(r'^\s*name:\s*', l):
        out.append(("name",kind,l.strip()))
        kind=None  # only first name after kind
    if re.match(r'^\s*namespace:\s*', l):
        # heuristic: namespace nearby to metadata block
        out.append(("namespace",kind,l.strip()))
# just print all lines that look relevant
for l in lines:
    if re.match(r'^\s*(kind|name|namespace):', l):
        print(l)
PY

echo
echo "=== $f1: last_over_time + runbookUrl presence ==="
rg -n "last_over_time|runbookUrl:" "$f1" || true

echo
echo "=== $f1: UpdateAvailable alert block (if present) for expr/for/runbookUrl ==="
python3 - <<'PY'
import re
path="install/0000_90_cluster-version-operator_02_servicemonitor.yaml"
lines=open(path,'r',encoding='utf-8').read().splitlines()
# Find block for alert: UpdateAvailable
start=None
for i,l in enumerate(lines):
    if re.match(r'^\s*-\s*alert:\s*UpdateAvailable\s*$', l):
        start=i
        break
if start is None:
    print("No UpdateAvailable alert in file")
    raise SystemExit
for l in lines[start:]:
    if l!=lines[start] and re.match(r'^\s*-\s*alert:\s*', l):
        break
    print(l)
PY

echo
echo "=== $f1: ClusterVersionOperatorDown block (if present): for + runbookUrl ==="
python3 - <<'PY'
import re
path="install/0000_90_cluster-version-operator_02_servicemonitor.yaml"
lines=open(path,'r',encoding='utf-8').read().splitlines()
start=None
target="ClusterVersionOperatorDown"
for i,l in enumerate(lines):
    if re.match(r'^\s*-\s*alert:\s*%s\s*$' % re.escape(target), l):
        start=i; break
if start is None:
    print("No ClusterVersionOperatorDown alert in file"); raise SystemExit
for l in lines[start:]:
    if l!=lines[start] and re.match(r'^\s*-\s*alert:\s*', l):
        break
    if 'runbookUrl:' in l or re.match(r'^\s*for:\s*', l) or re.match(r'^\s*-\s*alert:\s*', l) or re.match(r'^\s*expr:\s*|^\s*summary:\s*', l) or re.match(r'^\s*description:\s*', l):
        print(l)
PY

echo
echo "=== 0000_90_prometheusrule_servicemonitor.yaml: last_over_time + runbookUrl ==="
f2="install/0000_90_cluster-version-operator_02_prometheusrule_servicemonitor.yaml"
rg -n "last_over_time|runbookUrl:" "$f2" || true

echo
echo "=== 0000_90_prometheusrule_servicemonitor.yaml: all alert names ==="
rg -n "^[[:space:]]*-[[:space:]]*alert:" "$f2"

Repository: openshift/cluster-version-operator

Length of output: 2408


🏁 Script executed:

#!/bin/bash
set -euo pipefail

new="install/0000_50_cluster-update-console-plugin_81_prometheusrule.yaml"
old="install/0000_90_cluster-version-operator_02_servicemonitor.yaml"

echo "=== Alert names in new PrometheusRule ==="
rg -n "^[[:space:]]*-[[:space:]]*alert:" "$new" | sed -E 's/.*alert:[[:space:]]*//'

echo
echo "=== Alert names in old PrometheusRule (cluster-version-operator) ==="
# Extract only the PrometheusRule section for name: cluster-version-operator, then list alerts.
python3 - <<'PY'
import re
path="install/0000_90_cluster-version-operator_02_servicemonitor.yaml"
text=open(path,'r',encoding='utf-8').read().splitlines()

# Find PrometheusRule with metadata.name == cluster-version-operator
in_pr=False
in_meta=False
found=False
blocks=[]
current=[]
for i,l in enumerate(text):
    if re.match(r'^\s*kind:\s*PrometheusRule\s*$', l):
        in_pr=True
        in_meta=False
        found=False
        current=[l]
        continue
    if in_pr:
        current.append(l)
        if re.match(r'^\s*metadata:\s*$', l):
            in_meta=True
            continue
        if in_meta and re.match(r'^\s*name:\s*cluster-version-operator\s*$', l):
            found=True
        # end of document heuristic: next 'kind:' at same indent (assume file has single document or multiple separated by ---)
        if re.match(r'^---\s*$', l) and current:
            in_pr=False
    # For simplicity, when found alert names we will re-scan using a regex window.
PY

# Regex window approach: take from "kind: PrometheusRule" near "name: cluster-version-operator" to next "kind:" at same indent or EOF
python3 - <<'PY'
import re
path="install/0000_90_cluster-version-operator_02_servicemonitor.yaml"
s=open(path,'r',encoding='utf-8').read()

# Grab the PrometheusRule block that contains "kind: PrometheusRule" and "name: cluster-version-operator"
m=re.search(r'(kind:\s*PrometheusRule[\s\S]*?name:\s*cluster-version-operator[\s\S]*?)(?=kind:\s|$)', s)
if not m:
    print("Could not isolate PrometheusRule block"); raise SystemExit(1)
block=m.group(1)
alerts=re.findall(r'^\s*-\s*alert:\s*(.+?)\s*$', block, flags=re.M)
for a in alerts:
    print(a)
PY

Repository: openshift/cluster-version-operator

Length of output: 618


🏁 Script executed:

#!/bin/bash
set -euo pipefail

new="install/0000_50_cluster-update-console-plugin_81_prometheusrule.yaml"
old="install/0000_90_cluster-version-operator_02_servicemonitor.yaml"
old2="install/0000_90_cluster-version-operator_02_prometheusrule_servicemonitor.yaml"

echo "=== new: last_over_time/runbookUrl presence ==="
rg -n "last_over_time\\(|runbookUrl:" "$new" || true

echo
echo "=== new: expr contains == 0 or != 0 ==="
rg -n "(==[[:space:]]*0|!=[[:space:]]*0)" "$new" || true

echo
echo "=== new: metric name patterns with _conditions/_condition ==="
rg -n "cluster_.*_conditions|_conditions\\b|_condition\\b" "$new" || true

echo
echo "=== new: for: durations by alert ==="
python3 - <<'PY'
import re
path="install/0000_50_cluster-update-console-plugin_81_prometheusrule.yaml"
lines=open(path,'r',encoding='utf-8').read().splitlines()
alerts=[]
cur=None; buf=[]
for l in lines:
    m=re.match(r'^\s*-\s*alert:\s*(.+)\s*$', l)
    if m:
        if cur: alerts.append((cur,buf))
        cur=m.group(1).strip(); buf=[l]
    else:
        if cur: buf.append(l)
if cur: alerts.append((cur,buf))
for name,blk in alerts:
    dur=None
    for l in blk:
        m=re.match(r'^\s*for:\s*(.+)\s*$', l)
        if m:
            dur=m.group(1).strip(); break
    print(f"{name}: for={dur}")
PY

echo
echo "=== old: last_over_time/runbookUrl presence (same check) ==="
rg -n "last_over_time\\(|runbookUrl:" "$old" || true

echo
echo "=== old2: last_over_time/runbookUrl presence (same check) ==="
rg -n "last_over_time\\(|runbookUrl:" "$old2" || true

Repository: openshift/cluster-version-operator

Length of output: 3632


🏁 Script executed:

#!/bin/bash
set -euo pipefail

new="install/0000_50_cluster-update-console-plugin_81_prometheusrule.yaml"
old="install/0000_90_cluster-version-operator_02_servicemonitor.yaml"

python3 - <<'PY'
import re, json

def parse_prometheusrule(path, target_name="cluster-version-operator"):
    text=open(path,'r',encoding='utf-8').read().splitlines()

    # isolate first PrometheusRule document whose metadata.name matches target_name
    # (heuristic: find "kind: PrometheusRule", then metadata.name line)
    docs=[]
    cur_kind=None
    cur=[]
    for l in text:
        if l.strip()=="kind: PrometheusRule":
            cur_kind="PrometheusRule"
        cur.append(l)
    s="\n".join(text)

    # extract block between "kind: PrometheusRule" and next "---" at doc boundary
    # We'll approximate by searching for "kind: PrometheusRule" segments and then checking name.
    parts=re.split(r'(?m)^---\s*$', s)
    for p in parts:
        if "kind: PrometheusRule" not in p:
            continue
        # check metadata.name
        if not re.search(r'(?m)^\s*name:\s*%s\s*$' % re.escape(target_name), p):
            continue
        docs.append(p)

    if not docs:
        raise SystemExit(f"No PrometheusRule with name={target_name} found in {path}")
    p=docs[0]

    alerts={}
    # For each alert block, capture until next "- alert:"
    # We'll operate on YAML lines for stability.
    lines=p.splitlines()
    cur=None; buf=[]
    for l in lines:
        m=re.match(r'^\s*-\s*alert:\s*(.+?)\s*$', l)
        if m:
            if cur:
                alerts[cur]=buf
            cur=m.group(1).strip()
            buf=[l]
        else:
            if cur:
                buf.append(l)
    if cur:
        alerts[cur]=buf

    out={}
    for alert, blk in alerts.items():
        item={}
        for l in blk:
            m=re.match(r'^\s*expr:\s*(.*)$', l)
            if m:
                item['expr_firstline']=m.group(1).strip()
            m=re.match(r'^\s*for:\s*(.*)$', l)
            if m:
                item['for']=m.group(1).strip()
            m=re.match(r'^\s*runbookUrl:\s*(.*)$', l)
            if m:
                item['runbookUrl']=m.group(1).strip()
            m=re.match(r'^\s*summary:\s*(.*)$', l)
            if m:
                item['summary_firstline']=m.group(1).strip()
            if re.match(r'^\s*description:\s*', l):
                # keep first line only
                item['description_firstline']=l.strip()
        out[alert]=item
    return out

old_map=parse_prometheusrule(old)
new_map=parse_prometheusrule(new)

alerts=sorted(set(old_map)|set(new_map))
report=[]
for a in alerts:
    o=old_map.get(a,{})
    n=new_map.get(a,{})
    keys=set(o)|set(n)
    diff={}
    for k in sorted(keys):
        if o.get(k)!=n.get(k):
            diff[k]={'old':o.get(k),'new':n.get(k)}
    report.append((a,diff))

print(json.dumps(report, indent=2, sort_keys=False))
PY

Repository: openshift/cluster-version-operator

Length of output: 211


🏁 Script executed:

#!/bin/bash
set -euo pipefail

new="install/0000_50_cluster-update-console-plugin_81_prometheusrule.yaml"
old="install/0000_90_cluster-version-operator_02_servicemonitor.yaml"

python3 - <<'PY'
import re, json

def parse_prometheusrule(path, target_name="cluster-version-operator"):
    text=open(path,'r',encoding='utf-8').read().splitlines()
    s="\n".join(text)

    parts=re.split(r'(?m)^---\s*$', s)
    docs=[]
    for p in parts:
        if "kind: PrometheusRule" not in p:
            continue
        if re.search(r'(?m)^\s*name:\s*%s\s*$' % re.escape(target_name), p):
            docs.append(p)

    if not docs:
        raise SystemExit(f"No PrometheusRule with name={target_name} found in {path}")

    p=docs[0]

    alerts={}
    lines=p.splitlines()
    cur=None; buf=[]
    for l in lines:
        m=re.match(r'^\s*-\s*alert:\s*(.+?)\s*$', l)
        if m:
            if cur:
                alerts[cur]=buf
            cur=m.group(1).strip()
            buf=[l]
        else:
            if cur:
                buf.append(l)
    if cur:
        alerts[cur]=buf

    out={}
    for alert, blk in alerts.items():
        item={}
        for l in blk:
            m=re.match(r'^\s*expr:\s*(.*)$', l)
            if m:
                item['expr_firstline']=m.group(1).strip()
            m=re.match(r'^\s*for:\s*(.*)$', l)
            if m:
                item['for']=m.group(1).strip()
            m=re.match(r'^\s*runbookUrl:\s*(.*)$', l)
            if m:
                item['runbookUrl']=m.group(1).strip()
            if re.match(r'^\s*summary:\s*', l):
                item['summary_firstline']=l.strip().removeprefix('summary:').strip()
            if re.match(r'^\s*description:\s*', l):
                item['description_firstline']=l.strip().removeprefix('description:').strip()
        out[alert]=item
    return out

old_map=parse_prometheusrule(old)
new_map=parse_prometheusrule(new)

alerts=sorted(set(old_map)|set(new_map))
report=[]
for a in alerts:
    o=old_map.get(a,{})
    n=new_map.get(a,{})
    diff={}
    for k in sorted(set(o)|set(n)):
        if o.get(k)!=n.get(k):
            diff[k]={'old':o.get(k),'new':n.get(k)}
    report.append((a,diff))

print(json.dumps([{'alert':a,'diff':d} for a,d in report if d], indent=2))
print("\nUnchanged alerts:", [a for a,d in report if not d])
PY

Repository: openshift/cluster-version-operator

Length of output: 211


🏁 Script executed:

#!/bin/bash
set -euo pipefail

old="install/0000_90_cluster-version-operator_02_servicemonitor.yaml"
new="install/0000_50_cluster-update-console-plugin_81_prometheusrule.yaml"

python3 - <<PY
import re, json

def parse_prometheusrule(path, target_name="cluster-version-operator"):
    text=open(path,'r',encoding='utf-8').read().splitlines()
    s="\\n".join(text)

    parts=re.split(r'(?m)^---\\s*$', s)
    docs=[]
    for p in parts:
        if "kind: PrometheusRule" not in p:
            continue
        if re.search(r'(?m)^\\s*name:\\s*%s\\s*$' % re.escape(target_name), p):
            docs.append(p)
    if not docs:
        raise SystemExit(f"No PrometheusRule with name={target_name} found in {path}")
    p=docs[0]

    alerts={}
    lines=p.splitlines()
    cur=None; buf=[]
    for l in lines:
        m=re.match(r'^\\s*-\\s*alert:\\s*(.+?)\\s*$', l)
        if m:
            if cur:
                alerts[cur]=buf
            cur=m.group(1).strip()
            buf=[l]
        else:
            if cur:
                buf.append(l)
    if cur:
        alerts[cur]=buf

    out={}
    for alert, blk in alerts.items():
        item={}
        for l in blk:
            m=re.match(r'^\\s*expr:\\s*(.*)$', l)
            if m and 'expr_firstline' not in item:
                item['expr_firstline']=m.group(1).strip()
            m=re.match(r'^\\s*for:\\s*(.*)$', l)
            if m:
                item['for']=m.group(1).strip()
            m=re.match(r'^\\s*runbookUrl:\\s*(.*)$', l)
            if m:
                item['runbookUrl']=m.group(1).strip()
            m=re.match(r'^\\s*summary:\\s*(.*)$', l)
            if m:
                item['summary']=m.group(1).strip()
            m=re.match(r'^\\s*description:\\s*(.*)$', l)
            if m:
                item['description']=m.group(1).strip()
        out[alert]=item
    return out

old_map=parse_prometheusrule("$old")
new_map=parse_prometheusrule("$new")

alerts=sorted(set(old_map)|set(new_map))
report=[]
unchanged=[]
for a in alerts:
    o=old_map.get(a,{})
    n=new_map.get(a,{})
    diff={}
    for k in sorted(set(o)|set(n)):
        if o.get(k)!=n.get(k):
            diff[k]={'old':o.get(k),'new':n.get(k)}
    if diff:
        report.append({'alert':a,'diff':diff})
    else:
        unchanged.append(a)

print("Modified alerts:")
print(json.dumps(report, indent=2))
print("\\nUnchanged alerts:")
print(unchanged)
PY

Repository: openshift/cluster-version-operator

Length of output: 359


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import re, json
path="install/0000_50_cluster-update-console-plugin_81_prometheusrule.yaml"
lines=open(path,'r',encoding='utf-8').read().splitlines()

alerts={}
cur=None; buf=[]
for l in lines:
    m=re.match(r'^\s*-\s*alert:\s*(.+?)\s*$', l)
    if m:
        if cur: alerts[cur]=buf
        cur=m.group(1).strip(); buf=[l]
    else:
        if cur: buf.append(l)
if cur: alerts[cur]=buf

def get_label_severity(blk):
    sev=None
    # find first "severity:" under labels: within the alert block
    for i,l in enumerate(blk):
        if re.match(r'^\s*labels:\s*$', l):
            # scan until next "labels:" or next alert header
            for j in range(i+1, len(blk)):
                ll=blk[j]
                if re.match(r'^\s*-\s*alert:\s*', ll): break
                if re.match(r'^\s*[a-zA-Z0-9_-]+\s*:\s*', ll):
                    # first severity line
                    m=re.match(r'^\s*severity:\s*(.+?)\s*$', ll)
                    if m:
                        return m.group(1).strip()
    return sev

for name in ["UpdateAvailable","CannotRetrieveUpdates","CannotEvaluateConditionalUpdates"]:
    blk=alerts.get(name)
    print(name, "severity=", get_label_severity(blk) if blk else None)
PY

Repository: openshift/cluster-version-operator

Length of output: 200


🏁 Script executed:

#!/bin/bash
set -euo pipefail

old="install/0000_90_cluster-version-operator_02_servicemonitor.yaml"
new="install/0000_50_cluster-update-console-plugin_81_prometheusrule.yaml"

echo "=== Console-related metadata in old ($old) ==="
rg -n "capability\\.openshift\\.io/name|release\\.openshift\\.io/feature-set|console_url" "$old" || true
echo
echo "=== Console-related metadata in new ($new) ==="
rg -n "capability\\.openshift\\.io/name|release\\.openshift\\.io/feature-set|console_url" "$new" || true

Repository: openshift/cluster-version-operator

Length of output: 9664


🏁 Script executed:

#!/bin/bash
set -euo pipefail

old="install/0000_90_cluster-version-operator_02_servicemonitor.yaml"
new="install/0000_50_cluster-update-console-plugin_81_prometheusrule.yaml"

echo "=== Old file: release/include/exclude/feature-set annotations ==="
rg -n "exclude\\.release\\.openshift\\.io/|include\\.release\\.openshift\\.io/|feature-set|capability\\.openshift\\.io/name" "$old" || true

echo
echo "=== New file: release/include/exclude/feature-set annotations ==="
rg -n "exclude\\.release\\.openshift\\.io/|include\\.release\\.openshift\\.io/|feature-set|capability\\.openshift\\.io/name" "$new" || true

Repository: openshift/cluster-version-operator

Length of output: 753


Fix duplicate PrometheusRule and missing PrometheusRule annotations

  • install/0000_50_cluster-update-console-plugin_81_prometheusrule.yaml defines kind: PrometheusRule with metadata.name: cluster-version-operator and metadata.namespace: openshift-cluster-version, which matches the existing PrometheusRule in install/0000_90_cluster-version-operator_02_servicemonitor.yaml; since the alert set/name is the same, one will overwrite the other depending on apply order, potentially dropping the Console/feature-set gating annotations.
  • The alerts in this PrometheusRule have no runbookUrl fields and the expressions are not wrapped with last_over_time(...[5m]) per PrometheusRule conventions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@install/0000_50_cluster-update-console-plugin_81_prometheusrule.yaml` around
lines 1 - 13, This PrometheusRule duplicates an existing PrometheusRule with
metadata.name "cluster-version-operator" in namespace
"openshift-cluster-version" and lacks runbookUrl fields and proper
last_over_time wrapping; remove or rename this duplicate resource (or merge its
alerts into the existing PrometheusRule) so only one PrometheusRule with kind
PrometheusRule and name "cluster-version-operator" exists for that namespace,
and update each alert expression to wrap range queries with
last_over_time(...[5m]) and add appropriate runbookUrl annotations for each
alert to preserve Console/feature-set gating annotations (ensure the resource
that remains includes the Console gating annotations shown in
metadata.annotations).

spec:
groups:
- name: cluster-version
rules:
- alert: ClusterVersionOperatorDown
annotations:
summary: Cluster version operator has disappeared from Prometheus target discovery.
description: The operator may be down or disabled. The cluster will not be kept up to date and upgrades will not be possible. Inspect the openshift-cluster-version namespace for events or changes to the cluster-version-operator deployment or pods to diagnose and repair. {{ "{{ with $console_url := \"console_url\" | query }}{{ if ne (len (label \"url\" (first $console_url ) ) ) 0}} For more information refer to {{ label \"url\" (first $console_url ) }}/k8s/cluster/projects/openshift-cluster-version.{{ end }}{{ end }}" }}
runbook_url: https://github.com/openshift/runbooks/blob/master/alerts/cluster-version-operator/ClusterVersionOperatorDown.md
expr: |
absent(up{job="cluster-version-operator"} == 1)
for: 10m
labels:
namespace: openshift-cluster-version
severity: critical
- alert: CannotRetrieveUpdates
annotations:
summary: Cluster version operator has not retrieved updates in {{ "{{ $value | humanizeDuration }}" }}.
description: Failure to retrieve updates means that cluster administrators will need to monitor for available updates on their own or risk falling behind on security or other bugfixes. If the failure is expected, you can clear spec.channel in the ClusterVersion object to tell the cluster-version operator to not retrieve updates. Failure reason {{ "{{ with $cluster_operator_conditions := \"cluster_operator_conditions\" | query}}{{range $value := .}}{{if and (eq (label \"name\" $value) \"version\") (eq (label \"condition\" $value) \"RetrievedUpdates\") (eq (label \"endpoint\" $value) \"metrics\") (eq (value $value) 0.0)}}{{label \"reason\" $value}} {{end}}{{end}}{{end}}" }}. For more information refer to `oc get clusterversion/version -o=jsonpath="{.status.conditions[?(.type=='RetrievedUpdates')]}{'\n'}"`{{ "{{ with $console_url := \"console_url\" | query }}{{ if ne (len (label \"url\" (first $console_url ) ) ) 0}} or {{ label \"url\" (first $console_url ) }}/settings/cluster/{{ end }}{{ end }}" }}.
expr: |
max by (namespace)
(
(
time()-cluster_version_operator_update_retrieval_timestamp_seconds
) >= 3600
and ignoring(condition, name, reason)
(cluster_operator_conditions{name="version", condition="RetrievedUpdates", endpoint="metrics", reason!="NoChannel"})
)
labels:
severity: warning
- alert: UpdateAvailable
annotations:
summary: Your upstream update recommendation service recommends you update your cluster.
description: For more information refer to 'oc adm upgrade'{{ "{{ with $console_url := \"console_url\" | query }}{{ if ne (len (label \"url\" (first $console_url ) ) ) 0}} or {{ label \"url\" (first $console_url ) }}/settings/cluster/{{ end }}{{ end }}" }}.
expr: |
sum by (channel, namespace, upstream) (cluster_version_available_updates) > 0
labels:
severity: info
- alert: ClusterReleaseNotAccepted
annotations:
summary: The desired cluster release has not been accepted for at least an hour.
description: The desired cluster release has not been accepted because {{ "{{ $labels.reason }}" }}, and the cluster will continue to reconcile an earlier release instead of moving towards that desired release. For more information refer to 'oc adm upgrade'{{ "{{ with $console_url := \"console_url\" | query }}{{ if ne (len (label \"url\" (first $console_url ) ) ) 0}} or {{ label \"url\" (first $console_url ) }}/settings/cluster/{{ end }}{{ end }}" }}.
expr: |
max by (namespace, name, reason) (cluster_operator_conditions{name="version", condition="ReleaseAccepted", endpoint="metrics"} == 0)
for: 60m
labels:
severity: warning
- name: cluster-operators
rules:
- alert: ClusterNotUpgradeable
annotations:
summary: One or more cluster operators have been blocking minor or major version cluster updates for at least an hour.
description: In most cases, you will still be able to apply patch releases. Reason {{ "{{ with $cluster_operator_conditions := \"cluster_operator_conditions\" | query}}{{range $value := .}}{{if and (eq (label \"name\" $value) \"version\") (eq (label \"condition\" $value) \"Upgradeable\") (eq (label \"endpoint\" $value) \"metrics\") (eq (value $value) 0.0) (ne (len (label \"reason\" $value)) 0) }}{{label \"reason\" $value}}.{{end}}{{end}}{{end}}"}} For more information refer to 'oc adm upgrade'{{ "{{ with $console_url := \"console_url\" | query }}{{ if ne (len (label \"url\" (first $console_url ) ) ) 0}} or {{ label \"url\" (first $console_url ) }}/settings/cluster/{{ end }}{{ end }}" }}.
expr: |
max by (namespace, name, condition, endpoint) (cluster_operator_conditions{name="version", condition="Upgradeable", endpoint="metrics"} == 0)
for: 60m
labels:
severity: info
- alert: ClusterOperatorDown
annotations:
summary: Cluster operator has not been available for 10 minutes.
description: The {{ "{{ $labels.name }}" }} operator may be down or disabled because {{ "{{ $labels.reason }}" }}, and the components it manages may be unavailable or degraded. Cluster upgrades may not complete. For more information refer to '{{ "{{ if eq $labels.name \"version\" }}oc adm upgrade{{ else }}oc get -o yaml clusteroperator {{ $labels.name }}{{ end }}" }}'{{ "{{ with $console_url := \"console_url\" | query }}{{ if ne (len (label \"url\" (first $console_url ) ) ) 0}} or {{ label \"url\" (first $console_url ) }}/settings/cluster/{{ end }}{{ end }}" }}.
runbook_url: https://github.com/openshift/runbooks/blob/master/alerts/cluster-monitoring-operator/ClusterOperatorDown.md
expr: |
max by (namespace, name, reason) (cluster_operator_up{job="cluster-version-operator"} == 0)
for: 10m
labels:
severity: critical
- alert: ClusterOperatorDegraded
annotations:
summary: Cluster operator has been degraded for 30 minutes.
description: The {{ "{{ $labels.name }}" }} operator is degraded because {{ "{{ $labels.reason }}" }}, and the components it manages may have reduced quality of service. Cluster upgrades may not complete. For more information refer to '{{ "{{ if eq $labels.name \"version\" }}oc adm upgrade{{ else }}oc get -o yaml clusteroperator {{ $labels.name }}{{ end }}" }}'{{ "{{ with $console_url := \"console_url\" | query }}{{ if ne (len (label \"url\" (first $console_url ) ) ) 0}} or {{ label \"url\" (first $console_url ) }}/settings/cluster/{{ end }}{{ end }}" }}.
runbook_url: https://github.com/openshift/runbooks/blob/master/alerts/cluster-monitoring-operator/ClusterOperatorDegraded.md
expr: |
max by (namespace, name, reason)
(
(
cluster_operator_conditions{job="cluster-version-operator", name!="version", condition="Degraded"}
or on (namespace, name)
cluster_operator_conditions{job="cluster-version-operator", name="version", condition="Failing"}
or on (namespace, name)
group by (namespace, name) (cluster_operator_up{job="cluster-version-operator"})
) == 1
)
for: 30m
labels:
severity: warning
- alert: ClusterOperatorFlapping
annotations:
summary: Cluster operator up status is changing often.
description: The {{ "{{ $labels.name }}" }} operator behavior might cause upgrades to be unstable. For more information refer to '{{ "{{ if eq $labels.name \"version\" }}oc adm upgrade{{ else }}oc get -o yaml clusteroperator {{ $labels.name }}{{ end }}" }}'{{ "{{ with $console_url := \"console_url\" | query }}{{ if ne (len (label \"url\" (first $console_url ) ) ) 0}} or {{ label \"url\" (first $console_url ) }}/settings/cluster/{{ end }}{{ end }}" }}.
expr: |
max by (namespace, name) (changes(cluster_operator_up{job="cluster-version-operator"}[2m]) > 2)
for: 10m
labels:
severity: warning
- alert: CannotEvaluateConditionalUpdates
annotations:
summary: Cluster Version Operator cannot evaluate conditional update matches for {{ "{{ $value | humanizeDuration }}" }}.
description: Failure to evaluate conditional update matches means that Cluster Version Operator cannot decide whether an update path is recommended or not.
expr: |
max by (version, condition, status, reason)
(
(
time()-cluster_version_conditional_update_condition_seconds{condition="Recommended", status="Unknown"}
) >= 3600
)
labels:
severity: warning
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apiVersion: console.openshift.io/v1
kind: ConsolePlugin
metadata:
name: openshift-cluster-update-console-plugin
annotations:
kubernetes.io/description: The OpenShift cluster-update console plugin provides a web-console interface for managing ClusterVersion updates.
capability.openshift.io/name: Console
release.openshift.io/feature-set: TechPreviewNoUpgrade
exclude.release.openshift.io/internal-openshift-hosted: "true"
include.release.openshift.io/self-managed-high-availability: "true"
spec:
displayName: Cluster Updates
i18n:
loadType: Preload
backend:
type: Service
service:
name: openshift-cluster-update-console-plugin
namespace: openshift-cluster-update-console-plugin
port: 9001
basePath: /
5 changes: 3 additions & 2 deletions pkg/payload/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func LoadUpdate(dir, releaseImage, excludeIdentifier string, requiredFeatureSet
return nil, err
}

tasks := loadPayloadTasks(releaseDir, cvoDir, releaseImage, profile)
tasks := loadPayloadTasks(releaseDir, cvoDir, releaseImage, profile, payload.ImageRef)

var onlyKnownCaps *configv1.ClusterVersionCapabilitiesStatus

Expand Down Expand Up @@ -317,13 +317,14 @@ type payloadTasks struct {
skipFiles sets.Set[string]
}

func loadPayloadTasks(releaseDir, cvoDir, releaseImage, clusterProfile string) []payloadTasks {
func loadPayloadTasks(releaseDir, cvoDir, releaseImage, clusterProfile string, imageRef *imagev1.ImageStream) []payloadTasks {
cjf := filepath.Join(releaseDir, cincinnatiJSONFile)
irf := filepath.Join(releaseDir, imageReferencesFile)

mrc := manifestRenderConfig{
ReleaseImage: releaseImage,
ClusterProfile: clusterProfile,
Images: imagesFromImageRef(imageRef),
}

return []payloadTasks{{
Expand Down
23 changes: 23 additions & 0 deletions pkg/payload/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

"github.com/openshift/api/config"
configv1 "github.com/openshift/api/config/v1"
imagev1 "github.com/openshift/api/image/v1"
"github.com/openshift/library-go/pkg/manifest"
)

Expand All @@ -38,6 +39,13 @@ func Render(outputDir, releaseImage, clusterVersionManifestPath, featureGateMani
}
)

imageRef, err := loadImageReferences(releaseManifestsDir)
if err != nil {
klog.Warningf("Failed to load image references for manifest rendering: %v", err)
} else {
renderConfig.Images = imagesFromImageRef(imageRef)
}
Comment on lines +42 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if manifests in install/ use the .Images template variable

rg -n '{{.*\.Images' install/ --type yaml

Repository: openshift/cluster-version-operator

Length of output: 166


🏁 Script executed:

rg -n '\{\{[^}]*\.Images' install/ --type yaml

Repository: openshift/cluster-version-operator

Length of output: 213


Fail hard (or gate) image ref rendering when templates depend on .Images.

install/0000_50_cluster-update-console-plugin_50_deployment.yaml uses image: '{{index .Images "cluster-update-console-plugin"}}'. In pkg/payload/render.go, if loadImageReferences(...) fails the code only logs a warning and does not populate renderConfig.Images, so this template can render image: "", producing invalid rendered manifests. Rendering should return an error (or explicitly detect required .Images usage and fail) when image references can’t be loaded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/payload/render.go` around lines 42 - 47, The code currently silences
errors from loadImageReferences causing renderConfig.Images to be empty and
templates that reference .Images (e.g.,
install/0000_50_cluster-update-console-plugin_50_deployment.yaml) to render
invalid empty image fields; update the logic around loadImageReferences in
pkg/payload/render.go so that if loadImageReferences(releaseManifestsDir)
returns an error you propagate/return that error from the rendering path (or
explicitly gate and return an error when templates require .Images), rather than
only logging a warning — ensure renderConfig.Images is only left unset when that
is acceptable and otherwise call imagesFromImageRef(imageRef) and fail fast when
image refs cannot be loaded.


overrides, err := parseClusterVersionManifest(clusterVersionManifestPath)
if err != nil {
return fmt.Errorf("error parsing cluster version manifest: %w", err)
Expand Down Expand Up @@ -181,6 +189,21 @@ func renderDir(renderConfig manifestRenderConfig, idir, odir string, overrides [
type manifestRenderConfig struct {
ReleaseImage string
ClusterProfile string
Images map[string]string
}

// imagesFromImageRef builds a map from image short names to their resolved URIs.
func imagesFromImageRef(imageRef *imagev1.ImageStream) map[string]string {
images := make(map[string]string)
if imageRef == nil {
return images
}
for _, tag := range imageRef.Spec.Tags {
if tag.From != nil && tag.From.Kind == "DockerImage" {
images[tag.Name] = tag.From.Name
}
}
return images
}

// renderManifest Executes go text template from `manifestBytes` with `config`.
Expand Down
3 changes: 3 additions & 0 deletions pkg/payload/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ func Test_cvoManifests(t *testing.T) {
config := manifestRenderConfig{
ReleaseImage: "quay.io/cvo/release:latest",
ClusterProfile: "some-profile",
Images: map[string]string{
"cluster-update-console-plugin": "quay.io/openshift/cluster-update-console-plugin:latest",
},
}

tests := []struct {
Expand Down