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 }