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  }