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  })