github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/controllers/apps/opsrequest_controller_test.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package apps
    21  
    22  import (
    23  	"fmt"
    24  	"reflect"
    25  	"time"
    26  
    27  	. "github.com/onsi/ginkgo/v2"
    28  	. "github.com/onsi/gomega"
    29  
    30  	snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
    31  	"golang.org/x/exp/slices"
    32  	appsv1 "k8s.io/api/apps/v1"
    33  	corev1 "k8s.io/api/core/v1"
    34  	"k8s.io/apimachinery/pkg/api/meta"
    35  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    36  	"k8s.io/apimachinery/pkg/types"
    37  	"sigs.k8s.io/controller-runtime/pkg/client"
    38  
    39  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    40  	dpv1alpha1 "github.com/1aal/kubeblocks/apis/dataprotection/v1alpha1"
    41  	workloads "github.com/1aal/kubeblocks/apis/workloads/v1alpha1"
    42  	opsutil "github.com/1aal/kubeblocks/controllers/apps/operations/util"
    43  	"github.com/1aal/kubeblocks/pkg/constant"
    44  	dptypes "github.com/1aal/kubeblocks/pkg/dataprotection/types"
    45  	intctrlutil "github.com/1aal/kubeblocks/pkg/generics"
    46  	lorry "github.com/1aal/kubeblocks/pkg/lorry/client"
    47  	testapps "github.com/1aal/kubeblocks/pkg/testutil/apps"
    48  	testdp "github.com/1aal/kubeblocks/pkg/testutil/dataprotection"
    49  	testk8s "github.com/1aal/kubeblocks/pkg/testutil/k8s"
    50  )
    51  
    52  var _ = Describe("OpsRequest Controller", func() {
    53  	const clusterDefName = "test-clusterdef"
    54  	const clusterVersionName = "test-clusterversion"
    55  	const clusterNamePrefix = "test-cluster"
    56  	const mysqlCompDefName = "mysql"
    57  	const mysqlCompName = "mysql"
    58  	const defaultMinReadySeconds = 10
    59  
    60  	cleanEnv := func() {
    61  		// must wait till resources deleted and no longer existed before the testcases start,
    62  		// otherwise if later it needs to create some new resource objects with the same name,
    63  		// in race conditions, it will find the existence of old objects, resulting failure to
    64  		// create the new objects.
    65  		By("clean resources")
    66  
    67  		inNS := client.InNamespace(testCtx.DefaultNamespace)
    68  		ml := client.HasLabels{testCtx.TestObjLabelKey}
    69  
    70  		testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, intctrlutil.PodSignature, true, inNS, ml)
    71  		testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, intctrlutil.OpsRequestSignature, true, inNS, ml)
    72  		testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, intctrlutil.VolumeSnapshotSignature, true, inNS)
    73  
    74  		// delete cluster(and all dependent sub-resources), clusterversion and clusterdef
    75  		// TODO(review): why finalizers not removed
    76  		testapps.ClearClusterResourcesWithRemoveFinalizerOption(&testCtx)
    77  		testapps.ClearResources(&testCtx, intctrlutil.StorageClassSignature, ml)
    78  
    79  		// non-namespaced
    80  		testapps.ClearResources(&testCtx, intctrlutil.BackupPolicyTemplateSignature, ml)
    81  		testapps.ClearResources(&testCtx, intctrlutil.ComponentResourceConstraintSignature, ml)
    82  		testapps.ClearResources(&testCtx, intctrlutil.ComponentClassDefinitionSignature, ml)
    83  	}
    84  
    85  	BeforeEach(func() {
    86  		cleanEnv()
    87  	})
    88  
    89  	AfterEach(func() {
    90  		cleanEnv()
    91  	})
    92  
    93  	var (
    94  		clusterDefObj     *appsv1alpha1.ClusterDefinition
    95  		clusterVersionObj *appsv1alpha1.ClusterVersion
    96  		clusterObj        *appsv1alpha1.Cluster
    97  		clusterKey        types.NamespacedName
    98  	)
    99  
   100  	mockSetClusterStatusPhaseToRunning := func(namespacedName types.NamespacedName) {
   101  		Expect(testapps.GetAndChangeObjStatus(&testCtx, namespacedName,
   102  			func(fetched *appsv1alpha1.Cluster) {
   103  				// TODO: would be better to have hint for cluster.status.phase is mocked,
   104  				// i.e., add annotation info for the mocked context
   105  				fetched.Status.Phase = appsv1alpha1.RunningClusterPhase
   106  				if len(fetched.Status.Components) == 0 {
   107  					fetched.Status.Components = map[string]appsv1alpha1.ClusterComponentStatus{}
   108  					for _, v := range fetched.Spec.ComponentSpecs {
   109  						fetched.Status.SetComponentStatus(v.Name, appsv1alpha1.ClusterComponentStatus{
   110  							Phase: appsv1alpha1.RunningClusterCompPhase,
   111  						})
   112  					}
   113  					return
   114  				}
   115  				for componentKey, componentStatus := range fetched.Status.Components {
   116  					componentStatus.Phase = appsv1alpha1.RunningClusterCompPhase
   117  					fetched.Status.SetComponentStatus(componentKey, componentStatus)
   118  				}
   119  			})()).ShouldNot(HaveOccurred())
   120  	}
   121  
   122  	type resourceContext struct {
   123  		class    *appsv1alpha1.ComponentClass
   124  		resource corev1.ResourceRequirements
   125  	}
   126  
   127  	type verticalScalingContext struct {
   128  		source resourceContext
   129  		target resourceContext
   130  	}
   131  
   132  	testVerticalScaleCPUAndMemory := func(workloadType testapps.ComponentDefTplType, scalingCtx verticalScalingContext) {
   133  		const opsName = "mysql-verticalscaling"
   134  
   135  		By("Create class related objects")
   136  		testapps.NewComponentResourceConstraintFactory(testapps.DefaultResourceConstraintName).
   137  			AddConstraints(testapps.GeneralResourceConstraint).
   138  			AddSelector(appsv1alpha1.ClusterResourceConstraintSelector{
   139  				ClusterDefRef: clusterDefName,
   140  				Components: []appsv1alpha1.ComponentResourceConstraintSelector{
   141  					{
   142  						ComponentDefRef: mysqlCompDefName,
   143  						Rules:           []string{"c1", "c2", "c3", "c4"},
   144  					},
   145  				},
   146  			}).
   147  			Create(&testCtx).GetObject()
   148  
   149  		testapps.NewComponentClassDefinitionFactory("custom", clusterDefObj.Name, mysqlCompDefName).
   150  			AddClasses([]appsv1alpha1.ComponentClass{testapps.Class1c1g, testapps.Class2c4g}).
   151  			Create(&testCtx)
   152  
   153  		By("Create a cluster obj")
   154  		clusterFactory := testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix,
   155  			clusterDefObj.Name, clusterVersionObj.Name).WithRandomName().
   156  			AddComponent(mysqlCompName, mysqlCompDefName).
   157  			SetReplicas(1)
   158  		if scalingCtx.source.class != nil {
   159  			clusterFactory.SetClassDefRef(&appsv1alpha1.ClassDefRef{Class: scalingCtx.source.class.Name})
   160  		} else {
   161  			clusterFactory.SetResources(scalingCtx.source.resource)
   162  		}
   163  		clusterObj = clusterFactory.Create(&testCtx).GetObject()
   164  		clusterKey = client.ObjectKeyFromObject(clusterObj)
   165  
   166  		By("Waiting for the cluster enters creating phase")
   167  		Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.CreatingClusterPhase))
   168  
   169  		By("mock pod/sts are available and wait for cluster enter running phase")
   170  		podName := fmt.Sprintf("%s-%s-0", clusterObj.Name, mysqlCompName)
   171  		pod := testapps.MockConsensusComponentStsPod(&testCtx, nil, clusterObj.Name, mysqlCompName,
   172  			podName, "leader", "ReadWrite")
   173  		// the opsRequest will use startTime to check some condition.
   174  		// if there is no sleep for 1 second, unstable error may occur.
   175  		time.Sleep(time.Second)
   176  		if workloadType == testapps.StatefulMySQLComponent {
   177  			lastTransTime := metav1.NewTime(time.Now().Add(-1 * (defaultMinReadySeconds + 1) * time.Second))
   178  			Expect(testapps.ChangeObjStatus(&testCtx, pod, func() {
   179  				testk8s.MockPodAvailable(pod, lastTransTime)
   180  			})).ShouldNot(HaveOccurred())
   181  		}
   182  		var mysqlSts *appsv1.StatefulSet
   183  		var mysqlRSM *workloads.ReplicatedStateMachine
   184  		rsmList := testk8s.ListAndCheckRSMWithComponent(&testCtx, clusterKey, mysqlCompName)
   185  		mysqlRSM = &rsmList.Items[0]
   186  		mysqlSts = testapps.NewStatefulSetFactory(mysqlRSM.Namespace, mysqlRSM.Name, clusterKey.Name, mysqlCompName).
   187  			SetReplicas(*mysqlRSM.Spec.Replicas).Create(&testCtx).GetObject()
   188  		Expect(testapps.ChangeObjStatus(&testCtx, mysqlSts, func() {
   189  			testk8s.MockStatefulSetReady(mysqlSts)
   190  		})).ShouldNot(HaveOccurred())
   191  		Expect(testapps.ChangeObjStatus(&testCtx, mysqlRSM, func() {
   192  			testk8s.MockRSMReady(mysqlRSM, pod)
   193  		})).ShouldNot(HaveOccurred())
   194  		Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.RunningClusterPhase))
   195  
   196  		By("send VerticalScalingOpsRequest successfully")
   197  		opsKey := types.NamespacedName{Name: opsName, Namespace: testCtx.DefaultNamespace}
   198  		verticalScalingOpsRequest := testapps.NewOpsRequestObj(opsKey.Name, opsKey.Namespace,
   199  			clusterObj.Name, appsv1alpha1.VerticalScalingType)
   200  		if scalingCtx.target.class != nil {
   201  			verticalScalingOpsRequest.Spec.VerticalScalingList = []appsv1alpha1.VerticalScaling{
   202  				{
   203  					ComponentOps: appsv1alpha1.ComponentOps{ComponentName: mysqlCompName},
   204  					ClassDefRef: &appsv1alpha1.ClassDefRef{
   205  						Class: scalingCtx.target.class.Name,
   206  					},
   207  				},
   208  			}
   209  		} else {
   210  			verticalScalingOpsRequest.Spec.VerticalScalingList = []appsv1alpha1.VerticalScaling{
   211  				{
   212  					ComponentOps:         appsv1alpha1.ComponentOps{ComponentName: mysqlCompName},
   213  					ResourceRequirements: scalingCtx.target.resource,
   214  				},
   215  			}
   216  		}
   217  		Expect(testCtx.CreateObj(testCtx.Ctx, verticalScalingOpsRequest)).Should(Succeed())
   218  
   219  		By("wait for VerticalScalingOpsRequest is running")
   220  		Eventually(testapps.GetOpsRequestPhase(&testCtx, opsKey)).Should(Equal(appsv1alpha1.OpsRunningPhase))
   221  		Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.UpdatingClusterPhase))
   222  		Eventually(testapps.GetClusterComponentPhase(&testCtx, clusterKey, mysqlCompName)).Should(Equal(appsv1alpha1.UpdatingClusterCompPhase))
   223  		// TODO(refactor): try to check some ephemeral states?
   224  		// checkLatestOpsIsProcessing(clusterKey, verticalScalingOpsRequest.Spec.Type)
   225  
   226  		// By("check Cluster and changed component phase is VerticalScaling")
   227  		// Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, cluster *appsv1alpha1.Cluster) {
   228  		//	g.Expect(cluster.Status.Phase).To(Equal(appsv1alpha1.SpecReconcilingClusterPhase))
   229  		//	g.Expect(cluster.Status.Components[mysqlCompName].Phase).To(Equal(appsv1alpha1.SpecReconcilingClusterCompPhase))
   230  		// })).Should(Succeed())
   231  
   232  		By("mock bring Cluster and changed component back to running status")
   233  		Expect(testapps.GetAndChangeObjStatus(&testCtx, client.ObjectKeyFromObject(mysqlRSM), func(tmpRSM *workloads.ReplicatedStateMachine) {
   234  			testk8s.MockRSMReady(tmpRSM, pod)
   235  		})()).ShouldNot(HaveOccurred())
   236  		Eventually(testapps.GetClusterComponentPhase(&testCtx, clusterKey, mysqlCompName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase))
   237  		Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.RunningClusterPhase))
   238  		// checkLatestOpsHasProcessed(clusterKey)
   239  
   240  		By("notice opsrequest controller to run")
   241  		Expect(testapps.ChangeObj(&testCtx, verticalScalingOpsRequest, func(lopsReq *appsv1alpha1.OpsRequest) {
   242  			if lopsReq.Annotations == nil {
   243  				lopsReq.Annotations = map[string]string{}
   244  			}
   245  			lopsReq.Annotations[constant.ReconcileAnnotationKey] = time.Now().Format(time.RFC3339Nano)
   246  		})).ShouldNot(HaveOccurred())
   247  
   248  		By("check VerticalScalingOpsRequest succeed")
   249  		Eventually(testapps.GetOpsRequestPhase(&testCtx, opsKey)).Should(Equal(appsv1alpha1.OpsSucceedPhase))
   250  
   251  		By("check cluster resource requirements changed")
   252  		var targetRequests corev1.ResourceList
   253  		if scalingCtx.target.class != nil {
   254  			targetRequests = corev1.ResourceList{
   255  				corev1.ResourceCPU:    scalingCtx.target.class.CPU,
   256  				corev1.ResourceMemory: scalingCtx.target.class.Memory,
   257  			}
   258  		} else {
   259  			targetRequests = scalingCtx.target.resource.Requests
   260  		}
   261  
   262  		rsmList = testk8s.ListAndCheckRSMWithComponent(&testCtx, clusterKey, mysqlCompName)
   263  		mysqlRSM = &rsmList.Items[0]
   264  		Expect(reflect.DeepEqual(mysqlRSM.Spec.Template.Spec.Containers[0].Resources.Requests, targetRequests)).Should(BeTrue())
   265  
   266  		By("check OpsRequest reclaimed after ttl")
   267  		Expect(testapps.ChangeObj(&testCtx, verticalScalingOpsRequest, func(lopsReq *appsv1alpha1.OpsRequest) {
   268  			lopsReq.Spec.TTLSecondsAfterSucceed = 1
   269  		})).ShouldNot(HaveOccurred())
   270  
   271  		Eventually(testapps.CheckObjExists(&testCtx, client.ObjectKeyFromObject(verticalScalingOpsRequest), verticalScalingOpsRequest, false)).Should(Succeed())
   272  	}
   273  
   274  	// Scenarios
   275  
   276  	// TODO: should focus on OpsRequest control actions, and iterator through all component workload types.
   277  	Context("with Cluster which has MySQL StatefulSet", func() {
   278  		BeforeEach(func() {
   279  			By("Create a clusterDefinition obj")
   280  			clusterDefObj = testapps.NewClusterDefFactory(clusterDefName).
   281  				AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompDefName).
   282  				Create(&testCtx).GetObject()
   283  
   284  			By("Create a clusterVersion obj")
   285  			clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()).
   286  				AddComponentVersion(mysqlCompDefName).AddContainerShort("mysql", testapps.ApeCloudMySQLImage).
   287  				Create(&testCtx).GetObject()
   288  		})
   289  
   290  		It("create cluster by class, vertical scaling by resource", func() {
   291  			ctx := verticalScalingContext{
   292  				source: resourceContext{class: &testapps.Class1c1g},
   293  				target: resourceContext{resource: testapps.Class2c4g.ToResourceRequirements()},
   294  			}
   295  			testVerticalScaleCPUAndMemory(testapps.StatefulMySQLComponent, ctx)
   296  		})
   297  
   298  		It("create cluster by class, vertical scaling by class", func() {
   299  			ctx := verticalScalingContext{
   300  				source: resourceContext{class: &testapps.Class1c1g},
   301  				target: resourceContext{class: &testapps.Class2c4g},
   302  			}
   303  			testVerticalScaleCPUAndMemory(testapps.StatefulMySQLComponent, ctx)
   304  		})
   305  
   306  		It("create cluster by resource, vertical scaling by class", func() {
   307  			ctx := verticalScalingContext{
   308  				source: resourceContext{resource: testapps.Class1c1g.ToResourceRequirements()},
   309  				target: resourceContext{class: &testapps.Class2c4g},
   310  			}
   311  			testVerticalScaleCPUAndMemory(testapps.StatefulMySQLComponent, ctx)
   312  		})
   313  
   314  		It("create cluster by resource, vertical scaling by resource", func() {
   315  			ctx := verticalScalingContext{
   316  				source: resourceContext{resource: testapps.Class1c1g.ToResourceRequirements()},
   317  				target: resourceContext{resource: testapps.Class2c4g.ToResourceRequirements()},
   318  			}
   319  			testVerticalScaleCPUAndMemory(testapps.StatefulMySQLComponent, ctx)
   320  		})
   321  	})
   322  
   323  	Context("with Cluster which has MySQL ConsensusSet", func() {
   324  		BeforeEach(func() {
   325  			By("Create a clusterDefinition obj")
   326  			testk8s.MockEnableVolumeSnapshot(&testCtx, testk8s.DefaultStorageClassName)
   327  			clusterDefObj = testapps.NewClusterDefFactory(clusterDefName).
   328  				AddComponentDef(testapps.ConsensusMySQLComponent, mysqlCompDefName).
   329  				AddHorizontalScalePolicy(appsv1alpha1.HorizontalScalePolicy{
   330  					Type:                     appsv1alpha1.HScaleDataClonePolicyCloneVolume,
   331  					BackupPolicyTemplateName: backupPolicyTPLName,
   332  				}).Create(&testCtx).GetObject()
   333  
   334  			By("Create a clusterVersion obj")
   335  			clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()).
   336  				AddComponentVersion(mysqlCompDefName).AddContainerShort("mysql", testapps.ApeCloudMySQLImage).
   337  				Create(&testCtx).GetObject()
   338  		})
   339  
   340  		componentWorkload := func() client.Object {
   341  			rsmList := testk8s.ListAndCheckRSMWithComponent(&testCtx, clusterKey, mysqlCompName)
   342  			return &rsmList.Items[0]
   343  		}
   344  
   345  		mockCompRunning := func(replicas int32) {
   346  			wl := componentWorkload()
   347  			rsm, _ := wl.(*workloads.ReplicatedStateMachine)
   348  			sts := testapps.NewStatefulSetFactory(rsm.Namespace, rsm.Name, clusterKey.Name, mysqlCompName).
   349  				SetReplicas(*rsm.Spec.Replicas).GetObject()
   350  			testapps.CheckedCreateK8sResource(&testCtx, sts)
   351  
   352  			mockPods := testapps.MockConsensusComponentPods(&testCtx, sts, clusterObj.Name, mysqlCompName)
   353  			rsm, _ = wl.(*workloads.ReplicatedStateMachine)
   354  			Expect(testapps.ChangeObjStatus(&testCtx, rsm, func() {
   355  				testk8s.MockRSMReady(rsm, mockPods...)
   356  			})).ShouldNot(HaveOccurred())
   357  			Expect(testapps.ChangeObjStatus(&testCtx, sts, func() {
   358  				testk8s.MockStatefulSetReady(sts)
   359  			})).ShouldNot(HaveOccurred())
   360  
   361  			Eventually(testapps.GetClusterComponentPhase(&testCtx, clusterKey, mysqlCompName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase))
   362  		}
   363  
   364  		createMysqlCluster := func(replicas int32) {
   365  			createBackupPolicyTpl(clusterDefObj)
   366  
   367  			By("set component to horizontal with snapshot policy and create a cluster")
   368  			testk8s.MockEnableVolumeSnapshot(&testCtx, testk8s.DefaultStorageClassName)
   369  			if clusterDefObj.Spec.ComponentDefs[0].HorizontalScalePolicy == nil {
   370  				Expect(testapps.GetAndChangeObj(&testCtx, client.ObjectKeyFromObject(clusterDefObj),
   371  					func(clusterDef *appsv1alpha1.ClusterDefinition) {
   372  						clusterDef.Spec.ComponentDefs[0].HorizontalScalePolicy =
   373  							&appsv1alpha1.HorizontalScalePolicy{Type: appsv1alpha1.HScaleDataClonePolicyCloneVolume}
   374  					})()).ShouldNot(HaveOccurred())
   375  			}
   376  			pvcSpec := testapps.NewPVCSpec("1Gi")
   377  			clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix,
   378  				clusterDefObj.Name, clusterVersionObj.Name).WithRandomName().
   379  				AddComponent(mysqlCompName, mysqlCompDefName).
   380  				SetReplicas(replicas).
   381  				AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec).
   382  				Create(&testCtx).GetObject()
   383  			clusterKey = client.ObjectKeyFromObject(clusterObj)
   384  
   385  			By("mock component is Running")
   386  			mockCompRunning(replicas)
   387  
   388  			By("mock pvc created")
   389  			for i := 0; i < int(replicas); i++ {
   390  				pvcName := fmt.Sprintf("%s-%s-%s-%d", testapps.DataVolumeName, clusterKey.Name, mysqlCompName, i)
   391  				pvc := testapps.NewPersistentVolumeClaimFactory(testCtx.DefaultNamespace, pvcName, clusterKey.Name,
   392  					mysqlCompName, testapps.DataVolumeName).
   393  					SetStorage("1Gi").
   394  					SetStorageClass(testk8s.DefaultStorageClassName).
   395  					Create(&testCtx).
   396  					GetObject()
   397  				// mock pvc bound
   398  				Expect(testapps.GetAndChangeObjStatus(&testCtx, client.ObjectKeyFromObject(pvc), func(pvc *corev1.PersistentVolumeClaim) {
   399  					pvc.Status.Phase = corev1.ClaimBound
   400  				})()).ShouldNot(HaveOccurred())
   401  			}
   402  			// wait for cluster observed generation
   403  			Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(1))
   404  			mockSetClusterStatusPhaseToRunning(clusterKey)
   405  			Eventually(testapps.GetClusterComponentPhase(&testCtx, clusterKey, mysqlCompName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase))
   406  			Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.RunningClusterPhase))
   407  			Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(1))
   408  		}
   409  
   410  		createClusterHscaleOps := func(replicas int32) *appsv1alpha1.OpsRequest {
   411  			By("create a opsRequest to horizontal scale")
   412  			opsName := "hscale-ops-" + testCtx.GetRandomStr()
   413  			ops := testapps.NewOpsRequestObj(opsName, testCtx.DefaultNamespace,
   414  				clusterObj.Name, appsv1alpha1.HorizontalScalingType)
   415  			ops.Spec.HorizontalScalingList = []appsv1alpha1.HorizontalScaling{
   416  				{
   417  					ComponentOps: appsv1alpha1.ComponentOps{ComponentName: mysqlCompName},
   418  					Replicas:     replicas,
   419  				},
   420  			}
   421  			// for reconciling the ops labels
   422  			ops.Labels = nil
   423  			Expect(testCtx.CreateObj(testCtx.Ctx, ops)).Should(Succeed())
   424  			return ops
   425  		}
   426  
   427  		It("issue an VerticalScalingOpsRequest should change Cluster's resource requirements successfully", func() {
   428  			ctx := verticalScalingContext{
   429  				source: resourceContext{class: &testapps.Class1c1g},
   430  				target: resourceContext{resource: testapps.Class2c4g.ToResourceRequirements()},
   431  			}
   432  			testVerticalScaleCPUAndMemory(testapps.ConsensusMySQLComponent, ctx)
   433  		})
   434  
   435  		It("HorizontalScaling when not support snapshot", func() {
   436  			By("init backup policy template, mysql cluster and hscale ops")
   437  			testk8s.MockDisableVolumeSnapshot(&testCtx, testk8s.DefaultStorageClassName)
   438  
   439  			createMysqlCluster(3)
   440  			cluster := &appsv1alpha1.Cluster{}
   441  			Expect(testCtx.Cli.Get(testCtx.Ctx, clusterKey, cluster)).Should(Succeed())
   442  			initGeneration := cluster.Status.ObservedGeneration
   443  			Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(Equal(initGeneration))
   444  
   445  			ops := createClusterHscaleOps(5)
   446  			opsKey := client.ObjectKeyFromObject(ops)
   447  
   448  			By("expect component is Running if don't support volume snapshot during doing h-scale ops")
   449  			Eventually(testapps.GetOpsRequestPhase(&testCtx, opsKey)).Should(Equal(appsv1alpha1.OpsRunningPhase))
   450  			Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, fetched *appsv1alpha1.Cluster) {
   451  				// the cluster spec has been updated by ops-controller to scale out.
   452  				g.Expect(fetched.Generation == initGeneration+1).Should(BeTrue())
   453  				// expect cluster phase is Updating during Hscale.
   454  				g.Expect(fetched.Generation > fetched.Status.ObservedGeneration).Should(BeTrue())
   455  				g.Expect(fetched.Status.Phase).Should(Equal(appsv1alpha1.UpdatingClusterPhase))
   456  				// when snapshot is not supported, the expected component phase is running.
   457  				g.Expect(fetched.Status.Components[mysqlCompName].Phase).Should(Equal(appsv1alpha1.RunningClusterCompPhase))
   458  				// expect preCheckFailed condition to occur.
   459  				condition := meta.FindStatusCondition(fetched.Status.Conditions, appsv1alpha1.ConditionTypeProvisioningStarted)
   460  				g.Expect(condition).ShouldNot(BeNil())
   461  				g.Expect(condition.Status).Should(BeFalse())
   462  				g.Expect(condition.Reason).Should(Equal(ReasonPreCheckFailed))
   463  				g.Expect(condition.Message).Should(Equal("HorizontalScaleFailed: volume snapshot not support"))
   464  			}))
   465  
   466  			By("reset replicas to 3 and cluster phase should be reconciled to Running")
   467  			Expect(testapps.GetAndChangeObj(&testCtx, clusterKey, func(cluster *appsv1alpha1.Cluster) {
   468  				cluster.Spec.ComponentSpecs[0].Replicas = int32(3)
   469  			})()).ShouldNot(HaveOccurred())
   470  			Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, lcluster *appsv1alpha1.Cluster) {
   471  				g.Expect(lcluster.Generation == initGeneration+2).Should(BeTrue())
   472  				g.Expect(lcluster.Generation == lcluster.Status.ObservedGeneration).Should(BeTrue())
   473  				g.Expect(cluster.Status.Phase).Should(Equal(appsv1alpha1.RunningClusterPhase))
   474  			})).Should(Succeed())
   475  		})
   476  
   477  		It("HorizontalScaling via volume snapshot backup", func() {
   478  			By("init backup policy template, mysql cluster and hscale ops")
   479  			testk8s.MockEnableVolumeSnapshot(&testCtx, testk8s.DefaultStorageClassName)
   480  			oldReplicas := int32(3)
   481  			createMysqlCluster(oldReplicas)
   482  
   483  			replicas := int32(5)
   484  			ops := createClusterHscaleOps(replicas)
   485  			opsKey := client.ObjectKeyFromObject(ops)
   486  
   487  			By("expect cluster and component is reconciling the new spec")
   488  			Eventually(testapps.GetOpsRequestPhase(&testCtx, opsKey)).Should(Equal(appsv1alpha1.OpsRunningPhase))
   489  			Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, cluster *appsv1alpha1.Cluster) {
   490  				g.Expect(cluster.Generation == 2).Should(BeTrue())
   491  				g.Expect(cluster.Status.ObservedGeneration == 2).Should(BeTrue())
   492  				g.Expect(cluster.Status.Components[mysqlCompName].Phase).Should(Equal(appsv1alpha1.UpdatingClusterCompPhase))
   493  				// the expected cluster phase is Updating during Hscale.
   494  				g.Expect(cluster.Status.Phase).Should(Equal(appsv1alpha1.UpdatingClusterPhase))
   495  			})).Should(Succeed())
   496  
   497  			By("mock backup status is ready, component phase should change to Updating when component is horizontally scaling.")
   498  			backupKey := client.ObjectKey{Name: fmt.Sprintf("%s-%s-scaling",
   499  				clusterKey.Name, mysqlCompName), Namespace: testCtx.DefaultNamespace}
   500  			backup := &dpv1alpha1.Backup{}
   501  			Expect(k8sClient.Get(testCtx.Ctx, backupKey, backup)).Should(Succeed())
   502  			backup.Status.Phase = dpv1alpha1.BackupPhaseCompleted
   503  			testdp.MockBackupStatusMethod(backup, testdp.BackupMethodName, testapps.DataVolumeName, testdp.ActionSetName)
   504  			Expect(k8sClient.Status().Update(testCtx.Ctx, backup)).Should(Succeed())
   505  			Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, cluster *appsv1alpha1.Cluster) {
   506  				g.Expect(cluster.Status.Components[mysqlCompName].Phase).Should(Equal(appsv1alpha1.UpdatingClusterCompPhase))
   507  				g.Expect(cluster.Status.Phase).Should(Equal(appsv1alpha1.UpdatingClusterPhase))
   508  			})).Should(Succeed())
   509  
   510  			By("mock create volumesnapshot, which should done by backup controller")
   511  			vs := &snapshotv1.VolumeSnapshot{}
   512  			vs.Name = backupKey.Name
   513  			vs.Namespace = backupKey.Namespace
   514  			vs.Labels = map[string]string{
   515  				dptypes.BackupNameLabelKey: backupKey.Name,
   516  			}
   517  			pvcName := ""
   518  			vs.Spec = snapshotv1.VolumeSnapshotSpec{
   519  				Source: snapshotv1.VolumeSnapshotSource{
   520  					PersistentVolumeClaimName: &pvcName,
   521  				},
   522  			}
   523  			Expect(k8sClient.Create(testCtx.Ctx, vs)).Should(Succeed())
   524  			Eventually(testapps.CheckObjExists(&testCtx, backupKey, vs, true)).Should(Succeed())
   525  
   526  			mockComponentPVCsAndBound := func(comp *appsv1alpha1.ClusterComponentSpec) {
   527  				for i := 0; i < int(replicas); i++ {
   528  					for _, vct := range comp.VolumeClaimTemplates {
   529  						pvcKey := types.NamespacedName{
   530  							Namespace: clusterKey.Namespace,
   531  							Name:      fmt.Sprintf("%s-%s-%s-%d", vct.Name, clusterKey.Name, comp.Name, i),
   532  						}
   533  						testapps.NewPersistentVolumeClaimFactory(testCtx.DefaultNamespace, pvcKey.Name, clusterKey.Name,
   534  							comp.Name, testapps.DataVolumeName).SetStorage(vct.Spec.Resources.Requests.Storage().String()).AddLabelsInMap(map[string]string{
   535  							constant.AppInstanceLabelKey:    clusterKey.Name,
   536  							constant.KBAppComponentLabelKey: comp.Name,
   537  							constant.AppManagedByLabelKey:   constant.AppName,
   538  						}).CheckedCreate(&testCtx)
   539  						Eventually(testapps.GetAndChangeObjStatus(&testCtx, pvcKey, func(pvc *corev1.PersistentVolumeClaim) {
   540  							pvc.Status.Phase = corev1.ClaimBound
   541  							if pvc.Status.Capacity == nil {
   542  								pvc.Status.Capacity = corev1.ResourceList{}
   543  							}
   544  							pvc.Status.Capacity[corev1.ResourceStorage] = pvc.Spec.Resources.Requests[corev1.ResourceStorage]
   545  						})).Should(Succeed())
   546  					}
   547  				}
   548  			}
   549  
   550  			// mock pvcs have restored
   551  			mockComponentPVCsAndBound(clusterObj.Spec.GetComponentByName(mysqlCompName))
   552  			// check restore CR and mock it to Completed
   553  			checkRestoreAndSetCompleted(clusterKey, mysqlCompName, int(replicas-oldReplicas))
   554  
   555  			By("check the underlying workload been updated")
   556  			Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(componentWorkload()),
   557  				func(g Gomega, rsm *workloads.ReplicatedStateMachine) {
   558  					g.Expect(*rsm.Spec.Replicas).Should(Equal(replicas))
   559  				})).Should(Succeed())
   560  			rsm := componentWorkload()
   561  			Eventually(testapps.GetAndChangeObj(&testCtx, client.ObjectKeyFromObject(rsm), func(sts *appsv1.StatefulSet) {
   562  				sts.Spec.Replicas = &replicas
   563  			})).Should(Succeed())
   564  			Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(componentWorkload()),
   565  				func(g Gomega, sts *appsv1.StatefulSet) {
   566  					g.Expect(*sts.Spec.Replicas).Should(Equal(replicas))
   567  				})).Should(Succeed())
   568  
   569  			By("Checking pvc created")
   570  			Eventually(testapps.List(&testCtx, intctrlutil.PersistentVolumeClaimSignature,
   571  				client.MatchingLabels{
   572  					constant.AppInstanceLabelKey:    clusterKey.Name,
   573  					constant.KBAppComponentLabelKey: mysqlCompName,
   574  				}, client.InNamespace(clusterKey.Namespace))).Should(HaveLen(int(replicas)))
   575  
   576  			By("mock all new PVCs scaled bounded")
   577  			for i := 0; i < int(replicas); i++ {
   578  				pvcKey := types.NamespacedName{
   579  					Namespace: testCtx.DefaultNamespace,
   580  					Name:      fmt.Sprintf("%s-%s-%s-%d", testapps.DataVolumeName, clusterKey.Name, mysqlCompName, i),
   581  				}
   582  				Expect(testapps.GetAndChangeObjStatus(&testCtx, pvcKey, func(pvc *corev1.PersistentVolumeClaim) {
   583  					pvc.Status.Phase = corev1.ClaimBound
   584  				})()).Should(Succeed())
   585  			}
   586  
   587  			By("check the backup created for scaling has been deleted")
   588  			Eventually(testapps.CheckObjExists(&testCtx, backupKey, backup, false)).Should(Succeed())
   589  
   590  			By("mock component workload is running and expect cluster and component are running")
   591  			mockCompRunning(replicas)
   592  			Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, cluster *appsv1alpha1.Cluster) {
   593  				g.Expect(cluster.Status.Components[mysqlCompName].Phase).Should(Equal(appsv1alpha1.RunningClusterCompPhase))
   594  				g.Expect(cluster.Status.Phase).Should(Equal(appsv1alpha1.RunningClusterPhase))
   595  			})).Should(Succeed())
   596  			Eventually(testapps.GetOpsRequestPhase(&testCtx, opsKey)).Should(Equal(appsv1alpha1.OpsSucceedPhase))
   597  		})
   598  
   599  		It("HorizontalScaling when the number of pods is inconsistent with the number of replicas", func() {
   600  			newMockLorryClient(clusterKey, mysqlCompName, 2)
   601  			defer lorry.UnsetMockClient()
   602  
   603  			By("create a cluster with 3 pods")
   604  			createMysqlCluster(3)
   605  
   606  			By("mock component replicas to 4 and actual pods is 3")
   607  			Expect(testapps.ChangeObj(&testCtx, clusterObj, func(clusterObj *appsv1alpha1.Cluster) {
   608  				clusterObj.Spec.ComponentSpecs[0].Replicas = 4
   609  			})).Should(Succeed())
   610  
   611  			By("scale down the cluster replicas to 2")
   612  			phase := appsv1alpha1.OpsPendingPhase
   613  			replicas := int32(2)
   614  			ops := createClusterHscaleOps(replicas)
   615  			Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(ops), func(g Gomega, ops *appsv1alpha1.OpsRequest) {
   616  				phases := []appsv1alpha1.OpsPhase{appsv1alpha1.OpsRunningPhase, appsv1alpha1.OpsFailedPhase}
   617  				g.Expect(slices.Contains(phases, ops.Status.Phase)).Should(BeTrue())
   618  				phase = ops.Status.Phase
   619  			})).Should(Succeed())
   620  
   621  			// Since the component replicas is different with running pods, the cluster and component phase may be
   622  			// Running or Updating, it depends on the timing of cluster reconciling and ops request submission.
   623  			// If the phase is Updating, ops request will be failed because of cluster phase conflict.
   624  			if phase == appsv1alpha1.OpsFailedPhase {
   625  				return
   626  			}
   627  
   628  			By("wait for cluster and component phase are Updating")
   629  			Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, cluster *appsv1alpha1.Cluster) {
   630  				g.Expect(cluster.Status.Components[mysqlCompName].Phase).Should(Equal(appsv1alpha1.UpdatingClusterCompPhase))
   631  				g.Expect(cluster.Status.Phase).Should(Equal(appsv1alpha1.UpdatingClusterPhase))
   632  			})).Should(Succeed())
   633  
   634  			By("check the underlying workload been updated")
   635  			Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(componentWorkload()),
   636  				func(g Gomega, rsm *workloads.ReplicatedStateMachine) {
   637  					g.Expect(*rsm.Spec.Replicas).Should(Equal(replicas))
   638  				})).Should(Succeed())
   639  			rsm := componentWorkload()
   640  			Eventually(testapps.GetAndChangeObj(&testCtx, client.ObjectKeyFromObject(rsm), func(sts *appsv1.StatefulSet) {
   641  				sts.Spec.Replicas = &replicas
   642  			})).Should(Succeed())
   643  			Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(componentWorkload()),
   644  				func(g Gomega, sts *appsv1.StatefulSet) {
   645  					g.Expect(*sts.Spec.Replicas).Should(Equal(replicas))
   646  				})).Should(Succeed())
   647  
   648  			By("mock scale down successfully by deleting one pod ")
   649  			podName := fmt.Sprintf("%s-%s-%d", clusterObj.Name, mysqlCompName, 2)
   650  			dPodKeys := types.NamespacedName{Name: podName, Namespace: testCtx.DefaultNamespace}
   651  			testapps.DeleteObject(&testCtx, dPodKeys, &corev1.Pod{})
   652  
   653  			By("expect opsRequest phase to Succeed after cluster is Running")
   654  			mockCompRunning(replicas)
   655  			Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(ops), func(g Gomega, ops *appsv1alpha1.OpsRequest) {
   656  				g.Expect(ops.Status.Phase).Should(Equal(appsv1alpha1.OpsSucceedPhase))
   657  				g.Expect(ops.Status.Progress).Should(Equal("1/1"))
   658  			})).Should(Succeed())
   659  		})
   660  
   661  		It("delete Running opsRequest", func() {
   662  			By("Create a horizontalScaling ops")
   663  			testk8s.MockEnableVolumeSnapshot(&testCtx, testk8s.DefaultStorageClassName)
   664  			createMysqlCluster(3)
   665  			ops := createClusterHscaleOps(5)
   666  			opsKey := client.ObjectKeyFromObject(ops)
   667  			Eventually(testapps.GetOpsRequestPhase(&testCtx, opsKey)).Should(Equal(appsv1alpha1.OpsRunningPhase))
   668  
   669  			By("check if existing horizontalScaling opsRequest annotation in cluster")
   670  			Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, tmlCluster *appsv1alpha1.Cluster) {
   671  				opsSlice, _ := opsutil.GetOpsRequestSliceFromCluster(tmlCluster)
   672  				g.Expect(opsSlice).Should(HaveLen(1))
   673  				g.Expect(opsSlice[0].Name).Should(Equal(ops.Name))
   674  			})).Should(Succeed())
   675  
   676  			By("delete the Running ops")
   677  			testapps.DeleteObject(&testCtx, opsKey, ops)
   678  			Expect(testapps.ChangeObj(&testCtx, ops, func(lopsReq *appsv1alpha1.OpsRequest) {
   679  				lopsReq.SetFinalizers([]string{})
   680  			})).ShouldNot(HaveOccurred())
   681  
   682  			By("check the cluster annotation")
   683  			Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, tmlCluster *appsv1alpha1.Cluster) {
   684  				opsSlice, _ := opsutil.GetOpsRequestSliceFromCluster(tmlCluster)
   685  				g.Expect(opsSlice).Should(HaveLen(0))
   686  			})).Should(Succeed())
   687  		})
   688  
   689  		It("cancel HorizontalScaling opsRequest which is Running", func() {
   690  			By("create cluster and mock it to running")
   691  			testk8s.MockDisableVolumeSnapshot(&testCtx, testk8s.DefaultStorageClassName)
   692  			oldReplicas := int32(3)
   693  			createMysqlCluster(oldReplicas)
   694  			mockCompRunning(oldReplicas)
   695  
   696  			By("create a horizontalScaling ops")
   697  			ops := createClusterHscaleOps(5)
   698  			opsKey := client.ObjectKeyFromObject(ops)
   699  			Eventually(testapps.GetOpsRequestPhase(&testCtx, opsKey)).Should(Equal(appsv1alpha1.OpsRunningPhase))
   700  			Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.UpdatingClusterPhase))
   701  
   702  			By("create one pod")
   703  			podName := fmt.Sprintf("%s-%s-%d", clusterObj.Name, mysqlCompName, 3)
   704  			pod := testapps.MockConsensusComponentStsPod(&testCtx, nil, clusterObj.Name, mysqlCompName, podName, "follower", "Readonly")
   705  
   706  			By("cancel the opsRequest")
   707  			Eventually(testapps.ChangeObj(&testCtx, ops, func(opsRequest *appsv1alpha1.OpsRequest) {
   708  				opsRequest.Spec.Cancel = true
   709  			})).Should(Succeed())
   710  
   711  			By("check opsRequest is Cancelling")
   712  			Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(ops), func(g Gomega, opsRequest *appsv1alpha1.OpsRequest) {
   713  				g.Expect(opsRequest.Status.Phase).Should(Equal(appsv1alpha1.OpsCancellingPhase))
   714  				g.Expect(opsRequest.Status.CancelTimestamp.IsZero()).Should(BeFalse())
   715  				cancelCondition := meta.FindStatusCondition(opsRequest.Status.Conditions, appsv1alpha1.ConditionTypeCancelled)
   716  				g.Expect(cancelCondition).ShouldNot(BeNil())
   717  				g.Expect(cancelCondition.Reason).Should(Equal(appsv1alpha1.ReasonOpsCanceling))
   718  			})).Should(Succeed())
   719  
   720  			By("delete the created pod")
   721  			pod.Kind = constant.PodKind
   722  			testk8s.MockPodIsTerminating(ctx, testCtx, pod)
   723  			testk8s.RemovePodFinalizer(ctx, testCtx, pod)
   724  
   725  			By("opsRequest phase should be Cancelled")
   726  			Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(ops), func(g Gomega, opsRequest *appsv1alpha1.OpsRequest) {
   727  				g.Expect(opsRequest.Status.Phase).Should(Equal(appsv1alpha1.OpsCancelledPhase))
   728  				cancelCondition := meta.FindStatusCondition(opsRequest.Status.Conditions, appsv1alpha1.ConditionTypeCancelled)
   729  				g.Expect(cancelCondition).ShouldNot(BeNil())
   730  				g.Expect(cancelCondition.Reason).Should(Equal(appsv1alpha1.ReasonOpsCancelSucceed))
   731  			})).Should(Succeed())
   732  
   733  			By("cluster phase should be Running and delete the opsRequest annotation")
   734  			Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, tmlCluster *appsv1alpha1.Cluster) {
   735  				opsSlice, _ := opsutil.GetOpsRequestSliceFromCluster(tmlCluster)
   736  				g.Expect(opsSlice).Should(HaveLen(0))
   737  				g.Expect(tmlCluster.Status.Phase).Should(Equal(appsv1alpha1.RunningClusterPhase))
   738  			})).Should(Succeed())
   739  		})
   740  	})
   741  })