Skip to content

CloudNativePG: incident workflows, guarded operations and guided recovery - #1922

Merged
nadaverell merged 12 commits into
mainfrom
feature/cnpg-actions-runtime
Oct 15, 2026
Merged

nadaverell merged 12 commits into
mainfrom
feature/cnpg-actions-runtime

Conversation

@nadaverell

@nadaverell nadaverell commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Builds on the CloudNativePG fleet workspace merged in #1921. Operators can move from an affected PostgreSQL Cluster to deciding evidence, review a bounded intervention, and follow what the controller or database actually observed.

User flow

CNPG stays under Resources. Fleet views cover Clusters, Backups, Declarations, Pooling and Operator. The exact CNPG Cluster kind list uses the fleet presentation without changing its existing URL or drawer navigation; Cluster API’s colliding kind remains a generic table. Sidebar destination badges count affected clusters; outside the CloudNativePG views the Clusters badge reads the cheap workspace data only, so it is a lower bound (disk use and replication lag are counted on the views).

Cluster tabs are organized by task: Overview, Replication, Storage, Performance, Backups, Activity, Logs and Configuration/YAML. Serving availability, controller phase, operator health, backup success and restore validation remain separate facts. Unknown, denied and partial evidence never becomes zero, healthy or an empty inventory. A single instance or a Cluster with no backup destination is posture, stated without a problem mark on every surface; a replica cluster's replication never reads healthy without evidence about its upstream feed.

  • Create or restore: common target fields lead to exact YAML and server dry-run in strict Create mode. Restore starts with recovery-source evidence and preserves advanced edited YAML when going Back. Named Backups bind the recorded backup ID and PostgreSQL major. Recovery manifests exclude backup declarations so the new Cluster cannot write into its recovery archive. Technical checks, application validation and a human validation note remain distinct. A VolumeSnapshot Backup restores to the end of its snapshot only (a later point needs a WAL archive source the dialog does not configure), and recovery evidence counts only what the chosen source holds.
  • Set up backups: attach an existing same-namespace Barman ObjectStore under a reviewed archive identity, create a matching ScheduledBackup or narrowly repair its method, then show observed archiving and successful backup evidence. Each write has its own review. Unread inventory and remote ownership limits remain explicit.
  • Intervene: reviewed backup, switchover, restart/reload, fencing, hibernate/rehydrate, maintenance, backend cancel/terminate and fenced-standby replacement. ScheduledBackup and Pooler actions retain their guards. Connect supplies endpoint evidence, port-forward templates and psql without decoding credentials.
  • Follow the result: requested, progressing, completed, failed, stalled, superseded and unobservable states use current object identity and bounded observation. A just-submitted operation reads requested until the first read after the request. Fleet state is last observed in this browser tab (scoped to the API base, so Radar Hub clusters stay apart), not shared durable history. Reports include coverage/privacy limits and redact credential values.

Architecture and dependency boundary

This branch is based on current main and does not require #2042 or the alternative foundation #2124. Dependency capture and shared UI extractions from #2103/#2104 are already on main.

internal/cnpg/read_scope.go owns the small CNPG collector assessment, using existing cache readiness and namespace APIs. It performs no authorization, API calls or waiting. The HTTP adapter supplies the caller-authorized target; CNPG preserves useful partial rows and distinguishes denied, uncached, syncing and failed reads. Instance and Pooler inventories require all typed sources before reporting absence. A missing cache cannot establish completeness; configured exclusions remain distinguishable before sync.

There is no new public cache assessment API, generic coverage wire schema, MCP response change or Findings change in this PR. CNPG keeps its existing coverage/status response structure, including the additive uncached state and optional uncachedNamespaces. Newer UI continues to accept older Radar CNPG responses; new endpoints remain capability-gated. Moving to a shared assessment later can replace this local implementation without changing the CNPG callers or response contract.

The shared helpers this branch uses (internal/readscope namespace selection and scoped Pod listing, internal/imageutil, pkg/urlutil origin normalization, topology.ManagedByFromMeta) landed separately in #2143. This PR does not implement a competing collector assessment. Capacity/Vitals retain current-main collector behavior.

internal/cnpg owns runtime/storage/HA/history/inspection/report orchestration, preflight and conditional actions. internal/server adapts HTTP and caller identity. internal/integration carries domain coverage, bounded fan-out and reviewed-action errors; pure CNPG interpretation lives in the shared pkg module. Hosts own fetching, navigation and workflow state. The workspace shares one dynamic-kind wait budget; other read callbacks retain their bounded budgets.

Correctness boundaries

Writes bind reviewed context, object UID and relevant facts; resourceVersion conflicts refuse stale changes. Rolling restart, Pooler pause/resume and ScheduledBackup suspend/resume also bind the reviewed generation. Restart-instance, fencing and destroy refuse the target of an in-flight switchover or failover, restart-instance and fencing refuse a Pod labelled primary while the Cluster status names another (fencing a named instance whose Pod cannot be read is refused too, since its role cannot be checked), and unfence refuses while a cold VolumeSnapshot backup cannot be ruled out. A 503 without a code is an uncertain outcome (a proxy may have forwarded the write); Radar's own refusals before any write carry unavailable and stay retryable. Caller clients retain Kubernetes impersonation. Clients, cache and cancellation come from one captured connection state. Failed client construction cannot fall back to the server identity.

In-flight and ambiguous results are retained outside the query cache, bound to their original API target/context, and block duplicate operational writes until inspected. Backup/recovery evidence binds Cluster incarnations, recorded archive/server identities and source declarations. Fenced-standby replacement binds Pod/PVC/Job identities and reports completed steps on partial failure. Metrics preserve stale/ambiguous evidence separately from query failure. SQL inspection is fixed, bounded and read-only; backend signals bind PID and start time. Report bundles withhold query text (including pgaudit records and deadlock DETAIL) unless opted in, and always withhold initialization and import SQL and serialized pod patches.

Validation

  • At 19d476a: make tsc; all Go packages (go test ./... and the pkg module); k8s-ui (4,988 tests) and web (vitest run src, 2,718 tests). After merging main (Extract shared read-scope, image-tag, origin and managed-by helpers #2143's helper extraction): make tsc, and the Go tests for internal/cnpg, internal/issues, internal/server, internal/mcp, internal/readscope, internal/integration and the touched pkg packages; CI covers the rest.
  • Live, against an EKS test cluster with CloudNativePG 1.30.1 (healthy two-instance, single-instance with failing scheduled backups, and a Cluster stuck unschedulable): every fleet view, the CNPG Cluster kind list and the untouched Cluster API list, each Cluster tab, Create through server dry-run, the Set up backups guide, the report bundle (no credentials in the archive), the context guard, and two switchover round trips followed through the tracker to completion.
  • Resources pages outside the CloudNativePG views poll only /api/cnpg/workspace (no Prometheus-backed fleet reads).

Small fixtures do not establish large-fleet performance.

Release and consumer notes

  • Radar Hub alerting. New issue reasons reach Hub's alert rules and CEL filters: CNPGCertificateExpiring, CNPGCertificateExpired, CNPGScheduledRunNoBackup, CNPGScheduleDestinationMissing, CNPGInstanceReadinessMismatch, CNPGPrimaryLabelMismatch. CloudNativePG certificate expiry files under the existing certificate_not_ready category, and a scheduled run that produced no Backup files under backup_failed. Nothing is renamed or removed.
  • Hub against older Radars. The workspace's routes and actions are gated by the cnpgWorkspace and cnpgProtectionSetup flags, so a cluster on an earlier Radar shows the workspace as "needs a newer Radar".
  • k8s-ui. cnpgGitOpsSource and CNPGGitOpsSource are removed (the workspace now carries managedBy refs); no known consumer imports them. components/cnpg exports more.
  • Publish order. Publish k8s-ui from main after this merges, and make radar-app's @skyhook-io/k8s-ui peer floor name that version. Today's floor, >=1.17.1, is unpublished (npm has 1.17.0) and must not be satisfied by a k8s-ui built without this branch.
  • Persisted format. The restore-validation note (radar.skyhook.io/restore-validation on a restored Cluster) is format version: 1, and readers skip formats they do not know. The detached-PVC provenance annotation keeps its scalar UID meaning.

Limits and remaining review

Known and deliberately left for follow-up:

  • Backup follow-through does not yet bind the created Backup's UID (a same-name replacement reads as the same run), and operator Events infer ReplicaSet ownership from a name prefix.
  • A write accepted after its dialog unmounted (browser Back, connection recovery) gets no toast or tracker entry, and the result lock is cleared on success; the blocking-sessions dialog closes when its row leaves the 10 s poll.
  • CNPG issues on shared surfaces (/api/issues, Diagnose) have no persistence window, so a switchover can briefly raise a readiness mismatch.
  • Process-wide memos are bounded but not pruned on time (localCanIMemo, the runtime memo); one exec semaphore is shared by all users.
  • CloudNativePG code is in the main bundle, as every integration is today.
  • Newer UI against an older Radar: a few screens read "Readingâ€Ķ" or "not recorded" instead of naming the older Radar.
  • Cluster capabilities list the namespace's Backups and the hibernation inventories on every refresh, and the tracker refreshes them every 5 s while following an operation: a cost at large Backup counts.
  • Disk and lag readings kept after failed refreshes are not marked stale; the fleet's Needs attention count is exact even when those readings are unavailable (no Prometheus).
  • Action dialogs bind the facts as of their last refresh, not as of opening; an in-place restart's completion compares against the browser's clock at the response.
  • Unfence on a Cluster that declares volume snapshots has no override when the caller cannot list Backups.
  • Fleet disk and replication rows are joined to Clusters by namespace and name, not UID; a same-name replacement can show its predecessor's last reading until the next refresh. A UID can be added to those rows later without breaking older servers.
  • Coverage's uncachedNamespaces does not distinguish namespaces still syncing from ones the cache cannot read.
  • Some rules exist in Go and TypeScript (backup-to-Cluster matching, fence parsing); only backup declarations share test vectors.
  • The client treats any action error code as a refusal before any write. The integration guide states the rule; the client does not enforce it.

ObjectStore and Cluster declarations cannot be atomically locked together; visible declarations do not prove remote archive ownership or credential validity. Cloud installations need the updated chart read grants; changing only the image does not update roles. Store/credential provisioning, plugin installation, retention migration, arbitrary SQL and durable shared operation history remain outside scope.

@nadaverell
nadaverell requested a review from hisco as a code owner September 29, 2026 09:39
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add CloudNativePG actions, live runtime, and a shared GitOps write guard

âœĻ Enhancement 🐞 Bug fix 🧊 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

â€Ē Add CloudNativePG operations that refuse writes when reviewed cluster facts have changed.
â€Ē Show live PostgreSQL and PgBouncer state through the caller’s Kubernetes permissions.
â€Ē Share field-aware GitOps revert warnings across CloudNativePG, Set image, and investigation apply.
Diagram

graph TD
  UI["Web UI"] --> API["CNPG API"] --> Actions["Action handlers"] --> K8s["Kubernetes API"]
  API --> Runtime["Runtime readers"] --> Pods["CNPG Pod endpoints"]
  UI --> Guard["Shared write guard"] --> Evidence["Evidence endpoint"] --> K8s
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Read runtime through pods/exec
  • ➕ Could support richer SQL queries, including per-session detail.
  • ➖ Requires a more invasive grant and could expose query contents; loses the fixed-path proxy boundary.
2. Keep write warnings within each dialog
  • ➕ Smaller immediate change to existing Set image and investigation flows.
  • ➖ Duplicates policy interpretation and risks inconsistent revert warnings.

Recommendation: Keep the shared evidence-backed guard and bounded pods/proxy approach: they reuse caller authorization without requiring exec or duplicating GitOps policy logic. Review proxy safety, fact binding, and fail-closed confirmation closely; successful Backup creation and switchover were not verified live.

Files changed (44) +9994 / -427

Enhancement (27) +7086 / -19
cnpg_actions.goImplement guarded CNPG cluster and schedule actions +2045/-0

Implement guarded CNPG cluster and schedule actions

â€Ē Adds capabilities and impersonated handlers for backups, switchover, restarts, reload, fencing, hibernation, and schedules. Rechecks reviewed facts and permissions, uses optimistic locking or Pod UID preconditions, and reports stable conflict and uncertain-outcome codes.

internal/server/cnpg_actions.go

cnpg_runtime.goRead bounded CNPG and PgBouncer runtime data +1472/-0

Read bounded CNPG and PgBouncer runtime data

â€Ē Adds impersonated pods/proxy readers for fixed instance-manager and exporter paths. Validates Pod ownership, bounds and memoizes reads, and preserves denial and missing-data states.

internal/server/cnpg_runtime.go

gitops_write_evidence.goDerive field-level GitOps write evidence +787/-0

Derive field-level GitOps write evidence

â€Ē Checks requested paths against last-applied data, managed fields, ignore rules, and Argo, Flux, or Helm policies as the caller. Returns derived evidence rather than raw ownership metadata.

internal/server/gitops_write_evidence.go

server.goRegister CNPG and write-evidence routes +7/-0

Register CNPG and write-evidence routes

â€Ē Wires runtime, capability, action, and GitOps evidence endpoints into the API router.

internal/server/server.go

ActionConfirmDialog.tsxAdd reusable effect-first confirmation +227/-0

Add reusable effect-first confirmation

â€Ē Introduces context, warnings, expandable API writes, optional typed confirmation, guard gating, inline errors, and unknown-outcome locking.

packages/k8s-ui/src/components/shared/ActionConfirmDialog.tsx

CreateResourceDialog.tsxAllow strict create as the initial mode +5/-2

Allow strict create as the initial mode

â€Ē Adds an initial mode prop so prefilled restore manifests need not default to applying over existing objects.

packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx

GitOpsWriteWarning.tsxRender shared GitOps write-risk warnings +131/-0

Render shared GitOps write-risk warnings

â€Ē Displays revert reasons, owner navigation, pending and informational states, and acknowledgement controls when needed.

packages/k8s-ui/src/components/shared/GitOpsWriteWarning.tsx

index.tsExport new shared confirmation components +3/-1

Export new shared confirmation components

â€Ē Exports action confirmation, GitOps warning, and the Set image ownership contract.

packages/k8s-ui/src/components/shared/index.ts

drawer-components.tsxAllow alert banners to control spacing +4/-2

Allow alert banners to control spacing

â€Ē Adds an optional class name while preserving default spacing so warnings fit within dialogs.

packages/k8s-ui/src/components/ui/drawer-components.tsx

WorkloadView.tsxExpose domain actions in workload headers +5/-0

Expose domain actions in workload headers

â€Ē Adds an optional header-action renderer for drawer and expanded views.

packages/k8s-ui/src/components/workload/WorkloadView.tsx

gitops-write-guard.tsClassify per-write GitOps revert risk +445/-0

Classify per-write GitOps revert risk

â€Ē Adds a pure evaluator yielding none, info, may-revert, or will-revert assessments. Treats missing evidence conservatively and exposes a common acknowledgement gate.

packages/k8s-ui/src/utils/gitops-write-guard.ts

index.tsExport write-guard utilities +1/-0

Export write-guard utilities

â€Ē Exposes the evaluator and types through the k8s-ui utilities entry point.

packages/k8s-ui/src/utils/index.ts

client.tsAdd write-evidence API client +19/-0

Add write-evidence API client

â€Ē Posts resource, owner, and written paths to the server and types its evidence response.

web/src/api/client.ts

cnpg.tsType and fetch CNPG actions and runtime +273/-2

Type and fetch CNPG actions and runtime

â€Ē Adds capability and runtime contracts, polling hooks, action mutations, error-code extraction, and cache invalidation.

web/src/api/cnpg.ts

CNPGClusterRuntime.tsxShow live PostgreSQL runtime +472/-0

Show live PostgreSQL runtime

â€Ē Adds replication, session, transaction, storage, WAL, slot, and page-local trend views with source availability and instance actions.

web/src/components/cnpg/CNPGClusterRuntime.tsx

CNPGDetailPage.tsxAdd a Runtime tab to cluster details +16/-2

Add a Runtime tab to cluster details

â€Ē Mounts live runtime on Cluster pages and connects Pod-log navigation to the logs tab.

web/src/components/cnpg/CNPGDetailPage.tsx

CNPGPooling.tsxDisplay measured PgBouncer pressure +50/-7

Display measured PgBouncer pressure

â€Ē Replaces the placeholder with exporter-backed waiting clients, active servers, maximum wait, and reporting coverage; marks incomplete totals as lower bounds.

web/src/components/cnpg/CNPGPooling.tsx

CNPGSummaryHost.tsxUse live replication in the cluster summary +27/-1

Use live replication in the cluster summary

â€Ē Shows primary-reported streaming and replay lag when available, retaining the Kubernetes-only fallback otherwise.

web/src/components/cnpg/CNPGSummaryHost.tsx

CNPGClusterActions.tsxProvide cluster operations and confirmations +525/-0

Provide cluster operations and confirmations

â€Ē Adds capability-gated backup, switchover, restart, reload, fencing, and hibernation flows with effects, write plans, GitOps checks, and error handling.

web/src/components/cnpg/actions/CNPGClusterActions.tsx

CNPGInstanceActions.tsxAdd per-instance operations +48/-0

Add per-instance operations

â€Ē Exposes eligible promote, restart, fence, and lift-fence controls using per-instance capability reasons.

web/src/components/cnpg/actions/CNPGInstanceActions.tsx

CNPGRestoreDialog.tsxReview restore sources before creation +86/-0

Review restore sources before creation

â€Ē Selects a Backup or object-store source and optional point-in-time target, then opens the standard create flow with a recovery manifest.

web/src/components/cnpg/actions/CNPGRestoreDialog.tsx

CNPGScheduleActions.tsxAdd ScheduledBackup controls +99/-0

Add ScheduledBackup controls

â€Ē Provides capability-gated run-now, suspend, and resume dialogs with catch-up information and GitOps checks.

web/src/components/cnpg/actions/CNPGScheduleActions.tsx

actionModel.tsModel standby choice and recovery manifests +95/-0

Model standby choice and recovery manifests

â€Ē Supplies backup naming, method descriptions, standby ranking, source selection, and recovery manifest construction.

web/src/components/cnpg/actions/actionModel.ts

renderCNPGHeaderActions.tsxRoute CNPG header actions by kind +12/-0

Route CNPG header actions by kind

â€Ē Renders cluster or ScheduledBackup controls only for CloudNativePG resources.

web/src/components/cnpg/actions/renderCNPGHeaderActions.tsx

useCNPGWriteGuard.tsxAdapt shared guard to CNPG actions +58/-0

Adapt shared guard to CNPG actions

â€Ē Maps CNPG write effects to the common warning and confirmation contract.

web/src/components/cnpg/actions/useCNPGWriteGuard.tsx

InvestigationView.tsxResolve write risk for proposed fixes +35/-2

Resolve write risk for proposed fixes

â€Ē Requests a conservative guard for unspecified spec changes when Apply opens and passes owner navigation to the dialog.

web/src/components/diagnose/InvestigationView.tsx

useGitOpsWriteGuard.tsFetch and evaluate GitOps evidence on demand +139/-0

Fetch and evaluate GitOps evidence on demand

â€Ē Resolves ownership, requests field evidence for managed targets, and evaluates intended writes against the shared guard.

web/src/hooks/useGitOpsWriteGuard.ts

Refactor (5) +395 / -391
ResourceActionsBar.tsxPass evaluated ownership to Set image +4/-4

Pass evaluated ownership to Set image

â€Ē Replaces bespoke managed-image sources with the shared guard assessment passed to the image dialog.

packages/k8s-ui/src/components/shared/ResourceActionsBar.tsx

SetImageDialog.tsxReplace bespoke image warning with shared guard +43/-82

Replace bespoke image warning with shared guard

â€Ē Defines image-field write paths and uses common warning and confirmation logic instead of dialog-specific GitOps copy.

packages/k8s-ui/src/components/shared/SetImageDialog.tsx

ApplyDialog.tsxUse shared warning for investigation apply +19/-26

Use shared warning for investigation apply

â€Ē Replaces the managed-by warning with a guard assessment, owner link, and common confirmation gate.

web/src/components/diagnose/ApplyDialog.tsx

WorkloadView.tsxWire CNPG actions and reuse ownership resolution +87/-279

Wire CNPG actions and reuse ownership resolution

â€Ē Connects CNPG header actions, extracts ownership handling into a shared hook, and supplies the evaluated guard to Set image.

web/src/components/workload/WorkloadView.tsx

useResolvedGitOpsOwner.tsExtract reusable owner resolution +242/-0

Extract reusable owner resolution

â€Ē Moves direct and inherited GitOps or Helm ownership lookup, Argo namespace resolution, owner fetching, and source descriptions out of WorkloadView.

web/src/hooks/useResolvedGitOpsOwner.ts

Tests (9) +2428 / -12
cnpg_actions_test.goTest CNPG action guards and write shapes +898/-0

Test CNPG action guards and write shapes

â€Ē Uses fake Kubernetes clients to verify capabilities, conflicts, fencing and plugin cases, and action writes and errors.

internal/server/cnpg_actions_test.go

cnpg_runtime_test.goTest runtime parsing and proxy boundaries +693/-0

Test runtime parsing and proxy boundaries

â€Ē Exercises status and exporter fixtures, pooler metrics, access failures, Pod validation, and request limits.

internal/server/cnpg_runtime_test.go

gitops_write_evidence_test.goTest write-evidence classification +339/-0

Test write-evidence classification

â€Ē Covers field paths, last-applied and managed-field evidence, owner policies, and ignore rules.

internal/server/gitops_write_evidence_test.go

GitOpsWriteWarning.test.tsxTest shared warning presentation +70/-0

Test shared warning presentation

â€Ē Verifies revert copy, owner links, acknowledgement, pending checks, informational writes, and unmanaged rendering.

packages/k8s-ui/src/components/shared/GitOpsWriteWarning.test.tsx

SetImageDialog.test.tsTest guard gating for image changes +23/-6

Test guard gating for image changes

â€Ē Updates submit tests for managed acknowledgement, unresolved ownership, and existing draft and loading gates.

packages/k8s-ui/src/components/shared/SetImageDialog.test.ts

gitops-write-guard.test.tsTest GitOps risk decisions +295/-0

Test GitOps risk decisions

â€Ē Covers status and child writes, evidence, sync policies, exemptions, uncertainty, and acknowledgement gates.

packages/k8s-ui/src/utils/gitops-write-guard.test.ts

actionModel.test.tsTest action selection and restore manifests +56/-0

Test action selection and restore manifests

â€Ē Checks backup naming, standby preference, recovery sources, and manifests that do not inherit source archiving settings.

web/src/components/cnpg/actions/actionModel.test.ts

ApplyDialog.test.tsxTest investigation apply ownership gates +52/-5

Test investigation apply ownership gates

â€Ē Checks warning and acknowledgement behavior, unmanaged confirmation, and blocking during ownership resolution.

web/src/components/diagnose/ApplyDialog.test.tsx

WorkloadView.test.tsFollow extracted owner-lookup helper +2/-1

Follow extracted owner-lookup helper

â€Ē Updates ownership tests to import the helper from its reusable hook module.

web/src/components/workload/WorkloadView.test.ts

Documentation (3) +85 / -5
CLAUDE.mdDocument new CNPG and GitOps endpoints +4/-1

Document new CNPG and GitOps endpoints

â€Ē Lists runtime, action, and write-evidence routes, authorization boundaries, and the expanded CNPG demo mode.

CLAUDE.md

cnpg.mdDescribe live runtime, operations, and write warnings +34/-4

Describe live runtime, operations, and write warnings

â€Ē Documents runtime availability and permissions, action write shapes, fact-bound confirmation, restore, and the GitOps guard.

docs/cnpg.md

CNPG_WORKSPACE.mdRecord phase-two CNPG design +47/-0

Record phase-two CNPG design

â€Ē Adds the actions, runtime, fixtures, and shared-guard plan, including revisions on fact binding, fencing, and proxy safety.

docs/plans/CNPG_WORKSPACE.md

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (9) 📘 Rule violations (8) 📎 Requirement gaps (0) ðŸŽĻ UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Restricted users can download cluster reports 🐞 Bug â›Ļ Security
Description
handleCNPGClusterReport fetches the Cluster directly without applying authorizeCNPGCachedRead or
its namespace-access check. A user with Kubernetes permission to get a Cluster but no pod or
deployment list permission can fail Radar’s namespace discovery while still downloading that
Cluster’s report.
Code

internal/server/cnpg_report.go[68]

+	cluster, err := dyn.Resource(cnpgsvc.ClusterGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
Evidence
The report makes a request-scoped direct Get and writes the Cluster manifest, whereas the standard
CNPG read gate checks both discovered namespace access and Cluster-read permission. Namespace
discovery is based on pod or deployment list permissions, so it can reject a user whose separate
Cluster get permission permits the direct fetch.

internal/server/cnpg_report.go[50-79]
internal/server/cnpg_reads.go[15-24]
pkg/auth/permissions.go[226-275]
internal/cnpg/report.go[158-164]

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 report handler bypasses Radar’s namespace and Cluster-read authorization before its direct Kubernetes fetch.
## Fix Focus Areas
- internal/server/cnpg_report.go[60-72]
- internal/server/cnpg_reads.go[15-24]
## Recommended Fix
Apply the same namespace and Cluster-read authorization used by other CNPG Cluster reads before fetching or building the report; return a forbidden response when it fails.

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


2. Archive attachment leaves backups disabled 🐞 Bug ≡ Correctness
Description
archivingPlugin changes the existing plugin entry in its copied slice before prepareArchiving
reads the original plugins to calculate Unchanged. When a Barman plugin already exists with
enabled or isWALArchiver set to false, preview reports no change and confirmation returns
without enabling archiving.
Code

internal/cnpg/protection.go[R362-363]

+	old, _, _ := unstructured.NestedSlice(cluster.Object, "spec", "plugins")
+	out = ArchivingPreview{UID: string(cluster.GetUID()), Facts: ArchivingFacts{ClusterConfig: clusterDigest, ObjectStoreUID: string(store.GetUID()), ObjectStoreConfig: storeDigest, ObjectStore: p.ObjectStore, ServerName: p.ServerName}, Destination: chosen.path, Endpoint: chosen.endpoint, Plugin: plugin, Warnings: warnings, Unchanged: reflect.DeepEqual(old, plugins)}
Evidence
NestedSlice supplies the slice that archivingPlugin modifies, while prepareArchiving retrieves
the original configuration only afterward. Its equality check therefore compares the proposed slice
to a separately retrieved original; however, NestedSlice returns a deep copy, so this evidence
does not prove the proposed no-op. This finding should not be included.

internal/cnpg/protection.go[75-118]
internal/cnpg/protection.go[336-364]
internal/cnpg/protection.go[402-410]

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

## Issue description
Archive attachment treats changes to an existing disabled Barman plugin as unchanged and skips the write.
## Fix Focus Areas
- internal/cnpg/protection.go[75-118]
- internal/cnpg/protection.go[336-364]
## Recommended Fix
Capture the original plugin configuration before calling `archivingPlugin`, then compare that original against the proposed plugins. Add a regression test for enabling an existing disabled plugin.

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


3. Support bundles expose command passwords ✓ Resolved
Description
cnpgReportCleanPodSpec redacts environment values but leaves container command and args
unchanged. When a reported Pod or Job contains a credential-bearing connection URL in an argument,
the bundle writes that argument into its manifest.
Code

internal/cnpg/report.go[R663-665]

+func cnpgReportCleanPodSpec(spec *corev1.PodSpec) {
+	cnpgReportCleanContainers(spec.InitContainers)
+	cnpgReportCleanContainers(spec.Containers)
Evidence
The cleaner touches only Env[].Value, while the report serializes the cleaned Pod and Job specs
without a later redaction pass.

internal/cnpg/report.go[655-665]
internal/cnpg/report.go[175-220]

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

## Issue description
Pod and Job manifests in the support bundle retain credential-bearing command arguments.
## Fix Focus Areas
- internal/cnpg/report.go[655-665]
- internal/cnpg/report.go[175-220]
## Recommended Fix
Apply inline-secret redaction to container command and argument strings before serialization, including ephemeral containers, and test a credential-bearing connection URL in a Pod and Job.

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



Remediation recommended

4. Reused backup names falsely finish actions 🐞 Bug ≡ Correctness
Description
backupObserver finds a Backup by namespace and name alone, then treats its completed phase as
completion of the tracked operation. If the original Backup is deleted and its name is reused for
another Cluster, the replacement can complete the old backup or scheduled-run follow-through despite
the source Cluster identity check.
Code

web/src/components/cnpg/operations/model.ts[426]

+  const b = obs.backups.find((x) => x?.metadata?.name === name && x?.metadata?.namespace === op.namespace)
Evidence
The backend returns only the Backup name; tracking stores only that name, and the observer accepts
any namespace-local Backup with it before checking its phase. The existing Cluster UID check
distinguishes replacement Clusters, not replacement Backups.

internal/cnpg/actions.go[1437-1453]
web/src/components/cnpg/actions/CNPGClusterActions.tsx[289-292]
web/src/components/cnpg/operations/model.ts[301-314]
web/src/components/cnpg/operations/model.ts[421-447]

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

## Issue description
Backup follow-through can attribute a replacement, same-named Backup to an earlier operation.
## Fix Focus Areas
- internal/cnpg/actions.go[1437-1453]
- web/src/components/cnpg/actions/CNPGClusterActions.tsx[289-292]
- web/src/components/cnpg/operations/model.ts[421-435]
## Recommended Fix
Return and retain the created Backup UID, including after a resolved timeout, and require that UID when selecting the observed Backup. Apply the same identity binding to manual scheduled-run tracking.

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


5. Unrelated rollout events cloud diagnosis 🐞 Bug ≡ Correctness
Description
operatorEvents admits a ReplicaSet event whenever its object name starts with the selected
Deployment name, without establishing that the ReplicaSet belongs to that Deployment. An unrelated
ReplicaSet with that prefix contributes its messages to the operator diagnosis and can displace
relevant events from the bounded result.
Code

internal/cnpg/operator_diagnosis.go[R605-607]

+	for _, e := range events {
+		if e.InvolvedObject.Kind == "ReplicaSet" && strings.HasPrefix(e.InvolvedObject.Name, d.Name+"-") {
+			subjects[e.InvolvedObject.UID] = true
Evidence
The reader lists namespace Events, adds every prefix-matching ReplicaSet event UID to its subject
set, and then returns events matching that set, capped by a limit. The current-Pod owner-reference
join immediately above shows a UID-backed association is available for current ReplicaSets, but the
prefix branch does not use one.

internal/cnpg/operator_diagnosis.go[583-613]
internal/cnpg/recovery.go[429-449]

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

## Issue description
Operator diagnosis includes events from unrelated ReplicaSets whose names share the operator Deployment's prefix.
## Fix Focus Areas
- internal/cnpg/operator_diagnosis.go[583-613]
- internal/cnpg/recovery.go[429-449]
## Recommended Fix
Resolve ReplicaSets owned by the selected Deployment UID and admit their event UIDs, including historical rollout objects that remain available. Do not infer ownership solely from an Event object's name prefix.

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


6. Invalid action input returns status 422 📘 Rule violation ≡ Correctness
Description
writeActionError maps apierrors.IsInvalid(err) to http.StatusUnprocessableEntity rather than
HTTP 400. When Kubernetes rejects an action write as invalid, that branch returns a different status
from the required client-input response.
Code

internal/server/actions.go[R102-103]

+	case apierrors.IsInvalid(err):
+		status = http.StatusUnprocessableEntity
Evidence
Rule 3036624 assigns HTTP 400 to invalid client input. The added apierrors.IsInvalid branch
explicitly assigns HTTP 422.

Rule 3036624: Use canonical HTTP status codes for backend error responses
internal/server/actions.go[91-106]

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

## Issue description
Kubernetes invalid-object errors from action writes are returned as HTTP 422 rather than the required HTTP 400.
## Fix Focus Areas
- internal/server/actions.go[102-103]
## Recommended Fix
Map `apierrors.IsInvalid(err)` to `http.StatusBadRequest` and add a test for that error path.

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


View medium (17)
7. Lagging standbys show an unknown status 🐞 Bug ≡ Correctness
Description
cnpgStandbyBacklogTone returns unknown as soon as WAL-byte backlog is unavailable, before
evaluating a known replayLag. When the primary reports replay delay but WAL positions cannot be
compared, the standby's headline and status dot do not reflect that measured delay.
Code

web/src/components/cnpg/runtimeModel.ts[R71-73]

+  if (bytes === undefined) return 'unknown'
+  const byBytes: HealthLevel = bytes >= CNPG_BACKLOG_UNHEALTHY ? 'unhealthy' : bytes >= CNPG_BACKLOG_DEGRADED ? 'degraded' : 'healthy'
+  return replayLag === undefined ? byBytes : worseTone(byBytes, cnpgLagTone(replayLag))
Evidence
The early return discards the supplied replay delay. The replication view passes both measurements
into this function and renders its resulting tone in the standby status dot and headline.

web/src/components/cnpg/runtimeModel.ts[69-74]
web/src/components/cnpg/CNPGReplicationView.tsx[157-164]
web/src/components/cnpg/CNPGReplicationView.tsx[207-224]

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

## Issue description
A standby with measured replay delay receives an unknown status whenever its WAL-byte backlog cannot be calculated.
## Fix Focus Areas
- web/src/components/cnpg/runtimeModel.ts[69-74]
- web/src/components/cnpg/CNPGReplicationView.tsx[157-164]
## Recommended Fix
Evaluate replay delay when bytes are unavailable, reserving `unknown` for when neither measurement exists. Add cases for delay-only degraded and unhealthy standbys.

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


8. Cluster actions skip global toasts 📘 Rule violation ⚙ Maintainability
Description
useCNPGAction creates a mutation without meta.errorMessage or meta.successMessage. When an
action settles, its handlers invalidate queries, but the centralized mutation toast handlers have no
messages to display.
Code

web/src/api/cnpg.ts[R329-330]

+  return useMutation<CNPGActionResult, Error, CNPGActionMutation[Kind]>({
+    mutationFn: ({ action, request }) => guard(() =>
Evidence
The new mutation has no meta property, while the centralized mutation handlers only display
success and error toasts when the corresponding metadata messages exist.

Rule 3036648: React Query mutations must define toast metadata and avoid per-mutation toast handlers
web/src/api/cnpg.ts[316-345]
web/src/RadarApp.tsx[194-207]

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 cluster action mutation omits the toast metadata required by the centralized mutation handlers.
## Fix Focus Areas
- web/src/api/cnpg.ts[316-345]
## Recommended Fix
Provide `errorMessage` and `successMessage` in the mutation's top-level `meta`. Coordinate with the dialog's inline errors and caller-owned success toast to avoid duplicate notifications.

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


9. Backup setup skips global toasts 📘 Rule violation ⚙ Maintainability
Description
useCNPGArchivingPreview and useCNPGProtectionAction both create mutations without top-level
success or error message metadata. The centralized mutation handlers therefore have no messages for
either preview or action results, even though the action handler invalidates the affected queries.
Code

web/src/api/cnpg-protection.ts[R96-97]

+  return useMutation<CNPGActionResult, Error, ProtectionAction>({
+    mutationFn: ({ kind, action, request }) =>
Evidence
Neither newly added mutation supplies the two metadata messages required by the checklist; the
global handlers read only those fields when deciding whether to display toasts.

Rule 3036648: React Query mutations must define toast metadata and avoid per-mutation toast handlers
web/src/api/cnpg-protection.ts[43-56]
web/src/api/cnpg-protection.ts[93-109]
web/src/RadarApp.tsx[194-207]

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

## Issue description
Both backup setup mutations omit the metadata consumed by the centralized mutation toast handlers.
## Fix Focus Areas
- web/src/api/cnpg-protection.ts[43-56]
- web/src/api/cnpg-protection.ts[93-109]
## Recommended Fix
Add top-level `meta` objects with `errorMessage` and `successMessage` to both mutations, choosing messages appropriate to preview and write outcomes.

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


10. Restore is offered without backup evidence 🐞 Bug ≡ Correctness
Description
restoreSourcesForStore turns every server in status.serverRecoveryWindow into a selectable
source without checking for a recovery point or successful backup. When a server has only a failed
backup recorded, the dialog permits manifest review and creation even though that server supplies no
evidence of a recoverable backup.
Code

web/src/components/cnpg/recovery/restoreModel.ts[R130-132]

+  return getCNPGObjectStoreRecoveryWindows(store)
+    .sort((a, b) => Date.parse(b.lastSuccessfulBackupTime ?? '') - Date.parse(a.lastSuccessfulBackupTime ?? ''))
+    .map((w) => ({ kind: 'objectStore' as const, objectStore: name, serverName: w.server }))
Evidence
The recovery-window parser retains server entries with neither success nor recovery-point fields,
and restoreSourcesForStore maps all entries. The dialog disables restore only when the resulting
list is empty, then builds a recovery manifest from the selected source.

packages/k8s-ui/src/components/resources/resource-utils-cnpg.ts[644-665]
web/src/components/cnpg/recovery/restoreModel.ts[127-133]
web/src/components/cnpg/recovery/CNPGRestoreDialog.tsx[200-211]
web/src/components/cnpg/recovery/restoreModel.ts[347-383]

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

## Issue description
An ObjectStore server with no recorded recoverable backup is offered as a restore source.
## Fix Focus Areas
- web/src/components/cnpg/recovery/restoreModel.ts[127-133]
- web/src/components/cnpg/recovery/CNPGRestoreDialog.tsx[200-211]
## Recommended Fix
Exclude servers without a first recoverability point or successful-backup evidence from selectable restore sources, and show a clear no-backup explanation when none remain. Add a test with a failure-only recovery window.

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


11. Operators can signal unreviewed sessions 🐞 Bug ≡ Correctness
Description
cnpgRunSignalBackend validates the supplied Pod UID and backend start time but never establishes
that the PID and start-time pair came from a sessions result. A caller who submits a matching pair
for another live client backend can have the fixed SQL cancel or terminate it without that backend
appearing in the reviewed session facts.
Code

internal/cnpg/sessions.go[327]

+		out, err := x.c.Exec(ctx, x.cluster.GetNamespace(), p.Pod, defaultLogContainer, cnpgSignalArgv(p.PID, p.BackendStart), cnpgSignalSQL(fn))
Evidence
The action runner has no reviewed-fact bindings; its signal path checks Pod identity, then passes
the request's PID and timestamp directly to SQL that signals the matching backend.

internal/cnpg/actions.go[1115-1117]
internal/cnpg/sessions.go[275-285]
internal/cnpg/sessions.go[295-327]

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

## Issue description
Backend signal actions accept caller-supplied backend identities without binding them to the reviewed sessions evidence.
## Fix Focus Areas
- internal/cnpg/sessions.go[295-327]
- internal/cnpg/actions.go[1115-1117]
## Recommended Fix
Bind the action to a reviewed session identity and verify the PID, backend start, and Pod against fresh session evidence before executing the signal. Refuse when that evidence is missing or changed.

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


12. Restore can keep the wrong storage class 🐞 Bug ≡ Correctness
Description
withCNPGTarget writes a newly selected class to spec.storage.storageClass without updating an
existing spec.storage.pvcTemplate.storageClassName. When the source uses a PVC template with an
explicit class directive, the generated create or restore manifest retains that directive alongside
the different class selected in the guide.
Code

web/src/components/cnpg/creation/targetModel.ts[R49-51]

+  if (target.storageClass.trim() !== cnpgTargetFromManifest(manifest).storageClass) {
+    if (target.storageClass.trim()) storage.storageClass = target.storageClass.trim()
+    else {
Evidence
The target reader treats the PVC-template class as the current selection, while the writer changes
only the top-level class; the test confirms the resulting manifest contains both `storageClass:
fast and pvcTemplate.storageClassName: ''`.

web/src/components/cnpg/creation/targetModel.ts[15-22]
web/src/components/cnpg/creation/targetModel.ts[46-55]
web/src/components/cnpg/creation/targetModel.test.ts[68-86]

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

## Issue description
Selecting a storage class can leave a conflicting class directive in a copied PVC template.
## Fix Focus Areas
- web/src/components/cnpg/creation/targetModel.ts[46-55]
- web/src/components/cnpg/creation/targetModel.test.ts[68-86]
## Recommended Fix
When a target class changes, update the existing PVC-template storage class or remove its conflicting directive rather than leaving both class fields. Preserve explicit no-default-class semantics when the user has not changed the class.

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


13. Action refusals bypass the error helper 📘 Rule violation ≡ Correctness
Description
writeActionError writes ActionError responses with w.WriteHeader and a JSON encoder instead of
s.writeError. When a reviewed action is refused, its error response takes a separate formatting
path from the other errors handled by the same function.
Code

internal/server/actions.go[R80-82]

+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(ae.Status)
+		if encErr := json.NewEncoder(w).Encode(body); encErr != nil {
Evidence
Rule 3036617 requires error responses in touched handlers to use s.writeError. The added refusal
branch sets the error status and encodes its body directly, while the other branch calls
s.writeError.

Rule 3036617: Use standard JSON error helper in HTTP handlers
internal/server/actions.go[67-85]
internal/server/actions.go[101-102]

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

## Issue description
Structured action refusals write error responses directly instead of using the server error helper.
## Fix Focus Areas
- internal/server/actions.go[67-85]
## Recommended Fix
Extend the shared error-writing path to support the refusal's code, current facts, and completed steps, then use it for this branch.

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


14. Three cluster reads skip the connection gate 📘 Rule violation ☞ Reliability
Description
handleCNPGClusterRuntime, handleCNPGClusterStorage, and handleCNPGClusterHA start
cluster-backed reads without calling s.requireConnected(w). Requests to those new endpoints reach
their observation readers before the standard connectivity check that neighboring fleet handlers
perform.
Code

internal/server/cnpg_storage.go[R12-14]

+func (s *Server) handleCNPGClusterStorage(w http.ResponseWriter, r *http.Request) {
+	namespace, name := chi.URLParam(r, "namespace"), chi.URLParam(r, "name")
+	resp, err := s.cnpgReader(r).ClusterStorage(r.Context(), namespace, name)
Evidence
Rule 3036634 requires s.requireConnected(w) before cluster operations. Each cited handler calls a
cluster-backed reader first; the fleet handler in the same storage file shows the expected guard.

Rule 3036634: Cluster-touching HTTP handlers must enforce connectivity with requireConnected
internal/server/cnpg_storage.go[12-17]
internal/server/cnpg_runtime.go[59-64]
internal/server/cnpg_cluster_ha.go[20-25]
internal/server/cnpg_storage.go[24-27]

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

## Issue description
Three new cluster-reading handlers enter cluster-backed observations without the standard connectivity gate.
## Fix Focus Areas
- internal/server/cnpg_storage.go[12-14]
- internal/server/cnpg_runtime.go[59-61]
- internal/server/cnpg_cluster_ha.go[20-23]
## Recommended Fix
Call `s.requireConnected(w)` at the start of each handler and return immediately when it fails.

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


15. Non-healthy backup statuses lose badges 📘 Rule violation ⚙ Maintainability
Description
LastRun renders a healthy status with ` but renders other backup statuses as a colored ` using
toneTextClass. When the latest backup is not healthy, the same status position switches from the
shared badge presentation to custom colored text.
Code

web/src/components/cnpg/CNPGProtection.tsx[139]

+        {st.level === 'healthy' ? <Badge severity="success" size="sm">{st.text.toLowerCase()}</Badge> : <span className={toneTextClass(st.level)}>{st.text.toLowerCase()}</span>}
Evidence
Rule 3036677 requires badge-like status UI to use Badge with semantic appearance props. The added
conditional renders a badge for healthy status and a raw color-class span for the other statuses.

Rule 3036677: Use Badge components instead of hard-coded badge color strings
web/src/components/cnpg/CNPGProtection.tsx[134-142]

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 latest-run status uses a shared badge only in its healthy branch.
## Fix Focus Areas
- web/src/components/cnpg/CNPGProtection.tsx[134-140]
## Recommended Fix
Render the non-healthy statuses with `Badge` and map their levels to semantic severity props rather than color classes.

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


16. Partial namespace reads appear complete 🐞 Bug ≡ Correctness
Description
ListBlocking now gives all pending namespace informers one timeout but discards whether they all
synchronized before indexing their contents. When that shared wait expires, the workspace's
all-namespaces caller receives the available subset with no error and takes its complete-read path
rather than its partial-coverage path.
Code

pkg/k8score/dynamic_cache.go[R1448-1451]

+	if len(pending) > 0 {
+		ctx, cancel := context.WithTimeout(context.Background(), timeout)
+		cache.WaitForCacheSync(ctx.Done(), pending...)
+		cancel()
Evidence
The changed wait shortens the multi-informer synchronization window to one timeout, yet an
unsuccessful wait still falls through to indexing. The all-namespaces caller treats a nil list error
as success and only records partial coverage on its error path.

pkg/k8score/dynamic_cache.go[1442-1455]
internal/server/kind_access.go[188-220]

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

## Issue description
A shared cache-sync timeout can silently return a subset of namespaces as a complete workspace read.
## Fix Focus Areas
- pkg/k8score/dynamic_cache.go[1442-1455]
- internal/server/kind_access.go[188-220]
## Recommended Fix
Check the shared wait result and return an identifiable incomplete-sync error when any required informer remains unsynced, so the caller can use its partial-coverage handling.

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


17. Write-evidence failures use a different log format 📘 Rule violation ◔ Observability
Description
handleGitOpsWriteEvidence logs target-read failures as `Failed to read %s %s/%s for write
evidence: %v`, with an extra kind placeholder and words between the resource name and error. An
unclassified Kubernetes Get error then reaches the 500 response on that branch.
Code

internal/server/gitops_write_evidence.go[161]

+			log.Printf("[gitops] Failed to read %s %s/%s for write evidence: %v", sanitizeForLog(req.Kind), sanitizeForLog(req.Namespace), sanitizeForLog(req.Name), err)
Evidence
The log immediately precedes a 500 response but does not use the required %s/%s: %v ending.

Rule 3036628: Log 500 errors with standardized module/action format before writing the response
internal/server/gitops_write_evidence.go[153-164]


18. Destructive buttons use raw red backgrounds 📘 Rule violation ⚙ Maintainability
Description
ActionConfirmDialog assigns bg-red-600 and hover:bg-red-700 to its disruptive confirm button
instead of theme background tokens. Every action marked disruptive takes that branch, with no
documented design-spec exception above the button.
Code

packages/k8s-ui/src/components/shared/ActionConfirmDialog.tsx[213]

+            disruptive ? 'bg-red-600 text-white hover:bg-red-700' : 'btn-brand',
Evidence
The added button styling contains hardcoded background color utilities on the disruptive branch.

Rule 3036653: Use theme background tokens instead of hardcoded utility color classes
packages/k8s-ui/src/components/shared/ActionConfirmDialog.tsx[207-215]


19. A noun comment repeats its helper ✓ Resolved
Description
The new comment above countNoun describes the exact singular and plural strings returned by the
immediately following branches. A reader inspecting this helper gains no rationale or constraint
from the comment beyond what those return statements already show.
Code

internal/podlogs/snapshot.go[198]

+// countNoun reads "1 source", "2 sources".
Evidence
The comment's two examples are direct restatements of the helper's two return branches.

Rule 3036542: Avoid explanatory comments that restate obvious code behavior
internal/podlogs/snapshot.go[198-203]

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 comment above the noun helper only restates its return values.
## Fix Focus Areas
- internal/podlogs/snapshot.go[198-203]
## Recommended Fix
Delete the comment; retain the helper and its behavior.

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


20. Backups can use changed archive settings ✓ Resolved
Description
clusterActionRunners binds the backup action only to hibernation, while cnpgRunBackup selects
its method and destination from the freshly read Cluster. If backup configuration changes after the
operator opens the confirmation dialog, the action can create a Backup without reporting that the
relevant Cluster facts changed.
Code

internal/cnpg/actions.go[1107]

+	"backup":           {binds: []string{"hibernation"}, run: cnpgRunBackup},
Evidence
The runner binds only hibernation; the action compares only bound facts, then selects the requested
method against current Cluster facts and creates the Backup.

internal/cnpg/actions.go[1106-1108]
internal/cnpg/actions.go[1132-1167]
internal/cnpg/actions.go[1278-1305]
internal/cnpg/actions.go[1334-1356]

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

## Issue description
Backup confirmation checks hibernation but not the backup configuration used by the write.
## Fix Focus Areas
- internal/cnpg/actions.go[1106-1108]
- internal/cnpg/actions.go[1278-1356]
## Recommended Fix
Include the relevant backup method, plugin, and destination configuration in the reviewed facts and compare them against the freshly read Cluster before creating the Backup. Preserve the existing explicit backup-target comparison.

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


21. A replacement cluster can inherit a run ✓ Resolved
Description
CNPGScheduleActions tracks a manual scheduled-backup run without the source Cluster UID. If that
Cluster is recreated under the same name while the run remains unfinished, the tracker accepts the
replacement and the backup observer can attribute a same-named visible Backup to the old run.
Code

web/src/components/cnpg/actions/CNPGScheduleActions.tsx[R180-183]

+                  context: data.context,
+                  namespace,
+                  cluster: data.facts.cluster,
+                  target: { name: r.backup },
Evidence
The tracking call omits clusterUID; operation selection permits absent UIDs, the replacement check
runs only for recorded UIDs, and the backup observer matches by name and namespace.

web/src/components/cnpg/actions/CNPGScheduleActions.tsx[175-185]
web/src/components/cnpg/operations/CNPGOperationTracker.tsx[45-53]
web/src/components/cnpg/operations/model.ts[121-130]
web/src/components/cnpg/operations/model.ts[421-435]

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

## Issue description
Scheduled manual runs omit the Cluster UID that other tracked Cluster operations use to reject replacement Clusters.
## Fix Focus Areas
- web/src/components/cnpg/actions/CNPGScheduleActions.tsx[175-185]
- web/src/components/cnpg/operations/model.ts[121-130]
## Recommended Fix
Obtain the source Cluster UID at run time and store it on the tracked operation. Require that UID to match before attributing observations to the run.

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


22. Volume charts mislabel query failures ✓ Resolved
Description
ClusterHistory discards the state returned by usageScopeFailure and puts every PVC scope-check
failure into PVCAmbiguous. When scope labels mismatch or the Prometheus check fails,
queryCNPGHistory consequently marks the volume charts ambiguous instead of reporting the mismatch
or query error.
Code

internal/cnpg/history.go[527]

+			_, req.PVCAmbiguous = usageScopeFailure(err)
Evidence
usageScopeFailure distinguishes ambiguous, scope-mismatch, and error states; the caller discards
that distinction, and the chart builder maps any populated PVCAmbiguous to ambiguous.

internal/cnpg/history.go[119-127]
internal/cnpg/history.go[524-530]
internal/prometheus/cnpg_history.go[311-325]

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

## Issue description
PVC history drops the scope failure state, causing mismatch and query errors to appear as ambiguity.
## Fix Focus Areas
- internal/cnpg/history.go[524-530]
- internal/prometheus/cnpg_history.go[311-325]
## Recommended Fix
Carry both state and reason from PVC scope resolution into the history request, and assign that state to affected PVC charts instead of treating every failure as ambiguous.

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


23. Argo write warnings appear before lookup ✓ Resolved
Description
useResolvedGitOpsOwner starts an Application lookup for an Argo owner without a namespace but
excludes that lookup from lookupPending. While it is unresolved, useGitOpsWriteGuard requests
evidence with an empty owner namespace, so the write is classified as possibly reverting and asks
for acknowledgment before the ownership lookup finishes.
Code

web/src/hooks/useResolvedGitOpsOwner.ts[R131-132]

+    lookupPending: Boolean(inheritedLookupRef && inherited.isPending),
+    lookupError: Boolean(inheritedLookupRef && inherited.isError),
Evidence
The hook enables an asynchronous namespace lookup but reports only the inherited-owner lookup as
pending. The guard therefore enables evidence for the unresolved owner; a missing policy is
classified as may-revert.

web/src/hooks/useResolvedGitOpsOwner.ts[96-100]
web/src/hooks/useResolvedGitOpsOwner.ts[120-133]
web/src/hooks/useResolvedGitOpsOwner.ts[149-154]
web/src/hooks/useGitOpsWriteGuard.ts[64-67]
web/src/hooks/useGitOpsWriteGuard.ts[81-95]
packages/k8s-ui/src/utils/gitops-write-guard.ts[210-218]

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 GitOps write guard evaluates incomplete Argo ownership while the Application namespace lookup is still loading.
## Fix Focus Areas
- web/src/hooks/useResolvedGitOpsOwner.ts[96-100]
- web/src/hooks/useResolvedGitOpsOwner.ts[120-133]
- web/src/hooks/useGitOpsWriteGuard.ts[64-67]
- web/src/hooks/useGitOpsWriteGuard.ts[81-95]
## Recommended Fix
Include the enabled Argo Application lookup in ownership's pending state. Defer the write-evidence request until that lookup settles, then evaluate unresolved or failed ownership explicitly.

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


Grey Divider

Tip of the day
ðŸ’Ą Did you know, you can hide the parts of a finding you never read, like the evidence or the agent prompt

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread internal/server/cnpg_actions.go Outdated
Comment thread internal/server/cnpg_actions.go Outdated
Comment thread internal/server/gitops_write_evidence.go Outdated
Comment thread internal/server/cnpg_actions.go Outdated
Comment thread web/src/api/cnpg.ts Outdated
Comment thread internal/server/cnpg_runtime.go Outdated
Comment thread web/src/components/cnpg/CNPGClusterRuntime.tsx Outdated
Comment thread web/src/components/cnpg/actions/CNPGRestoreDialog.tsx Outdated
Comment thread internal/server/gitops_write_evidence.go Outdated
Comment thread internal/server/cnpg_actions.go Outdated

@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.

Stale Bugbot comment from a previous run.

Comment thread internal/server/cnpg_actions.go Outdated

@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.

Stale Bugbot comment from a previous run.

Comment thread web/src/components/cnpg/CNPGClusterRuntime.tsx Outdated
Comment thread web/src/components/cnpg/CNPGClusterRuntime.tsx Outdated

@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.

Stale Bugbot comment from a previous run.

Comment thread web/src/components/cnpg/CNPGPooling.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.

Stale Bugbot comment from a previous run.

Comment thread internal/server/cnpg_cluster_ha.go Outdated

@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.

Stale Bugbot comment from a previous run.

Comment thread internal/prometheus/cnpg_history.go Outdated
Comment thread internal/server/cnpg_cluster_activity.go

@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.

Stale Bugbot comment from a previous run.

Comment thread web/src/components/cnpg/CNPGOperatorDiagnosis.tsx
Comment thread internal/server/cnpg_report.go Outdated

@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.

Stale Bugbot comment from a previous run.

Comment thread internal/cnpg/errors.go
@nadaverell nadaverell changed the title CloudNativePG actions, live runtime and a reusable GitOps write guard CloudNativePG operations, live diagnosis, history and a reusable GitOps write guard Sep 30, 2026

@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.

Stale Bugbot comment from a previous run.

Comment thread internal/prometheus/cnpg_history.go Outdated

@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.

Stale Bugbot comment from a previous run.

Comment thread web/src/components/cnpg/operations/model.ts

@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.

Stale Bugbot comment from a previous run.

Comment thread internal/server/cnpg_history.go Outdated
Comment thread web/src/components/resource/PVCUsageBar.tsx Outdated

@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.

Stale Bugbot comment from a previous run.

Comment thread internal/server/cnpg_destroy.go Outdated
Comment thread internal/server/cnpg_destroy.go Outdated

@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.

Stale Bugbot comment from a previous run.

Comment thread packages/k8s-ui/src/components/cnpg/logicalReplication.ts
Comment thread packages/k8s-ui/src/components/cnpg/connect.ts Outdated

@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.

Stale Bugbot comment from a previous run.

Comment thread internal/server/cnpg_runtime.go Outdated

@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.

Stale Bugbot comment from a previous run.

Comment thread internal/server/cnpg_cluster_ha.go Outdated

@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.

Stale Bugbot comment from a previous run.

Comment thread web/src/components/cnpg/paths.ts Outdated

@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.

Stale Bugbot comment from a previous run.

Comment thread internal/server/cnpg_actions.go Outdated

@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.

Stale Bugbot comment from a previous run.

Comment thread web/src/components/cnpg/CNPGStorage.tsx Outdated
Comment thread web/src/components/cnpg/CNPGReplicationView.tsx Outdated

@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.

Stale Bugbot comment from a previous run.

Comment thread web/src/components/cnpg/runtimeModel.ts

@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.

Stale Bugbot comment from a previous run.

Comment thread pkg/cnpg/schedule_description.go

@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.

Stale Bugbot comment from a previous run.

Comment thread packages/k8s-ui/src/components/cnpg/ha.ts Outdated
Comment thread web/src/components/cnpg/actions/CNPGConnectButton.tsx Outdated
nadaverell added a commit that referenced this pull request Oct 4, 2026
â€Ķworkspace (#1969)

## Summary

The CloudNativePG workspace (#1921, #1922) carried a generic layer under
CNPG names: the facts and problem list on every summary, per-kind
coverage, the reviewed-action contract, grants, Prometheus series
attribution, and screen layout borrowed from Capacity's internals. This
PR moves that layer into shared, integration-neutral modules, so the
next workspace-style integration (Velero is the likely one) imports it
instead of copying CNPG.

Nothing here is released yet, so the three CNPG wire shapes that every
future workspace would copy are fixed now:
- grants are structured objects, not sentences;
- "not cached by Radar" is told apart from "no access";
- each object's GitOps manager comes from the server.

It also adds the version-skew gate the CNPG endpoints were missing.

Only mechanisms whose interface is already evident in today's code are
shared: each has two or more consumers, or is a contract every workspace
must follow. The rest is listed below as not shared yet.

## What changed

### Shared UI (`@skyhook-io/k8s-ui`)
- **`components/facts/`**, for any surface that shows observed values
(single-kind renderers included):
  - `Fact`, with `FactGrid`/`FactRow`/`FactValue`/`FactSource`
- `CertaintyGlyph`, moved from Capacity; `CapacityCertainty` stays as
the same type
  - `ManagedByText`
- **`components/problems/`**: `WorkspaceProblem<Category>` and
`ProblemCallout`/`ProblemList`/`ProblemMeta`, which take the workspace's
`rootKind` instead of a hard-coded `'Cluster'`, plus `OpenIssueContext`.
- **`ui/FoldSection`**: `SectionHeading`, `FoldSection`, `FoldSummary`.
- **Elsewhere in k8s-ui**:
  - `ui/RefLink` now uses the existing `ResourceRef`.
  - `toneTextClass` and `worseTone` live in `ui/status-tone`.
  - `utils/grant` adds `Grant`, `formatGrant` and `grantParts`.
- `issueReasonTitle` gives a reason one title on both the Issues page
and the workspace.
- **Kept CNPG-specific:** categories, ordering, wording and builders.
CNPG binds `WorkspaceProblem` to its own categories.
- **Badges:** CNPG badges now use `healthToSeverity` like the rest of
the app.
- **Buttons:** secondary buttons use a new `.btn-secondary` class
(documented in DESIGN.md) instead of ten hand-rolled class strings.
- **Sidebar:** `SidebarCategoryDestination.countLowerBound` renders a
count over partly read data as `â‰ĨN`, and its zero as unknown rather than
none.

### App (`web/`)
- **`components/workspace/`** holds the screen primitives Capacity and
CNPG share: `ScreenBody`, `ScreenEmptyState`, `Notice`, `Segments`,
`FilterChips`, `SectionTable` and its table classes,
`RefreshFailedNotice`, `GrantText`, and the text helpers. CNPG no longer
imports from `capacity/shared.tsx`.
- **`api/actions.ts`** is the client half of the action contract:
  - the `ActionCapability` and `ActionRequest` types
  - `actionErrorCode`, which an integration widens with its own codes
  - `actionOutcomeLocked` and `actionCompleted`
  - `capabilityReason`, now the one "why is this disabled" wording
- **Utilities:** `utils/drawer-trail.ts` holds the `?drawer=` codec, and
`utils/page-links.ts` the back label and the subject-filtered Issues
link.
- **Kind table:** CNPG's detail kinds derive kind and group from the
workspace kind table.

### Server
- **`internal/server/actions.go`**: the reviewed-action contract.
- `ActionCapability`, `ActionRequest`, `decodeActionRequest` and
`decodeActionParams`
- refusals: `changedAction`, `blockedAction`, `partialAction` and
`writeActionError`
- the permission decision: `grantPermission` and `capabilityVerdict`,
with a real SelfSubjectAccessReview in local mode
  - `mergePatchAtVersion`
- **Grants:** `Grant` lives in `internal/auth` and carries `In(ns)` and
`String()`.
- **Kind access:** `kind_access.go` covers per-kind access and coverage
(`readWorkspaceKind`, `typedKindScope`, `KindCoverage`);
`cache_scope.go` has `namespacesWithinCache`.
- **Other shared helpers:**
- `read_source.go`: one `ReadSource` shape for HA, recovery and storage
  - `fanout.go`: the bounded per-namespace fan-out
- `internal/prometheus/series_scope.go`: `SeriesIsolation`, already used
by the PVC usage charts
- **Kept CNPG-specific:** facts, guards, runners and the extra refusal
codes.
- **Exported manager detection:** `pkg/topology` now exports
`ManagedByFromMeta`. This is additive.

### Wire changes (CNPG endpoints, unreleased)
- **`grant`** is `{verb, group?, resource, subresource?, namespace?}`
(no namespace means cluster-wide) on every capability, read source,
chart and report item. The wording is unchanged and comes from
`Grant.String()` / `formatGrant`.
- **Kind coverage** gains:
  - a state `uncached`, for a scope Radar's cache does not cover at all;
- `uncachedNamespaces`, named under the same rule as `deniedNamespaces`.

Before, a namespace the caller can read but Radar does not cache was
reported as denied, and the UI said "No access". It now says "Radar does
not cache Pods in db".
- **`/api/cnpg/workspace`** gains `managedBy`, keyed `Kind/ns/name`.
- "Declared in" now uses the server's manager detection and links to the
Argo CD application or Flux object.
- Before, the client parsed labels itself. That misnamed Argo CD
applications living outside Argo CD's namespace (`ns_app`) and ignored
the Flux namespace label.

### Version skew
New `FeatureCapabilities` flags: `cnpgWorkspace` (every `/api/cnpg/*`
route except the two image-catalog lookups that predate it) and
`gitopsWriteEvidence`. Each has a `radarFeatures` entry.

Every CNPG query, mutation, log stream, report download and the
write-evidence query goes through `useRadarFeature`. Mutations are never
retried. On a Radar without the workspace:
- the sidebar offers no workspace destinations;
- `/workload` links and drawer Expand stay on the standard views;
- a Cluster's Logs tab shows its Pods' logs;
- a workspace screen opened directly says it needs a newer Radar.

### Vocabulary
On screen these areas are named by their subject. The block above a
sidebar category's kinds reads **Views**, and the copy says
"CloudNativePG views" and "CloudNativePG data", never "workspace",
because Radar Hub uses "workspace" for the customer's account.
"Workspace" remains the internal term for an integration with several
kinds and its own screens (the integration guide,
`SidebarCategoryWorkspace`, `/api/cnpg/workspace`).

### Docs
- **DESIGN.md:** the rules for unknown, partial and denied values are
written once. `capacity.md` and `cnpg.md` link to them and keep their
per-value tables.
- **`docs/INTEGRATION_GUIDE.md`:** a new "Workspace integrations"
chapter covering when a workspace is warranted, the pieces to import,
and what is not shared yet. CLAUDE.md points to it.

## Not shared yet
These have one consumer, and their interface should come from the
second:
- the workspace registry / App wiring
- the operation tracker
- the generic action runner
- the fixed-path `pods/proxy` reader
- the merged log stream
- the report bundle
- operator diagnosis
- a TTL-memo helper

Rollouts' capabilities answer "allowed" in local mode without asking the
apiserver. That is a real bug, but Rollouts returns bare booleans and
hides denied actions, so it needs its own PR.

## Public surface
- **k8s-ui:** exports that exist on main are unchanged. Renamed exports
were all added in #1921/#1922. `CapacityCertainty` is kept.
- **`pkg/`:** one additive export (`topology.ManagedByFromMeta`).
- **`pkg/capacityapi` v1alpha1:** unchanged.
- **radar-app:** surface unchanged.

## Testing
- **Go:** `go build ./...`; `go test ./...` in both modules. The
`cmd/desktop` env test is flaky and passes alone. `gofmt -l` is clean.
- **Frontend:**
  - type checks pass for web and k8s-ui;
  - vitest: k8s-ui 236 files / 4,369 tests, web 182 files / 2,028 tests;
  - eslint: 0 errors, and no new warnings.
- **Live on the kind CloudNativePG demo, full access and a view-only
identity:**
- every CNPG page rendered with structured grants and no "[object
Object]" or "undefined";
- a namespaced Argo CD tracking ID on a demo Cluster showed "Declared
in: Argo CD application argocd/payments", linked;
- with the capability flags removed from `/api/capabilities` (an older
Radar behind Radar Hub), the sidebar workspace, redirects, composed
summaries and actions fell back to the standard views, and `/cnpg`
showed the upgrade note.

Stacked on #1922 (which is stacked on #1921). Merge order: #1921, #1922,
then this PR, before a release ships `/api/cnpg/*`.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches CNPG API shapes, RBAC/coverage semantics, and cluster write
paths via the shared action layer; behavior is intended to be equivalent
with clearer grant and cache messaging.
> 
> **Overview**
> Extracts the generic workspace layer that CloudNativePG had under
CNPG-specific names into shared modules, so future integrations (e.g.
Capacity-style screens) import one contract instead of copying.
> 
> **Server:** Adds `internal/auth.Grant` (structured RBAC on the wire),
`internal/server/actions.go` (reviewed `ActionRequest`, capabilities,
409/partial errors, `mergePatchAtVersion`), `cache_scope.go` /
`kind_access` patterns, and `prometheus/series_scope.go` (shared
Prometheus isolation). CNPG handlers now use these; history/PVC denial
fields expose `*Grant` instead of strings. `FeatureCapabilities` adds
`cnpgWorkspace` and `gitopsWriteEvidence`. CNPG workspace coverage gains
`uncached` / `uncachedNamespaces` (distinct from denied) and `managedBy`
from server-side GitOps detection.
> 
> **UI:** New k8s-ui `facts/`, `problems/`, fold sections,
`formatGrant`, and app `components/workspace/` plus `api/actions.ts`.
CNPG is rewired to structured grants and shared action types; secondary
buttons use `.btn-secondary`.
> 
> **Docs:** DESIGN.md documents unknown/partial/denied values once;
INTEGRATION_GUIDE adds a workspace-integrations chapter; CLAUDE.md
points builders at it.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
5af6ed1. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

@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.

Stale Bugbot comment from a previous run.

Comment thread internal/issues/source_cnpg_schedule.go

@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.

Stale Bugbot comment from a previous run.

Comment thread web/src/components/cnpg/CNPGArchivingRepair.tsx Outdated
nadaverell added a commit that referenced this pull request Oct 13, 2026
The CNPG work in #1922 changes shared resource-page and navigation code
used throughout Radar. This PR isolates that frontend blast radius so
resource browsing, drawers, logs and Capacity can be reviewed
independently while keeping the existing user journeys.

Resource hosts own renderer overrides and resource-specific summaries,
logs and destinations. The application composes the same 31 existing
renderer wrappers once; the current CNPG adapter carries its
already-shipped callbacks, group collision rules, redirects and drawer
trail. Workspace route metadata owns titles, navigation placement,
namespace-view scope and context-switch query policy. Capacity forwards
only its existing resource-open callback and remains cluster-wide.

Shared workspace notices, empty states and table styles now have neutral
ownership rather than living under Capacity. The accessible certainty
glyph and labels move into k8s-ui with the same value/tooltip semantics.
The additive `ResourcesView.renderKindView` seam keeps the sidebar and
inspected resource while replacing one kind's table; null/false uses the
ordinary table. Row/search/bulk shortcuts, column preferences and table
sorting/filter aggregation are inactive during a replacement; sidebar
kind navigation stays available. Bulk dialogs/selections/comparison
picks are cleared when a replacement arrives for the same kind.

The review-sensitive boundaries are exact API-group identity, the stable
renderer map, URL state and context pins, inherited detail tabs, and the
table-to-custom-view transition. Existing server, RBAC and endpoint
contracts are retained.

Validation: both frontend type checks; full web suite (2,043 tests),
final focused integration/Capacity/route tests (146), final k8s-ui suite
(4,523 passed, one skipped); full `make build` including embed and Go
binary. A same-kind bulk-dialog regression fails before the fix and
passes afterwards.

Live validation used the locally built application against existing AWS
test clusters: normal resource tables/drawers/full pages, real Pod and
Deployment log streams, parent navigation, all four Capacity screens
including missing-pool recovery and node-removal presentation, and
existing CNPG detail redirects/context pins. Actual CSS widths 1280,
1920 and 2560 were measured and captured without page overflow.
Screenshots below show the preserved shared surfaces.

Packed app/UI tarballs were installed into a private real Hub copy
without source aliases. Its Vite production bundle and GET-only embedded
resource, logs, Capacity and CNPG navigation journeys pass. Full Hub
typechecking fails on an existing obsolete `NavCustomization.chrome`
field; an independent exact-main packed control fails identically. Hub
adoption must also remove its obsolete chrome/slot entries; no
compatibility mode was added here.

![Existing Deployment full
detail](https://github.com/user-attachments/assets/c8e2d8ad-2563-4185-813c-20529534de2d)

![Cluster-wide Capacity
overview](https://github.com/user-attachments/assets/9563b767-7ad8-41b2-974c-962c45bf79ce)
nadaverell added a commit that referenced this pull request Oct 13, 2026
â€Ķction (#2079)

CNPG setup needs to return from YAML review with edits intact, create
new objects safely, open a chosen executable, and select a chart
interval. Those capabilities belong in Radar's existing shared
components. This PR extracts them from #1922 so changes to ordinary
Radar workflows receive independent review; CNPG screens and operations
remain there.

### Shared contracts

- `CreateResourceDialog` adds initial/locked modes and a
draft-preserving Back action. Successful callbacks receive the first
result plus the exact submitted YAML after close; dry runs leave the
dialog open. Partial creation retains persisted-object history through
retries, review Back and file imports, and keeps recovery visible above
the footer. Unlocked recovery switches to Apply when preview is
available; direct creation stays in Create until the user chooses Apply.
- Apply results include the persisted object's optional UID on real
writes. Dry runs omit it. Existing fields and first-result callback
semantics remain compatible.
- Terminals accept an executable, title and context note. Explicit
commands retain their chosen container, distinguish sessions when
reusing tabs, refresh supplied metadata without reconnecting, and show
command-specific error guidance. Error overlays follow the active theme;
the terminal canvas and existing exec transport remain unchanged.
- `AreaChart` adds optional shaded intervals, controlled selection and
pointer/keyboard range callbacks. Range bands remain visible above
opaque area fills. Ordinary charts retain touch-tap inspection.
`AlertBanner` accepts an action beside its title while preserving its
no-action markup.

The shared package owns presentation and interaction; the host owns data
and transport. These options add no generic navigation or new endpoints.

Current main is merged normally. The only conflict was additive
shared-package documentation; both the merged resource-kind/certainty
guidance and these optional-flow contracts are retained. All runtime
feature files are unchanged. The existing `>=1.17.0` peer minimum is
retained.

### Validation

All nine CI checks pass on the merged head: full Go/pkg tests, frontend
typecheck/lint/build, 4,561 shared UI tests (one existing skip), 2,061
web tests, 140 Settings browser journeys, Helm and both CodeQL analyses.
Both package typechecks, 91 focused shared-component/resource-kind-view
tests and the Go persisted-UID/dry-run regression also pass locally. The
cancel-import regression fails before its one-line fix and passes
afterward. Test thresholds and existing assertions are unchanged.

Real AWS fixture calls verified dry-run absence,
create/update/partial-write UIDs and persisted values after refresh.
Browser checks exercised default and guided creation, reviewed writes,
Back retaining edits, three partial-recovery paths, chart
pointer/keyboard/gap behavior, light/dark banners, an actual shell,
direct `cat`, and a missing executable. Optional modes use a temporary
local harness importing production components and the real backend; it
adds no product route.

A private copy of the real Hub source bundled the packed companion
packages successfully. Its full TypeScript check reports the same
existing `NavCustomization.chrome` error against exact main and these
packages; this PR does not fix or claim to pass that inherited consumer
drift.

### Rendered examples

Actual shared components in the temporary harness, using the disposable
AWS namespace:

![Partial creation keeps persisted names and recovery visible above the
footer](https://github.com/user-attachments/assets/31e80e26-145e-4823-94aa-013ecba0e8bc)

![Explicit executable failure is readable in light mode and offers
Retry](https://github.com/user-attachments/assets/b55323f8-5e73-4c14-b478-2a87305017a7)

![Default chart beside controlled selection and an actionable warning in
dark
mode](https://github.com/user-attachments/assets/fb3d8f65-f8e0-4371-b21e-f74260c87227)
@cursor

cursor Bot commented Oct 13, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@cursor

cursor Bot commented Oct 14, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

nadaverell added a commit that referenced this pull request Oct 14, 2026
Cluster transitions replace clients and informer caches in separate
steps. A request that copies them independently can pair dependencies
from different generations. The existing client snapshot protects the
client/config pair, but does not also capture caches and the operation
cancellation context.

`CaptureClusterReads` gives readers a short, nonblocking capture window
around the process's context-operation lock. It refuses active or queued
context operations and allows simultaneous captures. Writers retain
their existing exclusive locking. Callbacks only copy dependencies; I/O
and context/configuration operations belong after the capture.

This guarantees consistency with context switching, authentication
recovery and configuration operations. It does not establish
startup/shutdown readiness: callers must still check the connection and
nil dependencies. `TryClusterConfiguration` waits at most 50 ms for
brief captures instead of refusing an unrelated settings update; an
active or queued context transition is still refused immediately.
Sustained contention retains the existing busy error.

This independent carve-out from #1922 adds the primitive without
changing an HTTP handler on main. Its concrete CNPG consumers capture
clients, caches and cancellation together in that feature.

Validation: the complete `internal/k8s` suite, including `go test -race
-p 2 ./internal/k8s`; deterministic active/queued writer,
concurrent-reader and replacement-serialization tests; `make build`; ten
read-only API checks on AWS `radar-test-nonprod`. Live context
transitions are covered by deterministic lifecycle tests rather than
switching a user's cluster. Visual testing is not applicable.

Design direction: if more callers need this capture seam, consider one
coherent connection generation containing clients, caches and
cancellation, published together. An atomic pointer alone does not
establish informer readiness or queued-transition behavior; those
lifecycle guarantees must survive that change. This PR keeps the bounded
lock-and-copy seam and does not introduce a new lifecycle framework.
nadaverell added a commit that referenced this pull request Oct 14, 2026
Workspace summaries need consistent ways to show what is known, why a
value is unread, where a finding came from, and what an operator is
being asked to change. Keeping those primitives under CNPG forces
another integration to import that domain or copy its presentation.

This promotes the existing facts, resource links, section headings and
problem presentation into shared k8s-ui modules. CNPG summaries use them
through compatibility wrappers that retain their public signatures.
Generic findings use the existing non-healthy HealthLevel tones,
including canonical orange alert and gray unknown; CNPG maps its own
critical/warning/posture severity at that boundary. Issue, audit and
measurement provenance form a discriminated union, and subjects derive
from ResourceRef. WorkspaceProblemCallout has the same name at its
definition and package export.

ActionConfirmDialog is additive. It takes one result object for an
ordinary error, an unknown outcome or a partially completed action;
unknown and partial outcomes lock repeat submission. Hosts classify
transport errors and retain the result across dismissal until they
review current state again. The component reuses the existing dialog and
disclosure primitives and owns no transport or authorization policy.

Facts, links, headings and problem presentation have existing CNPG
summary consumers on main. The dialog, grant display and ManagedByText
have concrete callers in #1922; they are new APIs in this carve-out.
FoldSection is used internally for original problem evidence. No package
version, peer dependency, route or setting changes. The reviewed-action
HTTP contract and host classifier stay in #1922.

Validation: make tsc, package type checking, the previously completed
full shared UI suite, focused public-export/provenance/CNPG
summary/action-interaction regressions, and make build. Ten read-only
AWS checks on radar-test-nonprod cover ordinary Radar paths and CNPG
not-installed; AWS has no CNPG installation. Playwright now renders the
fixture and verifies reference/Issues navigation, disclosure, typed
confirmation, pending dismissal locks and distinct unknown/partial
result locks. Sixteen complete screenshots were captured with measured
1280, 1920 and 2560 CSS-pixel viewports and inspected in both themes;
the summaries and review/error/pending dialogs read correctly. Browser
zoom required corrected screenshot bounds, with no application or
preference changes. Synthetic fixture checks are not live CNPG actions.
@nadaverell
nadaverell force-pushed the feature/cnpg-actions-runtime branch from 8648f65 to 42dc651 Compare October 15, 2026 13:17
â€Ķload

Writes
- Restart-instance and fence refuse the target of an in-flight switchover
  or failover, as destroy already did; capabilities and the POST agree.
- A rolling restart binds the Cluster generation, so a changed primary
  update strategy is refused as changed rather than applied unreviewed.
- A 503 without a code is an uncertain outcome (a proxy may have forwarded
  the write); Radar's own refusals before any write carry `unavailable`.

Reads and reports
- The report bundle withholds pgaudit statements, deadlock DETAIL queries,
  postImportApplicationSQL and the cnpg.io/podPatch annotation.
- The scheduled-run issue evaluates only lists synced in the namespace.
- The history memo no longer keeps cancelled or failed chart reads.
- Recovery Jobs report their cnpg.io/jobRole.

Follow-through and fleet
- The tracker waits for the first read after a request instead of reading
  "cannot be followed"; a long backup no longer reads stalled; a restart
  keeps instances fenced after the request in its completion criteria;
  records are scoped to the API base for Radar Hub; unreadable readiness
  stays unknown.
- The restore panel counts only recovery Jobs and stays completed once a
  restored instance is ready.
- The switchover dialog no longer flags a streaming standby whose reported
  checkpoint timeline lags a promotion.
- A single instance and a missing backup destination are posture on the
  Cluster page, as in the fleet.
- The Resources sidebar reads disk and lag only on CloudNativePG views.
- Connect copies over plain HTTP.

Cleanup: drop the unused grant table, match cold snapshots with
BackupMatchesCluster, reuse readscope.SelectNamespaces in MCP, correct
doc comments that named pre-rename identifiers.
â€Ķrestore points

Writes
- Unfence refuses when a cold VolumeSnapshot backup can't be ruled out:
  the Cluster declares snapshots and its Backups could not be listed.
- Pooler pause/resume and ScheduledBackup suspend/resume bind the reviewed
  generation, so a retargeted Pooler or schedule is refused as changed.
- The remaining pre-write refusals (destroy preflight, missing exec client)
  carry the `unavailable` code and stay retryable.
- The fence dialog disables an instance the server would refuse, with the
  reason.

Restore
- A VolumeSnapshot Backup offers only the end of its snapshot: a later
  point needs a WAL archive source the dialog does not configure.
- Recovery evidence counts only what the selected source holds (that
  archive's backups and recovery window, or that Backup), so another
  method's newer backup can't make an archive look current.
- The restore panel completes on a completed recovery Job too, and its
  footer states the rule the code applies.

Health and counts
- A replica cluster's replication never reads healthy without evidence
  about its upstream feed, on the Cluster page and in the fleet.
- Outside the CloudNativePG views the Clusters badge is a lower bound, and
  its zero reads as unknown with the reason.

Follow-through
- A fence supersedes an instance restart only when it covers that
  instance; a stale identity with failing reads reads unobservable;
  destroyInstance follows runtime for its replacement.

Doc comments that still named pre-rename identifiers are corrected.
â€Ķre completion

- Restart-instance and fencing refuse a Pod labelled primary while the
  Cluster status names another: the standby path would delete or stop a
  primary. Fence runs now read the instance Pods to see the label.
- The restore panel counts only a full recovery Job as proof the restore
  finished: a snapshot recovery Job without a WAL source only puts files in
  place, and PostgreSQL first starts on the instance.
- A replica cluster's fleet replication carries the upstream caveat even
  before it reports a primary, and a single-instance replica cluster no
  longer reads calm.
Without the Pod there is no role label to compare with the Cluster status,
so the mislabelled-primary refusal could not fire and a named fence could
stop a primary the status does not name yet. Fencing every instance does
not depend on the role and stays available.
The note lives on the Cluster, where Radars of other versions read it. Both
readers now skip a format version they do not know instead of reading it
with v1 meaning, and the server reader also skips a note recorded on an
earlier Cluster of the same name, as the client already did.

The integration guide now states the evolution rules a shipped workspace
flag implies: responses only grow, new routes, actions, required
parameters or bound facts need a new flag, and an action error code means
nothing was written.
docs/cnpg.md drops repeated sections, tuning constants, UI breakpoints, copy
strings and symbol names that live in the code, and keeps each rule with its
reason. The API section becomes one row per route with its gate and handler.
Fixes the Actions table that a blank line split, a stale claim that reports
call handlers, and paragraphs filed under the wrong section; adds the
operator-status qualifier, the unfence escape hatch and the Pooler runtime
gate.

INTEGRATION_GUIDE.md files the reviewed-write hook under Actions;
integrations.md drops a function name.
nadaverell added a commit that referenced this pull request Oct 15, 2026
â€Ķ2143)

Moves helpers that several handlers each carried into shared packages.
No behavior change is intended. `prom.NormalizeOrigin` is removed: Radar
Hub does not import `pkg/prom`, so nothing outside this repo calls it.
It is split out of #1922 so the parts that reach every namespaced read
can be reviewed on their own, and so they stay revertable and bisectable
apart from the CloudNativePG work.

## What moved

| New home | Was | Callers |
|---|---|---|
| `internal/readscope.NoNamespaceAccess` | `noNamespaceAccess` in
`server.go` and a copy in `internal/upgrade/evidence.go` | Every
namespaced REST read gate |
| `internal/readscope.SelectNamespaces` | `intersectNamespaces` (search)
and `intersectAllowedNamespaces` (MCP) | Search, MCP, capacity, server
namespace selection |
| `internal/readscope.ListPodsScoped` | `listPodsScoped` in
`node_metrics.go` | Vitals, dashboard, capacity, CNPG workspace |
| `internal/imageutil.ImageTag` | `imageTag` in `applications.go` |
Applications |
| `pkg/urlutil.NormalizeOrigin` | `prom.NormalizeOrigin`, re-wrapped as
`normalizeOrigin` in `internal/server`; both wrappers are gone |
Cross-site origin checks, CORS, Prometheus redirects and connections,
Argo CD and Kubecost connection checks |
| `topology.ManagedByFromMeta` (exported) | `detectManagedByFromMeta` |
Topology |


## Reviewing

Each call-site change is a rename to the shared helper. Semantics to
check are unchanged: a `nil` namespace list means all namespaces, a
non-nil empty list means no access, and `SelectNamespaces` with an empty
request returns the allowed set. `internal/readscope/scope_test.go` pins
those cases.

## Verification

- `go build ./...` (root and `pkg` modules)
- `go test ./internal/readscope/... ./internal/upgrade/...
./internal/mcp/... ./internal/server/...`
- `go test ./urlutil/... ./prom/... ./topology/... ./resourcecontext/...
./subject/...` in `pkg` (`urlutil` gets a table test pinning the origin
key)
- `go test ./internal/app/... ./internal/connections/...`

## Follow-up

#1922 merges `main` after this lands; these files then drop out of its
diff. This replaces the closed #2102.
Conflicts came only from the helper moves this branch also carried. Non-CNPG
files take main's version; the four CNPG server files keep this branch's,
which already call the shared helpers.
Adds two light-mode screenshots to the CloudNativePG section of
integrations.md, which syncs to the public docs, and rewrites its
summary paragraph for readers rather than implementers.
@cursor

cursor Bot commented Oct 15, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

The docs sync copies images into one flat folder by file name, so the
names need to be unique across integrations.
@nadaverell
nadaverell merged commit 0b8d6a1 into main Oct 15, 2026
9 checks passed
@nadaverell
nadaverell deleted the feature/cnpg-actions-runtime branch October 15, 2026 23:01
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.

2 participants