...

Source file src/github.com/datawire/ambassador/v2/pkg/envoytest/harness.go

Documentation: github.com/datawire/ambassador/v2/pkg/envoytest

     1  package envoytest
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"net"
     9  	"net/http"
    10  	"os"
    11  	"path"
    12  	"strings"
    13  	"sync"
    14  	"sync/atomic"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/pkg/errors"
    19  	"github.com/stretchr/testify/require"
    20  
    21  	"github.com/datawire/dlib/dexec"
    22  	"github.com/datawire/dlib/dhttp"
    23  	"github.com/datawire/dlib/dlog"
    24  )
    25  
    26  func GetLoopbackAddr(ctx context.Context, port int) (string, error) {
    27  	ip, err := GetLoopbackIp(ctx)
    28  	if err != nil {
    29  		return "", err
    30  	}
    31  	return fmt.Sprintf("%s:%d", ip, port), nil
    32  }
    33  
    34  func GetLoopbackIp(ctx context.Context) (string, error) {
    35  	if _, err := dexec.LookPath("envoy"); err == nil {
    36  		return "127.0.0.1", nil
    37  	}
    38  	cmd := dexec.CommandContext(ctx, "docker", "network", "inspect", "bridge", "--format={{(index .IPAM.Config 0).Gateway}}")
    39  	bs, err := cmd.Output()
    40  	if err != nil {
    41  		return "", errors.Wrapf(err, "error finding loopback ip")
    42  	}
    43  	return strings.TrimSpace(string(bs)), nil
    44  }
    45  
    46  var cidCounter int64
    47  
    48  // SetupEnvoy launches an envoy docker container that is configured to connect to the supplied ads
    49  // address and expose the supplied portmaps. A Cleanup function is registered to shutdown the
    50  // container at the end of the test suite.
    51  func SetupEnvoy(t *testing.T, adsAddress string, portmaps ...string) {
    52  	ctx := dlog.NewTestContext(t, false)
    53  
    54  	host, port, err := net.SplitHostPort(adsAddress)
    55  	require.NoError(t, err)
    56  
    57  	yaml := fmt.Sprintf(bootstrap, host, port)
    58  
    59  	var cmd *dexec.Cmd
    60  	var cidfile string
    61  	if _, err := dexec.LookPath("envoy"); err == nil {
    62  		cmd = dexec.CommandContext(ctx, "envoy", "--config-yaml", yaml)
    63  	} else {
    64  		counter := atomic.AddInt64(&cidCounter, 1)
    65  		cidfile = path.Join(os.TempDir(), fmt.Sprintf("envoy-%d-%d-cid", os.Getpid(), counter))
    66  
    67  		args := []string{"docker", "run", "--cidfile", cidfile}
    68  		for _, pm := range portmaps {
    69  			args = append(args, "-p", pm)
    70  		}
    71  		args = append(args, "--rm", "--entrypoint", "envoy", "docker.io/datawire/aes:1.6.2", "--config-yaml", yaml)
    72  		cmd = dexec.CommandContext(ctx, args[0], args[1:]...)
    73  	}
    74  
    75  	var out io.Writer
    76  	if os.Getenv("SHUTUP_ENVOY") != "" {
    77  		var err error
    78  		out, err = os.OpenFile(os.DevNull, os.O_WRONLY, 0)
    79  		if err != nil {
    80  			t.Error(err)
    81  			return
    82  		}
    83  	}
    84  	cmd.Stdout = out
    85  	cmd.Stderr = out
    86  	if err := cmd.Start(); err != nil {
    87  		t.Errorf("error starting envoy: %v", err)
    88  		return
    89  	}
    90  
    91  	if cidfile == "" {
    92  		// we started envoy without a container
    93  		t.Cleanup(func() {
    94  			if err := cmd.Process.Kill(); err != nil {
    95  				t.Error(err)
    96  			}
    97  			if _, err := cmd.Process.Wait(); err != nil {
    98  				t.Errorf("error tearing down envoy: %+v", err)
    99  			}
   100  		})
   101  	} else {
   102  		// we started envoy inside a container so we need cleanup using the container id we captured on startup
   103  		t.Cleanup(func() {
   104  			// try a few times just in case the test aborted super quickly
   105  			delay := 1 * time.Second
   106  			var cidBytes []byte
   107  			for {
   108  				var err error
   109  				cidBytes, err = ioutil.ReadFile(cidfile)
   110  				if err != nil {
   111  					if delay < 8*time.Second {
   112  						time.Sleep(delay)
   113  						delay = 2 * delay
   114  						continue
   115  					}
   116  
   117  					t.Logf("error reading envoy container id: %+v", err)
   118  					return
   119  				}
   120  				break
   121  			}
   122  			defer os.Remove(cidfile)
   123  
   124  			cid := strings.TrimSpace(string(cidBytes))
   125  
   126  			if err := dexec.CommandContext(ctx, "docker", "kill", cid).Run(); err != nil {
   127  				t.Logf("error killing envoy container %s: %+v", cid, err)
   128  				return
   129  			}
   130  
   131  			if err := dexec.CommandContext(ctx, "docker", "wait", cid).Run(); err != nil {
   132  				// No such container is an "expected" error since the container might exit before we get
   133  				// around to waiting for it.
   134  				if !strings.Contains(err.Error(), "No such container") {
   135  					t.Logf("error waiting for envoy container %s: %+v", cid, err)
   136  					return
   137  				}
   138  			}
   139  		})
   140  	}
   141  }
   142  
   143  // This is the bootstrap we use for starting envoy. This is hardcoded for now, but we may want to
   144  // make it configurable for fancier tests in the future.
   145  const bootstrap = `
   146  {
   147    "node": {
   148      "cluster": "ambassador-default",
   149      "id": "test-id"
   150    },
   151    "layered_runtime": {
   152      "layers": [
   153        {
   154          "name": "static_layer",
   155          "static_layer": {
   156            "envoy.deprecated_features:envoy.api.v2.route.HeaderMatcher.regex_match": true,
   157            "envoy.deprecated_features:envoy.api.v2.route.RouteMatch.regex": true,
   158            "envoy.deprecated_features:envoy.config.filter.http.ext_authz.v2.ExtAuthz.use_alpha": true,
   159            "envoy.deprecated_features:envoy.config.trace.v2.ZipkinConfig.HTTP_JSON_V1": true,
   160            "envoy.reloadable_features.ext_authz_http_service_enable_case_sensitive_string_matcher": false
   161          }
   162        }
   163      ]
   164    },
   165    "dynamic_resources": {
   166      "ads_config": {
   167        "api_type": "GRPC",
   168        "grpc_services": [
   169          {
   170            "envoy_grpc": {
   171              "cluster_name": "ads_cluster"
   172            }
   173          }
   174        ]
   175      },
   176      "cds_config": {
   177        "ads": {}
   178      },
   179      "lds_config": {
   180        "ads": {}
   181      }
   182    },
   183    "static_resources": {
   184      "clusters": [
   185        {
   186          "connect_timeout": "1s",
   187          "dns_lookup_family": "V4_ONLY",
   188          "http2_protocol_options": {},
   189          "lb_policy": "ROUND_ROBIN",
   190          "load_assignment": {
   191            "cluster_name": "ads_cluster",
   192            "endpoints": [
   193              {
   194                "lb_endpoints": [
   195                  {
   196                    "endpoint": {
   197                      "address": {
   198                        "socket_address": {
   199                          "address": "%s",
   200                          "port_value": %s,
   201                          "protocol": "TCP"
   202                        }
   203                      }
   204                    }
   205                  }
   206                ]
   207              }
   208            ]
   209          },
   210          "name": "ads_cluster"
   211        }
   212      ]
   213    }
   214  }
   215  `
   216  
   217  // SetupRequestLogger will launch an http server that binds to the supplied addresses, responds with
   218  // the supplied body, and records every request it receives for later examination.
   219  func SetupRequestLogger(t *testing.T, addresses ...string) *RequestLogger {
   220  	rl := NewRequestLogger()
   221  	SetupServer(t, rl, addresses...)
   222  	return rl
   223  }
   224  
   225  type RequestLogger struct {
   226  	Requests []*http.Request
   227  }
   228  
   229  var _ http.Handler = &RequestLogger{}
   230  
   231  func NewRequestLogger() *RequestLogger {
   232  	return &RequestLogger{}
   233  }
   234  
   235  func (rl *RequestLogger) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   236  	rl.Log(r)
   237  	_, _ = w.Write([]byte("Hello World"))
   238  }
   239  
   240  func (rl *RequestLogger) Log(r *http.Request) {
   241  	rl.Requests = append(rl.Requests, r)
   242  }
   243  
   244  // SetupServer will launch an http server that runs for the duration of the test, binds to the
   245  // supplied addresses using the supplied handler.
   246  func SetupServer(t *testing.T, handler http.Handler, addresses ...string) {
   247  	ctx, cancel := context.WithCancel(dlog.NewTestContext(t, false))
   248  	wg := &sync.WaitGroup{}
   249  	t.Cleanup(func() {
   250  		cancel()
   251  		wg.Wait()
   252  	})
   253  
   254  	sc := &dhttp.ServerConfig{Handler: handler}
   255  	for _, address := range addresses {
   256  		// capture the value of address for the closure below
   257  		addr := address
   258  		wg.Add(1)
   259  		go func() {
   260  			err := sc.ListenAndServe(ctx, addr)
   261  			if err != nil && err != context.Canceled {
   262  				t.Errorf("server exited with error: %+v", err)
   263  			}
   264  			wg.Done()
   265  		}()
   266  	}
   267  }
   268  

View as plain text