diff --git a/cmd/kops/delete_instancegroup.go b/cmd/kops/delete_instancegroup.go new file mode 100644 index 0000000000..22d56ed512 --- /dev/null +++ b/cmd/kops/delete_instancegroup.go @@ -0,0 +1,81 @@ +package main + +import ( + "fmt" + + "github.com/golang/glog" + "github.com/spf13/cobra" + "k8s.io/kops/upup/pkg/fi/cloudup" + "k8s.io/kops/upup/pkg/kutil" +) + +type DeleteInstanceceGroupCmd struct { +} + +var deleteInstanceceGroupCmd DeleteInstanceceGroupCmd + +func init() { + cmd := &cobra.Command{ + Use: "instancegroup", + Aliases: []string{"instancegroups", "ig"}, + Short: "Delete instancegroup", + Long: `Delete an instancegroup configuration.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + glog.Exitf("Specify name of instance group to delete") + } + if len(args) != 1 { + glog.Exitf("Can only edit one instance group at a time!") + } + err := deleteInstanceceGroupCmd.Run(args[0]) + if err != nil { + glog.Exitf("%v", err) + } + }, + } + + deleteCmd.AddCommand(cmd) +} + +func (c *DeleteInstanceceGroupCmd) Run(groupName string) error { + if groupName == "" { + return fmt.Errorf("name is required") + } + + _, cluster, err := rootCommand.Cluster() + if err != nil { + return err + } + + registry, err := rootCommand.InstanceGroupRegistry() + if err != nil { + return err + } + + group, err := registry.Find(groupName) + if err != nil { + return fmt.Errorf("error reading InstanceGroup %q: %v", groupName, err) + } + if group == nil { + return fmt.Errorf("InstanceGroup %q not found", groupName) + } + + cloud, err := cloudup.BuildCloud(cluster) + if err != nil { + return err + } + + d := &kutil.DeleteInstanceGroup{} + d.Cluster = cluster + d.Cloud = cloud + d.InstanceGroupRegistry = registry + + err = d.DeleteInstanceGroup(group) + if err != nil { + return err + } + + fmt.Printf("InstanceGroup %q deleted\n", group.Name) + + return nil +} diff --git a/cmd/kops/rollingupdate_cluster.go b/cmd/kops/rollingupdate_cluster.go index 52e2ecb66f..e3dfb2a955 100644 --- a/cmd/kops/rollingupdate_cluster.go +++ b/cmd/kops/rollingupdate_cluster.go @@ -61,11 +61,10 @@ func (c *RollingUpdateClusterCmd) Run() error { } d := &kutil.RollingUpdateCluster{} - - d.Cluster = cluster d.Cloud = cloud - groups, err := d.ListInstanceGroups(instancegroups) + warnUnmatched := true + groups, err := kutil.FindCloudInstanceGroups(cloud, cluster, instancegroups, warnUnmatched) if err != nil { return err } @@ -84,12 +83,18 @@ func (c *RollingUpdateClusterCmd) Run() error { t.AddColumn("READY", func(r *kutil.CloudInstanceGroup) string { return strconv.Itoa(len(r.Ready)) }) + t.AddColumn("MIN", func(r *kutil.CloudInstanceGroup) string { + return strconv.Itoa(r.MinSize()) + }) + t.AddColumn("MAX", func(r *kutil.CloudInstanceGroup) string { + return strconv.Itoa(r.MaxSize()) + }) var l []*kutil.CloudInstanceGroup for _, v := range groups { l = append(l, v) } - err := t.Render(l, os.Stdout, "NAME", "STATUS", "NEEDUPDATE", "READY") + err := t.Render(l, os.Stdout, "NAME", "STATUS", "NEEDUPDATE", "READY", "MIN", "MAX") if err != nil { return err } diff --git a/docs/instance_groups.md b/docs/instance_groups.md index 41865688b7..962438e857 100644 --- a/docs/instance_groups.md +++ b/docs/instance_groups.md @@ -33,6 +33,7 @@ To preview the change: > `kops create cluster --name --dryrun` ``` +... Will modify resources: *awstasks.LaunchConfiguration launchConfiguration/mycluster.mydomain.com InstanceType t2.medium -> t2.large @@ -66,14 +67,54 @@ The procedure to resize an instance group works the same way: ## Creating a new instance group -Suppose you want to add a new group of nodes -TODO +Suppose you want to add a new group of nodes, perhaps with a different instance type. You do this using +`kops create ig `. Currently it opens an editor with a skeleton configuration, allowing +you to edit it before creation. -## Deleting an instance group +So the procedure is: -TODO +* `kops create ig morenodes`, edit and save +* Preview: `kops create cluster --name --dryrun` +* Apply: `kops create cluster --name ` +* (no instances need to be relaunched, so no rolling-update is needed) ## Converting an instance group to use spot instances -TODO +Follow the normal procedure for reconfiguring an InstanceGroup, but set the maxPrice property to your bid. +For example, "0.10" represents a spot-price bid of $0.10 (10 cents) per hour. +Warning: the t2 family is not currently supported with spot pricing. You'll need to choose a different +instance type. + +An example spec looks like this: + +``` +metadata: + creationTimestamp: "2016-07-10T15:47:14Z" + name: nodes +spec: + machineType: m3.medium + maxPrice: "0.1" + maxSize: 3 + minSize: 3 + role: Node +``` + +($0.10 per hour is a huge over-bid for an m3.medium - this is only an example!) + +So the procedure is: + +* Edit: `kops edit ig nodes` +* Preview: `kops create cluster --name --dryrun` +* Apply: `kops create cluster --name ` +* Rolling-update, only if you want to apply changes immediately: `kops rolling-update cluster` + + +## Deleting an instance group + +If you decide you don't need an InstanceGroup any more, you delete it using: `kops delete ig ` + +Example: `kops delete ig morenodes` + +No rolling-update is needed (and note this is not currently graceful, so there may be interruptions to +workloads where the pods are running on those nodes). diff --git a/upup/pkg/api/registry.go b/upup/pkg/api/registry.go index 6e10453f04..61b7591d71 100644 --- a/upup/pkg/api/registry.go +++ b/upup/pkg/api/registry.go @@ -182,6 +182,18 @@ func (r *InstanceGroupRegistry) Find(name string) (*InstanceGroup, error) { return group, nil } +func (r *InstanceGroupRegistry) Delete(name string) (bool, error) { + p := r.stateStore.VFSPath().Join("instancegroup", name) + err := p.Remove() + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, fmt.Errorf("error deleting instancegroup configuration %q: %v", name, err) + } + return true, nil +} + func (r *InstanceGroupRegistry) ReadAll() ([]*InstanceGroup, error) { names, err := r.List() if err != nil { diff --git a/upup/pkg/kutil/delete_instancegroup.go b/upup/pkg/kutil/delete_instancegroup.go new file mode 100644 index 0000000000..f54f63c463 --- /dev/null +++ b/upup/pkg/kutil/delete_instancegroup.go @@ -0,0 +1,38 @@ +package kutil + +import ( + "fmt" + + "k8s.io/kops/upup/pkg/api" + "k8s.io/kops/upup/pkg/fi" +) + +// DeleteInstanceGroup removes the cloud resources for an InstanceGroup +type DeleteInstanceGroup struct { + Cluster *api.Cluster + Cloud fi.Cloud + InstanceGroupRegistry *api.InstanceGroupRegistry +} + +func (c *DeleteInstanceGroup) DeleteInstanceGroup(group *api.InstanceGroup) error { + groups, err := FindCloudInstanceGroups(c.Cloud, c.Cluster, []*api.InstanceGroup{group}, false) + cig := groups[group.Name] + if cig == nil { + return fmt.Errorf("InstanceGroup not found in cloud") + } + if len(groups) != 1 { + return fmt.Errorf("Multiple InstanceGroup resources found in cloud") + } + + err = cig.Delete(c.Cloud) + if err != nil { + return fmt.Errorf("error deleting cloud resources for InstanceGroup: %v", err) + } + + _, err = c.InstanceGroupRegistry.Delete(group.Name) + if err != nil { + return err + } + + return nil +} diff --git a/upup/pkg/kutil/rollingupdate_cluster.go b/upup/pkg/kutil/rollingupdate_cluster.go index 159516c899..414e13862a 100644 --- a/upup/pkg/kutil/rollingupdate_cluster.go +++ b/upup/pkg/kutil/rollingupdate_cluster.go @@ -15,18 +15,17 @@ import ( // RollingUpdateCluster restarts cluster nodes type RollingUpdateCluster struct { - Cluster *api.Cluster - Cloud fi.Cloud + Cloud fi.Cloud } -func (c *RollingUpdateCluster) ListInstanceGroups(instancegroups []*api.InstanceGroup) (map[string]*CloudInstanceGroup, error) { - cloud := c.Cloud.(*awsup.AWSCloud) +func FindCloudInstanceGroups(cloud fi.Cloud, cluster *api.Cluster, instancegroups []*api.InstanceGroup, warnUnmatched bool) (map[string]*CloudInstanceGroup, error) { + awsCloud := cloud.(*awsup.AWSCloud) groups := make(map[string]*CloudInstanceGroup) - tags := cloud.Tags() + tags := awsCloud.Tags() - asgs, err := findAutoscalingGroups(cloud, tags) + asgs, err := findAutoscalingGroups(awsCloud, tags) if err != nil { return nil, err } @@ -38,9 +37,9 @@ func (c *RollingUpdateCluster) ListInstanceGroups(instancegroups []*api.Instance var asgName string switch g.Spec.Role { case api.InstanceGroupRoleMaster: - asgName = g.Name + ".masters." + c.Cluster.Name + asgName = g.Name + ".masters." + cluster.Name case api.InstanceGroupRoleNode: - asgName = g.Name + "." + c.Cluster.Name + asgName = g.Name + "." + cluster.Name default: glog.Warningf("Ignoring InstanceGroup of unknown role %q", g.Spec.Role) continue @@ -54,7 +53,9 @@ func (c *RollingUpdateCluster) ListInstanceGroups(instancegroups []*api.Instance } } if instancegroup == nil { - glog.Warningf("Found ASG with no corresponding instance group: %q", name) + if warnUnmatched { + glog.Warningf("Found ASG with no corresponding instance group: %q", name) + } continue } group := buildCloudInstanceGroup(instancegroup, asg) @@ -107,12 +108,23 @@ type CloudInstanceGroup struct { Status string Ready []*autoscaling.Instance NeedUpdate []*autoscaling.Instance + + asg *autoscaling.Group +} + +func (c *CloudInstanceGroup) MinSize() int { + return int(aws.Int64Value(c.asg.MinSize)) +} + +func (c *CloudInstanceGroup) MaxSize() int { + return int(aws.Int64Value(c.asg.MaxSize)) } func buildCloudInstanceGroup(ig *api.InstanceGroup, g *autoscaling.Group) *CloudInstanceGroup { n := &CloudInstanceGroup{ ASGName: aws.StringValue(g.AutoScalingGroupName), InstanceGroup: ig, + asg: g, } findLaunchConfigurationName := aws.StringValue(g.LaunchConfigurationName) @@ -163,6 +175,41 @@ func (n *CloudInstanceGroup) RollingUpdate(cloud fi.Cloud) error { return nil } +func (g *CloudInstanceGroup) Delete(cloud fi.Cloud) error { + c := cloud.(*awsup.AWSCloud) + + // TODO: Graceful? + + // Delete ASG + { + asgName := aws.StringValue(g.asg.AutoScalingGroupName) + glog.V(2).Infof("Deleting autoscaling group %q", asgName) + request := &autoscaling.DeleteAutoScalingGroupInput{ + AutoScalingGroupName: g.asg.AutoScalingGroupName, + ForceDelete: aws.Bool(true), + } + _, err := c.Autoscaling.DeleteAutoScalingGroup(request) + if err != nil { + return fmt.Errorf("error deleting autoscaling group %q: %v", asgName, err) + } + } + + // Delete LaunchConfig + { + lcName := aws.StringValue(g.asg.LaunchConfigurationName) + glog.V(2).Infof("Deleting autoscaling launch configuration %q", lcName) + request := &autoscaling.DeleteLaunchConfigurationInput{ + LaunchConfigurationName: g.asg.LaunchConfigurationName, + } + _, err := c.Autoscaling.DeleteLaunchConfiguration(request) + if err != nil { + return fmt.Errorf("error deleting autoscaling launch configuration %q: %v", lcName, err) + } + } + + return nil +} + func (n *CloudInstanceGroup) String() string { return "CloudInstanceGroup:" + n.ASGName } diff --git a/upup/pkg/kutil/utils.go b/upup/pkg/kutil/utils.go index ed704c4ebe..36cae5480f 100644 --- a/upup/pkg/kutil/utils.go +++ b/upup/pkg/kutil/utils.go @@ -43,7 +43,6 @@ func findAutoscalingGroups(cloud *awsup.AWSCloud, tags map[string]string) ([]*au if err != nil { return nil, fmt.Errorf("error listing autoscaling cluster tags: %v", err) } - } if len(asgNames) != 0 {