github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/controllers/apps/operations/datascript_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 25 . "github.com/onsi/ginkgo/v2" 26 . "github.com/onsi/gomega" 27 batchv1 "k8s.io/api/batch/v1" 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 logf "sigs.k8s.io/controller-runtime/pkg/log" 32 "sigs.k8s.io/controller-runtime/pkg/reconcile" 33 34 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 35 "github.com/1aal/kubeblocks/pkg/constant" 36 intctrlutil "github.com/1aal/kubeblocks/pkg/controllerutil" 37 "github.com/1aal/kubeblocks/pkg/generics" 38 testapps "github.com/1aal/kubeblocks/pkg/testutil/apps" 39 viper "github.com/1aal/kubeblocks/pkg/viperx" 40 ) 41 42 var _ = Describe("DataScriptOps", func() { 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 clusterObj *appsv1alpha1.Cluster 50 opsResource *OpsResource 51 reqCtx intctrlutil.RequestCtx 52 ) 53 54 int32Ptr := func(i int32) *int32 { 55 return &i 56 } 57 58 cleanEnv := func() { 59 // must wait till resources deleted and no longer existed before the testcases start, 60 // otherwise if later it needs to create some new resource objects with the same name, 61 // in race conditions, it will find the existence of old objects, resulting failure to 62 // create the new objects. 63 By("clean resources") 64 65 // delete cluster(and all dependent sub-resources), clusterversion and clusterdef 66 testapps.ClearClusterResources(&testCtx) 67 68 // delete rest resources 69 inNS := client.InNamespace(testCtx.DefaultNamespace) 70 ml := client.HasLabels{testCtx.TestObjLabelKey} 71 // namespaced 72 testapps.ClearResources(&testCtx, generics.OpsRequestSignature, inNS, ml) 73 testapps.ClearResources(&testCtx, generics.SecretSignature, inNS, ml) 74 testapps.ClearResources(&testCtx, generics.ConfigMapSignature, inNS, ml) 75 testapps.ClearResources(&testCtx, generics.JobSignature, inNS, ml) 76 } 77 78 BeforeEach(cleanEnv) 79 80 AfterEach(cleanEnv) 81 82 createClusterDatascriptOps := func(comp string, ttlBeforeAbort int32) *appsv1alpha1.OpsRequest { 83 opsName := "datascript-ops-" + testCtx.GetRandomStr() 84 ops := testapps.NewOpsRequestObj(opsName, testCtx.DefaultNamespace, 85 clusterObj.Name, appsv1alpha1.DataScriptType) 86 ops.Spec.ScriptSpec = &appsv1alpha1.ScriptSpec{ 87 ComponentOps: appsv1alpha1.ComponentOps{ComponentName: comp}, 88 Script: []string{"CREATE TABLE test (id INT);"}, 89 } 90 ops.Spec.TTLSecondsBeforeAbort = int32Ptr(ttlBeforeAbort) 91 Expect(testCtx.CreateObj(testCtx.Ctx, ops)).Should(Succeed()) 92 return ops 93 } 94 95 patchOpsPhase := func(opsKey client.ObjectKey, phase appsv1alpha1.OpsPhase) { 96 ops := &appsv1alpha1.OpsRequest{} 97 Eventually(func(g Gomega) { 98 g.Expect(k8sClient.Get(testCtx.Ctx, opsKey, ops)).Should(Succeed()) 99 g.Expect(testapps.ChangeObjStatus(&testCtx, ops, func() { 100 ops.Status.Phase = phase 101 })).Should(Succeed()) 102 }).Should(Succeed()) 103 } 104 105 patchClusterStatus := func(phase appsv1alpha1.ClusterPhase) { 106 var compPhase appsv1alpha1.ClusterComponentPhase 107 switch phase { 108 case appsv1alpha1.RunningClusterPhase: 109 compPhase = appsv1alpha1.RunningClusterCompPhase 110 case appsv1alpha1.StoppedClusterPhase: 111 compPhase = appsv1alpha1.StoppedClusterCompPhase 112 case appsv1alpha1.FailedClusterPhase: 113 compPhase = appsv1alpha1.FailedClusterCompPhase 114 case appsv1alpha1.AbnormalClusterPhase: 115 compPhase = appsv1alpha1.AbnormalClusterCompPhase 116 case appsv1alpha1.CreatingClusterPhase: 117 compPhase = appsv1alpha1.CreatingClusterCompPhase 118 case appsv1alpha1.UpdatingClusterPhase: 119 compPhase = appsv1alpha1.UpdatingClusterCompPhase 120 } 121 122 Expect(testapps.ChangeObjStatus(&testCtx, clusterObj, func() { 123 clusterObj.Status.Phase = phase 124 clusterObj.Status.Components = map[string]appsv1alpha1.ClusterComponentStatus{ 125 consensusComp: { 126 Phase: compPhase, 127 }, 128 statelessComp: { 129 Phase: compPhase, 130 }, 131 statefulComp: { 132 Phase: compPhase, 133 }, 134 } 135 })).Should(Succeed()) 136 } 137 138 Context("with Cluster which has MySQL ConsensusSet", func() { 139 BeforeEach(func() { 140 By("mock cluster") 141 _, _, clusterObj = testapps.InitClusterWithHybridComps(&testCtx, clusterDefinitionName, 142 clusterVersionName, clusterName, statelessComp, statefulComp, consensusComp) 143 144 By("init opsResource") 145 opsResource = &OpsResource{ 146 Cluster: clusterObj, 147 Recorder: k8sManager.GetEventRecorderFor("opsrequest-controller"), 148 } 149 150 reqCtx = intctrlutil.RequestCtx{ 151 Ctx: testCtx.Ctx, 152 Log: logf.FromContext(testCtx.Ctx).WithValues("datascript", testCtx.DefaultNamespace), 153 } 154 }) 155 156 AfterEach(func() { 157 By("clean resources") 158 inNS := client.InNamespace(testCtx.DefaultNamespace) 159 testapps.ClearResources(&testCtx, generics.ClusterSignature, inNS, client.HasLabels{testCtx.TestObjLabelKey}) 160 testapps.ClearResources(&testCtx, generics.ServiceSignature, inNS, client.HasLabels{testCtx.TestObjLabelKey}) 161 testapps.ClearResources(&testCtx, generics.OpsRequestSignature, inNS, client.HasLabels{testCtx.TestObjLabelKey}) 162 testapps.ClearResources(&testCtx, generics.ServiceSignature, inNS, client.HasLabels{testCtx.TestObjLabelKey}) 163 testapps.ClearResources(&testCtx, generics.JobSignature, inNS, client.HasLabels{testCtx.TestObjLabelKey}) 164 }) 165 166 It("create a datascript ops with ttlSecondsBeforeAbort-0, abort immediately", func() { 167 By("patch cluster to creating") 168 patchClusterStatus(appsv1alpha1.CreatingClusterPhase) 169 170 By("create a datascript ops with ttlSecondsBeforeAbort=0") 171 // create a datascript ops with ttlSecondsBeforeAbort=0 172 ops := createClusterDatascriptOps(consensusComp, 0) 173 opsKey := client.ObjectKeyFromObject(ops) 174 patchOpsPhase(opsKey, appsv1alpha1.OpsCreatingPhase) 175 Expect(k8sClient.Get(testCtx.Ctx, opsKey, ops)).Should(Succeed()) 176 opsResource.OpsRequest = ops 177 178 reqCtx.Req = reconcile.Request{NamespacedName: opsKey} 179 By("check the opsRequest phase, should fail") 180 _, err := GetOpsManager().Do(reqCtx, k8sClient, opsResource) 181 Expect(err).Should(HaveOccurred()) 182 Expect(ops.Status.Phase).Should(Equal(appsv1alpha1.OpsFailedPhase)) 183 }) 184 185 It("create a datascript ops with ttlSecondsBeforeAbort=100, should requeue request", func() { 186 By("patch cluster to creating") 187 patchClusterStatus(appsv1alpha1.CreatingClusterPhase) 188 189 By("create a datascript ops with ttlSecondsBeforeAbort=100") 190 // create a datascript ops with ttlSecondsBeforeAbort=0 191 ops := createClusterDatascriptOps(consensusComp, 100) 192 opsKey := client.ObjectKeyFromObject(ops) 193 patchOpsPhase(opsKey, appsv1alpha1.OpsPendingPhase) 194 Expect(k8sClient.Get(testCtx.Ctx, opsKey, ops)).Should(Succeed()) 195 opsResource.OpsRequest = ops 196 prevOpsStatus := ops.Status.Phase 197 198 reqCtx.Req = reconcile.Request{NamespacedName: opsKey} 199 By("check the opsRequest phase") 200 _, err := GetOpsManager().Do(reqCtx, k8sClient, opsResource) 201 Expect(err).Should(Succeed()) 202 Expect(ops.Status.Phase).Should(Equal(prevOpsStatus)) 203 }) 204 205 It("create a datascript ops on running cluster", func() { 206 By("patch cluster to running") 207 patchClusterStatus(appsv1alpha1.RunningClusterPhase) 208 209 By("create a datascript ops with ttlSecondsBeforeAbort=0") 210 ops := createClusterDatascriptOps(consensusComp, 0) 211 opsResource.OpsRequest = ops 212 opsKey := client.ObjectKeyFromObject(ops) 213 patchOpsPhase(opsKey, appsv1alpha1.OpsCreatingPhase) 214 Expect(k8sClient.Get(testCtx.Ctx, opsKey, ops)).Should(Succeed()) 215 opsResource.OpsRequest = ops 216 217 reqCtx.Req = reconcile.Request{NamespacedName: opsKey} 218 By("check the opsRequest phase, should fail, cause pod is missing") 219 _, err := GetOpsManager().Do(reqCtx, k8sClient, opsResource) 220 Expect(err).Should(HaveOccurred()) 221 Expect(ops.Status.Phase).Should(Equal(appsv1alpha1.OpsFailedPhase)) 222 }) 223 224 It("reconcile a datascript ops on running cluster, patch job to complete", func() { 225 By("patch cluster to running") 226 patchClusterStatus(appsv1alpha1.RunningClusterPhase) 227 228 By("create a datascript ops with ttlSecondsBeforeAbort=0") 229 ops := createClusterDatascriptOps(consensusComp, 0) 230 opsResource.OpsRequest = ops 231 opsKey := client.ObjectKeyFromObject(ops) 232 patchOpsPhase(opsKey, appsv1alpha1.OpsRunningPhase) 233 Expect(k8sClient.Get(testCtx.Ctx, opsKey, ops)).Should(Succeed()) 234 opsResource.OpsRequest = ops 235 236 reqCtx.Req = reconcile.Request{NamespacedName: opsKey} 237 By("mock a job, missing service, should fail") 238 comp := clusterObj.Spec.GetComponentByName(consensusComp) 239 _, err := buildDataScriptJobs(reqCtx, k8sClient, clusterObj, comp, ops, "mysql") 240 Expect(err).Should(HaveOccurred()) 241 242 By("mock a service, should pass") 243 serviceName := fmt.Sprintf("%s-%s", clusterObj.Name, comp.Name) 244 service := &corev1.Service{ 245 ObjectMeta: metav1.ObjectMeta{Name: serviceName, Namespace: clusterObj.Namespace}, 246 Spec: corev1.ServiceSpec{Ports: []corev1.ServicePort{{Port: 3306}}}, 247 } 248 err = k8sClient.Create(testCtx.Ctx, service) 249 Expect(err).Should(Succeed()) 250 251 By("mock a job one more time, fail with missing secret") 252 _, err = buildDataScriptJobs(reqCtx, k8sClient, clusterObj, comp, ops, "mysql") 253 Expect(err).Should(HaveOccurred()) 254 Expect(err.Error()).Should(ContainSubstring("conn-credential")) 255 256 By("patch a secret name to ops, fail with missing secret") 257 secretName := fmt.Sprintf("%s-%s", clusterObj.Name, comp.Name) 258 patch := client.MergeFrom(ops.DeepCopy()) 259 ops.Spec.ScriptSpec.Secret = &appsv1alpha1.ScriptSecret{ 260 Name: secretName, 261 PasswordKey: "password", 262 UsernameKey: "username", 263 } 264 Expect(k8sClient.Patch(testCtx.Ctx, ops, patch)).Should(Succeed()) 265 266 _, err = buildDataScriptJobs(reqCtx, k8sClient, clusterObj, comp, ops, "mysql") 267 Expect(err).Should(HaveOccurred()) 268 Expect(err.Error()).Should(ContainSubstring(secretName)) 269 270 By("mock a secret, should pass") 271 secret := &corev1.Secret{ 272 ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: clusterObj.Namespace}, 273 Type: corev1.SecretTypeOpaque, 274 Data: map[string][]byte{ 275 "password": []byte("123456"), 276 "username": []byte("hellocoffee"), 277 }, 278 } 279 err = k8sClient.Create(testCtx.Ctx, secret) 280 Expect(err).Should(Succeed()) 281 282 By("create job, should pass") 283 viper.Set(constant.KBDataScriptClientsImage, "apecloud/kubeblocks-clients:latest") 284 jobs, err := buildDataScriptJobs(reqCtx, k8sClient, clusterObj, comp, ops, "mysql") 285 Expect(err).Should(Succeed()) 286 job := jobs[0] 287 Expect(k8sClient.Create(testCtx.Ctx, job)).Should(Succeed()) 288 289 By("reconcile the opsRequest phase") 290 _, err = GetOpsManager().Reconcile(reqCtx, k8sClient, opsResource) 291 Expect(err).Should(Succeed()) 292 Expect(ops.Status.Phase).Should(Equal(appsv1alpha1.OpsRunningPhase)) 293 294 By("patch job to succeed") 295 Eventually(func(g Gomega) { 296 g.Expect(testapps.ChangeObjStatus(&testCtx, job, func() { 297 job.Status.Succeeded = 1 298 job.Status.Conditions = append(job.Status.Conditions, 299 batchv1.JobCondition{ 300 Type: batchv1.JobComplete, 301 Status: corev1.ConditionTrue, 302 }) 303 })) 304 }).Should(Succeed()) 305 306 _, err = GetOpsManager().Reconcile(reqCtx, k8sClient, opsResource) 307 Expect(err).Should(Succeed()) 308 Expect(ops.Status.Phase).Should(Equal(appsv1alpha1.OpsSucceedPhase)) 309 310 Expect(k8sClient.Delete(testCtx.Ctx, service)).Should(Succeed()) 311 Expect(k8sClient.Delete(testCtx.Ctx, job)).Should(Succeed()) 312 Expect(k8sClient.Delete(testCtx.Ctx, secret)).Should(Succeed()) 313 }) 314 315 It("reconcile a datascript ops on running cluster, patch job to failed", func() { 316 By("patch cluster to running") 317 patchClusterStatus(appsv1alpha1.RunningClusterPhase) 318 319 By("create a datascript ops with ttlSecondsBeforeAbort=0") 320 ops := createClusterDatascriptOps(consensusComp, 0) 321 opsResource.OpsRequest = ops 322 opsKey := client.ObjectKeyFromObject(ops) 323 patchOpsPhase(opsKey, appsv1alpha1.OpsRunningPhase) 324 Expect(k8sClient.Get(testCtx.Ctx, opsKey, ops)).Should(Succeed()) 325 opsResource.OpsRequest = ops 326 327 reqCtx.Req = reconcile.Request{NamespacedName: opsKey} 328 comp := clusterObj.Spec.GetComponentByName(consensusComp) 329 By("mock a service, should pass") 330 serviceName := fmt.Sprintf("%s-%s", clusterObj.Name, comp.Name) 331 service := &corev1.Service{ 332 ObjectMeta: metav1.ObjectMeta{Name: serviceName, Namespace: clusterObj.Namespace}, 333 Spec: corev1.ServiceSpec{Ports: []corev1.ServicePort{{Port: 3306}}}, 334 } 335 err := k8sClient.Create(testCtx.Ctx, service) 336 Expect(err).Should(Succeed()) 337 338 By("patch a secret name to ops") 339 secretName := fmt.Sprintf("%s-%s", clusterObj.Name, comp.Name) 340 secret := &corev1.Secret{ 341 ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: clusterObj.Namespace}, 342 Type: corev1.SecretTypeOpaque, 343 Data: map[string][]byte{ 344 "password": []byte("123456"), 345 "username": []byte("hellocoffee"), 346 }, 347 } 348 patch := client.MergeFrom(ops.DeepCopy()) 349 ops.Spec.ScriptSpec.Secret = &appsv1alpha1.ScriptSecret{ 350 Name: secretName, 351 PasswordKey: "password", 352 UsernameKey: "username", 353 } 354 Expect(k8sClient.Patch(testCtx.Ctx, ops, patch)).Should(Succeed()) 355 356 By("mock a secret, should pass") 357 err = k8sClient.Create(testCtx.Ctx, secret) 358 Expect(err).Should(Succeed()) 359 360 By("create job, should pass") 361 viper.Set(constant.KBDataScriptClientsImage, "apecloud/kubeblocks-clients:latest") 362 jobs, err := buildDataScriptJobs(reqCtx, k8sClient, clusterObj, comp, ops, "mysql") 363 Expect(err).Should(Succeed()) 364 job := jobs[0] 365 Expect(k8sClient.Create(testCtx.Ctx, job)).Should(Succeed()) 366 367 By("reconcile the opsRequest phase") 368 _, err = GetOpsManager().Reconcile(reqCtx, k8sClient, opsResource) 369 Expect(err).Should(Succeed()) 370 Expect(ops.Status.Phase).Should(Equal(appsv1alpha1.OpsRunningPhase)) 371 372 By("patch job to failed") 373 Eventually(func(g Gomega) { 374 g.Expect(testapps.ChangeObjStatus(&testCtx, job, func() { 375 job.Status.Succeeded = 1 376 job.Status.Conditions = append(job.Status.Conditions, 377 batchv1.JobCondition{ 378 Type: batchv1.JobFailed, 379 Status: corev1.ConditionTrue, 380 }) 381 })) 382 }).Should(Succeed()) 383 384 _, err = GetOpsManager().Reconcile(reqCtx, k8sClient, opsResource) 385 Expect(err).Should(Succeed()) 386 Expect(ops.Status.Phase).Should(Equal(appsv1alpha1.OpsFailedPhase)) 387 388 Expect(k8sClient.Delete(testCtx.Ctx, service)).Should(Succeed()) 389 Expect(k8sClient.Delete(testCtx.Ctx, job)).Should(Succeed()) 390 Expect(k8sClient.Delete(testCtx.Ctx, secret)).Should(Succeed()) 391 }) 392 393 It("parse script from spec", func() { 394 cmName := "test-configmap" 395 secretName := "test-secret" 396 397 opsName := "datascript-ops-" + testCtx.GetRandomStr() 398 ops := testapps.NewOpsRequestObj(opsName, testCtx.DefaultNamespace, 399 clusterObj.Name, appsv1alpha1.DataScriptType) 400 ops.Spec.ScriptSpec = &appsv1alpha1.ScriptSpec{ 401 ComponentOps: appsv1alpha1.ComponentOps{ComponentName: consensusComp}, 402 Script: []string{"CREATE TABLE test (id INT);"}, 403 ScriptFrom: &appsv1alpha1.ScriptFrom{ 404 ConfigMapRef: []corev1.ConfigMapKeySelector{ 405 { 406 Key: "cm-key", 407 LocalObjectReference: corev1.LocalObjectReference{Name: cmName}, 408 }, 409 }, 410 SecretRef: []corev1.SecretKeySelector{ 411 { 412 Key: "secret-key", 413 LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, 414 }, 415 }, 416 }, 417 } 418 reqCtx.Req = reconcile.Request{NamespacedName: client.ObjectKeyFromObject(ops)} 419 _, err := getScriptContent(reqCtx, k8sClient, ops.Spec.ScriptSpec) 420 Expect(err).Should(HaveOccurred()) 421 422 // create configmap 423 configMap := &corev1.ConfigMap{ 424 ObjectMeta: metav1.ObjectMeta{ 425 Name: cmName, 426 Namespace: testCtx.DefaultNamespace, 427 }, 428 Data: map[string]string{ 429 "cm-key": "CREATE TABLE t1 (id INT);", 430 }, 431 } 432 433 Expect(k8sClient.Create(testCtx.Ctx, configMap)).Should(Succeed()) 434 _, err = getScriptContent(reqCtx, k8sClient, ops.Spec.ScriptSpec) 435 Expect(err).Should(HaveOccurred()) 436 437 // create configmap 438 secret := &corev1.Secret{ 439 ObjectMeta: metav1.ObjectMeta{ 440 Name: secretName, 441 Namespace: testCtx.DefaultNamespace, 442 }, 443 StringData: map[string]string{ 444 "secret-key": "CREATE TABLE t1 (id INT);", 445 }, 446 } 447 Expect(k8sClient.Create(testCtx.Ctx, secret)).Should(Succeed()) 448 _, err = getScriptContent(reqCtx, k8sClient, ops.Spec.ScriptSpec) 449 Expect(err).Should(Succeed()) 450 }) 451 }) 452 })