github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/test/integration/mysql_ha_test.go (about) 1 /* 2 Copyright (C) 2022-2023 ApeCloud Co., Ltd 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package appstest 18 19 import ( 20 "context" 21 "time" 22 23 . "github.com/onsi/ginkgo/v2" 24 . "github.com/onsi/gomega" 25 26 appsv1 "k8s.io/api/apps/v1" 27 corev1 "k8s.io/api/core/v1" 28 "k8s.io/apimachinery/pkg/types" 29 "sigs.k8s.io/controller-runtime/pkg/client" 30 31 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 32 "github.com/1aal/kubeblocks/pkg/common" 33 "github.com/1aal/kubeblocks/pkg/constant" 34 intctrlutil "github.com/1aal/kubeblocks/pkg/generics" 35 testapps "github.com/1aal/kubeblocks/pkg/testutil/apps" 36 testk8s "github.com/1aal/kubeblocks/pkg/testutil/k8s" 37 ) 38 39 var _ = Describe("MySQL High-Availability function", func() { 40 const clusterDefName = "test-clusterdef" 41 const clusterVersionName = "test-clusterversion" 42 const clusterNamePrefix = "test-cluster" 43 const scriptConfigName = "test-cluster-mysql-scripts" 44 const mysqlCompDefName = "replicasets" 45 const mysqlCompName = "mysql" 46 const leader = "leader" 47 const follower = "follower" 48 49 ctx := context.Background() 50 51 // Cleanups 52 53 cleanEnv := func() { 54 // must wait until resources deleted and no longer exist before the testcases start, 55 // otherwise if later it needs to create some new resource objects with the same name, 56 // in race conditions, it will find the existence of old objects, resulting failure to 57 // create the new objects. 58 By("clean resources") 59 60 // delete cluster(and all dependent sub-resources), clusterversion and clusterdef 61 testapps.ClearClusterResources(&testCtx) 62 63 // delete rest configurations 64 inNS := client.InNamespace(testCtx.DefaultNamespace) 65 ml := client.HasLabels{testCtx.TestObjLabelKey} 66 // namespaced 67 testapps.ClearResources(&testCtx, intctrlutil.ConfigMapSignature, inNS, ml) 68 // non-namespaced 69 testapps.ClearResources(&testCtx, intctrlutil.ConfigConstraintSignature, ml) 70 testapps.ClearResources(&testCtx, intctrlutil.BackupPolicyTemplateSignature, ml) 71 72 } 73 74 BeforeEach(cleanEnv) 75 76 AfterEach(cleanEnv) 77 78 // Testcases 79 80 var ( 81 clusterDefObj *appsv1alpha1.ClusterDefinition 82 clusterVersionObj *appsv1alpha1.ClusterVersion 83 clusterObj *appsv1alpha1.Cluster 84 clusterKey types.NamespacedName 85 ) 86 87 getRole := func(svc *corev1.Service) (role string) { 88 tunnel, err := testk8s.OpenTunnel(svc) 89 defer func() { 90 _ = tunnel.Close() 91 }() 92 Expect(err).NotTo(HaveOccurred()) 93 94 time.Sleep(time.Second) 95 96 db, err := tunnel.GetMySQLConn() 97 defer func() { 98 _ = db.Close() 99 }() 100 Expect(err).NotTo(HaveOccurred()) 101 102 if role, err = db.GetRole(ctx); err != nil { 103 return "" 104 } 105 return role 106 } 107 108 testThreeReplicasAndFailover := func() { 109 By("Create a cluster obj") 110 pvcSpec := testapps.NewPVCSpec("1Gi") 111 clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, 112 clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). 113 AddComponent(mysqlCompName, mysqlCompDefName). 114 SetReplicas(3).AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). 115 Create(&testCtx).GetObject() 116 clusterKey = client.ObjectKeyFromObject(clusterObj) 117 118 By("Waiting the cluster is created") 119 Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.RunningClusterPhase)) 120 121 By("Checking pods' role label") 122 stsList := testk8s.ListAndCheckStatefulSet(&testCtx, clusterKey) 123 sts := &stsList.Items[0] 124 pods, err := common.GetPodListByStatefulSet(ctx, k8sClient, sts) 125 Expect(err).To(Succeed()) 126 // should have 3 pods 127 Expect(len(pods)).Should(Equal(3)) 128 // 1 leader 129 // 2 followers 130 leaderCount, followerCount := 0, 0 131 for _, pod := range pods { 132 switch pod.Labels[constant.RoleLabelKey] { 133 case leader: 134 leaderCount++ 135 case follower: 136 followerCount++ 137 } 138 } 139 Expect(leaderCount).Should(Equal(1)) 140 Expect(followerCount).Should(Equal(2)) 141 142 By("Checking services status") 143 svcList := &corev1.ServiceList{} 144 Expect(k8sClient.List(ctx, svcList, client.MatchingLabels{ 145 constant.AppInstanceLabelKey: clusterKey.Name, 146 }, client.InNamespace(clusterKey.Namespace))).Should(Succeed()) 147 // we should have both external service and headless service 148 Expect(len(svcList.Items)).Should(Equal(2)) 149 var externalSvc corev1.Service 150 for _, svc := range svcList.Items { 151 if svc.Spec.ClusterIP != "None" { 152 externalSvc = svc 153 } 154 } 155 Expect(externalSvc).ShouldNot(BeNil()) 156 // getRole should be leader through service 157 Eventually(func() string { 158 return getRole(&externalSvc) 159 }).Should(Equal(leader)) 160 161 By("Deleting leader pod") 162 leaderPod := &corev1.Pod{} 163 for _, pod := range pods { 164 if pod.Labels[constant.RoleLabelKey] == leader { 165 leaderPod = &pod 166 break 167 } 168 } 169 Expect(k8sClient.Delete(ctx, leaderPod)).Should(Succeed()) 170 171 By("Waiting for pod recovered and new leader elected") 172 Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(sts), 173 func(g Gomega, sts *appsv1.StatefulSet) { 174 g.Expect(sts.Status.AvailableReplicas == 3).To(BeTrue()) 175 })).Should(Succeed()) 176 177 Eventually(func() string { 178 return getRole(&externalSvc) 179 }).Should(Equal(leader)) 180 } 181 182 // Scenarios 183 184 Context("with MySQL defined as Consensus type and three replicas", func() { 185 BeforeEach(func() { 186 By("Create configmap") 187 _ = testapps.CreateCustomizedObj(&testCtx, "resources/mysql-scripts.yaml", &corev1.ConfigMap{}, 188 testapps.WithName(scriptConfigName), testCtx.UseDefaultNamespace()) 189 190 By("Create a clusterDef obj") 191 mode := int32(0755) 192 clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). 193 SetConnectionCredential(map[string]string{"username": "root", "password": ""}, nil). 194 AddComponentDef(testapps.ConsensusMySQLComponent, mysqlCompDefName). 195 AddScriptTemplate(scriptConfigName, scriptConfigName, testCtx.DefaultNamespace, testapps.ScriptsVolumeName, &mode). 196 AddContainerEnv(testapps.DefaultMySQLContainerName, corev1.EnvVar{Name: "MYSQL_ALLOW_EMPTY_PASSWORD", Value: "yes"}). 197 Create(&testCtx).GetObject() 198 199 By("Create a clusterVersion obj") 200 clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). 201 AddComponentVersion(mysqlCompDefName).AddContainerShort(testapps.DefaultMySQLContainerName, testapps.ApeCloudMySQLImage). 202 Create(&testCtx).GetObject() 203 204 }) 205 206 It("should have one leader pod and two follower pods, and the service routes to the leader pod", func() { 207 testThreeReplicasAndFailover() 208 }) 209 }) 210 })