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  }