diff --git a/docs/user_docs/cli/cli.md b/docs/user_docs/cli/cli.md
index 6786501ae..708989538 100644
--- a/docs/user_docs/cli/cli.md
+++ b/docs/user_docs/cli/cli.md
@@ -8,6 +8,7 @@ sidebar_position: 1
Addon command.
+* [kbcli addon delete-resources-with-version](kbcli_addon_delete-resources-with-version.md) - Delete the sub-resources of specified addon and versions
* [kbcli addon describe](kbcli_addon_describe.md) - Describe an addon specification.
* [kbcli addon disable](kbcli_addon_disable.md) - Disable an addon.
* [kbcli addon enable](kbcli_addon_enable.md) - Enable an addon.
diff --git a/docs/user_docs/cli/kbcli_addon.md b/docs/user_docs/cli/kbcli_addon.md
index 83a423e66..c5a94004d 100644
--- a/docs/user_docs/cli/kbcli_addon.md
+++ b/docs/user_docs/cli/kbcli_addon.md
@@ -37,6 +37,7 @@ Addon command.
### SEE ALSO
+* [kbcli addon delete-resources-with-version](kbcli_addon_delete-resources-with-version.md) - Delete the sub-resources of specified addon and versions
* [kbcli addon describe](kbcli_addon_describe.md) - Describe an addon specification.
* [kbcli addon disable](kbcli_addon_disable.md) - Disable an addon.
* [kbcli addon enable](kbcli_addon_enable.md) - Enable an addon.
diff --git a/pkg/cmd/addon/addon.go b/pkg/cmd/addon/addon.go
index 2b5c3f9d8..777fb6827 100644
--- a/pkg/cmd/addon/addon.go
+++ b/pkg/cmd/addon/addon.go
@@ -116,6 +116,7 @@ func NewAddonCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.C
newInstallCmd(f, streams),
newUninstallCmd(f, streams),
newUpgradeCmd(f, streams),
+ newDeleteResourcesCmd(f, streams),
)
return cmd
}
diff --git a/pkg/cmd/addon/delete_resource.go b/pkg/cmd/addon/delete_resource.go
new file mode 100644
index 000000000..9f0ad0aa4
--- /dev/null
+++ b/pkg/cmd/addon/delete_resource.go
@@ -0,0 +1,287 @@
+/*
+Copyright (C) 2022-2024 ApeCloud Co., Ltd
+
+This file is part of KubeBlocks project
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+*/
+
+package addon
+
+import (
+ "context"
+ "fmt"
+ "regexp"
+
+ "github.com/spf13/cobra"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/cli-runtime/pkg/genericiooptions"
+ cmdutil "k8s.io/kubectl/pkg/cmd/util"
+ "k8s.io/kubectl/pkg/util/templates"
+
+ kbv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1"
+ v1alpha1 "github.com/apecloud/kubeblocks/apis/extensions/v1alpha1"
+
+ "github.com/apecloud/kbcli/pkg/types"
+ "github.com/apecloud/kbcli/pkg/util"
+)
+
+const (
+ versionPattern = `(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?)$`
+ helmReleaseNameKey = "meta.helm.sh/release-name"
+ helmReleaseNamePrefix = "kb-addon-"
+ helmResourcePolicyKey = "helm.sh/resource-policy"
+ helmResourcePolicyKeep = "keep"
+)
+
+// Resource types to be processed for deletion
+var resourceToDelete = []schema.GroupVersionResource{
+ types.CompDefGVR(),
+ types.ConfigmapGVR(),
+ types.ConfigConstraintGVR(),
+ types.ConfigConstraintOldGVR(),
+}
+
+var addonDeleteResourcesExample = templates.Examples(`
+ # Delete specific versions of redis addon resources
+ kbcli addon delete-resources-with-version redis --versions=0.9.1,0.9.2
+
+ # Delete all unused and outdated resources of redis addon
+ kbcli addon delete-resources-with-version redis --all-unused-versions=true
+`)
+
+type deleteResourcesOption struct {
+ *baseOption
+ name string
+ versions []string
+ allUnusedVersions bool
+
+ // if set to true, the newest resources will also be deleted, and this flag is not open to user, only used to delete all the resources while addon uninstalling.
+ deleteNewestVersion bool
+}
+
+func newDeleteResourcesOption(f cmdutil.Factory, streams genericiooptions.IOStreams) *deleteResourcesOption {
+ return &deleteResourcesOption{
+ baseOption: &baseOption{
+ Factory: f,
+ IOStreams: streams,
+ GVR: types.AddonGVR(),
+ },
+ allUnusedVersions: false,
+ deleteNewestVersion: false,
+ }
+}
+
+func newDeleteResourcesCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
+ o := newDeleteResourcesOption(f, streams)
+ cmd := &cobra.Command{
+ Use: "delete-resources-with-version",
+ Short: "Delete the sub-resources of specified addon and versions",
+ Example: addonDeleteResourcesExample,
+ ValidArgsFunction: util.ResourceNameCompletionFunc(f, o.GVR),
+ Run: func(cmd *cobra.Command, args []string) {
+ util.CheckErr(o.baseOption.complete())
+ util.CheckErr(o.Complete(args))
+ util.CheckErr(o.Validate())
+ util.CheckErr(o.Run())
+ },
+ }
+ cmd.Flags().StringSliceVar(&o.versions, "versions", nil, "Specify the versions of resources to delete.")
+ cmd.Flags().BoolVar(&o.allUnusedVersions, "all-unused-versions", false, "If set to true, all the resources "+
+ "which are not currently used and not with the newest version will be deleted.")
+ return cmd
+}
+
+func (o *deleteResourcesOption) Complete(args []string) error {
+ if args == nil {
+ return fmt.Errorf("no addon provided; please specify the name of addon")
+ }
+ o.name = args[0]
+ versions, err := o.getExistedVersions(o.name)
+ if err != nil {
+ return fmt.Errorf("failed to retrieve versions for resource %s: %v", o.name, err)
+ }
+ newestVersion, err := o.getNewestVersion(o.name)
+ if err != nil {
+ return fmt.Errorf("failed to retrieve version for resource %s: %v", o.name, err)
+ }
+ versionInUse, err := o.getInUseVersions(o.name)
+ if err != nil {
+ return fmt.Errorf("failed to retrieve versions for resource %s: %v", o.name, err)
+ }
+ if o.allUnusedVersions {
+ for k := range versions {
+ if !versionInUse[k] && k != newestVersion {
+ o.versions = append(o.versions, k)
+ }
+ }
+ if o.deleteNewestVersion {
+ o.versions = append(o.versions, newestVersion)
+ }
+ }
+ return nil
+}
+
+func (o *deleteResourcesOption) Validate() error {
+ if o.allUnusedVersions {
+ return nil
+ }
+ if o.versions == nil {
+ return fmt.Errorf("no versions specified and --all-versions flag is not set; please specify versions or set --all-unused-versions to true")
+ }
+ versions, err := o.getExistedVersions(o.name)
+ if err != nil {
+ return fmt.Errorf("failed to retrieve versions for resource %s: %v", o.name, err)
+ }
+ newestVersion, err := o.getNewestVersion(o.name)
+ if err != nil {
+ return fmt.Errorf("failed to retrieve version for resource %s: %v", o.name, err)
+ }
+ versionsInUse, err := o.getInUseVersions(o.name)
+ if err != nil {
+ return fmt.Errorf("failed to retrieve versions for resource %s: %v", o.name, err)
+ }
+ for _, v := range o.versions {
+ if !versions[v] {
+ return fmt.Errorf("specified version %s does not exist for resource %s", v, o.name)
+ }
+ if !o.deleteNewestVersion && v == newestVersion {
+ return fmt.Errorf("specified version %s cannot be deleted as it is the newest version", v)
+ }
+ if versionsInUse[v] {
+ return fmt.Errorf("specified version %s cannot be deleted as it is currently used", v)
+ }
+ }
+ return nil
+}
+
+func (o *deleteResourcesOption) Run() error {
+ return o.cleanSubResources(o.name, o.versions)
+}
+
+// extractVersion extracts the version from a resource name using the provided regex pattern.
+func extractVersion(name string) string {
+ versionRegex := regexp.MustCompile(versionPattern)
+ return versionRegex.FindString(name)
+}
+
+// getExistedVersions get all the existed versions of specified addon by listing the componentDef.
+func (o *deleteResourcesOption) getExistedVersions(addonName string) (map[string]bool, error) {
+ resources, err := o.Dynamic.Resource(types.CompDefGVR()).Namespace(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{})
+ if err != nil {
+ return nil, fmt.Errorf("failed to list resources for %s: %w", types.CompDefGVR(), err)
+ }
+
+ totalVersions := make(map[string]bool)
+ for _, item := range resources.Items {
+ annotations := item.GetAnnotations()
+ if annotations[helmReleaseNameKey] != helmReleaseNamePrefix+addonName {
+ continue
+ }
+
+ version := extractVersion(item.GetName())
+ if version != "" {
+ totalVersions[version] = true
+ }
+ }
+
+ return totalVersions, nil
+}
+
+// getNewestVersion retrieves the newest version of the addon
+func (o *deleteResourcesOption) getNewestVersion(addonName string) (string, error) {
+ addon := &v1alpha1.Addon{}
+ err := util.GetK8SClientObject(o.Dynamic, addon, types.AddonGVR(), "", addonName)
+ if err != nil {
+ return "", fmt.Errorf("failed to get addon: %w", err)
+ }
+ return getAddonVersion(addon), nil
+}
+
+// getInUseVersions retrieves the versions of resources that are currently in use.
+func (o *deleteResourcesOption) getInUseVersions(addonName string) (map[string]bool, error) {
+ InUseVersions := map[string]bool{}
+ labelSelector := util.BuildClusterLabel("", []string{addonName})
+ clusterList, err := o.Dynamic.Resource(types.ClusterGVR()).Namespace(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{LabelSelector: labelSelector})
+ if err != nil {
+ return nil, fmt.Errorf("failed to list clusters: %w", err)
+ }
+ if clusterList != nil && len(clusterList.Items) > 0 {
+ for _, item := range clusterList.Items {
+ var cluster kbv1alpha1.Cluster
+ if err = runtime.DefaultUnstructuredConverter.FromUnstructured(item.Object, &cluster); err != nil {
+ return nil, fmt.Errorf("failed to convert cluster to structured object: %w", err)
+ }
+ for _, spec := range cluster.Spec.ComponentSpecs {
+ version := extractVersion(spec.ComponentDef)
+ if version != "" {
+ InUseVersions[version] = true
+ }
+ }
+ }
+ }
+
+ return InUseVersions, nil
+}
+
+// cleanSubResources Cleans up specified addon resources.
+func (o *deleteResourcesOption) cleanSubResources(addon string, versionsToDelete []string) error {
+ versions := make(map[string]bool)
+ for _, v := range versionsToDelete {
+ versions[v] = true
+ }
+
+ // Iterate through each resource type
+ for _, gvr := range resourceToDelete {
+ // List all resources of the current type
+ resources, err := o.Dynamic.Resource(gvr).Namespace(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{})
+ if err != nil {
+ return fmt.Errorf("failed to list resources for %s: %w", gvr.Resource, err)
+ }
+
+ // Process each resource in the list
+ for _, item := range resources.Items {
+ // Skip resources not belong to specified addon
+ annotations := item.GetAnnotations()
+ if annotations[helmReleaseNameKey] != helmReleaseNamePrefix+addon {
+ continue
+ }
+
+ // Skip resources of other versions.
+ name := item.GetName()
+ extractedVersion := extractVersion(name)
+ if extractedVersion == "" || !versions[extractedVersion] {
+ continue
+ }
+
+ // Skip resources if the resource doesn't have the annotation helm.sh/resource-policy: keep
+ if annotations[helmResourcePolicyKey] != helmResourcePolicyKeep {
+ continue
+ }
+
+ // Delete the resource if it passes all checks, and only print msg when user calling.
+ if !o.deleteNewestVersion {
+ err := o.Dynamic.Resource(gvr).Namespace(item.GetNamespace()).Delete(context.Background(), name, metav1.DeleteOptions{})
+ if err != nil {
+ return fmt.Errorf("failed to delete resource %s/%s: %w", gvr.Resource, name, err)
+ }
+ fmt.Fprintf(o.Out, "Deleted resource: %s/%s\n", gvr.Resource, name)
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/pkg/cmd/addon/delete_resource_test.go b/pkg/cmd/addon/delete_resource_test.go
new file mode 100644
index 000000000..012bb3208
--- /dev/null
+++ b/pkg/cmd/addon/delete_resource_test.go
@@ -0,0 +1,267 @@
+/*
+Copyright (C) 2022-2024 ApeCloud Co., Ltd
+
+This file is part of KubeBlocks project
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+*/
+
+package addon
+
+import (
+ "bytes"
+ "fmt"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/cli-runtime/pkg/genericiooptions"
+ clientfake "k8s.io/client-go/rest/fake"
+ clienttesting "k8s.io/client-go/testing"
+ cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
+
+ kbv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1"
+ v1alpha1 "github.com/apecloud/kubeblocks/apis/extensions/v1alpha1"
+ "github.com/apecloud/kubeblocks/pkg/constant"
+
+ "github.com/apecloud/kbcli/pkg/testing"
+ "github.com/apecloud/kbcli/pkg/types"
+)
+
+var _ = Describe("delete_resources_with_version test", func() {
+ var (
+ streams genericiooptions.IOStreams
+ tf *cmdtesting.TestFactory
+ bufOut, bufErr *bytes.Buffer
+ addonName = "redis"
+ newestVersion = "0.9.3"
+ inUseVersion = "0.9.2"
+ unusedVersion = "0.9.1"
+ testAddonGVR = types.AddonGVR()
+ testCompDefGVR = types.CompDefGVR()
+ testClusterGVR = types.ClusterGVR()
+ testUnusedConfigGVR = types.ConfigmapGVR()
+ testResourceAnnotKey = helmReleaseNameKey
+ )
+
+ // Helper functions to create fake resources
+ createAddon := func(name, version string) *unstructured.Unstructured {
+ addon := &v1alpha1.Addon{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name,
+ Labels: map[string]string{
+ types.AddonVersionLabelKey: version,
+ },
+ },
+ Spec: v1alpha1.AddonSpec{
+ Version: version,
+ },
+ }
+ obj, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(addon)
+ u := &unstructured.Unstructured{Object: obj}
+ u.SetGroupVersionKind(testAddonGVR.GroupVersion().WithKind("Addon"))
+ return u
+ }
+
+ createComponentDef := func(name, addon, version string) *unstructured.Unstructured {
+ u := &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "apiVersion": testCompDefGVR.GroupVersion().String(),
+ "kind": "ComponentDefinition",
+ "metadata": map[string]interface{}{
+ "name": name + "-" + version,
+ "annotations": map[string]interface{}{
+ testResourceAnnotKey: helmReleaseNamePrefix + addon,
+ helmResourcePolicyKey: helmResourcePolicyKeep,
+ },
+ },
+ },
+ }
+ return u
+ }
+
+ createCluster := func(name, addon, version string) *unstructured.Unstructured {
+ cluster := &kbv1alpha1.Cluster{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name,
+ Labels: map[string]string{
+ constant.ClusterDefLabelKey: addon,
+ },
+ },
+ Spec: kbv1alpha1.ClusterSpec{
+ ComponentSpecs: []kbv1alpha1.ClusterComponentSpec{
+ {
+ ComponentDef: fmt.Sprintf("redis-%s", version),
+ },
+ },
+ },
+ }
+ obj, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(cluster)
+ u := &unstructured.Unstructured{Object: obj}
+ u.SetGroupVersionKind(testClusterGVR.GroupVersion().WithKind("Cluster"))
+ return u
+ }
+
+ createUnusedConfig := func(addon, version string) *unstructured.Unstructured {
+ u := &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "apiVersion": testUnusedConfigGVR.GroupVersion().String(),
+ "kind": "ConfigMap",
+ "metadata": map[string]interface{}{
+ "name": "config-" + unusedVersion,
+ "annotations": map[string]interface{}{
+ testResourceAnnotKey: helmReleaseNamePrefix + addon,
+ helmResourcePolicyKey: helmResourcePolicyKeep,
+ },
+ },
+ },
+ }
+ return u
+ }
+
+ BeforeEach(func() {
+ bufOut = new(bytes.Buffer)
+ bufErr = new(bytes.Buffer)
+ streams = genericiooptions.IOStreams{Out: bufOut, ErrOut: bufErr}
+ tf = cmdtesting.NewTestFactory().WithNamespace(testNamespace)
+ tf.FakeDynamicClient = testing.FakeDynamicClient()
+ tf.Client = &clientfake.RESTClient{}
+
+ // Populate dynamic client with test resources
+ // Addon
+ _, _ = tf.FakeDynamicClient.Invokes(clienttesting.NewCreateAction(testAddonGVR, "", createAddon(addonName, newestVersion)), nil)
+
+ // ComponentDefs with different versions
+ _, _ = tf.FakeDynamicClient.Invokes(clienttesting.NewCreateAction(testCompDefGVR, types.DefaultNamespace, createComponentDef("redis", addonName, newestVersion)), nil)
+ _, _ = tf.FakeDynamicClient.Invokes(clienttesting.NewCreateAction(testCompDefGVR, types.DefaultNamespace, createComponentDef("redis", addonName, inUseVersion)), nil)
+ _, _ = tf.FakeDynamicClient.Invokes(clienttesting.NewCreateAction(testCompDefGVR, types.DefaultNamespace, createComponentDef("redis", addonName, unusedVersion)), nil)
+
+ // Cluster using inUseVersion
+ _, _ = tf.FakeDynamicClient.Invokes(clienttesting.NewCreateAction(testClusterGVR, testNamespace, createCluster("test-cluster", addonName, inUseVersion)), nil)
+
+ // Unused config resource for unusedVersion
+ _, _ = tf.FakeDynamicClient.Invokes(clienttesting.NewCreateAction(testUnusedConfigGVR, types.DefaultNamespace, createUnusedConfig(addonName, unusedVersion)), nil)
+ })
+
+ AfterEach(func() {
+ tf.Cleanup()
+ })
+
+ It("test delete_resources_with_versions cmd creation", func() {
+ Expect(newDeleteResourcesCmd(tf, streams)).ShouldNot(BeNil())
+ })
+
+ It("test baseOption complete", func() {
+ option := newDeleteResourcesOption(tf, streams)
+ Expect(option).ShouldNot(BeNil())
+ Expect(option.baseOption.complete()).Should(Succeed())
+ })
+
+ It("test no addon name provided", func() {
+ option := newDeleteResourcesOption(tf, streams)
+ err := option.Complete(nil)
+ Expect(err).Should(HaveOccurred())
+ Expect(err.Error()).Should(ContainSubstring("no addon provided"))
+ })
+
+ It("test no versions and no --all-unused-versions", func() {
+ option := newDeleteResourcesOption(tf, streams)
+ option.Dynamic = tf.FakeDynamicClient
+ option.Factory = tf
+ err := option.Complete([]string{addonName})
+ Expect(err).ShouldNot(HaveOccurred())
+
+ // Validate should fail due to no versions and no all-unused-versions
+ err = option.Validate()
+ Expect(err).Should(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("please specify versions or set --all-unused-versions to true"))
+ })
+
+ It("test specifying a non-existent version", func() {
+ option := newDeleteResourcesOption(tf, streams)
+ option.versions = []string{"1.0.0"}
+ option.Dynamic = tf.FakeDynamicClient
+ option.Factory = tf
+ err := option.Complete([]string{addonName})
+ Expect(err).ShouldNot(HaveOccurred())
+
+ err = option.Validate()
+ Expect(err).Should(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("does not exist"))
+ })
+
+ It("test specifying newest version without deleteNewestVersion flag", func() {
+ option := newDeleteResourcesOption(tf, streams)
+ option.versions = []string{newestVersion} // newest version
+ option.Dynamic = tf.FakeDynamicClient
+ option.Factory = tf
+ err := option.Complete([]string{addonName})
+ Expect(err).ShouldNot(HaveOccurred())
+
+ err = option.Validate()
+ Expect(err).Should(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("cannot be deleted as it is the newest version"))
+ })
+
+ It("test specifying an in-use version", func() {
+ option := newDeleteResourcesOption(tf, streams)
+ option.versions = []string{inUseVersion}
+ option.Dynamic = tf.FakeDynamicClient
+ option.Factory = tf
+ err := option.Complete([]string{addonName})
+ Expect(err).ShouldNot(HaveOccurred())
+
+ err = option.Validate()
+ Expect(err).Should(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("cannot be deleted as it is currently used"))
+ })
+
+ It("test specifying an unused old version directly", func() {
+ option := newDeleteResourcesOption(tf, streams)
+ option.versions = []string{unusedVersion}
+ option.Dynamic = tf.FakeDynamicClient
+ option.Factory = tf
+ err := option.Complete([]string{addonName})
+ Expect(err).ShouldNot(HaveOccurred())
+
+ // Validate should succeed
+ err = option.Validate()
+ Expect(err).ShouldNot(HaveOccurred())
+
+ // Run should delete resources associated with unusedVersion
+ err = option.Run()
+ Expect(err).ShouldNot(HaveOccurred())
+ Expect(bufOut.String()).To(ContainSubstring("Deleted resource: configmaps/" + "config-" + unusedVersion))
+ })
+
+ It("test using --all-unused-versions", func() {
+ option := newDeleteResourcesOption(tf, streams)
+ option.allUnusedVersions = true
+ option.Dynamic = tf.FakeDynamicClient
+ option.Factory = tf
+ err := option.Complete([]string{addonName})
+ Expect(err).ShouldNot(HaveOccurred())
+
+ // Validate should succeed now that we have automatically set unused versions
+ err = option.Validate()
+ Expect(err).ShouldNot(HaveOccurred())
+
+ // Run should delete all unused and non-newest versions. In this case, unusedVersion = "0.9.1"
+ err = option.Run()
+ Expect(err).ShouldNot(HaveOccurred())
+ Expect(bufOut.String()).To(ContainSubstring("Deleted resource: configmaps/" + "config-" + unusedVersion))
+ })
+})
diff --git a/pkg/cmd/addon/uninstall.go b/pkg/cmd/addon/uninstall.go
index 3c8dd4747..5c84ce6df 100644
--- a/pkg/cmd/addon/uninstall.go
+++ b/pkg/cmd/addon/uninstall.go
@@ -79,7 +79,12 @@ func newUninstallCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cob
func (o *uninstallOption) Run() error {
for _, name := range o.names {
- err := o.Dynamic.Resource(o.GVR).Delete(context.Background(), name, metav1.DeleteOptions{})
+ // delete the resources, because some resources won't be deleted by helm.
+ err := o.deleteAllMultiVersionsResources(name)
+ if err != nil {
+ return err
+ }
+ err = o.Dynamic.Resource(o.GVR).Delete(context.Background(), name, metav1.DeleteOptions{})
if err != nil {
return err
}
@@ -96,3 +101,25 @@ func (o *uninstallOption) checkBeforeUninstall() error {
}
return CheckAddonUsedByCluster(o.Dynamic, o.names, o.In)
}
+
+func (o *uninstallOption) deleteAllMultiVersionsResources(name string) error {
+ dro := &deleteResourcesOption{
+ baseOption: &baseOption{
+ Factory: o.Factory,
+ IOStreams: o.IOStreams,
+ GVR: types.AddonGVR(),
+ },
+ allUnusedVersions: true,
+ deleteNewestVersion: true,
+ }
+ if err := dro.baseOption.complete(); err != nil {
+ return err
+ }
+ if err := dro.Complete([]string{name}); err != nil {
+ return err
+ }
+ if err := dro.Run(); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/pkg/cmd/addon/uninstall_test.go b/pkg/cmd/addon/uninstall_test.go
new file mode 100644
index 000000000..cebe041c5
--- /dev/null
+++ b/pkg/cmd/addon/uninstall_test.go
@@ -0,0 +1,58 @@
+/*
+Copyright (C) 2022-2024 ApeCloud Co., Ltd
+
+# This file is part of KubeBlocks project
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+*/
+
+package addon
+
+import (
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ "k8s.io/cli-runtime/pkg/genericiooptions"
+ clientfake "k8s.io/client-go/rest/fake"
+ cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
+
+ "github.com/apecloud/kbcli/pkg/testing"
+)
+
+var _ = Describe("uninstall test", func() {
+ var (
+ streams genericiooptions.IOStreams
+ tf *cmdtesting.TestFactory
+ )
+
+ BeforeEach(func() {
+ streams, _, _, _ = genericiooptions.NewTestIOStreams()
+ tf = cmdtesting.NewTestFactory().WithNamespace(testNamespace)
+ tf.FakeDynamicClient = testing.FakeDynamicClient()
+ tf.Client = &clientfake.RESTClient{}
+ })
+
+ AfterEach(func() {
+ tf.Cleanup()
+ })
+
+ It("text uninstall cmd", func() {
+ Expect(newUninstallCmd(tf, streams)).ShouldNot(BeNil())
+ })
+
+ It("test baseOption complete", func() {
+ option := newUninstallOption(tf, streams)
+ Expect(option).ShouldNot(BeNil())
+ Expect(option.baseOption.complete()).Should(Succeed())
+ })
+})