-
Notifications
You must be signed in to change notification settings - Fork 239
/
ephemeral.go
149 lines (123 loc) · 4.13 KB
/
ephemeral.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package machine
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"github.com/superfly/flyctl/api"
"github.com/superfly/flyctl/flaps"
"github.com/superfly/flyctl/internal/ctrlc"
"github.com/superfly/flyctl/internal/spinner"
"github.com/superfly/flyctl/iostreams"
"github.com/superfly/flyctl/terminal"
)
type EphemeralInput struct {
LaunchInput api.LaunchMachineInput
What string
}
func LaunchEphemeral(ctx context.Context, input *EphemeralInput) (*api.Machine, func(), error) {
var (
io = iostreams.FromContext(ctx)
colorize = io.ColorScheme()
flapsClient = flaps.FromContext(ctx)
)
if !input.LaunchInput.Config.AutoDestroy {
return nil, nil, errors.New("ephemeral machines must be configured to auto-destroy (this is a bug)")
}
machine, err := flapsClient.Launch(ctx, input.LaunchInput)
if err != nil {
return nil, nil, err
}
creationMsg := "Created an ephemeral machine " + colorize.Bold(machine.ID)
if input.What != "" {
creationMsg += " " + input.What
}
fmt.Fprintf(io.Out, "%s.\n", creationMsg)
sp := spinner.Run(io, fmt.Sprintf("Waiting for %s to start ...", colorize.Bold(machine.ID)))
defer sp.Stop()
const waitTimeout = 15 * time.Second
var flapsErr *flaps.FlapsError
t := time.NewTicker(time.Second)
defer t.Stop()
for {
err = flapsClient.Wait(ctx, machine, api.MachineStateStarted, waitTimeout)
if err == nil {
return machine, makeCleanupFunc(ctx, machine), nil
}
if errors.As(err, &flapsErr) && flapsErr.ResponseStatusCode == http.StatusRequestTimeout {
// The machine may not be ready yet.
} else {
break
}
select {
case <-ctx.Done():
terminal.Warn("You may need to destroy the machine manually (`fly machine destroy`).")
return nil, nil, ctx.Err()
case <-t.C:
}
}
var destroyed bool
if flapsErr != nil && flapsErr.ResponseStatusCode == http.StatusNotFound {
destroyed, err = checkMachineDestruction(ctx, machine, err)
}
if !destroyed {
terminal.Warn("You may need to destroy the machine manually (`fly machine destroy`).")
}
return nil, nil, err
}
func checkMachineDestruction(ctx context.Context, machine *api.Machine, firstErr error) (bool, error) {
flapsClient := flaps.FromContext(ctx)
machine, err := flapsClient.Get(ctx, machine.ID)
if err != nil {
return false, fmt.Errorf("failed to check status of machine: %w", err)
}
if machine.State != api.MachineStateDestroyed && machine.State != api.MachineStateDestroying {
return false, firstErr
}
var exitEvent *api.MachineEvent
for _, event := range machine.Events {
if event.Type == "exit" {
exitEvent = event
break
}
}
if exitEvent == nil || exitEvent.Request == nil {
return true, errors.New("machine was destroyed unexpectedly")
}
exitCode, err := exitEvent.Request.GetExitCode()
if err != nil {
return true, errors.New("machine exited unexpectedly")
}
return true, fmt.Errorf("machine exited unexpectedly with code %v", exitCode)
}
func makeCleanupFunc(ctx context.Context, machine *api.Machine) func() {
var (
io = iostreams.FromContext(ctx)
colorize = io.ColorScheme()
flapsClient = flaps.FromContext(ctx)
)
return func() {
const stopTimeout = 15 * time.Second
stopCtx, cancel := context.WithTimeout(context.Background(), stopTimeout)
stopCtx, cancel = ctrlc.HookCancelableContext(stopCtx, cancel)
defer cancel()
stopInput := api.StopMachineInput{
ID: machine.ID,
Timeout: api.Duration{Duration: stopTimeout},
}
if err := flapsClient.Stop(stopCtx, stopInput, ""); err != nil {
terminal.Warnf("Failed to stop ephemeral machine: %v", err)
terminal.Warn("You may need to destroy it manually (`fly machine destroy`).")
return
}
fmt.Fprintf(io.Out, "Waiting for ephemeral machine %s to be destroyed ...", colorize.Bold(machine.ID))
if err := flapsClient.Wait(stopCtx, machine, api.MachineStateDestroyed, stopTimeout); err != nil {
fmt.Fprintf(io.Out, " %s!\n", colorize.Red("failed"))
terminal.Warnf("Failed to wait for ephemeral machine to be destroyed: %v", err)
terminal.Warn("You may need to destroy it manually (`fly machine destroy`).")
} else {
fmt.Fprintf(io.Out, " %s.\n", colorize.Green("done"))
}
}
}