cmd/hub/lifecycle/ready.go (162 lines of code) (raw):

// Copyright (c) 2022 EPAM Systems, Inc. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package lifecycle import ( "context" "fmt" "log" "net" "net/http" "strings" "time" "github.com/epam/hubctl/cmd/hub/config" "github.com/epam/hubctl/cmd/hub/manifest" "github.com/epam/hubctl/cmd/hub/parameters" "github.com/epam/hubctl/cmd/hub/util" ) func waitForReadyConditions(ctx context.Context, conditions []manifest.ReadyCondition, parameters parameters.LockedParameters, outputs parameters.CapturedOutputs, componentDepends []string) error { for _, condition := range conditions { err := waitForReadyCondition(ctx, condition, parameters, outputs, componentDepends) if err != nil { return err } } return nil } func expandReadyConditionParameter(what string, value string, componentDepends []string, kv map[string]interface{}) string { piggy := manifest.Parameter{Name: fmt.Sprintf("lifecycle.readyCondition.%s", what), Value: value} parameters.ExpandParameter(&piggy, componentDepends, kv) return util.String(piggy.Value) } const defaultReadyConditionWaitSeconds = 1200 func waitForReadyCondition(ctx context.Context, condition manifest.ReadyCondition, params parameters.LockedParameters, outputs parameters.CapturedOutputs, componentDepends []string) error { if condition.PauseSeconds > 0 { why := "" if config.Verbose { if condition.DNS != "" || condition.URL != "" { why = " before checking for ready condition(s)" } log.Printf("Sleeping %d seconds%s", condition.PauseSeconds, why) } select { case <-ctx.Done(): return context.Canceled case <-time.After(time.Duration(condition.PauseSeconds) * time.Second): } } if condition.DNS == "" && condition.URL == "" { return nil } wait := condition.WaitSeconds if wait <= 0 { wait = defaultReadyConditionWaitSeconds } kv := parameters.ParametersAndOutputsKV(params, outputs, nil) if condition.DNS != "" { fqdn := expandReadyConditionParameter("DNS", condition.DNS, componentDepends, kv) err := waitForFqdn(ctx, maybeStripPort(fqdn), wait) if err != nil { return err } } if condition.URL != "" { url := expandReadyConditionParameter("URL", condition.URL, componentDepends, kv) err := waitForUrl(ctx, url, wait) if err != nil { return err } } return nil } func maybeStripPort(fqdn string) string { i := strings.Index(fqdn, ":") if i > 0 { return fqdn[0:i] } return fqdn } func waitForFqdn(ctx context.Context, fqdn string, waitSeconds int) error { if config.Verbose { log.Printf("Waiting for `%s` in DNS to resolve to an accessible address", fqdn) } start := time.Now() lastMsg := "" for time.Since(start) < time.Duration(waitSeconds)*time.Second { addrs, err := net.DefaultResolver.LookupHost(ctx, fqdn) if config.Verbose { msg := "" if err != nil { msg = fmt.Sprintf("%v", err) } else { msg = fmt.Sprintf("Resolved `%s` into: %v", fqdn, addrs) } if config.Debug || (config.Verbose && lastMsg != msg) { log.Print(msg) lastMsg = msg } } if util.ContextCanceled(err) { return err } if err == nil && len(addrs) > 0 { addr := addrs[0] if len(addr) >= 7 && addr != "127.0.0.1" && addr != "1.0.0.1" { return nil } } time.Sleep(10 * time.Second) } return fmt.Errorf("Timeout waiting for `%s` to resolve", fqdn) } func waitForUrl(ctx context.Context, url string, waitSeconds int) error { if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { return fmt.Errorf("Only HTTP and HTTPS is supported in lifecycle.readyCondition.URL, expanded to `%s`", url) } if config.Verbose { log.Printf("Waiting for `%s` to respond", url) } interval := time.Duration(10) * time.Second client := util.RobustHttpClient(interval, true) client.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse } start := time.Now() lastMsg := "" for time.Since(start) < time.Duration(waitSeconds)*time.Second { req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return err } response, err := client.Do(req) if config.Verbose { msg := "" if err != nil { msg = fmt.Sprintf("%v", err) } else { if config.Trace { msg = fmt.Sprintf("`%s` responded with:\n\t%+v", url, response) } else { msg = fmt.Sprintf("`%s` responded with: %s", url, response.Status) } } if config.Debug || (config.Verbose && lastMsg != msg) { log.Print(msg) lastMsg = msg } } if util.ContextCanceled(err) { return err } if err == nil { response.Body.Close() if response.StatusCode >= 100 && response.StatusCode < 500 { return nil } if config.Verbose { log.Printf("`%s` responded with: %s", url, response.Status) } } time.Sleep(interval) } return fmt.Errorf("Timeout waiting for `%s` to respond", url) }