github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/apis/apps/v1alpha1/opsrequest_webhook_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 v1alpha1
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  
    23  	. "github.com/onsi/ginkgo/v2"
    24  	. "github.com/onsi/gomega"
    25  
    26  	"github.com/sethvargo/go-password/password"
    27  	corev1 "k8s.io/api/core/v1"
    28  	storagev1 "k8s.io/api/storage/v1"
    29  	"k8s.io/apimachinery/pkg/api/resource"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/kubectl/pkg/util/storage"
    32  	"sigs.k8s.io/controller-runtime/pkg/client"
    33  
    34  	"github.com/1aal/kubeblocks/pkg/constant"
    35  	// testapps "github.com/1aal/kubeblocks/pkg/testutil/apps"
    36  )
    37  
    38  var _ = Describe("OpsRequest webhook", func() {
    39  	const (
    40  		componentName      = "replicasets"
    41  		proxyComponentName = "proxy"
    42  	)
    43  	var (
    44  		randomStr                    = testCtx.GetRandomStr()
    45  		clusterDefinitionName        = "opswebhook-mysql-definition-" + randomStr
    46  		clusterVersionName           = "opswebhook-mysql-clusterversion-" + randomStr
    47  		clusterVersionNameForUpgrade = "opswebhook-mysql-upgrade-" + randomStr
    48  		clusterName                  = "opswebhook-mysql-" + randomStr
    49  		opsRequestName               = "opswebhook-mysql-ops-" + randomStr
    50  	)
    51  
    52  	int32Ptr := func(i int32) *int32 {
    53  		return &i
    54  	}
    55  
    56  	cleanupObjects := func() {
    57  		// Add any setup steps that needs to be executed before each test
    58  		err := k8sClient.DeleteAllOf(ctx, &OpsRequest{}, client.InNamespace(testCtx.DefaultNamespace), client.HasLabels{testCtx.TestObjLabelKey})
    59  		Expect(err).NotTo(HaveOccurred())
    60  		err = k8sClient.DeleteAllOf(ctx, &Cluster{}, client.InNamespace(testCtx.DefaultNamespace), client.HasLabels{testCtx.TestObjLabelKey})
    61  		Expect(err).NotTo(HaveOccurred())
    62  		err = k8sClient.DeleteAllOf(ctx, &ClusterVersion{}, client.HasLabels{testCtx.TestObjLabelKey})
    63  		Expect(err).NotTo(HaveOccurred())
    64  		err = k8sClient.DeleteAllOf(ctx, &ClusterDefinition{}, client.HasLabels{testCtx.TestObjLabelKey})
    65  		Expect(err).NotTo(HaveOccurred())
    66  		err = k8sClient.DeleteAllOf(ctx, &storagev1.StorageClass{})
    67  		Expect(err).NotTo(HaveOccurred())
    68  	}
    69  	BeforeEach(func() {
    70  		// Add any setup steps that needs to be executed before each test
    71  		cleanupObjects()
    72  	})
    73  
    74  	AfterEach(func() {
    75  		// Add any teardown steps that needs to be executed after each test
    76  		cleanupObjects()
    77  	})
    78  
    79  	addClusterRequestAnnotation := func(cluster *Cluster, opsName string, toClusterPhase ClusterPhase) {
    80  		clusterPatch := client.MergeFrom(cluster.DeepCopy())
    81  		cluster.Annotations = map[string]string{
    82  			opsRequestAnnotationKey: fmt.Sprintf(`[{"name":"%s","clusterPhase":"%s"}]`, opsName, toClusterPhase),
    83  		}
    84  		Expect(k8sClient.Patch(ctx, cluster, clusterPatch)).Should(Succeed())
    85  	}
    86  
    87  	createStorageClass := func(ctx context.Context, storageClassName string, isDefault string, allowVolumeExpansion bool) *storagev1.StorageClass {
    88  		storageClass := &storagev1.StorageClass{
    89  			ObjectMeta: metav1.ObjectMeta{
    90  				Name: storageClassName,
    91  				Annotations: map[string]string{
    92  					storage.IsDefaultStorageClassAnnotation: isDefault,
    93  				},
    94  			},
    95  			Provisioner:          "kubernetes.io/no-provisioner",
    96  			AllowVolumeExpansion: &allowVolumeExpansion,
    97  		}
    98  		err := testCtx.CheckedCreateObj(ctx, storageClass)
    99  		Expect(err).Should(BeNil())
   100  		return storageClass
   101  	}
   102  
   103  	createPVC := func(clusterName, compName, storageClassName, vctName string, index int) *corev1.PersistentVolumeClaim {
   104  		pvc := &corev1.PersistentVolumeClaim{
   105  			ObjectMeta: metav1.ObjectMeta{
   106  				Name:      fmt.Sprintf("%s-%s-%s-%d", vctName, clusterName, compName, index),
   107  				Namespace: testCtx.DefaultNamespace,
   108  				Labels: map[string]string{
   109  					constant.AppInstanceLabelKey:             clusterName,
   110  					constant.VolumeClaimTemplateNameLabelKey: vctName,
   111  					constant.KBAppComponentLabelKey:          compName,
   112  				},
   113  			},
   114  			Spec: corev1.PersistentVolumeClaimSpec{
   115  				AccessModes: []corev1.PersistentVolumeAccessMode{
   116  					corev1.ReadWriteOnce,
   117  				},
   118  				Resources: corev1.ResourceRequirements{
   119  					Requests: corev1.ResourceList{
   120  						"storage": resource.MustParse("1Gi"),
   121  					},
   122  				},
   123  				StorageClassName: &storageClassName,
   124  			},
   125  		}
   126  		Expect(testCtx.CheckedCreateObj(ctx, pvc)).ShouldNot(HaveOccurred())
   127  		Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(pvc), pvc)).ShouldNot(HaveOccurred())
   128  		patch := client.MergeFrom(pvc.DeepCopy())
   129  		pvc.Status.Capacity = corev1.ResourceList{
   130  			"storage": resource.MustParse("1Gi"),
   131  		}
   132  		Expect(k8sClient.Status().Patch(ctx, pvc, patch)).ShouldNot(HaveOccurred())
   133  		return pvc
   134  	}
   135  
   136  	notFoundComponentsString := func(notFoundComponents string) string {
   137  		return fmt.Sprintf("components: [%s] not found", notFoundComponents)
   138  	}
   139  
   140  	testUpgrade := func(cluster *Cluster) {
   141  		opsRequest := createTestOpsRequest(clusterName, opsRequestName+"-upgrade", UpgradeType)
   142  
   143  		By("By testing when spec.upgrade is null")
   144  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring("spec.upgrade"))
   145  
   146  		By("By creating a new clusterVersion for upgrade")
   147  		newClusterVersion := createTestClusterVersionObj(clusterDefinitionName, clusterVersionNameForUpgrade)
   148  		Expect(testCtx.CreateObj(ctx, newClusterVersion)).Should(Succeed())
   149  
   150  		By("By testing when target cluster version not exist")
   151  		opsRequest.Spec.Upgrade = &Upgrade{ClusterVersionRef: clusterVersionName + "-not-exist"}
   152  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring("not found"))
   153  
   154  		By("Test Cluster Phase")
   155  		opsRequest.Name = opsRequestName + "-upgrade-cluster-phase"
   156  		opsRequest.Spec.Upgrade = &Upgrade{ClusterVersionRef: clusterVersionName}
   157  		OpsRequestBehaviourMapper[UpgradeType] = OpsRequestBehaviour{
   158  			FromClusterPhases: []ClusterPhase{RunningClusterPhase},
   159  			ToClusterPhase:    UpdatingClusterPhase, // original VersionUpgradingPhase,
   160  		}
   161  		// TODO: do VersionUpgradingPhase condition value check
   162  
   163  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring("Upgrade is forbidden"))
   164  		// update cluster phase to Running
   165  		clusterPatch := client.MergeFrom(cluster.DeepCopy())
   166  		cluster.Status.Phase = RunningClusterPhase
   167  		Expect(k8sClient.Status().Patch(ctx, cluster, clusterPatch)).Should(Succeed())
   168  
   169  		By("Test existing other operations in cluster")
   170  		// update cluster existing operations
   171  		addClusterRequestAnnotation(cluster, "testOpsName", UpdatingClusterPhase)
   172  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).Should(ContainSubstring("existing OpsRequest: testOpsName"))
   173  		// test opsRequest reentry
   174  		addClusterRequestAnnotation(cluster, opsRequest.Name, UpdatingClusterPhase)
   175  
   176  		By("By creating a upgrade opsRequest, it should be succeed")
   177  		opsRequest.Spec.Upgrade.ClusterVersionRef = newClusterVersion.Name
   178  		Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).Should(Succeed())
   179  		Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: opsRequest.Name,
   180  			Namespace: opsRequest.Namespace}, opsRequest)).Should(Succeed())
   181  
   182  		By("Expect an error for cancelling this opsRequest")
   183  		opsRequest.Spec.Cancel = true
   184  		Expect(k8sClient.Update(context.Background(), opsRequest).Error()).Should(ContainSubstring("forbidden to cancel the opsRequest which type not in ['VerticalScaling','HorizontalScaling']"))
   185  	}
   186  
   187  	testVerticalScaling := func(cluster *Cluster) {
   188  		verticalScalingList := []VerticalScaling{
   189  			{
   190  				ComponentOps:         ComponentOps{ComponentName: "vs-not-exist"},
   191  				ResourceRequirements: corev1.ResourceRequirements{},
   192  			},
   193  			{
   194  				ComponentOps: ComponentOps{ComponentName: proxyComponentName},
   195  				ResourceRequirements: corev1.ResourceRequirements{
   196  					Requests: corev1.ResourceList{
   197  						"cpu":    resource.MustParse("100m"),
   198  						"memory": resource.MustParse("100Mi"),
   199  					},
   200  				},
   201  			},
   202  			{
   203  				ComponentOps: ComponentOps{ComponentName: componentName},
   204  				ResourceRequirements: corev1.ResourceRequirements{
   205  					Requests: corev1.ResourceList{
   206  						"cpu":    resource.MustParse("200m"),
   207  						"memory": resource.MustParse("100Mi"),
   208  					},
   209  					Limits: corev1.ResourceList{
   210  						"cpu":    resource.MustParse("100m"),
   211  						"memory": resource.MustParse("100Mi"),
   212  					},
   213  				},
   214  			},
   215  		}
   216  
   217  		By("By testing verticalScaling opsRequest components is not exist")
   218  		opsRequest := createTestOpsRequest(clusterName, opsRequestName, VerticalScalingType)
   219  		opsRequest.Spec.VerticalScalingList = []VerticalScaling{verticalScalingList[0]}
   220  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring(notFoundComponentsString("vs-not-exist")))
   221  
   222  		By("By testing verticalScaling opsRequest components is not consistent")
   223  		opsRequest = createTestOpsRequest(clusterName, opsRequestName, VerticalScalingType)
   224  		// [0] is not exist, and [1] is valid.
   225  		opsRequest.Spec.VerticalScalingList = []VerticalScaling{verticalScalingList[0], verticalScalingList[1]}
   226  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring("not found"))
   227  
   228  		By("By testing verticalScaling opsRequest components partly")
   229  		opsRequest = createTestOpsRequest(clusterName, opsRequestName, VerticalScalingType)
   230  		opsRequest.Spec.VerticalScalingList = []VerticalScaling{verticalScalingList[1]}
   231  		Expect(testCtx.CreateObj(ctx, opsRequest) == nil).Should(BeTrue())
   232  
   233  		By("By testing requests cpu less than limits cpu")
   234  		opsRequest = createTestOpsRequest(clusterName, opsRequestName, VerticalScalingType)
   235  		opsRequest.Spec.VerticalScalingList = []VerticalScaling{verticalScalingList[2]}
   236  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring("must be less than or equal to cpu limit"))
   237  
   238  		By("expect successful")
   239  		opsRequest.Spec.VerticalScalingList[0].Requests[corev1.ResourceCPU] = resource.MustParse("100m")
   240  		Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).Should(Succeed())
   241  
   242  		By("test spec immutable")
   243  		newClusterName := clusterName + "1"
   244  		newCluster, _ := createTestCluster(clusterDefinitionName, clusterVersionName, newClusterName)
   245  		Expect(testCtx.CheckedCreateObj(ctx, newCluster)).Should(Succeed())
   246  
   247  		testSpecImmutable := func(phase OpsPhase) {
   248  			By(fmt.Sprintf("spec is immutable when status.phase in %s", phase))
   249  			patch := client.MergeFrom(opsRequest.DeepCopy())
   250  			opsRequest.Status.Phase = phase
   251  			Expect(k8sClient.Status().Patch(ctx, opsRequest, patch)).Should(Succeed())
   252  
   253  			patch = client.MergeFrom(opsRequest.DeepCopy())
   254  			opsRequest.Spec.Cancel = true
   255  			Expect(k8sClient.Patch(ctx, opsRequest, patch).Error()).To(ContainSubstring(fmt.Sprintf("is forbidden when status.Phase is %s", phase)))
   256  		}
   257  		phaseList := []OpsPhase{OpsSucceedPhase, OpsFailedPhase, OpsCancelledPhase}
   258  		for _, phase := range phaseList {
   259  			testSpecImmutable(phase)
   260  		}
   261  
   262  		By("test spec immutable except for cancel")
   263  		testSpecImmutableExpectForCancel := func(phase OpsPhase) {
   264  			patch := client.MergeFrom(opsRequest.DeepCopy())
   265  			opsRequest.Status.Phase = phase
   266  			Expect(k8sClient.Status().Patch(ctx, opsRequest, patch)).Should(Succeed())
   267  
   268  			patch = client.MergeFrom(opsRequest.DeepCopy())
   269  			By(fmt.Sprintf("cancel opsRequest when ops phase is %s", phase))
   270  			opsRequest.Spec.Cancel = !opsRequest.Spec.Cancel
   271  			Expect(k8sClient.Patch(ctx, opsRequest, patch)).ShouldNot(HaveOccurred())
   272  
   273  			By(fmt.Sprintf("expect an error for updating spec.ClusterRef when ops phase is %s", phase))
   274  			opsRequest.Spec.ClusterRef = newClusterName
   275  			Expect(k8sClient.Patch(ctx, opsRequest, patch).Error()).To(ContainSubstring("forbidden to update spec.clusterRef"))
   276  		}
   277  
   278  		phaseList = []OpsPhase{OpsCreatingPhase, OpsRunningPhase, OpsCancellingPhase}
   279  		for _, phase := range phaseList {
   280  			testSpecImmutableExpectForCancel(phase)
   281  		}
   282  	}
   283  
   284  	testVolumeExpansion := func(cluster *Cluster) {
   285  		getSingleVolumeExpansionList := func(compName, vctName, storage string) []VolumeExpansion {
   286  			return []VolumeExpansion{
   287  				{
   288  					ComponentOps: ComponentOps{ComponentName: compName},
   289  					VolumeClaimTemplates: []OpsRequestVolumeClaimTemplate{
   290  						{
   291  							Name:    vctName,
   292  							Storage: resource.MustParse(storage),
   293  						},
   294  					},
   295  				},
   296  			}
   297  		}
   298  		defaultVCTName := "data"
   299  		logVCTName := "log"
   300  		targetStorage := "2Gi"
   301  		By("By testing volumeExpansion - target component not exist")
   302  		opsRequest := createTestOpsRequest(clusterName, opsRequestName, VolumeExpansionType)
   303  		opsRequest.Spec.VolumeExpansionList = getSingleVolumeExpansionList("ve-not-exist", defaultVCTName, targetStorage)
   304  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring(notFoundComponentsString("ve-not-exist")))
   305  
   306  		By("By testing volumeExpansion - target volume not exist")
   307  		volumeExpansionList := []VolumeExpansion{{
   308  			ComponentOps: ComponentOps{ComponentName: componentName},
   309  			VolumeClaimTemplates: []OpsRequestVolumeClaimTemplate{
   310  				{
   311  					Name:    logVCTName,
   312  					Storage: resource.MustParse(targetStorage),
   313  				},
   314  				{
   315  					Name:    defaultVCTName,
   316  					Storage: resource.MustParse(targetStorage),
   317  				},
   318  			},
   319  		},
   320  		}
   321  		opsRequest.Spec.VolumeExpansionList = volumeExpansionList
   322  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring("volumeClaimTemplates: [log] not found in component: " + componentName))
   323  
   324  		By("By testing volumeExpansion - storageClass do not support volume expansion")
   325  		volumeExpansionList = getSingleVolumeExpansionList(componentName, defaultVCTName, targetStorage)
   326  		opsRequest.Spec.VolumeExpansionList = volumeExpansionList
   327  		notSupportMsg := fmt.Sprintf("volumeClaimTemplate: [data] not support volume expansion in component: %s, you can view infos by command: kubectl get sc", componentName)
   328  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring(notSupportMsg))
   329  
   330  		By("testing volumeExpansion - storageClass supports volume expansion")
   331  		storageClassName := "standard"
   332  		storageClass := createStorageClass(testCtx.Ctx, storageClassName, "true", true)
   333  		Expect(storageClass).ShouldNot(BeNil())
   334  		// mock to create pvc
   335  		createPVC(clusterName, componentName, storageClassName, defaultVCTName, 0)
   336  
   337  		By("create a pvc and storageClass does not support volume expansion")
   338  		storageClassName1 := "standard1"
   339  		storageClass1 := createStorageClass(testCtx.Ctx, storageClassName1, "false", false)
   340  		Expect(storageClass1).ShouldNot(BeNil())
   341  		createPVC(clusterName, componentName, storageClassName1, logVCTName, 0)
   342  
   343  		By("testing volumeExpansion with smaller storage, expect an error occurs")
   344  		opsRequest.Spec.VolumeExpansionList = getSingleVolumeExpansionList(componentName, defaultVCTName, "500Mi")
   345  		Expect(testCtx.CreateObj(ctx, opsRequest)).Should(HaveOccurred())
   346  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring(`requested storage size of volumeClaimTemplate "data" can not less than status.capacity.storage "1Gi"`))
   347  
   348  		By("testing other volumeExpansion opsRequest exists")
   349  		opsRequest.Spec.VolumeExpansionList = getSingleVolumeExpansionList(componentName, defaultVCTName, targetStorage)
   350  		Expect(testCtx.CreateObj(ctx, opsRequest)).ShouldNot(HaveOccurred())
   351  		// mock ops to running
   352  		patch := client.MergeFrom(opsRequest.DeepCopy())
   353  		opsRequest.Status.Phase = OpsRunningPhase
   354  		Expect(k8sClient.Status().Patch(ctx, opsRequest, patch)).ShouldNot(HaveOccurred())
   355  		// create another ops
   356  		opsRequest1 := createTestOpsRequest(clusterName, opsRequestName+"1", VolumeExpansionType)
   357  		opsRequest1.Spec.VolumeExpansionList = getSingleVolumeExpansionList(componentName, defaultVCTName, "3Gi")
   358  		Expect(testCtx.CreateObj(ctx, opsRequest1).Error()).Should(ContainSubstring("existing other VolumeExpansion OpsRequest"))
   359  	}
   360  
   361  	testHorizontalScaling := func(clusterDef *ClusterDefinition, cluster *Cluster) {
   362  		hScalingList := []HorizontalScaling{
   363  			{
   364  				ComponentOps: ComponentOps{ComponentName: "hs-not-exist"},
   365  				Replicas:     2,
   366  			},
   367  			{
   368  				ComponentOps: ComponentOps{ComponentName: proxyComponentName},
   369  				Replicas:     2,
   370  			},
   371  			{
   372  				ComponentOps: ComponentOps{ComponentName: componentName},
   373  				Replicas:     2,
   374  			},
   375  		}
   376  
   377  		By("By testing horizontalScaling - delete component proxy from cluster definition which is exist in cluster")
   378  		patch := client.MergeFrom(clusterDef.DeepCopy())
   379  		// delete component proxy from cluster definition
   380  		if clusterDef.Spec.ComponentDefs[0].Name == proxyComponentName {
   381  			clusterDef.Spec.ComponentDefs = clusterDef.Spec.ComponentDefs[1:]
   382  		} else {
   383  			clusterDef.Spec.ComponentDefs = clusterDef.Spec.ComponentDefs[:1]
   384  		}
   385  		Expect(k8sClient.Patch(ctx, clusterDef, patch)).Should(Succeed())
   386  		tmp := &ClusterDefinition{}
   387  		_ = k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterDef.Name, Namespace: clusterDef.Namespace}, tmp)
   388  		Expect(len(tmp.Spec.ComponentDefs)).Should(Equal(1))
   389  
   390  		By("By testing horizontalScaling - target component not exist")
   391  		opsRequest := createTestOpsRequest(clusterName, opsRequestName, HorizontalScalingType)
   392  		opsRequest.Spec.HorizontalScalingList = []HorizontalScaling{hScalingList[0]}
   393  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring(notFoundComponentsString("hs-not-exist")))
   394  
   395  		By("By testing horizontalScaling - target component not exist partly")
   396  		opsRequest = createTestOpsRequest(clusterName, opsRequestName, HorizontalScalingType)
   397  		opsRequest.Spec.HorizontalScalingList = []HorizontalScaling{hScalingList[0], hScalingList[2]}
   398  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring(notFoundComponentsString("hs-not-exist")))
   399  
   400  		By("By testing horizontalScaling. if api is legal, it will create successfully")
   401  		opsRequest = createTestOpsRequest(clusterName, opsRequestName, HorizontalScalingType)
   402  		opsRequest.Spec.HorizontalScalingList = []HorizontalScaling{hScalingList[2]}
   403  		Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).Should(Succeed())
   404  
   405  		By("test min, max is zero")
   406  		opsRequest = createTestOpsRequest(clusterName, opsRequestName, HorizontalScalingType)
   407  		opsRequest.Spec.HorizontalScalingList = []HorizontalScaling{hScalingList[2]}
   408  		opsRequest.Spec.HorizontalScalingList[0].Replicas = 5
   409  		Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).Should(Succeed())
   410  	}
   411  
   412  	testSwitchover := func(clusterDef *ClusterDefinition, cluster *Cluster) {
   413  		switchoverList := []Switchover{
   414  			{
   415  				ComponentOps: ComponentOps{ComponentName: "switchover-component-not-exist"},
   416  				InstanceName: "*",
   417  			},
   418  			{
   419  				ComponentOps: ComponentOps{ComponentName: componentName},
   420  				InstanceName: "",
   421  			},
   422  			{
   423  				ComponentOps: ComponentOps{ComponentName: componentName},
   424  				InstanceName: "switchover-instance-name-not-exist",
   425  			},
   426  			{
   427  				ComponentOps: ComponentOps{ComponentName: componentName},
   428  				InstanceName: "*",
   429  			},
   430  			{
   431  				ComponentOps: ComponentOps{ComponentName: componentName},
   432  				InstanceName: fmt.Sprintf("%s-%s-0", cluster.Name, componentName),
   433  			},
   434  		}
   435  
   436  		By("By testing horizontalScaling - delete component proxy from cluster definition which is exist in cluster")
   437  		patch := client.MergeFrom(clusterDef.DeepCopy())
   438  		// delete component proxy from cluster definition
   439  		if clusterDef.Spec.ComponentDefs[0].Name == proxyComponentName {
   440  			clusterDef.Spec.ComponentDefs = clusterDef.Spec.ComponentDefs[1:]
   441  		} else {
   442  			clusterDef.Spec.ComponentDefs = clusterDef.Spec.ComponentDefs[:1]
   443  		}
   444  		Expect(k8sClient.Patch(ctx, clusterDef, patch)).Should(Succeed())
   445  		tmp := &ClusterDefinition{}
   446  		_ = k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterDef.Name, Namespace: clusterDef.Namespace}, tmp)
   447  		Expect(len(tmp.Spec.ComponentDefs)).Should(Equal(1))
   448  
   449  		By("By testing switchover - target component not exist")
   450  		opsRequest := createTestOpsRequest(clusterName, opsRequestName, SwitchoverType)
   451  		opsRequest.Spec.SwitchoverList = []Switchover{switchoverList[0]}
   452  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring(notFoundComponentsString("switchover-component-not-exist")))
   453  
   454  		By("By testing switchover - target switchover.Instance cannot be empty")
   455  		opsRequest = createTestOpsRequest(clusterName, opsRequestName, SwitchoverType)
   456  		opsRequest.Spec.SwitchoverList = []Switchover{switchoverList[1]}
   457  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring("switchover.instanceName"))
   458  
   459  		By("By testing switchover - clusterDefinition has no switchoverSpec and do not support switchover")
   460  		opsRequest = createTestOpsRequest(clusterName, opsRequestName, SwitchoverType)
   461  		opsRequest.Spec.SwitchoverList = []Switchover{switchoverList[3]}
   462  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring("does not support switchover"))
   463  
   464  		By("By testing switchover - target switchover.Instance cannot be empty")
   465  		patch = client.MergeFrom(clusterDef.DeepCopy())
   466  		commandExecutorEnvItem := CommandExecutorEnvItem{
   467  			Image: "",
   468  		}
   469  		commandExecutorItem := CommandExecutorItem{
   470  			Command: []string{"echo", "hello"},
   471  			Args:    []string{},
   472  		}
   473  		switchoverSpec := &SwitchoverSpec{
   474  			WithCandidate: &SwitchoverAction{
   475  				CmdExecutorConfig: &CmdExecutorConfig{
   476  					CommandExecutorEnvItem: commandExecutorEnvItem,
   477  					CommandExecutorItem:    commandExecutorItem,
   478  				},
   479  			},
   480  			WithoutCandidate: &SwitchoverAction{
   481  				CmdExecutorConfig: &CmdExecutorConfig{
   482  					CommandExecutorEnvItem: commandExecutorEnvItem,
   483  					CommandExecutorItem:    commandExecutorItem,
   484  				},
   485  			},
   486  		}
   487  		clusterDef.Spec.ComponentDefs[0].SwitchoverSpec = switchoverSpec
   488  		Expect(k8sClient.Patch(ctx, clusterDef, patch)).Should(Succeed())
   489  		tmp = &ClusterDefinition{}
   490  		_ = k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterDef.Name, Namespace: clusterDef.Namespace}, tmp)
   491  		Expect(len(tmp.Spec.ComponentDefs)).Should(Equal(1))
   492  
   493  		By("By testing switchover - switchover.InstanceName is * and should succeed ")
   494  		opsRequest = createTestOpsRequest(clusterName, opsRequestName, SwitchoverType)
   495  		opsRequest.Spec.SwitchoverList = []Switchover{switchoverList[3]}
   496  		Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).Should(Succeed())
   497  	}
   498  
   499  	testWhenClusterDeleted := func(cluster *Cluster, opsRequest *OpsRequest) {
   500  		By("delete cluster")
   501  		newCluster := &Cluster{}
   502  		Expect(k8sClient.Get(ctx, client.ObjectKey{Name: clusterName, Namespace: cluster.Namespace}, newCluster)).Should(Succeed())
   503  		Expect(k8sClient.Delete(ctx, newCluster)).Should(Succeed())
   504  
   505  		By("test path labels")
   506  		Eventually(k8sClient.Get(ctx, client.ObjectKey{Name: clusterName, Namespace: cluster.Namespace}, &Cluster{})).Should(HaveOccurred())
   507  
   508  		patch := client.MergeFrom(opsRequest.DeepCopy())
   509  		opsRequest.Labels["test"] = "test-ops"
   510  		Expect(k8sClient.Patch(ctx, opsRequest, patch)).Should(Succeed())
   511  	}
   512  
   513  	testRestart := func(cluster *Cluster) *OpsRequest {
   514  		By("By testing restart when componentNames is not correct")
   515  		opsRequest := createTestOpsRequest(clusterName, opsRequestName, RestartType)
   516  		opsRequest.Spec.RestartList = []ComponentOps{
   517  			{ComponentName: "replicasets1"},
   518  		}
   519  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring(notFoundComponentsString("replicasets1")))
   520  
   521  		By("By testing restart. if api is legal, it will create successfully")
   522  		opsRequest.Spec.RestartList[0].ComponentName = componentName
   523  		Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).Should(Succeed())
   524  		return opsRequest
   525  	}
   526  
   527  	testReconfiguring := func(cluster *Cluster, clusterDef *ClusterDefinition) {
   528  		opsRequest := createTestOpsRequest(clusterName, opsRequestName+"-reconfiguring", ReconfiguringType)
   529  
   530  		createReconfigureObj := func(compName string) *Reconfigure {
   531  			return &Reconfigure{
   532  				ComponentOps: ComponentOps{ComponentName: compName},
   533  				Configurations: []ConfigurationItem{{Name: "for-test",
   534  					Keys: []ParameterConfig{{
   535  						Key: "test",
   536  						Parameters: []ParameterPair{{
   537  							Key:   "test",
   538  							Value: func(t string) *string { return &t }("test")}},
   539  					}},
   540  				}}}
   541  		}
   542  
   543  		By("By testing when spec.reconfiguring is null")
   544  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring("spec.reconfigure"))
   545  
   546  		By("By testing when target cluster definition not exist")
   547  		opsRequest.Spec.Reconfigure = createReconfigureObj(componentName + "-not-exist")
   548  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring("not found"))
   549  		opsRequest.Spec.Reconfigure = createReconfigureObj(componentName)
   550  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring("not found"))
   551  
   552  		By("By creating a configmap")
   553  		Expect(testCtx.CheckedCreateObj(ctx, createTestConfigmap(fmt.Sprintf("%s-%s-%s", opsRequest.Spec.ClusterRef, componentName, "for-test")))).Should(Succeed())
   554  		opsRequest.Spec.Reconfigure = createReconfigureObj(componentName)
   555  		Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring("not found in configmap"))
   556  
   557  		By("By test reconfiguring for file content")
   558  		r := createReconfigureObj(componentName)
   559  		r.Configurations[0].Keys[0].FileContent = "new context"
   560  		opsRequest.Spec.Reconfigure = r
   561  		Expect(testCtx.CreateObj(ctx, opsRequest)).To(Succeed())
   562  	}
   563  
   564  	Context("When clusterVersion create and update", func() {
   565  		It("Should webhook validate passed", func() {
   566  			By("By create a clusterDefinition")
   567  
   568  			// wait until ClusterDefinition and ClusterVersion created
   569  			clusterDef, _ := createTestClusterDefinitionObj(clusterDefinitionName)
   570  			Expect(testCtx.CheckedCreateObj(ctx, clusterDef)).Should(Succeed())
   571  			By("By creating a clusterVersion")
   572  			clusterVersion := createTestClusterVersionObj(clusterDefinitionName, clusterVersionName)
   573  			Expect(testCtx.CheckedCreateObj(ctx, clusterVersion)).Should(Succeed())
   574  
   575  			opsRequest := createTestOpsRequest(clusterName, opsRequestName, UpgradeType)
   576  
   577  			// create Cluster
   578  			By("By testing spec.clusterDef is legal")
   579  			Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).Should(HaveOccurred())
   580  			By("By create a new cluster ")
   581  			cluster, _ := createTestCluster(clusterDefinitionName, clusterVersionName, clusterName)
   582  			Expect(testCtx.CheckedCreateObj(ctx, cluster)).Should(Succeed())
   583  
   584  			testUpgrade(cluster)
   585  
   586  			testVerticalScaling(cluster)
   587  
   588  			testVolumeExpansion(cluster)
   589  
   590  			testHorizontalScaling(clusterDef, cluster)
   591  
   592  			testSwitchover(clusterDef, cluster)
   593  
   594  			testReconfiguring(cluster, clusterDef)
   595  
   596  			opsRequest = testRestart(cluster)
   597  
   598  			testWhenClusterDeleted(cluster, opsRequest)
   599  		})
   600  
   601  		It("check datascript opts", func() {
   602  			OpsRequestBehaviourMapper[DataScriptType] = OpsRequestBehaviour{
   603  				FromClusterPhases: []ClusterPhase{RunningClusterPhase},
   604  			}
   605  
   606  			By("By create a clusterDefinition")
   607  			clusterDef, _ := createTestClusterDefinitionObj(clusterDefinitionName)
   608  			Expect(testCtx.CheckedCreateObj(ctx, clusterDef)).Should(Succeed())
   609  			By("By creating a clusterVersion")
   610  			clusterVersion := createTestClusterVersionObj(clusterDefinitionName, clusterVersionName)
   611  			Expect(testCtx.CheckedCreateObj(ctx, clusterVersion)).Should(Succeed())
   612  
   613  			opsRequest := createTestOpsRequest(clusterName, opsRequestName, DataScriptType)
   614  			opsRequest.Spec.ScriptSpec = &ScriptSpec{
   615  				ComponentOps: ComponentOps{ComponentName: componentName},
   616  			}
   617  
   618  			// create Cluster
   619  			By("By testing spec.clusterDef is legal")
   620  			Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).Should(HaveOccurred())
   621  			By("By create a new cluster ")
   622  			cluster, _ := createTestCluster(clusterDefinitionName, clusterVersionName, clusterName)
   623  			Expect(testCtx.CheckedCreateObj(ctx, cluster)).Should(Succeed())
   624  
   625  			By("By testing dataScript without script, should fail")
   626  			Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).Should(HaveOccurred())
   627  
   628  			By("By testing dataScript, with script, no wait, should fail")
   629  			opsRequest.Spec.ScriptSpec.Script = []string{"create database test;"}
   630  			Expect(testCtx.CheckedCreateObj(ctx, opsRequest).Error()).To(ContainSubstring("DataScript is forbidden"))
   631  
   632  			By("By testing dataScript, with illegal configmap, should fail")
   633  			opsRequest.Spec.ScriptSpec.ScriptFrom = &ScriptFrom{
   634  				ConfigMapRef: []corev1.ConfigMapKeySelector{
   635  					{
   636  						LocalObjectReference: corev1.LocalObjectReference{
   637  							Name: "test-cm",
   638  						},
   639  						Key: "createdb",
   640  					},
   641  				},
   642  			}
   643  
   644  			Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).Should(HaveOccurred())
   645  
   646  			By("By testing dataScript, with illegal scriptFrom, should fail")
   647  			opsRequest.Spec.ScriptSpec.ScriptFrom.SecretRef = nil
   648  			Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).Should(HaveOccurred())
   649  
   650  			// patch cluster to running
   651  			By("By patching cluster to running")
   652  			Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(cluster), cluster)).Should(Succeed())
   653  			clusterPatch := client.MergeFrom(cluster.DeepCopy())
   654  			cluster.Status.Phase = RunningClusterPhase
   655  			Expect(k8sClient.Status().Patch(ctx, cluster, clusterPatch)).Should(Succeed())
   656  
   657  			Eventually(func() bool {
   658  				_ = k8sClient.Get(ctx, client.ObjectKeyFromObject(cluster), cluster)
   659  				return cluster.Status.Phase == RunningClusterPhase
   660  			}).Should(BeTrue())
   661  
   662  			opsRequest.Spec.ScriptSpec.Script = []string{"create database test;"}
   663  			opsRequest.Spec.ScriptSpec.ScriptFrom = nil
   664  			opsRequest.Spec.TTLSecondsBeforeAbort = int32Ptr(0)
   665  			Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).Should(Succeed())
   666  		})
   667  	})
   668  })
   669  
   670  func createTestOpsRequest(clusterName, opsRequestName string, opsType OpsType) *OpsRequest {
   671  	randomStr, _ := password.Generate(6, 0, 0, true, false)
   672  	return &OpsRequest{
   673  		ObjectMeta: metav1.ObjectMeta{
   674  			Name:      opsRequestName + randomStr,
   675  			Namespace: "default",
   676  			Labels: map[string]string{
   677  				"app.kubernetes.io/instance": clusterName,
   678  				"ops.kubeblocks.io/ops-type": string(opsType),
   679  			},
   680  		},
   681  		Spec: OpsRequestSpec{
   682  			ClusterRef: clusterName,
   683  			Type:       opsType,
   684  		},
   685  	}
   686  }
   687  
   688  func createTestConfigmap(cmName string) *corev1.ConfigMap {
   689  	return &corev1.ConfigMap{
   690  		ObjectMeta: metav1.ObjectMeta{
   691  			Name:      cmName,
   692  			Namespace: "default",
   693  		},
   694  		Data: map[string]string{
   695  			"key1": "value1",
   696  			"key2": "value2",
   697  		},
   698  	}
   699  }