github.com/oam-dev/kubevela@v1.9.11/references/cli/delete.go (about)

     1  /*
     2  Copyright 2021 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cli
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/AlecAivazis/survey/v2"
    26  	"github.com/crossplane/crossplane-runtime/pkg/meta"
    27  	"github.com/kubevela/pkg/multicluster"
    28  	"github.com/kubevela/pkg/util/slices"
    29  	"github.com/spf13/cobra"
    30  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    31  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    32  	apitypes "k8s.io/apimachinery/pkg/types"
    33  	"k8s.io/apimachinery/pkg/util/wait"
    34  	"k8s.io/kubectl/pkg/util/i18n"
    35  	"k8s.io/kubectl/pkg/util/templates"
    36  	"sigs.k8s.io/controller-runtime/pkg/client"
    37  
    38  	"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
    39  	"github.com/oam-dev/kubevela/apis/core.oam.dev/condition"
    40  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
    41  	"github.com/oam-dev/kubevela/apis/types"
    42  	velacmd "github.com/oam-dev/kubevela/pkg/cmd"
    43  	cmdutil "github.com/oam-dev/kubevela/pkg/cmd/util"
    44  	"github.com/oam-dev/kubevela/pkg/oam"
    45  	"github.com/oam-dev/kubevela/pkg/resourcekeeper"
    46  	"github.com/oam-dev/kubevela/pkg/resourcetracker"
    47  	com "github.com/oam-dev/kubevela/references/common"
    48  )
    49  
    50  // DeleteOptions options for vela delete command
    51  type DeleteOptions struct {
    52  	AppNames  []string
    53  	Namespace string
    54  
    55  	All         bool
    56  	Wait        bool
    57  	Orphan      bool
    58  	Force       bool
    59  	Interactive bool
    60  
    61  	AssumeYes bool
    62  }
    63  
    64  // Complete .
    65  func (opt *DeleteOptions) Complete(f velacmd.Factory, cmd *cobra.Command, args []string) error {
    66  	opt.AppNames = args
    67  	opt.Namespace = velacmd.GetNamespace(f, cmd)
    68  	opt.AssumeYes = assumeYes
    69  	if len(opt.AppNames) > 0 && opt.All {
    70  		return fmt.Errorf("application name and --all cannot be both set")
    71  	}
    72  	if opt.All {
    73  		apps := &v1beta1.ApplicationList{}
    74  		if err := f.Client().List(cmd.Context(), apps, client.InNamespace(opt.Namespace)); err != nil {
    75  			return fmt.Errorf("failed to load application in namespace %s: %w", opt.Namespace, err)
    76  		}
    77  		opt.AppNames = slices.Map(apps.Items, func(app v1beta1.Application) string { return app.Name })
    78  	}
    79  	return nil
    80  }
    81  
    82  // Validate validate if vela delete args are valid
    83  func (opt *DeleteOptions) Validate() error {
    84  	switch {
    85  	case len(opt.AppNames) == 0 && !opt.All:
    86  		return fmt.Errorf("no application provided for deletion")
    87  	case len(opt.AppNames) == 0 && opt.All:
    88  		return fmt.Errorf("no application found in namespace %s for deletion", opt.Namespace)
    89  	case opt.Interactive && (opt.Force || opt.Orphan):
    90  		return fmt.Errorf("--interactive cannot be used together with --force and --orphan")
    91  	}
    92  	return nil
    93  }
    94  
    95  func (opt *DeleteOptions) getDeletingStatus(ctx context.Context, f velacmd.Factory, appKey apitypes.NamespacedName) (done bool, msg string, err error) {
    96  	app := &v1beta1.Application{}
    97  	err = f.Client().Get(ctx, appKey, app)
    98  	switch {
    99  	case kerrors.IsNotFound(err):
   100  		return true, "", nil
   101  	case err != nil:
   102  		return false, "", err
   103  	case app.DeletionTimestamp == nil:
   104  		return false, "application deletion is not handled by apiserver yet", nil
   105  	case app.Status.Phase != common.ApplicationDeleting:
   106  		return false, "application deletion is not handled by controller yet", nil
   107  	default:
   108  		if cond := slices.Find(app.Status.Conditions, func(cond condition.Condition) bool { return cond.Reason == condition.ReasonDeleting }); cond != nil {
   109  			return false, cond.Message, nil
   110  		}
   111  		return false, "", nil
   112  	}
   113  }
   114  
   115  // DeleteApp delete one application
   116  func (opt *DeleteOptions) DeleteApp(f velacmd.Factory, cmd *cobra.Command, app *v1beta1.Application) error {
   117  	ctx := cmd.Context()
   118  
   119  	// delete the application interactively
   120  	if opt.Interactive {
   121  		if err := opt.interactiveDelete(ctx, f, cmd, app); err != nil {
   122  			return err
   123  		}
   124  		_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Exit interactive deletion mode. You can switch to normal mode and continue with automatic deletion.\n")
   125  		return nil
   126  	}
   127  
   128  	if !opt.AssumeYes {
   129  		if !NewUserInput().AskBool(fmt.Sprintf("Are you sure to delete the application %s/%s", app.Namespace, app.Name), &UserInputOptions{opt.AssumeYes}) {
   130  			_, _ = fmt.Fprintf(cmd.OutOrStdout(), "skip deleting appplication %s/%s\n", app.Namespace, app.Name)
   131  			return nil
   132  		}
   133  	}
   134  	_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Start deleting appplication %s/%s\n", app.Namespace, app.Name)
   135  
   136  	// orphan app
   137  	if opt.Orphan {
   138  		if err := opt.orphan(ctx, f, app); err != nil {
   139  			return err
   140  		}
   141  	}
   142  
   143  	// delete app
   144  	if app.DeletionTimestamp == nil {
   145  		if err := opt.delete(ctx, f, app); err != nil {
   146  			return err
   147  		}
   148  	}
   149  
   150  	// force delete the application
   151  	if opt.Force {
   152  		if err := com.PrepareToForceDeleteTerraformComponents(ctx, f.Client(), app.Namespace, app.Name); err != nil {
   153  			return err
   154  		}
   155  		if err := opt.forceDelete(ctx, f, app); err != nil {
   156  			return err
   157  		}
   158  	}
   159  
   160  	// wait for deletion finished
   161  	if opt.Wait {
   162  		if err := opt.wait(ctx, f, app); err != nil {
   163  			return err
   164  		}
   165  	}
   166  
   167  	_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Delete appplication %s/%s succeeded\n", app.Namespace, app.Name)
   168  	return nil
   169  }
   170  
   171  func (opt *DeleteOptions) orphan(ctx context.Context, f velacmd.Factory, app *v1beta1.Application) error {
   172  	if !slices.Contains(app.GetFinalizers(), oam.FinalizerOrphanResource) {
   173  		meta.AddFinalizer(app, oam.FinalizerOrphanResource)
   174  		if err := f.Client().Update(ctx, app); err != nil {
   175  			return fmt.Errorf("failed to set orphan resource finalizer to application %s/%s: %w", app.Namespace, app.Name, err)
   176  		}
   177  	}
   178  	return nil
   179  }
   180  
   181  func (opt *DeleteOptions) forceDelete(ctx context.Context, f velacmd.Factory, app *v1beta1.Application) error {
   182  	return wait.PollImmediate(3*time.Second, 1*time.Minute, func() (done bool, err error) {
   183  		err = f.Client().Get(ctx, client.ObjectKeyFromObject(app), app)
   184  		if kerrors.IsNotFound(err) {
   185  			return true, nil
   186  		}
   187  		rk, err := resourcekeeper.NewResourceKeeper(ctx, f.Client(), app)
   188  		if err != nil {
   189  			return false, fmt.Errorf("failed to create resource keeper to run garbage collection: %w", err)
   190  		}
   191  		if done, _, err = rk.GarbageCollect(ctx); err != nil && !kerrors.IsConflict(err) {
   192  			return false, fmt.Errorf("failed to run garbage collect: %w", err)
   193  		}
   194  		if done {
   195  			meta.RemoveFinalizer(app, oam.FinalizerResourceTracker)
   196  			meta.RemoveFinalizer(app, oam.FinalizerOrphanResource)
   197  			if err = f.Client().Update(ctx, app); err != nil && !kerrors.IsConflict(err) && !kerrors.IsNotFound(err) {
   198  				return false, fmt.Errorf("failed to update app finalizer: %w", err)
   199  			}
   200  		}
   201  		return false, nil
   202  	})
   203  }
   204  
   205  func (opt *DeleteOptions) deleteResource(ctx context.Context, f velacmd.Factory, mr v1beta1.ManagedResource, app *v1beta1.Application) error {
   206  	obj := &unstructured.Unstructured{}
   207  	obj.SetGroupVersionKind(mr.GroupVersionKind())
   208  	if err := f.Client().Get(multicluster.WithCluster(ctx, mr.Cluster), mr.NamespacedName(), obj); err != nil {
   209  		return client.IgnoreNotFound(err)
   210  	}
   211  	if !resourcekeeper.IsResourceManagedByApplication(obj, app) {
   212  		return nil
   213  	}
   214  	return resourcekeeper.DeleteManagedResourceInApplication(ctx, f.Client(), mr, obj, app)
   215  }
   216  
   217  func _getManagedResourceSource(mr v1beta1.ManagedResource) string {
   218  	src := "in cluster local"
   219  	if mr.Cluster != "" {
   220  		src = fmt.Sprintf("in cluster %s", mr.Cluster)
   221  	}
   222  	if mr.Namespace != "" {
   223  		src += fmt.Sprintf(", namespace %s", mr.Namespace)
   224  	}
   225  	groups := strings.Split(mr.APIVersion, "/")
   226  	group := "." + groups[0]
   227  	if len(groups) == 0 {
   228  		group = ""
   229  	}
   230  	return fmt.Sprintf("%s%s %s %s", strings.ToLower(mr.Kind), group, mr.Name, src)
   231  }
   232  
   233  func (opt *DeleteOptions) interactiveDelete(ctx context.Context, f velacmd.Factory, cmd *cobra.Command, app *v1beta1.Application) error {
   234  	for {
   235  		rootRT, currentRT, historyRTs, _, err := resourcetracker.ListApplicationResourceTrackers(ctx, f.Client(), app)
   236  		if err != nil {
   237  			return fmt.Errorf("failed to get ResourceTrackers for application %s/%s: %w", app.Namespace, app.Name, err)
   238  		}
   239  		rts := slices.Filter(append(historyRTs, currentRT, rootRT), func(rt *v1beta1.ResourceTracker) bool { return rt != nil })
   240  		rs := map[string]v1beta1.ManagedResource{}
   241  		for _, rt := range rts {
   242  			for _, mr := range rt.Spec.ManagedResources {
   243  				rs[_getManagedResourceSource(mr)] = mr
   244  			}
   245  		}
   246  		var opts []string
   247  		for k := range rs {
   248  			opts = append(opts, k)
   249  		}
   250  		if len(opts) == 0 {
   251  			_, _ = fmt.Fprintf(cmd.OutOrStdout(), "No resources found for application %s/%s\n", app.Namespace, app.Name)
   252  			return nil
   253  		}
   254  		prompt := &survey.Select{
   255  			Message: "Please choose which resource to delete",
   256  			Options: append(opts, "exit"),
   257  		}
   258  		var choice string
   259  		if err = survey.AskOne(prompt, &choice); err != nil {
   260  			return fmt.Errorf("exit on error: %w", err)
   261  		}
   262  		if choice == "exit" {
   263  			break
   264  		}
   265  		mr := rs[choice]
   266  		if err = opt.deleteResource(ctx, f, mr, app); err != nil {
   267  			if !NewUserInput().AskBool(fmt.Sprintf("Error encountered while recycling %s: %s.\nDo you want to skip this error?", choice, err.Error()), &UserInputOptions{AssumeYes: opt.AssumeYes}) {
   268  				return fmt.Errorf("deletion aborted")
   269  			}
   270  		} else {
   271  			_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Successfully recycled resource %s\n", choice)
   272  		}
   273  		for _, rt := range rts {
   274  			if slices.Index(rt.Spec.ManagedResources, func(r v1beta1.ManagedResource) bool { return r.ResourceKey() == mr.ResourceKey() }) >= 0 {
   275  				rt.Spec.ManagedResources = slices.Filter(rt.Spec.ManagedResources, func(r v1beta1.ManagedResource) bool { return r.ResourceKey() != mr.ResourceKey() })
   276  				if err = f.Client().Update(ctx, rt); err != nil {
   277  					_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Error encountered when updating ResourceTracker %s: %s\n", rt.Name, err.Error())
   278  				}
   279  			}
   280  		}
   281  	}
   282  	return nil
   283  }
   284  
   285  func (opt *DeleteOptions) delete(ctx context.Context, f velacmd.Factory, app *v1beta1.Application) error {
   286  	if err := f.Client().Delete(ctx, app); client.IgnoreNotFound(err) != nil {
   287  		return fmt.Errorf("failed to delete application %s/%s: %w", app.Namespace, app.Name, err)
   288  	}
   289  	return nil
   290  }
   291  
   292  func (opt *DeleteOptions) wait(ctx context.Context, f velacmd.Factory, app *v1beta1.Application) error {
   293  	spinner := newTrackingSpinnerWithDelay(fmt.Sprintf("deleting application %s/%s", app.Namespace, app.Name), time.Second)
   294  	spinner.Start()
   295  	defer spinner.Stop()
   296  	return wait.PollImmediate(2*time.Second, 5*time.Minute, func() (done bool, err error) {
   297  		var msg string
   298  		done, msg, err = opt.getDeletingStatus(ctx, f, client.ObjectKeyFromObject(app))
   299  		applySpinnerNewSuffix(spinner, msg)
   300  		return done, err
   301  	})
   302  }
   303  
   304  // Run vela delete
   305  func (opt *DeleteOptions) Run(f velacmd.Factory, cmd *cobra.Command) error {
   306  	for _, appName := range opt.AppNames {
   307  		app := &v1beta1.Application{}
   308  		if err := f.Client().Get(cmd.Context(), apitypes.NamespacedName{Namespace: opt.Namespace, Name: appName}, app); err != nil {
   309  			if kerrors.IsNotFound(err) {
   310  				_, _ = fmt.Fprintf(cmd.OutOrStdout(), "application %s/%s already deleted\n", opt.Namespace, appName)
   311  				return nil
   312  			}
   313  			return fmt.Errorf("failed to get application %s/%s: %w", opt.Namespace, appName, err)
   314  		}
   315  		if err := opt.DeleteApp(f, cmd, app); err != nil {
   316  			return err
   317  		}
   318  	}
   319  	return nil
   320  }
   321  
   322  var (
   323  	deleteLong = templates.LongDesc(i18n.T(`
   324  		Delete applications
   325  
   326  		Delete KubeVela applications. KubeVela application deletion is associated
   327  		with the recycle of underlying resources. By default, the resources created
   328  		by the KubeVela application will be deleted once it is not in use or the
   329  		application is deleted. There is garbage-collect policy in KubeVela application
   330  		that you can use to configure customized recycle rules.
   331  
   332  		This command supports delete application in various modes.
   333  		Natively, you can use it like "kubectl delete app [app-name]". 
   334  		In the cases you only want to delete the application but leave the 
   335  		resources there, you can use the --orphan parameter.
   336  		In the cases the server-side controller is uninstalled, or you want to
   337  		manually skip some errors in the deletion process (like lack privileges or
   338  		handle cluster disconnection), you can use the --force parameter. 
   339  	`))
   340  
   341  	deleteExample = templates.Examples(i18n.T(`
   342  		# Delete an application
   343  		vela delete my-app
   344  	
   345  		# Delete multiple applications in a namespace
   346  		vela delete app-1 app-2 -n example
   347  
   348  		# Delete all applications in one namespace
   349  		vela delete -n example --all
   350  
   351  		# Delete application without waiting to be deleted
   352  		vela delete my-app --wait=false
   353  
   354  		# Delete application without confirmation
   355  		vela delete my-app -y
   356  
   357  		# Force delete application at client-side
   358  		vela delete my-app -f
   359  
   360  		# Delete application by orphaning resources and skip recycling them
   361  		vela delete my-app --orphan
   362  
   363  		# Delete application interactively
   364  		vela delete my-app -i
   365  	`))
   366  )
   367  
   368  // NewDeleteCommand Delete App
   369  func NewDeleteCommand(f velacmd.Factory, order string) *cobra.Command {
   370  	o := &DeleteOptions{
   371  		Wait: true,
   372  	}
   373  	cmd := &cobra.Command{
   374  		Use:                   "delete",
   375  		DisableFlagsInUseLine: true,
   376  		Short:                 i18n.T("Delete an application."),
   377  		Long:                  deleteLong,
   378  		Example:               deleteExample,
   379  		Annotations: map[string]string{
   380  			types.TagCommandOrder: order,
   381  			types.TagCommandType:  types.TypeStart,
   382  		},
   383  		Run: func(cmd *cobra.Command, args []string) {
   384  			cmdutil.CheckErr(o.Complete(f, cmd, args))
   385  			cmdutil.CheckErr(o.Validate())
   386  			cmdutil.CheckErr(o.Run(f, cmd))
   387  		},
   388  	}
   389  
   390  	cmd.PersistentFlags().BoolVarP(&o.Wait, "wait", "w", o.Wait, "wait util the application is deleted completely")
   391  	cmd.PersistentFlags().BoolVarP(&o.All, "all", "", o.All, "delete all the application under the given namespace")
   392  	cmd.PersistentFlags().BoolVarP(&o.Orphan, "orphan", "o", o.Orphan, "delete the application and orphan managed resources")
   393  	cmd.PersistentFlags().BoolVarP(&o.Force, "force", "f", o.Force, "force delete the application")
   394  	cmd.PersistentFlags().BoolVarP(&o.Interactive, "interactive", "i", o.Interactive, "delete the application interactively")
   395  
   396  	return velacmd.NewCommandBuilder(f, cmd).
   397  		WithNamespaceFlag().
   398  		WithResponsiveWriter().
   399  		Build()
   400  }