github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/test/integration/mysql_scale_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  	"fmt"
    21  
    22  	. "github.com/onsi/ginkgo/v2"
    23  	. "github.com/onsi/gomega"
    24  
    25  	appsv1 "k8s.io/api/apps/v1"
    26  	corev1 "k8s.io/api/core/v1"
    27  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    28  	"k8s.io/apimachinery/pkg/api/resource"
    29  	"k8s.io/apimachinery/pkg/types"
    30  	"sigs.k8s.io/controller-runtime/pkg/client"
    31  
    32  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    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 Scaling 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  
    47  	// Cleanups
    48  
    49  	cleanAll := func() {
    50  		// must wait until resources deleted and no longer exist before the testcases start,
    51  		// otherwise if later it needs to create some new resource objects with the same name,
    52  		// in race conditions, it will find the existence of old objects, resulting failure to
    53  		// create the new objects.
    54  		By("clean resources")
    55  
    56  		// delete cluster(and all dependent sub-resources), clusterversion and clusterdef
    57  		testapps.ClearClusterResources(&testCtx)
    58  
    59  		inNS := client.InNamespace(testCtx.DefaultNamespace)
    60  		ml := client.HasLabels{testCtx.TestObjLabelKey}
    61  		// namespaced
    62  		testapps.ClearResources(&testCtx, intctrlutil.OpsRequestSignature, inNS, ml)
    63  		testapps.ClearResources(&testCtx, intctrlutil.ConfigMapSignature, inNS, ml)
    64  	}
    65  
    66  	BeforeEach(cleanAll)
    67  
    68  	AfterEach(cleanAll)
    69  
    70  	// Testcases
    71  
    72  	var (
    73  		clusterDefObj     *appsv1alpha1.ClusterDefinition
    74  		clusterVersionObj *appsv1alpha1.ClusterVersion
    75  		clusterObj        *appsv1alpha1.Cluster
    76  		clusterKey        types.NamespacedName
    77  	)
    78  
    79  	testVerticalScaleCPUAndMemory := func() {
    80  		const opsName = "mysql-verticalscaling"
    81  
    82  		By("Create a cluster obj")
    83  		resources := corev1.ResourceRequirements{
    84  			Limits: corev1.ResourceList{
    85  				"cpu":    resource.MustParse("800m"),
    86  				"memory": resource.MustParse("512Mi"),
    87  			},
    88  			Requests: corev1.ResourceList{
    89  				"cpu":    resource.MustParse("500m"),
    90  				"memory": resource.MustParse("256Mi"),
    91  			},
    92  		}
    93  		clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix,
    94  			clusterDefObj.Name, clusterVersionObj.Name).WithRandomName().
    95  			AddComponent(mysqlCompName, mysqlCompDefName).
    96  			SetResources(resources).SetReplicas(1).
    97  			Create(&testCtx).GetObject()
    98  		clusterKey = client.ObjectKeyFromObject(clusterObj)
    99  		Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(1))
   100  
   101  		By("check cluster running")
   102  		Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, cluster *appsv1alpha1.Cluster) {
   103  			g.Expect(cluster.Status.Phase).To(Equal(appsv1alpha1.RunningClusterPhase))
   104  		})).Should(Succeed())
   105  
   106  		By("send VerticalScalingOpsRequest successfully")
   107  		opsKey := types.NamespacedName{Name: opsName, Namespace: testCtx.DefaultNamespace}
   108  		verticalScalingOpsRequest := testapps.NewOpsRequestObj(opsKey.Name, opsKey.Namespace,
   109  			clusterObj.Name, appsv1alpha1.VerticalScalingType)
   110  		verticalScalingOpsRequest.Spec.TTLSecondsAfterSucceed = 0
   111  		verticalScalingOpsRequest.Spec.VerticalScalingList = []appsv1alpha1.VerticalScaling{
   112  			{
   113  				ComponentOps: appsv1alpha1.ComponentOps{ComponentName: mysqlCompName},
   114  				ResourceRequirements: corev1.ResourceRequirements{
   115  					Requests: corev1.ResourceList{
   116  						"cpu":    resource.MustParse("400m"),
   117  						"memory": resource.MustParse("300Mi"),
   118  					},
   119  				},
   120  			},
   121  		}
   122  		Expect(testCtx.CreateObj(testCtx.Ctx, verticalScalingOpsRequest)).Should(Succeed())
   123  
   124  		By("check VerticalScalingOpsRequest succeed")
   125  		Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(verticalScalingOpsRequest),
   126  			func(g Gomega, ops *appsv1alpha1.OpsRequest) {
   127  				g.Expect(ops.Status.Phase == appsv1alpha1.OpsSucceedPhase).To(BeTrue())
   128  			})).Should(Succeed())
   129  
   130  		By("check cluster resource requirements changed")
   131  		Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, fetched *appsv1alpha1.Cluster) {
   132  			g.Expect(fetched.Spec.ComponentSpecs[0].Resources.Requests).To(Equal(
   133  				verticalScalingOpsRequest.Spec.VerticalScalingList[0].Requests))
   134  		})).Should(Succeed())
   135  
   136  		By("check OpsRequest reclaimed after ttl")
   137  		Expect(testapps.ChangeObj(&testCtx, verticalScalingOpsRequest, func(lopsReq *appsv1alpha1.OpsRequest) {
   138  			lopsReq.Spec.TTLSecondsAfterSucceed = 1
   139  		})).Should(Succeed())
   140  
   141  		By("OpsRequest reclaimed after ttl")
   142  		Eventually(func() error {
   143  			return k8sClient.Get(testCtx.Ctx, client.ObjectKeyFromObject(verticalScalingOpsRequest), verticalScalingOpsRequest)
   144  		}).Should(Satisfy(apierrors.IsNotFound))
   145  	}
   146  
   147  	testVerticalScaleStorage := func() {
   148  		oldStorageValue := resource.MustParse("1Gi")
   149  		newStorageValue := resource.MustParse("2Gi")
   150  
   151  		By("Check StorageClass")
   152  		defaultStorageClass := testk8s.GetDefaultStorageClass(&testCtx)
   153  		if defaultStorageClass == nil {
   154  			Skip("No default StorageClass found")
   155  		} else if !(defaultStorageClass.AllowVolumeExpansion != nil && *defaultStorageClass.AllowVolumeExpansion) {
   156  			Skip("Default StorageClass doesn't allow resize")
   157  		}
   158  
   159  		By("Create a cluster obj with both log and data volume of 1GB size")
   160  		dataPvcSpec := testapps.NewPVCSpec(oldStorageValue.String())
   161  		logPvcSpec := dataPvcSpec
   162  		logPvcSpec.StorageClassName = &defaultStorageClass.Name
   163  		clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix,
   164  			clusterDefObj.Name, clusterVersionObj.Name).WithRandomName().
   165  			AddComponent(mysqlCompName, mysqlCompDefName).
   166  			AddVolumeClaimTemplate(testapps.DataVolumeName, dataPvcSpec).
   167  			AddVolumeClaimTemplate(testapps.LogVolumeName, logPvcSpec).
   168  			Create(&testCtx).GetObject()
   169  		clusterKey = client.ObjectKeyFromObject(clusterObj)
   170  
   171  		Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(1))
   172  
   173  		By("Check the replicas")
   174  		Eventually(func(g Gomega) {
   175  			stsList := &appsv1.StatefulSetList{}
   176  			g.Expect(k8sClient.List(testCtx.Ctx, stsList, client.MatchingLabels{
   177  				constant.AppInstanceLabelKey: clusterKey.Name,
   178  			}, client.InNamespace(clusterKey.Namespace))).To(Succeed())
   179  			g.Expect(len(stsList.Items) > 0).To(BeTrue())
   180  			sts := &stsList.Items[0]
   181  			g.Expect(sts.Spec.Replicas).NotTo(BeNil())
   182  			g.Expect(sts.Status.AvailableReplicas).To(Equal(*sts.Spec.Replicas))
   183  		}).Should(Succeed())
   184  
   185  		By("Check the pvc")
   186  		Eventually(func() bool {
   187  			pvcList := &corev1.PersistentVolumeClaimList{}
   188  			Expect(k8sClient.List(testCtx.Ctx, pvcList, client.InNamespace(clusterKey.Namespace))).Should(Succeed())
   189  			return len(pvcList.Items) != 0
   190  		}).Should(BeTrue())
   191  
   192  		By("Update volume size")
   193  		Eventually(testapps.GetAndChangeObj(&testCtx, clusterKey, func(fetched *appsv1alpha1.Cluster) {
   194  			comp := &fetched.Spec.ComponentSpecs[0]
   195  			comp.VolumeClaimTemplates[0].Spec.Resources.Requests[corev1.ResourceStorage] = newStorageValue
   196  			comp.VolumeClaimTemplates[1].Spec.Resources.Requests[corev1.ResourceStorage] = newStorageValue
   197  		})).Should(Succeed())
   198  
   199  		Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(2))
   200  
   201  		By("Checking the PVC")
   202  		stsList := testk8s.ListAndCheckStatefulSet(&testCtx, clusterKey)
   203  		for _, sts := range stsList.Items {
   204  			for _, vct := range sts.Spec.VolumeClaimTemplates {
   205  				for i := *sts.Spec.Replicas - 1; i >= 0; i-- {
   206  					pvc := &corev1.PersistentVolumeClaim{}
   207  					pvcKey := types.NamespacedName{
   208  						Namespace: clusterKey.Namespace,
   209  						Name:      fmt.Sprintf("%s-%s-%d", vct.Name, sts.Name, i),
   210  					}
   211  					Expect(k8sClient.Get(testCtx.Ctx, pvcKey, pvc)).Should(Succeed())
   212  					Expect(pvc.Spec.Resources.Requests[corev1.ResourceStorage]).To(Equal(newStorageValue))
   213  				}
   214  			}
   215  		}
   216  	}
   217  
   218  	// Scenarios
   219  
   220  	Context("with MySQL defined as a stateful component", func() {
   221  		BeforeEach(func() {
   222  			_ = testapps.CreateCustomizedObj(&testCtx, "resources/mysql-scripts.yaml", &corev1.ConfigMap{},
   223  				testapps.WithName(scriptConfigName), testCtx.UseDefaultNamespace())
   224  
   225  			By("Create a clusterDef obj")
   226  			mode := int32(0755)
   227  			clusterDefObj = testapps.NewClusterDefFactory(clusterDefName).
   228  				AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompDefName).
   229  				AddScriptTemplate(scriptConfigName, scriptConfigName, testCtx.DefaultNamespace, testapps.ScriptsVolumeName, &mode).
   230  				Create(&testCtx).GetObject()
   231  
   232  			By("Create a clusterVersion obj")
   233  			clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()).
   234  				AddComponentVersion(mysqlCompDefName).AddContainerShort(testapps.DefaultMySQLContainerName, testapps.ApeCloudMySQLImage).
   235  				Create(&testCtx).GetObject()
   236  		})
   237  
   238  		It("should handle VerticalScalingOpsRequest and change Cluster's cpu&memory requirements", func() {
   239  			testVerticalScaleCPUAndMemory()
   240  		})
   241  
   242  		It("should handle PVC resize requests if cluster has storage class which enables dynamic-provisioning", func() {
   243  			testVerticalScaleStorage()
   244  		})
   245  	})
   246  
   247  	Context("with MySQL defined as a consensus component", func() {
   248  		BeforeEach(func() {
   249  			By("Create configmap")
   250  			_ = testapps.CreateCustomizedObj(&testCtx, "resources/mysql-scripts.yaml", &corev1.ConfigMap{},
   251  				testapps.WithName(scriptConfigName), testCtx.UseDefaultNamespace())
   252  
   253  			By("Create a clusterDef obj")
   254  			mode := int32(0755)
   255  			clusterDefObj = testapps.NewClusterDefFactory(clusterDefName).
   256  				AddComponentDef(testapps.ConsensusMySQLComponent, mysqlCompDefName).
   257  				AddScriptTemplate(scriptConfigName, scriptConfigName, testCtx.DefaultNamespace, testapps.ScriptsVolumeName, &mode).
   258  				Create(&testCtx).GetObject()
   259  
   260  			By("Create a clusterVersion obj")
   261  			clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()).
   262  				AddComponentVersion(mysqlCompDefName).AddContainerShort(testapps.DefaultMySQLContainerName, testapps.ApeCloudMySQLImage).
   263  				Create(&testCtx).GetObject()
   264  		})
   265  
   266  		It("should handle VerticalScalingOpsRequest and change Cluster's cpu&memory requirements", func() {
   267  			testVerticalScaleCPUAndMemory()
   268  		})
   269  
   270  		It("should handle PVC resize requests if cluster has storage class which enables dynamic-provisioning", func() {
   271  			testVerticalScaleStorage()
   272  		})
   273  	})
   274  })