github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/controllers/apps/operations/horizontal_scaling_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 operations
    21  
    22  import (
    23  	"fmt"
    24  	"time"
    25  
    26  	. "github.com/onsi/ginkgo/v2"
    27  	. "github.com/onsi/gomega"
    28  	corev1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"sigs.k8s.io/controller-runtime/pkg/client"
    31  
    32  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    33  	opsutil "github.com/1aal/kubeblocks/controllers/apps/operations/util"
    34  	"github.com/1aal/kubeblocks/pkg/constant"
    35  	intctrlutil "github.com/1aal/kubeblocks/pkg/controllerutil"
    36  	"github.com/1aal/kubeblocks/pkg/generics"
    37  	testapps "github.com/1aal/kubeblocks/pkg/testutil/apps"
    38  	testk8s "github.com/1aal/kubeblocks/pkg/testutil/k8s"
    39  )
    40  
    41  var _ = Describe("HorizontalScaling OpsRequest", func() {
    42  
    43  	var (
    44  		randomStr             = testCtx.GetRandomStr()
    45  		clusterDefinitionName = "cluster-definition-for-ops-" + randomStr
    46  		clusterVersionName    = "clusterversion-for-ops-" + randomStr
    47  		clusterName           = "cluster-for-ops-" + randomStr
    48  	)
    49  
    50  	cleanEnv := func() {
    51  		// must wait till resources deleted and no longer existed before the testcases start,
    52  		// otherwise if later it needs to create some new resource objects with the same name,
    53  		// in race conditions, it will find the existence of old objects, resulting failure to
    54  		// create the new objects.
    55  		By("clean resources")
    56  
    57  		// delete cluster(and all dependent sub-resources), clusterversion and clusterdef
    58  		testapps.ClearClusterResources(&testCtx)
    59  
    60  		// delete rest resources
    61  		inNS := client.InNamespace(testCtx.DefaultNamespace)
    62  		ml := client.HasLabels{testCtx.TestObjLabelKey}
    63  		// namespaced
    64  		testapps.ClearResources(&testCtx, generics.OpsRequestSignature, inNS, ml)
    65  		// default GracePeriod is 30s
    66  		testapps.ClearResources(&testCtx, generics.PodSignature, inNS, ml, client.GracePeriodSeconds(0))
    67  	}
    68  
    69  	BeforeEach(cleanEnv)
    70  
    71  	AfterEach(cleanEnv)
    72  
    73  	initClusterAnnotationAndPhaseForOps := func(opsRes *OpsResource) {
    74  		Expect(opsutil.PatchClusterOpsAnnotations(ctx, k8sClient, opsRes.Cluster, nil)).Should(Succeed())
    75  		Expect(testapps.ChangeObjStatus(&testCtx, opsRes.Cluster, func() {
    76  			opsRes.Cluster.Status.Phase = appsv1alpha1.RunningClusterPhase
    77  		})).ShouldNot(HaveOccurred())
    78  	}
    79  
    80  	Context("Test OpsRequest", func() {
    81  
    82  		commonHScaleConsensusCompTest := func(reqCtx intctrlutil.RequestCtx, replicas int) (*OpsResource, []corev1.Pod) {
    83  			By("init operations resources with CLusterDefinition/ClusterVersion/Hybrid components Cluster/consensus Pods")
    84  			opsRes, _, _ := initOperationsResources(clusterDefinitionName, clusterVersionName, clusterName)
    85  			podList := initConsensusPods(ctx, k8sClient, opsRes, clusterName)
    86  
    87  			By(fmt.Sprintf("create opsRequest for horizontal scaling of consensus component from 3 to %d", replicas))
    88  			initClusterAnnotationAndPhaseForOps(opsRes)
    89  			opsRes.OpsRequest = createHorizontalScaling(clusterName, replicas)
    90  			mockComponentIsOperating(opsRes.Cluster, appsv1alpha1.UpdatingClusterCompPhase, consensusComp)
    91  
    92  			By("expect for opsRequest phase is Creating after doing action")
    93  			_, err := GetOpsManager().Do(reqCtx, k8sClient, opsRes)
    94  			Expect(err).ShouldNot(HaveOccurred())
    95  			Eventually(testapps.GetOpsRequestPhase(&testCtx, client.ObjectKeyFromObject(opsRes.OpsRequest))).Should(Equal(appsv1alpha1.OpsCreatingPhase))
    96  
    97  			By(fmt.Sprintf("expect for the replicas of consensus component is %d after doing action again when opsRequest phase is Creating", replicas))
    98  			_, err = GetOpsManager().Do(reqCtx, k8sClient, opsRes)
    99  			Expect(err).ShouldNot(HaveOccurred())
   100  			Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(opsRes.Cluster), func(g Gomega, tmpCluster *appsv1alpha1.Cluster) {
   101  				g.Expect(tmpCluster.Spec.GetComponentByName(consensusComp).Replicas).Should(BeEquivalentTo(replicas))
   102  			})).Should(Succeed())
   103  
   104  			By("Test OpsManager.Reconcile function when horizontal scaling OpsRequest is Running")
   105  			opsRes.OpsRequest.Status.Phase = appsv1alpha1.OpsRequestKind
   106  			_, err = GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes)
   107  			Expect(err).ShouldNot(HaveOccurred())
   108  			return opsRes, podList
   109  		}
   110  
   111  		checkOpsRequestPhaseIsSucceed := func(reqCtx intctrlutil.RequestCtx, opsRes *OpsResource) {
   112  			By("expect for opsRequest phase is Succeed after pods has been scaled and component phase is Running")
   113  			// mock consensus component is Running
   114  			mockConsensusCompToRunning(opsRes)
   115  			_, err := GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes)
   116  			Expect(err).ShouldNot(HaveOccurred())
   117  			Expect(opsRes.OpsRequest.Status.Phase).Should(Equal(appsv1alpha1.OpsSucceedPhase))
   118  		}
   119  
   120  		checkCancelledSucceed := func(reqCtx intctrlutil.RequestCtx, opsRes *OpsResource) {
   121  			_, err := GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes)
   122  			Expect(err).ShouldNot(HaveOccurred())
   123  			Expect(opsRes.OpsRequest.Status.Phase).Should(Equal(appsv1alpha1.OpsCancelledPhase))
   124  			opsProgressDetails := opsRes.OpsRequest.Status.Components[consensusComp].ProgressDetails
   125  			Expect(len(opsProgressDetails)).Should(Equal(1))
   126  			Expect(opsProgressDetails[0].Status).Should(Equal(appsv1alpha1.SucceedProgressStatus))
   127  		}
   128  
   129  		It("test scaling down replicas", func() {
   130  			reqCtx := intctrlutil.RequestCtx{Ctx: testCtx.Ctx}
   131  			opsRes, podList := commonHScaleConsensusCompTest(reqCtx, 1)
   132  			By("mock two pods are deleted")
   133  			for i := 0; i < 2; i++ {
   134  				pod := &podList[i]
   135  				pod.Kind = constant.PodKind
   136  				testk8s.MockPodIsTerminating(ctx, testCtx, pod)
   137  				testk8s.RemovePodFinalizer(ctx, testCtx, pod)
   138  			}
   139  			checkOpsRequestPhaseIsSucceed(reqCtx, opsRes)
   140  		})
   141  
   142  		It("test scaling out replicas", func() {
   143  			reqCtx := intctrlutil.RequestCtx{Ctx: testCtx.Ctx}
   144  			opsRes, _ := commonHScaleConsensusCompTest(reqCtx, 5)
   145  			By("mock two pods are created")
   146  			for i := 3; i < 5; i++ {
   147  				podName := fmt.Sprintf("%s-%s-%d", clusterName, consensusComp, i)
   148  				testapps.MockConsensusComponentStsPod(&testCtx, nil, clusterName, consensusComp, podName, "follower", "Readonly")
   149  			}
   150  			checkOpsRequestPhaseIsSucceed(reqCtx, opsRes)
   151  		})
   152  
   153  		It("test canceling HScale opsRequest which scales down replicas of component", func() {
   154  			reqCtx := intctrlutil.RequestCtx{Ctx: testCtx.Ctx}
   155  			opsRes, podList := commonHScaleConsensusCompTest(reqCtx, 1)
   156  
   157  			By("mock one pod has been deleted")
   158  			pod := &podList[0]
   159  			pod.Kind = constant.PodKind
   160  			testk8s.MockPodIsTerminating(ctx, testCtx, pod)
   161  			testk8s.RemovePodFinalizer(ctx, testCtx, pod)
   162  
   163  			By("cancel HScale opsRequest after one pod has been deleted")
   164  			cancelOpsRequest(reqCtx, opsRes, time.Now().Add(-1*time.Second))
   165  
   166  			By("re-create the deleted pod")
   167  			podName := fmt.Sprintf("%s-%s-%d", clusterName, consensusComp, 0)
   168  			testapps.MockConsensusComponentStsPod(&testCtx, nil, clusterName, consensusComp, podName, "leader", "ReadWrite")
   169  
   170  			By("expect for opsRequest phase is Succeed after pods has been scaled and component phase is Running")
   171  			mockConsensusCompToRunning(opsRes)
   172  			checkCancelledSucceed(reqCtx, opsRes)
   173  		})
   174  
   175  		It("test canceling HScale opsRequest which scales out replicas of component", func() {
   176  			reqCtx := intctrlutil.RequestCtx{Ctx: testCtx.Ctx}
   177  			opsRes, _ := commonHScaleConsensusCompTest(reqCtx, 5)
   178  
   179  			By("mock one pod is created")
   180  			podName := fmt.Sprintf("%s-%s-%d", clusterName, consensusComp, 3)
   181  			pod := testapps.MockConsensusComponentStsPod(&testCtx, nil, clusterName, consensusComp, podName, "follower", "Readonly")
   182  
   183  			By("cancel HScale opsRequest after pne pod is created")
   184  			cancelOpsRequest(reqCtx, opsRes, time.Now().Add(-1*time.Second))
   185  
   186  			By("delete the created pod")
   187  			pod.Kind = constant.PodKind
   188  			testk8s.MockPodIsTerminating(ctx, testCtx, pod)
   189  			testk8s.RemovePodFinalizer(ctx, testCtx, pod)
   190  
   191  			By("expect for opsRequest phase is Succeed after pods has been scaled and component phase is Running")
   192  			mockConsensusCompToRunning(opsRes)
   193  			checkCancelledSucceed(reqCtx, opsRes)
   194  		})
   195  	})
   196  })
   197  
   198  func createHorizontalScaling(clusterName string, replicas int) *appsv1alpha1.OpsRequest {
   199  	horizontalOpsName := "horizontal-scaling-ops-" + testCtx.GetRandomStr()
   200  	ops := testapps.NewOpsRequestObj(horizontalOpsName, testCtx.DefaultNamespace,
   201  		clusterName, appsv1alpha1.HorizontalScalingType)
   202  	ops.Spec.HorizontalScalingList = []appsv1alpha1.HorizontalScaling{
   203  		{
   204  			ComponentOps: appsv1alpha1.ComponentOps{ComponentName: consensusComp},
   205  			Replicas:     int32(replicas),
   206  		},
   207  	}
   208  	return testapps.CreateOpsRequest(ctx, testCtx, ops)
   209  }
   210  
   211  func cancelOpsRequest(reqCtx intctrlutil.RequestCtx, opsRes *OpsResource, cancelTime time.Time) {
   212  	opsRequest := opsRes.OpsRequest
   213  	opsRequest.Spec.Cancel = true
   214  	opsBehaviour := GetOpsManager().OpsMap[opsRequest.Spec.Type]
   215  	Expect(testapps.ChangeObjStatus(&testCtx, opsRequest, func() {
   216  		opsRequest.Status.CancelTimestamp = metav1.Time{Time: cancelTime}
   217  		opsRequest.Status.Phase = appsv1alpha1.OpsCancellingPhase
   218  	})).Should(Succeed())
   219  	Expect(opsBehaviour.CancelFunc(reqCtx, k8sClient, opsRes)).ShouldNot(HaveOccurred())
   220  }
   221  
   222  func mockConsensusCompToRunning(opsRes *OpsResource) {
   223  	// mock consensus component is Running
   224  	compStatus := opsRes.Cluster.Status.Components[consensusComp]
   225  	compStatus.Phase = appsv1alpha1.RunningClusterCompPhase
   226  	opsRes.Cluster.Status.Components[consensusComp] = compStatus
   227  }