-
Notifications
You must be signed in to change notification settings - Fork 173
/
Copy pathwait.go
212 lines (176 loc) · 5.75 KB
/
wait.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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors
// Package utils provides generic helper functions.
package utils
import (
"errors"
"fmt"
"net"
"net/http"
"path"
"strconv"
"strings"
"time"
"github.com/zarf-dev/zarf/src/api/v1alpha1"
"github.com/zarf-dev/zarf/src/pkg/utils/exec"
"github.com/zarf-dev/zarf/src/pkg/message"
)
// isJSONPathWaitType checks if the condition is a JSONPath or condition.
func isJSONPathWaitType(condition string) bool {
if len(condition) == 0 || condition[0] != '{' || !strings.Contains(condition, "=") || !strings.Contains(condition, "}") {
return false
}
return true
}
// ExecuteWait executes the wait-for command.
func ExecuteWait(waitTimeout, waitNamespace, condition, kind, identifier string, timeout time.Duration) error {
// Handle network endpoints.
switch kind {
case "http", "https", "tcp":
return waitForNetworkEndpoint(kind, identifier, condition, timeout)
}
// Type of wait, condition or JSONPath
var waitType string
// Check if waitType is JSONPath or condition
if isJSONPathWaitType(condition) {
waitType = "jsonpath="
} else {
waitType = "condition="
}
// Get the Zarf command configuration.
zarfCommand, err := GetFinalExecutableCommand()
if err != nil {
return fmt.Errorf("could not locate the current Zarf binary path: %w", err)
}
identifierMsg := identifier
// If the identifier contains an equals sign, convert to a label selector.
if strings.ContainsRune(identifier, '=') {
identifierMsg = fmt.Sprintf(" with label `%s`", identifier)
identifier = fmt.Sprintf("-l %s", identifier)
}
// Set the timeout for the wait-for command.
expired := time.After(timeout)
// Set the custom message for optional namespace.
namespaceMsg := ""
namespaceFlag := ""
if waitNamespace != "" {
namespaceFlag = fmt.Sprintf("-n %s", waitNamespace)
namespaceMsg = fmt.Sprintf(" in namespace %s", waitNamespace)
}
// Setup the spinner messages.
conditionMsg := fmt.Sprintf("Waiting for %s%s%s to be %s.", kind, identifierMsg, namespaceMsg, condition)
existMsg := fmt.Sprintf("Waiting for %s%s to exist.", path.Join(kind, identifierMsg), namespaceMsg)
spinner := message.NewProgressSpinner(existMsg)
// Get the OS shell to execute commands in
shell, shellArgs := exec.GetOSShell(v1alpha1.Shell{Windows: "cmd"})
defer spinner.Stop()
for {
// Delay the check for 1 second
time.Sleep(time.Second)
select {
case <-expired:
return errors.New("wait timed out")
default:
spinner.Updatef(existMsg)
// Check if the resource exists.
zarfKubectlGet := fmt.Sprintf("%s tools kubectl get %s %s %s", zarfCommand, namespaceFlag, kind, identifier)
stdout, stderr, err := exec.Cmd(shell, append(shellArgs, zarfKubectlGet)...)
if err != nil {
message.Debug(stdout, stderr, err)
continue
}
resourceNotFound := strings.Contains(stderr, "No resources found") && identifier == ""
if resourceNotFound {
message.Debug(stdout, stderr, err)
continue
}
// If only checking for existence, exit here.
switch condition {
case "", "exist", "exists":
spinner.Success()
return nil
}
spinner.Updatef(conditionMsg)
// Wait for the resource to meet the given condition.
zarfKubectlWait := fmt.Sprintf("%s tools kubectl wait %s %s %s --for %s%s --timeout=%s",
zarfCommand, namespaceFlag, kind, identifier, waitType, condition, waitTimeout)
// If there is an error, log it and try again.
if stdout, stderr, err := exec.Cmd(shell, append(shellArgs, zarfKubectlWait)...); err != nil {
message.Debug(stdout, stderr, err)
continue
}
// And just like that, success!
spinner.Successf(conditionMsg)
return nil
}
}
}
// waitForNetworkEndpoint waits for a network endpoint to respond.
func waitForNetworkEndpoint(resource, name, condition string, timeout time.Duration) error {
// Set the timeout for the wait-for command.
expired := time.After(timeout)
// Setup the spinner messages.
condition = strings.ToLower(condition)
if condition == "" {
condition = "success"
}
spinner := message.NewProgressSpinner("Waiting for network endpoint %s://%s to respond %s.", resource, name, condition)
defer spinner.Stop()
delay := 100 * time.Millisecond
for {
// Delay the check for 100ms the first time and then 1 second after that.
time.Sleep(delay)
delay = time.Second
select {
case <-expired:
return errors.New("wait timed out")
default:
switch resource {
case "http", "https":
// Handle HTTP and HTTPS endpoints.
url := fmt.Sprintf("%s://%s", resource, name)
// Default to checking for a 2xx response.
if condition == "success" {
// Try to get the URL and check the status code.
resp, err := http.Get(url)
// If the status code is not in the 2xx range, try again.
if err != nil || resp.StatusCode < 200 || resp.StatusCode > 299 {
message.Debug(err)
continue
}
// Success, break out of the switch statement.
break
}
// Convert the condition to an int and check if it's a valid HTTP status code.
code, err := strconv.Atoi(condition)
if err != nil {
return fmt.Errorf("http status code %s is not an integer: %w", condition, err)
}
if http.StatusText(code) == "" {
return errors.New("http status code %s is unknown")
}
// Try to get the URL and check the status code.
resp, err := http.Get(url)
if err != nil || resp.StatusCode != code {
message.Debug(err)
continue
}
default:
// Fallback to any generic protocol using net.Dial
conn, err := net.Dial(resource, name)
if err != nil {
message.Debug(err)
continue
}
err = conn.Close()
if err != nil {
message.Debug(err)
continue
}
}
// Yay, we made it!
spinner.Success()
return nil
}
}
}