github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/controllers/apps/operations/reconfigure_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  	"encoding/json"
    24  
    25  	. "github.com/onsi/ginkgo/v2"
    26  	. "github.com/onsi/gomega"
    27  	"github.com/spf13/cast"
    28  
    29  	corev1 "k8s.io/api/core/v1"
    30  	"sigs.k8s.io/controller-runtime/pkg/client"
    31  	"sigs.k8s.io/controller-runtime/pkg/log"
    32  
    33  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    34  	opsutil "github.com/1aal/kubeblocks/controllers/apps/operations/util"
    35  	"github.com/1aal/kubeblocks/pkg/configuration/core"
    36  	"github.com/1aal/kubeblocks/pkg/constant"
    37  	"github.com/1aal/kubeblocks/pkg/controller/builder"
    38  	configutil "github.com/1aal/kubeblocks/pkg/controller/configuration"
    39  	intctrlutil "github.com/1aal/kubeblocks/pkg/controllerutil"
    40  	"github.com/1aal/kubeblocks/pkg/generics"
    41  	testapps "github.com/1aal/kubeblocks/pkg/testutil/apps"
    42  )
    43  
    44  var _ = Describe("Reconfigure OpsRequest", func() {
    45  
    46  	var (
    47  		randomStr             = testCtx.GetRandomStr()
    48  		clusterDefinitionName = "cluster-definition-for-ops-" + randomStr
    49  		clusterVersionName    = "clusterversion-for-ops-" + randomStr
    50  		clusterName           = "cluster-for-ops-" + randomStr
    51  	)
    52  
    53  	cleanEnv := func() {
    54  		// must wait till resources deleted and no longer existed before the testcases start,
    55  		// otherwise if later it needs to create some new resource objects with the same name,
    56  		// in race conditions, it will find the existence of old objects, resulting failure to
    57  		// create the new objects.
    58  		By("clean resources")
    59  
    60  		// delete cluster(and all dependent sub-resources), clusterversion and clusterdef
    61  		testapps.ClearClusterResources(&testCtx)
    62  
    63  		// delete rest resources
    64  		inNS := client.InNamespace(testCtx.DefaultNamespace)
    65  		ml := client.HasLabels{testCtx.TestObjLabelKey}
    66  		// namespaced
    67  		testapps.ClearResources(&testCtx, generics.OpsRequestSignature, inNS, ml)
    68  		testapps.ClearResources(&testCtx, generics.ConfigMapSignature, inNS, ml)
    69  		// non-namespaced
    70  		testapps.ClearResources(&testCtx, generics.ConfigConstraintSignature, ml)
    71  	}
    72  
    73  	BeforeEach(cleanEnv)
    74  
    75  	AfterEach(cleanEnv)
    76  
    77  	initClusterForOps := func(opsRes *OpsResource) {
    78  		Expect(opsutil.PatchClusterOpsAnnotations(ctx, k8sClient, opsRes.Cluster, nil)).Should(Succeed())
    79  		opsRes.Cluster.Status.Phase = appsv1alpha1.RunningClusterPhase
    80  	}
    81  
    82  	assureCfgTplObj := func(tplName, cmName, ns string) (*corev1.ConfigMap, *appsv1alpha1.ConfigConstraint) {
    83  		By("Assuring an cm obj")
    84  		cfgCM := testapps.NewCustomizedObj("operations_config/config-template.yaml",
    85  			&corev1.ConfigMap{}, testapps.WithNamespacedName(cmName, ns))
    86  		cfgTpl := testapps.NewCustomizedObj("operations_config/config-constraint.yaml",
    87  			&appsv1alpha1.ConfigConstraint{}, testapps.WithNamespacedName(tplName, ns))
    88  		Expect(testCtx.CheckedCreateObj(ctx, cfgCM)).Should(Succeed())
    89  		Expect(testCtx.CheckedCreateObj(ctx, cfgTpl)).Should(Succeed())
    90  
    91  		return cfgCM, cfgTpl
    92  	}
    93  
    94  	assureConfigInstanceObj := func(clusterName, componentName, ns string, cdComponent *appsv1alpha1.ClusterComponentDefinition) (*appsv1alpha1.Configuration, *corev1.ConfigMap) {
    95  		if len(cdComponent.ConfigSpecs) == 0 {
    96  			return nil, nil
    97  		}
    98  
    99  		By("create configuration cr")
   100  		configuration := builder.NewConfigurationBuilder(testCtx.DefaultNamespace, core.GenerateComponentConfigurationName(clusterName, componentName)).
   101  			ClusterRef(clusterName).
   102  			Component(componentName)
   103  		for _, configSpec := range cdComponent.ConfigSpecs {
   104  			configuration.AddConfigurationItem(configSpec)
   105  		}
   106  		Expect(testCtx.CheckedCreateObj(ctx, configuration.GetObject())).Should(Succeed())
   107  
   108  		// update status
   109  		By("update configuration status")
   110  		revision := "1"
   111  		Eventually(testapps.GetAndChangeObjStatus(&testCtx, client.ObjectKeyFromObject(configuration.GetObject()),
   112  			func(config *appsv1alpha1.Configuration) {
   113  				revision = cast.ToString(config.GetGeneration())
   114  				for _, item := range config.Spec.ConfigItemDetails {
   115  					configutil.CheckAndUpdateItemStatus(config, item, revision)
   116  				}
   117  			})).Should(Succeed())
   118  
   119  		By("create configmap for configSpecs")
   120  		var cmObj *corev1.ConfigMap
   121  		for _, configSpec := range cdComponent.ConfigSpecs {
   122  			cmInsName := core.GetComponentCfgName(clusterName, componentName, configSpec.Name)
   123  			By("create configmap: " + cmInsName)
   124  			cfgCM := testapps.NewCustomizedObj("operations_config/config-template.yaml",
   125  				&corev1.ConfigMap{},
   126  				testapps.WithNamespacedName(cmInsName, ns),
   127  				testapps.WithLabels(
   128  					constant.AppNameLabelKey, clusterName,
   129  					constant.ConfigurationRevision, revision,
   130  					constant.AppInstanceLabelKey, clusterName,
   131  					constant.KBAppComponentLabelKey, componentName,
   132  					constant.CMConfigurationTemplateNameLabelKey, configSpec.TemplateRef,
   133  					constant.CMConfigurationConstraintsNameLabelKey, configSpec.ConfigConstraintRef,
   134  					constant.CMConfigurationSpecProviderLabelKey, configSpec.Name,
   135  					constant.CMConfigurationTypeLabelKey, constant.ConfigInstanceType,
   136  				),
   137  			)
   138  			Expect(testCtx.CheckedCreateObj(ctx, cfgCM)).Should(Succeed())
   139  			cmObj = cfgCM
   140  		}
   141  		return configuration.GetObject(), cmObj
   142  	}
   143  
   144  	assureMockReconfigureData := func(policyName string) (*OpsResource, *appsv1alpha1.Configuration, *corev1.ConfigMap) {
   145  		By("init operations resources ")
   146  		opsRes, clusterDef, clusterObject := initOperationsResources(clusterDefinitionName, clusterVersionName, clusterName)
   147  
   148  		var (
   149  			cfgObj       *corev1.ConfigMap
   150  			config       *appsv1alpha1.Configuration
   151  			stsComponent *appsv1alpha1.ClusterComponentDefinition
   152  		)
   153  		By("Test Reconfigure")
   154  		{
   155  			// mock cluster is Running to support reconfiguring ops
   156  			By("mock cluster status")
   157  			patch := client.MergeFrom(clusterObject.DeepCopy())
   158  			clusterObject.Status.Phase = appsv1alpha1.RunningClusterPhase
   159  			Expect(k8sClient.Status().Patch(ctx, clusterObject, patch)).Should(Succeed())
   160  		}
   161  
   162  		{
   163  			By("mock config tpl")
   164  			cmObj, tplObj := assureCfgTplObj("mysql-tpl-test", "mysql-cm-test", testCtx.DefaultNamespace)
   165  			By("update clusterdefinition tpl")
   166  			patch := client.MergeFrom(clusterDef.DeepCopy())
   167  			for i := range clusterDef.Spec.ComponentDefs {
   168  				component := &clusterDef.Spec.ComponentDefs[i]
   169  				if component.Name != consensusComp {
   170  					continue
   171  				}
   172  				stsComponent = component
   173  				component.ConfigSpecs = []appsv1alpha1.ComponentConfigSpec{{
   174  					ComponentTemplateSpec: appsv1alpha1.ComponentTemplateSpec{
   175  						Name:        "mysql-test",
   176  						TemplateRef: cmObj.Name,
   177  						VolumeName:  "mysql-config",
   178  						Namespace:   testCtx.DefaultNamespace,
   179  					},
   180  					ConfigConstraintRef: tplObj.Name,
   181  				}}
   182  			}
   183  
   184  			Expect(k8sClient.Patch(ctx, clusterDef, patch)).Should(Succeed())
   185  			By("mock config cm object")
   186  			config, cfgObj = assureConfigInstanceObj(clusterName, consensusComp, testCtx.DefaultNamespace, stsComponent)
   187  		}
   188  
   189  		return opsRes, config, cfgObj
   190  	}
   191  
   192  	Context("Test Reconfigure", func() {
   193  		It("Test Reconfigure OpsRequest with restart", func() {
   194  			opsRes, configuration, _ := assureMockReconfigureData("simple")
   195  			reqCtx := intctrlutil.RequestCtx{
   196  				Ctx:      testCtx.Ctx,
   197  				Log:      log.FromContext(ctx).WithName("Reconfigure"),
   198  				Recorder: opsRes.Recorder,
   199  			}
   200  
   201  			By("mock reconfigure success")
   202  			ops := testapps.NewOpsRequestObj("reconfigure-ops-"+randomStr, testCtx.DefaultNamespace,
   203  				clusterName, appsv1alpha1.ReconfiguringType)
   204  			ops.Spec.Reconfigure = &appsv1alpha1.Reconfigure{
   205  				Configurations: []appsv1alpha1.ConfigurationItem{{
   206  					Name: "mysql-test",
   207  					Keys: []appsv1alpha1.ParameterConfig{{
   208  						Key: "my.cnf",
   209  						Parameters: []appsv1alpha1.ParameterPair{
   210  							{
   211  								Key:   "binlog_stmt_cache_size",
   212  								Value: func() *string { v := "4096"; return &v }(),
   213  							},
   214  							{
   215  								Key:   "key",
   216  								Value: func() *string { v := "abcd"; return &v }(),
   217  							},
   218  						},
   219  					}},
   220  				}},
   221  				ComponentOps: appsv1alpha1.ComponentOps{ComponentName: consensusComp},
   222  			}
   223  
   224  			By("Init Reconfiguring opsrequest")
   225  			opsRes.OpsRequest = ops
   226  			Expect(testCtx.CheckedCreateObj(ctx, ops)).Should(Succeed())
   227  			initClusterForOps(opsRes)
   228  
   229  			opsManager := GetOpsManager()
   230  			By("init ops phase")
   231  			_, err := opsManager.Do(reqCtx, k8sClient, opsRes)
   232  			Expect(err).ShouldNot(HaveOccurred())
   233  
   234  			By("Reconfigure configure")
   235  			_, err = opsManager.Do(reqCtx, k8sClient, opsRes)
   236  			Expect(err).ShouldNot(HaveOccurred())
   237  
   238  			Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(opsRes.OpsRequest), opsRes.OpsRequest)).Should(Succeed())
   239  			_, _ = opsManager.Reconcile(reqCtx, k8sClient, opsRes)
   240  			Expect(opsRes.OpsRequest.Status.Phase).Should(Equal(appsv1alpha1.OpsRunningPhase))
   241  
   242  			By("mock configuration.status.phase to Finished")
   243  			var item *appsv1alpha1.ConfigurationItemDetail
   244  			Eventually(testapps.GetAndChangeObjStatus(&testCtx, client.ObjectKeyFromObject(configuration),
   245  				func(config *appsv1alpha1.Configuration) {
   246  					item = config.Spec.GetConfigurationItem("mysql-test")
   247  					for i := 0; i < len(config.Status.ConfigurationItemStatus); i++ {
   248  						config.Status.ConfigurationItemStatus[i].Phase = appsv1alpha1.CFinishedPhase
   249  						if config.Status.ConfigurationItemStatus[i].Name == item.Name {
   250  							config.Status.ConfigurationItemStatus[i].ReconcileDetail = &appsv1alpha1.ReconcileDetail{
   251  								Policy:          "simple",
   252  								CurrentRevision: config.Status.ConfigurationItemStatus[i].UpdateRevision,
   253  								SucceedCount:    2,
   254  								ExpectedCount:   2,
   255  							}
   256  						}
   257  					}
   258  				})).Should(Succeed())
   259  
   260  			By("mock configmap controller to updated")
   261  			Eventually(testapps.GetAndChangeObj(&testCtx, client.ObjectKey{
   262  				Name:      core.GetComponentCfgName(clusterName, consensusComp, "mysql-test"),
   263  				Namespace: testCtx.DefaultNamespace},
   264  				func(cm *corev1.ConfigMap) {
   265  					b, err := json.Marshal(item)
   266  					Expect(err).ShouldNot(HaveOccurred())
   267  					if cm.Annotations == nil {
   268  						cm.Annotations = make(map[string]string)
   269  					}
   270  					cm.Annotations[constant.ConfigAppliedVersionAnnotationKey] = string(b)
   271  					b, err = json.Marshal(intctrlutil.Result{
   272  						Phase:      appsv1alpha1.CFinishedPhase,
   273  						Policy:     "simple",
   274  						ExecResult: "none",
   275  					})
   276  					Expect(err).ShouldNot(HaveOccurred())
   277  					cm.Annotations[core.GenerateRevisionPhaseKey("1")] = string(b)
   278  				})).Should(Succeed())
   279  
   280  			By("Reconfigure operation success")
   281  			// Expect(reAction.Handle(eventContext, ops.Name, appsv1alpha1.OpsSucceedPhase, nil)).Should(Succeed())
   282  			Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(opsRes.OpsRequest), opsRes.OpsRequest)).Should(Succeed())
   283  			_, _ = opsManager.Reconcile(reqCtx, k8sClient, opsRes)
   284  			Expect(opsRes.OpsRequest.Status.Phase).Should(Equal(appsv1alpha1.OpsSucceedPhase))
   285  
   286  		})
   287  
   288  		It("Test Reconfigure OpsRequest with autoReload", func() {
   289  			opsRes, _, _ := assureMockReconfigureData("autoReload")
   290  			reqCtx := intctrlutil.RequestCtx{
   291  				Ctx:      testCtx.Ctx,
   292  				Log:      log.FromContext(ctx).WithName("Reconfigure"),
   293  				Recorder: opsRes.Recorder,
   294  			}
   295  
   296  			By("mock reconfigure success")
   297  			ops := testapps.NewOpsRequestObj("reconfigure-ops-"+randomStr+"-reload", testCtx.DefaultNamespace,
   298  				clusterName, appsv1alpha1.ReconfiguringType)
   299  			ops.Spec.Reconfigure = &appsv1alpha1.Reconfigure{
   300  				Configurations: []appsv1alpha1.ConfigurationItem{{
   301  					Name: "mysql-test",
   302  					Keys: []appsv1alpha1.ParameterConfig{{
   303  						Key: "my.cnf",
   304  						Parameters: []appsv1alpha1.ParameterPair{
   305  							{
   306  								Key:   "binlog_stmt_cache_size",
   307  								Value: func() *string { v := "4096"; return &v }(),
   308  							}},
   309  					}},
   310  				}},
   311  				ComponentOps: appsv1alpha1.ComponentOps{ComponentName: consensusComp},
   312  			}
   313  
   314  			By("Init Reconfiguring opsrequest")
   315  			opsRes.OpsRequest = ops
   316  			Expect(testCtx.CheckedCreateObj(ctx, ops)).Should(Succeed())
   317  			initClusterForOps(opsRes)
   318  
   319  			opsManager := GetOpsManager()
   320  			// reAction := reconfigureAction{}
   321  			By("Reconfigure configure")
   322  			_, err := opsManager.Do(reqCtx, k8sClient, opsRes)
   323  			Expect(err).ShouldNot(HaveOccurred())
   324  			Eventually(testapps.GetOpsRequestPhase(&testCtx, client.ObjectKeyFromObject(opsRes.OpsRequest))).Should(Equal(appsv1alpha1.OpsCreatingPhase))
   325  			// do reconfigure
   326  			_, err = opsManager.Do(reqCtx, k8sClient, opsRes)
   327  			Expect(err).ShouldNot(HaveOccurred())
   328  			By("configuration Reconcile callback")
   329  
   330  			// Expect(reAction.Handle(eventContext, ops.Name, appsv1alpha1.OpsSucceedPhase, nil)).Should(Succeed())
   331  			By("Reconfigure configure")
   332  			_, _ = opsManager.Reconcile(reqCtx, k8sClient, opsRes)
   333  			// mock cluster.status.component.phase to Updating
   334  			mockClusterCompPhase := func(clusterObj *appsv1alpha1.Cluster, phase appsv1alpha1.ClusterComponentPhase) {
   335  				clusterObject := clusterObj.DeepCopy()
   336  				patch := client.MergeFrom(clusterObject.DeepCopy())
   337  				compStatus := clusterObject.Status.Components[consensusComp]
   338  				compStatus.Phase = phase
   339  				clusterObject.Status.Components[consensusComp] = compStatus
   340  				Expect(k8sClient.Status().Patch(ctx, clusterObject, patch)).Should(Succeed())
   341  			}
   342  			mockClusterCompPhase(opsRes.Cluster, appsv1alpha1.UpdatingClusterCompPhase)
   343  			Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(opsRes.Cluster), opsRes.Cluster)).Should(Succeed())
   344  
   345  			By("check cluster.status.components[*].phase == Reconfiguring")
   346  			Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(opsRes.OpsRequest), opsRes.OpsRequest)).Should(Succeed())
   347  			Expect(opsRes.Cluster.Status.Components[consensusComp].Phase).Should(Equal(appsv1alpha1.UpdatingClusterCompPhase)) // appsv1alpha1.ReconfiguringPhase
   348  			// TODO: add status condition expect
   349  			_, _ = opsManager.Reconcile(reqCtx, k8sClient, opsRes)
   350  			// mock cluster.status.component.phase to Running
   351  			mockClusterCompPhase(opsRes.Cluster, appsv1alpha1.RunningClusterCompPhase)
   352  
   353  			By("check cluster.status.components[*].phase == Running")
   354  			Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(opsRes.Cluster), opsRes.Cluster)).Should(Succeed())
   355  			Expect(opsRes.Cluster.Status.Components[consensusComp].Phase).Should(Equal(appsv1alpha1.RunningClusterCompPhase))
   356  		})
   357  
   358  	})
   359  })