github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/kubeblocks/uninstall.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 kubeblocks
    21  
    22  import (
    23  	"bytes"
    24  	"context"
    25  	"fmt"
    26  	"sort"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/pkg/errors"
    31  	"github.com/spf13/cobra"
    32  	"golang.org/x/exp/maps"
    33  	"helm.sh/helm/v3/pkg/repo"
    34  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    35  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    36  	"k8s.io/apimachinery/pkg/runtime"
    37  	"k8s.io/apimachinery/pkg/runtime/schema"
    38  	k8sapitypes "k8s.io/apimachinery/pkg/types"
    39  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    40  	"k8s.io/apimachinery/pkg/util/wait"
    41  	"k8s.io/cli-runtime/pkg/genericiooptions"
    42  	"k8s.io/client-go/dynamic"
    43  	"k8s.io/klog/v2"
    44  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    45  	"k8s.io/kubectl/pkg/util/templates"
    46  
    47  	extensionsv1alpha1 "github.com/1aal/kubeblocks/apis/extensions/v1alpha1"
    48  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    49  	"github.com/1aal/kubeblocks/pkg/cli/spinner"
    50  	"github.com/1aal/kubeblocks/pkg/cli/types"
    51  	"github.com/1aal/kubeblocks/pkg/cli/util"
    52  	"github.com/1aal/kubeblocks/pkg/cli/util/helm"
    53  )
    54  
    55  var (
    56  	uninstallExample = templates.Examples(`
    57  		# uninstall KubeBlocks
    58          kbcli kubeblocks uninstall`)
    59  )
    60  
    61  type UninstallOptions struct {
    62  	Factory cmdutil.Factory
    63  	Options
    64  
    65  	// AutoApprove if true, skip interactive approval
    66  	AutoApprove     bool
    67  	removePVs       bool
    68  	removePVCs      bool
    69  	RemoveNamespace bool
    70  	addons          []*extensionsv1alpha1.Addon
    71  	Quiet           bool
    72  	force           bool
    73  }
    74  
    75  func newUninstallCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
    76  	o := &UninstallOptions{
    77  		Options: Options{
    78  			IOStreams: streams,
    79  		},
    80  		Factory: f,
    81  		force:   true,
    82  	}
    83  	cmd := &cobra.Command{
    84  		Use:     "uninstall",
    85  		Short:   "Uninstall KubeBlocks.",
    86  		Args:    cobra.NoArgs,
    87  		Example: uninstallExample,
    88  		Run: func(cmd *cobra.Command, args []string) {
    89  			util.CheckErr(o.Complete(f, cmd))
    90  			util.CheckErr(o.PreCheck())
    91  			util.CheckErr(o.Uninstall())
    92  		},
    93  	}
    94  
    95  	cmd.Flags().BoolVar(&o.AutoApprove, "auto-approve", false, "Skip interactive approval before uninstalling KubeBlocks")
    96  	cmd.Flags().BoolVar(&o.removePVs, "remove-pvs", false, "Remove PersistentVolume or not")
    97  	cmd.Flags().BoolVar(&o.removePVCs, "remove-pvcs", false, "Remove PersistentVolumeClaim or not")
    98  	cmd.Flags().BoolVar(&o.RemoveNamespace, "remove-namespace", false, "Remove default created \"kb-system\" namespace or not")
    99  	cmd.Flags().DurationVar(&o.Timeout, "timeout", 300*time.Second, "Time to wait for uninstalling KubeBlocks, such as --timeout=5m")
   100  	cmd.Flags().BoolVar(&o.Wait, "wait", true, "Wait for KubeBlocks to be uninstalled, including all the add-ons. It will wait for a --timeout period")
   101  	return cmd
   102  }
   103  
   104  func (o *UninstallOptions) PreCheck() error {
   105  	// wait user to confirm
   106  	if !o.AutoApprove {
   107  		printer.Warning(o.Out, "this action will remove all KubeBlocks resources.\n")
   108  		if err := confirmUninstall(o.In); err != nil {
   109  			return err
   110  		}
   111  	}
   112  
   113  	// check if there is any resource should be removed first, if so, return error
   114  	// and ask user to remove them manually
   115  	if err := checkResources(o.Dynamic); err != nil {
   116  		return err
   117  	}
   118  
   119  	// verify where kubeblocks is installed
   120  	kbNamespace, err := util.GetKubeBlocksNamespace(o.Client)
   121  	if err != nil {
   122  		printer.Warning(o.Out, "failed to locate KubeBlocks meta, will clean up all KubeBlocks resources.\n")
   123  		if !o.Quiet {
   124  			fmt.Fprintf(o.Out, "to find out the namespace where KubeBlocks is installed, please use:\n\t'kbcli kubeblocks status'\n")
   125  			fmt.Fprintf(o.Out, "to uninstall KubeBlocks completely, please use:\n\t`kbcli kubeblocks uninstall -n <namespace>`\n")
   126  		}
   127  	}
   128  
   129  	o.Namespace = kbNamespace
   130  	if kbNamespace != "" {
   131  		fmt.Fprintf(o.Out, "Uninstall KubeBlocks in namespace \"%s\"\n", kbNamespace)
   132  	}
   133  
   134  	return nil
   135  }
   136  
   137  func (o *UninstallOptions) Uninstall() error {
   138  	printSpinner := func(s spinner.Interface, err error) {
   139  		if err == nil || apierrors.IsNotFound(err) ||
   140  			strings.Contains(err.Error(), "release: not found") {
   141  			s.Success()
   142  			return
   143  		}
   144  		s.Fail()
   145  		fmt.Fprintf(o.Out, "  %s\n", err.Error())
   146  	}
   147  	newSpinner := func(msg string) spinner.Interface {
   148  		return spinner.New(o.Out, spinner.WithMessage(fmt.Sprintf("%-50s", msg)))
   149  	}
   150  
   151  	// uninstall all KubeBlocks addons
   152  	if err := o.uninstallAddons(); err != nil {
   153  		fmt.Fprintf(o.Out, "Failed to uninstall addons, run \"kbcli kubeblocks uninstall\" to retry.\n")
   154  		return err
   155  	}
   156  
   157  	// uninstall helm release that will delete custom resources, but since finalizers is not empty,
   158  	// custom resources will not be deleted, so we will remove finalizers later.
   159  	v, _ := util.GetVersionInfo(o.Client)
   160  	chart := helm.InstallOpts{
   161  		Name:           types.KubeBlocksChartName,
   162  		Namespace:      o.Namespace,
   163  		ForceUninstall: o.force,
   164  		// KubeBlocks chart has a hook to delete addons, but we have already deleted addons,
   165  		// and that webhook may fail, so we need to disable hooks.
   166  		DisableHooks: true,
   167  	}
   168  	printSpinner(newSpinner("Uninstall helm release "+types.KubeBlocksReleaseName+" "+v.KubeBlocks),
   169  		chart.Uninstall(o.HelmCfg))
   170  
   171  	// remove repo
   172  	printSpinner(newSpinner("Remove helm repo "+types.KubeBlocksChartName),
   173  		helm.RemoveRepo(&repo.Entry{Name: types.KubeBlocksChartName}))
   174  
   175  	// get KubeBlocks objects, then try to remove them
   176  	objs, err := getKBObjects(o.Dynamic, o.Namespace, o.addons)
   177  	if err != nil {
   178  		fmt.Fprintf(o.ErrOut, "Failed to get KubeBlocks objects %s", err.Error())
   179  	}
   180  
   181  	// remove finalizers of custom resources, then that will be deleted
   182  	printSpinner(newSpinner("Remove built-in custom resources"), removeCustomResources(o.Dynamic, objs))
   183  
   184  	var gvrs []schema.GroupVersionResource
   185  	for k := range objs {
   186  		gvrs = append(gvrs, k)
   187  	}
   188  	sort.SliceStable(gvrs, func(i, j int) bool {
   189  		g1 := gvrs[i]
   190  		g2 := gvrs[j]
   191  		return strings.Compare(g1.Resource, g2.Resource) < 0
   192  	})
   193  
   194  	for _, gvr := range gvrs {
   195  		if gvr == types.PVCGVR() && !o.removePVCs {
   196  			continue
   197  		}
   198  		if gvr == types.PVGVR() && !o.removePVs {
   199  			continue
   200  		}
   201  		if v, ok := objs[gvr]; !ok || len(v.Items) == 0 {
   202  			continue
   203  		}
   204  		printSpinner(newSpinner("Remove "+gvr.Resource), deleteObjects(o.Dynamic, gvr, objs[gvr]))
   205  	}
   206  
   207  	// delete namespace if it is default namespace
   208  	if o.Namespace == types.DefaultNamespace && o.RemoveNamespace {
   209  		printSpinner(newSpinner("Remove namespace "+types.DefaultNamespace),
   210  			deleteNamespace(o.Client, types.DefaultNamespace))
   211  	}
   212  
   213  	if o.Wait {
   214  		fmt.Fprintln(o.Out, "Uninstall KubeBlocks done.")
   215  	} else {
   216  		fmt.Fprintf(o.Out, "KubeBlocks is uninstalling, run \"kbcli kubeblocks status -A\" to check kubeblocks resources.\n")
   217  	}
   218  	return nil
   219  }
   220  
   221  // uninstallAddons uninstalls all KubeBlocks addons
   222  func (o *UninstallOptions) uninstallAddons() error {
   223  	var (
   224  		allErrs []error
   225  		err     error
   226  		header  = "Wait for addons to be disabled"
   227  		s       spinner.Interface
   228  		msg     string
   229  	)
   230  
   231  	addons := make(map[string]*extensionsv1alpha1.Addon)
   232  	processAddons := func(uninstall bool) error {
   233  		objects, err := o.Dynamic.Resource(types.AddonGVR()).List(context.TODO(), metav1.ListOptions{
   234  			LabelSelector: buildKubeBlocksSelectorLabels(),
   235  		})
   236  		if err != nil && !apierrors.IsNotFound(err) {
   237  			klog.V(1).Infof("Failed to get KubeBlocks addons %s", err.Error())
   238  			allErrs = append(allErrs, err)
   239  			return utilerrors.NewAggregate(allErrs)
   240  		}
   241  		if objects == nil {
   242  			return nil
   243  		}
   244  
   245  		for _, obj := range objects.Items {
   246  			addon := &extensionsv1alpha1.Addon{}
   247  			if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, addon); err != nil {
   248  				klog.V(1).Infof("Failed to convert KubeBlocks addon %s", err.Error())
   249  				allErrs = append(allErrs, err)
   250  				continue
   251  			}
   252  
   253  			if uninstall {
   254  				// we only need to uninstall addons that are not disabled
   255  				if addon.Spec.InstallSpec.IsDisabled() {
   256  					continue
   257  				}
   258  				addons[addon.Name] = addon
   259  				o.addons = append(o.addons, addon)
   260  
   261  				// uninstall addons
   262  				if err = disableAddon(o.Dynamic, addon); err != nil {
   263  					klog.V(1).Infof("Failed to uninstall KubeBlocks addon %s %s", addon.Name, err.Error())
   264  					allErrs = append(allErrs, err)
   265  				}
   266  			} else {
   267  				// update cached addon if exists
   268  				if _, ok := addons[addon.Name]; ok {
   269  					addons[addon.Name] = addon
   270  				}
   271  			}
   272  		}
   273  		return utilerrors.NewAggregate(allErrs)
   274  	}
   275  
   276  	suffixMsg := func(msg string) string {
   277  		return fmt.Sprintf("%-50s", msg)
   278  	}
   279  
   280  	if !o.Wait {
   281  		s = spinner.New(o.Out, spinner.WithMessage(fmt.Sprintf("%-50s", "Uninstall KubeBlocks addons")))
   282  	} else {
   283  		s = spinner.New(o.Out, spinner.WithMessage(fmt.Sprintf("%-50s", header)))
   284  	}
   285  
   286  	// get all addons and uninstall them
   287  	if err = processAddons(true); err != nil {
   288  		s.Fail()
   289  		return err
   290  	}
   291  
   292  	if len(addons) == 0 || !o.Wait {
   293  		s.Success()
   294  		return nil
   295  	}
   296  
   297  	spinnerDone := func() {
   298  		s.SetFinalMsg(msg)
   299  		s.Done("")
   300  		fmt.Fprintln(o.Out)
   301  	}
   302  
   303  	// check if all addons are disabled, if so, then we will stop checking addons
   304  	// status otherwise, we will wait for a while and check again
   305  	if err = wait.PollImmediate(5*time.Second, o.Timeout, func() (bool, error) {
   306  		// we only check addons status, do not try to uninstall addons again
   307  		if err = processAddons(false); err != nil {
   308  			return false, err
   309  		}
   310  		status := checkAddons(maps.Values(addons), false)
   311  		msg = suffixMsg(fmt.Sprintf("%s\n  %s", header, status.outputMsg))
   312  		s.SetMessage(msg)
   313  		if status.allDisabled {
   314  			spinnerDone()
   315  			return true, nil
   316  		} else if status.hasFailed {
   317  			return false, errors.New("some addons are failed to disabled")
   318  		}
   319  		return false, nil
   320  	}); err != nil {
   321  		spinnerDone()
   322  		printAddonMsg(o.Out, maps.Values(addons), false)
   323  		allErrs = append(allErrs, err)
   324  	}
   325  	return utilerrors.NewAggregate(allErrs)
   326  }
   327  
   328  func checkResources(dynamic dynamic.Interface) error {
   329  	ctx := context.Background()
   330  	gvrList := []schema.GroupVersionResource{
   331  		types.ClusterGVR(),
   332  		types.BackupGVR(),
   333  	}
   334  
   335  	crs := map[string][]string{}
   336  	for _, gvr := range gvrList {
   337  		objList, err := dynamic.Resource(gvr).List(ctx, metav1.ListOptions{})
   338  		if err != nil && !apierrors.IsNotFound(err) {
   339  			return err
   340  		}
   341  
   342  		if objList == nil {
   343  			continue
   344  		}
   345  
   346  		for _, item := range objList.Items {
   347  			crs[gvr.Resource] = append(crs[gvr.Resource], item.GetName())
   348  		}
   349  	}
   350  
   351  	if len(crs) > 0 {
   352  		errMsg := bytes.NewBufferString("failed to uninstall, the following resources need to be removed first\n")
   353  		for k, v := range crs {
   354  			errMsg.WriteString(fmt.Sprintf("  %s: %s\n", k, strings.Join(v, " ")))
   355  		}
   356  		return errors.Errorf(errMsg.String())
   357  	}
   358  	return nil
   359  }
   360  
   361  func disableAddon(dynamic dynamic.Interface, addon *extensionsv1alpha1.Addon) error {
   362  	klog.V(1).Infof("Uninstall %s, status %s", addon.Name, addon.Status.Phase)
   363  	if _, err := dynamic.Resource(types.AddonGVR()).Patch(context.TODO(), addon.Name, k8sapitypes.JSONPatchType,
   364  		[]byte("[{\"op\": \"replace\", \"path\": \"/spec/install/enabled\", \"value\": false }]"),
   365  		metav1.PatchOptions{}); err != nil && !apierrors.IsNotFound(err) {
   366  		return err
   367  	}
   368  	return nil
   369  }