github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/pkg/utils/wait.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: 2021-Present The Jackal Authors 3 4 // Package utils provides generic helper functions. 5 package utils 6 7 import ( 8 "fmt" 9 "net" 10 "net/http" 11 "path" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/Racer159/jackal/src/pkg/utils/exec" 17 18 "github.com/Racer159/jackal/src/config/lang" 19 "github.com/Racer159/jackal/src/pkg/message" 20 ) 21 22 // isJSONPathWaitType checks if the condition is a JSONPath or condition. 23 func isJSONPathWaitType(condition string) bool { 24 if len(condition) == 0 || condition[0] != '{' || !strings.Contains(condition, "=") || !strings.Contains(condition, "}") { 25 return false 26 } 27 28 return true 29 } 30 31 // ExecuteWait executes the wait-for command. 32 func ExecuteWait(waitTimeout, waitNamespace, condition, kind, identifier string, timeout time.Duration) error { 33 // Handle network endpoints. 34 switch kind { 35 case "http", "https", "tcp": 36 waitForNetworkEndpoint(kind, identifier, condition, timeout) 37 return nil 38 } 39 40 // Type of wait, condition or JSONPath 41 var waitType string 42 43 // Check if waitType is JSONPath or condition 44 if isJSONPathWaitType(condition) { 45 waitType = "jsonpath=" 46 } else { 47 waitType = "condition=" 48 } 49 50 // Get the Jackal command configuration. 51 jackalCommand, err := GetFinalExecutableCommand() 52 if err != nil { 53 message.Fatal(err, lang.CmdToolsWaitForErrJackalPath) 54 } 55 56 identifierMsg := identifier 57 58 // If the identifier contains an equals sign, convert to a label selector. 59 if strings.ContainsRune(identifier, '=') { 60 identifierMsg = fmt.Sprintf(" with label `%s`", identifier) 61 identifier = fmt.Sprintf("-l %s", identifier) 62 } 63 64 // Set the timeout for the wait-for command. 65 expired := time.After(timeout) 66 67 // Set the custom message for optional namespace. 68 namespaceMsg := "" 69 namespaceFlag := "" 70 if waitNamespace != "" { 71 namespaceFlag = fmt.Sprintf("-n %s", waitNamespace) 72 namespaceMsg = fmt.Sprintf(" in namespace %s", waitNamespace) 73 } 74 75 // Setup the spinner messages. 76 conditionMsg := fmt.Sprintf("Waiting for %s%s%s to be %s.", kind, identifierMsg, namespaceMsg, condition) 77 existMsg := fmt.Sprintf("Waiting for %s%s to exist.", path.Join(kind, identifierMsg), namespaceMsg) 78 spinner := message.NewProgressSpinner(existMsg) 79 80 // Get the OS shell to execute commands in 81 shell, shellArgs := exec.GetOSShell(exec.Shell{Windows: "cmd"}) 82 83 defer spinner.Stop() 84 85 for { 86 // Delay the check for 1 second 87 time.Sleep(time.Second) 88 89 select { 90 case <-expired: 91 message.Fatal(nil, lang.CmdToolsWaitForErrTimeout) 92 93 default: 94 spinner.Updatef(existMsg) 95 // Check if the resource exists. 96 jackalKubectlGet := fmt.Sprintf("%s tools kubectl get %s %s %s", jackalCommand, namespaceFlag, kind, identifier) 97 stdout, stderr, err := exec.Cmd(shell, append(shellArgs, jackalKubectlGet)...) 98 if err != nil { 99 message.Debug(stdout, stderr, err) 100 continue 101 } 102 103 resourceNotFound := strings.Contains(stderr, "No resources found") && identifier == "" 104 if resourceNotFound { 105 message.Debug(stdout, stderr, err) 106 continue 107 } 108 109 // If only checking for existence, exit here. 110 switch condition { 111 case "", "exist", "exists": 112 spinner.Success() 113 return nil 114 } 115 116 spinner.Updatef(conditionMsg) 117 // Wait for the resource to meet the given condition. 118 jackalKubectlWait := fmt.Sprintf("%s tools kubectl wait %s %s %s --for %s%s --timeout=%s", 119 jackalCommand, namespaceFlag, kind, identifier, waitType, condition, waitTimeout) 120 121 // If there is an error, log it and try again. 122 if stdout, stderr, err := exec.Cmd(shell, append(shellArgs, jackalKubectlWait)...); err != nil { 123 message.Debug(stdout, stderr, err) 124 continue 125 } 126 127 // And just like that, success! 128 spinner.Successf(conditionMsg) 129 return nil 130 } 131 } 132 } 133 134 // waitForNetworkEndpoint waits for a network endpoint to respond. 135 func waitForNetworkEndpoint(resource, name, condition string, timeout time.Duration) { 136 // Set the timeout for the wait-for command. 137 expired := time.After(timeout) 138 139 // Setup the spinner messages. 140 condition = strings.ToLower(condition) 141 if condition == "" { 142 condition = "success" 143 } 144 spinner := message.NewProgressSpinner("Waiting for network endpoint %s://%s to respond %s.", resource, name, condition) 145 defer spinner.Stop() 146 147 delay := 100 * time.Millisecond 148 149 for { 150 // Delay the check for 100ms the first time and then 1 second after that. 151 time.Sleep(delay) 152 delay = time.Second 153 154 select { 155 case <-expired: 156 message.Fatal(nil, lang.CmdToolsWaitForErrTimeout) 157 158 default: 159 switch resource { 160 161 case "http", "https": 162 // Handle HTTP and HTTPS endpoints. 163 url := fmt.Sprintf("%s://%s", resource, name) 164 165 // Default to checking for a 2xx response. 166 if condition == "success" { 167 // Try to get the URL and check the status code. 168 resp, err := http.Get(url) 169 170 // If the status code is not in the 2xx range, try again. 171 if err != nil || resp.StatusCode < 200 || resp.StatusCode > 299 { 172 message.Debug(err) 173 continue 174 } 175 176 // Success, break out of the switch statement. 177 break 178 } 179 180 // Convert the condition to an int and check if it's a valid HTTP status code. 181 code, err := strconv.Atoi(condition) 182 if err != nil || http.StatusText(code) == "" { 183 message.Fatal(err, lang.CmdToolsWaitForErrConditionString) 184 } 185 186 // Try to get the URL and check the status code. 187 resp, err := http.Get(url) 188 if err != nil || resp.StatusCode != code { 189 message.Debug(err) 190 continue 191 } 192 193 default: 194 // Fallback to any generic protocol using net.Dial 195 conn, err := net.Dial(resource, name) 196 if err != nil { 197 message.Debug(err) 198 continue 199 } 200 defer conn.Close() 201 } 202 203 // Yay, we made it! 204 spinner.Success() 205 return 206 } 207 } 208 }