@@ -2,10 +2,14 @@ package singlek8s
22
33import (
44 "fmt"
5+ "strings"
56
7+ mantlev1 "github.com/cybozu-go/mantle/api/v1"
8+ "github.com/cybozu-go/mantle/internal/controller"
69 "github.com/cybozu-go/mantle/test/util"
710 . "github.com/onsi/ginkgo/v2"
811 . "github.com/onsi/gomega"
12+ corev1 "k8s.io/api/core/v1"
913)
1014
1115type multiRookCephTest struct {
@@ -96,9 +100,26 @@ func (test *multiRookCephTest) testMain() {
96100 var srcImageName1 , srcImageName2 , srcImageNameDummy string
97101 var expectImageAndSnaps1 , expectImageAndSnaps2 []string
98102 var dummySnapInfo , dummyCloneInfo * rbdInfo
103+ var backup1 , backup2 * mantlev1.MantleBackup
99104
100- It ("create PVC, backup, and restore in the tenant namespace 1" , func () {
101- test .createPVCBackupRestoreWithData (test .tenantNamespace1 , test .storageClassName1 , testData1 )
105+ // --- Cluster 1 backup lifecycle ---
106+
107+ It ("create PVC and write data in tenant namespace 1" , func () {
108+ err := applyPVCTemplate (test .tenantNamespace1 , test .pvcName , test .storageClassName1 )
109+ Expect (err ).NotTo (HaveOccurred ())
110+ err = writeTestData (test .tenantNamespace1 , test .pvcName , testData1 )
111+ Expect (err ).NotTo (HaveOccurred ())
112+ })
113+
114+ It ("create backup in tenant namespace 1 and wait for snapshot" , func () {
115+ err := applyMantleBackupTemplate (test .tenantNamespace1 , test .pvcName , test .mantleBackupName )
116+ Expect (err ).NotTo (HaveOccurred ())
117+
118+ Eventually (func (g Gomega ) {
119+ ok , err := isMantleBackupSnapshotCaptured (test .tenantNamespace1 , test .mantleBackupName )
120+ g .Expect (err ).NotTo (HaveOccurred ())
121+ g .Expect (ok ).To (BeTrue ())
122+ }).Should (Succeed ())
102123
103124 pvName , err := getPVFromPVC (test .tenantNamespace1 , test .pvcName )
104125 Expect (err ).NotTo (HaveOccurred ())
@@ -111,6 +132,86 @@ func (test *multiRookCephTest) testMain() {
111132 }
112133 })
113134
135+ It ("check labels on MantleBackup after backup in tenant namespace 1" , func () {
136+ pvc , err := getObject [corev1.PersistentVolumeClaim ]("pvc" , test .tenantNamespace1 , test .pvcName )
137+ Expect (err ).NotTo (HaveOccurred ())
138+
139+ mb , err := getMB (test .tenantNamespace1 , test .mantleBackupName )
140+ Expect (err ).NotTo (HaveOccurred ())
141+
142+ mantleLabels := make (map [string ]string )
143+ for k , v := range mb .GetLabels () {
144+ if strings .HasPrefix (k , "mantle.cybozu.io/" ) {
145+ mantleLabels [k ] = v
146+ }
147+ }
148+ Expect (mantleLabels ).To (Equal (map [string ]string {
149+ "mantle.cybozu.io/cluster-id" : cephCluster1Namespace ,
150+ "mantle.cybozu.io/local-backup-target-pvc-uid" : string (pvc .GetUID ()),
151+ }))
152+ })
153+
154+ It ("verify for tenant namespace 1 runs only in ceph cluster 1, is cleaned up, and never appears in ceph cluster 2" , func () {
155+ // The MantleBackup already exists, so the verify resource names (derived from its
156+ // UID) are known now, before verify runs. This lets us start monitoring ceph cluster 2
157+ // from the beginning of the verify lifecycle.
158+ mb , err := getMB (test .tenantNamespace1 , test .mantleBackupName )
159+ Expect (err ).NotTo (HaveOccurred ())
160+ backup1 = mb
161+
162+ jobName := controller .MakeVerifyJobName (backup1 )
163+ pvcName := controller .MakeVerifyPVCName (backup1 )
164+
165+ // Continuously monitor until ceph cluster 1 finishes verify and cleans up its
166+ // resources. On every poll, hard-assert (via the outer Expect, not g.Expect) that
167+ // the verify resources never appear in ceph cluster 2, so even a transient
168+ // appearance fails the test immediately instead of being tolerated as a retryable
169+ // condition. A single point-in-time check would miss resources that are created
170+ // briefly in the wrong namespace and then cleaned up quickly.
171+ Eventually (func (g Gomega ) {
172+ // The verify resources must NEVER appear in ceph cluster 2 at any point, so it doesn't use 'g'.
173+ existsJob2 , err := checkJobExists (cephCluster2Namespace , jobName )
174+ Expect (err ).NotTo (HaveOccurred ())
175+ Expect (existsJob2 ).To (BeFalse (), "verify job must not run in ceph cluster 2" )
176+ existsPVC2 , err := checkPVCExists (cephCluster2Namespace , pvcName )
177+ Expect (err ).NotTo (HaveOccurred ())
178+ Expect (existsPVC2 ).To (BeFalse (), "verify PVC must not be created in ceph cluster 2" )
179+
180+ mb , err := getMB (test .tenantNamespace1 , test .mantleBackupName )
181+ g .Expect (err ).NotTo (HaveOccurred ())
182+ // If the ceph cluster 2 controller mistakenly runs verify for this backup,
183+ // the verify job fails because the RBD snapshot does not exist in ceph cluster 2,
184+ // which causes IsVerifiedFalse to be set instead of IsVerifiedTrue.
185+ Expect (mb .IsVerifiedFalse ()).To (BeFalse (),
186+ "verify failed: ceph cluster 2 controller may have run verify for ceph cluster 1's backup" )
187+
188+ // Terminating condition: verify succeeded in ceph cluster 1 and its resources
189+ // have been cleaned up.
190+ g .Expect (mb .IsVerifiedTrue ()).To (BeTrue ())
191+ existsJob1 , err := checkJobExists (cephCluster1Namespace , jobName )
192+ g .Expect (err ).NotTo (HaveOccurred ())
193+ g .Expect (existsJob1 ).To (BeFalse (), "verify job should be cleaned up from ceph cluster 1" )
194+ existsPVC1 , err := checkPVCExists (cephCluster1Namespace , pvcName )
195+ g .Expect (err ).NotTo (HaveOccurred ())
196+ g .Expect (existsPVC1 ).To (BeFalse (), "verify PVC should be cleaned up from ceph cluster 1" )
197+ }).Should (Succeed ())
198+ })
199+
200+ It ("create restore in tenant namespace 1 and wait for ready" , func () {
201+ err := applyMantleRestoreTemplate (test .tenantNamespace1 , test .mantleRestoreName , test .mantleBackupName )
202+ Expect (err ).NotTo (HaveOccurred ())
203+ Eventually (func () bool {
204+ return isMantleRestoreReady (test .tenantNamespace1 , test .mantleRestoreName )
205+ }).Should (BeTrue ())
206+ })
207+
208+ // --- Dummy data ---
209+ // Manually create RBD images/snaps/clones in ceph cluster 2 using the same names as
210+ // those created by ceph cluster 1's controller. This lets the subsequent RBD state
211+ // checks verify that ceph cluster 1's controller never touches ceph cluster 2's RBD
212+ // images. No corresponding dummy data is placed in ceph cluster 1 because the test
213+ // focuses on this one-directional isolation: cluster 1 must not interfere with cluster 2.
214+
114215 It ("create the dummy RBD image & snap in the ceph cluster 2 with the same name as ceph cluster 1" , func () {
115216 By ("get source image name from the PV/PVC" )
116217 pvName , err := getPVFromPVC (test .tenantNamespace1 , test .pvcName )
@@ -136,7 +237,7 @@ func (test *multiRookCephTest) testMain() {
136237 Expect (err ).NotTo (HaveOccurred ())
137238 })
138239
139- It ("check the rbd images in the both clusters" , func () {
240+ It ("check the rbd images in both clusters after cluster 1 backup " , func () {
140241 imageAndSnaps1 , err := getImageAndSnapNames (cephCluster1Namespace , test .poolName )
141242 Expect (err ).NotTo (HaveOccurred ())
142243 Expect (imageAndSnaps1 ).To (ConsistOf (expectImageAndSnaps1 ))
@@ -146,8 +247,24 @@ func (test *multiRookCephTest) testMain() {
146247 Expect (imageAndSnaps2 ).To (ConsistOf (expectImageAndSnaps1 ))
147248 })
148249
149- It ("create PVC, backup, and restore in the tenant namespace 2" , func () {
150- test .createPVCBackupRestoreWithData (test .tenantNamespace2 , test .storageClassName2 , testData2 )
250+ // --- Cluster 2 backup lifecycle ---
251+
252+ It ("create PVC and write data in tenant namespace 2" , func () {
253+ err := applyPVCTemplate (test .tenantNamespace2 , test .pvcName , test .storageClassName2 )
254+ Expect (err ).NotTo (HaveOccurred ())
255+ err = writeTestData (test .tenantNamespace2 , test .pvcName , testData2 )
256+ Expect (err ).NotTo (HaveOccurred ())
257+ })
258+
259+ It ("create backup in tenant namespace 2 and wait for snapshot" , func () {
260+ err := applyMantleBackupTemplate (test .tenantNamespace2 , test .pvcName , test .mantleBackupName )
261+ Expect (err ).NotTo (HaveOccurred ())
262+
263+ Eventually (func (g Gomega ) {
264+ ok , err := isMantleBackupSnapshotCaptured (test .tenantNamespace2 , test .mantleBackupName )
265+ g .Expect (err ).NotTo (HaveOccurred ())
266+ g .Expect (ok ).To (BeTrue ())
267+ }).Should (Succeed ())
151268
152269 pvName , err := getPVFromPVC (test .tenantNamespace2 , test .pvcName )
153270 Expect (err ).NotTo (HaveOccurred ())
@@ -160,6 +277,73 @@ func (test *multiRookCephTest) testMain() {
160277 }
161278 })
162279
280+ It ("check labels on MantleBackup after backup in tenant namespace 2" , func () {
281+ pvc , err := getObject [corev1.PersistentVolumeClaim ]("pvc" , test .tenantNamespace2 , test .pvcName )
282+ Expect (err ).NotTo (HaveOccurred ())
283+
284+ mb , err := getMB (test .tenantNamespace2 , test .mantleBackupName )
285+ Expect (err ).NotTo (HaveOccurred ())
286+
287+ Expect (mb .GetLabels ()).To (HaveKeyWithValue ("mantle.cybozu.io/cluster-id" , cephCluster2Namespace ))
288+ Expect (mb .GetLabels ()).To (HaveKeyWithValue ("mantle.cybozu.io/local-backup-target-pvc-uid" , string (pvc .GetUID ())))
289+ })
290+
291+ It ("verify for tenant namespace 2 runs only in ceph cluster 2, is cleaned up, and never appears in ceph cluster 1" , func () {
292+ // The MantleBackup already exists, so the verify resource names (derived from its
293+ // UID) are known now, before verify runs. This lets us start monitoring ceph cluster 1
294+ // from the beginning of the verify lifecycle.
295+ mb , err := getMB (test .tenantNamespace2 , test .mantleBackupName )
296+ Expect (err ).NotTo (HaveOccurred ())
297+ backup2 = mb
298+
299+ jobName := controller .MakeVerifyJobName (backup2 )
300+ pvcName := controller .MakeVerifyPVCName (backup2 )
301+
302+ // Continuously monitor until ceph cluster 2 finishes verify and cleans up its
303+ // resources. On every poll, hard-assert (via the outer Expect, not g.Expect) that
304+ // the verify resources never appear in ceph cluster 1, so even a transient
305+ // appearance fails the test immediately instead of being tolerated as a retryable
306+ // condition. A single point-in-time check would miss resources that are created
307+ // briefly in the wrong namespace and then cleaned up quickly.
308+ Eventually (func (g Gomega ) {
309+ // The verify resources must NEVER appear in ceph cluster 1 at any point, so it doesn't use 'g'.
310+ existsJob1 , err := checkJobExists (cephCluster1Namespace , jobName )
311+ Expect (err ).NotTo (HaveOccurred ())
312+ Expect (existsJob1 ).To (BeFalse (), "verify job must not run in ceph cluster 1" )
313+ existsPVC1 , err := checkPVCExists (cephCluster1Namespace , pvcName )
314+ Expect (err ).NotTo (HaveOccurred ())
315+ Expect (existsPVC1 ).To (BeFalse (), "verify PVC must not be created in ceph cluster 1" )
316+
317+ mb , err := getMB (test .tenantNamespace2 , test .mantleBackupName )
318+ g .Expect (err ).NotTo (HaveOccurred ())
319+ // If the ceph cluster 1 controller mistakenly runs verify for this backup,
320+ // the verify job fails because the RBD snapshot does not exist in ceph cluster 1,
321+ // which causes IsVerifiedFalse to be set instead of IsVerifiedTrue.
322+ Expect (mb .IsVerifiedFalse ()).To (BeFalse (),
323+ "verify failed: ceph cluster 1 controller may have run verify for ceph cluster 2's backup" )
324+
325+ // Terminating condition: verify succeeded in ceph cluster 2 and its resources
326+ // have been cleaned up.
327+ g .Expect (mb .IsVerifiedTrue ()).To (BeTrue ())
328+ existsJob2 , err := checkJobExists (cephCluster2Namespace , jobName )
329+ g .Expect (err ).NotTo (HaveOccurred ())
330+ g .Expect (existsJob2 ).To (BeFalse (), "verify job should be cleaned up from ceph cluster 2" )
331+ existsPVC2 , err := checkPVCExists (cephCluster2Namespace , pvcName )
332+ g .Expect (err ).NotTo (HaveOccurred ())
333+ g .Expect (existsPVC2 ).To (BeFalse (), "verify PVC should be cleaned up from ceph cluster 2" )
334+ }).Should (Succeed ())
335+ })
336+
337+ It ("create restore in tenant namespace 2 and wait for ready" , func () {
338+ err := applyMantleRestoreTemplate (test .tenantNamespace2 , test .mantleRestoreName , test .mantleBackupName )
339+ Expect (err ).NotTo (HaveOccurred ())
340+ Eventually (func () bool {
341+ return isMantleRestoreReady (test .tenantNamespace2 , test .mantleRestoreName )
342+ }).Should (BeTrue ())
343+ })
344+
345+ // --- Cross-cluster RBD state and data integrity checks ---
346+
163347 It ("check the rbd images in the both clusters" , func () {
164348 imageAndSnaps1 , err := getImageAndSnapNames (cephCluster1Namespace , test .poolName )
165349 Expect (err ).NotTo (HaveOccurred ())
@@ -176,6 +360,8 @@ func (test *multiRookCephTest) testMain() {
176360 Expect (backupData2 ).To (Equal (testData2 ))
177361 })
178362
363+ // --- Deletion isolation checks ---
364+
179365 It ("delete the resources in the tenant namespace 1" , func () {
180366 test .deleteBackupRestore (test .tenantNamespace1 )
181367 })
@@ -230,23 +416,6 @@ func (test *multiRookCephTest) testMain() {
230416 })
231417}
232418
233- func (test * multiRookCephTest ) createPVCBackupRestoreWithData (ns , sc string , data []byte ) {
234- err := applyPVCTemplate (ns , test .pvcName , sc )
235- Expect (err ).NotTo (HaveOccurred ())
236- err = writeTestData (ns , test .pvcName , data )
237- Expect (err ).NotTo (HaveOccurred ())
238-
239- err = applyMantleBackupTemplate (ns , test .pvcName , test .mantleBackupName )
240- Expect (err ).NotTo (HaveOccurred ())
241-
242- err = applyMantleRestoreTemplate (ns , test .mantleRestoreName , test .mantleBackupName )
243- Expect (err ).NotTo (HaveOccurred ())
244-
245- Eventually (func () bool {
246- return isMantleRestoreReady (ns , test .mantleRestoreName )
247- }).Should (BeTrue ())
248- }
249-
250419func (test * multiRookCephTest ) deleteBackupRestore (ns string ) {
251420 _ , stderr , err := kubectl ("delete" , "mantlerestore" , "-n" , ns , test .mantleRestoreName )
252421 Expect (err ).NotTo (HaveOccurred (), string (stderr ))
0 commit comments