github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/operations_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 cluster
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"strings"
    26  
    27  	. "github.com/onsi/ginkgo/v2"
    28  	. "github.com/onsi/gomega"
    29  	corev1 "k8s.io/api/core/v1"
    30  	"k8s.io/apimachinery/pkg/api/resource"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/runtime"
    33  	"k8s.io/cli-runtime/pkg/genericiooptions"
    34  	clientfake "k8s.io/client-go/rest/fake"
    35  	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
    36  
    37  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    38  	"github.com/1aal/kubeblocks/pkg/cli/testing"
    39  	"github.com/1aal/kubeblocks/pkg/constant"
    40  	testapps "github.com/1aal/kubeblocks/pkg/testutil/apps"
    41  )
    42  
    43  var _ = Describe("operations", func() {
    44  	const (
    45  		clusterName  = "cluster-ops"
    46  		clusterName1 = "cluster-ops1"
    47  	)
    48  	var (
    49  		streams genericiooptions.IOStreams
    50  		tf      *cmdtesting.TestFactory
    51  		in      *bytes.Buffer
    52  	)
    53  
    54  	BeforeEach(func() {
    55  		streams, in, _, _ = genericiooptions.NewTestIOStreams()
    56  		tf = cmdtesting.NewTestFactory().WithNamespace(testing.Namespace)
    57  		clusterWithTwoComps := testing.FakeCluster(clusterName, testing.Namespace)
    58  		clusterWithOneComp := clusterWithTwoComps.DeepCopy()
    59  		clusterWithOneComp.Name = clusterName1
    60  		clusterWithOneComp.Spec.ComponentSpecs = []appsv1alpha1.ClusterComponentSpec{
    61  			clusterWithOneComp.Spec.ComponentSpecs[0],
    62  		}
    63  		clusterWithOneComp.Spec.ComponentSpecs[0].ClassDefRef = &appsv1alpha1.ClassDefRef{Class: testapps.Class1c1gName}
    64  		classDef := testapps.NewComponentClassDefinitionFactory("custom", clusterWithOneComp.Spec.ClusterDefRef, testing.ComponentDefName).
    65  			AddClasses([]appsv1alpha1.ComponentClass{testapps.Class1c1g}).
    66  			GetObject()
    67  		resourceConstraint := testapps.NewComponentResourceConstraintFactory(testapps.DefaultResourceConstraintName).
    68  			AddConstraints(testapps.ProductionResourceConstraint).
    69  			AddSelector(appsv1alpha1.ClusterResourceConstraintSelector{
    70  				ClusterDefRef: testing.ClusterDefName,
    71  				Components: []appsv1alpha1.ComponentResourceConstraintSelector{
    72  					{
    73  						ComponentDefRef: testing.ComponentDefName,
    74  						Rules:           []string{"c1"},
    75  					},
    76  				},
    77  			}).
    78  			GetObject()
    79  		pods := testing.FakePods(2, clusterWithOneComp.Namespace, clusterName1)
    80  		tf.Client = &clientfake.RESTClient{}
    81  		tf.FakeDynamicClient = testing.FakeDynamicClient(testing.FakeClusterDef(),
    82  			testing.FakeClusterVersion(), clusterWithTwoComps, clusterWithOneComp, classDef, &pods.Items[0], &pods.Items[1], resourceConstraint)
    83  	})
    84  
    85  	AfterEach(func() {
    86  		tf.Cleanup()
    87  	})
    88  
    89  	initCommonOperationOps := func(opsType appsv1alpha1.OpsType, clusterName string, hasComponentNamesFlag bool, objs ...runtime.Object) *OperationsOptions {
    90  		o := newBaseOperationsOptions(tf, streams, opsType, hasComponentNamesFlag)
    91  		o.Dynamic = tf.FakeDynamicClient
    92  		o.Client = testing.FakeClientSet(objs...)
    93  		o.Name = clusterName
    94  		o.Namespace = testing.Namespace
    95  		return o
    96  	}
    97  
    98  	getOpsName := func(opsType appsv1alpha1.OpsType, phase appsv1alpha1.OpsPhase) string {
    99  		return strings.ToLower(string(opsType)) + "-" + strings.ToLower(string(phase))
   100  	}
   101  
   102  	generationOps := func(opsType appsv1alpha1.OpsType, phase appsv1alpha1.OpsPhase) *appsv1alpha1.OpsRequest {
   103  		return &appsv1alpha1.OpsRequest{
   104  			ObjectMeta: metav1.ObjectMeta{
   105  				Name:      getOpsName(opsType, phase),
   106  				Namespace: testing.Namespace,
   107  			},
   108  			Spec: appsv1alpha1.OpsRequestSpec{
   109  				ClusterRef: "test-cluster",
   110  				Type:       opsType,
   111  			},
   112  			Status: appsv1alpha1.OpsRequestStatus{
   113  				Phase: phase,
   114  			},
   115  		}
   116  
   117  	}
   118  
   119  	It("Upgrade Ops", func() {
   120  		o := newBaseOperationsOptions(tf, streams, appsv1alpha1.UpgradeType, false)
   121  		o.Dynamic = tf.FakeDynamicClient
   122  
   123  		By("validate o.name is null")
   124  		Expect(o.Validate()).To(MatchError(missingClusterArgErrMassage))
   125  
   126  		By("validate upgrade when cluster-version is null")
   127  		o.Namespace = testing.Namespace
   128  		o.Name = clusterName
   129  		o.OpsType = appsv1alpha1.UpgradeType
   130  		Expect(o.Validate()).To(MatchError("missing cluster-version"))
   131  
   132  		By("expect to validate success")
   133  		o.ClusterVersionRef = "test-cluster-version"
   134  		in.Write([]byte(o.Name + "\n"))
   135  		Expect(o.Validate()).Should(Succeed())
   136  	})
   137  
   138  	It("VolumeExpand Ops", func() {
   139  		compName := "replicasets"
   140  		vctName := "data"
   141  		persistentVolumeClaim := &corev1.PersistentVolumeClaim{
   142  			ObjectMeta: metav1.ObjectMeta{
   143  				Name:      fmt.Sprintf("%s-%s-%s-%d", vctName, clusterName, compName, 0),
   144  				Namespace: testing.Namespace,
   145  				Labels: map[string]string{
   146  					constant.AppInstanceLabelKey:             clusterName,
   147  					constant.VolumeClaimTemplateNameLabelKey: vctName,
   148  					constant.KBAppComponentLabelKey:          compName,
   149  				},
   150  			},
   151  			Spec: corev1.PersistentVolumeClaimSpec{
   152  				AccessModes: []corev1.PersistentVolumeAccessMode{
   153  					corev1.ReadWriteOnce,
   154  				},
   155  				Resources: corev1.ResourceRequirements{
   156  					Requests: corev1.ResourceList{
   157  						"storage": resource.MustParse("3Gi"),
   158  					},
   159  				},
   160  			},
   161  			Status: corev1.PersistentVolumeClaimStatus{
   162  				Capacity: map[corev1.ResourceName]resource.Quantity{
   163  					"storage": resource.MustParse("1Gi"),
   164  				},
   165  			},
   166  		}
   167  		o := initCommonOperationOps(appsv1alpha1.VolumeExpansionType, clusterName, true, persistentVolumeClaim)
   168  		By("validate volumeExpansion when components is null")
   169  		Expect(o.Validate()).To(MatchError(`missing components, please specify the "--components" flag for multi-components cluster`))
   170  
   171  		By("validate volumeExpansion when vct-names is null")
   172  		o.ComponentNames = []string{compName}
   173  		Expect(o.Validate()).To(MatchError("missing volume-claim-templates"))
   174  
   175  		By("validate volumeExpansion when storage is null")
   176  		o.VCTNames = []string{vctName}
   177  		Expect(o.Validate()).To(MatchError("missing storage"))
   178  
   179  		By("validate recovery from volume expansion failure")
   180  		o.Storage = "2Gi"
   181  		Expect(o.Validate()).Should(Succeed())
   182  		Expect(o.Out.(*bytes.Buffer).String()).To(ContainSubstring("Warning: this opsRequest is a recovery action for volume expansion failure and will re-create the PersistentVolumeClaims when RECOVER_VOLUME_EXPANSION_FAILURE=false"))
   183  
   184  		By("validate passed")
   185  		o.Storage = "4Gi"
   186  		in.Write([]byte(o.Name + "\n"))
   187  		Expect(o.Validate()).Should(Succeed())
   188  	})
   189  
   190  	It("Vscale Ops", func() {
   191  		o := initCommonOperationOps(appsv1alpha1.VerticalScalingType, clusterName1, true)
   192  		By("test CompleteComponentsFlag function")
   193  		o.ComponentNames = nil
   194  		By("expect to auto complete components when cluster has only one component")
   195  		Expect(o.CompleteComponentsFlag()).Should(Succeed())
   196  		Expect(o.ComponentNames[0]).Should(Equal(testing.ComponentName))
   197  
   198  		By("validate invalid class")
   199  		o.Class = "class-not-exists"
   200  		in.Write([]byte(o.Name + "\n"))
   201  		Expect(o.Validate()).Should(HaveOccurred())
   202  
   203  		By("expect to validate success with class")
   204  		o.Class = testapps.Class1c1gName
   205  		in.Write([]byte(o.Name + "\n"))
   206  		Expect(o.Validate()).ShouldNot(HaveOccurred())
   207  
   208  		By("validate invalid resource")
   209  		o.Class = ""
   210  		o.CPU = "100"
   211  		o.Memory = "100Gi"
   212  		in.Write([]byte(o.Name + "\n"))
   213  		Expect(o.Validate()).Should(HaveOccurred())
   214  
   215  		By("validate invalid resource")
   216  		o.Class = ""
   217  		o.CPU = "1g"
   218  		o.Memory = "100Gi"
   219  		in.Write([]byte(o.Name + "\n"))
   220  		Expect(o.Validate()).Should(HaveOccurred())
   221  
   222  		By("validate invalid resource")
   223  		o.Class = ""
   224  		o.CPU = "1"
   225  		o.Memory = "100MB"
   226  		in.Write([]byte(o.Name + "\n"))
   227  		Expect(o.Validate()).Should(HaveOccurred())
   228  
   229  		By("expect to validate success with resource")
   230  		o.Class = ""
   231  		o.CPU = "1"
   232  		o.Memory = "1Gi"
   233  		in.Write([]byte(o.Name + "\n"))
   234  		Expect(o.Validate()).ShouldNot(HaveOccurred())
   235  	})
   236  
   237  	It("Hscale Ops", func() {
   238  		o := initCommonOperationOps(appsv1alpha1.HorizontalScalingType, clusterName1, true)
   239  		By("test CompleteComponentsFlag function")
   240  		o.ComponentNames = nil
   241  		By("expect to auto complete components when cluster has only one component")
   242  		Expect(o.CompleteComponentsFlag()).Should(Succeed())
   243  		Expect(o.ComponentNames[0]).Should(Equal(testing.ComponentName))
   244  
   245  		By("expect to Validate success")
   246  		o.Replicas = 1
   247  		in.Write([]byte(o.Name + "\n"))
   248  		Expect(o.Validate()).Should(Succeed())
   249  
   250  		By("expect for componentNames is nil when cluster has only two component")
   251  		o.Name = clusterName
   252  		o.ComponentNames = nil
   253  		Expect(o.CompleteComponentsFlag()).Should(Succeed())
   254  		Expect(o.ComponentNames).Should(BeEmpty())
   255  	})
   256  
   257  	It("Restart ops", func() {
   258  		o := initCommonOperationOps(appsv1alpha1.RestartType, clusterName, true)
   259  		By("expect for not found error")
   260  		o.Args = []string{clusterName + "2"}
   261  		Expect(o.Complete())
   262  		Expect(o.CompleteRestartOps().Error()).Should(ContainSubstring("not found"))
   263  
   264  		By("expect for complete success")
   265  		o.Name = clusterName
   266  		Expect(o.CompleteRestartOps()).Should(Succeed())
   267  
   268  		By("test Restart command")
   269  		restartCmd := NewRestartCmd(tf, streams)
   270  		_, _ = in.Write([]byte(clusterName + "\n"))
   271  		done := testing.Capture()
   272  		restartCmd.Run(restartCmd, []string{clusterName})
   273  		capturedOutput, _ := done()
   274  		Expect(testing.ContainExpectStrings(capturedOutput, "kbcli cluster describe-ops")).Should(BeTrue())
   275  	})
   276  
   277  	It("cancel ops", func() {
   278  		By("init some opsRequests which are needed for canceling opsRequest")
   279  		completedPhases := []appsv1alpha1.OpsPhase{appsv1alpha1.OpsCancelledPhase, appsv1alpha1.OpsSucceedPhase, appsv1alpha1.OpsFailedPhase}
   280  		supportedOpsType := []appsv1alpha1.OpsType{appsv1alpha1.VerticalScalingType, appsv1alpha1.HorizontalScalingType}
   281  		notSupportedOpsType := []appsv1alpha1.OpsType{appsv1alpha1.RestartType, appsv1alpha1.UpgradeType}
   282  		processingPhases := []appsv1alpha1.OpsPhase{appsv1alpha1.OpsPendingPhase, appsv1alpha1.OpsCreatingPhase, appsv1alpha1.OpsRunningPhase}
   283  		opsList := make([]runtime.Object, 0)
   284  		for _, opsType := range supportedOpsType {
   285  			for _, phase := range completedPhases {
   286  				opsList = append(opsList, generationOps(opsType, phase))
   287  			}
   288  			for _, phase := range processingPhases {
   289  				opsList = append(opsList, generationOps(opsType, phase))
   290  			}
   291  			// mock cancelling opsRequest
   292  			opsList = append(opsList, generationOps(opsType, appsv1alpha1.OpsCancellingPhase))
   293  		}
   294  
   295  		for _, opsType := range notSupportedOpsType {
   296  			opsList = append(opsList, generationOps(opsType, appsv1alpha1.OpsRunningPhase))
   297  		}
   298  		tf.FakeDynamicClient = testing.FakeDynamicClient(opsList...)
   299  
   300  		By("expect an error for not supported phase")
   301  		o := newBaseOperationsOptions(tf, streams, "", false)
   302  		o.Dynamic = tf.FakeDynamicClient
   303  		o.Namespace = testing.Namespace
   304  		o.autoApprove = true
   305  		for _, phase := range completedPhases {
   306  			for _, opsType := range supportedOpsType {
   307  				o.Name = getOpsName(opsType, phase)
   308  				Expect(cancelOps(o).Error()).Should(Equal(fmt.Sprintf("can not cancel the opsRequest when phase is %s", phase)))
   309  			}
   310  		}
   311  
   312  		By("expect an error for not supported opsType")
   313  		for _, opsType := range notSupportedOpsType {
   314  			o.Name = getOpsName(opsType, appsv1alpha1.OpsRunningPhase)
   315  			Expect(cancelOps(o).Error()).Should(Equal(fmt.Sprintf("opsRequest type: %s not support cancel action", opsType)))
   316  		}
   317  
   318  		By("expect an error for cancelling opsRequest")
   319  		for _, opsType := range supportedOpsType {
   320  			o.Name = getOpsName(opsType, appsv1alpha1.OpsCancellingPhase)
   321  			Expect(cancelOps(o).Error()).Should(Equal(fmt.Sprintf(`opsRequest "%s" is cancelling`, o.Name)))
   322  		}
   323  
   324  		By("expect succeed for canceling the opsRequest which is processing")
   325  		for _, phase := range processingPhases {
   326  			for _, opsType := range supportedOpsType {
   327  				o.Name = getOpsName(opsType, phase)
   328  				Expect(cancelOps(o)).Should(Succeed())
   329  			}
   330  		}
   331  	})
   332  
   333  	It("Switchover ops", func() {
   334  		o := initCommonOperationOps(appsv1alpha1.SwitchoverType, clusterName1, false)
   335  		By("expect to auto complete components when cluster has only one component")
   336  		Expect(o.CompleteComponentsFlag()).Should(Succeed())
   337  		Expect(o.ComponentNames[0]).Should(Equal(testing.ComponentName))
   338  
   339  		By("expect for componentNames is nil when cluster has only two component")
   340  		o.Name = clusterName
   341  		o.ComponentNames = nil
   342  		Expect(o.CompleteComponentsFlag()).Should(Succeed())
   343  		Expect(o.ComponentNames).Should(BeEmpty())
   344  
   345  		By("validate failed because there are multi-components in cluster and not specify the component")
   346  		Expect(o.CompleteComponentsFlag()).Should(Succeed())
   347  		Expect(o.Validate()).ShouldNot(Succeed())
   348  		Expect(testing.ContainExpectStrings(o.Validate().Error(), "there are multiple components in cluster, please use --component to specify the component for promote")).Should(BeTrue())
   349  
   350  		By("validate failed because o.Instance is illegal ")
   351  		o.Name = clusterName1
   352  		o.Instance = fmt.Sprintf("%s-%s-%d", clusterName1, testing.ComponentName, 5)
   353  		Expect(o.Validate()).ShouldNot(Succeed())
   354  		Expect(testing.ContainExpectStrings(o.Validate().Error(), "not found")).Should(BeTrue())
   355  
   356  		By("validate failed because o.Instance is already leader and cannot be promoted")
   357  		o.Instance = fmt.Sprintf("%s-pod-%d", clusterName1, 0)
   358  		Expect(o.Validate()).ShouldNot(Succeed())
   359  		Expect(testing.ContainExpectStrings(o.Validate().Error(), "cannot be promoted because it is already the primary or leader instance")).Should(BeTrue())
   360  
   361  		By("validate failed because o.Instance does not belong to the current component")
   362  		o.Instance = fmt.Sprintf("%s-pod-%d", clusterName1, 1)
   363  		Expect(o.Validate()).ShouldNot(Succeed())
   364  		Expect(testing.ContainExpectStrings(o.Validate().Error(), "does not belong to the current component")).Should(BeTrue())
   365  
   366  		By("validate failed because mock component has no switchoverSpec, does not support switchover")
   367  		o.Name = clusterName
   368  		o.Instance = ""
   369  		o.Component = testing.ComponentName
   370  		Expect(o.Validate()).ShouldNot(Succeed())
   371  		Expect(testing.ContainExpectStrings(o.Validate().Error(), "does not support switchover")).Should(BeTrue())
   372  	})
   373  })