Skip to content

Commit 9f2186f

Browse files
committed
implement e2e test for lock feature
Signed-off-by: Yuji Ito <llamerada.jp@gmail.com>
1 parent 3d6b41a commit 9f2186f

4 files changed

Lines changed: 174 additions & 11 deletions

File tree

.github/workflows/e2e-multiple-k8s-clusters.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ jobs:
3131
- change-to-primary
3232
- change-to-secondary
3333
- change-to-standalone
34+
- lock
3435
- "!//" # select unlabelled tests
3536
include:
3637
- backup-transfer-part-size: 10Mi # equal to the PVC size

internal/controller/mantlebackup_controller.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1895,6 +1895,8 @@ func (r *MantleBackupReconciler) startImport(
18951895
backup *mantlev1.MantleBackup,
18961896
target *snapshotTarget,
18971897
) (ctrl.Result, error) {
1898+
logger := log.FromContext(ctx)
1899+
18981900
if !r.doesMantleBackupHaveSyncModeAnnot(backup) {
18991901
// SetSynchronizing is not called yet or the cache is stale.
19001902
return ctrl.Result{}, nil
@@ -1925,7 +1927,7 @@ func (r *MantleBackupReconciler) startImport(
19251927
return ctrl.Result{}, fmt.Errorf("failed to lock the volume: %w", err)
19261928
}
19271929
if !locked {
1928-
// another process is holding the lock
1930+
logger.Info("the volume is locked by another process", "uid", string(backup.GetUID()))
19291931
return requeueReconciliation(), nil
19301932
}
19311933

test/e2e/multik8s/lock_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package multik8s
2+
3+
import (
4+
"encoding/json"
5+
"time"
6+
7+
mantlev1 "github.com/cybozu-go/mantle/api/v1"
8+
"github.com/cybozu-go/mantle/internal/ceph"
9+
"github.com/cybozu-go/mantle/internal/controller"
10+
. "github.com/cybozu-go/mantle/test/e2e/multik8s/testutil"
11+
"github.com/cybozu-go/mantle/test/util"
12+
. "github.com/onsi/ginkgo/v2"
13+
. "github.com/onsi/gomega"
14+
corev1 "k8s.io/api/core/v1"
15+
)
16+
17+
var _ = Describe("Locking", Label("lock"), func() {
18+
namespace := util.GetUniqueName("ns-")
19+
pvcName := util.GetUniqueName("pvc-")
20+
backupName0 := util.GetUniqueName("mb-")
21+
backupName1 := util.GetUniqueName("mb-")
22+
dummyLockID := "dummy-lock-id"
23+
var controllerPod string
24+
var poolName, imageName string
25+
26+
It("should setup environment", func(ctx SpecContext) {
27+
SetupEnvironment(namespace)
28+
// Create PVC and MantleBackup in the primary cluster
29+
CreatePVC(ctx, PrimaryK8sCluster, namespace, pvcName)
30+
CreateMantleBackup(PrimaryK8sCluster, namespace, pvcName, backupName0)
31+
WaitMantleBackupSynced(namespace, backupName0)
32+
var err error
33+
controllerPod, err = GetControllerPodName(SecondaryK8sCluster)
34+
Expect(err).NotTo(HaveOccurred())
35+
})
36+
37+
It("should lock the volume in the secondary cluster", func() {
38+
mb0, err := GetMB(SecondaryK8sCluster, namespace, backupName0)
39+
Expect(err).NotTo(HaveOccurred())
40+
41+
pvStored := corev1.PersistentVolume{}
42+
err = json.Unmarshal([]byte(mb0.Status.PVManifest), &pvStored)
43+
Expect(err).NotTo(HaveOccurred())
44+
poolName = pvStored.Spec.CSI.VolumeAttributes["pool"]
45+
imageName = pvStored.Spec.CSI.VolumeAttributes["imageName"]
46+
47+
// locked
48+
_, _, err = Kubectl(SecondaryK8sCluster, nil, "exec", "-n", CephClusterNamespace, controllerPod, "--",
49+
"rbd", "-p", poolName, "lock", "add", imageName, dummyLockID)
50+
Expect(err).NotTo(HaveOccurred())
51+
})
52+
53+
It("should create additional backup and wait for the log", func(ctx SpecContext) {
54+
CreateMantleBackup(PrimaryK8sCluster, namespace, pvcName, backupName1)
55+
var mb1 *mantlev1.MantleBackup
56+
Eventually(func() error {
57+
var err error
58+
mb1, err = GetMB(SecondaryK8sCluster, namespace, backupName1)
59+
return err
60+
}).Should(Succeed())
61+
62+
err := WaitControllerLog(ctx, SecondaryK8sCluster,
63+
"the volume is locked by another process.*"+string(mb1.GetUID()),
64+
3*time.Minute)
65+
Expect(err).NotTo(HaveOccurred())
66+
})
67+
68+
It("checks that the jobs are not created", func(ctx SpecContext) {
69+
mb1, err := GetMB(SecondaryK8sCluster, namespace, backupName1)
70+
Expect(err).NotTo(HaveOccurred())
71+
Expect(
72+
CheckJobExist(SecondaryK8sCluster, CephClusterNamespace, controller.MantleZeroOutJobPrefix+string(mb1.GetUID())),
73+
).To(BeFalse())
74+
Expect(
75+
CheckJobExist(SecondaryK8sCluster, CephClusterNamespace, controller.MantleImportJobPrefix+string(mb1.GetUID())),
76+
).To(BeFalse())
77+
})
78+
79+
It("should unlock the volume in the secondary cluster", func() {
80+
stdout, _, err := Kubectl(SecondaryK8sCluster, nil, "exec", "-n", CephClusterNamespace, controllerPod, "--",
81+
"rbd", "-p", poolName, "--format", "json", "lock", "ls", imageName)
82+
Expect(err).NotTo(HaveOccurred())
83+
var locks []*ceph.RBDLock
84+
err = json.Unmarshal(stdout, &locks)
85+
Expect(err).NotTo(HaveOccurred())
86+
Expect(locks).To(HaveLen(1))
87+
88+
// unlock
89+
_, _, err = Kubectl(SecondaryK8sCluster, nil, "exec", "-n", CephClusterNamespace, controllerPod, "--",
90+
"rbd", "-p", poolName, "lock", "rm", imageName, dummyLockID, locks[0].Locker)
91+
Expect(err).NotTo(HaveOccurred())
92+
})
93+
94+
It("should resume backup creation and complete it", func() {
95+
WaitMantleBackupSynced(namespace, backupName1)
96+
})
97+
98+
It("should not exist locks after backup completion", func() {
99+
stdout, _, err := Kubectl(SecondaryK8sCluster, nil, "exec", "-n", CephClusterNamespace, controllerPod, "--",
100+
"rbd", "-p", poolName, "--format", "json", "lock", "ls", imageName)
101+
Expect(err).NotTo(HaveOccurred())
102+
var locks []*ceph.RBDLock
103+
err = json.Unmarshal(stdout, &locks)
104+
Expect(err).NotTo(HaveOccurred())
105+
Expect(locks).To(HaveLen(0))
106+
})
107+
})

test/e2e/multik8s/testutil/util.go

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package testutil
22

33
import (
4+
"bufio"
45
"bytes"
56
"context"
67
_ "embed"
@@ -9,6 +10,7 @@ import (
910
"fmt"
1011
"os"
1112
"os/exec"
13+
"regexp"
1214
"slices"
1315
"strings"
1416
"time"
@@ -764,17 +766,13 @@ func WaitUploadJobCreated(ctx SpecContext, cluster int, namespace, backupName st
764766
}).Should(Succeed())
765767
}
766768

767-
func WaitJobDeleted(ctx SpecContext, cluster int, namespace, jobName string) {
769+
func CheckJobExist(clusterNo int, namespace, jobName string) bool {
768770
GinkgoHelper()
769-
By("waiting for a job to be deleted")
770-
Eventually(ctx, func(g Gomega) {
771-
jobs, err := GetJobList(cluster, CephClusterNamespace)
772-
g.Expect(err).NotTo(HaveOccurred())
773-
exist := slices.ContainsFunc(jobs.Items, func(job batchv1.Job) bool {
774-
return job.GetName() == jobName
775-
})
776-
g.Expect(exist).To(BeFalse())
777-
}).Should(Succeed())
771+
jobs, err := GetJobList(clusterNo, namespace)
772+
Expect(err).NotTo(HaveOccurred())
773+
return slices.ContainsFunc(jobs.Items, func(job batchv1.Job) bool {
774+
return job.GetName() == jobName
775+
})
778776
}
779777

780778
func WaitComponentJobsDeleted(
@@ -1079,3 +1077,58 @@ func ChangeComponentJobScript(
10791077
}
10801078
}).Should(Succeed())
10811079
}
1080+
1081+
func WaitControllerLog(ctx SpecContext, clusterNo int, pattern string, duration time.Duration) error {
1082+
timeoutCtx, cancel := context.WithTimeout(ctx, duration)
1083+
defer cancel()
1084+
1085+
matcher := regexp.MustCompile(pattern)
1086+
1087+
fields, err := getKubectlInvocation(clusterNo)
1088+
if err != nil {
1089+
panic(err)
1090+
}
1091+
fields = append(fields, "logs", "-n", CephClusterNamespace, "deployment/"+MantleControllerDeployName, "-f")
1092+
1093+
command := exec.CommandContext(timeoutCtx, fields[0], fields[1:]...)
1094+
stdoutPipe, err := command.StdoutPipe()
1095+
if err != nil {
1096+
panic(err)
1097+
}
1098+
err = command.Start()
1099+
if err != nil {
1100+
panic(err)
1101+
}
1102+
defer func() {
1103+
_ = command.Process.Kill()
1104+
_ = command.Wait()
1105+
}()
1106+
1107+
// read stdout line by line until the pattern is found
1108+
scanner := bufio.NewScanner(stdoutPipe)
1109+
found := make(chan struct{})
1110+
go func() {
1111+
for scanner.Scan() {
1112+
select {
1113+
case <-timeoutCtx.Done():
1114+
return
1115+
default:
1116+
}
1117+
line := scanner.Text()
1118+
if matcher.MatchString(line) {
1119+
close(found)
1120+
return
1121+
}
1122+
}
1123+
if scanner.Err() != nil {
1124+
panic(scanner.Err())
1125+
}
1126+
}()
1127+
1128+
select {
1129+
case <-timeoutCtx.Done():
1130+
return timeoutCtx.Err()
1131+
case <-found:
1132+
return nil
1133+
}
1134+
}

0 commit comments

Comments
 (0)