Skip to content
This repository was archived by the owner on Jan 7, 2025. It is now read-only.

Commit 489067f

Browse files
committed
Try gracefully shutting down before deleting
Signed-off-by: Johan Siebens <[email protected]>
1 parent ab34c89 commit 489067f

File tree

4 files changed

+123
-20
lines changed

4 files changed

+123
-20
lines changed

plugin/digitalocean.go

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"io/ioutil"
7+
"strconv"
78
"time"
89

910
"github.com/digitalocean/godo"
@@ -27,12 +28,12 @@ type dropletTemplate struct {
2728
userData string
2829
}
2930

30-
func (t *TargetPlugin) scaleOut(ctx context.Context, num int64, template *dropletTemplate, config map[string]string) error {
31-
log := t.logger.With("action", "scale_out", "tag", template.nodeClass, "count", num)
31+
func (t *TargetPlugin) scaleOut(ctx context.Context, desired, diff int64, template *dropletTemplate, config map[string]string) error {
32+
log := t.logger.With("action", "scale_out", "tag", template.nodeClass, "count", diff)
3233

3334
log.Debug("creating DigitalOcean droplets")
3435

35-
for i := int64(0); i < num; i++ {
36+
for i := int64(0); i < diff; i++ {
3637
createRequest := &godo.DropletCreateRequest{
3738
Name: template.nodeClass + "-" + randstr.String(6),
3839
Region: template.region,
@@ -65,7 +66,7 @@ func (t *TargetPlugin) scaleOut(ctx context.Context, num int64, template *drople
6566

6667
log.Debug("successfully created DigitalOcean droplets")
6768

68-
if err := t.ensureDropletsAreStable(ctx, template); err != nil {
69+
if err := t.ensureDropletsAreStable(ctx, template, desired); err != nil {
6970
return fmt.Errorf("failed to confirm scale out DigitalOcean droplets: %v", err)
7071
}
7172

@@ -74,9 +75,9 @@ func (t *TargetPlugin) scaleOut(ctx context.Context, num int64, template *drople
7475
return nil
7576
}
7677

77-
func (t *TargetPlugin) scaleIn(ctx context.Context, num int64, template *dropletTemplate, config map[string]string) error {
78+
func (t *TargetPlugin) scaleIn(ctx context.Context, desired, diff int64, template *dropletTemplate, config map[string]string) error {
7879

79-
ids, err := t.clusterUtils.RunPreScaleInTasks(ctx, config, int(num))
80+
ids, err := t.clusterUtils.RunPreScaleInTasks(ctx, config, int(diff))
8081
if err != nil {
8182
return fmt.Errorf("failed to perform pre-scale Nomad scale in tasks: %v", err)
8283
}
@@ -98,9 +99,9 @@ func (t *TargetPlugin) scaleIn(ctx context.Context, num int64, template *droplet
9899
return fmt.Errorf("failed to delete instances: %v", err)
99100
}
100101

101-
log.Debug("successfully deleted DigitalOcean droplets")
102+
log.Debug("successfully started deletion process")
102103

103-
if err := t.ensureDropletsAreStable(ctx, template); err != nil {
104+
if err := t.ensureDropletsAreStable(ctx, template, desired); err != nil {
104105
return fmt.Errorf("failed to confirm scale in DigitalOcean droplets: %v", err)
105106
}
106107

@@ -114,11 +115,11 @@ func (t *TargetPlugin) scaleIn(ctx context.Context, num int64, template *droplet
114115
return nil
115116
}
116117

117-
func (t *TargetPlugin) ensureDropletsAreStable(ctx context.Context, template *dropletTemplate) error {
118+
func (t *TargetPlugin) ensureDropletsAreStable(ctx context.Context, template *dropletTemplate, desired int64) error {
118119

119120
f := func(ctx context.Context) (bool, error) {
120-
total, active, err := t.countDroplets(ctx, template)
121-
if total == active || err != nil {
121+
_, active, err := t.countDroplets(ctx, template)
122+
if desired == active || err != nil {
122123
return true, err
123124
} else {
124125
return false, fmt.Errorf("waiting for droplets to become stable")
@@ -130,6 +131,7 @@ func (t *TargetPlugin) ensureDropletsAreStable(ctx context.Context, template *dr
130131

131132
func (t *TargetPlugin) deleteDroplets(ctx context.Context, tag string, instanceIDs map[string]bool) error {
132133
// create options. initially, these will be blank
134+
var dropletsToDelete []int
133135
opt := &godo.ListOptions{}
134136
for {
135137
droplets, resp, err := t.client.Droplets.ListByTag(ctx, tag, opt)
@@ -140,15 +142,19 @@ func (t *TargetPlugin) deleteDroplets(ctx context.Context, tag string, instanceI
140142
for _, d := range droplets {
141143
_, ok := instanceIDs[d.Name]
142144
if ok {
143-
_, err := t.client.Droplets.Delete(ctx, d.ID)
144-
if err != nil {
145-
return err
146-
}
145+
go func(dropletId int) {
146+
log := t.logger.With("action", "delete", "droplet_id", strconv.Itoa(dropletId))
147+
err := shutdownDroplet(dropletId, t.client, log)
148+
if err != nil {
149+
log.Error("error deleting droplet", err)
150+
}
151+
}(d.ID)
152+
dropletsToDelete = append(dropletsToDelete, d.ID)
147153
}
148154
}
149155

150-
// if we are at the last page, break out the for loop
151-
if resp.Links == nil || resp.Links.IsLastPage() {
156+
// if we deleted all droplets or if we are at the last page, break out the for loop
157+
if len(dropletsToDelete) == len(instanceIDs) || resp.Links == nil || resp.Links.IsLastPage() {
152158
break
153159
}
154160

plugin/plugin.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,13 @@ func (t *TargetPlugin) Scale(action sdk.ScalingAction, config map[string]string)
123123
return fmt.Errorf("failed to describe DigitalOcedroplets: %v", err)
124124
}
125125

126-
num, direction := t.calculateDirection(total, action.Count)
126+
diff, direction := t.calculateDirection(total, action.Count)
127127

128128
switch direction {
129129
case "in":
130-
err = t.scaleIn(ctx, num, template, config)
130+
err = t.scaleIn(ctx, action.Count, diff, template, config)
131131
case "out":
132-
err = t.scaleOut(ctx, num, template, config)
132+
err = t.scaleOut(ctx, action.Count, diff, template, config)
133133
default:
134134
t.logger.Info("scaling not required", "tag", template.nodeClass,
135135
"current_count", total, "strategy_count", action.Count)

plugin/shutdown.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package plugin
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/digitalocean/godo"
9+
"github.com/hashicorp/go-hclog"
10+
)
11+
12+
func shutdownDroplet(
13+
dropletId int,
14+
client *godo.Client,
15+
log hclog.Logger) error {
16+
17+
// Gracefully power off the droplet.
18+
log.Debug("Gracefully shutting down droplet...")
19+
_, _, err := client.DropletActions.PowerOff(context.TODO(), dropletId)
20+
if err != nil {
21+
// If we get an error the first time, actually report it
22+
return fmt.Errorf("error shutting down droplet: %s", err)
23+
}
24+
25+
err = waitForDropletState("off", dropletId, client, log, 5*time.Minute)
26+
if err != nil {
27+
log.Warn("Timeout while waiting to for droplet to become 'off'")
28+
}
29+
30+
log.Debug("Deleting Droplet...")
31+
_, err = client.Droplets.Delete(context.TODO(), dropletId)
32+
if err != nil {
33+
return fmt.Errorf("error deleting droplet: %s", err)
34+
}
35+
36+
return nil
37+
}

plugin/wait.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package plugin
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/digitalocean/godo"
9+
"github.com/hashicorp/go-hclog"
10+
)
11+
12+
func waitForDropletState(
13+
desiredState string, dropletId int,
14+
client *godo.Client,
15+
log hclog.Logger,
16+
timeout time.Duration) error {
17+
done := make(chan struct{})
18+
defer close(done)
19+
20+
result := make(chan error, 1)
21+
go func() {
22+
attempts := 0
23+
for {
24+
attempts += 1
25+
26+
log.Debug(fmt.Sprintf("Checking droplet status... (attempt: %d)", attempts))
27+
droplet, _, err := client.Droplets.Get(context.TODO(), dropletId)
28+
if err != nil {
29+
result <- err
30+
return
31+
}
32+
33+
if droplet.Status == desiredState {
34+
result <- nil
35+
return
36+
}
37+
38+
// Wait 3 seconds in between
39+
time.Sleep(3 * time.Second)
40+
41+
// Verify we shouldn't exit
42+
select {
43+
case <-done:
44+
// We finished, so just exit the goroutine
45+
return
46+
default:
47+
// Keep going
48+
}
49+
}
50+
}()
51+
52+
log.Debug(fmt.Sprintf("Waiting for up to %d seconds for droplet to become %s", timeout/time.Second, desiredState))
53+
select {
54+
case err := <-result:
55+
return err
56+
case <-time.After(timeout):
57+
err := fmt.Errorf("timeout while waiting to for droplet to become '%s'", desiredState)
58+
return err
59+
}
60+
}

0 commit comments

Comments
 (0)