From 268701b2530ee64ddb6c46efacb8689dfa93cd89 Mon Sep 17 00:00:00 2001 From: micnncim Date: Mon, 21 Sep 2020 03:05:00 +0900 Subject: [PATCH] Add --wait and --timeout flags (#72) --- README.md | 2 + go.sum | 3 ++ pkg/cmd/cmd.go | 110 +++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 94 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index f4fab27..261dd5c 100644 --- a/README.md +++ b/README.md @@ -174,10 +174,12 @@ Flags: -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2) -s, --server string The address and port of the Kubernetes API server --template string Template string or path to template file to use when -o=go-template, -o=go-template-file. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview]. + --timeout duration The length of time to wait before giving up on a delete, zero means determine a timeout from the size of the object --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used --token string Bearer token for authentication to the API server --user string The name of the kubeconfig user to use -v, --version If true, show the version of this plugin + --wait If true, wait for resources to be gone before returning. This waits for finalizers. ``` diff --git a/go.sum b/go.sum index d8d84a4..9349cdc 100644 --- a/go.sum +++ b/go.sum @@ -136,6 +136,7 @@ github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7 h1:5ZkaAPbicIKTF2I64qf5Fh8Aa83Q/dnOafMYV0OMwjA= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -174,6 +175,7 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -186,6 +188,7 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmg github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index b7f02cb..b9228df 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -5,16 +5,21 @@ import ( "errors" "fmt" "strings" + "time" "github.com/spf13/cobra" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/printers" cliresource "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth" cmdutil "k8s.io/kubectl/pkg/cmd/util" + cmdwait "k8s.io/kubectl/pkg/cmd/wait" "github.com/micnncim/kubectl-prune/pkg/determiner" "github.com/micnncim/kubectl-prune/pkg/prompt" @@ -52,17 +57,21 @@ Delete unused resources. Supported resources: printedOperationTypeDeleted = "deleted" ) +var timeWeek = 168 * time.Hour + type Options struct { configFlags *genericclioptions.ConfigFlags printFlags *genericclioptions.PrintFlags - namespace string - allNamespaces bool - chunkSize int64 - labelSelector string - fieldSelector string - gracePeriod int - forceDeletion bool + namespace string + allNamespaces bool + chunkSize int64 + labelSelector string + fieldSelector string + gracePeriod int + forceDeletion bool + waitForDeletion bool + timeout time.Duration quiet bool interactive bool @@ -72,9 +81,10 @@ type Options struct { dryRunStrategy cmdutil.DryRunStrategy dryRunVerifier *cliresource.DryRunVerifier - determiner determiner.Determiner - printer printers.ResourcePrinter - result *cliresource.Result + determiner determiner.Determiner + dynamicClient dynamic.Interface + printer printers.ResourcePrinter + result *cliresource.Result genericclioptions.IOStreams } @@ -97,7 +107,7 @@ func NewCmdPrune(streams genericclioptions.IOStreams) *cobra.Command { Example: pruneExample, Run: func(cmd *cobra.Command, args []string) { if o.showVersion { - fmt.Fprintf(o.Out, "%s (%s)\n", version.Version, version.Revision) + o.Infof("%s (%s)\n", version.Version, version.Revision) return } @@ -119,6 +129,8 @@ func NewCmdPrune(streams genericclioptions.IOStreams) *cobra.Command { cmd.Flags().StringVar(&o.fieldSelector, "field-selector", "", "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.") cmd.Flags().IntVar(&o.gracePeriod, "grace-period", -1, "Period of time in seconds given to the resource to terminate gracefully. Ignored if negative. Set to 1 for immediate shutdown. Can only be set to 0 when --force is true (force deletion).") cmd.Flags().BoolVar(&o.forceDeletion, "force", false, "If true, immediately remove resources from API and bypass graceful deletion. Note that immediate deletion of some resources may result in inconsistency or data loss and requires confirmation.") + cmd.Flags().BoolVar(&o.waitForDeletion, "wait", false, "If true, wait for resources to be gone before returning. This waits for finalizers.") + cmd.Flags().DurationVar(&o.timeout, "timeout", 0, "The length of time to wait before giving up on a delete, zero means determine a timeout from the size of the object") cmd.Flags().BoolVarP(&o.quiet, "quiet", "q", false, "If true, no output is produced") cmd.Flags().BoolVarP(&o.interactive, "interactive", "i", false, "If true, a prompt asks whether resources can be deleted") cmd.Flags().BoolVarP(&o.showVersion, "version", "v", false, "If true, show the version of this plugin") @@ -158,17 +170,17 @@ func (o *Options) Complete(f cmdutil.Factory, args []string, cmd *cobra.Command) if err != nil { return } - dynamicClient, err := f.DynamicClient() + o.dynamicClient, err = f.DynamicClient() if err != nil { return } - resourceClient := resource.NewClient(clientset, dynamicClient) + resourceClient := resource.NewClient(clientset, o.dynamicClient) discoveryClient, err := f.ToDiscoveryClient() if err != nil { return err } - o.dryRunVerifier = cliresource.NewDryRunVerifier(dynamicClient, discoveryClient) + o.dryRunVerifier = cliresource.NewDryRunVerifier(o.dynamicClient, discoveryClient) namespace := o.namespace if o.allNamespaces { @@ -220,7 +232,7 @@ func (o *Options) Validate(args []string) error { switch { case o.forceDeletion && o.gracePeriod == 0: - fmt.Fprintf(o.ErrOut, "warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.\n") + o.Errorf("warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.\n") case o.forceDeletion && o.gracePeriod > 0: return fmt.Errorf("--force and --grace-period greater than 0 cannot be specified together") } @@ -229,6 +241,13 @@ func (o *Options) Validate(args []string) error { } func (o *Options) Run(ctx context.Context, f cmdutil.Factory) error { + deletedInfos := []*cliresource.Info{} + uidMap := cmdwait.UIDMap{} + deleteOpts := &metav1.DeleteOptions{} + if o.gracePeriod >= 0 { + deleteOpts = metav1.NewDeleteOptions(int64(o.gracePeriod)) + } + if err := o.result.Visit(func(info *cliresource.Info, err error) error { if info.Namespace == metav1.NamespaceSystem { return nil // ignore resources in kube-system namespace @@ -242,6 +261,8 @@ func (o *Options) Run(ctx context.Context, f cmdutil.Factory) error { return nil // skip deletion } + deletedInfos = append(deletedInfos, info) + if o.interactive { kind := info.Object.GetObjectKind().GroupVersionKind().Kind if ok := prompt.Confirm(fmt.Sprintf("Are you sure to delete %s/%s?", strings.ToLower(kind), info.Name)); !ok { @@ -259,14 +280,10 @@ func (o *Options) Run(ctx context.Context, f cmdutil.Factory) error { } } - opts := &metav1.DeleteOptions{} - if o.gracePeriod >= 0 { - opts = metav1.NewDeleteOptions(int64(o.gracePeriod)) - } - _, err = cliresource. + resp, err := cliresource. NewHelper(info.Client, info.Mapping). DryRun(o.dryRunStrategy == cmdutil.DryRunServer). - DeleteWithOptions(info.Namespace, info.Name, opts) + DeleteWithOptions(info.Namespace, info.Name, deleteOpts) if err != nil { return err } @@ -275,14 +292,65 @@ func (o *Options) Run(ctx context.Context, f cmdutil.Factory) error { o.printObj(info.Object) } + loc := cmdwait.ResourceLocation{ + GroupResource: info.Mapping.Resource.GroupResource(), + Namespace: info.Namespace, + Name: info.Name, + } + if status, ok := resp.(*metav1.Status); ok && status.Details != nil { + uidMap[loc] = status.Details.UID + return nil + } + + accessor, err := meta.Accessor(resp) + if err != nil { + // we don't have UID, but we didn't fail the delete, next best thing is just skipping the UID + o.Infof("%v\n", err) + return nil + } + uidMap[loc] = accessor.GetUID() + return nil }); err != nil { return err } + if !o.waitForDeletion { + return nil + } + + timeout := o.timeout + if timeout == 0 { + timeout = timeWeek + } + waitOpts := cmdwait.WaitOptions{ + ResourceFinder: genericclioptions.ResourceFinderForResult(cliresource.InfoListVisitor(deletedInfos)), + UIDMap: uidMap, + DynamicClient: o.dynamicClient, + Timeout: timeout, + Printer: printers.NewDiscardingPrinter(), + ConditionFn: cmdwait.IsDeleted, + IOStreams: o.IOStreams, + } + err := waitOpts.RunWait() + if apierrors.IsForbidden(err) || apierrors.IsMethodNotSupported(err) { + // if we're forbidden from waiting, we shouldn't fail. + // if the resource doesn't support a verb we need, we shouldn't fail. + o.Errorf("%v\n", err) + return nil + } + return nil } +func (o *Options) Infof(format string, a ...interface{}) { + fmt.Fprintf(o.Out, format, a...) +} + +func (o *Options) Errorf(format string, a ...interface{}) { + fmt.Fprintf(o.ErrOut, format, a...) +} + func (o *Options) printObj(obj runtime.Object) error { return o.printer.PrintObj(obj, o.Out) }