mirror of
https://github.com/kubevela/kubevela.git
synced 2026-02-14 18:10:21 +00:00
Some checks failed
CodeQL / Analyze (go) (push) Failing after 6m23s
Definition-Lint / definition-doc (push) Failing after 3m8s
E2E MultiCluster Test / detect-noop (push) Successful in 6s
E2E Test / detect-noop (push) Successful in 3s
Go / detect-noop (push) Successful in 2s
license / Check for unapproved licenses (push) Failing after 18s
Registry / publish-core-images (push) Failing after 1m4s
Unit-Test / detect-noop (push) Successful in 20s
Sync SDK / sync_sdk (push) Failing after 3m9s
Go / staticcheck (push) Successful in 6m17s
Go / check-diff (push) Failing after 19m4s
Go / check-core-image-build (push) Failing after 5m44s
Go / check-cli-image-build (push) Failing after 3m31s
Unit-Test / unit-tests (push) Failing after 13m54s
Go / lint (push) Failing after 1h53m27s
Scorecards supply-chain security / Scorecards analysis (push) Failing after 1m27s
E2E MultiCluster Test / e2e-multi-cluster-tests (v1.29) (push) Has been cancelled
E2E Test / e2e-tests (v1.29) (push) Has been cancelled
Go / check-windows (push) Has been cancelled
* chore: update k8s to 1.29 Signed-off-by: phantomnat <w.nattadej@gmail.com> * fix: unit test Signed-off-by: phantomnat <w.nattadej@gmail.com> * fix: lint Signed-off-by: phantomnat <w.nattadej@gmail.com> * fix: lint Signed-off-by: phantomnat <w.nattadej@gmail.com> * fix: e2e Signed-off-by: phantomnat <w.nattadej@gmail.com> * fix: lint and e2e test Signed-off-by: phantomnat <w.nattadej@gmail.com> * test(e2e): increase timeout Signed-off-by: phantomnat <w.nattadej@gmail.com> * fix e2e and scripts Signed-off-by: AiRanthem <zhongtianyun.zty@alibaba-inc.com> * make reviewable Signed-off-by: AiRanthem <zhongtianyun.zty@alibaba-inc.com> * rollback a unnecessary ut change Signed-off-by: AiRanthem <zhongtianyun.zty@alibaba-inc.com> * update go.mod to import merged workflow Signed-off-by: AiRanthem <zhongtianyun.zty@alibaba-inc.com> --------- Signed-off-by: phantomnat <w.nattadej@gmail.com> Signed-off-by: AiRanthem <zhongtianyun.zty@alibaba-inc.com> Co-authored-by: phantomnat <w.nattadej@gmail.com>
401 lines
14 KiB
Go
401 lines
14 KiB
Go
/*
|
|
Copyright 2021 The KubeVela Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/AlecAivazis/survey/v2"
|
|
"github.com/crossplane/crossplane-runtime/pkg/meta"
|
|
"github.com/kubevela/pkg/multicluster"
|
|
"github.com/kubevela/pkg/util/slices"
|
|
"github.com/spf13/cobra"
|
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
apitypes "k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/util/wait"
|
|
"k8s.io/kubectl/pkg/util/i18n"
|
|
"k8s.io/kubectl/pkg/util/templates"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
|
"github.com/oam-dev/kubevela/apis/core.oam.dev/condition"
|
|
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
|
"github.com/oam-dev/kubevela/apis/types"
|
|
velacmd "github.com/oam-dev/kubevela/pkg/cmd"
|
|
cmdutil "github.com/oam-dev/kubevela/pkg/cmd/util"
|
|
"github.com/oam-dev/kubevela/pkg/oam"
|
|
"github.com/oam-dev/kubevela/pkg/resourcekeeper"
|
|
"github.com/oam-dev/kubevela/pkg/resourcetracker"
|
|
com "github.com/oam-dev/kubevela/references/common"
|
|
)
|
|
|
|
// DeleteOptions options for vela delete command
|
|
type DeleteOptions struct {
|
|
AppNames []string
|
|
Namespace string
|
|
|
|
All bool
|
|
Wait bool
|
|
Orphan bool
|
|
Force bool
|
|
Interactive bool
|
|
|
|
AssumeYes bool
|
|
}
|
|
|
|
// Complete .
|
|
func (opt *DeleteOptions) Complete(f velacmd.Factory, cmd *cobra.Command, args []string) error {
|
|
opt.AppNames = args
|
|
opt.Namespace = velacmd.GetNamespace(f, cmd)
|
|
opt.AssumeYes = assumeYes
|
|
if len(opt.AppNames) > 0 && opt.All {
|
|
return fmt.Errorf("application name and --all cannot be both set")
|
|
}
|
|
if opt.All {
|
|
apps := &v1beta1.ApplicationList{}
|
|
if err := f.Client().List(cmd.Context(), apps, client.InNamespace(opt.Namespace)); err != nil {
|
|
return fmt.Errorf("failed to load application in namespace %s: %w", opt.Namespace, err)
|
|
}
|
|
opt.AppNames = slices.Map(apps.Items, func(app v1beta1.Application) string { return app.Name })
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate validate if vela delete args are valid
|
|
func (opt *DeleteOptions) Validate() error {
|
|
switch {
|
|
case len(opt.AppNames) == 0 && !opt.All:
|
|
return fmt.Errorf("no application provided for deletion")
|
|
case len(opt.AppNames) == 0 && opt.All:
|
|
return fmt.Errorf("no application found in namespace %s for deletion", opt.Namespace)
|
|
case opt.Interactive && (opt.Force || opt.Orphan):
|
|
return fmt.Errorf("--interactive cannot be used together with --force and --orphan")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (opt *DeleteOptions) getDeletingStatus(ctx context.Context, f velacmd.Factory, appKey apitypes.NamespacedName) (done bool, msg string, err error) {
|
|
app := &v1beta1.Application{}
|
|
err = f.Client().Get(ctx, appKey, app)
|
|
switch {
|
|
case kerrors.IsNotFound(err):
|
|
return true, "", nil
|
|
case err != nil:
|
|
return false, "", err
|
|
case app.DeletionTimestamp == nil:
|
|
return false, "application deletion is not handled by apiserver yet", nil
|
|
case app.Status.Phase != common.ApplicationDeleting:
|
|
return false, "application deletion is not handled by controller yet", nil
|
|
default:
|
|
if cond := slices.Find(app.Status.Conditions, func(cond condition.Condition) bool { return cond.Reason == condition.ReasonDeleting }); cond != nil {
|
|
return false, cond.Message, nil
|
|
}
|
|
return false, "", nil
|
|
}
|
|
}
|
|
|
|
// DeleteApp delete one application
|
|
func (opt *DeleteOptions) DeleteApp(f velacmd.Factory, cmd *cobra.Command, app *v1beta1.Application) error {
|
|
ctx := cmd.Context()
|
|
|
|
// delete the application interactively
|
|
if opt.Interactive {
|
|
if err := opt.interactiveDelete(ctx, f, cmd, app); err != nil {
|
|
return err
|
|
}
|
|
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Exit interactive deletion mode. You can switch to normal mode and continue with automatic deletion.\n")
|
|
return nil
|
|
}
|
|
|
|
if !opt.AssumeYes {
|
|
if !NewUserInput().AskBool(fmt.Sprintf("Are you sure to delete the application %s/%s", app.Namespace, app.Name), &UserInputOptions{opt.AssumeYes}) {
|
|
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "skip deleting application %s/%s\n", app.Namespace, app.Name)
|
|
return nil
|
|
}
|
|
}
|
|
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Start deleting application %s/%s\n", app.Namespace, app.Name)
|
|
|
|
// orphan app
|
|
if opt.Orphan {
|
|
if err := opt.orphan(ctx, f, app); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// delete app
|
|
if app.DeletionTimestamp == nil {
|
|
if err := opt.delete(ctx, f, app); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// force delete the application
|
|
if opt.Force {
|
|
if err := com.PrepareToForceDeleteTerraformComponents(ctx, f.Client(), app.Namespace, app.Name); err != nil {
|
|
return err
|
|
}
|
|
if err := opt.forceDelete(ctx, f, app); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// wait for deletion finished
|
|
if opt.Wait {
|
|
if err := opt.wait(ctx, f, app); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Delete application %s/%s succeeded\n", app.Namespace, app.Name)
|
|
return nil
|
|
}
|
|
|
|
func (opt *DeleteOptions) orphan(ctx context.Context, f velacmd.Factory, app *v1beta1.Application) error {
|
|
if !slices.Contains(app.GetFinalizers(), oam.FinalizerOrphanResource) {
|
|
meta.AddFinalizer(app, oam.FinalizerOrphanResource)
|
|
if err := f.Client().Update(ctx, app); err != nil {
|
|
return fmt.Errorf("failed to set orphan resource finalizer to application %s/%s: %w", app.Namespace, app.Name, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (opt *DeleteOptions) forceDelete(ctx context.Context, f velacmd.Factory, app *v1beta1.Application) error {
|
|
return wait.PollUntilContextTimeout(ctx, 3*time.Second, 1*time.Minute, true, func(ctx context.Context) (done bool, err error) {
|
|
err = f.Client().Get(ctx, client.ObjectKeyFromObject(app), app)
|
|
if kerrors.IsNotFound(err) {
|
|
return true, nil
|
|
}
|
|
rk, err := resourcekeeper.NewResourceKeeper(ctx, f.Client(), app)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to create resource keeper to run garbage collection: %w", err)
|
|
}
|
|
if done, _, err = rk.GarbageCollect(ctx); err != nil && !kerrors.IsConflict(err) {
|
|
return false, fmt.Errorf("failed to run garbage collect: %w", err)
|
|
}
|
|
if done {
|
|
meta.RemoveFinalizer(app, oam.FinalizerResourceTracker)
|
|
meta.RemoveFinalizer(app, oam.FinalizerOrphanResource)
|
|
if err = f.Client().Update(ctx, app); err != nil && !kerrors.IsConflict(err) && !kerrors.IsNotFound(err) {
|
|
return false, fmt.Errorf("failed to update app finalizer: %w", err)
|
|
}
|
|
}
|
|
return false, nil
|
|
})
|
|
}
|
|
|
|
func (opt *DeleteOptions) deleteResource(ctx context.Context, f velacmd.Factory, mr v1beta1.ManagedResource, app *v1beta1.Application) error {
|
|
obj := &unstructured.Unstructured{}
|
|
obj.SetGroupVersionKind(mr.GroupVersionKind())
|
|
if err := f.Client().Get(multicluster.WithCluster(ctx, mr.Cluster), mr.NamespacedName(), obj); err != nil {
|
|
return client.IgnoreNotFound(err)
|
|
}
|
|
if !resourcekeeper.IsResourceManagedByApplication(obj, app) {
|
|
return nil
|
|
}
|
|
return resourcekeeper.DeleteManagedResourceInApplication(ctx, f.Client(), mr, obj, app)
|
|
}
|
|
|
|
func _getManagedResourceSource(mr v1beta1.ManagedResource) string {
|
|
src := "in cluster local"
|
|
if mr.Cluster != "" {
|
|
src = fmt.Sprintf("in cluster %s", mr.Cluster)
|
|
}
|
|
if mr.Namespace != "" {
|
|
src += fmt.Sprintf(", namespace %s", mr.Namespace)
|
|
}
|
|
groups := strings.Split(mr.APIVersion, "/")
|
|
group := "." + groups[0]
|
|
if len(groups) == 0 {
|
|
group = ""
|
|
}
|
|
return fmt.Sprintf("%s%s %s %s", strings.ToLower(mr.Kind), group, mr.Name, src)
|
|
}
|
|
|
|
func (opt *DeleteOptions) interactiveDelete(ctx context.Context, f velacmd.Factory, cmd *cobra.Command, app *v1beta1.Application) error {
|
|
for {
|
|
rootRT, currentRT, historyRTs, _, err := resourcetracker.ListApplicationResourceTrackers(ctx, f.Client(), app)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get ResourceTrackers for application %s/%s: %w", app.Namespace, app.Name, err)
|
|
}
|
|
rts := slices.Filter(append(historyRTs, currentRT, rootRT), func(rt *v1beta1.ResourceTracker) bool { return rt != nil })
|
|
rs := map[string]v1beta1.ManagedResource{}
|
|
for _, rt := range rts {
|
|
for _, mr := range rt.Spec.ManagedResources {
|
|
rs[_getManagedResourceSource(mr)] = mr
|
|
}
|
|
}
|
|
var opts []string
|
|
for k := range rs {
|
|
opts = append(opts, k)
|
|
}
|
|
if len(opts) == 0 {
|
|
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "No resources found for application %s/%s\n", app.Namespace, app.Name)
|
|
return nil
|
|
}
|
|
prompt := &survey.Select{
|
|
Message: "Please choose which resource to delete",
|
|
Options: append(opts, "exit"),
|
|
}
|
|
var choice string
|
|
if err = survey.AskOne(prompt, &choice); err != nil {
|
|
return fmt.Errorf("exit on error: %w", err)
|
|
}
|
|
if choice == "exit" {
|
|
break
|
|
}
|
|
mr := rs[choice]
|
|
if err = opt.deleteResource(ctx, f, mr, app); err != nil {
|
|
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}) {
|
|
return fmt.Errorf("deletion aborted")
|
|
}
|
|
} else {
|
|
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Successfully recycled resource %s\n", choice)
|
|
}
|
|
for _, rt := range rts {
|
|
if slices.Index(rt.Spec.ManagedResources, func(r v1beta1.ManagedResource) bool { return r.ResourceKey() == mr.ResourceKey() }) >= 0 {
|
|
rt.Spec.ManagedResources = slices.Filter(rt.Spec.ManagedResources, func(r v1beta1.ManagedResource) bool { return r.ResourceKey() != mr.ResourceKey() })
|
|
if err = f.Client().Update(ctx, rt); err != nil {
|
|
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Error encountered when updating ResourceTracker %s: %s\n", rt.Name, err.Error())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (opt *DeleteOptions) delete(ctx context.Context, f velacmd.Factory, app *v1beta1.Application) error {
|
|
if err := f.Client().Delete(ctx, app); client.IgnoreNotFound(err) != nil {
|
|
return fmt.Errorf("failed to delete application %s/%s: %w", app.Namespace, app.Name, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (opt *DeleteOptions) wait(ctx context.Context, f velacmd.Factory, app *v1beta1.Application) error {
|
|
spinner := newTrackingSpinnerWithDelay(fmt.Sprintf("deleting application %s/%s", app.Namespace, app.Name), time.Second)
|
|
spinner.Start()
|
|
defer spinner.Stop()
|
|
return wait.PollUntilContextTimeout(ctx, 2*time.Second, 5*time.Minute, true, func(ctx context.Context) (done bool, err error) {
|
|
var msg string
|
|
done, msg, err = opt.getDeletingStatus(ctx, f, client.ObjectKeyFromObject(app))
|
|
applySpinnerNewSuffix(spinner, msg)
|
|
return done, err
|
|
})
|
|
}
|
|
|
|
// Run vela delete
|
|
func (opt *DeleteOptions) Run(f velacmd.Factory, cmd *cobra.Command) error {
|
|
for _, appName := range opt.AppNames {
|
|
app := &v1beta1.Application{}
|
|
if err := f.Client().Get(cmd.Context(), apitypes.NamespacedName{Namespace: opt.Namespace, Name: appName}, app); err != nil {
|
|
if kerrors.IsNotFound(err) {
|
|
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "application %s/%s already deleted\n", opt.Namespace, appName)
|
|
return nil
|
|
}
|
|
return fmt.Errorf("failed to get application %s/%s: %w", opt.Namespace, appName, err)
|
|
}
|
|
if err := opt.DeleteApp(f, cmd, app); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
deleteLong = templates.LongDesc(i18n.T(`
|
|
Delete applications
|
|
|
|
Delete KubeVela applications. KubeVela application deletion is associated
|
|
with the recycle of underlying resources. By default, the resources created
|
|
by the KubeVela application will be deleted once it is not in use or the
|
|
application is deleted. There is garbage-collect policy in KubeVela application
|
|
that you can use to configure customized recycle rules.
|
|
|
|
This command supports delete application in various modes.
|
|
Natively, you can use it like "kubectl delete app [app-name]".
|
|
In the cases you only want to delete the application but leave the
|
|
resources there, you can use the --orphan parameter.
|
|
In the cases the server-side controller is uninstalled, or you want to
|
|
manually skip some errors in the deletion process (like lack privileges or
|
|
handle cluster disconnection), you can use the --force parameter.
|
|
`))
|
|
|
|
deleteExample = templates.Examples(i18n.T(`
|
|
# Delete an application
|
|
vela delete my-app
|
|
|
|
# Delete multiple applications in a namespace
|
|
vela delete app-1 app-2 -n example
|
|
|
|
# Delete all applications in one namespace
|
|
vela delete -n example --all
|
|
|
|
# Delete application without waiting to be deleted
|
|
vela delete my-app --wait=false
|
|
|
|
# Delete application without confirmation
|
|
vela delete my-app -y
|
|
|
|
# Force delete application at client-side
|
|
vela delete my-app -f
|
|
|
|
# Delete application by orphaning resources and skip recycling them
|
|
vela delete my-app --orphan
|
|
|
|
# Delete application interactively
|
|
vela delete my-app -i
|
|
`))
|
|
)
|
|
|
|
// NewDeleteCommand Delete App
|
|
func NewDeleteCommand(f velacmd.Factory, order string) *cobra.Command {
|
|
o := &DeleteOptions{
|
|
Wait: true,
|
|
}
|
|
cmd := &cobra.Command{
|
|
Use: "delete",
|
|
DisableFlagsInUseLine: true,
|
|
Short: i18n.T("Delete an application."),
|
|
Long: deleteLong,
|
|
Example: deleteExample,
|
|
Annotations: map[string]string{
|
|
types.TagCommandOrder: order,
|
|
types.TagCommandType: types.TypeStart,
|
|
},
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
|
cmdutil.CheckErr(o.Validate())
|
|
cmdutil.CheckErr(o.Run(f, cmd))
|
|
},
|
|
}
|
|
|
|
cmd.PersistentFlags().BoolVarP(&o.Wait, "wait", "w", o.Wait, "wait util the application is deleted completely")
|
|
cmd.PersistentFlags().BoolVarP(&o.All, "all", "", o.All, "delete all the application under the given namespace")
|
|
cmd.PersistentFlags().BoolVarP(&o.Orphan, "orphan", "o", o.Orphan, "delete the application and orphan managed resources")
|
|
cmd.PersistentFlags().BoolVarP(&o.Force, "force", "f", o.Force, "force delete the application")
|
|
cmd.PersistentFlags().BoolVarP(&o.Interactive, "interactive", "i", o.Interactive, "delete the application interactively")
|
|
|
|
return velacmd.NewCommandBuilder(f, cmd).
|
|
WithNamespaceFlag().
|
|
WithResponsiveWriter().
|
|
Build()
|
|
}
|