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