github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/playground/destroy.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 playground
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"os"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/spf13/cobra"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    32  	"k8s.io/apimachinery/pkg/runtime"
    33  	apitypes "k8s.io/apimachinery/pkg/types"
    34  	"k8s.io/apimachinery/pkg/util/wait"
    35  	"k8s.io/cli-runtime/pkg/genericiooptions"
    36  	"k8s.io/client-go/dynamic"
    37  	"k8s.io/client-go/kubernetes"
    38  	"k8s.io/klog/v2"
    39  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    40  	"k8s.io/kubectl/pkg/util/templates"
    41  
    42  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    43  	cp "github.com/1aal/kubeblocks/pkg/cli/cloudprovider"
    44  	"github.com/1aal/kubeblocks/pkg/cli/cmd/kubeblocks"
    45  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    46  	"github.com/1aal/kubeblocks/pkg/cli/spinner"
    47  	"github.com/1aal/kubeblocks/pkg/cli/types"
    48  	"github.com/1aal/kubeblocks/pkg/cli/util"
    49  	"github.com/1aal/kubeblocks/pkg/cli/util/helm"
    50  	"github.com/1aal/kubeblocks/pkg/cli/util/prompt"
    51  )
    52  
    53  var (
    54  	destroyExample = templates.Examples(`
    55  		# destroy playground cluster
    56  		kbcli playground destroy`)
    57  )
    58  
    59  type destroyOptions struct {
    60  	genericiooptions.IOStreams
    61  	baseOptions
    62  
    63  	// purge resources, before destroying kubernetes cluster we should delete cluster and
    64  	// uninstall KubeBlocks
    65  	autoApprove bool
    66  	purge       bool
    67  	// timeout represents the timeout for the destruction process.
    68  	timeout time.Duration
    69  }
    70  
    71  func newDestroyCmd(streams genericiooptions.IOStreams) *cobra.Command {
    72  	o := &destroyOptions{
    73  		IOStreams: streams,
    74  	}
    75  	cmd := &cobra.Command{
    76  		Use:     "destroy",
    77  		Short:   "Destroy the playground KubeBlocks and kubernetes cluster.",
    78  		Example: destroyExample,
    79  		Run: func(cmd *cobra.Command, args []string) {
    80  			util.CheckErr(o.complete(cmd))
    81  			util.CheckErr(o.validate())
    82  			util.CheckErr(o.destroy())
    83  		},
    84  	}
    85  
    86  	cmd.Flags().BoolVar(&o.purge, "purge", true, "Purge all resources before destroying kubernetes cluster, delete all clusters created by KubeBlocks and uninstall KubeBlocks.")
    87  	cmd.Flags().DurationVar(&o.timeout, "timeout", 300*time.Second, "Time to wait for destroying KubeBlocks, such as --timeout=10m")
    88  	cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before destroying the playground")
    89  	return cmd
    90  }
    91  
    92  func (o *destroyOptions) destroy() error {
    93  	if o.prevCluster == nil {
    94  		return fmt.Errorf("no playground cluster found")
    95  	}
    96  
    97  	if o.prevCluster.CloudProvider == cp.Local {
    98  		return o.destroyLocal()
    99  	}
   100  	return o.destroyCloud()
   101  }
   102  
   103  // destroyLocal destroy local k3d cluster that will destroy all resources
   104  func (o *destroyOptions) destroyLocal() error {
   105  	provider, _ := cp.New(cp.Local, "", o.Out, o.ErrOut)
   106  	s := spinner.New(o.Out, spinnerMsg("Delete playground k3d cluster "+o.prevCluster.ClusterName))
   107  	defer s.Fail()
   108  	if err := provider.DeleteK8sCluster(o.prevCluster); err != nil {
   109  		if !strings.Contains(err.Error(), "no cluster found") &&
   110  			!strings.Contains(err.Error(), "does not exist") {
   111  			return err
   112  		}
   113  	}
   114  	s.Success()
   115  
   116  	if err := o.removeKubeConfig(); err != nil {
   117  		return err
   118  	}
   119  	return o.removeStateFile()
   120  }
   121  
   122  // destroyCloud destroys cloud kubernetes cluster, before destroying, we should delete
   123  // all clusters created by KubeBlocks, uninstall KubeBlocks and remove the KubeBlocks
   124  // namespace that will destroy all resources created by KubeBlocks, avoid to leave resources behind
   125  func (o *destroyOptions) destroyCloud() error {
   126  	var err error
   127  
   128  	printer.Warning(o.Out, `This action will destroy the kubernetes cluster, there may be residual resources,
   129    please confirm and manually clean up related resources after this action.
   130  
   131  `)
   132  
   133  	fmt.Fprintf(o.Out, "Do you really want to destroy the kubernetes cluster %s?\n%s\n\n  The operation cannot be rollbacked. Only 'yes' will be accepted to confirm.\n\n",
   134  		o.prevCluster.ClusterName, o.prevCluster.String())
   135  
   136  	// confirm to destroy
   137  	if !o.autoApprove {
   138  		entered, _ := prompt.NewPrompt("Enter a value:", nil, o.In).Run()
   139  		if entered != yesStr {
   140  			fmt.Fprintf(o.Out, "\nPlayground destroy cancelled.\n")
   141  			return cmdutil.ErrExit
   142  		}
   143  	}
   144  
   145  	o.startTime = time.Now()
   146  
   147  	// for cloud provider, we should delete all clusters created by KubeBlocks first,
   148  	// uninstall KubeBlocks and remove the KubeBlocks namespace, then destroy the
   149  	// playground cluster, avoid to leave resources behind.
   150  	// delete all clusters created by KubeBlocks, MUST BE VERY CAUTIOUS, use the right
   151  	// kubeconfig and context, otherwise, it will delete the wrong cluster.
   152  	if err = o.deleteClustersAndUninstallKB(); err != nil {
   153  		if strings.Contains(err.Error(), kubeClusterUnreachableErr.Error()) {
   154  			printer.Warning(o.Out, err.Error())
   155  		} else {
   156  			return err
   157  		}
   158  	}
   159  
   160  	// destroy playground kubernetes cluster
   161  	cpPath, err := cloudProviderRepoDir(o.prevCluster.KbcliVersion)
   162  	if err != nil {
   163  		return err
   164  	}
   165  
   166  	provider, err := cp.New(o.prevCluster.CloudProvider, cpPath, o.Out, o.ErrOut)
   167  	if err != nil {
   168  		return err
   169  	}
   170  
   171  	fmt.Fprintf(o.Out, "Destroy %s %s cluster %s...\n",
   172  		o.prevCluster.CloudProvider, cp.K8sService(o.prevCluster.CloudProvider), o.prevCluster.ClusterName)
   173  	if err = provider.DeleteK8sCluster(o.prevCluster); err != nil {
   174  		return err
   175  	}
   176  
   177  	// remove the cluster kubeconfig from the use default kubeconfig
   178  	if err = o.removeKubeConfig(); err != nil {
   179  		return err
   180  	}
   181  
   182  	// at last, remove the state file
   183  	if err = o.removeStateFile(); err != nil {
   184  		return err
   185  	}
   186  
   187  	fmt.Fprintf(o.Out, "Playground destroy completed in %s.\n", time.Since(o.startTime).Truncate(time.Second))
   188  	return nil
   189  }
   190  
   191  func (o *destroyOptions) deleteClustersAndUninstallKB() error {
   192  	var err error
   193  
   194  	if !o.purge {
   195  		klog.V(1).Infof("Skip to delete all clusters created by KubeBlocks and uninstall KubeBlocks")
   196  		return nil
   197  	}
   198  
   199  	if o.prevCluster.KubeConfig == "" {
   200  		fmt.Fprintf(o.Out, "No kubeconfig found for kubernetes cluster %s in %s \n",
   201  			o.prevCluster.ClusterName, o.stateFilePath)
   202  		return nil
   203  	}
   204  
   205  	// write kubeconfig content to a temporary file and use it
   206  	if err = writeAndUseKubeConfig(o.prevCluster.KubeConfig, o.kubeConfigPath, o.Out); err != nil {
   207  		return err
   208  	}
   209  
   210  	client, dynamic, err := getKubeClient()
   211  	if err != nil {
   212  		return err
   213  	}
   214  
   215  	// delete all clusters created by KubeBlocks
   216  	if err = o.deleteClusters(dynamic); err != nil {
   217  		return err
   218  	}
   219  
   220  	// uninstall KubeBlocks and remove namespace created by KubeBlocks
   221  	return o.uninstallKubeBlocks(client, dynamic)
   222  }
   223  
   224  // delete all clusters created by KubeBlocks
   225  func (o *destroyOptions) deleteClusters(dynamic dynamic.Interface) error {
   226  	var err error
   227  	ctx := context.Background()
   228  	// get all clusters in all namespaces
   229  	getClusters := func() (*unstructured.UnstructuredList, error) {
   230  		return dynamic.Resource(types.ClusterGVR()).Namespace(metav1.NamespaceAll).
   231  			List(context.Background(), metav1.ListOptions{})
   232  	}
   233  
   234  	// get all clusters and check if satisfy the checkFn
   235  	checkClusters := func(checkFn func(cluster *appsv1alpha1.Cluster) bool) (bool, error) {
   236  		res := true
   237  		clusters, err := getClusters()
   238  		if err != nil {
   239  			return false, err
   240  		}
   241  		for _, item := range clusters.Items {
   242  			cluster := &appsv1alpha1.Cluster{}
   243  			if err = runtime.DefaultUnstructuredConverter.FromUnstructured(item.Object, cluster); err != nil {
   244  				return false, err
   245  			}
   246  			if !checkFn(cluster) {
   247  				res = false
   248  				break
   249  			}
   250  		}
   251  		return res, nil
   252  	}
   253  
   254  	// delete all clusters
   255  	deleteClusters := func(clusters *unstructured.UnstructuredList) error {
   256  		for _, cluster := range clusters.Items {
   257  			if err = dynamic.Resource(types.ClusterGVR()).Namespace(cluster.GetNamespace()).
   258  				Delete(ctx, cluster.GetName(), *metav1.NewDeleteOptions(0)); err != nil {
   259  				return err
   260  			}
   261  		}
   262  		return nil
   263  	}
   264  
   265  	s := spinner.New(o.Out, spinnerMsg("Delete clusters created by KubeBlocks"))
   266  	defer s.Fail()
   267  
   268  	// get all clusters
   269  	clusters, err := getClusters()
   270  	if clusters == nil || len(clusters.Items) == 0 {
   271  		s.Success()
   272  		return nil
   273  	}
   274  
   275  	checkWipeOut := false
   276  	// set all cluster termination policy to WipeOut to delete all resources, otherwise
   277  	// the cluster will be deleted but the resources will be left
   278  	for _, item := range clusters.Items {
   279  		cluster := &appsv1alpha1.Cluster{}
   280  		if err = runtime.DefaultUnstructuredConverter.FromUnstructured(item.Object, cluster); err != nil {
   281  			return err
   282  		}
   283  		if cluster.Spec.TerminationPolicy == appsv1alpha1.WipeOut {
   284  			continue
   285  		}
   286  
   287  		// terminate policy is not WipeOut, set it to WipeOut
   288  		klog.V(1).Infof("Set cluster %s termination policy to WipeOut", cluster.Name)
   289  		if _, err = dynamic.Resource(types.ClusterGVR()).Namespace(cluster.Namespace).Patch(ctx, cluster.Name, apitypes.JSONPatchType,
   290  			[]byte(fmt.Sprintf("[{\"op\": \"replace\", \"path\": \"/spec/terminationPolicy\", \"value\": \"%s\" }]",
   291  				appsv1alpha1.WipeOut)), metav1.PatchOptions{}); err != nil {
   292  			return err
   293  		}
   294  
   295  		// set some cluster termination policy to WipeOut, need to check again
   296  		checkWipeOut = true
   297  	}
   298  
   299  	// check all clusters termination policy is WipeOut
   300  	if checkWipeOut {
   301  		if err = wait.PollUntilContextTimeout(context.Background(), 5*time.Second,
   302  			o.timeout, true, func(_ context.Context) (bool, error) {
   303  				return checkClusters(func(cluster *appsv1alpha1.Cluster) bool {
   304  					if cluster.Spec.TerminationPolicy != appsv1alpha1.WipeOut {
   305  						klog.V(1).Infof("Cluster %s termination policy is %s", cluster.Name, cluster.Spec.TerminationPolicy)
   306  					}
   307  					return cluster.Spec.TerminationPolicy == appsv1alpha1.WipeOut
   308  				})
   309  			}); err != nil {
   310  			return err
   311  		}
   312  	}
   313  
   314  	// delete all clusters
   315  	if err = deleteClusters(clusters); err != nil {
   316  		return err
   317  	}
   318  
   319  	// check and wait all clusters are deleted
   320  	if err = wait.PollUntilContextTimeout(context.Background(), 5*time.Second,
   321  		o.timeout, true, func(_ context.Context) (bool, error) {
   322  			return checkClusters(func(cluster *appsv1alpha1.Cluster) bool {
   323  				// always return false if any cluster is not deleted
   324  				klog.V(1).Infof("Cluster %s is not deleted", cluster.Name)
   325  				return false
   326  			})
   327  		}); err != nil {
   328  		return err
   329  	}
   330  
   331  	s.Success()
   332  	return nil
   333  }
   334  
   335  func (o *destroyOptions) uninstallKubeBlocks(client kubernetes.Interface, dynamic dynamic.Interface) error {
   336  	var err error
   337  	uninstall := kubeblocks.UninstallOptions{
   338  		Options: kubeblocks.Options{
   339  			IOStreams: o.IOStreams,
   340  			Client:    client,
   341  			Dynamic:   dynamic,
   342  			Wait:      true,
   343  		},
   344  		AutoApprove:     true,
   345  		RemoveNamespace: true,
   346  		Quiet:           true,
   347  	}
   348  
   349  	uninstall.HelmCfg = helm.NewConfig("", o.kubeConfigPath, "", klog.V(1).Enabled())
   350  	if err = uninstall.PreCheck(); err != nil {
   351  		return err
   352  	}
   353  	if err = uninstall.Uninstall(); err != nil {
   354  		return err
   355  	}
   356  	return nil
   357  }
   358  
   359  func (o *destroyOptions) removeKubeConfig() error {
   360  	s := spinner.New(o.Out, spinnerMsg("Remove kubeconfig from "+defaultKubeConfigPath))
   361  	defer s.Fail()
   362  	if err := kubeConfigRemove(o.prevCluster.KubeConfig, defaultKubeConfigPath); err != nil {
   363  		if os.IsNotExist(err) {
   364  			s.Success()
   365  			return nil
   366  		} else {
   367  			return err
   368  		}
   369  	}
   370  	s.Success()
   371  
   372  	clusterContext, err := kubeConfigCurrentContext(o.prevCluster.KubeConfig)
   373  	if err != nil {
   374  		return err
   375  	}
   376  
   377  	// check if current context in kubeconfig is deleted, if yes, notify user to set current context
   378  	currentContext, err := kubeConfigCurrentContextFromFile(defaultKubeConfigPath)
   379  	if err != nil {
   380  		return err
   381  	}
   382  
   383  	// current context is deleted, notify user to set current context with kubectl
   384  	if currentContext == clusterContext {
   385  		printer.Warning(o.Out, "this removed your active context, use \"kubectl config use-context\" to select a different one\n")
   386  	}
   387  	return nil
   388  }
   389  
   390  // remove state file
   391  func (o *destroyOptions) removeStateFile() error {
   392  	s := spinner.New(o.Out, spinnerMsg("Remove state file %s", o.stateFilePath))
   393  	defer s.Fail()
   394  	if err := removeStateFile(o.stateFilePath); err != nil {
   395  		return err
   396  	}
   397  	s.Success()
   398  	return nil
   399  }
   400  
   401  func (o *destroyOptions) complete(cmd *cobra.Command) error {
   402  	// enable log
   403  	if err := util.EnableLogToFile(cmd.Flags()); err != nil {
   404  		return fmt.Errorf("failed to enable the log file %s", err.Error())
   405  	}
   406  	return nil
   407  }