Skip to content

fix(cnpg): detect a fully-down cluster instead of showing it as starting - #1614

Merged
hisco merged 3 commits into
mainfrom
eyal/rad-403-cnpg-all-down-detection
Sep 3, 2026
Merged

hisco merged 3 commits into
mainfrom
eyal/rad-403-cnpg-all-down-detection

Conversation

@hisco

@hisco hisco commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Problem

CNPG omits status.readyInstances when it is 0 (omitempty int), so a cluster with zero ready instances serializes identically to one whose status was never written — the field is simply absent in both. Radar's badge (getCNPGClusterStatus) and the Go issue detector (detectCNPGClusterIssues) both gated the all-down path on the field being present, so a fully-down cluster never reached it: it fell through to the transient-phase branch, showed amber "Starting Instances" forever, and raised no issue. A dead database, silent.

(The existing "all-down" tests hid this by fabricating readyInstances: 0, a shape CNPG never emits.)

Fix

Distinguish the cases the absent field collapses together, using status.currentPrimary (CNPG sets it on first primary election and never clears it — the version-robust "was up" signal, present on 1.27/1.28):

  • No status yet → Unknown (unchanged — absence is still not zero)
  • Reported, 0 ready, no primary elected → first bootstrap → amber, no alarm
  • Reported, 0 ready, primary present → was up and now down → red badge + Critical issue
  • Hibernated / fully-fenced (cnpg.io/hibernation, cnpg.io/fencedInstances: ["*"]) → intentionally 0 ready → neutral, no alarm

A 5-minute grace on the Ready=False transition absorbs a routine single-instance restart before alarming — the same on-read, stateless grace mechanism CNPG's existing timers already use (conditions.FindFalseConditionWithTime). It escalates immediately if there is no Ready condition to time from.

One shared availability read drives the badge, drawer banner, row count, and cell colour, and the Go detector mirrors it field-for-field, so all surfaces agree. Golden fixtures now use the real omitted-ready wire shape rather than a fabricated zero.

Tests

Rewrote the shared testdata/cnpg/badge-issue-matrix.json to realistic omitted-ready shapes and added: was-up fully down (→ red + Critical), first bootstrap (→ amber, no issue), slow restore past grace (→ amber), hibernated, all-fenced, plus the unchanged healthy / partial / status-never-written cases. The was-up-down / hibernated / fenced cases fail against the pre-fix code. Full k8s-ui suite and go test ./internal/issues/... pass.

Two known, non-blocking edges

  1. A 1-instance, was-up cluster whose only pod is restarting within the grace while its phase still lags at "healthy" can briefly show 0/1 next to a green "Healthy" badge. Narrow, self-heals in ≤5 min, not a false page or a silent outage.
  2. The cell colour branch has no dedicated unit test (brittle class-name assertion avoided); the underlying getCNPGClusterAvailability is thoroughly covered.

Ticket: RAD-403 (Velero/CNPG/Kyverno review).

https://claude.ai/code/session_01KxZ3xt2G4KpexrKSoXQ91S


Note

Medium Risk
Changes core CNPG health and alerting semantics across UI and the issue detector; incorrect parity could cause missed outages or false criticals, though behavior is heavily pinned by the shared golden matrix.

Overview
Fixes false “starting” and silent outages when CloudNativePG omits status.readyInstances at zero — the same wire shape as “status not written yet.”

Shared availability logic (getCNPGClusterAvailability in the UI, mirrored in detectCNPGClusterIssues) now treats omitted ready as 0 only after the operator has reported (phase, currentPrimary, or conditions), uses currentPrimary as “was up” to separate first bootstrap from regression, and applies carve-outs for hibernation and all-instance fencing plus a 5-minute Ready=False grace (kept in sync with cnpgDownGrace / CNPG_DOWN_GRACE_MS).

Surfaces aligned: list badge, instances cell (including red styling), cluster drawer “down” banner, and Go CNPGClusterDegraded Critical for total outage; partial shortfalls still need an explicit ready count above zero.

Tests: shared badge-issue-matrix.json uses realistic omitted-ready fixtures and optional metadata.annotations; Go golden tests and TS golden/unit tests extended for bootstrap, hibernated, fenced, grace, and was-up-down cases.

Reviewed by Cursor Bugbot for commit ef7b2ea. Bugbot is set up for automated code reviews on this repo. Configure here.

CNPG omits status.readyInstances when it is 0 (omitempty), so a cluster
with zero ready instances serializes identically to one whose status was
never written. The badge and the issue detector both gated the all-down
path on the field being present, so a fully-down cluster was unreachable
there: it fell through to the transient-phase branch, showed amber
Starting Instances forever, and raised no issue.

Distinguish the cases by whether the operator has reported and whether a
primary was ever elected (status.currentPrimary, which CNPG sets on first
election and never clears):
- no status yet -> Unknown (unchanged; absence is still not zero)
- reported, zero ready, no primary yet -> first bootstrap, amber, no alarm
- reported, zero ready, primary present -> was up and now down, red badge
  and a Critical issue
Hibernated and fully-fenced clusters are intentionally at zero ready, so
they read neutral. A short grace on the Ready=False transition absorbs a
routine single-instance restart before alarming.

One shared availability read drives the badge, the drawer banner, the row
count, and the cell colour, and the Go detector mirrors it, so the four
surfaces agree. The golden fixtures now use the real omitted-ready wire
shape rather than a fabricated zero.

Ticket: RAD-403.

Claude-Session: https://claude.ai/code/session_01KxZ3xt2G4KpexrKSoXQ91S
@hisco
hisco requested a review from nadaverell as a code owner September 3, 2026 09:13
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix detection of fully-down CNPG clusters with omitted ready counts

🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Detects previously healthy CNPG clusters whose omitted ready count represents total outage.
• Exempts bootstrap, hibernation, fencing, and recent restarts from false outage alarms.
• Aligns badges, drawer, counts, colors, and Critical issues through shared availability rules.
Diagram

graph TD
  R["CNPG Resource"] --> Z{"Zero ready?"}
  Z -- Yes --> P{"Primary elected?"}
  P -- Yes --> I{"Intentional stop?"}
  I -- No --> G{"Grace elapsed?"}
  G -- Yes --> O["Confirmed outage"] --> S["Red UI and issue"]
  Z -- No --> N["Non-outage state"]
  P -- No --> N
  I -- Yes --> N
  G -- No --> N
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Backend-computed availability
  • ➕ Provides one authoritative implementation instead of mirrored Go and TypeScript logic.
  • ➕ Eliminates the risk of grace periods or exception rules drifting between surfaces.
  • ➖ Requires API changes and makes immediate UI rendering dependent on backend enrichment.
  • ➖ Reduces the frontend's ability to render directly from Kubernetes resources.
  • ➖ Adds migration and compatibility overhead disproportionate to this focused fix.
2. Infer availability from pods
  • ➕ Uses direct workload state rather than interpreting omitted CNPG status fields.
  • ➕ Could provide more detailed per-instance outage diagnostics.
  • ➖ Requires additional pod queries, permissions, caching, and ownership correlation.
  • ➖ Introduces race conditions between pod and Cluster observations.
  • ➖ Couples cluster health presentation to lower-level resources and increases runtime cost.

Recommendation: Keep the PR's status-based approach. currentPrimary, intentional-stop annotations, and Ready transition time provide a low-cost, version-robust decision without extra API calls. Mirrored implementations are appropriate because both backend and frontend must evaluate raw resources independently; the shared golden matrix and synchronized grace constants mitigate parity risk.

Files changed (10) +530 / -65

Bug fix (4) +223 / -22
source_cnpg.goDetect omitted-ready CNPG outages in the issue detector +90/-14

Detect omitted-ready CNPG outages in the issue detector

• Resolves an omitted ready count to zero after CNPG has reported status, then uses currentPrimary to identify a previously serving cluster. Adds hibernation, all-fenced, and five-minute restart-grace exemptions before emitting a Critical CNPGClusterDegraded issue.

internal/issues/source_cnpg.go

CNPGClusterRenderer.tsxDrive drawer outage state from shared availability logic +6/-2

Drive drawer outage state from shared availability logic

• Replaces raw ready-count detection with the CNPG availability verdict. The drawer now recognizes omitted-ready outages while excluding hibernated, fenced, and grace-period clusters.

packages/k8s-ui/src/components/resources/renderers/CNPGClusterRenderer.tsx

cnpg-cells.tsxHighlight confirmed CNPG outages in instance cells +12/-1

Highlight confirmed CNPG outages in instance cells

• Uses the shared availability verdict to render fully-down instance counts in red. Partial shortfalls remain yellow and normal states retain the secondary text color.

packages/k8s-ui/src/components/resources/renderers/cnpg-cells.tsx

resource-utils-cnpg.tsCentralize frontend CNPG availability classification +115/-5

Centralize frontend CNPG availability classification

• Introduces a shared availability helper using reported status, currentPrimary, annotations, and Ready=False age. Badge and instance-count logic now correctly distinguish unknown bootstrap state, deliberate shutdown, and confirmed outage when readyInstances is omitted.

packages/k8s-ui/src/components/resources/resource-utils-cnpg.ts

Tests (6) +307 / -43
source_cnpg_golden_test.goPass CNPG annotations into backend golden cases +13/-5

Pass CNPG annotations into backend golden cases

• Extends golden-case decoding with optional metadata and copies fixture annotations into test resources. This enables shared coverage for hibernated and fenced clusters.

internal/issues/source_cnpg_golden_test.go

source_cnpg_test.goCover CNPG bootstrap, outage, fencing, and grace behavior +108/-9

Cover CNPG bootstrap, outage, fencing, and grace behavior

• Updates outage fixtures to include prior-primary evidence and realistic omitted ready counts. Adds tests distinguishing bootstrap and intentional shutdown from regressions, including recent and expired Ready=False grace periods.

internal/issues/source_cnpg_test.go

CNPGClusterRenderer.test.tsxVerify drawer outage messaging for omitted ready counts +27/-3

Verify drawer outage messaging for omitted ready counts

• Confirms the drawer reports Cluster Down for a previously serving cluster with omitted readyInstances. Also verifies a first-bootstrap cluster without a primary does not receive the outage banner.

packages/k8s-ui/src/components/resources/renderers/CNPGClusterRenderer.test.tsx

resource-utils-cnpg.golden.test.tsExtend frontend golden parity checks with metadata +13/-5

Extend frontend golden parity checks with metadata

• Passes fixture metadata into badge evaluation and updates instance-count assertions to treat currentPrimary as evidence that an omitted ready count means zero. This preserves badge and row-count consistency.

packages/k8s-ui/src/components/resources/resource-utils-cnpg.golden.test.ts

resource-utils-cnpg.test.tsTest the CNPG availability state matrix +70/-2

Test the CNPG availability state matrix

• Adds coverage for bootstrapping, genuine outages, restart grace, hibernation, full and partial fencing, malformed annotations, and neutral badge states. Existing zero-ready tests now include prior-primary evidence.

packages/k8s-ui/src/components/resources/resource-utils-cnpg.test.ts

badge-issue-matrix.jsonModel realistic CNPG omitted-ready wire states +76/-19

Model realistic CNPG omitted-ready wire states

• Reworks shared golden fixtures to include currentPrimary on established clusters and omit readyInstances for zero-ready states. Adds bootstrap, slow restore, hibernation, and all-fenced cases while preserving frontend/backend parity expectations.

testdata/cnpg/badge-issue-matrix.json

@qodo-code-review

qodo-code-review Bot commented Sep 3, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Count uses palette colors ✗ Dismissed 📘 Rule violation ⚙ Maintainability
Description
The modified instance-count cell uses the hardcoded Tailwind classes text-red-400 and
text-yellow-400 instead of approved theme text tokens. This bypasses theme-aware text coloring for
the rendered count.
Code

packages/k8s-ui/src/components/resources/renderers/cnpg-cells.tsx[79]

+            down ? 'text-red-400' : readyKnown && ready < desired ? 'text-yellow-400' : 'text-theme-text-secondary',
Relevance

●●● Strong

Theme-aware text styling is an established frontend convention; hardcoded palette colors bypass it.

PR-#319

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 3036659 requires modified frontend text to use theme text utilities and disallows non-theme
palette text classes. The changed conditional directly emits text-red-400 and text-yellow-400.

Rule 3036659: Use theme text color utility classes instead of hardcoded Tailwind gray classes
packages/k8s-ui/src/components/resources/renderers/cnpg-cells.tsx[76-80]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The CNPG instance-count cell selects hardcoded Tailwind palette text colors for outage and shortfall states.

## Issue Context
Use approved theme text tokens rather than `text-red-400` or `text-yellow-400`. If status emphasis requires additional semantics, use an existing theme-aware semantic utility rather than a raw palette class.

## Fix Focus Areas
- packages/k8s-ui/src/components/resources/renderers/cnpg-cells.tsx[76-80]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Comments reference implementation history ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
Added comments describe the old okR gate, the old readyInstances-presence gate, and the `old
presence gate`. These explicit references to prior implementation history violate the code-comment
policy.
Code

internal/issues/source_cnpg.go[R315-316]

+	// reachable at all — the old okR gate left it byte-identical to a statusless
+	// one. Absent EVERYTHING stays the truly-statusless case, still no signal.
Relevance

●●● Strong

Explicit historical implementation references violate the repository’s stated comment policy.

PR-#1446

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 3036538 prohibits added code comments that describe explicit change history. The cited comments
use old or describe how a former presence-based implementation made the outage path unreachable.

Rule 3036538: Disallow references to tickets or PR history in code comments
internal/issues/source_cnpg.go[312-316]
internal/issues/source_cnpg_test.go[541-542]
packages/k8s-ui/src/components/resources/renderers/CNPGClusterRenderer.test.tsx[43-46]
packages/k8s-ui/src/components/resources/resource-utils-cnpg.ts[184-188]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Several added comments refer explicitly to the previous implementation using phrases such as `old okR gate` and `old presence gate`.

## Issue Context
Comments should explain the current invariant—CNPG omits zero-valued `readyInstances`, while `currentPrimary` distinguishes a prior election—without describing PR or implementation history.

## Fix Focus Areas
- internal/issues/source_cnpg.go[312-316]
- internal/issues/source_cnpg_test.go[541-542]
- packages/k8s-ui/src/components/resources/renderers/CNPGClusterRenderer.test.tsx[43-46]
- packages/k8s-ui/src/components/resources/resource-utils-cnpg.ts[184-188]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 41 rules
✅ Web pages:
  +16 more
Review mode: ⚖️ Balanced

Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread internal/issues/source_cnpg.go Outdated
Comment thread packages/k8s-ui/src/components/resources/renderers/cnpg-cells.tsx

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit fcdcdbb. Configure here.

) {
return null
}
return 'down'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Wake-up treated as cluster outage

Medium Severity

Waking a hibernated cluster or lifting a full fence still matches the was-up outage path. currentPrimary is never cleared and Ready=False keeps its old lastTransitionTime, so the 5-minute grace is already elapsed. Both the badge and the detector then raise a Critical CNPGClusterDegraded for an operator-intended restart.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fcdcdbb. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed reachable, and verified against CNPG source: hibernation keeps currentPrimary and leaves Ready=False with a stale lastTransitionTime, and the hibernation marker (annotation and condition) is removed on wake — so a wake-up is indistinguishable from a genuine outage on the Cluster status alone. No CR-only signal separates them without either missing real node-loss outages or adding detector state the stateless badge cannot share. Decision: accept the transient window (it self-clears once the first instance is Ready) to keep every real outage covered; documented at the verdict in ef7b2ea. A Go-only observation-time debounce is the follow-up if on-call noise proves unacceptable.

… change history

The comments explaining CNPG's readyInstances-at-zero omission referenced the
prior implementation ("old okR gate", "old presence gate", "made unreachable").
Restate them as the current WHY: CNPG omits readyInstances (omitempty), so
absence on a cluster that reported anything else is a real 0, and currentPrimary
distinguishes a was-up regression from a first bootstrap.

Claude-Session: https://claude.ai/code/session_01KxZ3xt2G4KpexrKSoXQ91S
A cluster woken from hibernation or lifted from a full fence briefly presents
the same shape as an outage; CNPG has dropped the hibernation marker by then, so
it cannot be told apart from the Cluster status alone. Document the limitation
where the verdict is decided, on both the badge and the detector.

Claude-Session: https://claude.ai/code/session_01KxZ3xt2G4KpexrKSoXQ91S
@hisco
hisco merged commit f5b6683 into main Sep 3, 2026
9 checks passed
jfillman pushed a commit to jfillman/radar that referenced this pull request Sep 4, 2026
…ing (skyhook-io#1614)

## Problem

CNPG omits `status.readyInstances` when it is 0 (`omitempty` int), so a
cluster with **zero ready instances serializes identically to one whose
status was never written** — the field is simply absent in both. Radar's
badge (`getCNPGClusterStatus`) and the Go issue detector
(`detectCNPGClusterIssues`) both gated the all-down path on the field
being *present*, so a fully-down cluster never reached it: it fell
through to the transient-phase branch, showed amber "Starting Instances"
forever, and raised no issue. A dead database, silent.

(The existing "all-down" tests hid this by fabricating `readyInstances:
0`, a shape CNPG never emits.)

## Fix

Distinguish the cases the absent field collapses together, using
`status.currentPrimary` (CNPG sets it on first primary election and
never clears it — the version-robust "was up" signal, present on
1.27/1.28):

- **No status yet** → Unknown (unchanged — absence is still not zero)
- **Reported, 0 ready, no primary elected** → first bootstrap → amber,
no alarm
- **Reported, 0 ready, primary present** → was up and now down → red
badge + **Critical** issue
- **Hibernated / fully-fenced** (`cnpg.io/hibernation`,
`cnpg.io/fencedInstances: ["*"]`) → intentionally 0 ready → neutral, no
alarm

A **5-minute grace** on the `Ready=False` transition absorbs a routine
single-instance restart before alarming — the same on-read, stateless
grace mechanism CNPG's existing timers already use
(`conditions.FindFalseConditionWithTime`). It escalates immediately if
there is no `Ready` condition to time from.

One shared availability read drives the **badge, drawer banner, row
count, and cell colour**, and the **Go detector mirrors it
field-for-field**, so all surfaces agree. Golden fixtures now use the
real *omitted*-ready wire shape rather than a fabricated zero.

## Tests

Rewrote the shared `testdata/cnpg/badge-issue-matrix.json` to realistic
omitted-ready shapes and added: was-up fully down (→ red + Critical),
first bootstrap (→ amber, no issue), slow restore past grace (→ amber),
hibernated, all-fenced, plus the unchanged healthy / partial /
status-never-written cases. The was-up-down / hibernated / fenced cases
fail against the pre-fix code. Full k8s-ui suite and `go test
./internal/issues/...` pass.

## Two known, non-blocking edges

1. A 1-instance, was-up cluster whose only pod is restarting *within*
the grace while its phase still lags at "healthy" can briefly show `0/1`
next to a green "Healthy" badge. Narrow, self-heals in ≤5 min, not a
false page or a silent outage.
2. The cell colour branch has no dedicated unit test (brittle class-name
assertion avoided); the underlying `getCNPGClusterAvailability` is
thoroughly covered.

Ticket: RAD-403 (Velero/CNPG/Kyverno review).

https://claude.ai/code/session_01KxZ3xt2G4KpexrKSoXQ91S

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes core CNPG health and alerting semantics across UI and the
issue detector; incorrect parity could cause missed outages or false
criticals, though behavior is heavily pinned by the shared golden
matrix.
> 
> **Overview**
> Fixes **false “starting” and silent outages** when CloudNativePG omits
`status.readyInstances` at zero — the same wire shape as “status not
written yet.”
> 
> **Shared availability logic** (`getCNPGClusterAvailability` in the UI,
mirrored in `detectCNPGClusterIssues`) now treats omitted ready as **0
only after the operator has reported** (phase, `currentPrimary`, or
conditions), uses **`currentPrimary` as “was up”** to separate first
bootstrap from regression, and applies carve-outs for **hibernation**
and **all-instance fencing** plus a **5-minute `Ready=False` grace**
(kept in sync with `cnpgDownGrace` / `CNPG_DOWN_GRACE_MS`).
> 
> **Surfaces aligned:** list badge, instances cell (including red
styling), cluster drawer “down” banner, and Go **`CNPGClusterDegraded`
Critical** for total outage; partial shortfalls still need an explicit
ready count above zero.
> 
> **Tests:** shared `badge-issue-matrix.json` uses realistic
omitted-ready fixtures and optional `metadata.annotations`; Go golden
tests and TS golden/unit tests extended for bootstrap, hibernated,
fenced, grace, and was-up-down cases.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
ef7b2ea. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant