Skip to content

Commit 1a06ca2

Browse files
jparrillclaude
andauthored
CNTRLPLANE-2685, CNTRLPLANE-2707: integrate HCPEtcdBackup lifecycle into OADP backup flow (#238)
* fix(deps): update hypershift API to latest main Update github.com/openshift/hypershift/api to v0.0.0-20260406110001-bcf6adaf131f. This brings in the HCPEtcdBackup CRD types, HCPEtcdBackupConfig in ManagedEtcdSpec.Backup, and related condition/reason constants needed for CNTRLPLANE-2685 integration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Juan Manuel Parrilla Madrid <jparrill@redhat.com> * feat(backup): integrate HCPEtcdBackup lifecycle into OADP backup flow Add etcdSnapshot backup method that creates and monitors HCPEtcdBackup CRs during Velero backup. When etcdBackupMethod=etcdSnapshot is configured in the plugin ConfigMap, the plugin: - Creates an HCPEtcdBackup CR in the HCP namespace using BSL storage config - Copies BSL credentials to the HO namespace (remapping key for controller) - Polls the CR until backup completes or fails - Excludes etcd pods and PVCs from Velero backup (no CSI/FS backup needed) - Stores the etcd snapshot alongside the Velero backup data in the BSL The default method remains volumeSnapshot (unchanged behavior). Also cleans up dead config parameters (readoptNodes, managedServices, awsRegenPrivateLink) and registers apiextensionsv1 in the scheme for CRD existence checks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Juan Manuel Parrilla Madrid <jparrill@redhat.com> * docs: add HCPEtcdBackup implementation reference Document the full HCPEtcdBackup integration including architecture, backup/restore flows, configuration, credential handling, storage layout, dependency chain (PRs #8139, #8010, #8017, #8040, #8114, enhancement #1945), and troubleshooting guide. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Juan Manuel Parrilla Madrid <jparrill@redhat.com> * test: add comprehensive unit tests for etcd backup orchestrator and validation Cover Execute paths for all item kinds, orchestrator lifecycle (CreateEtcdBackup, VerifyInProgress, WaitForCompletion), and platform validation for both backup and restore validators. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Juan Manuel Parrilla Madrid <jparrill@redhat.com> --------- Signed-off-by: Juan Manuel Parrilla Madrid <jparrill@redhat.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8d7c320 commit 1a06ca2

41 files changed

Lines changed: 4546 additions & 461 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/references/HCPEtcdBackup/HCPEtcdBackup-implementation.md

Lines changed: 533 additions & 0 deletions
Large diffs are not rendered by default.

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ require (
88
github.com/kubernetes-csi/external-snapshotter/client/v8 v8.4.0
99
github.com/onsi/gomega v1.39.1
1010
github.com/openshift/hive/apis v0.0.0-20241220022629-3f49f26197ff
11-
github.com/openshift/hypershift/api v0.0.0-20260317154635-8eaac177f1b0
11+
github.com/openshift/hypershift/api v0.0.0-20260410203959-783f7956d4f9
1212
github.com/sirupsen/logrus v1.9.4
1313
github.com/vmware-tanzu/velero v1.14.0
1414
k8s.io/api v0.35.3
@@ -26,7 +26,7 @@ require (
2626
github.com/google/gnostic-models v0.7.0 // indirect
2727
github.com/google/go-cmp v0.7.0 // indirect
2828
github.com/google/uuid v1.6.0 // indirect
29-
github.com/openshift/api v0.0.0-20260120150926-4c643a652d54 // indirect
29+
github.com/openshift/api v0.0.0-20260304122341-cf5d8996109f // indirect
3030
github.com/openshift/custom-resource-status v1.1.3-0.20220503160415-f2fdb4999d87 // indirect
3131
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
3232
github.com/prometheus/client_golang v1.23.2 // indirect

go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,23 +192,23 @@ github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9k
192192
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
193193
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
194194
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
195-
github.com/onsi/ginkgo/v2 v2.28.0 h1:Rrf+lVLmtlBIKv6KrIGJCjyY8N36vDVcutbGJkyqjJc=
196-
github.com/onsi/ginkgo/v2 v2.28.0/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
195+
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
196+
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
197197
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
198198
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
199199
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
200200
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
201201
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
202202
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
203203
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
204-
github.com/openshift/api v0.0.0-20260120150926-4c643a652d54 h1:Gm81lfkiXFgg/N0x90WsplERGF2NqYjK0vd8YY/aFpU=
205-
github.com/openshift/api v0.0.0-20260120150926-4c643a652d54/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY=
204+
github.com/openshift/api v0.0.0-20260304122341-cf5d8996109f h1:M8y0oBq/KRkuSNFlUMQRAn2MrXJh1mzTCFgbLpPWQbM=
205+
github.com/openshift/api v0.0.0-20260304122341-cf5d8996109f/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY=
206206
github.com/openshift/custom-resource-status v1.1.3-0.20220503160415-f2fdb4999d87 h1:cHyxR+Y8rAMT6m1jQCaYGRwikqahI0OjjUDhFNf3ySQ=
207207
github.com/openshift/custom-resource-status v1.1.3-0.20220503160415-f2fdb4999d87/go.mod h1:DB/Mf2oTeiAmVVX1gN+NEqweonAPY0TKUwADizj8+ZA=
208208
github.com/openshift/hive/apis v0.0.0-20241220022629-3f49f26197ff h1:6C1z4xMAruyeiTFGqahxNDpI1cXPCjpaFeIeIodty08=
209209
github.com/openshift/hive/apis v0.0.0-20241220022629-3f49f26197ff/go.mod h1:1vBNCcWNpQyFCz83PWYT/lHUFJ9ost2t5FijHElh6gQ=
210-
github.com/openshift/hypershift/api v0.0.0-20260317154635-8eaac177f1b0 h1:x5DgHyFXF9zpgTH4JYDpBhQnASEIj6z1WXdeHl1DGAI=
211-
github.com/openshift/hypershift/api v0.0.0-20260317154635-8eaac177f1b0/go.mod h1:eYDwJzXCU+0HO9DdvhBAA143z7woIJ5dV71TaJXIkgk=
210+
github.com/openshift/hypershift/api v0.0.0-20260410203959-783f7956d4f9 h1:mVzLud8ewZi1W8dnV37L+eY9IJsoFfpkASgPvDRM61Q=
211+
github.com/openshift/hypershift/api v0.0.0-20260410203959-783f7956d4f9/go.mod h1:mC9+bqb81FmG924VOO+7P0ofKQgvY1SdPhFp0oJ9U44=
212212
github.com/openshift/velero v0.10.2-0.20260323170432-5ef912f438f6 h1:nWv9+tEi34VMFMOziVZLOKvn3yXDqk68ODXlzpYU2S0=
213213
github.com/openshift/velero v0.10.2-0.20260323170432-5ef912f438f6/go.mod h1:YKHPM2FpS+5WrcciMi5j4bo/JMnXXrR1M+j1DoTA26U=
214214
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=

pkg/common/scheme.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
veleroapiv1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
88
veleroapiv2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1"
99
corev1 "k8s.io/api/core/v1"
10+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
1011
"k8s.io/apimachinery/pkg/runtime"
1112
)
1213

@@ -35,6 +36,9 @@ func init() {
3536
if err := hive.AddToScheme(CustomScheme); err != nil {
3637
errs = append(errs, err)
3738
}
39+
if err := apiextensionsv1.AddToScheme(CustomScheme); err != nil {
40+
errs = append(errs, err)
41+
}
3842

3943
if len(errs) > 0 {
4044
panic(errs)

pkg/common/types.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ const (
1616

1717
// Integration with Hypershift, more info here: https://github.com/openshift/hypershift/pull/6195
1818
HostedClusterRestoredFromBackupAnnotation string = "hypershift.openshift.io/restored-from-backup"
19+
// Etcd snapshot URL annotation: set during backup so the restore plugin can read it
20+
// (Velero strips status from items during restore, so we persist it as an annotation)
21+
EtcdSnapshotURLAnnotation string = "hypershift.openshift.io/etcd-snapshot-url"
1922

2023
// hypershift/cluster-api kinds
2124
HostedClusterKind string = "HostedCluster"
@@ -25,6 +28,25 @@ const (
2528
PersistentVolumeClaimKind string = "PersistentVolumeClaim"
2629
ClusterDeploymentKind string = "ClusterDeployment"
2730
DataVolumeKind string = "DataVolume"
31+
HCPEtcdBackupKind string = "HCPEtcdBackup"
32+
33+
// Default HyperShift Operator namespace
34+
DefaultHONamespace string = "hypershift"
35+
// ConfigMap key to override the HO namespace
36+
ConfigKeyHONamespace string = "hoNamespace"
37+
38+
// Etcd backup method configuration
39+
ConfigKeyEtcdBackupMethod string = "etcdBackupMethod"
40+
EtcdBackupMethodVolume string = "volumeSnapshot"
41+
EtcdBackupMethodEtcdSnapshot string = "etcdSnapshot"
42+
43+
// Velero annotation to exclude specific volumes from backup
44+
BackupVolumesExcludesAnnotation string = "backup.velero.io/backup-volumes-excludes"
45+
// Etcd data volume name in the StatefulSet pod
46+
EtcdDataVolumeName string = "data"
47+
// Etcd PVC name prefix (StatefulSet pattern: {volumeName}-{stsName}-{index})
48+
EtcdPVCPrefix string = "data-etcd-"
49+
2850
)
2951

3052
var (

pkg/common/utils.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package common
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"os"
78
"path/filepath"
@@ -163,6 +164,32 @@ func GetHCPNamespace(name, namespace string) string {
163164
return fmt.Sprintf("%s-%s", namespace, name)
164165
}
165166

167+
// GetHostedCluster finds the HostedCluster that owns the HCP by deriving
168+
// its namespace and name from the HCP namespace convention: {hc-namespace}-{hc-name}.
169+
func GetHostedCluster(ctx context.Context, c crclient.Client, includedNamespaces []string, hcpNamespace string) (*hyperv1.HostedCluster, error) {
170+
var errs []error
171+
for _, ns := range includedNamespaces {
172+
if ns == hcpNamespace {
173+
continue
174+
}
175+
hcList := &hyperv1.HostedClusterList{}
176+
if err := c.List(ctx, hcList, crclient.InNamespace(ns)); err != nil {
177+
errs = append(errs, fmt.Errorf("list HostedClusters in namespace %s: %w", ns, err))
178+
continue
179+
}
180+
for i := range hcList.Items {
181+
hc := &hcList.Items[i]
182+
if GetHCPNamespace(hc.Name, hc.Namespace) == hcpNamespace {
183+
return hc, nil
184+
}
185+
}
186+
}
187+
if len(errs) > 0 {
188+
return nil, errors.Join(errs...)
189+
}
190+
return nil, nil
191+
}
192+
166193
// ShouldEndPluginExecution checks if the plugin should end execution by verifying if the required
167194
// Hypershift resources (HostedControlPlane and HostedCluster) exist in the cluster.
168195
// Returns true if the plugin should end execution (i.e., if this is not a Hypershift cluster).

pkg/common/utils_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,34 @@ func TestShouldEndPluginExecution(t *testing.T) {
573573
}
574574
}
575575

576+
func TestGetHostedCluster(t *testing.T) {
577+
scheme := runtime.NewScheme()
578+
_ = hyperv1.AddToScheme(scheme)
579+
580+
t.Run("When GetHostedCluster runs with a HostedCluster matching HCP namespace, It Should return that cluster", func(t *testing.T) {
581+
g := NewWithT(t)
582+
hc := &hyperv1.HostedCluster{
583+
ObjectMeta: metav1.ObjectMeta{Name: "my-cluster", Namespace: "clusters"},
584+
}
585+
c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(hc).Build()
586+
587+
result, err := GetHostedCluster(context.TODO(), c, []string{"clusters", "clusters-my-cluster"}, "clusters-my-cluster")
588+
g.Expect(err).NotTo(HaveOccurred())
589+
g.Expect(result).NotTo(BeNil())
590+
g.Expect(result.Name).To(Equal("my-cluster"))
591+
g.Expect(result.Namespace).To(Equal("clusters"))
592+
})
593+
594+
t.Run("When GetHostedCluster runs with no HostedClusters in client, It Should return nil", func(t *testing.T) {
595+
g := NewWithT(t)
596+
c := fake.NewClientBuilder().WithScheme(scheme).Build()
597+
598+
result, err := GetHostedCluster(context.TODO(), c, []string{"clusters", "clusters-my-cluster"}, "clusters-my-cluster")
599+
g.Expect(err).NotTo(HaveOccurred())
600+
g.Expect(result).To(BeNil())
601+
})
602+
}
603+
576604
func TestCRDExists(t *testing.T) {
577605
scheme := runtime.NewScheme()
578606
_ = hyperv1.AddToScheme(scheme)

0 commit comments

Comments
 (0)