github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/controllers/extensions/addon_controller_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 extensions
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"time"
    26  
    27  	. "github.com/onsi/ginkgo/v2"
    28  	. "github.com/onsi/gomega"
    29  
    30  	batchv1 "k8s.io/api/batch/v1"
    31  	corev1 "k8s.io/api/core/v1"
    32  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/apimachinery/pkg/types"
    35  	"k8s.io/client-go/kubernetes/scheme"
    36  	"k8s.io/client-go/tools/record"
    37  	ctrl "sigs.k8s.io/controller-runtime"
    38  	"sigs.k8s.io/controller-runtime/pkg/client"
    39  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    40  
    41  	extensionsv1alpha1 "github.com/1aal/kubeblocks/apis/extensions/v1alpha1"
    42  	"github.com/1aal/kubeblocks/pkg/constant"
    43  	"github.com/1aal/kubeblocks/pkg/generics"
    44  	"github.com/1aal/kubeblocks/pkg/testutil"
    45  	testapps "github.com/1aal/kubeblocks/pkg/testutil/apps"
    46  	viper "github.com/1aal/kubeblocks/pkg/viperx"
    47  )
    48  
    49  var _ = Describe("Addon controller", func() {
    50  	cleanEnv := func() {
    51  		// must wait till resources deleted and no longer existed before the testcases start,
    52  		// otherwise if later it needs to create some new resource objects with the same name,
    53  		// in race conditions, it will find the existence of old objects, resulting failure to
    54  		// create the new objects.
    55  		By("clean resources")
    56  		// non-namespaced
    57  		ml := client.HasLabels{testCtx.TestObjLabelKey}
    58  		testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.AddonSignature, true, ml)
    59  
    60  		inNS := client.InNamespace(viper.GetString(constant.CfgKeyCtrlrMgrNS))
    61  		testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.JobSignature, true, inNS,
    62  			client.HasLabels{
    63  				constant.AddonNameLabelKey,
    64  			})
    65  		testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.JobSignature, true, inNS,
    66  			client.HasLabels{
    67  				constant.AppManagedByLabelKey,
    68  			})
    69  
    70  		// delete rest mocked objects
    71  		testapps.ClearResources(&testCtx, generics.ConfigMapSignature, inNS, ml)
    72  		testapps.ClearResources(&testCtx, generics.SecretSignature, inNS, ml)
    73  
    74  		// By("deleting the Namespace to perform the tests")
    75  		// Eventually(func(g Gomega) {
    76  		// 	namespace := testCtx.GetNamespaceObj()
    77  		// 	err := testCtx.Cli.Delete(testCtx.Ctx, &namespace)
    78  		// 	g.Expect(client.IgnoreNotFound(err)).To(Not(HaveOccurred()))
    79  		// 	g.Expect(client.IgnoreNotFound(testCtx.Cli.Get(
    80  		// 		testCtx.Ctx, testCtx.GetNamespaceKey(), &namespace))).To(Not(HaveOccurred()))
    81  		// }).Should(Succeed())
    82  	}
    83  
    84  	BeforeEach(func() {
    85  		cleanEnv()
    86  	})
    87  
    88  	AfterEach(func() {
    89  		cleanEnv()
    90  	})
    91  
    92  	Context("Addon controller test", func() {
    93  		var addon *extensionsv1alpha1.Addon
    94  		var key types.NamespacedName
    95  		BeforeEach(func() {
    96  			cleanEnv()
    97  			const distro = "kubeblocks"
    98  			testutil.SetKubeServerVersionWithDistro("1", "24", "0", distro)
    99  			Expect(client.IgnoreAlreadyExists(testCtx.CreateNamespace())).To(Not(HaveOccurred()))
   100  		})
   101  
   102  		AfterEach(func() {
   103  			cleanEnv()
   104  			viper.Set(constant.CfgKeyCtrlrMgrTolerations, "")
   105  			viper.Set(constant.CfgKeyCtrlrMgrAffinity, "")
   106  			viper.Set(constant.CfgKeyCtrlrMgrNodeSelector, "")
   107  		})
   108  
   109  		doReconcile := func() (ctrl.Result, error) {
   110  			addonReconciler := &AddonReconciler{
   111  				Client: testCtx.Cli,
   112  				Scheme: testCtx.Cli.Scheme(),
   113  			}
   114  			req := reconcile.Request{
   115  				NamespacedName: key,
   116  			}
   117  			return addonReconciler.Reconcile(ctx, req)
   118  		}
   119  
   120  		doReconcileOnce := func(g Gomega) {
   121  			By("Reconciling once")
   122  			result, err := doReconcile()
   123  			Expect(err).To(Not(HaveOccurred()))
   124  			Expect(result.Requeue).Should(BeFalse())
   125  		}
   126  
   127  		getJob := func(g Gomega, jobKey client.ObjectKey) *batchv1.Job {
   128  			job := &batchv1.Job{}
   129  			g.Eventually(func(g Gomega) {
   130  				_, err := doReconcile()
   131  				g.Expect(err).To(Not(HaveOccurred()))
   132  				g.Expect(testCtx.Cli.Get(ctx, jobKey, job)).Should(Succeed())
   133  			}).Should(Succeed())
   134  			return job
   135  		}
   136  
   137  		fakeCompletedJob := func(g Gomega, jobKey client.ObjectKey) {
   138  			job := getJob(g, jobKey)
   139  			job.Status.Succeeded = 1
   140  			job.Status.Active = 0
   141  			job.Status.Failed = 0
   142  			g.Expect(testCtx.Cli.Status().Update(ctx, job)).Should(Succeed())
   143  		}
   144  
   145  		fakeFailedJob := func(g Gomega, jobKey client.ObjectKey) {
   146  			job := getJob(g, jobKey)
   147  			job.Status.Failed = 1
   148  			job.Status.Active = 0
   149  			job.Status.Succeeded = 0
   150  			g.Expect(testCtx.Cli.Status().Update(ctx, job)).Should(Succeed())
   151  		}
   152  
   153  		fakeActiveJob := func(g Gomega, jobKey client.ObjectKey) {
   154  			job := getJob(g, jobKey)
   155  			job.Status.Active = 1
   156  			job.Status.Succeeded = 0
   157  			job.Status.Failed = 0
   158  			g.Expect(testCtx.Cli.Status().Update(ctx, job)).Should(Succeed())
   159  		}
   160  
   161  		checkedJobDeletion := func(g Gomega, jobKey client.ObjectKey) {
   162  			job := &batchv1.Job{}
   163  			err := testCtx.Cli.Get(ctx, jobKey, job)
   164  			if err == nil {
   165  				g.Expect(job.DeletionTimestamp.IsZero()).Should(BeFalse())
   166  				return
   167  			}
   168  			g.Expect(apierrors.IsNotFound(err)).Should(BeTrue())
   169  		}
   170  
   171  		fakeInstallationCompletedJob := func(expectedObservedGeneration int) {
   172  			jobKey := client.ObjectKey{
   173  				Namespace: viper.GetString(constant.CfgKeyCtrlrMgrNS),
   174  				Name:      getInstallJobName(addon),
   175  			}
   176  			Eventually(func(g Gomega) {
   177  				fakeCompletedJob(g, jobKey)
   178  			}).Should(Succeed())
   179  			Eventually(func(g Gomega) {
   180  				_, err := doReconcile()
   181  				g.Expect(err).To(Not(HaveOccurred()))
   182  				addon = &extensionsv1alpha1.Addon{}
   183  				g.Expect(testCtx.Cli.Get(ctx, key, addon)).To(Not(HaveOccurred()))
   184  				g.Expect(addon.Status.Phase).Should(Equal(extensionsv1alpha1.AddonEnabled))
   185  				g.Expect(addon.Status.ObservedGeneration).Should(BeEquivalentTo(expectedObservedGeneration))
   186  				checkedJobDeletion(g, jobKey)
   187  			}).Should(Succeed())
   188  		}
   189  
   190  		fakeInstallationFailedJob := func(expectedObservedGeneration int) {
   191  			jobKey := client.ObjectKey{
   192  				Namespace: viper.GetString(constant.CfgKeyCtrlrMgrNS),
   193  				Name:      getInstallJobName(addon),
   194  			}
   195  			Eventually(func(g Gomega) {
   196  				fakeFailedJob(g, jobKey)
   197  			}).Should(Succeed())
   198  			Eventually(func(g Gomega) {
   199  				_, err := doReconcile()
   200  				g.Expect(err).To(Not(HaveOccurred()))
   201  				addon = &extensionsv1alpha1.Addon{}
   202  				g.Expect(testCtx.Cli.Get(ctx, key, addon)).To(Not(HaveOccurred()))
   203  				g.Expect(addon.Status.Phase).Should(Equal(extensionsv1alpha1.AddonFailed))
   204  				g.Expect(addon.Status.ObservedGeneration).Should(BeEquivalentTo(expectedObservedGeneration))
   205  			}).Should(Succeed())
   206  		}
   207  
   208  		fakeUninstallationFailedJob := func(expectedObservedGeneration int) {
   209  			jobKey := client.ObjectKey{
   210  				Namespace: viper.GetString(constant.CfgKeyCtrlrMgrNS),
   211  				Name:      getUninstallJobName(addon),
   212  			}
   213  			Eventually(func(g Gomega) {
   214  				fakeFailedJob(g, jobKey)
   215  			}).Should(Succeed())
   216  			Eventually(func(g Gomega) {
   217  				result, err := doReconcile()
   218  				g.Expect(err).To(Not(HaveOccurred()))
   219  				g.Expect(result.Requeue).To(BeTrue())
   220  			}).Should(Succeed())
   221  			Eventually(func(g Gomega) {
   222  				checkedJobDeletion(g, jobKey)
   223  			}).Should(Succeed())
   224  		}
   225  
   226  		createAddonSpecWithRequiredAttributes := func(modifiers func(newOjb *extensionsv1alpha1.Addon)) {
   227  			if modifiers != nil {
   228  				addon = testapps.CreateCustomizedObj(&testCtx, "addon/addon.yaml",
   229  					&extensionsv1alpha1.Addon{}, modifiers)
   230  			} else {
   231  				addon = testapps.CreateCustomizedObj(&testCtx, "addon/addon.yaml",
   232  					&extensionsv1alpha1.Addon{})
   233  			}
   234  			key = types.NamespacedName{
   235  				Name: addon.Name,
   236  			}
   237  			Expect(addon.Spec.DefaultInstallValues).ShouldNot(BeEmpty())
   238  		}
   239  
   240  		addonStatusPhaseCheck := func(genID int, expectPhase extensionsv1alpha1.AddonPhase, handler func()) {
   241  			Eventually(func(g Gomega) {
   242  				_, err := doReconcile()
   243  				Expect(err).To(Not(HaveOccurred()))
   244  				addon = &extensionsv1alpha1.Addon{}
   245  				g.Expect(testCtx.Cli.Get(ctx, key, addon)).To(Not(HaveOccurred()))
   246  				g.Expect(addon.Generation).Should(BeEquivalentTo(genID))
   247  				g.Expect(addon.Spec.InstallSpec).ShouldNot(BeNil())
   248  				g.Expect(addon.Status.ObservedGeneration).Should(BeEquivalentTo(genID))
   249  				g.Expect(addon.Status.Phase).Should(Equal(expectPhase))
   250  			}).Should(Succeed())
   251  
   252  			if handler == nil {
   253  				return
   254  			}
   255  
   256  			handler()
   257  			Eventually(func(g Gomega) {
   258  				_, err := doReconcile()
   259  				Expect(err).To(Not(HaveOccurred()))
   260  				addon = &extensionsv1alpha1.Addon{}
   261  				g.Expect(testCtx.Cli.Get(ctx, key, addon)).To(Not(HaveOccurred()))
   262  				g.Expect(addon.Generation).Should(BeEquivalentTo(genID))
   263  				g.Expect(addon.Status.ObservedGeneration).Should(BeEquivalentTo(genID))
   264  				g.Expect(addon.Status.Phase).Should(Equal(expectPhase))
   265  			}).Should(Succeed())
   266  		}
   267  
   268  		enablingPhaseCheck := func(genID int) {
   269  			addonStatusPhaseCheck(genID, extensionsv1alpha1.AddonEnabling, func() {
   270  				By("By fake active install job")
   271  				jobKey := client.ObjectKey{
   272  					Namespace: viper.GetString(constant.CfgKeyCtrlrMgrNS),
   273  					Name:      getInstallJobName(addon),
   274  				}
   275  				Eventually(func(g Gomega) {
   276  					fakeActiveJob(g, jobKey)
   277  				}).Should(Succeed())
   278  			})
   279  		}
   280  
   281  		disablingPhaseCheck := func(genID int) {
   282  			addonStatusPhaseCheck(genID, extensionsv1alpha1.AddonDisabling, nil)
   283  		}
   284  
   285  		checkAddonDeleted := func(g Gomega) {
   286  			addon = &extensionsv1alpha1.Addon{}
   287  			err := testCtx.Cli.Get(ctx, key, addon)
   288  			g.Expect(err).To(HaveOccurred())
   289  			g.Expect(apierrors.IsNotFound(err)).Should(BeTrue())
   290  		}
   291  
   292  		disableAddon := func(genID int) {
   293  			addon = &extensionsv1alpha1.Addon{}
   294  			Expect(testCtx.Cli.Get(ctx, key, addon)).To(Not(HaveOccurred()))
   295  			addon.Spec.InstallSpec.Enabled = false
   296  			Expect(testCtx.Cli.Update(ctx, addon)).Should(Succeed())
   297  			disablingPhaseCheck(genID)
   298  		}
   299  
   300  		fakeHelmRelease := func() {
   301  			// create fake helm release
   302  			helmRelease := &corev1.Secret{
   303  				ObjectMeta: metav1.ObjectMeta{
   304  					Name:      fmt.Sprintf("sh.helm.release.v1.%s.v1", addon.Name),
   305  					Namespace: viper.GetString(constant.CfgKeyCtrlrMgrNS),
   306  					Labels: map[string]string{
   307  						"owner": "helm",
   308  						"name":  getHelmReleaseName(addon),
   309  					},
   310  				},
   311  				Type: "helm.sh/release.v1",
   312  			}
   313  			Expect(testCtx.CreateObj(ctx, helmRelease)).Should(Succeed())
   314  		}
   315  
   316  		It("should successfully reconcile a custom resource for Addon with spec.type=Helm", func() {
   317  			By("By create an addon")
   318  			createAddonSpecWithRequiredAttributes(func(newOjb *extensionsv1alpha1.Addon) {
   319  				newOjb.Spec.Type = extensionsv1alpha1.HelmType
   320  				newOjb.Spec.Helm = &extensionsv1alpha1.HelmTypeInstallSpec{
   321  					InstallOptions: extensionsv1alpha1.HelmInstallOptions{
   322  						"--debug": "true",
   323  					},
   324  					ChartLocationURL: "file:///test-charts.tgz",
   325  					ChartsImage:      "kubeblocks-charts",
   326  				}
   327  			})
   328  
   329  			By("By checking status.observedGeneration and status.phase=disabled")
   330  			Eventually(func(g Gomega) {
   331  				doReconcileOnce(g)
   332  				addon = &extensionsv1alpha1.Addon{}
   333  				g.Expect(testCtx.Cli.Get(ctx, key, addon)).To(Not(HaveOccurred()))
   334  				g.Expect(addon.Status.ObservedGeneration).Should(BeEquivalentTo(1))
   335  				g.Expect(addon.Status.Phase).Should(Equal(extensionsv1alpha1.AddonDisabled))
   336  			}).Should(Succeed())
   337  
   338  			By("By enabling addon with default install")
   339  			defaultInstall := addon.Spec.DefaultInstallValues[0].AddonInstallSpec
   340  			addon.Spec.InstallSpec = defaultInstall.DeepCopy()
   341  			addon.Spec.InstallSpec.Enabled = true
   342  			Expect(testCtx.Cli.Update(ctx, addon)).Should(Succeed())
   343  			enablingPhaseCheck(2)
   344  
   345  			By("By enabled addon with fake completed installation job status")
   346  			fakeInstallationCompletedJob(2)
   347  
   348  			By("By checking init container")
   349  			jobKey := client.ObjectKey{
   350  				Namespace: viper.GetString(constant.CfgKeyCtrlrMgrNS),
   351  				Name:      getInstallJobName(addon),
   352  			}
   353  			Eventually(func(g Gomega) {
   354  				fakeActiveJob(g, jobKey)
   355  			}).Should(Succeed())
   356  			Eventually(func(g Gomega) {
   357  				job := getJob(g, jobKey)
   358  				g.Expect(job.Spec.Template.Spec.InitContainers).Should(HaveLen(1))
   359  			}).Should(Succeed())
   360  
   361  			By("By disabling enabled addon")
   362  			// create fake helm release
   363  			fakeHelmRelease()
   364  			disableAddon(3)
   365  
   366  			By("By disabled an enabled addon with fake completed uninstall job status")
   367  			uninstallJobKey := client.ObjectKey{
   368  				Namespace: viper.GetString(constant.CfgKeyCtrlrMgrNS),
   369  				Name:      getUninstallJobName(addon),
   370  			}
   371  			Eventually(func(g Gomega) {
   372  				fakeCompletedJob(g, uninstallJobKey)
   373  			}).Should(Succeed())
   374  
   375  			Eventually(func(g Gomega) {
   376  				_, err := doReconcile()
   377  				g.Expect(err).To(Not(HaveOccurred()))
   378  				addon = &extensionsv1alpha1.Addon{}
   379  				g.Expect(testCtx.Cli.Get(ctx, key, addon)).To(Not(HaveOccurred()))
   380  				g.Expect(addon.Status.ObservedGeneration).Should(BeEquivalentTo(3))
   381  				g.Expect(addon.Status.Phase).Should(Equal(extensionsv1alpha1.AddonDisabled))
   382  				checkedJobDeletion(g, uninstallJobKey)
   383  			}).Should(Succeed())
   384  
   385  			By("By delete addon with disabled status")
   386  			Expect(testCtx.Cli.Delete(ctx, addon)).To(Not(HaveOccurred()))
   387  			Eventually(func(g Gomega) {
   388  				_, err := doReconcile()
   389  				g.Expect(err).To(Not(HaveOccurred()))
   390  			}).Should(Succeed())
   391  		})
   392  
   393  		createAutoInstallAddon := func() {
   394  			By("By create an addon with auto-install")
   395  			createAddonSpecWithRequiredAttributes(func(newOjb *extensionsv1alpha1.Addon) {
   396  				newOjb.Spec.Installable.AutoInstall = true
   397  			})
   398  
   399  			By("By addon autoInstall auto added")
   400  			enablingPhaseCheck(2)
   401  		}
   402  
   403  		It("should successfully reconcile a custom resource for Addon with autoInstall=true", func() {
   404  			createAutoInstallAddon()
   405  
   406  			By("By enable addon with fake completed install job status")
   407  			fakeInstallationCompletedJob(2)
   408  
   409  			By("By delete addon with enabled status")
   410  			Expect(testCtx.Cli.Delete(ctx, addon)).To(Not(HaveOccurred()))
   411  			Eventually(func(g Gomega) {
   412  				_, err := doReconcile()
   413  				g.Expect(err).To(Not(HaveOccurred()))
   414  				checkAddonDeleted(g)
   415  			}).Should(Succeed())
   416  		})
   417  
   418  		It("should successfully reconcile a custom resource for Addon with autoInstall=true with failed uninstall job", func() {
   419  			createAutoInstallAddon()
   420  
   421  			By("By enable addon with fake completed install job status")
   422  			fakeInstallationCompletedJob(2)
   423  
   424  			By("By disabling enabled addon")
   425  			fakeHelmRelease()
   426  			disableAddon(3)
   427  
   428  			By("By failed an uninstallation job")
   429  			fakeUninstallationFailedJob(3)
   430  
   431  			By("By delete addon with disabling status")
   432  			Expect(testCtx.Cli.Delete(ctx, addon)).To(Not(HaveOccurred()))
   433  			Eventually(func(g Gomega) {
   434  				_, err := doReconcile()
   435  				g.Expect(err).To(Not(HaveOccurred()))
   436  				checkAddonDeleted(g)
   437  			}).Should(Succeed())
   438  		})
   439  
   440  		It("should successfully reconcile a custom resource for Addon deletion while enabling", func() {
   441  			By("By create an addon with auto-install")
   442  			createAddonSpecWithRequiredAttributes(func(newOjb *extensionsv1alpha1.Addon) {
   443  				newOjb.Spec.Installable.AutoInstall = true
   444  			})
   445  
   446  			By("By addon autoInstall auto added")
   447  			enablingPhaseCheck(2)
   448  
   449  			By("By delete addon with enabling status")
   450  			Expect(testCtx.Cli.Delete(ctx, addon)).To(Not(HaveOccurred()))
   451  			jobKey := client.ObjectKey{
   452  				Namespace: viper.GetString(constant.CfgKeyCtrlrMgrNS),
   453  				Name:      getUninstallJobName(addon),
   454  			}
   455  			Eventually(func(g Gomega) {
   456  				_, err := doReconcile()
   457  				g.Expect(err).To(Not(HaveOccurred()))
   458  				checkAddonDeleted(g)
   459  				checkedJobDeletion(g, jobKey)
   460  			}).Should(Succeed())
   461  		})
   462  
   463  		It("should successfully reconcile a custom resource for Addon deletion while disabling", func() {
   464  			createAutoInstallAddon()
   465  
   466  			By("By enable addon with fake completed install job status")
   467  			fakeInstallationCompletedJob(2)
   468  
   469  			By("By disabling addon")
   470  			disableAddon(3)
   471  
   472  			By("By delete addon with disabling status")
   473  			Expect(testCtx.Cli.Delete(ctx, addon)).To(Not(HaveOccurred()))
   474  			jobKey := client.ObjectKey{
   475  				Namespace: viper.GetString(constant.CfgKeyCtrlrMgrNS),
   476  				Name:      getUninstallJobName(addon),
   477  			}
   478  			Eventually(func(g Gomega) {
   479  				_, err := doReconcile()
   480  				g.Expect(err).To(Not(HaveOccurred()))
   481  				checkAddonDeleted(g)
   482  				checkedJobDeletion(g, jobKey)
   483  			}).Should(Succeed())
   484  		})
   485  
   486  		It("should successfully reconcile a custom resource for Addon with autoInstall=true with status.phase=Failed", func() {
   487  			createAutoInstallAddon()
   488  
   489  			By("By enabled addon with fake failed install job status")
   490  			fakeInstallationFailedJob(2)
   491  
   492  			By("By disabling addon with failed status")
   493  			disableAddon(3)
   494  
   495  			By("By delete addon with failed status")
   496  			Expect(testCtx.Cli.Delete(ctx, addon)).To(Not(HaveOccurred()))
   497  			jobKey := client.ObjectKey{
   498  				Namespace: viper.GetString(constant.CfgKeyCtrlrMgrNS),
   499  				Name:      getUninstallJobName(addon),
   500  			}
   501  			Eventually(func(g Gomega) {
   502  				_, err := doReconcile()
   503  				g.Expect(err).To(Not(HaveOccurred()))
   504  				checkAddonDeleted(g)
   505  				checkedJobDeletion(g, jobKey)
   506  			}).Should(Succeed())
   507  		})
   508  
   509  		It("should successfully reconcile a custom resource for Addon run job with controller manager schedule settings", func() {
   510  			viper.Set(constant.CfgKeyCtrlrMgrAffinity,
   511  				"{\"nodeAffinity\":{\"preferredDuringSchedulingIgnoredDuringExecution\":[{\"preference\":{\"matchExpressions\":[{\"key\":\"kb-controller\",\"operator\":\"In\",\"values\":[\"true\"]}]},\"weight\":100}]}}")
   512  			viper.Set(constant.CfgKeyCtrlrMgrTolerations,
   513  				"[{\"key\":\"key1\", \"operator\": \"Exists\", \"effect\": \"NoSchedule\"}]")
   514  			viper.Set(constant.CfgKeyCtrlrMgrNodeSelector, "{\"beta.kubernetes.io/arch\":\"amd64\"}")
   515  
   516  			By("By create an addon with auto-install")
   517  			createAddonSpecWithRequiredAttributes(func(newOjb *extensionsv1alpha1.Addon) {
   518  				newOjb.Spec.Installable.AutoInstall = true
   519  			})
   520  
   521  			By("By addon autoInstall auto added")
   522  			enablingPhaseCheck(2)
   523  
   524  			By("By checking status.observedGeneration and status.phase=disabled")
   525  			jobKey := client.ObjectKey{
   526  				Namespace: viper.GetString(constant.CfgKeyCtrlrMgrNS),
   527  				Name:      getInstallJobName(addon),
   528  			}
   529  			Eventually(func(g Gomega) {
   530  				_, err := doReconcile()
   531  				g.Expect(err).To(Not(HaveOccurred()))
   532  				job := &batchv1.Job{}
   533  				g.Eventually(testCtx.Cli.Get(ctx, jobKey, job)).Should(Succeed())
   534  				g.Expect(job.Spec.Template.Spec.Tolerations).ShouldNot(BeEmpty())
   535  				g.Expect(job.Spec.Template.Spec.NodeSelector).ShouldNot(BeEmpty())
   536  				g.Expect(job.Spec.Template.Spec.Affinity).ShouldNot(BeNil())
   537  				g.Expect(job.Spec.Template.Spec.Affinity.NodeAffinity).ShouldNot(BeNil())
   538  			}).Should(Succeed())
   539  		})
   540  
   541  		It("should successfully reconcile a custom resource for Addon with no matching installable selector", func() {
   542  			By("By create an addon with no matching installable selector")
   543  			createAddonSpecWithRequiredAttributes(func(newOjb *extensionsv1alpha1.Addon) {
   544  				newOjb.Spec.Installable.Selectors[0].Values = []string{"some-others"}
   545  			})
   546  
   547  			By("By checking status.observedGeneration and status.phase=disabled")
   548  			Eventually(func(g Gomega) {
   549  				doReconcileOnce(g)
   550  				addon = &extensionsv1alpha1.Addon{}
   551  				g.Expect(testCtx.Cli.Get(ctx, key, addon)).To(Not(HaveOccurred()))
   552  				g.Expect(addon.Status.Phase).Should(Equal(extensionsv1alpha1.AddonDisabled))
   553  				g.Expect(addon.Status.Conditions).ShouldNot(BeEmpty())
   554  				g.Expect(addon.Status.ObservedGeneration).Should(BeEquivalentTo(1))
   555  			}).Should(Succeed())
   556  
   557  			By("By addon with failed installable check")
   558  			// "extensions.kubeblocks.io/skip-installable-check"
   559  		})
   560  
   561  		It("should successfully reconcile a custom resource for Addon with CM and secret ref values", func() {
   562  			By("By create an addon with spec.helm.installValues.configMapRefs set")
   563  			cm := testapps.CreateCustomizedObj(&testCtx, "addon/cm-values.yaml",
   564  				&corev1.ConfigMap{}, func(newCM *corev1.ConfigMap) {
   565  					newCM.Namespace = viper.GetString(constant.CfgKeyCtrlrMgrNS)
   566  				})
   567  			secret := testapps.CreateCustomizedObj(&testCtx, "addon/secret-values.yaml",
   568  				&corev1.Secret{}, func(newSecret *corev1.Secret) {
   569  					newSecret.Namespace = viper.GetString(constant.CfgKeyCtrlrMgrNS)
   570  				})
   571  
   572  			By("By addon enabled via auto-install")
   573  			createAddonSpecWithRequiredAttributes(func(newOjb *extensionsv1alpha1.Addon) {
   574  				newOjb.Spec.Installable.AutoInstall = true
   575  				for k := range cm.Data {
   576  					newOjb.Spec.Helm.InstallValues.ConfigMapRefs = append(newOjb.Spec.Helm.InstallValues.ConfigMapRefs,
   577  						extensionsv1alpha1.DataObjectKeySelector{
   578  							Name: cm.Name,
   579  							Key:  k,
   580  						})
   581  				}
   582  				for k := range secret.Data {
   583  					newOjb.Spec.Helm.InstallValues.SecretRefs = append(newOjb.Spec.Helm.InstallValues.SecretRefs,
   584  						extensionsv1alpha1.DataObjectKeySelector{
   585  							Name: secret.Name,
   586  							Key:  k,
   587  						})
   588  				}
   589  			})
   590  			enablingPhaseCheck(2)
   591  
   592  			By("By enabled addon with fake completed install job status")
   593  			fakeInstallationCompletedJob(2)
   594  		})
   595  
   596  		It("should failed reconcile a custom resource for Addon with missing CM ref values", func() {
   597  			By("By create an addon with spec.helm.installValues.configMapRefs set")
   598  			cm := testapps.CreateCustomizedObj(&testCtx, "addon/cm-values.yaml",
   599  				&corev1.ConfigMap{}, func(newCM *corev1.ConfigMap) {
   600  					newCM.Namespace = viper.GetString(constant.CfgKeyCtrlrMgrNS)
   601  				})
   602  
   603  			By("By addon enabled via auto-install")
   604  			createAddonSpecWithRequiredAttributes(func(newOjb *extensionsv1alpha1.Addon) {
   605  				newOjb.Spec.Installable.AutoInstall = true
   606  				newOjb.Spec.Helm.InstallValues.ConfigMapRefs = append(newOjb.Spec.Helm.InstallValues.ConfigMapRefs,
   607  					extensionsv1alpha1.DataObjectKeySelector{
   608  						Name: cm.Name,
   609  						Key:  "unknown",
   610  					})
   611  			})
   612  			addonStatusPhaseCheck(2, extensionsv1alpha1.AddonFailed, nil)
   613  		})
   614  
   615  		It("should failed reconcile a custom resource for Addon with missing secret ref values", func() {
   616  			By("By create an addon with spec.helm.installValues.configMapRefs set")
   617  			secret := testapps.CreateCustomizedObj(&testCtx, "addon/secret-values.yaml",
   618  				&corev1.Secret{}, func(newSecret *corev1.Secret) {
   619  					newSecret.Namespace = viper.GetString(constant.CfgKeyCtrlrMgrNS)
   620  				})
   621  			By("By addon enabled via auto-install")
   622  			createAddonSpecWithRequiredAttributes(func(newOjb *extensionsv1alpha1.Addon) {
   623  				newOjb.Spec.Installable.AutoInstall = true
   624  				newOjb.Spec.Helm.InstallValues.SecretRefs = append(newOjb.Spec.Helm.InstallValues.SecretRefs,
   625  					extensionsv1alpha1.DataObjectKeySelector{
   626  						Name: secret.Name,
   627  						Key:  "unknown",
   628  					})
   629  			})
   630  			addonStatusPhaseCheck(2, extensionsv1alpha1.AddonFailed, nil)
   631  		})
   632  	})
   633  
   634  	Context("Addon controller SetupWithManager", func() {
   635  		It("Do controller SetupWithManager init. flow", func() {
   636  			By("check SetupWithManager")
   637  			k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
   638  				Scheme:             scheme.Scheme,
   639  				MetricsBindAddress: "0",
   640  			})
   641  			Expect(err).ToNot(HaveOccurred())
   642  			reconciler := &AddonReconciler{
   643  				Client: k8sManager.GetClient(),
   644  				Scheme: k8sManager.GetScheme(),
   645  			}
   646  			err = reconciler.SetupWithManager(k8sManager)
   647  			Expect(err).ToNot(HaveOccurred())
   648  
   649  			testEventRecorder := func(recorder record.EventRecorder) {
   650  				reconciler.Recorder = recorder
   651  				addon := &extensionsv1alpha1.Addon{}
   652  				reconciler.Event(addon, corev1.EventTypeNormal, "reason", "message")
   653  				reconciler.Eventf(addon, corev1.EventTypeNormal, "reason", "%s", "message")
   654  				reconciler.AnnotatedEventf(addon, map[string]string{
   655  					"key": "value",
   656  				}, corev1.EventTypeNormal, "reason", "message")
   657  			}
   658  
   659  			By("test nil event recorder")
   660  			testEventRecorder(nil)
   661  
   662  			By("test event recorder")
   663  			testEventRecorder(k8sManager.GetEventRecorderFor("addon-controller"))
   664  
   665  			By("start manager")
   666  			go func() {
   667  				defer GinkgoRecover()
   668  				timeoutCtx, cancelFunc := context.WithTimeout(ctx, time.Second)
   669  				Expect(k8sManager.Start(timeoutCtx)).ToNot(HaveOccurred(), "failed to run manager")
   670  				if cancelFunc != nil {
   671  					cancelFunc()
   672  				}
   673  			}()
   674  
   675  			createJob := func(keys ...string) *batchv1.Job {
   676  				job := &batchv1.Job{
   677  					ObjectMeta: metav1.ObjectMeta{
   678  						GenerateName: "addon-test-job",
   679  						Namespace:    testCtx.DefaultNamespace,
   680  						Labels:       map[string]string{},
   681  					},
   682  					Spec: batchv1.JobSpec{
   683  						Template: corev1.PodTemplateSpec{
   684  							Spec: corev1.PodSpec{
   685  								RestartPolicy: corev1.RestartPolicyNever,
   686  								Containers: []corev1.Container{
   687  									{
   688  										Name:  "kubeblocks",
   689  										Image: "busybox",
   690  									},
   691  								},
   692  							},
   693  						},
   694  					},
   695  				}
   696  				for _, key := range keys {
   697  					job.ObjectMeta.Labels[key] = constant.AppName
   698  				}
   699  				return job
   700  			}
   701  
   702  			By("create watch obj type without required labels")
   703  			job := createJob()
   704  			Expect(k8sClient.Create(ctx, job)).ToNot(HaveOccurred())
   705  
   706  			By("create watch obj with label=" + constant.AddonNameLabelKey)
   707  			job = createJob(constant.AddonNameLabelKey)
   708  			Expect(k8sClient.Create(ctx, job)).ToNot(HaveOccurred())
   709  
   710  			By("create watch obj with label=" + constant.AppManagedByLabelKey)
   711  			job = createJob(constant.AppManagedByLabelKey)
   712  			Expect(k8sClient.Create(ctx, job)).ToNot(HaveOccurred())
   713  
   714  			By("create watch obj with required labels")
   715  			job = createJob(constant.AddonNameLabelKey, constant.AppManagedByLabelKey)
   716  			Expect(k8sClient.Create(ctx, job)).ToNot(HaveOccurred())
   717  		})
   718  	})
   719  })