-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathwait.go
More file actions
159 lines (129 loc) · 4.45 KB
/
Copy pathwait.go
File metadata and controls
159 lines (129 loc) · 4.45 KB
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
150
151
152
153
154
155
156
157
158
159
package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/Jeffail/gabs/v2"
)
// Terminal states — the run is finished and will not change further.
var terminalStates = map[string]bool{
"applied": true,
"errored": true,
"discarded": true,
"canceled": true,
"planned_and_finished": true,
"force_canceled": true,
}
// States that definitely block on human input regardless of run configuration.
// A run sitting in these states will not progress without someone clicking approve.
var blockedOnApprovalStates = map[string]bool{
"policy_checked": true, // Soft-mandatory policy failed; needs override
"policy_override": true, // Override was requested; awaiting action
"cost_estimated": true, // Cost estimate produced; awaiting approval (if gated)
}
// Success states (exit code 0)
var successStates = map[string]bool{
"applied": true,
"planned_and_finished": true,
}
// waitForRun polls a run until it reaches a terminal state.
// Prints status transitions to stderr and exits with appropriate code.
func waitForRun(runID string, timeout time.Duration) {
if runID == "" {
fmt.Fprintln(os.Stderr, "Error: -run flag is required")
os.Exit(ExitError)
}
deadline := time.Now().Add(timeout)
interval := 2 * time.Second
maxInterval := 10 * time.Second
lastStatus := ""
fmt.Fprintf(os.Stderr, "Waiting for run %s...\n", runID)
for {
if time.Now().After(deadline) {
fmt.Fprintf(os.Stderr, "Error: Timeout waiting for run %s after %s\n", runID, timeout)
os.Exit(ExitTransientError)
}
status, runData := fetchRunStatus(runID)
if status != lastStatus {
if lastStatus != "" {
fmt.Fprintf(os.Stderr, "%s -> %s\n", lastStatus, status)
} else {
fmt.Fprintf(os.Stderr, "Status: %s\n", status)
}
lastStatus = status
}
if terminalStates[status] {
// Print the final run data to stdout
fmt.Println(runData.StringIndent("", " "))
if successStates[status] {
fmt.Fprintf(os.Stderr, "Run %s completed successfully (%s)\n", runID, status)
os.Exit(ExitSuccess)
} else {
fmt.Fprintf(os.Stderr, "Run %s finished with status: %s\n", runID, status)
os.Exit(ExitError)
}
}
// Hard stop: states that definitely block on human input regardless of config
// (policy override or cost approval).
if blockedOnApprovalStates[status] {
fmt.Println(runData.StringIndent("", " "))
fmt.Fprintf(os.Stderr, "Run %s is blocked waiting for approval (status: %s). Cannot proceed automatically.\n", runID, status)
os.Exit(ExitError)
}
// "planned" is ambiguous — the run auto-applies if auto-apply is on, otherwise
// it sits waiting for manual confirmation. Check the run's auto-apply flag.
// If auto-apply is off, the run is effectively blocked.
if status == "planned" {
autoApply, ok := runData.Path("auto-apply").Data().(bool)
if ok && !autoApply {
fmt.Println(runData.StringIndent("", " "))
fmt.Fprintf(os.Stderr, "Run %s requires manual confirmation (auto-apply is disabled). Cannot proceed automatically.\n", runID)
os.Exit(ExitError)
}
// Otherwise keep polling — the run will transition to confirmed/applying shortly.
}
// "confirmed" is a brief transitional state on the way to applying; just keep polling.
time.Sleep(interval)
// Backoff: increase interval up to max
if interval < maxInterval {
interval = interval + 1*time.Second
if interval > maxInterval {
interval = maxInterval
}
}
}
}
// fetchRunStatus makes a GET request to fetch the run and returns its status and parsed data.
func fetchRunStatus(runID string) (string, *gabs.Container) {
apiURL := "https://" + ScalrHostname + BasePath + "/runs/" + runID
req, err := http.NewRequest("GET", apiURL, nil)
checkErr(err)
setScalrHeaders(req)
res, err := doWithRetry(req)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: Request failed: %s\n", err)
os.Exit(ExitTransientError)
}
defer res.Body.Close()
resBody, err := io.ReadAll(res.Body)
checkErr(err)
if res.StatusCode >= 300 {
showError(resBody, res.StatusCode)
}
response, err := gabs.ParseJSON(resBody)
checkErr(err)
parsed := parseData(response)
// For single-object responses, parseData returns an array with one item
item := parsed.Search("0")
if item == nil {
fmt.Fprintln(os.Stderr, "Error: Unexpected response format")
os.Exit(ExitError)
}
status := ""
if item.Exists("status") {
status, _ = item.Path("status").Data().(string)
}
return status, item
}