From 5095ae93fda67cb6d20accb15d0fb3beb4f3c9ff Mon Sep 17 00:00:00 2001 From: John Gardiner Myers Date: Thu, 8 Jul 2021 06:42:11 -0700 Subject: [PATCH 1/5] Remove -rc.0 k8s versions from completion --- cmd/kops/create_cluster.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmd/kops/create_cluster.go b/cmd/kops/create_cluster.go index 857879fb9598a..644a2cdb4754b 100644 --- a/cmd/kops/create_cluster.go +++ b/cmd/kops/create_cluster.go @@ -866,11 +866,15 @@ func completeKubernetesVersion(cmd *cobra.Command, args []string, toComplete str } // Remove pre-release versions that have a subsequent stable version. + // Also remove the non-useful -rc.0 versions. for _, version := range versions.UnsortedList() { split := strings.Split(version, "-") if len(split) > 1 && versions.Has(split[0]) { versions.Delete(version) } + if strings.HasSuffix(version, "-rc.0") { + versions.Delete(version) + } } return versions.List(), cobra.ShellCompDirectiveNoFileComp From c864dc02ca974ef0ef5c8aa8f21cd8056648ae2d Mon Sep 17 00:00:00 2001 From: John Gardiner Myers Date: Thu, 8 Jul 2021 06:57:15 -0700 Subject: [PATCH 2/5] Clean up "kops delete -f" --- cmd/kops/delete.go | 48 ++++++++------------------- docs/cli/kops.md | 2 +- docs/cli/kops_delete.md | 20 ++--------- docs/cli/kops_delete_cluster.md | 2 +- docs/cli/kops_delete_instance.md | 2 +- docs/cli/kops_delete_instancegroup.md | 2 +- docs/cli/kops_delete_secret.md | 2 +- 7 files changed, 22 insertions(+), 56 deletions(-) diff --git a/cmd/kops/delete.go b/cmd/kops/delete.go index 4a3f62ce1b235..7669fff4b56f2 100644 --- a/cmd/kops/delete.go +++ b/cmd/kops/delete.go @@ -30,7 +30,6 @@ import ( "k8s.io/kops/pkg/sshcredentials" "k8s.io/kops/util/pkg/text" "k8s.io/kops/util/pkg/vfs" - cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) @@ -41,59 +40,40 @@ type DeleteOptions struct { } var ( - deleteLong = templates.LongDesc(i18n.T(` - Delete Kubernetes clusters, instancegroups, instances, and secrets, or a combination of the before mentioned. - `)) - deleteExample = templates.Examples(i18n.T(` - # Delete an instance - kops delete instance i-0a5ed581b862d3425 - # Delete a cluster using a manifest file kops delete -f my-cluster.yaml # Delete a cluster using a pasted manifest file from stdin. pbpaste | kops delete -f - - - # Delete a cluster in AWS. - kops delete cluster --name=k8s.example.com --state=s3://my-state-store - - # Delete an instancegroup for the k8s-cluster.example.com cluster. - # The --yes option runs the command immediately. - kops delete ig --name=k8s-cluster.example.com node-example --yes `)) - deleteShort = i18n.T("Delete clusters, instancegroups, instances, or secrets.") + deleteShort = i18n.T("Delete clusters, instancegroups, instances, and secrets.") ) func NewCmdDelete(f *util.Factory, out io.Writer) *cobra.Command { options := &DeleteOptions{} cmd := &cobra.Command{ - Use: "delete -f FILENAME [--yes]", + Use: "delete {-f FILENAME}...", Short: deleteShort, - Long: deleteLong, Example: deleteExample, SuggestFor: []string{"rm"}, - Run: func(cmd *cobra.Command, args []string) { - ctx := context.TODO() - if len(options.Filenames) == 0 { - cmd.Help() - return - } - cmdutil.CheckErr(RunDelete(ctx, f, out, options)) + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return RunDelete(context.TODO(), f, out, options) }, } cmd.Flags().StringSliceVarP(&options.Filenames, "filename", "f", options.Filenames, "Filename to use to delete the resource") - cmd.Flags().BoolVarP(&options.Yes, "yes", "y", options.Yes, "Specify --yes to delete the resource") + cmd.Flags().BoolVarP(&options.Yes, "yes", "y", options.Yes, "Specify --yes to immediately delete the resource") cmd.MarkFlagRequired("filename") // create subcommands cmd.AddCommand(NewCmdDeleteCluster(f, out)) + cmd.AddCommand(NewCmdDeleteInstance(f, out)) cmd.AddCommand(NewCmdDeleteInstanceGroup(f, out)) cmd.AddCommand(NewCmdDeleteSecret(f, out)) - cmd.AddCommand(NewCmdDeleteInstance(f, out)) return cmd } @@ -108,12 +88,12 @@ func RunDelete(ctx context.Context, factory *util.Factory, out io.Writer, d *Del if f == "-" { contents, err = ConsumeStdin() if err != nil { - return fmt.Errorf("error reading from stdin: %v", err) + return fmt.Errorf("reading from stdin: %v", err) } } else { contents, err = vfs.Context.ReadFile(f) if err != nil { - return fmt.Errorf("error reading file %q: %v", f, err) + return fmt.Errorf("reading file %q: %v", f, err) } } @@ -121,7 +101,7 @@ func RunDelete(ctx context.Context, factory *util.Factory, out io.Writer, d *Del for _, section := range sections { o, gvk, err := kopscodecs.Decode(section, nil) if err != nil { - return fmt.Errorf("error parsing file %q: %v", f, err) + return fmt.Errorf("parsing file %q: %v", f, err) } switch v := o.(type) { @@ -132,7 +112,7 @@ func RunDelete(ctx context.Context, factory *util.Factory, out io.Writer, d *Del } err = RunDeleteCluster(ctx, factory, out, options) if err != nil { - exitWithError(err) + return err } deletedClusters.Insert(v.ObjectMeta.Name) case *kopsapi.InstanceGroup: @@ -150,7 +130,7 @@ func RunDelete(ctx context.Context, factory *util.Factory, out io.Writer, d *Del err := RunDeleteInstanceGroup(ctx, factory, out, options) if err != nil { - exitWithError(err) + return err } case *kopsapi.SSHCredential: fingerprint, err := sshcredentials.Fingerprint(v.Spec.PublicKey) @@ -167,11 +147,11 @@ func RunDelete(ctx context.Context, factory *util.Factory, out io.Writer, d *Del err = RunDeleteSecret(ctx, factory, out, options) if err != nil { - exitWithError(err) + return err } default: klog.V(2).Infof("Type of object was %T", v) - return fmt.Errorf("Unhandled kind %q in %s", gvk, f) + return fmt.Errorf("unhandled kind %q in %s", gvk, f) } } } diff --git a/docs/cli/kops.md b/docs/cli/kops.md index 0698617cb3c70..d2af771d2a206 100644 --- a/docs/cli/kops.md +++ b/docs/cli/kops.md @@ -39,7 +39,7 @@ kOps is Kubernetes Operations. * [kops completion](kops_completion.md) - generate the autocompletion script for the specified shell * [kops create](kops_create.md) - Create a resource by command line, filename or stdin. -* [kops delete](kops_delete.md) - Delete clusters, instancegroups, instances, or secrets. +* [kops delete](kops_delete.md) - Delete clusters, instancegroups, instances, and secrets. * [kops describe](kops_describe.md) - Describe a resource. * [kops distrust](kops_distrust.md) - Distrust keypairs. * [kops edit](kops_edit.md) - Edit clusters and other resources. diff --git a/docs/cli/kops_delete.md b/docs/cli/kops_delete.md index b05228a5b80b8..ed0723c0961d9 100644 --- a/docs/cli/kops_delete.md +++ b/docs/cli/kops_delete.md @@ -3,34 +3,20 @@ ## kops delete -Delete clusters, instancegroups, instances, or secrets. - -### Synopsis - -Delete Kubernetes clusters, instancegroups, instances, and secrets, or a combination of the before mentioned. +Delete clusters, instancegroups, instances, and secrets. ``` -kops delete -f FILENAME [--yes] [flags] +kops delete {-f FILENAME}... [flags] ``` ### Examples ``` - # Delete an instance - kops delete instance i-0a5ed581b862d3425 - # Delete a cluster using a manifest file kops delete -f my-cluster.yaml # Delete a cluster using a pasted manifest file from stdin. pbpaste | kops delete -f - - - # Delete a cluster in AWS. - kops delete cluster --name=k8s.example.com --state=s3://my-state-store - - # Delete an instancegroup for the k8s-cluster.example.com cluster. - # The --yes option runs the command immediately. - kops delete ig --name=k8s-cluster.example.com node-example --yes ``` ### Options @@ -38,7 +24,7 @@ kops delete -f FILENAME [--yes] [flags] ``` -f, --filename strings Filename to use to delete the resource -h, --help help for delete - -y, --yes Specify --yes to delete the resource + -y, --yes Specify --yes to immediately delete the resource ``` ### Options inherited from parent commands diff --git a/docs/cli/kops_delete_cluster.md b/docs/cli/kops_delete_cluster.md index 8bf888fff23a2..a855d4a11a92f 100644 --- a/docs/cli/kops_delete_cluster.md +++ b/docs/cli/kops_delete_cluster.md @@ -54,5 +54,5 @@ kops delete cluster CLUSTERNAME [--yes] [flags] ### SEE ALSO -* [kops delete](kops_delete.md) - Delete clusters, instancegroups, instances, or secrets. +* [kops delete](kops_delete.md) - Delete clusters, instancegroups, instances, and secrets. diff --git a/docs/cli/kops_delete_instance.md b/docs/cli/kops_delete_instance.md index de6f739f5911a..bab93fff49b61 100644 --- a/docs/cli/kops_delete_instance.md +++ b/docs/cli/kops_delete_instance.md @@ -64,5 +64,5 @@ kops delete instance [flags] ### SEE ALSO -* [kops delete](kops_delete.md) - Delete clusters, instancegroups, instances, or secrets. +* [kops delete](kops_delete.md) - Delete clusters, instancegroups, instances, and secrets. diff --git a/docs/cli/kops_delete_instancegroup.md b/docs/cli/kops_delete_instancegroup.md index b1d0423d64947..11a9317539625 100644 --- a/docs/cli/kops_delete_instancegroup.md +++ b/docs/cli/kops_delete_instancegroup.md @@ -52,5 +52,5 @@ kops delete instancegroup [flags] ### SEE ALSO -* [kops delete](kops_delete.md) - Delete clusters, instancegroups, instances, or secrets. +* [kops delete](kops_delete.md) - Delete clusters, instancegroups, instances, and secrets. diff --git a/docs/cli/kops_delete_secret.md b/docs/cli/kops_delete_secret.md index caee4f96bee59..c0d6c4a8e8991 100644 --- a/docs/cli/kops_delete_secret.md +++ b/docs/cli/kops_delete_secret.md @@ -50,5 +50,5 @@ kops delete secret [flags] ### SEE ALSO -* [kops delete](kops_delete.md) - Delete clusters, instancegroups, instances, or secrets. +* [kops delete](kops_delete.md) - Delete clusters, instancegroups, instances, and secrets. From b16b742b052ac45f11655236a7792893d841e5cd Mon Sep 17 00:00:00 2001 From: John Gardiner Myers Date: Thu, 8 Jul 2021 13:07:20 -0700 Subject: [PATCH 3/5] Implement completion for "kops delete cluster" --- cmd/kops/delete_cluster.go | 39 +++++++++++++++------------------ docs/cli/kops_delete_cluster.md | 6 ++--- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/cmd/kops/delete_cluster.go b/cmd/kops/delete_cluster.go index d6aba78b38970..933f142de57cb 100644 --- a/cmd/kops/delete_cluster.go +++ b/cmd/kops/delete_cluster.go @@ -26,6 +26,7 @@ import ( "k8s.io/klog/v2" "k8s.io/kops/cmd/kops/util" kopsapi "k8s.io/kops/pkg/apis/kops" + "k8s.io/kops/pkg/commands/commandutils" "k8s.io/kops/pkg/kubeconfig" "k8s.io/kops/pkg/resources" resourceops "k8s.io/kops/pkg/resources/ops" @@ -48,7 +49,7 @@ type DeleteClusterOptions struct { var ( deleteClusterLong = templates.LongDesc(i18n.T(` Deletes a Kubernetes cluster and all associated resources. Resources include instancegroups, - secrets and the state store. There is no "UNDO" for this command. + secrets, and the state store. There is no "UNDO" for this command. `)) deleteClusterExample = templates.Examples(i18n.T(` @@ -65,25 +66,14 @@ func NewCmdDeleteCluster(f *util.Factory, out io.Writer) *cobra.Command { options := &DeleteClusterOptions{} cmd := &cobra.Command{ - Use: "cluster CLUSTERNAME [--yes]", - Short: deleteClusterShort, - Long: deleteClusterLong, - Example: deleteClusterExample, - Run: func(cmd *cobra.Command, args []string) { - ctx := context.TODO() - - err := rootCommand.ProcessArgs(args) - if err != nil { - exitWithError(err) - } - - // Note _not_ ClusterName(); we only want the --name flag - options.ClusterName = rootCommand.clusterName - - err = RunDeleteCluster(ctx, f, out, options) - if err != nil { - exitWithError(err) - } + Use: "cluster [CLUSTER]", + Short: deleteClusterShort, + Long: deleteClusterLong, + Example: deleteClusterExample, + Args: rootCommand.clusterNameArgsNoKubeconfig(&options.ClusterName), + ValidArgsFunction: commandutils.CompleteClusterName(&rootCommand, true), + RunE: func(cmd *cobra.Command, args []string) error { + return RunDeleteCluster(context.TODO(), f, out, options) }, } @@ -91,7 +81,9 @@ func NewCmdDeleteCluster(f *util.Factory, out io.Writer) *cobra.Command { cmd.Flags().BoolVar(&options.Unregister, "unregister", options.Unregister, "Don't delete cloud resources, just unregister the cluster") cmd.Flags().BoolVar(&options.External, "external", options.External, "Delete an external cluster") - cmd.Flags().StringVar(&options.Region, "region", options.Region, "region") + cmd.Flags().StringVar(&options.Region, "region", options.Region, "External cluster's cloud region") + cmd.RegisterFlagCompletionFunc("region", completeRegion) + return cmd } @@ -214,3 +206,8 @@ func RunDeleteCluster(ctx context.Context, f *util.Factory, out io.Writer, optio fmt.Fprintf(out, "\nDeleted cluster: %q\n", clusterName) return nil } + +func completeRegion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // TODO call into cloud provider(s) to get list of valid regions + return nil, cobra.ShellCompDirectiveNoFileComp +} diff --git a/docs/cli/kops_delete_cluster.md b/docs/cli/kops_delete_cluster.md index a855d4a11a92f..23ce82d988d8c 100644 --- a/docs/cli/kops_delete_cluster.md +++ b/docs/cli/kops_delete_cluster.md @@ -7,10 +7,10 @@ Delete a cluster. ### Synopsis -Deletes a Kubernetes cluster and all associated resources. Resources include instancegroups, secrets and the state store. There is no "UNDO" for this command. +Deletes a Kubernetes cluster and all associated resources. Resources include instancegroups, secrets, and the state store. There is no "UNDO" for this command. ``` -kops delete cluster CLUSTERNAME [--yes] [flags] +kops delete cluster [CLUSTER] [flags] ``` ### Examples @@ -26,7 +26,7 @@ kops delete cluster CLUSTERNAME [--yes] [flags] ``` --external Delete an external cluster -h, --help help for cluster - --region string region + --region string External cluster's cloud region --unregister Don't delete cloud resources, just unregister the cluster -y, --yes Specify --yes to delete the cluster ``` From 56b57b5326bba0c6878472775671624a33476411 Mon Sep 17 00:00:00 2001 From: John Gardiner Myers Date: Fri, 9 Jul 2021 09:45:17 -0700 Subject: [PATCH 4/5] Implement completion for "kops delete instance" --- cmd/kops/delete_instance.go | 217 +++++++++++++++++++++---------- docs/cli/kops_delete.md | 2 +- docs/cli/kops_delete_instance.md | 12 +- 3 files changed, 158 insertions(+), 73 deletions(-) diff --git a/cmd/kops/delete_instance.go b/cmd/kops/delete_instance.go index cae9599073bfc..76d4ac3aba8cf 100644 --- a/cmd/kops/delete_instance.go +++ b/cmd/kops/delete_instance.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "os" + "strings" "time" "github.com/spf13/cobra" @@ -32,6 +33,7 @@ import ( "k8s.io/kops/cmd/kops/util" kopsapi "k8s.io/kops/pkg/apis/kops" "k8s.io/kops/pkg/cloudinstances" + "k8s.io/kops/pkg/commands/commandutils" "k8s.io/kops/pkg/instancegroups" "k8s.io/kops/pkg/validation" "k8s.io/kops/upup/pkg/fi/cloudup" @@ -40,7 +42,7 @@ import ( ) // DeleteInstanceOptions is the command Object for an instance deletion. -type deleteInstanceOptions struct { +type DeleteInstanceOptions struct { Yes bool CloudOnly bool @@ -70,7 +72,7 @@ type deleteInstanceOptions struct { Surge bool } -func (o *deleteInstanceOptions) initDefaults() { +func (o *DeleteInstanceOptions) initDefaults() { d := &RollingUpdateOptions{} d.InitDefaults() @@ -102,64 +104,56 @@ func NewCmdDeleteInstance(f *util.Factory, out io.Writer) *cobra.Command { kops delete instance --cloudonly i-0a5ed581b862d3425 --yes `)) - deleteInstanceShort := i18n.T(`Delete an instance`) + deleteInstanceShort := i18n.T(`Delete an instance.`) - var options deleteInstanceOptions + var options DeleteInstanceOptions options.initDefaults() cmd := &cobra.Command{ - Use: "instance", + Use: "instance INSTANCE|NODE", Short: deleteInstanceShort, Long: deleteInstanceLong, Example: deleteInstanceExample, + Args: func(cmd *cobra.Command, args []string) error { + options.ClusterName = rootCommand.ClusterName(true) + if options.ClusterName == "" { + return fmt.Errorf("--name is required") + } + + if len(args) == 0 { + return fmt.Errorf("must specify ID of instance or name of node to delete") + } + options.InstanceID = args[0] + + if len(args) != 1 { + return fmt.Errorf("can only delete one instance at a time") + } + + return nil + }, + ValidArgsFunction: completeInstanceOrNode(&options), + RunE: func(cmd *cobra.Command, args []string) error { + return RunDeleteInstance(context.TODO(), f, out, &options) + }, } - cmd.Flags().BoolVar(&options.CloudOnly, "cloudonly", options.CloudOnly, "Perform deletion update without confirming progress with k8s") + cmd.Flags().BoolVar(&options.CloudOnly, "cloudonly", options.CloudOnly, "Perform deletion update without confirming progress with Kubernetes") cmd.Flags().BoolVar(&options.Surge, "surge", options.Surge, "Surge by detaching the node from the ASG before deletion") cmd.Flags().DurationVar(&options.ValidationTimeout, "validation-timeout", options.ValidationTimeout, "Maximum time to wait for a cluster to validate") - cmd.Flags().Int32Var(&options.ValidateCount, "validate-count", options.ValidateCount, "Amount of times that a cluster needs to be validated after single node update") + cmd.Flags().Int32Var(&options.ValidateCount, "validate-count", options.ValidateCount, "Number of times that a cluster needs to be validated after single node update") cmd.Flags().DurationVar(&options.PostDrainDelay, "post-drain-delay", options.PostDrainDelay, "Time to wait after draining each node") - cmd.Flags().BoolVar(&options.FailOnDrainError, "fail-on-drain-error", true, "The deletion will fail if draining a node fails.") - cmd.Flags().BoolVar(&options.FailOnValidate, "fail-on-validate-error", true, "The deletion will fail if the cluster fails to validate.") + cmd.Flags().BoolVar(&options.FailOnDrainError, "fail-on-drain-error", true, "Fail if draining a node fails") + cmd.Flags().BoolVar(&options.FailOnValidate, "fail-on-validate-error", true, "Fail if the cluster fails to validate") cmd.Flags().BoolVarP(&options.Yes, "yes", "y", options.Yes, "Specify --yes to immediately delete the instance") - cmd.Run = func(cmd *cobra.Command, args []string) { - ctx := context.TODO() - - clusterName := rootCommand.ClusterName(true) - - if clusterName == "" { - exitWithError(fmt.Errorf("--name is required")) - return - } - - options.ClusterName = clusterName - if len(args) == 0 { - exitWithError(fmt.Errorf("specify ID of instance to delete")) - } - if len(args) != 1 { - exitWithError(fmt.Errorf("can only delete one instance at a time")) - } - - options.InstanceID = args[0] - - err := RunDeleteInstance(ctx, f, os.Stdout, &options) - if err != nil { - exitWithError(err) - return - } - - } - return cmd } -func RunDeleteInstance(ctx context.Context, f *util.Factory, out io.Writer, options *deleteInstanceOptions) error { - - clientset, err := f.Clientset() +func RunDeleteInstance(ctx context.Context, f *util.Factory, out io.Writer, options *DeleteInstanceOptions) error { + clientSet, err := f.Clientset() if err != nil { return err } @@ -169,36 +163,17 @@ func RunDeleteInstance(ctx context.Context, f *util.Factory, out io.Writer, opti return err } - contextName := cluster.ObjectMeta.Name - clientGetter := genericclioptions.NewConfigFlags(true) - clientGetter.Context = &contextName - - config, err := clientGetter.ToRESTConfig() - if err != nil { - return fmt.Errorf("cannot load kubecfg settings for %q: %v", contextName, err) - } - var nodes []v1.Node var k8sClient kubernetes.Interface + var host string if !options.CloudOnly { - k8sClient, err = kubernetes.NewForConfig(config) - if err != nil { - return fmt.Errorf("cannot build kube client for %q: %v", contextName, err) - } - - nodeList, err := k8sClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) + k8sClient, host, nodes, err = getNodes(ctx, cluster, true) if err != nil { - fmt.Fprintf(os.Stderr, "Unable to reach the kubernetes API.\n") - fmt.Fprintf(os.Stderr, "Use --cloudonly to do a deletion without confirming progress with the k8s API\n\n") - return fmt.Errorf("error listing nodes in cluster: %v", err) - } - - if nodeList != nil { - nodes = nodeList.Items + return err } } - list, err := clientset.InstanceGroupsFor(cluster).List(ctx, metav1.ListOptions{}) + list, err := clientSet.InstanceGroupsFor(cluster).List(ctx, metav1.ListOptions{}) if err != nil { return err } @@ -265,7 +240,7 @@ func RunDeleteInstance(ctx context.Context, f *util.Factory, out io.Writer, opti var clusterValidator validation.ClusterValidator if !options.CloudOnly { - clusterValidator, err = validation.NewClusterValidator(cluster, cloud, list, config.Host, k8sClient) + clusterValidator, err = validation.NewClusterValidator(cluster, cloud, list, host, k8sClient) if err != nil { return fmt.Errorf("cannot create cluster validator: %v", err) } @@ -275,12 +250,45 @@ func RunDeleteInstance(ctx context.Context, f *util.Factory, out io.Writer, opti return d.UpdateSingleInstance(cloudMember, options.Surge) } -func deleteNodeMatch(cloudMember *cloudinstances.CloudInstance, options *deleteInstanceOptions) bool { +func getNodes(ctx context.Context, cluster *kopsapi.Cluster, verbose bool) (kubernetes.Interface, string, []v1.Node, error) { + var nodes []v1.Node + var k8sClient kubernetes.Interface + + contextName := cluster.ObjectMeta.Name + clientGetter := genericclioptions.NewConfigFlags(true) + clientGetter.Context = &contextName + + config, err := clientGetter.ToRESTConfig() + if err != nil { + return nil, "", nil, fmt.Errorf("cannot load kubecfg settings for %q: %v", contextName, err) + } + + k8sClient, err = kubernetes.NewForConfig(config) + if err != nil { + return nil, "", nil, fmt.Errorf("cannot build kube client for %q: %v", contextName, err) + } + + nodeList, err := k8sClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) + if err != nil { + if verbose { + fmt.Fprintf(os.Stderr, "Unable to reach the kubernetes API.\n") + fmt.Fprintf(os.Stderr, "Use --cloudonly to do a deletion without confirming progress with the k8s API\n\n") + } + return nil, "", nil, fmt.Errorf("listing nodes in cluster: %v", err) + } + + if nodeList != nil { + nodes = nodeList.Items + } + return k8sClient, config.Host, nodes, nil +} + +func deleteNodeMatch(cloudMember *cloudinstances.CloudInstance, options *DeleteInstanceOptions) bool { return cloudMember.ID == options.InstanceID || (!options.CloudOnly && cloudMember.Node != nil && cloudMember.Node.Name == options.InstanceID) } -func findDeletionNode(groups map[string]*cloudinstances.CloudInstanceGroup, options *deleteInstanceOptions) *cloudinstances.CloudInstance { +func findDeletionNode(groups map[string]*cloudinstances.CloudInstanceGroup, options *DeleteInstanceOptions) *cloudinstances.CloudInstance { for _, group := range groups { for _, r := range group.Ready { if deleteNodeMatch(r, options) { @@ -295,3 +303,80 @@ func findDeletionNode(groups map[string]*cloudinstances.CloudInstanceGroup, opti } return nil } + +func completeInstanceOrNode(options *DeleteInstanceOptions) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) > 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + commandutils.ConfigureKlogForCompletion() + ctx := context.TODO() + + cluster, clientSet, completions, directive := GetClusterForCompletion(ctx, &rootCommand, nil) + if cluster == nil { + return completions, directive + } + + var nodes []v1.Node + var err error + if !options.CloudOnly { + _, _, nodes, err = getNodes(ctx, cluster, false) + if err != nil { + cobra.CompErrorln(err.Error()) + } + } + + list, err := clientSet.InstanceGroupsFor(cluster).List(ctx, metav1.ListOptions{}) + if err != nil { + return commandutils.CompletionError("listing instance groups", err) + } + + var instanceGroups []*kopsapi.InstanceGroup + for i := range list.Items { + instanceGroups = append(instanceGroups, &list.Items[i]) + } + + cloud, err := cloudup.BuildCloud(cluster) + if err != nil { + return commandutils.CompletionError("initializing cloud", err) + } + + groups, err := cloud.GetCloudGroups(cluster, instanceGroups, false, nodes) + if err != nil { + return commandutils.CompletionError("listing instances", err) + } + + completions = nil + longestGroup := 0 + for _, group := range groups { + if group.InstanceGroup != nil && longestGroup < len(group.InstanceGroup.Name) { + longestGroup = len(group.InstanceGroup.Name) + } + } + for _, group := range groups { + for _, instance := range group.Ready { + completions = appendInstance(completions, instance, longestGroup) + } + for _, instance := range group.NeedUpdate { + completions = appendInstance(completions, instance, longestGroup) + } + } + + return completions, cobra.ShellCompDirectiveNoFileComp + } +} + +func appendInstance(completions []string, instance *cloudinstances.CloudInstance, longestGroup int) []string { + completion := instance.ID + if instance.CloudInstanceGroup.InstanceGroup != nil { + completion += "\t" + instance.CloudInstanceGroup.InstanceGroup.Name + + if instance.Node != nil { + padding := strings.Repeat(" ", longestGroup+1-len(instance.CloudInstanceGroup.InstanceGroup.Name)) + completion += padding + instance.Node.Name + completions = append(completions, instance.Node.Name+"\t"+instance.CloudInstanceGroup.InstanceGroup.Name+padding+instance.ID) + } + } + return append(completions, completion) +} diff --git a/docs/cli/kops_delete.md b/docs/cli/kops_delete.md index ed0723c0961d9..7a0cb85ec0a8d 100644 --- a/docs/cli/kops_delete.md +++ b/docs/cli/kops_delete.md @@ -52,7 +52,7 @@ kops delete {-f FILENAME}... [flags] * [kops](kops.md) - kOps is Kubernetes Operations. * [kops delete cluster](kops_delete_cluster.md) - Delete a cluster. -* [kops delete instance](kops_delete_instance.md) - Delete an instance +* [kops delete instance](kops_delete_instance.md) - Delete an instance. * [kops delete instancegroup](kops_delete_instancegroup.md) - Delete instancegroup * [kops delete secret](kops_delete_secret.md) - Delete a secret diff --git a/docs/cli/kops_delete_instance.md b/docs/cli/kops_delete_instance.md index bab93fff49b61..0c608fe4ab6db 100644 --- a/docs/cli/kops_delete_instance.md +++ b/docs/cli/kops_delete_instance.md @@ -3,14 +3,14 @@ ## kops delete instance -Delete an instance +Delete an instance. ### Synopsis Delete an instance. By default, it will detach the instance from the instance group, drain it, then terminate it. ``` -kops delete instance [flags] +kops delete instance INSTANCE|NODE [flags] ``` ### Examples @@ -30,13 +30,13 @@ kops delete instance [flags] ### Options ``` - --cloudonly Perform deletion update without confirming progress with k8s - --fail-on-drain-error The deletion will fail if draining a node fails. (default true) - --fail-on-validate-error The deletion will fail if the cluster fails to validate. (default true) + --cloudonly Perform deletion update without confirming progress with Kubernetes + --fail-on-drain-error Fail if draining a node fails (default true) + --fail-on-validate-error Fail if the cluster fails to validate (default true) -h, --help help for instance --post-drain-delay duration Time to wait after draining each node (default 5s) --surge Surge by detaching the node from the ASG before deletion (default true) - --validate-count int32 Amount of times that a cluster needs to be validated after single node update (default 2) + --validate-count int32 Number of times that a cluster needs to be validated after single node update (default 2) --validation-timeout duration Maximum time to wait for a cluster to validate (default 15m0s) -y, --yes Specify --yes to immediately delete the instance ``` From ea8cd3b758311de1b683255b4b605c730fbef364 Mon Sep 17 00:00:00 2001 From: John Gardiner Myers Date: Fri, 9 Jul 2021 11:22:15 -0700 Subject: [PATCH 5/5] Implement completion for "kops delete instancegroup" --- cmd/kops/delete_instancegroup.go | 89 +++++++++++++++++---------- cmd/kops/rollingupdate_cluster.go | 14 +++-- docs/cli/kops_delete.md | 2 +- docs/cli/kops_delete_instancegroup.md | 6 +- 4 files changed, 69 insertions(+), 42 deletions(-) diff --git a/cmd/kops/delete_instancegroup.go b/cmd/kops/delete_instancegroup.go index ccc4d5d289d5b..d3a6e8fda3941 100644 --- a/cmd/kops/delete_instancegroup.go +++ b/cmd/kops/delete_instancegroup.go @@ -19,6 +19,7 @@ package main import ( "context" "fmt" + "strings" "io" "os" @@ -26,6 +27,7 @@ import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/kops/cmd/kops/util" + "k8s.io/kops/pkg/apis/kops" "k8s.io/kops/pkg/instancegroups" "k8s.io/kops/upup/pkg/fi/cloudup" "k8s.io/kops/util/pkg/ui" @@ -34,12 +36,12 @@ import ( ) var ( - deleteIgLong = templates.LongDesc(i18n.T(` - Delete an instancegroup configuration. kOps has the concept of "instance groups", + deleteInstanceGroupLong = templates.LongDesc(i18n.T(` + Delete an instance group configuration. kOps has the concept of "instance groups", which are a group of similar virtual machines. On AWS, they map to an - AutoScalingGroup. An ig work either as a Kubernetes master or a node.`)) + AutoScalingGroup.`)) - deleteIgExample = templates.Examples(i18n.T(` + deleteInstanceGroupExample = templates.Examples(i18n.T(` # Delete an instancegroup for the k8s-cluster.example.com cluster. # The --yes option runs the command immediately. @@ -47,7 +49,7 @@ var ( kops delete ig --name=k8s-cluster.example.com node-example --yes `)) - deleteIgShort = i18n.T(`Delete instancegroup`) + deleteInstanceGroupShort = i18n.T(`Delete instance group.`) ) type DeleteInstanceGroupOptions struct { @@ -60,28 +62,36 @@ func NewCmdDeleteInstanceGroup(f *util.Factory, out io.Writer) *cobra.Command { options := &DeleteInstanceGroupOptions{} cmd := &cobra.Command{ - Use: "instancegroup", + Use: "instancegroup INSTANCE_GROUP", Aliases: []string{"instancegroups", "ig"}, - Short: deleteIgShort, - Long: deleteIgLong, - Example: deleteIgExample, - Run: func(cmd *cobra.Command, args []string) { - ctx := context.TODO() + Short: deleteInstanceGroupShort, + Long: deleteInstanceGroupLong, + Example: deleteInstanceGroupExample, + Args: func(cmd *cobra.Command, args []string) error { + options.ClusterName = rootCommand.ClusterName(true) + + if options.ClusterName == "" { + return fmt.Errorf("--name is required") + } if len(args) == 0 { - exitWithError(fmt.Errorf("Specify name of instance group to delete")) + return fmt.Errorf("must specify the name of instance group to delete") } + + options.GroupName = args[0] + if len(args) != 1 { - exitWithError(fmt.Errorf("Can only edit one instance group at a time!")) + return fmt.Errorf("can only edit one instance group at a time") } - groupName := args[0] - options.GroupName = groupName - - options.ClusterName = rootCommand.ClusterName(true) + return nil + }, + ValidArgsFunction: completeInstanceGroup(nil, &[]string{strings.ToLower(string(kops.InstanceGroupRoleMaster))}), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.TODO() if !options.Yes { - message := fmt.Sprintf("Do you really want to delete instance group %q? This action cannot be undone.", groupName) + message := fmt.Sprintf("Do you really want to delete instance group %q? This action cannot be undone.", options.GroupName) c := &ui.ConfirmArgs{ Out: out, @@ -92,7 +102,7 @@ func NewCmdDeleteInstanceGroup(f *util.Factory, out io.Writer) *cobra.Command { confirmed, err := ui.GetConfirm(c) if err != nil { - exitWithError(err) + return err } if !confirmed { os.Exit(1) @@ -101,10 +111,7 @@ func NewCmdDeleteInstanceGroup(f *util.Factory, out io.Writer) *cobra.Command { } } - err := RunDeleteInstanceGroup(ctx, f, out, options) - if err != nil { - exitWithError(err) - } + return RunDeleteInstanceGroup(ctx, f, out, options) }, } @@ -123,12 +130,7 @@ func RunDeleteInstanceGroup(ctx context.Context, f *util.Factory, out io.Writer, return fmt.Errorf("GroupName is required") } - clusterName := options.ClusterName - if clusterName == "" { - return fmt.Errorf("ClusterName is required") - } - - cluster, err := GetCluster(ctx, f, clusterName) + cluster, err := GetCluster(ctx, f, options.ClusterName) if err != nil { return err } @@ -146,18 +148,37 @@ func RunDeleteInstanceGroup(ctx context.Context, f *util.Factory, out io.Writer, return fmt.Errorf("InstanceGroup %q not found", groupName) } - cloud, err := cloudup.BuildCloud(cluster) - if err != nil { - return err - } - fmt.Fprintf(out, "InstanceGroup %q found for deletion\n", groupName) + if group.Spec.Role == kops.InstanceGroupRoleMaster { + groups, err := clientset.InstanceGroupsFor(cluster).List(ctx, metav1.ListOptions{}) + if err != nil { + return fmt.Errorf("listing InstanceGroups: %v", err) + } + + onlyMaster := true + for _, ig := range groups.Items { + if ig.Name != groupName && ig.Spec.Role == kops.InstanceGroupRoleMaster { + onlyMaster = false + break + } + } + + if onlyMaster { + return fmt.Errorf("cannot delete the only control plane instance group") + } + } + if !options.Yes { fmt.Fprintf(out, "\nMust specify --yes to delete instancegroup\n") return nil } + cloud, err := cloudup.BuildCloud(cluster) + if err != nil { + return err + } + d := &instancegroups.DeleteInstanceGroup{} d.Cluster = cluster d.Cloud = cloud diff --git a/cmd/kops/rollingupdate_cluster.go b/cmd/kops/rollingupdate_cluster.go index 61bb2726ff58b..0d642f4770df2 100644 --- a/cmd/kops/rollingupdate_cluster.go +++ b/cmd/kops/rollingupdate_cluster.go @@ -190,7 +190,7 @@ func NewCmdRollingUpdateCluster(f *util.Factory, out io.Writer) *cobra.Command { cmd.Flags().DurationVar(&options.PostDrainDelay, "post-drain-delay", options.PostDrainDelay, "Time to wait after draining each node") cmd.Flags().BoolVarP(&options.Interactive, "interactive", "i", options.Interactive, "Prompt to continue after each instance is updated") cmd.Flags().StringSliceVar(&options.InstanceGroups, "instance-group", options.InstanceGroups, "Instance groups to update (defaults to all if not specified)") - cmd.RegisterFlagCompletionFunc("instance-group", completeInstanceGroup(&options)) + cmd.RegisterFlagCompletionFunc("instance-group", completeInstanceGroup(&options.InstanceGroups, &options.InstanceGroupRoles)) cmd.Flags().StringSliceVar(&options.InstanceGroupRoles, "instance-group-roles", options.InstanceGroupRoles, "Instance group roles to update ("+strings.Join(allRoles, ",")+")") cmd.RegisterFlagCompletionFunc("instance-group-roles", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return sets.NewString(allRoles...).Delete(options.InstanceGroupRoles...).List(), cobra.ShellCompDirectiveNoFileComp @@ -429,7 +429,7 @@ func RunRollingUpdateCluster(ctx context.Context, f *util.Factory, out io.Writer return d.RollingUpdate(groups, list) } -func completeInstanceGroup(options *RollingUpdateOptions) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { +func completeInstanceGroup(selectedInstanceGroups *[]string, selectedInstanceGroupRoles *[]string) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { commandutils.ConfigureKlogForCompletion() ctx := context.TODO() @@ -444,8 +444,14 @@ func completeInstanceGroup(options *RollingUpdateOptions) func(cmd *cobra.Comman return commandutils.CompletionError("listing instance groups", err) } - alreadySelected := sets.NewString(options.InstanceGroups...) - alreadySelectedRoles := sets.NewString(options.InstanceGroupRoles...) + alreadySelected := sets.NewString() + if selectedInstanceGroups != nil { + alreadySelected = alreadySelected.Insert(*selectedInstanceGroups...) + } + alreadySelectedRoles := sets.NewString() + if selectedInstanceGroupRoles != nil { + alreadySelectedRoles = alreadySelectedRoles.Insert(*selectedInstanceGroupRoles...) + } var igs []string for _, ig := range list.Items { if !alreadySelected.Has(ig.Name) && !alreadySelectedRoles.Has(strings.ToLower(string(ig.Spec.Role))) { diff --git a/docs/cli/kops_delete.md b/docs/cli/kops_delete.md index 7a0cb85ec0a8d..2efa10ea91915 100644 --- a/docs/cli/kops_delete.md +++ b/docs/cli/kops_delete.md @@ -53,6 +53,6 @@ kops delete {-f FILENAME}... [flags] * [kops](kops.md) - kOps is Kubernetes Operations. * [kops delete cluster](kops_delete_cluster.md) - Delete a cluster. * [kops delete instance](kops_delete_instance.md) - Delete an instance. -* [kops delete instancegroup](kops_delete_instancegroup.md) - Delete instancegroup +* [kops delete instancegroup](kops_delete_instancegroup.md) - Delete instance group. * [kops delete secret](kops_delete_secret.md) - Delete a secret diff --git a/docs/cli/kops_delete_instancegroup.md b/docs/cli/kops_delete_instancegroup.md index 11a9317539625..f3fe7df9e99a1 100644 --- a/docs/cli/kops_delete_instancegroup.md +++ b/docs/cli/kops_delete_instancegroup.md @@ -3,14 +3,14 @@ ## kops delete instancegroup -Delete instancegroup +Delete instance group. ### Synopsis -Delete an instancegroup configuration. kOps has the concept of "instance groups", which are a group of similar virtual machines. On AWS, they map to an AutoScalingGroup. An ig work either as a Kubernetes master or a node. +Delete an instance group configuration. kOps has the concept of "instance groups", which are a group of similar virtual machines. On AWS, they map to an AutoScalingGroup. ``` -kops delete instancegroup [flags] +kops delete instancegroup INSTANCE_GROUP [flags] ``` ### Examples