Skip to content

Commit debb9f8

Browse files
authored
feat(platform): add implementation of backup-strategy controller for etcd (#2641)
<!-- Thank you for making a contribution! Here are some tips for you: - Use Conventional Commits for the PR title: `type(scope): description` - Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore - Scopes are not an exhaustive list — pick the most specific scope for the change and extend the list when a genuinely new area appears. Examples: - System components: dashboard, platform, operator, cilium, kube-ovn, linstor, fluxcd, cluster-api - Managed apps: postgres, mariadb, redis, kafka, clickhouse, virtual-machine, kubernetes - Development and maintenance: api, hack, tests, ci, docs, maintenance - Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or add a `BREAKING CHANGE:` footer - If it's a work in progress, consider creating this PR as a draft. - Don't hesistate to ask for opinion and review in the community chats, even if it's still a draft. - Add the label `backport` if it's a bugfix that needs to be backported to a previous version. --> ## What this PR does ### Screenshots <!-- REQUIRED for UI changes: attach screenshots or screen recordings demonstrating the visual impact of your changes. PRs with UI changes without screenshots will not be merged. --> ### Release note <!-- Write a release note: - Explain what has changed internally and for users. - Start with the same `type(scope):` prefix as in the PR title - Follow the guidelines at https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md. --> ```release-note add implementation of backup-strategy controller for etcd ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Cluster-scoped Etcd backup strategy CRD (S3-only) plus controller support for BackupJob (snapshot → Backup) and destructive in-place RestoreJob flows; snapshot metadata persisted and destination validation enforced. * **Documentation** * Complete etcd backup/restore README and admin/tenant scenario guides. * **Examples & Tests** * End-to-end demo scripts (helpers, stepper, cleanup, runner), Bats e2e, and extensive unit tests. * **Chores** * RBAC updates for etcd access, chart version bumped, demo env files added to .gitignore. <!-- review_stack_entry_start --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/cozystack/cozystack/pull/2641?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents 436a918 + d80f67e commit debb9f8

38 files changed

Lines changed: 5687 additions & 85 deletions

‎.gitignore‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,12 @@ tmp/
8484
# build revision marker (generated by make image-packages)
8585
packages/core/platform/.build-revision
8686
.claude/
87+
88+
# Credentials and runtime state minted by the backup/restore demo scripts
89+
# under examples/backups/*/. Files are written by 00-helpers.sh +
90+
# 02-create-bucket.sh + 04-create-backupjob.sh; cleanup.sh deletes them
91+
# on teardown. Keep them out of git so a tenant who runs the demo and
92+
# then `git add .` does not accidentally commit S3 keys.
93+
examples/backups/*/.bucket-info.env
94+
examples/backups/*/.sentinel.env
95+
examples/backups/*/.backup-name.env
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Package v1alpha1 defines strategy.backups.cozystack.io API types.
3+
//
4+
// Group: strategy.backups.cozystack.io
5+
// Version: v1alpha1
6+
package v1alpha1
7+
8+
import (
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/runtime"
11+
)
12+
13+
func init() {
14+
SchemeBuilder.Register(func(s *runtime.Scheme) error {
15+
s.AddKnownTypes(GroupVersion,
16+
&Etcd{},
17+
&EtcdList{},
18+
)
19+
return nil
20+
})
21+
}
22+
23+
const (
24+
EtcdStrategyKind = "Etcd"
25+
)
26+
27+
// +kubebuilder:object:root=true
28+
// +kubebuilder:subresource:status
29+
// +kubebuilder:resource:scope=Cluster
30+
31+
// Etcd defines a backup strategy that delegates execution to the
32+
// etcd-operator (etcd.aenix.io). The strategy carries a templated backup
33+
// destination configuration; the driver materialises one
34+
// etcd.aenix.io/v1alpha1 EtcdBackup per Cozystack BackupJob and surfaces
35+
// it as a Cozystack Backup artefact.
36+
//
37+
// Restore is in-place only: the driver suspends the source Etcd
38+
// HelmRelease, snapshots the live EtcdCluster spec, deletes it, and
39+
// re-creates the EtcdCluster with spec.bootstrap.restore.source
40+
// pointing at the Backup's S3 coordinates, then resumes the HR after
41+
// the cluster reaches Ready.
42+
//
43+
// To-copy restore is NOT supported: RestoreJob.spec.targetApplicationRef
44+
// is a TypedLocalObjectReference (no namespace field), so there is no
45+
// API surface for a cross-namespace restore today. Additionally, the
46+
// chart pins the Helm release name to "etcd"
47+
// (packages/extra/etcd/templates/check-release-name.yaml), so two
48+
// apps.cozystack.io/Etcd applications cannot coexist in one namespace
49+
// regardless of whether the API were extended. Submitting a RestoreJob
50+
// with a non-empty TargetApplicationRef terminates with phase=Failed
51+
// and a message that explains the limitation.
52+
type Etcd struct {
53+
metav1.TypeMeta `json:",inline"`
54+
metav1.ObjectMeta `json:"metadata,omitempty"`
55+
56+
Spec EtcdSpec `json:"spec,omitempty"`
57+
Status EtcdStatus `json:"status,omitempty"`
58+
}
59+
60+
// +kubebuilder:object:root=true
61+
62+
// EtcdList contains a list of Etcd backup strategies.
63+
type EtcdList struct {
64+
metav1.TypeMeta `json:",inline"`
65+
metav1.ListMeta `json:"metadata,omitempty"`
66+
Items []Etcd `json:"items"`
67+
}
68+
69+
// EtcdSpec specifies the desired etcd-operator-driven backup strategy.
70+
type EtcdSpec struct {
71+
// Template carries the templated EtcdBackup destination configuration
72+
// applied per BackupJob (and re-rendered against the same
73+
// .Application / .Parameters at restore time). String fields support
74+
// Helm-style Go templating with two top-level values:
75+
// .Application - the application object (apps.cozystack.io/Etcd)
76+
// .Parameters - the parameters from the matched BackupClassStrategy.
77+
// These values MUST NOT carry credentials; route S3
78+
// access keys through
79+
// Destination.S3.CredentialsSecretRef.
80+
Template EtcdTemplate `json:"template"`
81+
}
82+
83+
// EtcdTemplate describes the templated EtcdBackup-shaped configuration the
84+
// driver renders per BackupJob (and on the restore path stamps onto
85+
// EtcdCluster.spec.bootstrap.restore.source).
86+
type EtcdTemplate struct {
87+
// Destination defines where the etcd snapshot will be stored. Field
88+
// semantics mirror etcd.aenix.io/v1alpha1 EtcdBackup.spec.destination.
89+
Destination EtcdDestinationTemplate `json:"destination"`
90+
}
91+
92+
// EtcdDestinationTemplate mirrors etcd.aenix.io/v1alpha1
93+
// EtcdBackup.spec.destination, RESTRICTED to S3.
94+
//
95+
// The upstream etcd-operator EtcdBackup.spec.destination accepts either
96+
// S3 or PVC, but the operator's filename convention is asymmetric
97+
// between backup and restore on the PVC path:
98+
//
99+
// - backup-agent writes to <mount>/<subPath>/<backupName>.db (with
100+
// subPath) or <mount>/<backupName>.db (empty subPath), and may add
101+
// BACKUP_INCLUDE_REVISION / BACKUP_TIMESTAMP suffixes;
102+
// - restore-agent reads <mount>/<subPath> as a FILE (with subPath) or
103+
// <mount>/snapshot.db (empty subPath).
104+
//
105+
// With either subPath setting, a PVC backup is unrestoreable via the
106+
// strategy: the restore-agent opens the wrong path and 404s/EISDIRs.
107+
// Until the upstream gains symmetric semantics (or surfaces the final
108+
// file path via the equivalent of BackupSnapshot.URI for PVC), this
109+
// driver intentionally narrows the API contract to S3 only. The S3
110+
// path is fully exercised end-to-end on dev7 and covered by unit +
111+
// e2e tests.
112+
// +kubebuilder:validation:XValidation:rule="has(self.s3)",message="s3 destination is required (PVC is intentionally not supported by this strategy until upstream restore-agent reads the same path the backup-agent writes; see the strategy comments)"
113+
type EtcdDestinationTemplate struct {
114+
// S3 configures an S3-compatible storage target. Templating is
115+
// supported on every string field (Bucket, Endpoint, Key, Region,
116+
// CredentialsSecretRef.Name).
117+
S3 *EtcdS3Template `json:"s3"`
118+
}
119+
120+
// EtcdS3Template is a typed, kubebuilder-validated mirror of
121+
// etcd.aenix.io/v1alpha1 EtcdBackup.spec.destination.s3. Restores reuse
122+
// the same shape: the driver re-renders the strategy and stamps the
123+
// resulting block onto EtcdCluster.spec.bootstrap.restore.source.s3 of the
124+
// target Etcd application.
125+
type EtcdS3Template struct {
126+
// Bucket is the S3 (or compatible) bucket name. Templating is
127+
// supported.
128+
// +kubebuilder:validation:MinLength=1
129+
Bucket string `json:"bucket"`
130+
131+
// Endpoint is the S3-compatible endpoint URL, including scheme (e.g.
132+
// "https://s3.amazonaws.com" or
133+
// "http://seaweedfs-s3.tenant-root.svc:8333"). Templating is
134+
// supported.
135+
// +kubebuilder:validation:MinLength=1
136+
Endpoint string `json:"endpoint"`
137+
138+
// Key is the key prefix (directory path) within the bucket. The
139+
// etcd-operator appends the snapshot filename automatically.
140+
// Templating is supported.
141+
// +optional
142+
Key string `json:"key,omitempty"`
143+
144+
// Region is the AWS region for the S3 bucket. Templating is
145+
// supported.
146+
// +optional
147+
Region string `json:"region,omitempty"`
148+
149+
// ForcePathStyle forces path-style S3 URLs (e.g., endpoint/bucket/key)
150+
// instead of virtual-hosted-style (e.g., bucket.endpoint/key). Most
151+
// S3-compatible providers (MinIO, Ceph, seaweedfs-s3) require path
152+
// style.
153+
// +optional
154+
ForcePathStyle *bool `json:"forcePathStyle,omitempty"`
155+
156+
// CredentialsSecretRef references a Secret containing
157+
// AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY keys. The Secret must
158+
// live in the application's namespace. Templating is supported on
159+
// Name (typical pattern:
160+
// "{{ .Application.metadata.name }}-etcd-backup-creds").
161+
CredentialsSecretRef EtcdLocalObjectReference `json:"credentialsSecretRef"`
162+
}
163+
164+
// EtcdLocalObjectReference is a minimal local Secret reference. The driver
165+
// looks the Secret up in the application namespace.
166+
type EtcdLocalObjectReference struct {
167+
// Name is the Secret name. Templating is supported.
168+
// +kubebuilder:validation:MinLength=1
169+
Name string `json:"name"`
170+
}
171+
172+
// EtcdStatus reports observed state for the strategy CR. Driver
173+
// controllers surface diagnostic conditions here (e.g. validation issues).
174+
type EtcdStatus struct {
175+
// Conditions holds the latest available observations.
176+
// +optional
177+
// +listType=map
178+
// +listMapKey=type
179+
Conditions []metav1.Condition `json:"conditions,omitempty"`
180+
}

‎api/backups/strategy/v1alpha1/zz_generated.deepcopy.go‎

Lines changed: 169 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎cmd/backupstrategy-controller/main.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import (
4141
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
4242
"github.com/cozystack/cozystack/internal/backupcontroller"
4343
"github.com/cozystack/cozystack/internal/backupcontroller/cnpgtypes"
44+
"github.com/cozystack/cozystack/internal/backupcontroller/etcdapp"
45+
"github.com/cozystack/cozystack/internal/backupcontroller/etcdtypes"
4446
"github.com/cozystack/cozystack/internal/backupcontroller/foundationdbapp"
4547
"github.com/cozystack/cozystack/internal/backupcontroller/foundationdbtypes"
4648
"github.com/cozystack/cozystack/internal/backupcontroller/mariadbapp"
@@ -67,6 +69,8 @@ func init() {
6769
utilruntime.Must(mariadbapp.AddToScheme(scheme))
6870
utilruntime.Must(foundationdbtypes.AddToScheme(scheme))
6971
utilruntime.Must(foundationdbapp.AddToScheme(scheme))
72+
utilruntime.Must(etcdtypes.AddToScheme(scheme))
73+
utilruntime.Must(etcdapp.AddToScheme(scheme))
7074
// +kubebuilder:scaffold:scheme
7175
}
7276

0 commit comments

Comments
 (0)