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