Repository navigation
feat: surface backup state for managed applications (backend + console) #2609
Description
Summary
The backups.cozystack.io/v1alpha1.Backup resource is already produced by the CNPGStrategy (Postgres) and AltinityStrategy (ClickHouse) controllers and persisted per tenant namespace, but a tenant has no first-class way to discover or read this state today: the managed-application CR does not carry a backup summary, no field-selector contract is documented, and the console has no surface to render backups on. This issue tracks closing that gap end-to-end across the platform and the console, as a follow-up surface track for the backups epic #1713 — which covers backup production, not visibility.
Updated 2026-08-03. Two things in this issue have gone stale since filing: the console is no longer a separate repository (cozystack/cozystack-ui is archived — the console was vendored into packages/system/dashboard/images/console/, so section D is a cozystack PR, not a cross-repo one), and part of section D has since landed. Corrected in place below.
Motivation
A tenant has no built-in way to answer questions like "when was my managed Postgres last backed up successfully", "what recoverability point is available right now", "did the last scheduled backup fail and why", or "which backups exist that I can restore from". The underlying data already exists — in Backup CRs, in postgresql.cnpg.io/v1.Cluster.status, in CNPG Prometheus metrics — but it is scattered across resource kinds and is not surfaced on the CR that the tenant actually interacts with. Cozystack targets low-skill operators, so dropping into kubectl get backups.postgresql.cnpg.io or scraping Prometheus is not an acceptable read path.
The console is a pure SPA against the Kubernetes API with no BFF, so the aggregation must live on the platform side; the console then renders whatever the CR exposes.
Current state
Already implemented:
BackupCR with+kubebuilder:selectablefieldonspec.applicationRef.{apiGroup,kind,name}(api/backups/v1alpha1/backup_types.go:112-114) — server-side filtered listing is wire-ready but undocumented.CNPGStrategycontroller mirrorscnpg.io/Backup→ cozystackBackupand records driver metadata such ascnpg.io/backup-nameandcnpg.io/destination-path(internal/backupcontroller/cnpgstrategy_controller.go).AltinityStrategycontroller does the equivalent for ClickHouse (internal/backupcontroller/altinitystrategy_controller.go).BackupJobandRestoreJobaction CRs exist for the write path.- Console application detail page already has a tab system (Overview, Workloads, Services, Ingresses, Secrets, Events, VNC) ready to host one more tab (
packages/system/dashboard/images/console/apps/console/src/routes/detail/ApplicationDetailPage.tsx). - A tenant-scoped Backups list surface now exists —
routes/BackupResourceListPage.tsxplus the sidebar section listing Plans / Backup Jobs / Backups / Restore Jobs (sidebar-sections.tsx). This is the flat namespace-wide list only; it is not the per-application tab this issue asks for. - The tab allow-list already exists as an enum on the CRD (
cozystack.io_applicationdefinitions.yaml,dashboard.tabs, currentlyworkloads|ingresses|services|secrets|yaml), which settles the mechanism debated in open question 5 below — it needs abackupsvalue, not a new field.
Missing:
- No backup summary on managed-app
status—kubectl get postgres fooreveals nothing about backups. - No documented field-selector contract for clients.
- No convenience labels on
BackupCRs pointing at the parent application (only theapplicationReffield, which is awkward for label selectors and dashboards). - No printer columns on
Backupforkubectl get backups. - No per-application console surface — no Backups tab on the application detail page, no backup widget on Overview, no backup-health chip on the app card. (The flat namespace-wide list landed, see above.)
Proposed scope
A. Status enrichment on managed-app CR (platform)
Add a uniform status.backup sub-block to every managed-application CR that supports backups (Postgres, ClickHouse, future drivers). Proposed schema, identical across strategies so the console can render one widget:
status:
backup:
health: Healthy | Degraded | Failed | Unknown
lastSuccessfulAt: <RFC3339>
lastSuccessfulName: <Backup CR name>
lastFailedAt: <RFC3339>
lastError: <truncated message>
firstRecoverabilityPoint: <RFC3339> # null when the driver does not report it
nextScheduledAt: <RFC3339> # populated only if a Plan targets this app
totalBackups: <int>
scheduleEnabled: <bool>The source of truth is the list of Backup CRs filtered by spec.applicationRef. Driver-specific fields not present on the cozystack Backup (notably firstRecoverabilityPoint from cnpg.io/Cluster.status) are mirrored from the driver-native resource by the same controller that writes the summary.
Decision: a single backup-status aggregator controller, not per-strategy writes. The aggregator watches Backup and managed-app CRs and is the sole writer of <App>.status.backup. This avoids divergence between CNPG and Altinity output and keeps strategy controllers focused on their write path. Open for objection.
B. Field-selector contract and labels (platform)
Document the selectable-fields contract for Backup in api/backups/v1alpha1/DESIGN.md so clients can rely on it. In addition to the existing field selectors, strategy controllers set convenience labels on every Backup they create:
backups.cozystack.io/application-name=<name>backups.cozystack.io/application-kind=<kind>backups.cozystack.io/strategy=<strategy-name>
Field selectors are the canonical API; labels make kubectl get backups -l ... and third-party dashboards work without learning the field-selector path.
C. Printer columns (platform)
Add +kubebuilder:printcolumn annotations to Backup (Phase, Application, TakenAt, Age, Size) and to managed-app CRs (LAST BACKUP, BACKUP HEALTH) so kubectl get is useful out of the box.
D. Console — Backups tab and backup widget (in-tree)
Work under packages/system/dashboard/images/console/, all of it driven by data the platform exposes after A–C land:
- New
BackupsTab.tsxunderapps/console/src/routes/detail/, registered in the existing tab array inApplicationDetailPage.tsx. UsesuseK8sList<Backup>with the documented field selectors. Table columns: TakenAt, Phase (status badge), Strategy, Plan, Artifact URI with copy, Size, Age, Error (truncated and expandable). Sorted by TakenAt descending. Empty state with a hint when no backups exist. - Tab visibility — the tab appears for applications whose
ApplicationDefinition.spec.dashboard.tabsincludesbackups. That enum already exists and is the natural hint, so no new field is needed; the alternative auto-detect approach stays rejected to avoid first-load flicker. OverviewTab.tsxgets a compact backup widget that readsinstance.status.backupand shows health, last successful backup (relative + absolute), first recoverability point, next scheduled, and a link to the Backups tab. Hidden silently whenstatus.backupis absent so older platform versions or apps without backups render unchanged.AppCard.tsxgains an inline warning chip whenstatus.backup.healthisFailedorDegraded.HealthyandUnknownstay unmarked to avoid visual noise.- The console's shared types are extended with the
Backuptype and the newstatus.backupshape.
E. Documentation
Update api/backups/v1alpha1/DESIGN.md with the read path and the field-selector contract. Add a "Reading backup state" section to packages/apps/postgres/README.md (and the ClickHouse equivalent) explaining what kubectl get postgres foo and kubectl get backups -l ... return. A docs-site PR in cozystack/website cross-links these from the user-facing manual.
Out of scope
- Restore UX in the console (creating
RestoreJobinteractively) — obvious follow-up, reserve the route but do not implement. Note aBackupCreatePageroute is already registered and reachable by URL though nothing links to it (see Dashboard shows Add button for Backups page for cluster-admin, leads to blank form #2165); decide whether to drop or reuse it. - Triggering ad-hoc backups from the console — same.
- Plan and BackupClass management UI — same.
- Backup size / dedupe ratio sourced from Barman cloud metadata or Prometheus — separate ticket once a delivery path is chosen.
- PVC / ZFS snapshot backup mechanics — covered by epic Backups for managed apps #1713.
Acceptance criteria
kubectl get postgres <name> -o jsonpath='{.status.backup}'returns a populated summary on a cluster with at least one completed backup; the same for ClickHouse.kubectl get backups -n <tenant> --field-selector spec.applicationRef.name=<app>,spec.applicationRef.kind=Postgresreturns only matching backups, and the equivalentkubectl get backups -l backups.cozystack.io/application-name=<app>works.kubectl get backups -n <tenant>shows useful printer columns out of the box.- The aggregator controller is covered by unit tests with success-only, failure-only, and mixed
Backuphistories, including the "no backups yet" case. - Helm-unittest fixtures are updated for every chart whose schema changes.
DESIGN.mddocuments the read path and the field-selector contract.- In the console, the Backups tab renders correctly for a Postgres instance with backups, with an empty state for one without, and is hidden for applications whose
ApplicationDefinition.spec.dashboard.tabsomitsbackups. - The Overview backup widget appears when
status.backupis populated and is silently absent otherwise; the app card warns onFailed/Degradedhealth only. - Console typecheck and lint pass; no React hooks-rule warnings.
Open questions
- Aggregator controller vs per-strategy writes — preference?
- Should
nextScheduledAtbe sourced from cozystackPlanonly, or also fromcnpg.io/ScheduledBackupif a user creates one outside the cozystack framework? firstRecoverabilityPointshape — hide the field when the driver does not provide it, or always present with a null value?- Feature gate or default-on?
Tab visibility — confirm theAnswered: theApplicationDefinition.spec.dashboard.featureshint as the mechanism, or pick something else?dashboard.tabsenum already exists on the ApplicationDefinition CRD, so the decision is just to add abackupsvalue to it.
Related
- Parent epic Backups for managed apps #1713 (backup production track)
- Design doc:
api/backups/v1alpha1/DESIGN.md - Console: in-tree at
packages/system/dashboard/images/console/(cozystack/cozystack-uiis archived)
Activity
androndo commentedon Aug 3, 2026
Refreshed the body after auditing every open backup issue against
main(04b742670). Sections A, B, C and E are untouched — they are entirely open, and I verified that: nostatus.backupblock or aggregator controller exists,Backupstill carries only the threeselectablefieldmarkers with noprintcolumn, no convenience labels are written anywhere, and the field-selector contract is still undocumented.What I corrected:
cozystack/cozystack-uiis archived; the console was vendored intopackages/system/dashboard/images/console/, so section D is a cozystack PR and its file paths were wrong. Retargeted them.routes/BackupResourceListPage.tsxplus the sidebar section for Plans / Backup Jobs / Backups / Restore Jobs). That is the flat namespace-wide list only — no per-application tab, no Overview widget, no app-card chip — so I narrowed the "missing" bullet rather than removing it.ApplicationDefinition.spec.dashboard.tabsis an enum on the CRD (workloads|ingresses|services|secrets|yaml). Tab visibility needs abackupsvalue added to it, not the newdashboard.featuresfield this issue proposed. Struck the question and updated section D accordingly.backup.*schema entirely, so nothing here depends on it.BackupCreatePageroute (reachable by URL, nothing links to it) next to the out-of-scope restore UX, since whoever builds this tab should decide whether to drop or reuse it — see Dashboard shows Add button for Backups page for cluster-admin, leads to blank form #2165.