Skip to content

Commit 3711f8f

Browse files
jparrillclaude
andcommitted
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>
1 parent b61f00d commit 3711f8f

4 files changed

Lines changed: 1162 additions & 255 deletions

File tree

pkg/core/backup_test.go

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,293 @@
11
package core
2+
3+
// Test scenario names follow: "When <action or context>, It Should <expected outcome>".
4+
5+
import (
6+
"context"
7+
"testing"
8+
9+
. "github.com/onsi/gomega"
10+
common "github.com/openshift/hypershift-oadp-plugin/pkg/common"
11+
plugtypes "github.com/openshift/hypershift-oadp-plugin/pkg/core/types"
12+
hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
13+
"github.com/sirupsen/logrus"
14+
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
15+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
16+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
18+
"k8s.io/apimachinery/pkg/runtime"
19+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
20+
)
21+
22+
// mockValidator implements validation.BackupValidator for testing.
23+
type mockValidator struct {
24+
validatePlatformErr error
25+
}
26+
27+
func (m *mockValidator) ValidatePluginConfig(_ map[string]string) (*plugtypes.BackupOptions, error) {
28+
return &plugtypes.BackupOptions{}, nil
29+
}
30+
31+
func (m *mockValidator) ValidatePlatformConfig(_ *hyperv1.HostedControlPlane, _ *velerov1.Backup) error {
32+
return m.validatePlatformErr
33+
}
34+
35+
func newTestBackupPlugin(objects ...runtime.Object) *BackupPlugin {
36+
scheme := common.CustomScheme
37+
38+
hcpCRD := &apiextensionsv1.CustomResourceDefinition{
39+
ObjectMeta: metav1.ObjectMeta{Name: "hostedcontrolplanes.hypershift.openshift.io"},
40+
}
41+
42+
hcp := &hyperv1.HostedControlPlane{
43+
ObjectMeta: metav1.ObjectMeta{Name: "test-hcp", Namespace: "clusters-test"},
44+
Spec: hyperv1.HostedControlPlaneSpec{
45+
Platform: hyperv1.PlatformSpec{Type: hyperv1.AWSPlatform},
46+
},
47+
}
48+
49+
allObjects := []runtime.Object{hcpCRD, hcp}
50+
allObjects = append(allObjects, objects...)
51+
52+
client := fake.NewClientBuilder().
53+
WithScheme(scheme).
54+
WithRuntimeObjects(allObjects...).
55+
Build()
56+
57+
return &BackupPlugin{
58+
log: logrus.New(),
59+
ctx: context.Background(),
60+
client: client,
61+
config: map[string]string{},
62+
validator: &mockValidator{},
63+
hcp: hcp,
64+
BackupOptions: &plugtypes.BackupOptions{},
65+
hoNamespace: "hypershift",
66+
etcdBackupMethod: common.EtcdBackupMethodVolume,
67+
}
68+
}
69+
70+
func newUnstructuredItem(kind, apiVersion, name, namespace string) *unstructured.Unstructured {
71+
return &unstructured.Unstructured{
72+
Object: map[string]any{
73+
"apiVersion": apiVersion,
74+
"kind": kind,
75+
"metadata": map[string]any{
76+
"name": name,
77+
"namespace": namespace,
78+
},
79+
},
80+
}
81+
}
82+
83+
func newTestBackup() *velerov1.Backup {
84+
return &velerov1.Backup{
85+
ObjectMeta: metav1.ObjectMeta{Name: "test-backup", Namespace: "openshift-adp"},
86+
Spec: velerov1.BackupSpec{
87+
IncludedNamespaces: []string{"clusters", "clusters-test"},
88+
},
89+
}
90+
}
91+
92+
func TestExecute(t *testing.T) {
93+
falseVal := false
94+
95+
tests := []struct {
96+
name string
97+
setup func(*BackupPlugin)
98+
item func() *unstructured.Unstructured
99+
backup func() *velerov1.Backup
100+
wantNilResult bool
101+
wantErr bool
102+
assert func(*GomegaWithT, runtime.Unstructured, *BackupPlugin)
103+
}{
104+
// HostedCluster cases
105+
{
106+
name: "When Execute processes a HostedCluster item, It Should add restore annotation",
107+
item: func() *unstructured.Unstructured {
108+
return newUnstructuredItem("HostedCluster", "hypershift.openshift.io/v1beta1", "my-hc", "clusters")
109+
},
110+
backup: newTestBackup,
111+
assert: func(g *GomegaWithT, result runtime.Unstructured, _ *BackupPlugin) {
112+
metadata := result.UnstructuredContent()["metadata"].(map[string]any)
113+
annotations := metadata["annotations"].(map[string]any)
114+
_, exists := annotations[common.HostedClusterRestoredFromBackupAnnotation]
115+
g.Expect(exists).To(BeTrue())
116+
},
117+
},
118+
{
119+
name: "When Execute processes a HostedCluster with cached etcdSnapshotURL, It Should inject URL into status and annotation",
120+
setup: func(bp *BackupPlugin) {
121+
bp.etcdSnapshotURL = "s3://bucket/backups/test/etcd-backup/snapshot.db"
122+
},
123+
item: func() *unstructured.Unstructured {
124+
return newUnstructuredItem("HostedCluster", "hypershift.openshift.io/v1beta1", "my-hc", "clusters")
125+
},
126+
backup: newTestBackup,
127+
assert: func(g *GomegaWithT, result runtime.Unstructured, _ *BackupPlugin) {
128+
metadata := result.UnstructuredContent()["metadata"].(map[string]any)
129+
annotations := metadata["annotations"].(map[string]any)
130+
g.Expect(annotations[common.EtcdSnapshotURLAnnotation]).To(Equal("s3://bucket/backups/test/etcd-backup/snapshot.db"))
131+
132+
status := result.UnstructuredContent()["status"].(map[string]any)
133+
g.Expect(status["lastSuccessfulEtcdBackupURL"]).To(Equal("s3://bucket/backups/test/etcd-backup/snapshot.db"))
134+
},
135+
},
136+
// HostedControlPlane cases
137+
{
138+
name: "When Execute processes a HostedControlPlane with volumeSnapshot method, It Should not create etcd backup",
139+
item: func() *unstructured.Unstructured {
140+
item := newUnstructuredItem("HostedControlPlane", "hypershift.openshift.io/v1beta1", "test-hcp", "clusters-test")
141+
item.Object["spec"] = map[string]any{
142+
"platform": map[string]any{"type": "AWS"},
143+
}
144+
return item
145+
},
146+
backup: newTestBackup,
147+
assert: func(g *GomegaWithT, _ runtime.Unstructured, bp *BackupPlugin) {
148+
g.Expect(bp.etcdOrchestrator).To(BeNil())
149+
},
150+
},
151+
// Pod cases
152+
{
153+
name: "When Execute processes an etcd Pod with volumeSnapshot method and fsBackup disabled, It Should add FSBackup label",
154+
item: func() *unstructured.Unstructured {
155+
return newUnstructuredItem("Pod", "v1", "etcd-0", "clusters-test")
156+
},
157+
backup: func() *velerov1.Backup {
158+
b := newTestBackup()
159+
b.Spec.DefaultVolumesToFsBackup = &falseVal
160+
return b
161+
},
162+
assert: func(g *GomegaWithT, result runtime.Unstructured, _ *BackupPlugin) {
163+
metadata := result.UnstructuredContent()["metadata"].(map[string]any)
164+
labels := metadata["labels"].(map[string]any)
165+
g.Expect(labels[common.FSBackupLabelName]).To(Equal("true"))
166+
},
167+
},
168+
{
169+
name: "When Execute processes an etcd Pod with etcdSnapshot method, It Should skip the pod",
170+
setup: func(bp *BackupPlugin) {
171+
bp.etcdBackupMethod = common.EtcdBackupMethodEtcdSnapshot
172+
},
173+
item: func() *unstructured.Unstructured {
174+
return newUnstructuredItem("Pod", "v1", "etcd-0", "clusters-test")
175+
},
176+
backup: newTestBackup,
177+
wantNilResult: true,
178+
},
179+
{
180+
name: "When Execute processes a non-etcd Pod, It Should pass through unchanged",
181+
item: func() *unstructured.Unstructured {
182+
return newUnstructuredItem("Pod", "v1", "kube-apiserver-0", "clusters-test")
183+
},
184+
backup: newTestBackup,
185+
assert: func(g *GomegaWithT, result runtime.Unstructured, _ *BackupPlugin) {
186+
metadata := result.UnstructuredContent()["metadata"].(map[string]any)
187+
g.Expect(metadata["name"]).To(Equal("kube-apiserver-0"))
188+
},
189+
},
190+
// PVC cases
191+
{
192+
name: "When Execute processes an etcd PVC with etcdSnapshot method, It Should skip the PVC",
193+
setup: func(bp *BackupPlugin) {
194+
bp.etcdBackupMethod = common.EtcdBackupMethodEtcdSnapshot
195+
},
196+
item: func() *unstructured.Unstructured {
197+
return newUnstructuredItem("PersistentVolumeClaim", "v1", "data-etcd-0", "clusters-test")
198+
},
199+
backup: newTestBackup,
200+
wantNilResult: true,
201+
},
202+
{
203+
name: "When Execute processes a PVC with kubevirt RHCOS label, It Should skip the PVC",
204+
item: func() *unstructured.Unstructured {
205+
item := newUnstructuredItem("PersistentVolumeClaim", "v1", "rhcos-disk", "clusters-test")
206+
item.Object["metadata"].(map[string]any)["labels"] = map[string]any{
207+
common.KubevirtRHCOSLabel: "true",
208+
}
209+
return item
210+
},
211+
backup: newTestBackup,
212+
wantNilResult: true,
213+
},
214+
{
215+
name: "When Execute processes a regular PVC, It Should pass through unchanged",
216+
item: func() *unstructured.Unstructured {
217+
return newUnstructuredItem("PersistentVolumeClaim", "v1", "some-data", "clusters-test")
218+
},
219+
backup: newTestBackup,
220+
},
221+
// DataVolume cases
222+
{
223+
name: "When Execute processes a DataVolume with kubevirt RHCOS label, It Should skip it",
224+
item: func() *unstructured.Unstructured {
225+
item := newUnstructuredItem("DataVolume", "cdi.kubevirt.io/v1beta1", "rhcos-dv", "clusters-test")
226+
item.Object["metadata"].(map[string]any)["labels"] = map[string]any{
227+
common.KubevirtRHCOSLabel: "true",
228+
}
229+
return item
230+
},
231+
backup: newTestBackup,
232+
wantNilResult: true,
233+
},
234+
}
235+
236+
for _, tt := range tests {
237+
t.Run(tt.name, func(t *testing.T) {
238+
g := NewWithT(t)
239+
bp := newTestBackupPlugin()
240+
if tt.setup != nil {
241+
tt.setup(bp)
242+
}
243+
244+
result, _, err := bp.Execute(tt.item(), tt.backup())
245+
if tt.wantErr {
246+
g.Expect(err).To(HaveOccurred())
247+
return
248+
}
249+
g.Expect(err).NotTo(HaveOccurred())
250+
251+
if tt.wantNilResult {
252+
g.Expect(result).To(BeNil())
253+
return
254+
}
255+
256+
g.Expect(result).NotTo(BeNil())
257+
if tt.assert != nil {
258+
tt.assert(g, result, bp)
259+
}
260+
})
261+
}
262+
}
263+
264+
func TestWaitForEtcdBackupCompletion(t *testing.T) {
265+
tests := []struct {
266+
name string
267+
setup func(*BackupPlugin)
268+
}{
269+
{
270+
name: "When orchestrator is nil, It Should return nil",
271+
setup: func(bp *BackupPlugin) {
272+
bp.etcdOrchestrator = nil
273+
},
274+
},
275+
{
276+
name: "When snapshotURL is already cached, It Should return nil without polling",
277+
setup: func(bp *BackupPlugin) {
278+
bp.etcdSnapshotURL = "s3://cached-url"
279+
},
280+
},
281+
}
282+
283+
for _, tt := range tests {
284+
t.Run(tt.name, func(t *testing.T) {
285+
g := NewWithT(t)
286+
bp := newTestBackupPlugin()
287+
tt.setup(bp)
288+
289+
err := bp.waitForEtcdBackupCompletion(context.TODO())
290+
g.Expect(err).NotTo(HaveOccurred())
291+
})
292+
}
293+
}

0 commit comments

Comments
 (0)