88 common "github.com/openshift/hypershift-oadp-plugin/pkg/common"
99 plugtypes "github.com/openshift/hypershift-oadp-plugin/pkg/core/types"
1010 validation "github.com/openshift/hypershift-oadp-plugin/pkg/core/validation"
11+ "github.com/openshift/hypershift-oadp-plugin/pkg/etcdbackup"
1112 "github.com/openshift/hypershift-oadp-plugin/pkg/platform/agent"
1213 hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
1314 "github.com/sirupsen/logrus"
@@ -31,6 +32,11 @@ type BackupPlugin struct {
3132 validator validation.BackupValidator
3233 hcp * hyperv1.HostedControlPlane
3334 * plugtypes.BackupOptions
35+
36+ // Etcd backup orchestration
37+ etcdOrchestrator * etcdbackup.Orchestrator
38+ hoNamespace string
39+ etcdBackupMethod string
3440}
3541
3642// NewBackupPlugin instantiates BackupPlugin.
@@ -71,12 +77,27 @@ func NewBackupPlugin(logger logrus.FieldLogger) (*BackupPlugin, error) {
7177 Client : client ,
7278 }
7379
80+ hoNamespace := common .DefaultHONamespace
81+ if v , ok := pluginConfig .Data [common .ConfigKeyHONamespace ]; ok && v != "" {
82+ hoNamespace = v
83+ }
84+
85+ etcdBackupMethod := common .EtcdBackupMethodVolume
86+ if v , ok := pluginConfig .Data [common .ConfigKeyEtcdBackupMethod ]; ok && v != "" {
87+ etcdBackupMethod = v
88+ }
89+ if etcdBackupMethod != common .EtcdBackupMethodVolume && etcdBackupMethod != common .EtcdBackupMethodEtcdSnapshot {
90+ return nil , fmt .Errorf ("invalid etcdBackupMethod %q: must be %q or %q" , etcdBackupMethod , common .EtcdBackupMethodVolume , common .EtcdBackupMethodEtcdSnapshot )
91+ }
92+
7493 bp := & BackupPlugin {
75- log : logger ,
76- client : client ,
77- config : pluginConfig .Data ,
78- ctx : ctx ,
79- validator : validator ,
94+ log : logger ,
95+ client : client ,
96+ config : pluginConfig .Data ,
97+ ctx : ctx ,
98+ validator : validator ,
99+ hoNamespace : hoNamespace ,
100+ etcdBackupMethod : etcdBackupMethod ,
80101 }
81102
82103 if bp .BackupOptions , err = bp .validator .ValidatePluginConfig (bp .config ); err != nil {
@@ -119,7 +140,14 @@ func (p *BackupPlugin) Execute(item runtime.Unstructured, backup *velerov1.Backu
119140 }
120141 return nil , nil , fmt .Errorf ("error getting HCP namespace: %v" , err )
121142 }
143+ }
122144
145+ // Etcd backup: create HCPEtcdBackup CR as early as possible (once).
146+ // Only when etcdBackupMethod is "etcdSnapshot".
147+ if p .etcdBackupMethod == common .EtcdBackupMethodEtcdSnapshot {
148+ if err := p .createEtcdBackup (ctx , backup ); err != nil {
149+ return nil , nil , fmt .Errorf ("error creating HCPEtcdBackup: %v" , err )
150+ }
123151 }
124152
125153 kind := item .GetObjectKind ().GroupVersionKind ().Kind
@@ -134,6 +162,11 @@ func (p *BackupPlugin) Execute(item runtime.Unstructured, backup *velerov1.Backu
134162 return nil , nil , fmt .Errorf ("error checking platform configuration: %v" , err )
135163 }
136164
165+ // Etcd backup: wait for completion
166+ if err := p .waitForEtcdBackupCompletion (ctx ); err != nil {
167+ return nil , nil , err
168+ }
169+
137170 case kind == common .HostedClusterKind :
138171 metadata , err := meta .Accessor (item )
139172 if err != nil {
@@ -142,16 +175,28 @@ func (p *BackupPlugin) Execute(item runtime.Unstructured, backup *velerov1.Backu
142175 common .AddAnnotation (metadata , common .HostedClusterRestoredFromBackupAnnotation , "" )
143176 p .log .Infof ("Added restore annotation to HostedCluster %s" , metadata .GetName ())
144177
178+ // Etcd backup: wait for completion
179+ if err := p .waitForEtcdBackupCompletion (ctx ); err != nil {
180+ return nil , nil , err
181+ }
182+
145183 case kind == "Pod" :
146- // In case of FSBackup, we need to add the label to the pod
147- if backup .Spec .DefaultVolumesToFsBackup != nil && ! * backup .Spec .DefaultVolumesToFsBackup {
148- metadata , err := meta .Accessor (item )
149- if err != nil {
150- return nil , nil , fmt .Errorf ("error getting metadata accessor: %v" , err )
151- }
184+ metadata , err := meta .Accessor (item )
185+ if err != nil {
186+ return nil , nil , fmt .Errorf ("error getting metadata accessor: %v" , err )
187+ }
152188
153- if strings .Contains (metadata .GetName (), "etcd-" ) {
154- common .AddLabel (metadata , common .FSBackupLabelName , "true" )
189+ if strings .Contains (metadata .GetName (), "etcd-" ) {
190+ switch p .etcdBackupMethod {
191+ case common .EtcdBackupMethodEtcdSnapshot :
192+ // Skip etcd pods entirely, snapshot is handled by HCPEtcdBackup.
193+ // This prevents both FSBackup and CSI VolumeSnapshots of etcd volumes.
194+ p .log .Infof ("Skipping etcd pod %s from backup (using etcdSnapshot method)" , metadata .GetName ())
195+ return nil , nil , nil
196+ case common .EtcdBackupMethodVolume :
197+ if backup .Spec .DefaultVolumesToFsBackup != nil && ! * backup .Spec .DefaultVolumesToFsBackup {
198+ common .AddLabel (metadata , common .FSBackupLabelName , "true" )
199+ }
155200 }
156201 }
157202
@@ -172,7 +217,77 @@ func (p *BackupPlugin) Execute(item runtime.Unstructured, backup *velerov1.Backu
172217 if _ , exists := labels [common .KubevirtRHCOSLabel ]; exists {
173218 return nil , nil , nil
174219 }
220+
221+ // Exclude etcd data PVCs when using etcdSnapshot method.
222+ // PVC names follow the StatefulSet pattern: data-etcd-{index}
223+ if kind == common .PersistentVolumeClaimKind &&
224+ strings .HasPrefix (metadata .GetName (), common .EtcdPVCPrefix ) &&
225+ p .etcdBackupMethod == common .EtcdBackupMethodEtcdSnapshot {
226+ p .log .Infof ("Excluding etcd PVC %s from backup (using etcdSnapshot method)" , metadata .GetName ())
227+ return nil , nil , nil
228+ }
175229 }
176230
177231 return item , nil , nil
178232}
233+
234+ // createEtcdBackup creates an HCPEtcdBackup CR in the HCP namespace.
235+ // It is idempotent: if the orchestrator already created a backup, it returns immediately.
236+ // Requires the HCPEtcdBackup CRD to exist in the cluster (safenet check).
237+ func (p * BackupPlugin ) createEtcdBackup (ctx context.Context , backup * velerov1.Backup ) error {
238+ // Already created by a previous Execute() call
239+ if p .etcdOrchestrator != nil && p .etcdOrchestrator .IsCreated () {
240+ return nil
241+ }
242+
243+ crdExists , err := common .CRDExists (ctx , "hcpetcdbackups.hypershift.openshift.io" , p .client )
244+ if err != nil {
245+ return fmt .Errorf ("failed to check for HCPEtcdBackup CRD: %w" , err )
246+ }
247+ if ! crdExists {
248+ return fmt .Errorf ("etcdBackupMethod is %q but HCPEtcdBackup CRD not found in the cluster" , common .EtcdBackupMethodEtcdSnapshot )
249+ }
250+
251+ oadpNS , err := common .GetCurrentNamespace ()
252+ if err != nil {
253+ return fmt .Errorf ("failed to get OADP namespace: %w" , err )
254+ }
255+
256+ p .etcdOrchestrator = etcdbackup .NewOrchestrator (p .log , p .client , p .hoNamespace , oadpNS )
257+
258+ // Fetch the HostedCluster for encryption config
259+ hc , err := common .GetHostedCluster (ctx , p .client , backup .Spec .IncludedNamespaces , p .hcp .Namespace )
260+ if err != nil {
261+ p .log .Warnf ("Could not find HostedCluster for encryption config: %v" , err )
262+ }
263+
264+ if err := p .etcdOrchestrator .CreateEtcdBackup (ctx , backup , p .hcp .Namespace , hc ); err != nil {
265+ return err
266+ }
267+
268+ if err := p .etcdOrchestrator .VerifyInProgress (ctx ); err != nil {
269+ return err
270+ }
271+
272+ return nil
273+ }
274+
275+ // waitForEtcdBackupCompletion waits for the HCPEtcdBackup to finish and cleans up
276+ // the copied credential Secret. It is a no-op if no etcd backup was created.
277+ func (p * BackupPlugin ) waitForEtcdBackupCompletion (ctx context.Context ) error {
278+ if p .etcdOrchestrator == nil || ! p .etcdOrchestrator .IsCreated () {
279+ return nil
280+ }
281+
282+ snapshotURL , err := p .etcdOrchestrator .WaitForCompletion (ctx )
283+ if err != nil {
284+ return fmt .Errorf ("HCPEtcdBackup failed: %v" , err )
285+ }
286+ p .log .Infof ("HCPEtcdBackup completed, snapshotURL: %s" , snapshotURL )
287+
288+ if cleanupErr := p .etcdOrchestrator .CleanupCredentialSecret (ctx ); cleanupErr != nil {
289+ p .log .Warnf ("Failed to cleanup etcd backup credential Secret: %v" , cleanupErr )
290+ }
291+
292+ return nil
293+ }
0 commit comments