Skip to content

Commit 51675f3

Browse files
committed
feat(platform): add default backupclass
Signed-off-by: Andrey Kolkov <[email protected]>
1 parent d80f67e commit 51675f3

31 files changed

Lines changed: 1810 additions & 32 deletions

‎api/backups/v1alpha1/DESIGN.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ It does **not** implement the backup logic itself.
1818

1919
This document covers only the **core** API and its contracts with drivers, not driver implementations.
2020

21+
### Platform-managed default flow
22+
23+
As of Phase 2, Cozystack also ships **opinionated defaults** for the entire stack:
24+
25+
* A platform-managed S3 bucket `cozy-backups` (provisioned through `apps.cozystack.io/Bucket` in `cozy-backup-controller`).
26+
* Pre-rendered `Strategy` CRs `cozy-default-{cnpg,mariadb,etcd,altinity,foundationdb,velero-vminstance,velero-vmdisk}`.
27+
* A single cluster-wide `BackupClass` `cozy-default` whose `spec.strategies[]` binds each supported `apps.cozystack.io/<Kind>` to the matching strategy.
28+
* A `CredentialsProjector` inside `backupstrategy-controller` that copies the bucket-controller Secret into each tenant namespace as `cozy-backups-creds` on demand (per `BackupJob`/`RestoreJob` reconcile) and into a configured list of system namespaces (e.g. `cozy-velero`) on a periodic tick.
29+
30+
Tenants reference `cozy-default` from `BackupJob`/`Plan`/`RestoreJob`; they never see the bucket controller Secret, never write to a Secret in their own namespace (default RBAC denies it), and never share an S3 path with another tenant (every default strategy template prefixes the object key with `<namespace>/<application>`).
31+
32+
The platform-managed flow is layered *on top of* — not in place of — the core API. Custom `BackupClass`/Strategy CRs targeting other buckets continue to work; tenants picking `cozy-default` simply opt into the platform's pre-wired stack.
33+
2134
---
2235

2336
## 2. Goals and non-goals

‎cmd/backupstrategy-controller/main.go‎

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,19 +174,36 @@ func main() {
174174
os.Exit(1)
175175
}
176176

177+
credentialsConfig := backupcontroller.BackupCredentialsConfig{
178+
SourceNamespace: os.Getenv("BACKUP_STORAGE_SECRET_NAMESPACE"),
179+
SourceSecretName: os.Getenv("BACKUP_STORAGE_SECRET_NAME"),
180+
TargetSecretName: os.Getenv("BACKUP_STORAGE_CREDS_SECRET_NAME"),
181+
Endpoint: os.Getenv("BACKUP_STORAGE_ENDPOINT"),
182+
Region: os.Getenv("BACKUP_STORAGE_REGION"),
183+
}
184+
185+
if systemNamespaces := os.Getenv("BACKUP_STORAGE_SYSTEM_NAMESPACES"); systemNamespaces != "" {
186+
if err := mgr.Add(backupcontroller.NewSystemCredentialsProjector(mgr.GetClient(), credentialsConfig, systemNamespaces, 0)); err != nil {
187+
setupLog.Error(err, "unable to add SystemCredentialsProjector runnable")
188+
os.Exit(1)
189+
}
190+
}
191+
177192
if err = (&backupcontroller.BackupJobReconciler{
178-
Client: mgr.GetClient(),
179-
Scheme: mgr.GetScheme(),
180-
Recorder: mgr.GetEventRecorderFor("backup-controller"),
193+
Client: mgr.GetClient(),
194+
Scheme: mgr.GetScheme(),
195+
Recorder: mgr.GetEventRecorderFor("backup-controller"),
196+
CredentialsConfig: credentialsConfig,
181197
}).SetupWithManager(mgr); err != nil {
182198
setupLog.Error(err, "unable to create controller", "controller", "BackupJob")
183199
os.Exit(1)
184200
}
185201

186202
if err = (&backupcontroller.RestoreJobReconciler{
187-
Client: mgr.GetClient(),
188-
Scheme: mgr.GetScheme(),
189-
Recorder: mgr.GetEventRecorderFor("restore-controller"),
203+
Client: mgr.GetClient(),
204+
Scheme: mgr.GetScheme(),
205+
Recorder: mgr.GetEventRecorderFor("restore-controller"),
206+
CredentialsConfig: credentialsConfig,
190207
}).SetupWithManager(mgr); err != nil {
191208
setupLog.Error(err, "unable to create controller", "controller", "RestoreJob")
192209
os.Exit(1)

‎examples/backups/clickhouse/README.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# ClickHouse backup/restore example
22

3+
> **Heads up — most clusters do not need this walk-through.** Cozystack
4+
> ships a platform-managed `cozy-default` `BackupClass` together with
5+
> the system bucket `cozy-backups`. To use the default flow, set
6+
> `backup.enabled: true` and `backup.useSystemBucket: true` on the
7+
> ClickHouse release; tenants do not provision a Bucket or supply S3
8+
> credentials. See
9+
> [`operations/services/backup-classes`](https://cozystack.io/docs/next/operations/services/backup-classes/)
10+
> for the supported BackupJob / Plan flow against `cozy-default`. The
11+
> walk-through below covers the **legacy** path that wires a per-app
12+
> Bucket, custom strategy CR, and bespoke BackupClass — useful when an
13+
> admin needs a tuned non-default policy.
14+
315
This directory shows how to back up and restore a Cozystack-managed
416
`ClickHouse` application using the cluster's `Altinity` backup strategy
517
driver. The chart materialises an [Altinity `clickhouse-backup`][altinity]

‎examples/backups/etcd/README.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Etcd backup/restore example
22

3+
> **Heads up — most clusters do not need this walk-through.** Cozystack
4+
> ships a platform-managed `cozy-default` `BackupClass` together with
5+
> the system bucket `cozy-backups`. Tenants reference `cozy-default`
6+
> directly from BackupJob / Plan / RestoreJob without provisioning a
7+
> Bucket or supplying S3 credentials. See
8+
> [`operations/services/backup-classes`](https://cozystack.io/docs/next/operations/services/backup-classes/).
9+
> The walk-through below covers the **legacy** path that wires a
10+
> per-app Bucket and bespoke BackupClass — useful for tuned non-default
11+
> policies.
12+
313
This directory shows how to back up and restore a Cozystack-managed `Etcd`
414
application using the cluster's `Etcd` backup strategy driver. The driver
515
delegates to the [etcd-operator][etcd-operator]: each Cozystack `BackupJob`

‎examples/backups/foundationdb/README.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# FoundationDB backup/restore example
22

3+
> **Heads up — most clusters do not need this walk-through.** Cozystack
4+
> ships a platform-managed `cozy-default` `BackupClass` together with
5+
> the system bucket `cozy-backups`. Tenants reference `cozy-default`
6+
> directly from BackupJob / Plan / RestoreJob without provisioning a
7+
> Bucket or supplying S3 credentials. See
8+
> [`operations/services/backup-classes`](https://cozystack.io/docs/next/operations/services/backup-classes/).
9+
> The walk-through below covers the **legacy** path that wires a
10+
> per-app Bucket, `blob_credentials.json` Secret, and bespoke
11+
> BackupClass — useful for tuned non-default policies.
12+
313
This directory shows how to back up and restore a Cozystack-managed
414
`FoundationDB` application using the cluster's `FoundationDB` backup
515
strategy driver. The driver delegates to the [FoundationDB Kubernetes

‎examples/backups/mariadb/README.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# MariaDB backup / restore example
22

3+
> **Heads up — most clusters do not need this walk-through.** Cozystack
4+
> ships a platform-managed `cozy-default` `BackupClass` together with
5+
> the system bucket `cozy-backups`. Tenants reference `cozy-default`
6+
> directly from BackupJob / Plan / RestoreJob without provisioning a
7+
> Bucket or supplying S3 credentials. See
8+
> [`operations/services/backup-classes`](https://cozystack.io/docs/next/operations/services/backup-classes/).
9+
> The walk-through below covers the **legacy** path that wires a
10+
> per-app Bucket and bespoke BackupClass — useful for tuned non-default
11+
> policies (custom retention, encryption, secondary buckets).
12+
313
End-to-end example for backing up and restoring a Cozystack-managed
414
MariaDB application via the `strategy.backups.cozystack.io/v1alpha1`
515
`MariaDB` strategy driver. Files are numbered so that
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#!/usr/bin/env bats
2+
3+
# E2E for the platform-managed cozy-default BackupClass.
4+
#
5+
# Covers the parts of the design where tenants no longer supply S3
6+
# configuration:
7+
# 1. The cozystack-installed cozy-default BackupClass exists and binds
8+
# Postgres -> cozy-default-cnpg without any tenant-side bucket/secret
9+
# setup.
10+
# 2. A Postgres BackupJob referencing cozy-default succeeds and lands a
11+
# Ready Backup artefact under the s3://cozy-backups/<ns>/<app>/ prefix
12+
# enforced by the default strategy template.
13+
# 3. The platform-projected cozy-backups-creds Secret in the tenant
14+
# namespace is not readable by the tenant ServiceAccount — the RBAC
15+
# check anchors the credentials-isolation invariant.
16+
#
17+
# Edge cases (projection retry on missing source Secret, format
18+
# normalisation accessKey -> AWS_ACCESS_KEY_ID) belong in the Go unit
19+
# tests at internal/backupcontroller/credentials_projector_test.go.
20+
21+
TEST_NAMESPACE='tenant-test'
22+
TEST_POSTGRES_NAME='pgapp'
23+
TEST_BACKUPJOB_NAME='pg-default-job'
24+
TEST_PROJECTED_SECRET='cozy-backups-creds'
25+
SYSTEM_BUCKET_NS='cozy-backup-controller'
26+
SYSTEM_BUCKET_NAME='cozy-backups'
27+
28+
setup_file() {
29+
kubectl -n "$TEST_NAMESPACE" delete backupjob.backups.cozystack.io --all --ignore-not-found --timeout=60s
30+
kubectl -n "$TEST_NAMESPACE" delete backup.backups.cozystack.io --all --ignore-not-found --timeout=60s
31+
kubectl -n "$TEST_NAMESPACE" delete postgres.apps.cozystack.io --all --ignore-not-found --timeout=2m
32+
kubectl -n "$TEST_NAMESPACE" delete secret "$TEST_PROJECTED_SECRET" --ignore-not-found --timeout=60s
33+
}
34+
35+
teardown_file() {
36+
setup_file
37+
}
38+
39+
print_log() {
40+
echo "# $1" >&3
41+
}
42+
43+
dump_diagnostics() {
44+
echo "# --- diagnostics ---" >&3
45+
kubectl get backupclass cozy-default -o yaml >&3 2>&1 || true
46+
kubectl -n "$SYSTEM_BUCKET_NS" get bucket,secret >&3 2>&1 || true
47+
kubectl -n "$TEST_NAMESPACE" get postgres,cnpgcluster,backupjob,backup,secret -o wide >&3 2>&1 || true
48+
kubectl -n "$TEST_NAMESPACE" describe backupjob "$TEST_BACKUPJOB_NAME" >&3 2>&1 || true
49+
kubectl -n cozy-backupstrategy-controller logs -l app=backupstrategy-controller --tail=200 >&3 2>&1 || true
50+
}
51+
52+
@test "Platform-managed cozy-default BackupClass and bucket exist" {
53+
kubectl get backupclass cozy-default >/dev/null
54+
kubectl -n "$SYSTEM_BUCKET_NS" get bucket "$SYSTEM_BUCKET_NAME" >/dev/null
55+
KIND_COUNT=$(kubectl get backupclass cozy-default -o jsonpath='{range .spec.strategies[*]}{.application.kind}{"\n"}{end}' | sort -u | wc -l)
56+
[ "$KIND_COUNT" -ge 3 ]
57+
}
58+
59+
@test "Postgres BackupJob via cozy-default Succeeds without tenant S3 config" {
60+
print_log "Apply Postgres app with no chart-level backup.* values"
61+
kubectl apply -f - <<EOF
62+
apiVersion: apps.cozystack.io/v1alpha1
63+
kind: Postgres
64+
metadata:
65+
name: ${TEST_POSTGRES_NAME}
66+
namespace: ${TEST_NAMESPACE}
67+
spec:
68+
replicas: 1
69+
size: 1Gi
70+
resources:
71+
cpu: 100m
72+
memory: 256Mi
73+
backup:
74+
enabled: true
75+
EOF
76+
77+
kubectl -n "$TEST_NAMESPACE" wait hr "${TEST_POSTGRES_NAME}" --for=condition=ready --timeout=600s
78+
79+
print_log "Submit BackupJob referencing cozy-default"
80+
kubectl apply -f - <<EOF
81+
apiVersion: backups.cozystack.io/v1alpha1
82+
kind: BackupJob
83+
metadata:
84+
name: ${TEST_BACKUPJOB_NAME}
85+
namespace: ${TEST_NAMESPACE}
86+
spec:
87+
applicationRef:
88+
apiGroup: apps.cozystack.io
89+
kind: Postgres
90+
name: ${TEST_POSTGRES_NAME}
91+
backupClassName: cozy-default
92+
EOF
93+
94+
print_log "Wait for BackupJob phase=Succeeded"
95+
kubectl -n "$TEST_NAMESPACE" wait backupjob.backups.cozystack.io "$TEST_BACKUPJOB_NAME" \
96+
--for=jsonpath='{.status.phase}'=Succeeded --timeout=1200s || { dump_diagnostics; false; }
97+
98+
print_log "Inspect the projected credentials Secret"
99+
kubectl -n "$TEST_NAMESPACE" get secret "$TEST_PROJECTED_SECRET" >/dev/null
100+
AK=$(kubectl -n "$TEST_NAMESPACE" get secret "$TEST_PROJECTED_SECRET" -o jsonpath='{.data.AWS_ACCESS_KEY_ID}')
101+
SK=$(kubectl -n "$TEST_NAMESPACE" get secret "$TEST_PROJECTED_SECRET" -o jsonpath='{.data.AWS_SECRET_ACCESS_KEY}')
102+
[ -n "$AK" ]
103+
[ -n "$SK" ]
104+
}
105+
106+
@test "Tenant ServiceAccount cannot read the projected credentials Secret" {
107+
# The projection deliberately omits the lineage tenantresource label so
108+
# the Secret is not promoted to a TenantSecret view. The default
109+
# cozy:tenant:base aggregation has zero verbs on core/v1.Secret, so
110+
# `kubectl auth can-i` MUST return no.
111+
RESULT=$(kubectl auth can-i get secret "$TEST_PROJECTED_SECRET" \
112+
--namespace "$TEST_NAMESPACE" \
113+
--as "system:serviceaccount:${TEST_NAMESPACE}:default" 2>/dev/null || true)
114+
[ "$RESULT" = "no" ]
115+
}

0 commit comments

Comments
 (0)