github.com/blend/go-sdk@v1.20220411.3/envoyutil/wait_test.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_test 9 10 import ( 11 "bytes" 12 "context" 13 "fmt" 14 "io" 15 "net/http" 16 "net/http/httptest" 17 "net/url" 18 "regexp" 19 "strings" 20 "sync/atomic" 21 "testing" 22 "time" 23 24 "github.com/blend/go-sdk/assert" 25 "github.com/blend/go-sdk/env" 26 "github.com/blend/go-sdk/ex" 27 "github.com/blend/go-sdk/web" 28 29 "github.com/blend/go-sdk/envoyutil" 30 ) 31 32 // NOTE: Ensure that 33 // - `TimeoutError` satisfies `error` 34 // - `BadReadCloser` satisfies `io.ReadCloser` 35 // - `MockHTTPGetClient` satisfies `envoyutil.HTTPGetClient` 36 var ( 37 _ error = (*TimeoutError)(nil) 38 _ io.ReadCloser = (*BadReadCloser)(nil) 39 _ envoyutil.HTTPGetClient = (*MockHTTPGetClient)(nil) 40 ) 41 42 func TestMaybeWaitForAdmin(t *testing.T) { 43 it := assert.New(t) 44 45 defer env.Restore() 46 env.SetEnv(env.New()) 47 48 // No-op (WAIT_FOR_ENVOY is not set.) 49 var logBuffer bytes.Buffer 50 log := InMemoryLog(&logBuffer) 51 err := envoyutil.MaybeWaitForAdmin(log) 52 it.Nil(err) 53 it.Empty(logBuffer.Bytes()) 54 logBuffer.Reset() 55 56 // Happy-path; WAIT_FOR_ENVOY / ENVOY_ADMIN_PORT set. 57 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 58 w.WriteHeader(http.StatusOK) 59 fmt.Fprint(w, envoyutil.EnumStateLive+"\n") 60 })) 61 defer server.Close() 62 63 port := strings.TrimPrefix(server.URL, "http://127.0.0.1:") 64 env.Env().Set(envoyutil.EnvVarWaitFlag, "true") 65 env.Env().Set(envoyutil.EnvVarAdminPort, port) 66 err = envoyutil.MaybeWaitForAdmin(log) 67 it.Nil(err) 68 expected := strings.Join([]string{ 69 "[debug] Checking if Envoy is ready, attempt 1", 70 "[debug] Envoy is ready", 71 "", 72 }, "\n") 73 it.Equal(expected, logBuffer.String()) 74 logBuffer.Reset() 75 } 76 77 func TestWaitForAdminExecute(t *testing.T) { 78 it := assert.New(t) 79 80 // Failure with error that isn't timeout or connection error. 81 mhgc := &MockHTTPGetClient{Error: ex.New("known failure")} 82 wfa := envoyutil.WaitForAdmin{HTTPClient: mhgc} 83 err := wfa.Execute(context.TODO()) 84 it.True(ex.Is(err, envoyutil.ErrTimedOut)) 85 86 // Repeated failures with timeout 87 ue := &url.Error{ 88 Op: "Get", 89 URL: "http://localhost:15000/ready", 90 Err: &TimeoutError{}, 91 } 92 mhgc = &MockHTTPGetClient{Error: ue} 93 wfa = envoyutil.WaitForAdmin{HTTPClient: mhgc, Sleep: time.Nanosecond} 94 err = wfa.Execute(context.TODO()) 95 it.True(ex.Is(err, envoyutil.ErrTimedOut)) 96 97 // Success after repeated failures. 98 var logBuffer bytes.Buffer 99 log := InMemoryLog(&logBuffer) 100 mhgc = &MockHTTPGetClient{ 101 Error: ue, 102 SwitchAfter: 3, 103 SwitchResponse: &http.Response{ 104 StatusCode: http.StatusOK, 105 Body: io.NopCloser(bytes.NewReader([]byte(envoyutil.EnumStateLive + "\n"))), 106 }, 107 } 108 wfa = envoyutil.WaitForAdmin{Log: log, HTTPClient: mhgc, Sleep: time.Nanosecond} 109 err = wfa.Execute(context.TODO()) 110 it.Nil(err) 111 112 // NOTE: This regex is intended to work across Go minor versions. In go1.14, the quotes 113 // were added (in the standard library) around `http://localhost:15000/ready`. 114 expectedPattern := strings.Join([]string{ 115 `\[debug\] Checking if Envoy is ready, attempt 1`, 116 `\[debug\] Envoy is not ready; connection failed: Get (")?http://localhost:15000/ready(")?: TimeoutError`, 117 `\[debug\] Envoy is not yet ready, sleeping for 1ns`, 118 `\[debug\] Checking if Envoy is ready, attempt 2`, 119 `\[debug\] Envoy is not ready; connection failed: Get (")?http://localhost:15000/ready(")?: TimeoutError`, 120 `\[debug\] Envoy is not yet ready, sleeping for 1ns`, 121 `\[debug\] Checking if Envoy is ready, attempt 3`, 122 `\[debug\] Envoy is ready`, 123 "", 124 }, "\n") 125 re := regexp.MustCompile("(?m)^" + expectedPattern + "$") 126 it.True(re.Match(logBuffer.Bytes())) 127 } 128 129 func TestIsReady(t *testing.T) { 130 it := assert.New(t) 131 132 responses := make(chan web.RawResult, 1) 133 // Happy-path; WAIT_FOR_ENVOY / ENVOY_ADMIN_PORT set. 134 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 135 result := <-responses 136 w.WriteHeader(result.StatusCode) 137 _, _ = w.Write(result.Response) 138 })) 139 defer server.Close() 140 141 port := strings.TrimPrefix(server.URL, "http://127.0.0.1:") 142 wfa := envoyutil.WaitForAdmin{ 143 Port: port, 144 Sleep: time.Nanosecond, 145 HTTPClient: &http.Client{Timeout: time.Second}, 146 } 147 148 // Non-200 response code. 149 responses <- web.RawResult{ 150 Response: []byte("PRE_INITIALIZING\n"), 151 StatusCode: http.StatusServiceUnavailable, 152 } 153 ok := wfa.IsReady() 154 it.False(ok) 155 156 // 200 response code, but invalid body 157 responses <- web.RawResult{ 158 Response: []byte("INITIALIZING\n"), 159 StatusCode: http.StatusOK, 160 } 161 ok = wfa.IsReady() 162 it.False(ok) 163 164 // Error reading response body. 165 bodyErr := ex.New("Filesystem oops") 166 body := &BadReadCloser{Error: bodyErr} 167 mhgc := &MockHTTPGetClient{Response: &http.Response{Body: body}} 168 wfa = envoyutil.WaitForAdmin{ 169 Port: port, 170 Sleep: time.Nanosecond, 171 HTTPClient: mhgc, 172 } 173 ok = wfa.IsReady() 174 it.False(ok) 175 } 176 177 type MockHTTPGetClient struct { 178 Response *http.Response 179 Error error 180 // CallCount tracks the number of times `Get()` has been called. 181 CallCount uint32 182 183 // SwitchAfter is a `CallCount` target. Once the `CallCount` reaches this 184 // value, the mocked response from `Get()` will change from `Response, Error` 185 // to `SwitchResponse, SwitchError`. 186 SwitchAfter uint32 187 SwitchResponse *http.Response 188 SwitchError error 189 } 190 191 func (mhgc *MockHTTPGetClient) Get(url string) (resp *http.Response, err error) { 192 count := atomic.AddUint32(&mhgc.CallCount, 1) 193 if mhgc.SwitchAfter > 0 && count >= mhgc.SwitchAfter { 194 return mhgc.SwitchResponse, mhgc.SwitchError 195 } 196 197 return mhgc.Response, mhgc.Error 198 } 199 200 type TimeoutError struct { 201 } 202 203 func (te TimeoutError) Timeout() bool { 204 return true 205 } 206 207 func (te TimeoutError) Error() string { 208 return "TimeoutError" 209 } 210 211 type BadReadCloser struct { 212 Error error 213 } 214 215 func (brc *BadReadCloser) Read(p []byte) (n int, err error) { 216 return 0, brc.Error 217 } 218 219 func (brc *BadReadCloser) Close() error { 220 return brc.Error 221 }