github.com/blend/go-sdk@v1.20220411.3/envoyutil/wait.go (about) 1 /* 2 3 Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file. 5 6 */ 7 8 package envoyutil 9 10 import ( 11 "context" 12 "fmt" 13 "io" 14 "net/http" 15 "strings" 16 "sync/atomic" 17 "time" 18 19 "github.com/blend/go-sdk/env" 20 "github.com/blend/go-sdk/ex" 21 "github.com/blend/go-sdk/logger" 22 "github.com/blend/go-sdk/retry" 23 ) 24 25 // NOTE: Ensure that 26 // - `http.Client` satisfies `HTTPGetClient` 27 // - `WaitForAdmin.executeOnce` satisfies `retry.Action` 28 var ( 29 _ HTTPGetClient = (*http.Client)(nil) 30 _ retry.ActionerFunc = (*WaitForAdmin)(nil).executeOnce 31 ) 32 33 var ( 34 // ErrFailedAttempt is an error class returned when Envoy fails to be 35 // ready on a single attempt. 36 ErrFailedAttempt = ex.Class("Envoy not yet ready") 37 // ErrTimedOut is an error class returned when Envoy fails to be ready 38 // after exhausting all attempts. 39 ErrTimedOut = ex.Class("Timed out waiting for Envoy to be ready") 40 ) 41 42 const ( 43 // EnvVarWaitFlag is an environment variable which specifies whether 44 // a wait function should wait for the Envoy Admin API to be ready. 45 EnvVarWaitFlag = "WAIT_FOR_ENVOY" 46 // EnvVarAdminPort is an environment variable which provides an override 47 // for the Envoy Admin API port. 48 EnvVarAdminPort = "ENVOY_ADMIN_PORT" 49 // DefaultAdminPort is the default port used for the Envoy Admin API. 50 DefaultAdminPort = "15000" 51 // EnumStateLive is a `envoy.admin.v3.ServerInfo.State` value indicating 52 // the Envoy server is LIVE. Other possible values of this enum are 53 // DRAINING, PRE_INITIALIZING and INITIALIZING, but they are not used 54 // here. 55 // See: https://github.com/envoyproxy/envoy/blob/b867a4dfae32e600ea0a4087dc7925ded5e2ab2a/api/envoy/admin/v3/server_info.proto#L24-L36 56 EnumStateLive = "LIVE" 57 ) 58 59 // HTTPGetClient captures a small part of the `http.Client` interface needed 60 // to execute a GET request. 61 type HTTPGetClient interface { 62 Get(url string) (resp *http.Response, err error) 63 } 64 65 // WaitForAdmin encapsulates the settings needed to wait until the Envoy Admin 66 // API is ready. 67 type WaitForAdmin struct { 68 // Port is the port (on localhost) where the Envoy Admin API is running. 69 Port string 70 // Sleep is the amount of time to sleep in between failed liveness 71 // checks for the Envoy API. 72 Sleep time.Duration 73 // HTTPClient is the HTTP client to use when sending requests. 74 HTTPClient HTTPGetClient 75 // Log is an optional logger to be used when executing. 76 Log logger.Log 77 // Attempt is a counter for the number of attempts that have been made 78 // to `executeOnce()`. This makes no attempt at "resetting" or guarding 79 // against concurrent usage or re-usage of a `WaitForAdmin` struct. 80 Attempt uint32 81 } 82 83 // IsReady makes a single request to the Envoy Admin API and checks if 84 // the status is ready. 85 func (wfa *WaitForAdmin) IsReady() bool { 86 readyURL := fmt.Sprintf("http://localhost:%s/ready", wfa.Port) 87 resp, err := wfa.HTTPClient.Get(readyURL) 88 if err != nil { 89 logger.MaybeDebugf(wfa.Log, "Envoy is not ready; connection failed: %s", err) 90 return false 91 } 92 93 defer resp.Body.Close() 94 body, err := io.ReadAll(resp.Body) 95 if err != nil { 96 logger.MaybeDebug(wfa.Log, "Envoy is not ready; failed to read response body") 97 return false 98 } 99 100 if resp.StatusCode != http.StatusOK { 101 logger.MaybeDebugf(wfa.Log, "Envoy is not ready; response status code: %d", resp.StatusCode) 102 return false 103 } 104 105 if string(body) != EnumStateLive+"\n" { 106 logger.MaybeDebugf(wfa.Log, "Envoy is not ready; response body: %q", string(body)) 107 return false 108 } 109 110 return true 111 } 112 113 func (wfa *WaitForAdmin) executeOnce(_ context.Context, _ interface{}) (interface{}, error) { 114 attempt := atomic.AddUint32(&wfa.Attempt, 1) 115 logger.MaybeDebugf(wfa.Log, "Checking if Envoy is ready, attempt %d", attempt) 116 if wfa.IsReady() { 117 logger.MaybeDebug(wfa.Log, "Envoy is ready") 118 return nil, nil 119 } 120 121 logger.MaybeDebugf(wfa.Log, "Envoy is not yet ready, sleeping for %s", wfa.Sleep) 122 return nil, ErrFailedAttempt 123 } 124 125 // Execute will communicate with the Envoy admin port running on `localhost`, 126 // which defaults to 15000 but can be overridden with `ENVOY_ADMIN_PORT`. It 127 // will send `GET /ready` up to 10 times, sleeping for `wfa.Sleep` in between 128 // if the response is not 200 OK with a body of `LIVE\n`. 129 func (wfa *WaitForAdmin) Execute(ctx context.Context) error { 130 _, err := retry.Retry( 131 ctx, 132 retry.ActionerFunc(wfa.executeOnce), 133 nil, 134 retry.OptConstantDelay(wfa.Sleep), 135 retry.OptMaxAttempts(10), 136 ) 137 if ex.Is(err, ErrFailedAttempt) { 138 return ex.New(ErrTimedOut) 139 } 140 return err 141 } 142 143 // MaybeWaitForAdmin will check if Envoy is running if the `WAIT_FOR_ENVOY` 144 // environment variable is set. This will communicate with the Envoy admin 145 // port running on `localhost`, which defaults to 15000 but can be overridden 146 // with `ENVOY_ADMIN_PORT`. It will send `GET /ready` up to 10 times, sleeping 147 // for 1 second in between if the response is not 200 OK with a body of 148 // `LIVE\n`. 149 func MaybeWaitForAdmin(log logger.Log) error { 150 if !strings.EqualFold(env.Env()[EnvVarWaitFlag], "true") { 151 return nil 152 } 153 154 hc := &http.Client{Timeout: time.Second} 155 wfa := WaitForAdmin{ 156 Port: env.Env().String(EnvVarAdminPort, DefaultAdminPort), 157 Sleep: time.Second, 158 HTTPClient: hc, 159 Log: log, 160 Attempt: 0, 161 } 162 163 ctx := context.Background() 164 return wfa.Execute(ctx) 165 }