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