From f97edc8107f029695af850fde8f3500f0deafad7 Mon Sep 17 00:00:00 2001 From: d976045024 <976045024@qq.com> Date: Sun, 28 Apr 2024 09:05:10 +0800 Subject: [PATCH 1/6] fix: install the release of dependencies when enable an addon (#5780) --- apis/extensions/v1alpha1/addon_types.go | 9 + .../extensions/addon_controller_stages.go | 204 +++++++++++++++++- .../extensions/addon_controller_test.go | 162 ++++++++++++++ 3 files changed, 370 insertions(+), 5 deletions(-) diff --git a/apis/extensions/v1alpha1/addon_types.go b/apis/extensions/v1alpha1/addon_types.go index 61fb2ef6242..bd205bbd0d9 100644 --- a/apis/extensions/v1alpha1/addon_types.go +++ b/apis/extensions/v1alpha1/addon_types.go @@ -80,6 +80,10 @@ type AddonSpec struct { // // +optional CliPlugins []CliPlugin `json:"cliPlugins,omitempty"` + + // Specifies the dependencies of this addon + // +optional + Dependencies []DependencySpec `json:"dependencies,omitempty"` } // AddonStatus defines the observed state of an add-on. @@ -472,6 +476,11 @@ type ResourceRequirements struct { Requests corev1.ResourceList `json:"requests,omitempty"` } +type DependencySpec struct { + Name string `json:"name"` + Version string `json:"version,omitempty"` +} + // +genclient // +genclient:nonNamespaced // +k8s:openapi-gen=true diff --git a/controllers/extensions/addon_controller_stages.go b/controllers/extensions/addon_controller_stages.go index 9a496dc8ac6..b1560cbe98b 100644 --- a/controllers/extensions/addon_controller_stages.go +++ b/controllers/extensions/addon_controller_stages.go @@ -26,6 +26,11 @@ import ( "strings" "time" + "github.com/Masterminds/semver/v3" + extensionsv1alpha1 "github.com/apecloud/kubeblocks/apis/extensions/v1alpha1" + "github.com/apecloud/kubeblocks/pkg/constant" + intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" + viper "github.com/apecloud/kubeblocks/pkg/viperx" ctrlerihandler "github.com/authzed/controller-idioms/handler" "golang.org/x/exp/slices" batchv1 "k8s.io/api/batch/v1" @@ -33,14 +38,10 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - - extensionsv1alpha1 "github.com/apecloud/kubeblocks/apis/extensions/v1alpha1" - "github.com/apecloud/kubeblocks/pkg/constant" - intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" - viper "github.com/apecloud/kubeblocks/pkg/viperx" ) type stageCtx struct { @@ -262,6 +263,23 @@ func (r *deletionStage) Handle(ctx context.Context) { func (r *installableCheckStage) Handle(ctx context.Context) { r.process(func(addon *extensionsv1alpha1.Addon) { r.reqCtx.Log.V(1).Info("installableCheckStage", "phase", addon.Status.Phase) + + // check if the installation/enable of current addon has a circular dependency + if addon.Status.Phase != extensionsv1alpha1.AddonEnabling && len(addon.Spec.Dependencies) != 0 { + if _, _, err := checkAddonDependency(ctx, &r.stageCtx, addon); err != nil { + r.reconciler.Event(addon, corev1.EventTypeWarning, InstallableRequirementUnmatched, err.Error()) + r.reqCtx.Log.V(1).Info("") + patch := client.MergeFrom(addon.DeepCopy()) + addon.Status.Phase = extensionsv1alpha1.AddonFailed + if err := r.reconciler.Status().Patch(ctx, addon, patch); err != nil { + r.setRequeueWithErr(err, "") + } + r.setReconciled() + return + } + } + //fmt.Printf("addon %s, check = true\n", addon.Name) + if addon.Spec.Installable == nil { return } @@ -477,6 +495,43 @@ func (r *helmTypeInstallStage) Handle(ctx context.Context) { r.reqCtx.Log.V(1).Info("helmTypeInstallStage", "phase", addon.Status.Phase) mgrNS := viper.GetString(constant.CfgKeyCtrlrMgrNS) + if _, sequenceDependencies, err := checkAddonDependency(ctx, &r.stageCtx, addon); err != nil { + r.setRequeueWithErr(err, "") + return + } else { + allDependenciesEnabled := true + for _, dependencyName := range sequenceDependencies { + if dependencyName == addon.Name { + continue + } + tmpKey := types.NamespacedName{Namespace: r.reqCtx.Req.Namespace, Name: dependencyName} + dependencyAddon := &extensionsv1alpha1.Addon{} + if err := r.reconciler.Get(ctx, tmpKey, dependencyAddon); err != nil { + r.setRequeueWithErr(err, "") + return + } + if dependencyAddon.Status.Phase != extensionsv1alpha1.AddonEnabled { + allDependenciesEnabled = false + // enable the dependency addon + if dependencyAddon.Spec.InstallSpec == nil { + enabledAddonWithDefaultValues(ctx, &r.stageCtx, dependencyAddon, AddonAutoInstall, "") + } else if dependencyAddon.Spec.InstallSpec.Enabled != true { + dependencyAddon.Spec.InstallSpec.Enabled = true + if err := r.reconciler.Client.Update(ctx, dependencyAddon); err != nil { + r.setRequeueWithErr(err, "") + return + } + } + } + fmt.Printf("addon %v has been activated\n", dependencyAddon.Name) + } + + if !allDependenciesEnabled { + r.setReconciled() + return + } + } + key := client.ObjectKey{ Namespace: mgrNS, Name: getInstallJobName(addon), @@ -820,6 +875,145 @@ func (r *terminalStateStage) Handle(ctx context.Context) { r.next.Handle(ctx) } +func checkVersionMatched(requiredVersion, currentVersion string) (bool, error) { + if len(currentVersion) == 0 { + fmt.Println("not specify the version") + return false, nil + } + if len(requiredVersion) == 0 { + return true, nil + } + if strings.Contains(currentVersion, "-") { + addPreReleaseInfo := func(constraint string) string { + constraint = strings.Trim(constraint, " ") + split := strings.Split(constraint, "-") + if len(split) == 1 && (strings.HasPrefix(constraint, ">") || strings.Contains(constraint, "<")) { + constraint += "-0" + } + return constraint + } + rules := strings.Split(requiredVersion, ",") + for i := range rules { + rules[i] = addPreReleaseInfo(rules[i]) + } + requiredVersion = strings.Join(rules, ",") + } + constraint, err := semver.NewConstraint(requiredVersion) + if err != nil { + return false, err + } + v, err := semver.NewVersion(currentVersion) + if err != nil { + return false, err + } + validate, _ := constraint.Validate(v) + return validate, nil +} + +// check if all the dependency are installed and not fail +// check if circular dependency is existing +// give the top sort of all the dependencies, return it +func checkAddonDependency(ctx context.Context, stageCtx *stageCtx, addon *extensionsv1alpha1.Addon) (bool, []string, error) { + AddonIdToName := map[int]string{} + AddonNameToId := map[string]int{} + visited := map[string]bool{} + addonList := &extensionsv1alpha1.AddonList{} + if err := stageCtx.reconciler.List(ctx, addonList, client.InNamespace(addon.Namespace)); err != nil { + return false, nil, err + } + + // construct an empty graph + addonCount := len(addonList.Items) + graph := make([][]int, addonCount) + for i := range graph { + graph[i] = make([]int, addonCount) + } + indegree := make([]int, addonCount) + + // construct the map between name and id + for i, item := range addonList.Items { + AddonNameToId[item.Name] = i + AddonIdToName[i] = item.Name + } + + // construct the graph which represents the dependency relationship among addons + var constructGraph func(addon *extensionsv1alpha1.Addon) error + constructGraph = func(addon *extensionsv1alpha1.Addon) error { + visited[addon.Name] = true + currentID := AddonNameToId[addon.Name] + for _, dependency := range addon.Spec.Dependencies { + var dependencyID int + var exist bool + if dependencyID, exist = AddonNameToId[dependency.Name]; !exist { + return fmt.Errorf("dependency %s not exist", dependency.Name) + } + + graph[dependencyID][currentID]++ + indegree[currentID]++ + if !visited[dependency.Name] { + dependencyAddon := &extensionsv1alpha1.Addon{} + if err := stageCtx.reconciler.Get(ctx, types.NamespacedName{Namespace: stageCtx.reqCtx.Req.Namespace, Name: dependency.Name}, dependencyAddon); err != nil { + return err + } + if dependencyAddon.Status.Phase == extensionsv1alpha1.AddonFailed { + return fmt.Errorf("dependency %s failed", dependency.Name) + } + if versionMatched, err := checkVersionMatched(dependency.Version, dependencyAddon.Spec.Version); err != nil { + return err + } else if !versionMatched { + return fmt.Errorf("version %s not matched", dependencyAddon.Spec.Version) + } + if err := constructGraph(dependencyAddon); err != nil { + return err + } + } + } + return nil + } + + if err := constructGraph(addon); err != nil { + return false, nil, err + } + + // TopSort + queue := make([]int, 0) + result := make([]int, 0) + + for i, degree := range indegree { + if degree == 0 { + queue = append(queue, i) + } + } + + for len(queue) > 0 { + top := queue[0] + result = append(result, top) + queue = queue[1:] + for i := 0; i < addonCount; i++ { + if graph[top][i] != 0 { + indegree[i]-- + if indegree[i] == 0 { + queue = append(queue, i) + } + } + } + } + + // if circular dependency is existing + if len(result) != addonCount { + return false, nil, fmt.Errorf("there is a circular dependency cycle") + } + + sequenceDependencyName := make([]string, 0) + for _, id := range result { + if visited[AddonIdToName[id]] { + sequenceDependencyName = append(sequenceDependencyName, AddonIdToName[id]) + } + } + + return true, sequenceDependencyName, nil +} + // attachVolumeMount attaches a volumes to pod and added container.VolumeMounts to a ConfigMap // or Secret referenced key as file, and add --values={volumeMountPath}/{selector.Key} to // helm install/upgrade args diff --git a/controllers/extensions/addon_controller_test.go b/controllers/extensions/addon_controller_test.go index f3ae5ad598b..75d58ddc6cd 100644 --- a/controllers/extensions/addon_controller_test.go +++ b/controllers/extensions/addon_controller_test.go @@ -118,6 +118,17 @@ var _ = Describe("Addon controller", func() { return addonReconciler.Reconcile(ctx, req) } + //doReconcileWithKey := func(specifiedKey types.NamespacedName) (ctrl.Result, error) { + // addonReconciler := &AddonReconciler{ + // Client: testCtx.Cli, + // Scheme: testCtx.Cli.Scheme(), + // } + // req := reconcile.Request{ + // NamespacedName: specifiedKey, + // } + // return addonReconciler.Reconcile(ctx, req) + //} + doReconcileOnce := func(g Gomega) { By("Reconciling once") result, err := doReconcile() @@ -401,6 +412,157 @@ var _ = Describe("Addon controller", func() { enablingPhaseCheck(2) } + It("should should successfully enable an addon with right dependencies", func() { + By("create an addon with dependencies") + dependencies := [][]extensionsv1alpha1.DependencySpec{ + { + {Name: "b", Version: "1.0.0"}, + {Name: "c", Version: "1.0.0"}, + }, + { + {Name: "d", Version: "1.0.0"}, + {Name: "e", Version: "1.0.0"}, + {Name: "f", Version: "1.0.0"}, + }, + { + {Name: "h", Version: "1.0.0"}, + }, + {}, + { + {Name: "g", Version: "1.0.0"}, + {Name: "d", Version: "1.0.0"}, + }, + { + {Name: "c", Version: "1.0.0"}, + }, + {}, + {}, + } + names := []string{"a", "b", "c", "d", "e", "f", "g", "h"} + for i := range names { + createAddonSpecWithRequiredAttributes(func(newOjb *extensionsv1alpha1.Addon) { + newOjb.Name = names[i] + newOjb.Spec.Installable.AutoInstall = true + newOjb.Spec.Version = "1.0.0" + if i < len(dependencies) { + newOjb.Spec.Dependencies = dependencies[i] + } + }) + Expect(key.Name).Should(BeEquivalentTo(names[i])) + } + key.Name = "a" + //enablingPhaseCheck(2) + Eventually(func(g Gomega) { + _, err := doReconcile() + g.Expect(err).To(Not(HaveOccurred())) + addon := &extensionsv1alpha1.Addon{} + g.Expect(testCtx.Cli.Get(ctx, key, addon)).To(Not(HaveOccurred())) + g.Expect(addon.Status.Phase).Should(Equal(extensionsv1alpha1.AddonEnabling)) + }).Should(Succeed()) + + sequence := []string{"d", "g", "h", "e", "c", "f", "b"} + Eventually(func(g Gomega) { + _, err := doReconcile() + g.Expect(err).To(Not(HaveOccurred())) + for _, seq := range sequence { + addon := &extensionsv1alpha1.Addon{} + tmpKey := key + tmpKey.Name = seq + g.Expect(testCtx.Cli.Get(ctx, tmpKey, addon)).To(Not(HaveOccurred())) + g.Expect(addon.Spec.InstallSpec).ShouldNot(BeNil()) + g.Expect(addon.Spec.InstallSpec.Enabled).Should(Equal(true)) + } + }).Should(Succeed()) + + for _, seq := range sequence { + key.Name = seq + Eventually(func(g Gomega) { + _, err := doReconcile() + g.Expect(err).To(Not(HaveOccurred())) + addon := &extensionsv1alpha1.Addon{} + g.Expect(testCtx.Cli.Get(ctx, key, addon)).To(Not(HaveOccurred())) + g.Expect(addon.Status.Phase).Should(Equal(extensionsv1alpha1.AddonEnabling)) + }).Should(Succeed()) + jobKey := client.ObjectKey{ + Namespace: viper.GetString(constant.CfgKeyCtrlrMgrNS), + Name: fmt.Sprintf("install-%s-addon", seq), + } + Eventually(func(g Gomega) { + fakeCompletedJob(g, jobKey) + }).Should(Succeed()) + Eventually(func(g Gomega) { + _, err := doReconcile() + g.Expect(err).To(Not(HaveOccurred())) + addon = &extensionsv1alpha1.Addon{} + g.Expect(testCtx.Cli.Get(ctx, key, addon)).To(Not(HaveOccurred())) + g.Expect(addon.Status.Phase).Should(Equal(extensionsv1alpha1.AddonEnabled)) + checkedJobDeletion(g, jobKey) + }).Should(Succeed()) + } + key.Name = "a" + jobKey := client.ObjectKey{ + Namespace: viper.GetString(constant.CfgKeyCtrlrMgrNS), + Name: fmt.Sprintf("install-%s-addon", "a"), + } + Eventually(func(g Gomega) { + fakeCompletedJob(g, jobKey) + }).Should(Succeed()) + Eventually(func(g Gomega) { + _, err := doReconcile() + g.Expect(err).To(Not(HaveOccurred())) + addon := &extensionsv1alpha1.Addon{} + g.Expect(testCtx.Cli.Get(ctx, key, addon)).To(Not(HaveOccurred())) + g.Expect(addon.Status.Phase).Should(Equal(extensionsv1alpha1.AddonEnabled)) + checkedJobDeletion(g, jobKey) + }).Should(Succeed()) + }) + + It("should fail to enable or install an addon with version of dependencies not matched", func() { + By("create an addon with dependencies") + dependencies := [][]extensionsv1alpha1.DependencySpec{ + { + {Name: "b", Version: "1.0.0"}, + {Name: "c", Version: "1.0.0"}, + }, + { + {Name: "d", Version: "1.0.0"}, + {Name: "e", Version: "1.0.0"}, + {Name: "f", Version: "1.0.0"}, + }, + { + {Name: "h", Version: "1.0.0"}, + }, + {}, + { + {Name: "g", Version: "1.0.0"}, + {Name: "d", Version: "1.0.0"}, + }, + { + {Name: "c", Version: "1.0.0"}, + }, + {}, + {}, + } + names := []string{"a", "b", "c", "d", "e", "f", "g", "h"} + for i := range names { + createAddonSpecWithRequiredAttributes(func(newOjb *extensionsv1alpha1.Addon) { + newOjb.Name = names[i] + newOjb.Spec.Installable.AutoInstall = true + if i < len(dependencies) { + newOjb.Spec.Dependencies = dependencies[i] + } + }) + Expect(key.Name).Should(BeEquivalentTo(names[i])) + } + key.Name = "a" + Eventually(func(g Gomega) { + doReconcile() + addon := &extensionsv1alpha1.Addon{} + g.Expect(testCtx.Cli.Get(ctx, key, addon)).To(Not(HaveOccurred())) + g.Expect(addon.Status.Phase).Should(Equal(extensionsv1alpha1.AddonFailed)) + }).Should(Succeed()) + }) + It("should successfully reconcile a custom resource for Addon with autoInstall=true", func() { createAutoInstallAddon() From ca6703548ce5829e3fefd095385e526288c050ba Mon Sep 17 00:00:00 2001 From: d976045024 <976045024@qq.com> Date: Sun, 28 Apr 2024 09:33:05 +0800 Subject: [PATCH 2/6] fix: install the release of dependencies when enable an addon (#5780) --- .../extensions/addon_controller_stages.go | 290 +++++++++--------- .../extensions/addon_controller_test.go | 15 +- 2 files changed, 147 insertions(+), 158 deletions(-) diff --git a/controllers/extensions/addon_controller_stages.go b/controllers/extensions/addon_controller_stages.go index b1560cbe98b..8f0fc79717c 100644 --- a/controllers/extensions/addon_controller_stages.go +++ b/controllers/extensions/addon_controller_stages.go @@ -27,10 +27,6 @@ import ( "time" "github.com/Masterminds/semver/v3" - extensionsv1alpha1 "github.com/apecloud/kubeblocks/apis/extensions/v1alpha1" - "github.com/apecloud/kubeblocks/pkg/constant" - intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" - viper "github.com/apecloud/kubeblocks/pkg/viperx" ctrlerihandler "github.com/authzed/controller-idioms/handler" "golang.org/x/exp/slices" batchv1 "k8s.io/api/batch/v1" @@ -42,6 +38,11 @@ import ( corev1client "k8s.io/client-go/kubernetes/typed/core/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + + extensionsv1alpha1 "github.com/apecloud/kubeblocks/apis/extensions/v1alpha1" + "github.com/apecloud/kubeblocks/pkg/constant" + intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" + viper "github.com/apecloud/kubeblocks/pkg/viperx" ) type stageCtx struct { @@ -278,7 +279,6 @@ func (r *installableCheckStage) Handle(ctx context.Context) { return } } - //fmt.Printf("addon %s, check = true\n", addon.Name) if addon.Spec.Installable == nil { return @@ -515,7 +515,7 @@ func (r *helmTypeInstallStage) Handle(ctx context.Context) { // enable the dependency addon if dependencyAddon.Spec.InstallSpec == nil { enabledAddonWithDefaultValues(ctx, &r.stageCtx, dependencyAddon, AddonAutoInstall, "") - } else if dependencyAddon.Spec.InstallSpec.Enabled != true { + } else if !dependencyAddon.Spec.InstallSpec.Enabled { dependencyAddon.Spec.InstallSpec.Enabled = true if err := r.reconciler.Client.Update(ctx, dependencyAddon); err != nil { r.setRequeueWithErr(err, "") @@ -875,145 +875,6 @@ func (r *terminalStateStage) Handle(ctx context.Context) { r.next.Handle(ctx) } -func checkVersionMatched(requiredVersion, currentVersion string) (bool, error) { - if len(currentVersion) == 0 { - fmt.Println("not specify the version") - return false, nil - } - if len(requiredVersion) == 0 { - return true, nil - } - if strings.Contains(currentVersion, "-") { - addPreReleaseInfo := func(constraint string) string { - constraint = strings.Trim(constraint, " ") - split := strings.Split(constraint, "-") - if len(split) == 1 && (strings.HasPrefix(constraint, ">") || strings.Contains(constraint, "<")) { - constraint += "-0" - } - return constraint - } - rules := strings.Split(requiredVersion, ",") - for i := range rules { - rules[i] = addPreReleaseInfo(rules[i]) - } - requiredVersion = strings.Join(rules, ",") - } - constraint, err := semver.NewConstraint(requiredVersion) - if err != nil { - return false, err - } - v, err := semver.NewVersion(currentVersion) - if err != nil { - return false, err - } - validate, _ := constraint.Validate(v) - return validate, nil -} - -// check if all the dependency are installed and not fail -// check if circular dependency is existing -// give the top sort of all the dependencies, return it -func checkAddonDependency(ctx context.Context, stageCtx *stageCtx, addon *extensionsv1alpha1.Addon) (bool, []string, error) { - AddonIdToName := map[int]string{} - AddonNameToId := map[string]int{} - visited := map[string]bool{} - addonList := &extensionsv1alpha1.AddonList{} - if err := stageCtx.reconciler.List(ctx, addonList, client.InNamespace(addon.Namespace)); err != nil { - return false, nil, err - } - - // construct an empty graph - addonCount := len(addonList.Items) - graph := make([][]int, addonCount) - for i := range graph { - graph[i] = make([]int, addonCount) - } - indegree := make([]int, addonCount) - - // construct the map between name and id - for i, item := range addonList.Items { - AddonNameToId[item.Name] = i - AddonIdToName[i] = item.Name - } - - // construct the graph which represents the dependency relationship among addons - var constructGraph func(addon *extensionsv1alpha1.Addon) error - constructGraph = func(addon *extensionsv1alpha1.Addon) error { - visited[addon.Name] = true - currentID := AddonNameToId[addon.Name] - for _, dependency := range addon.Spec.Dependencies { - var dependencyID int - var exist bool - if dependencyID, exist = AddonNameToId[dependency.Name]; !exist { - return fmt.Errorf("dependency %s not exist", dependency.Name) - } - - graph[dependencyID][currentID]++ - indegree[currentID]++ - if !visited[dependency.Name] { - dependencyAddon := &extensionsv1alpha1.Addon{} - if err := stageCtx.reconciler.Get(ctx, types.NamespacedName{Namespace: stageCtx.reqCtx.Req.Namespace, Name: dependency.Name}, dependencyAddon); err != nil { - return err - } - if dependencyAddon.Status.Phase == extensionsv1alpha1.AddonFailed { - return fmt.Errorf("dependency %s failed", dependency.Name) - } - if versionMatched, err := checkVersionMatched(dependency.Version, dependencyAddon.Spec.Version); err != nil { - return err - } else if !versionMatched { - return fmt.Errorf("version %s not matched", dependencyAddon.Spec.Version) - } - if err := constructGraph(dependencyAddon); err != nil { - return err - } - } - } - return nil - } - - if err := constructGraph(addon); err != nil { - return false, nil, err - } - - // TopSort - queue := make([]int, 0) - result := make([]int, 0) - - for i, degree := range indegree { - if degree == 0 { - queue = append(queue, i) - } - } - - for len(queue) > 0 { - top := queue[0] - result = append(result, top) - queue = queue[1:] - for i := 0; i < addonCount; i++ { - if graph[top][i] != 0 { - indegree[i]-- - if indegree[i] == 0 { - queue = append(queue, i) - } - } - } - } - - // if circular dependency is existing - if len(result) != addonCount { - return false, nil, fmt.Errorf("there is a circular dependency cycle") - } - - sequenceDependencyName := make([]string, 0) - for _, id := range result { - if visited[AddonIdToName[id]] { - sequenceDependencyName = append(sequenceDependencyName, AddonIdToName[id]) - } - } - - return true, sequenceDependencyName, nil -} - // attachVolumeMount attaches a volumes to pod and added container.VolumeMounts to a ConfigMap // or Secret referenced key as file, and add --values={volumeMountPath}/{selector.Key} to // helm install/upgrade args @@ -1276,3 +1137,142 @@ func findDataKey[V string | []byte](data map[string]V, refObj extensionsv1alpha1 } return false } + +func checkVersionMatched(requiredVersion, currentVersion string) (bool, error) { + if len(currentVersion) == 0 { + fmt.Println("not specify the version") + return false, nil + } + if len(requiredVersion) == 0 { + return true, nil + } + if strings.Contains(currentVersion, "-") { + addPreReleaseInfo := func(constraint string) string { + constraint = strings.Trim(constraint, " ") + split := strings.Split(constraint, "-") + if len(split) == 1 && (strings.HasPrefix(constraint, ">") || strings.Contains(constraint, "<")) { + constraint += "-0" + } + return constraint + } + rules := strings.Split(requiredVersion, ",") + for i := range rules { + rules[i] = addPreReleaseInfo(rules[i]) + } + requiredVersion = strings.Join(rules, ",") + } + constraint, err := semver.NewConstraint(requiredVersion) + if err != nil { + return false, err + } + v, err := semver.NewVersion(currentVersion) + if err != nil { + return false, err + } + validate, _ := constraint.Validate(v) + return validate, nil +} + +// check if all the dependency are installed and not fail +// check if circular dependency is existing +// give the top sort of all the dependencies, return it +func checkAddonDependency(ctx context.Context, stageCtx *stageCtx, addon *extensionsv1alpha1.Addon) (bool, []string, error) { + AddonIDToName := map[int]string{} + AddonNameToID := map[string]int{} + visited := map[string]bool{} + addonList := &extensionsv1alpha1.AddonList{} + if err := stageCtx.reconciler.List(ctx, addonList, client.InNamespace(addon.Namespace)); err != nil { + return false, nil, err + } + + // construct an empty graph + addonCount := len(addonList.Items) + graph := make([][]int, addonCount) + for i := range graph { + graph[i] = make([]int, addonCount) + } + indegree := make([]int, addonCount) + + // construct the map between name and id + for i, item := range addonList.Items { + AddonNameToID[item.Name] = i + AddonIDToName[i] = item.Name + } + + // construct the graph which represents the dependency relationship among addons + var constructGraph func(addon *extensionsv1alpha1.Addon) error + constructGraph = func(addon *extensionsv1alpha1.Addon) error { + visited[addon.Name] = true + currentID := AddonNameToID[addon.Name] + for _, dependency := range addon.Spec.Dependencies { + var dependencyID int + var exist bool + if dependencyID, exist = AddonNameToID[dependency.Name]; !exist { + return fmt.Errorf("dependency %s not exist", dependency.Name) + } + + graph[dependencyID][currentID]++ + indegree[currentID]++ + if !visited[dependency.Name] { + dependencyAddon := &extensionsv1alpha1.Addon{} + if err := stageCtx.reconciler.Get(ctx, types.NamespacedName{Namespace: stageCtx.reqCtx.Req.Namespace, Name: dependency.Name}, dependencyAddon); err != nil { + return err + } + if dependencyAddon.Status.Phase == extensionsv1alpha1.AddonFailed { + return fmt.Errorf("dependency %s failed", dependency.Name) + } + if versionMatched, err := checkVersionMatched(dependency.Version, dependencyAddon.Spec.Version); err != nil { + return err + } else if !versionMatched { + return fmt.Errorf("version %s not matched", dependencyAddon.Spec.Version) + } + if err := constructGraph(dependencyAddon); err != nil { + return err + } + } + } + return nil + } + + if err := constructGraph(addon); err != nil { + return false, nil, err + } + + // TopSort + queue := make([]int, 0) + result := make([]int, 0) + + for i, degree := range indegree { + if degree == 0 { + queue = append(queue, i) + } + } + + for len(queue) > 0 { + top := queue[0] + result = append(result, top) + queue = queue[1:] + for i := 0; i < addonCount; i++ { + if graph[top][i] != 0 { + indegree[i]-- + if indegree[i] == 0 { + queue = append(queue, i) + } + } + } + } + + // if circular dependency is existing + if len(result) != addonCount { + return false, nil, fmt.Errorf("there is a circular dependency cycle") + } + + sequenceDependencyName := make([]string, 0) + for _, id := range result { + if visited[AddonIDToName[id]] { + sequenceDependencyName = append(sequenceDependencyName, AddonIDToName[id]) + } + } + + return true, sequenceDependencyName, nil +} diff --git a/controllers/extensions/addon_controller_test.go b/controllers/extensions/addon_controller_test.go index 75d58ddc6cd..905d4aaf4cc 100644 --- a/controllers/extensions/addon_controller_test.go +++ b/controllers/extensions/addon_controller_test.go @@ -118,17 +118,6 @@ var _ = Describe("Addon controller", func() { return addonReconciler.Reconcile(ctx, req) } - //doReconcileWithKey := func(specifiedKey types.NamespacedName) (ctrl.Result, error) { - // addonReconciler := &AddonReconciler{ - // Client: testCtx.Cli, - // Scheme: testCtx.Cli.Scheme(), - // } - // req := reconcile.Request{ - // NamespacedName: specifiedKey, - // } - // return addonReconciler.Reconcile(ctx, req) - //} - doReconcileOnce := func(g Gomega) { By("Reconciling once") result, err := doReconcile() @@ -451,7 +440,6 @@ var _ = Describe("Addon controller", func() { Expect(key.Name).Should(BeEquivalentTo(names[i])) } key.Name = "a" - //enablingPhaseCheck(2) Eventually(func(g Gomega) { _, err := doReconcile() g.Expect(err).To(Not(HaveOccurred())) @@ -556,7 +544,8 @@ var _ = Describe("Addon controller", func() { } key.Name = "a" Eventually(func(g Gomega) { - doReconcile() + _, err := doReconcile() + g.Expect(err).To(Not(HaveOccurred())) addon := &extensionsv1alpha1.Addon{} g.Expect(testCtx.Cli.Get(ctx, key, addon)).To(Not(HaveOccurred())) g.Expect(addon.Status.Phase).Should(Equal(extensionsv1alpha1.AddonFailed)) From 9e4c075592d9e63496568ae504c71eaa81b58285 Mon Sep 17 00:00:00 2001 From: d976045024 <976045024@qq.com> Date: Sun, 28 Apr 2024 09:43:29 +0800 Subject: [PATCH 3/6] feat: support installing releases of dependencies when enabling an addon (#5780) --- controllers/extensions/addon_controller_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/controllers/extensions/addon_controller_test.go b/controllers/extensions/addon_controller_test.go index 905d4aaf4cc..b22efb5a1f3 100644 --- a/controllers/extensions/addon_controller_test.go +++ b/controllers/extensions/addon_controller_test.go @@ -437,7 +437,6 @@ var _ = Describe("Addon controller", func() { newOjb.Spec.Dependencies = dependencies[i] } }) - Expect(key.Name).Should(BeEquivalentTo(names[i])) } key.Name = "a" Eventually(func(g Gomega) { From b7beb937508f96867bbfb235b7428bd82565b6f5 Mon Sep 17 00:00:00 2001 From: d976045024 <976045024@qq.com> Date: Sun, 28 Apr 2024 09:46:57 +0800 Subject: [PATCH 4/6] feat: support installing releases of dependencies when enabling an addon (#5780) --- apis/extensions/v1alpha1/addon_types.go | 1 + 1 file changed, 1 insertion(+) diff --git a/apis/extensions/v1alpha1/addon_types.go b/apis/extensions/v1alpha1/addon_types.go index bd205bbd0d9..699f1a1709c 100644 --- a/apis/extensions/v1alpha1/addon_types.go +++ b/apis/extensions/v1alpha1/addon_types.go @@ -82,6 +82,7 @@ type AddonSpec struct { CliPlugins []CliPlugin `json:"cliPlugins,omitempty"` // Specifies the dependencies of this addon + // // +optional Dependencies []DependencySpec `json:"dependencies,omitempty"` } From ace52ac251862d9af4a86396b8141f422772383c Mon Sep 17 00:00:00 2001 From: d976045024 <976045024@qq.com> Date: Sun, 28 Apr 2024 10:13:44 +0800 Subject: [PATCH 5/6] feat: support installing releases of dependencies when enabling an addon (#5780) --- controllers/extensions/addon_controller_stages.go | 8 +++++--- controllers/extensions/addon_controller_test.go | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/controllers/extensions/addon_controller_stages.go b/controllers/extensions/addon_controller_stages.go index 8f0fc79717c..6abf1072eaa 100644 --- a/controllers/extensions/addon_controller_stages.go +++ b/controllers/extensions/addon_controller_stages.go @@ -495,6 +495,7 @@ func (r *helmTypeInstallStage) Handle(ctx context.Context) { r.reqCtx.Log.V(1).Info("helmTypeInstallStage", "phase", addon.Status.Phase) mgrNS := viper.GetString(constant.CfgKeyCtrlrMgrNS) + // if there are some dependencies of current addon, we need to enabled them in topological order if _, sequenceDependencies, err := checkAddonDependency(ctx, &r.stageCtx, addon); err != nil { r.setRequeueWithErr(err, "") return @@ -504,13 +505,13 @@ func (r *helmTypeInstallStage) Handle(ctx context.Context) { if dependencyName == addon.Name { continue } - tmpKey := types.NamespacedName{Namespace: r.reqCtx.Req.Namespace, Name: dependencyName} dependencyAddon := &extensionsv1alpha1.Addon{} - if err := r.reconciler.Get(ctx, tmpKey, dependencyAddon); err != nil { + if err := r.reconciler.Get(ctx, types.NamespacedName{Namespace: r.reqCtx.Req.Namespace, Name: dependencyName}, dependencyAddon); err != nil { r.setRequeueWithErr(err, "") return } if dependencyAddon.Status.Phase != extensionsv1alpha1.AddonEnabled { + // the release of the dependency is not installed(not enabled) allDependenciesEnabled = false // enable the dependency addon if dependencyAddon.Spec.InstallSpec == nil { @@ -527,7 +528,8 @@ func (r *helmTypeInstallStage) Handle(ctx context.Context) { } if !allDependenciesEnabled { - r.setReconciled() + // some dependencies are not enabled, wait for them to be enabled + r.setRequeueAfter(time.Second, "") return } } diff --git a/controllers/extensions/addon_controller_test.go b/controllers/extensions/addon_controller_test.go index b22efb5a1f3..39c0acf079b 100644 --- a/controllers/extensions/addon_controller_test.go +++ b/controllers/extensions/addon_controller_test.go @@ -431,7 +431,9 @@ var _ = Describe("Addon controller", func() { for i := range names { createAddonSpecWithRequiredAttributes(func(newOjb *extensionsv1alpha1.Addon) { newOjb.Name = names[i] - newOjb.Spec.Installable.AutoInstall = true + if i == 0 { + newOjb.Spec.Installable.AutoInstall = true + } newOjb.Spec.Version = "1.0.0" if i < len(dependencies) { newOjb.Spec.Dependencies = dependencies[i] From 61dae0ca7cd75724058b5d7452308cf94eb629b8 Mon Sep 17 00:00:00 2001 From: d976045024 <976045024@qq.com> Date: Mon, 6 May 2024 09:53:13 +0800 Subject: [PATCH 6/6] feat: add required mark to dependency and support check in disablingStage (#5780) --- apis/extensions/v1alpha1/addon_types.go | 9 +- .../extensions/addon_controller_stages.go | 185 +++++++++--------- .../extensions/addon_controller_test.go | 85 +++++++- 3 files changed, 188 insertions(+), 91 deletions(-) diff --git a/apis/extensions/v1alpha1/addon_types.go b/apis/extensions/v1alpha1/addon_types.go index 699f1a1709c..8a80715e095 100644 --- a/apis/extensions/v1alpha1/addon_types.go +++ b/apis/extensions/v1alpha1/addon_types.go @@ -478,7 +478,14 @@ type ResourceRequirements struct { } type DependencySpec struct { - Name string `json:"name"` + // Specifies the name of the dependency. + // + // +kubebuilder:validation:Required + Name string `json:"name"` + + // Specifies the version of the dependency. + // + // +kubebuilder:validation:Required Version string `json:"version,omitempty"` } diff --git a/controllers/extensions/addon_controller_stages.go b/controllers/extensions/addon_controller_stages.go index 6abf1072eaa..5731d448124 100644 --- a/controllers/extensions/addon_controller_stages.go +++ b/controllers/extensions/addon_controller_stages.go @@ -39,6 +39,8 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + graph2 "github.com/apecloud/kubeblocks/pkg/controller/graph" + extensionsv1alpha1 "github.com/apecloud/kubeblocks/apis/extensions/v1alpha1" "github.com/apecloud/kubeblocks/pkg/constant" intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" @@ -269,9 +271,17 @@ func (r *installableCheckStage) Handle(ctx context.Context) { if addon.Status.Phase != extensionsv1alpha1.AddonEnabling && len(addon.Spec.Dependencies) != 0 { if _, _, err := checkAddonDependency(ctx, &r.stageCtx, addon); err != nil { r.reconciler.Event(addon, corev1.EventTypeWarning, InstallableRequirementUnmatched, err.Error()) - r.reqCtx.Log.V(1).Info("") + r.reqCtx.Log.V(1).Info(err.Error()) patch := client.MergeFrom(addon.DeepCopy()) addon.Status.Phase = extensionsv1alpha1.AddonFailed + addon.Status.ObservedGeneration = addon.Generation + meta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: extensionsv1alpha1.ConditionTypeChecked, + Status: metav1.ConditionFalse, + ObservedGeneration: addon.Generation, + Reason: InstallableRequirementUnmatched, + Message: err.Error(), + }) if err := r.reconciler.Status().Patch(ctx, addon, patch); err != nil { r.setRequeueWithErr(err, "") } @@ -383,6 +393,14 @@ func (r *progressingHandler) Handle(ctx context.Context) { return } if addon.Status.Phase != extensionsv1alpha1.AddonDisabling { + if depended, err := DependByOtherAddon(ctx, &r.stageCtx, addon); err != nil { + r.setRequeueWithErr(err, "") + return + } else if depended { + r.reqCtx.Log.V(1).Info("other addons are depended on it, can not be disabled") + r.setReconciled() + return + } patchPhase(extensionsv1alpha1.AddonDisabling, DisablingAddon) return } @@ -495,45 +513,6 @@ func (r *helmTypeInstallStage) Handle(ctx context.Context) { r.reqCtx.Log.V(1).Info("helmTypeInstallStage", "phase", addon.Status.Phase) mgrNS := viper.GetString(constant.CfgKeyCtrlrMgrNS) - // if there are some dependencies of current addon, we need to enabled them in topological order - if _, sequenceDependencies, err := checkAddonDependency(ctx, &r.stageCtx, addon); err != nil { - r.setRequeueWithErr(err, "") - return - } else { - allDependenciesEnabled := true - for _, dependencyName := range sequenceDependencies { - if dependencyName == addon.Name { - continue - } - dependencyAddon := &extensionsv1alpha1.Addon{} - if err := r.reconciler.Get(ctx, types.NamespacedName{Namespace: r.reqCtx.Req.Namespace, Name: dependencyName}, dependencyAddon); err != nil { - r.setRequeueWithErr(err, "") - return - } - if dependencyAddon.Status.Phase != extensionsv1alpha1.AddonEnabled { - // the release of the dependency is not installed(not enabled) - allDependenciesEnabled = false - // enable the dependency addon - if dependencyAddon.Spec.InstallSpec == nil { - enabledAddonWithDefaultValues(ctx, &r.stageCtx, dependencyAddon, AddonAutoInstall, "") - } else if !dependencyAddon.Spec.InstallSpec.Enabled { - dependencyAddon.Spec.InstallSpec.Enabled = true - if err := r.reconciler.Client.Update(ctx, dependencyAddon); err != nil { - r.setRequeueWithErr(err, "") - return - } - } - } - fmt.Printf("addon %v has been activated\n", dependencyAddon.Name) - } - - if !allDependenciesEnabled { - // some dependencies are not enabled, wait for them to be enabled - r.setRequeueAfter(time.Second, "") - return - } - } - key := client.ObjectKey{ Namespace: mgrNS, Name: getInstallJobName(addon), @@ -816,6 +795,47 @@ func (r *enablingStage) Handle(ctx context.Context) { r.helmTypeInstallStage.stageCtx = r.stageCtx r.process(func(addon *extensionsv1alpha1.Addon) { r.reqCtx.Log.V(1).Info("enablingStage", "phase", addon.Status.Phase) + // if there are some dependencies of current addon, we need to enabled them in topological order + if _, sequenceDependencies, err := checkAddonDependency(ctx, &r.stageCtx, addon); err != nil { + r.setRequeueWithErr(err, "") + return + } else { + allDependenciesEnabled := true + for _, dependency := range sequenceDependencies { + dependencyName := dependency.(string) + if dependencyName == addon.Name { + continue + } + dependencyAddon := &extensionsv1alpha1.Addon{} + if err := r.reconciler.Get(ctx, types.NamespacedName{Namespace: r.reqCtx.Req.Namespace, Name: dependencyName}, dependencyAddon); err != nil { + r.setRequeueWithErr(err, "") + return + } + if dependencyAddon.Status.Phase != extensionsv1alpha1.AddonEnabled { + // the release of the dependency is not installed(not enabled) + allDependenciesEnabled = false + // enable the dependency addon + if dependencyAddon.Spec.InstallSpec == nil { + enabledAddonWithDefaultValues(ctx, &r.stageCtx, dependencyAddon, AddonAutoInstall, "") + } else if !dependencyAddon.Spec.InstallSpec.Enabled { + patch := client.MergeFrom(addon.DeepCopy()) + dependencyAddon.Spec.InstallSpec.Enabled = true + if err := r.reconciler.Patch(ctx, dependencyAddon, patch); err != nil { + r.setRequeueWithErr(err, "") + return + } + } + } + msg := fmt.Sprintf("dependency %s is enabled", dependencyName) + r.reqCtx.Log.V(1).Info(msg) + } + + if !allDependenciesEnabled { + // some dependencies are not enabled, wait for them to be enabled + r.setRequeueAfter(time.Second, "") + return + } + } switch addon.Spec.Type { case extensionsv1alpha1.HelmType: r.helmTypeInstallStage.Handle(ctx) @@ -1142,12 +1162,12 @@ func findDataKey[V string | []byte](data map[string]V, refObj extensionsv1alpha1 func checkVersionMatched(requiredVersion, currentVersion string) (bool, error) { if len(currentVersion) == 0 { - fmt.Println("not specify the version") return false, nil } if len(requiredVersion) == 0 { return true, nil } + requiredVersion = ">=" + requiredVersion if strings.Contains(currentVersion, "-") { addPreReleaseInfo := func(constraint string) string { constraint = strings.Trim(constraint, " ") @@ -1178,43 +1198,29 @@ func checkVersionMatched(requiredVersion, currentVersion string) (bool, error) { // check if all the dependency are installed and not fail // check if circular dependency is existing // give the top sort of all the dependencies, return it -func checkAddonDependency(ctx context.Context, stageCtx *stageCtx, addon *extensionsv1alpha1.Addon) (bool, []string, error) { - AddonIDToName := map[int]string{} - AddonNameToID := map[string]int{} +func checkAddonDependency(ctx context.Context, stageCtx *stageCtx, addon *extensionsv1alpha1.Addon) (bool, []graph2.Vertex, error) { + addonNameToID := map[string]int{} visited := map[string]bool{} addonList := &extensionsv1alpha1.AddonList{} if err := stageCtx.reconciler.List(ctx, addonList, client.InNamespace(addon.Namespace)); err != nil { return false, nil, err } - // construct an empty graph - addonCount := len(addonList.Items) - graph := make([][]int, addonCount) - for i := range graph { - graph[i] = make([]int, addonCount) - } - indegree := make([]int, addonCount) - + dag := graph2.NewDAG() // construct the map between name and id for i, item := range addonList.Items { - AddonNameToID[item.Name] = i - AddonIDToName[i] = item.Name + addonNameToID[item.Name] = i } // construct the graph which represents the dependency relationship among addons var constructGraph func(addon *extensionsv1alpha1.Addon) error constructGraph = func(addon *extensionsv1alpha1.Addon) error { visited[addon.Name] = true - currentID := AddonNameToID[addon.Name] for _, dependency := range addon.Spec.Dependencies { - var dependencyID int - var exist bool - if dependencyID, exist = AddonNameToID[dependency.Name]; !exist { + if _, exist := addonNameToID[dependency.Name]; !exist { return fmt.Errorf("dependency %s not exist", dependency.Name) } - - graph[dependencyID][currentID]++ - indegree[currentID]++ + dag.AddConnect(addon.Name, dependency.Name) if !visited[dependency.Name] { dependencyAddon := &extensionsv1alpha1.Addon{} if err := stageCtx.reconciler.Get(ctx, types.NamespacedName{Namespace: stageCtx.reqCtx.Req.Namespace, Name: dependency.Name}, dependencyAddon); err != nil { @@ -1236,45 +1242,46 @@ func checkAddonDependency(ctx context.Context, stageCtx *stageCtx, addon *extens return nil } + dag.AddVertex(addon.Name) if err := constructGraph(addon); err != nil { return false, nil, err } // TopSort - queue := make([]int, 0) - result := make([]int, 0) - - for i, degree := range indegree { - if degree == 0 { - queue = append(queue, i) - } + result, err := TopSortForDependency(dag) + if err != nil { + return false, nil, err } + return true, result, nil +} - for len(queue) > 0 { - top := queue[0] - result = append(result, top) - queue = queue[1:] - for i := 0; i < addonCount; i++ { - if graph[top][i] != 0 { - indegree[i]-- - if indegree[i] == 0 { - queue = append(queue, i) - } - } - } +func TopSortForDependency(dag *graph2.DAG) ([]graph2.Vertex, error) { + result := make([]graph2.Vertex, 0) + walkFunc := func(v graph2.Vertex) error { + result = append(result, v) + return nil } - // if circular dependency is existing - if len(result) != addonCount { - return false, nil, fmt.Errorf("there is a circular dependency cycle") + if err := dag.WalkReverseTopoOrder(walkFunc, nil); err != nil { + // this will validate cases of self-cycle and cycle + return nil, err } + return result, nil +} - sequenceDependencyName := make([]string, 0) - for _, id := range result { - if visited[AddonIDToName[id]] { - sequenceDependencyName = append(sequenceDependencyName, AddonIDToName[id]) +func DependByOtherAddon(ctx context.Context, stageCtx *stageCtx, addon *extensionsv1alpha1.Addon) (bool, error) { + addonList := &extensionsv1alpha1.AddonList{} + if err := stageCtx.reconciler.List(ctx, addonList, client.InNamespace(addon.Namespace)); err != nil { + return false, err + } + for _, item := range addonList.Items { + if item.Spec.Dependencies != nil { + for _, dependency := range item.Spec.Dependencies { + if dependency.Name == addon.Name { + return true, nil + } + } } } - - return true, sequenceDependencyName, nil + return false, nil } diff --git a/controllers/extensions/addon_controller_test.go b/controllers/extensions/addon_controller_test.go index 39c0acf079b..fb3458f0876 100644 --- a/controllers/extensions/addon_controller_test.go +++ b/controllers/extensions/addon_controller_test.go @@ -22,6 +22,7 @@ package extensions import ( "context" "fmt" + "testing" "time" . "github.com/onsi/ginkgo/v2" @@ -449,7 +450,7 @@ var _ = Describe("Addon controller", func() { g.Expect(addon.Status.Phase).Should(Equal(extensionsv1alpha1.AddonEnabling)) }).Should(Succeed()) - sequence := []string{"d", "g", "h", "e", "c", "f", "b"} + sequence := []string{"h", "c", "d", "g", "e", "f", "b"} Eventually(func(g Gomega) { _, err := doReconcile() g.Expect(err).To(Not(HaveOccurred())) @@ -504,6 +505,19 @@ var _ = Describe("Addon controller", func() { g.Expect(addon.Status.Phase).Should(Equal(extensionsv1alpha1.AddonEnabled)) checkedJobDeletion(g, jobKey) }).Should(Succeed()) + + key.Name = "d" + addon := &extensionsv1alpha1.Addon{} + Expect(testCtx.Cli.Get(ctx, key, addon)).To(Not(HaveOccurred())) + addon.Spec.InstallSpec.Enabled = false + Expect(testCtx.Cli.Update(ctx, addon)).To(Succeed()) + Eventually(func(g Gomega) { + _, err := doReconcile() + g.Expect(err).To(Not(HaveOccurred())) + addon = &extensionsv1alpha1.Addon{} + g.Expect(testCtx.Cli.Get(ctx, key, addon)).To(Not(HaveOccurred())) + g.Expect(addon.Status.Phase).ShouldNot(Equal(extensionsv1alpha1.AddonDisabling)) + }).Should(Succeed()) }) It("should fail to enable or install an addon with version of dependencies not matched", func() { @@ -553,6 +567,60 @@ var _ = Describe("Addon controller", func() { }).Should(Succeed()) }) + It("should fail to enable or install an addon with circular dependency", func() { + By("create an addon with dependencies") + dependencies := [][]extensionsv1alpha1.DependencySpec{ + { + {Name: "b", Version: "1.0.0"}, + }, + { + {Name: "c", Version: "1.0.0"}, + }, + { + {Name: "d", Version: "1.0.0"}, + }, + { + {Name: "b", Version: "1.0.0"}, + }, + } + names := []string{"a", "b", "c", "d"} + for i := range names { + createAddonSpecWithRequiredAttributes(func(newOjb *extensionsv1alpha1.Addon) { + newOjb.Name = names[i] + newOjb.Spec.Version = "1.0.0" + newOjb.Spec.Installable.AutoInstall = true + if i < len(dependencies) { + newOjb.Spec.Dependencies = dependencies[i] + } + }) + Expect(key.Name).Should(BeEquivalentTo(names[i])) + } + key.Name = "a" + Eventually(func(g Gomega) { + _, err := doReconcile() + g.Expect(err).To(Not(HaveOccurred())) + addon := &extensionsv1alpha1.Addon{} + g.Expect(testCtx.Cli.Get(ctx, key, addon)).To(Not(HaveOccurred())) + g.Expect(addon.Status.Phase).Should(Equal(extensionsv1alpha1.AddonFailed)) + }).Should(Succeed()) + }) + + It("should fail to reconcile a custom resource with dependencies not fully specified", func() { + addon := &extensionsv1alpha1.Addon{ + ObjectMeta: metav1.ObjectMeta{ + Name: "no-depend-version", + }, + Spec: extensionsv1alpha1.AddonSpec{ + Dependencies: []extensionsv1alpha1.DependencySpec{ + { + Name: "dependency", + }, + }, + }, + } + Expect(testCtx.Cli.Create(ctx, addon)).To(HaveOccurred()) + }) + It("should successfully reconcile a custom resource for Addon with autoInstall=true", func() { createAutoInstallAddon() @@ -870,3 +938,18 @@ var _ = Describe("Addon controller", func() { }) }) }) + +func TestVersionValidation(t *testing.T) { + if match, _ := checkVersionMatched("0.8.0", "0.9.0"); !match { + t.Error("should return true for valid version") + } + if match, _ := checkVersionMatched("0.9.0", "0.9.0"); !match { + t.Error("should return true for valid version") + } + if match, _ := checkVersionMatched("0.9.0", "0.9.0-beta"); !match { + t.Error("should return true for valid version") + } + if match, _ := checkVersionMatched("0.9.0", "0.8.0"); match { + t.Error("should return false for invalid version") + } +}