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