Repository: tetratelabs/getenvoy (go)

Issue:
`getenvoy run` should terminate without errors on `Ctrl + C`
**To Reproduce**
1. Run Envoy
   ```shell
   $ getenvoy run standard:1.13.1 -- -c configs/google_com_proxy.v2.yaml
   ```
2. Press `Ctrl + C`

   ```shell
   ^C2020-04-09T13:12:34.913119Z	info	GetEnvoy process (PID=2480) received SIGINT
   2020-04-09T13:12:34.927305Z	error	9 errors occurred:
   	* Get http://localhost:15000/config_dump: dial tcp [::1]:15000: connect: connection refused
   	* Get http://localhost:15000/server_info: dial tcp [::1]:15000: connect: connection refused
   	* Get http://localhost:15000/certs: dial tcp [::1]:15000: connect: connection refused
   	* Get http://localhost:15000/contention: dial tcp [::1]:15000: connect: connection refused
   	* Get http://localhost:15000/listeners: dial tcp [::1]:15000: connect: connection refused
   	* Get http://localhost:15000/memory: dial tcp [::1]:15000: connect: connection refused
   	* Get http://localhost:15000/stats?format=json: dial tcp [::1]:15000: connect: connection refused
   	* Get http://localhost:15000/runtime: dial tcp [::1]:15000: connect: connection refused
   	* Get http://localhost:15000/clusters: dial tcp [::1]:15000: connect: connection refused
   
   
   2020-04-09T13:13:00.423797Z	error	error in getting open file statistics: not implemented yet
   2020-04-09T13:13:00.423848Z	info	Sending Envoy process (PID=2486) SIGINT
   [2020-04-09 15:13:00.423][5715348][warning][main] [external/envoy/source/server/server.cc:494] caught SIGINT
   [2020-04-09 15:13:00.423][5715348][info][main] [external/envoy/source/server/server.cc:602] shutting down server instance
   [2020-04-09 15:13:00.423][5715348][info][main] [external/envoy/source/server/server.cc:556] main dispatch loop exited
   [2020-04-09 15:13:00.430][5715348][info][main] [external/envoy/source/server/server.cc:595] exiting
   ```
   Notice `connect: connection refused` errors in the log.

**Expected behavior**

 * `getenvoy run` should terminate without errors

**Environment (please complete the relevant information):**
 - OS: `MacOS`
 - Envoy Version: `1.13.1`



Pull request description:
Enables admin port detection by default
This enables admin port detection internally via via the Envoy 1.3 flag
`--admin-address-path`. If a user preset this, we don't interfere with
it. However, if it isn't passed as an envoy arg, we assign it to
$debugDir/admin-address.txt. At startup, this path is read prior to
checking any admin connections.

In doing so, this fixes use of configuration which use a non-default
port, like `getenvoy run standard:1.16.2 -- -c
/tmp/google_com_proxy.v2.yaml`

This also stops the examples from using a fixed admin port, and
instead using an ephemeral port by default.

Fixes #86

Patch:
diff --git a/pkg/binary/envoy/adminport.go b/pkg/binary/envoy/adminport.go
deleted file mode 100644
index 1bc5990..0000000
--- a/pkg/binary/envoy/adminport.go
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright 2019 Tetrate
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package envoy
-
-import (
-	"fmt"
-	"net"
-	"os"
-	"path/filepath"
-
-	"github.com/tetratelabs/log"
-
-	"github.com/tetratelabs/getenvoy/pkg/binary"
-)
-
-// EnableAdminAddressDetection sets the "--admin-address-path" flag and reads it back before attempting ready checks.
-//
-// Notably, this allows ephemeral admin ports via bootstrap configuration admin/port_value=0
-func EnableAdminAddressDetection(r *Runtime) {
-	r.RegisterPreStart(appendArg)
-}
-
-func appendArg(r binary.Runner) error {
-	e, ok := r.(*Runtime)
-	if !ok {
-		return fmt.Errorf("unable to use admin address detection")
-	}
-	r.AppendArgs([]string{"--admin-address-path", filepath.Join(r.DebugStore(), "admin-address.txt")})
-
-	// NOTE: this is in the envoy package to allow sneaky access to overwrite
-	e.getAdminAddress = readAdminAddressFile
-	return nil
-}
-
-func readAdminAddressFile(r *Runtime) string {
-	adminAddressFile := filepath.Join(r.DebugStore(), "admin-address.txt")
-	adminAddress, err := os.ReadFile(adminAddressFile) //nolint:gosec
-	if err != nil {
-		log.Debugf("unable to read %s: %v", adminAddressFile, err)
-		return ""
-	}
-	if _, _, err := net.SplitHostPort(string(adminAddress)); err != nil {
-		log.Debugf("invalid admin address in %s: %v", adminAddressFile, err)
-		return ""
-	}
-	return string(adminAddress)
-}
diff --git a/pkg/binary/envoy/config.go b/pkg/binary/envoy/config.go
index 36d8ba6..f9a0a62 100644
--- a/pkg/binary/envoy/config.go
+++ b/pkg/binary/envoy/config.go
@@ -50,7 +50,6 @@ func ParseMode(s string) Mode {
 // NewConfig creates and mutates a config object based on passed params
 func NewConfig(options ...func(*Config)) *Config {
 	cfg := &Config{
-		AdminPort:      15000,
 		DrainDuration:  &durationpb.Duration{Seconds: 30},
 		ConnectTimeout: &durationpb.Duration{Seconds: 5},
 	}
@@ -61,6 +60,7 @@ func NewConfig(options ...func(*Config)) *Config {
 }
 
 // Config store Envoy config information for use by bootstrap and arg mutators
+// TODO: only used by istio until #178 deletes it
 type Config struct {
 	XDSAddress     string
 	Mode           Mode
@@ -68,20 +68,6 @@ type Config struct {
 	ALSAddresss    string
 	DrainDuration  *durationpb.Duration
 	ConnectTimeout *durationpb.Duration
-	AdminAddress   string
-	AdminPort      int32
-}
-
-// GetAdminAddress returns a host:port formatted address of the Envoy admin listener.
-func (c *Config) GetAdminAddress() string {
-	if c.AdminPort == 0 {
-		return ""
-	}
-	address := c.AdminAddress
-	if address == "" {
-		address = "localhost"
-	}
-	return fmt.Sprintf("%s:%d", address, c.AdminPort)
 }
 
 // SaveConfig saves configuration string in getenvoy
diff --git a/pkg/binary/envoy/controlplane/istio.go b/pkg/binary/envoy/controlplane/istio.go
index 9c49658..26051a3 100644
--- a/pkg/binary/envoy/controlplane/istio.go
+++ b/pkg/binary/envoy/controlplane/istio.go
@@ -103,7 +103,9 @@ func generateIstioConfig(e *envoy.Runtime) meshconfig.ProxyConfig {
 	cfg := mesh.DefaultProxyConfig()
 	cfg.ConfigPath = e.DebugStore()
 	cfg.DiscoveryAddress = e.Config.XDSAddress
-	cfg.ProxyAdminPort = e.Config.AdminPort
+	// Choosing a random port doesn't help because envoy_bootstrap.json hard-codes 15020 15021 and 15090
+	// See https://github.com/istio/istio/issues/31138
+	cfg.ProxyAdminPort = 15000
 	cfg.ProxyBootstrapTemplatePath = filepath.Join(e.TmplDir, "istio_bootstrap_tmpl.json")
 	cfg.EnvoyAccessLogService = &meshconfig.RemoteService{Address: e.Config.ALSAddresss}
 	// Required: Defaults to MUTUAL_TLS, but we don't configure auth, yet, so it has to be set to NONE
diff --git a/pkg/binary/envoy/debug/admin.go b/pkg/binary/envoy/debug/admin.go
index 69f2f18..cfc8f81 100644
--- a/pkg/binary/envoy/debug/admin.go
+++ b/pkg/binary/envoy/debug/admin.go
@@ -22,7 +22,6 @@ import (
 	"os"
 	"path/filepath"
 
-	"github.com/tetratelabs/log"
 	"github.com/tetratelabs/multierror"
 
 	"github.com/tetratelabs/getenvoy/pkg/binary"
@@ -52,13 +51,13 @@ func retrieveAdminAPIData(r binary.Runner) error {
 	if !ok {
 		return errors.New("binary.Runner is not an Envoy runtime")
 	}
-	if e.GetAdminAddress() == "" {
-		log.Warnf("unable to capture Envoy configuration and metrics since Envoy Admin listener is not enabled")
-		return nil
+	adminAddress, err := e.GetAdminAddress()
+	if err != nil {
+		return fmt.Errorf("unable to capture Envoy configuration and metrics: %w", err)
 	}
 	var multiErr *multierror.Error
 	for path, file := range adminAPIPaths {
-		resp, err := http.Get(fmt.Sprintf("http://%s/%v", e.GetAdminAddress(), path))
+		resp, err := http.Get(fmt.Sprintf("http://%s/%v", adminAddress, path))
 		if err != nil {
 			multiErr = multierror.Append(multiErr, err)
 			continue
diff --git a/pkg/binary/envoy/run.go b/pkg/binary/envoy/run.go
index 8ddfad6..c27f3cb 100644
--- a/pkg/binary/envoy/run.go
+++ b/pkg/binary/envoy/run.go
@@ -54,7 +54,10 @@ func (r *Runtime) RunPath(path string, args []string) error {
 	cmd.SysProcAttr = sysProcAttr()
 	r.cmd = cmd
 
-	r.handlePreStart()
+	err := r.handlePreStart()
+	if err != nil {
+		return err
+	}
 
 	if cmd.Stdout == nil {
 		cmd.Stdout = os.Stdout
@@ -75,6 +78,8 @@ func (r *Runtime) RunPath(path string, args []string) error {
 
 	go waitForExit(cmd, waitCancel) // waits in a goroutine. We may need to kill the process if a signal occurs first.
 
+	awaitAdminAddress(sigCtx, r)
+
 	// Block until we receive SIGINT or are canceled because Envoy has died
 	<-sigCtx.Done()
 
@@ -91,6 +96,19 @@ func (r *Runtime) RunPath(path string, args []string) error {
 	return r.handlePostTermination()
 }
 
+// awaitAdminAddress waits up to 2 seconds for the admin address to be available and logs it.
+// See https://github.com/envoyproxy/envoy/issues/16050 for moving this logging upstream.
+func awaitAdminAddress(sigCtx context.Context, r *Runtime) {
+	for i := 0; i < 10 && sigCtx.Err() == nil; i++ {
+		adminAddress, adminErr := r.GetAdminAddress()
+		if adminErr == nil {
+			log.Infof("discovered admin address: %v", adminAddress)
+			return
+		}
+		time.Sleep(200 * time.Millisecond)
+	}
+}
+
 // DebugStore returns the location at which the runtime instance persists debug data for this given instance
 // Getters typically aren't idiomatic Go, however, this one is deliberately part of the runner interface
 func (r *Runtime) DebugStore() string {
diff --git a/pkg/binary/envoy/runtime.go b/pkg/binary/envoy/runtime.go
index f577d22..6c9b882 100644
--- a/pkg/binary/envoy/runtime.go
+++ b/pkg/binary/envoy/runtime.go
@@ -17,10 +17,14 @@ package envoy
 import (
 	"context"
 	"fmt"
+	"net"
 	"net/http"
+	"os"
 	"os/exec"
 	"path/filepath"
 
+	"github.com/tetratelabs/log"
+
 	"github.com/tetratelabs/getenvoy/pkg/binary"
 	"github.com/tetratelabs/getenvoy/pkg/common"
 	ioutil "github.com/tetratelabs/getenvoy/pkg/util/io"
@@ -87,28 +91,35 @@ type Runtime struct {
 	RootDir  string
 	debugDir string
 	TmplDir  string
-	Config   *Config
 
 	WorkingDir string
 	IO         ioutil.StdStreams
+	Config     *Config // TODO: only used by istio until #178 deletes it
 
-	cmd           *exec.Cmd
-	fakeInterrupt context.CancelFunc
+	cmd                            *exec.Cmd
+	adminAddress, adminAddressPath string
+	fakeInterrupt                  context.CancelFunc
 
-	preStart        []func(binary.Runner) error
-	preTermination  []func(binary.Runner) error
-	postTermination []func(binary.Runner) error
+	preStart, preTermination, postTermination []func(binary.Runner) error
 
-	getAdminAddress func(r *Runtime) string
-	isReady         bool
+	isReady bool
 }
 
 // GetAdminAddress returns the current admin address in host:port format, or empty if not yet available.
-func (r *Runtime) GetAdminAddress() string {
-	if r.getAdminAddress == nil {
-		return r.Config.GetAdminAddress()
+// Exported for debug.EnableEnvoyAdminDataCollection, which is always on due to debug.EnableAll.
+func (r *Runtime) GetAdminAddress() (string, error) {
+	if r.adminAddress != "" { // We don't expect the admin address to change once written, so cache it.
+		return r.adminAddress, nil
+	}
+	adminAddress, err := os.ReadFile(r.adminAddressPath) //nolint:gosec
+	if err != nil {
+		return "", fmt.Errorf("unable to read %s: %w", r.adminAddressPath, err)
+	}
+	if _, _, err := net.SplitHostPort(string(adminAddress)); err != nil {
+		return "", fmt.Errorf("invalid admin address in %s: %w", r.adminAddressPath, err)
 	}
-	return r.getAdminAddress(r)
+	r.adminAddress = string(adminAddress)
+	return r.adminAddress, nil
 }
 
 // Status indicates the state of the child process
@@ -139,8 +150,9 @@ func (r *Runtime) GetPid() (int, error) {
 // envoyReady reads the HTTP status from the /ready endpoint and returns binary.StatusReady on 200 or
 // binary.StatusInitializing on 503.
 func (r *Runtime) envoyReady() (int, error) {
-	adminAddress := r.GetAdminAddress()
-	if adminAddress == "" { // don't yet have an admin address
+	adminAddress, err := r.GetAdminAddress()
+	if err != nil { // don't yet have an admin address
+		log.Debugf("%v", err) // don't fill logs
 		return binary.StatusInitializing, nil
 	}
 
diff --git a/pkg/binary/envoy/start.go b/pkg/binary/envoy/start.go
index 6083185..004a0a1 100644
--- a/pkg/binary/envoy/start.go
+++ b/pkg/binary/envoy/start.go
@@ -15,21 +15,48 @@
 package envoy
 
 import (
-	"github.com/tetratelabs/log"
+	"fmt"
+	"path/filepath"
 
 	"github.com/tetratelabs/getenvoy/pkg/binary"
 )
 
-func (r *Runtime) handlePreStart() {
+func (r *Runtime) handlePreStart() error {
 	// Execute all registered preStart functions
 	for _, f := range r.preStart {
 		if err := f(r); err != nil {
-			log.Error(err.Error())
+			return err
 		}
 	}
+	// Add admin path after we know the debug store location and other functions apply.
+	// NOTE: getenvoy isn't supported as a library, so we don't really need to worry about preStart hooks conflicting.
+	// If we are able to know the debug path sooner, we could refactor this later to be more direct.
+	return r.ensureAdminAddressPath()
 }
 
 // RegisterPreStart registers the passed functions to be run before Envoy has started
 func (r *Runtime) RegisterPreStart(f ...func(binary.Runner) error) {
 	r.preStart = append(r.preStart, f...)
 }
+
+// ensureAdminAddressPath sets the "--admin-address-path" flag so that it can be used in /ready checks. If a value
+// already exists, it will be returned. Otherwise, the flag will be set to the file "admin-address.txt" in the
+// debug directory. We don't use the working directory as sometimes that is a source directory.
+//
+// Notably, this allows ephemeral admin ports via bootstrap configuration admin/port_value=0 (minimum Envoy 1.12 for macOS support)
+func (r *Runtime) ensureAdminAddressPath() error {
+	args := r.cmd.Args
+	flag := `--admin-address-path`
+	for i, a := range args {
+		if a == flag {
+			if i+1 == len(args) || args[i+1] == "" {
+				return fmt.Errorf(`missing value to argument %q`, flag)
+			}
+			r.adminAddressPath = args[i+1]
+			return nil
+		}
+	}
+	r.adminAddressPath = filepath.Join(r.DebugStore(), "admin-address.txt")
+	r.cmd.Args = append(r.cmd.Args, flag, r.adminAddressPath)
+	return nil
+}
diff --git a/pkg/binary/interface.go b/pkg/binary/interface.go
index e9f7e41..008dfea 100644
--- a/pkg/binary/interface.go
+++ b/pkg/binary/interface.go
@@ -27,7 +27,6 @@ type Runner interface {
 	RegisterPreStart(f ...func(Runner) error)
 	RegisterPreTermination(f ...func(Runner) error)
 	RegisterPostTermination(f ...func(Runner) error)
-	FakeInterrupt()
 	Status() int
 	GetPid() (int, error)
 	AppendArgs([]string)
diff --git a/pkg/extension/workspace/example/envoy/template/defaults.go b/pkg/extension/workspace/example/envoy/template/defaults.go
index a626776..8b5f3c4 100644
--- a/pkg/extension/workspace/example/envoy/template/defaults.go
+++ b/pkg/extension/workspace/example/envoy/template/defaults.go
@@ -41,7 +41,7 @@ var (
 					SocketAddress: &envoycore.SocketAddress{
 						Address: "127.0.0.1",
 						PortSpecifier: &envoycore.SocketAddress_PortValue{
-							PortValue: 9901,
+							PortValue: 0, // dynamic port
 						},
 					},
 				},
diff --git a/pkg/extension/workspace/example/runtime/getenvoy/runtime.go b/pkg/extension/workspace/example/runtime/getenvoy/runtime.go
index 3e3d779..b0b389a 100644
--- a/pkg/extension/workspace/example/runtime/getenvoy/runtime.go
+++ b/pkg/extension/workspace/example/runtime/getenvoy/runtime.go
@@ -48,11 +48,6 @@ func (r *runtime) Run(ctx *types.RunContext) (errs error) {
 		func(r *envoy.Runtime) {
 			r.WorkingDir = configDir.GetDir()
 			r.IO = ctx.IO
-
-			// configure Admin listener (0 value will be interpreted as "turned off")
-			address := configDir.GetBootstrap().GetAdmin().GetAddress().GetSocketAddress()
-			r.Config.AdminAddress = address.GetAddress()
-			r.Config.AdminPort = int32(address.GetPortValue())
 		}).
 		AndAll(debug.EnableAll())...,
 	)
diff --git a/pkg/flavors/postgres/postgres.go b/pkg/flavors/postgres/postgres.go
index 6a6bbf2..95e5bd4 100644
--- a/pkg/flavors/postgres/postgres.go
+++ b/pkg/flavors/postgres/postgres.go
@@ -228,7 +228,7 @@ admin:
   address:
     socket_address:
       address: 0.0.0.0
-      port_value: 15000
+      port_value: 0
 
 static_resources:
   listeners:


Test patch:
diff --git a/pkg/binary/envoy/controlplane/istio_test.go b/pkg/binary/envoy/controlplane/istio_test.go
index 5bd08ff..5b92e5e 100644
--- a/pkg/binary/envoy/controlplane/istio_test.go
+++ b/pkg/binary/envoy/controlplane/istio_test.go
@@ -59,9 +59,6 @@ func TestEnableIstioBootstrap(t *testing.T) {
 	runtime, err := envoy.NewRuntime(
 		func(r *envoy.Runtime) {
 			r.Config.Mode = envoy.ParseMode("loadbalancer")
-			// Choosing a random port doesn't help because envoy_bootstrap.json hard-codes 15020 15021 and 15090
-			// See https://github.com/istio/istio/issues/31138
-			r.Config.AdminPort = 15000
 			r.Config.XDSAddress = pilotGrpc
 			r.Config.IPAddresses = []string{"127.0.0.1"} // prevent calling controlplane.retrieveIPs() on CI hosts
 		},
diff --git a/pkg/binary/envoy/debug/node_test.go b/pkg/binary/envoy/debug/node_test.go
index 4bd7800..14b17a7 100644
--- a/pkg/binary/envoy/debug/node_test.go
+++ b/pkg/binary/envoy/debug/node_test.go
@@ -39,7 +39,12 @@ func TestEnableNodeCollection(t *testing.T) {
 		path := filepath.Join(r.DebugStore(), file)
 		f, err := os.Stat(path)
 		require.NoError(t, err, "error stating %v", path)
-		require.NotEmpty(t, f.Size(), "file %v was empty", path)
+
+		// While usually not, ps can be empty due to deadline timeout getting a ps listing. Instead of flakey tests, we
+		// don't enforce this.
+		if file != "node/ps.txt" {
+			require.NotEmpty(t, f.Size(), "file %v was empty", path)
+		}
 
 		if strings.HasSuffix(file, ".json") {
 			raw, err := os.ReadFile(path)
diff --git a/pkg/binary/envoy/run_test.go b/pkg/binary/envoy/run_test.go
index 5d8b8ab..010669b 100644
--- a/pkg/binary/envoy/run_test.go
+++ b/pkg/binary/envoy/run_test.go
@@ -72,10 +72,11 @@ func TestRuntime_RunPath(t *testing.T) {
 				r.RunPath(sleep, tc.args)
 			}()
 
+			expectedStatus := binary.StatusInitializing // because we won't have an admin URL to check
 			require.Eventually(t, func() bool {
-				return r.Status() == binary.StatusStarted || r.Status() == binary.StatusTerminated
-			}, 10*time.Second, 100*time.Millisecond, "never achieved StatusStarted or StatusTerminated")
-			require.Equal(t, binary.StatusStarted, r.Status(), "never achieved StatusStarted or StatusTerminated")
+				return r.Status() == expectedStatus || r.Status() == binary.StatusTerminated
+			}, 10*time.Second, 100*time.Millisecond, "never achieved StatusInitializing or StatusTerminated")
+			require.Equal(t, expectedStatus, r.Status(), "never achieved StatusInitializing or StatusTerminated")
 
 			tc.killerFunc(r)
 			wg.Wait()
diff --git a/pkg/binary/envoy/start_test.go b/pkg/binary/envoy/start_test.go
new file mode 100644
index 0000000..a33d0b3
--- /dev/null
+++ b/pkg/binary/envoy/start_test.go
@@ -0,0 +1,163 @@
+// Copyright 2019 Tetrate
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package envoy
+
+import (
+	"errors"
+	"os/exec"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/tetratelabs/getenvoy/pkg/binary"
+)
+
+// fakeDebugDir is not actually written to as the commands tested are never invoked.
+const fakeDebugDir = "~/.getenvoy/debug/1"
+
+func TestHandlePreStartReturnsOnFirstError(t *testing.T) {
+	r, _ := NewRuntime()
+	err1, err2 := errors.New("1"), errors.New("2")
+	r.RegisterPreStart(func(runner binary.Runner) error {
+		return err1
+	})
+	r.RegisterPreStart(func(runner binary.Runner) error {
+		return err2
+	})
+
+	actualErr := r.(*Runtime).handlePreStart()
+	require.Equal(t, err1, actualErr)
+}
+
+func TestHandlePreStartReturnsError(t *testing.T) {
+	r, _ := NewRuntime()
+	first := false
+	err := errors.New("1")
+	r.RegisterPreStart(func(runner binary.Runner) error {
+		first = true
+		return nil
+	})
+	r.RegisterPreStart(func(runner binary.Runner) error {
+		return err
+	})
+
+	actualErr := r.(*Runtime).handlePreStart()
+	require.Equal(t, true, first)
+	require.Equal(t, err, actualErr)
+}
+
+func TestHandlePreStartEnsuresAdminAddressPath(t *testing.T) {
+	r, _ := NewRuntime()
+	e := r.(*Runtime)
+	e.cmd = exec.Command("envoy")
+
+	// Verify the admin address path set (same assertion as ensureAdminAddressPath)
+	actualErr := r.(*Runtime).handlePreStart()
+	require.NoError(t, actualErr)
+	require.Equal(t, []string{"envoy", "--admin-address-path", e.adminAddressPath}, e.cmd.Args)
+}
+
+func TestHandlePreStartEnsuresAdminAddressPathLast(t *testing.T) {
+	r, _ := NewRuntime()
+	e := r.(*Runtime)
+	e.cmd = exec.Command("envoy")
+
+	r.RegisterPreStart(func(runner binary.Runner) error {
+		runner.AppendArgs([]string{"--admin-address-path", "/tmp/admin.txt"})
+		return nil
+	})
+
+	actualErr := r.(*Runtime).handlePreStart()
+	require.NoError(t, actualErr)
+	require.Equal(t, "/tmp/admin.txt", e.adminAddressPath)
+	require.Equal(t, []string{"envoy", "--admin-address-path", "/tmp/admin.txt"}, e.cmd.Args)
+}
+
+func TestEnsureAdminAddressPath(t *testing.T) {
+	tests := []struct {
+		name                 string
+		args                 []string
+		wantAdminAddressPath string
+		wantArgs             []string
+	}{
+		{
+			name:                 "no args",
+			args:                 []string{"envoy"},
+			wantAdminAddressPath: "~/.getenvoy/debug/1/admin-address.txt",
+			wantArgs:             []string{"envoy", "--admin-address-path", "~/.getenvoy/debug/1/admin-address.txt"},
+		},
+		{
+			name:                 "args",
+			args:                 []string{"envoy", "-c", "/tmp/google_com_proxy.v2.yaml"},
+			wantAdminAddressPath: "~/.getenvoy/debug/1/admin-address.txt",
+			wantArgs:             []string{"envoy", "-c", "/tmp/google_com_proxy.v2.yaml", "--admin-address-path", "~/.getenvoy/debug/1/admin-address.txt"},
+		},
+		{
+			name:                 "already",
+			args:                 []string{"envoy", "--admin-address-path", "/tmp/admin.txt", "-c", "/tmp/google_com_proxy.v2.yaml"},
+			wantAdminAddressPath: "/tmp/admin.txt",
+			wantArgs:             []string{"envoy", "--admin-address-path", "/tmp/admin.txt", "-c", "/tmp/google_com_proxy.v2.yaml"},
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			r, _ := NewRuntime()
+			e := r.(*Runtime)
+			e.cmd = exec.Command(tt.args[0], tt.args[1:]...)
+			e.debugDir = fakeDebugDir
+
+			err := e.ensureAdminAddressPath()
+			require.NoError(t, err)
+			require.Equal(t, tt.wantAdminAddressPath, e.adminAddressPath)
+			require.Equal(t, tt.wantArgs, e.cmd.Args)
+		})
+	}
+}
+
+func TestEnsureAdminAddressPathValidateExisting(t *testing.T) {
+	tests := []struct {
+		name        string
+		args        []string
+		expectedErr string
+	}{
+		{
+			name:        "value empty",
+			args:        []string{"envoy", "--admin-address-path", "", "-c", "/tmp/google_com_proxy.v2.yaml"},
+			expectedErr: `missing value to argument "--admin-address-path"`,
+		},
+		{
+			name:        "value missing",
+			args:        []string{"envoy", "-c", "/tmp/google_com_proxy.v2.yaml", "--admin-address-path"},
+			expectedErr: `missing value to argument "--admin-address-path"`,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			r, _ := NewRuntime()
+			e := r.(*Runtime)
+			e.cmd = exec.Command(tt.args[0], tt.args[1:]...)
+			e.debugDir = fakeDebugDir
+
+			err := e.ensureAdminAddressPath()
+			require.Equal(t, tt.args, e.cmd.Args)
+			require.Empty(t, e.adminAddressPath)
+			require.EqualError(t, err, tt.expectedErr)
+		})
+	}
+}
diff --git a/pkg/binary/envoytest/util.go b/pkg/binary/envoytest/util.go
index 64ff8f7..e79871d 100644
--- a/pkg/binary/envoytest/util.go
+++ b/pkg/binary/envoytest/util.go
@@ -109,13 +109,11 @@ func RequireRunTerminate(t *testing.T, r binary.Runner, options RunKillOptions)
 		args = append(args,
 			"--config-yaml", "admin: {access_log_path: '/dev/stdout', address: {socket_address: {address: '127.0.0.1', port_value: 0}}}",
 		)
-		// Allows us the status checker to read the resolved admin port after envoy starts
-		envoy.EnableAdminAddressDetection(r.(*envoy.Runtime))
 	}
 
 	// This ensures on any panic the envoy process is terminated, which can prevent test hangs.
 	deferredInterrupt := func() {
-		r.FakeInterrupt()
+		r.(*envoy.Runtime).FakeInterrupt()
 	}
 
 	defer func() {
@@ -143,7 +141,7 @@ func RequireRunTerminate(t *testing.T, r binary.Runner, options RunKillOptions)
 	require.Equal(t, expectedStatus, r.Status(), "never achieved status(%d)", expectedStatus)
 
 	// Now, terminate the server.
-	r.FakeInterrupt()
+	r.(*envoy.Runtime).FakeInterrupt()
 	deferredInterrupt = nil
 
 	select { // Await run completion
diff --git a/pkg/cmd/extension/run/cmd_test.go b/pkg/cmd/extension/run/cmd_test.go
index 46d19a4..2e6569f 100644
--- a/pkg/cmd/extension/run/cmd_test.go
+++ b/pkg/cmd/extension/run/cmd_test.go
@@ -157,21 +157,24 @@ func TestGetEnvoyExtensionRun(t *testing.T) {
 
 	// Run "getenvoy extension run"
 	c, stdout, stderr := cmd.NewRootCommand()
-	c.SetArgs([]string{"extension", "run", "--home-dir", config.envoyHome})
+	c.SetArgs([]string{"extension", "run", "--home-dir", config.envoyHome,
+		// prevents generated admin-address-path which makes assertions difficult
+		"--envoy-options", "--admin-address-path /admin-address.txt",
+	})
 	err := cmdutil.Execute(c)
 
 	// Verify the command invoked, passing the correct default commandline
 	require.NoError(t, err, `expected no error running [%v]`, c)
 
-	envoyBin := filepath.Join(config.envoyHome, "builds/standard/1.17.1", config.platform, "/bin/envoy")
 	// The working directory of envoy isn't the same as docker or the workspace
-	envoyWd := cmd.ParseEnvoyWorkDirectory(stdout)
+	envoyWd := cmd.ParseEnvoyWorkDirectory(t, stdout.String(), `couldn't find envoy wd running [%v]`, c)
 
+	envoyBin := filepath.Join(config.envoyHome, "builds/standard/1.17.1", config.platform, "/bin/envoy")
 	// We expect docker to build from the correct path, as the current user and mount a volume for the correct workspace.
 	expectedStdout := fmt.Sprintf(`%s/docker run -u %s --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init getenvoy/extension-rust-builder:latest build --output-file target/getenvoy/extension.wasm
 envoy pwd: %s
 envoy bin: %s
-envoy args: -c %s/envoy.tmpl.yaml`,
+envoy args: -c %s/envoy.tmpl.yaml --admin-address-path /admin-address.txt`,
 		config.dockerDir, config.expectedUidGid, runtime.GOOS, config.workspaceDir, envoyWd, envoyBin, envoyWd)
 	require.Equal(t, expectedStdout+"\n", stdout.String(), `expected stdout running [%v]`, c)
 	require.Equal(t, "docker stderr\nenvoy stderr\n", stderr.String(), `expected stderr running [%v]`, c)
@@ -280,7 +283,7 @@ func TestGetEnvoyExtensionRunWithOptions(t *testing.T) {
 	require.NoError(t, err, `expected no error running [%v]`, c)
 
 	// The working directory of envoy is a temp directory not controlled by this test, so we have to parse it.
-	envoyWd := cmd.ParseEnvoyWorkDirectory(stdout)
+	envoyWd := cmd.ParseEnvoyWorkDirectory(t, stdout.String(), `couldn't find envoy wd running [%v]`, c)
 
 	envoyArgs := fmt.Sprintf(`-c %s/envoy.tmpl.yaml --concurrency 2 --component-log-level wasm:debug,config:trace`, envoyWd)
 	require.Contains(t, stdout.String(), "envoy args: "+envoyArgs, `expected stdout running [%v]`, c)
@@ -298,7 +301,10 @@ func TestGetEnvoyExtensionRunWithWasm(t *testing.T) {
 
 	// Run "getenvoy extension run --extension-file /path/to/extension.wasm"
 	c, stdout, stderr := cmd.NewRootCommand()
-	c.SetArgs([]string{"extension", "run", "--home-dir", config.envoyHome, "--extension-file", wasmFile})
+	c.SetArgs([]string{"extension", "run", "--home-dir", config.envoyHome, "--extension-file", wasmFile,
+		// prevents generated admin-address-path which makes assertions difficult
+		"--envoy-options", "--admin-address-path /admin-address.txt",
+	})
 	err = cmdutil.Execute(c)
 
 	// Verify the command invoked, passing the correct default commandline
@@ -307,12 +313,12 @@ func TestGetEnvoyExtensionRunWithWasm(t *testing.T) {
 	envoyBin := filepath.Join(config.envoyHome, "builds/standard/1.17.1", config.platform, "/bin/envoy")
 
 	// The working directory of envoy is a temp directory not controlled by this test, so we have to parse it.
-	envoyWd := cmd.ParseEnvoyWorkDirectory(stdout)
+	envoyWd := cmd.ParseEnvoyWorkDirectory(t, stdout.String(), `couldn't find envoy wd running [%v]`, c)
 
 	// We expect docker to not have ran, since we supplied a pre-existing wasm. However, envoy should have.
 	expectedStdout := fmt.Sprintf(`envoy pwd: %s
 envoy bin: %s
-envoy args: -c %s/envoy.tmpl.yaml`,
+envoy args: -c %s/envoy.tmpl.yaml --admin-address-path /admin-address.txt`,
 		envoyWd, envoyBin, envoyWd)
 	require.Equal(t, expectedStdout+"\n", stdout.String(), `expected stdout running [%v]`, c)
 	require.Equal(t, "envoy stderr\n", stderr.String(), `expected stderr running [%v]`, c)
diff --git a/pkg/extension/workspace/example/envoy/template/expand_test.go b/pkg/extension/workspace/example/envoy/template/expand_test.go
index 07f1574..a4f3719 100644
--- a/pkg/extension/workspace/example/envoy/template/expand_test.go
+++ b/pkg/extension/workspace/example/envoy/template/expand_test.go
@@ -32,7 +32,7 @@ func TestExpand(t *testing.T) {
 		{
 			name:     "default value: admin",
 			input:    `{{ .GetEnvoy.DefaultValue "admin" }}`,
-			expected: `{"accessLogPath":"/dev/null","address":{"socketAddress":{"address":"127.0.0.1","portValue":9901}}}`,
+			expected: `{"accessLogPath":"/dev/null","address":{"socketAddress":{"address":"127.0.0.1","portValue":0}}}`,
 		},
 		{
 			name:     "default value: admin.access_log_path",
@@ -42,7 +42,7 @@ func TestExpand(t *testing.T) {
 		{
 			name:     "default value: admin.address",
 			input:    `{{ .GetEnvoy.DefaultValue "admin.address" }}`,
-			expected: `{"socketAddress":{"address":"127.0.0.1","portValue":9901}}`,
+			expected: `{"socketAddress":{"address":"127.0.0.1","portValue":0}}`,
 		},
 		{
 			name:     "default value: admin.address.socket.address",
@@ -52,7 +52,7 @@ func TestExpand(t *testing.T) {
 		{
 			name:     "default value: admin.address.socket.port",
 			input:    `{{ .GetEnvoy.DefaultValue "admin.address.socket.port" }}`,
-			expected: `9901`,
+			expected: `0`,
 		},
 		{
 			name:     "extension: name",
diff --git a/pkg/extension/workspace/example/runtime/configdir/configdir_test.go b/pkg/extension/workspace/example/runtime/configdir/configdir_test.go
index 74218fc..75db580 100644
--- a/pkg/extension/workspace/example/runtime/configdir/configdir_test.go
+++ b/pkg/extension/workspace/example/runtime/configdir/configdir_test.go
@@ -99,7 +99,7 @@ func TestNewConfigDir(t *testing.T) {
 			require.NotNil(t, bootstrap)
 			require.Equal(t, "/dev/null", bootstrap.GetAdmin().GetAccessLogPath())
 			require.Equal(t, "127.0.0.1", bootstrap.GetAdmin().GetAddress().GetSocketAddress().GetAddress())
-			require.Equal(t, uint32(9901), bootstrap.GetAdmin().GetAddress().GetSocketAddress().GetPortValue())
+			require.Equal(t, uint32(0), bootstrap.GetAdmin().GetAddress().GetSocketAddress().GetPortValue())
 
 			// verify contents of the config dir
 			for _, fileName := range ctx.Opts.Example.GetFiles().GetNames() {
diff --git a/pkg/extension/workspace/example/runtime/getenvoy/runtime_test.go b/pkg/extension/workspace/example/runtime/getenvoy/runtime_test.go
index 9815447..e20ef95 100644
--- a/pkg/extension/workspace/example/runtime/getenvoy/runtime_test.go
+++ b/pkg/extension/workspace/example/runtime/getenvoy/runtime_test.go
@@ -52,12 +52,12 @@ func TestRuntimeRun(t *testing.T) {
 	require.NoError(t, err, `expected no error running running [%v]`, ctx)
 
 	// The working directory of envoy is a temp directory not controlled by this test, so we have to parse it.
-	envoyWd := cmd.ParseEnvoyWorkDirectory(stdout)
+	envoyWd := cmd.ParseEnvoyWorkDirectory(t, stdout.String(), `couldn't find envoy wd running [%v]`, ctx)
 
 	// Verify we executed the indicated envoy binary, and it captured the arguments we expected
 	expectedStdout := fmt.Sprintf(`envoy pwd: %s
 envoy bin: %s
-envoy args: -c %s/envoy.tmpl.yaml
+envoy args: -c %s/envoy.tmpl.yaml --admin-address-path /admin-address.txt
 `, envoyWd, ctx.Opts.Envoy.Path, envoyWd)
 	require.Equal(t, expectedStdout, stdout.String(), `expected stdout running [%v]`, ctx)
 
@@ -109,6 +109,8 @@ func runContext(workspace model.Workspace, example model.Example, envoyPath stri
 			},
 			Envoy: runtime.EnvoyOpts{
 				Path: envoyPath,
+				// prevents generated admin-address-path which makes assertions difficult
+				Args: []string{"--admin-address-path", "/admin-address.txt"},
 			},
 		},
 		IO: ioutil.StdStreams{
diff --git a/pkg/test/cmd/command.go b/pkg/test/cmd/command.go
index b6a890c..e7076ef 100644
--- a/pkg/test/cmd/command.go
+++ b/pkg/test/cmd/command.go
@@ -109,8 +109,8 @@ echo >&2 envoy stderr
 }
 
 // ParseEnvoyWorkDirectory returns the CWD captured by the script generated by InitFakeEnvoyHome.
-func ParseEnvoyWorkDirectory(stdout *bytes.Buffer) string {
+func ParseEnvoyWorkDirectory(t *testing.T, stdout string, msgAndArgs ...interface{}) string {
 	re := regexp.MustCompile(`.*envoy pwd: (.*)\n.*`)
-	envoyWd := re.FindStringSubmatch(stdout.String())[1]
-	return envoyWd
+	require.Regexp(t, re, stdout, msgAndArgs...)
+	return re.FindStringSubmatch(stdout)[1]
 }
diff --git a/test/e2e/getenvoy_extension_run_test.go b/test/e2e/getenvoy_extension_run_test.go
index 9068fb2..2ff2a89 100644
--- a/test/e2e/getenvoy_extension_run_test.go
+++ b/test/e2e/getenvoy_extension_run_test.go
@@ -15,20 +15,13 @@
 package e2e_test
 
 import (
-	"os"
-	"path/filepath"
-	"regexp"
 	"testing"
 	"time"
 
-	"github.com/mholt/archiver/v3"
 	"github.com/stretchr/testify/require"
 	"github.com/tetratelabs/log"
 
-	"github.com/tetratelabs/getenvoy/pkg/common"
 	. "github.com/tetratelabs/getenvoy/pkg/test/morerequire"
-	e2e "github.com/tetratelabs/getenvoy/test/e2e/util"
-	utilenvoy "github.com/tetratelabs/getenvoy/test/e2e/util/envoy"
 )
 
 const extensionName = "getenvoy_extension_run"
@@ -56,35 +49,18 @@ func TestGetEnvoyExtensionRun(t *testing.T) {
 			requireExtensionInit(t, workDir, test.Category, test.Language, extensionName)
 			defer requireExtensionClean(t, workDir)
 
-			// "getenvoy extension run" only returns stdout because `docker run -t` redirects stderr to stdout.
 			c := getEnvoy("extension run").Args(getToolchainContainerOptions()...)
-			_, stderr, terminate := c.Start(t, terminateTimeout)
+			stdout, stderr, terminate := c.Start(t, terminateTimeout)
 
 			// The underlying call is conditional to ensure errors that raise before we stop the server, stop it.
 			deferredTerminate := terminate
 			defer func() {
-				deferredTerminate()
+				if deferredTerminate != nil {
+					deferredTerminate()
+				}
 			}()
 
-			stderrLines := e2e.StreamLines(stderr).Named("stderr")
-
-			log.Infof(`waiting for Envoy Admin address to get logged after running [%v]`, c)
-			adminAddressPattern := regexp.MustCompile(`admin address: ([^:]+:[0-9]+)`)
-			line, err := stderrLines.FirstMatch(adminAddressPattern).Wait(10 * time.Minute) // give time to compile the extension
-			require.NoError(t, err, `error parsing admin address from stderr of [%v]`, c)
-			adminAddress := adminAddressPattern.FindStringSubmatch(line)[1]
-
-			log.Infof(`waiting for Envoy start-up to complete after running [%v]`, c)
-			_, err = stderrLines.FirstMatch(regexp.MustCompile(`starting main dispatch loop`)).Wait(1 * time.Minute)
-			require.NoError(t, err, `error parsing startup from stderr of [%v]`, c)
-
-			log.Infof(`waiting for Envoy client to connect after running [%v]`, c)
-			envoyClient, err := utilenvoy.NewClient(adminAddress)
-			require.NoError(t, err, `error from envoy client %s after running [%v]`, adminAddress, c)
-			require.Eventually(t, func() bool {
-				ready, e := envoyClient.IsReady()
-				return e == nil && ready
-			}, 1*time.Minute, 100*time.Millisecond, `envoy client %s never ready after running [%v]`, adminAddress, c)
+			envoyClient := requireEnvoyReady(t, stdout, stderr, c)
 
 			log.Infof(`waiting for Wasm extensions after running [%v]`, c)
 			require.Eventually(t, func() bool {
@@ -96,65 +72,13 @@ func TestGetEnvoyExtensionRun(t *testing.T) {
 				concurrency := stats.GetMetric("server.concurrency")
 				activeWasmVms := stats.GetMetric("wasm.envoy.wasm.runtime.v8.active")
 				return concurrency != nil && activeWasmVms != nil && activeWasmVms.Value == concurrency.Value+2
-			}, 1*time.Minute, 100*time.Millisecond, `wasm stats never found after running [%v]`, adminAddress, c)
+			}, 1*time.Minute, 100*time.Millisecond, `wasm stats never found after running [%v]`, c)
 
 			log.Infof(`stopping Envoy after running [%v]`, c)
 			terminate()
-			deferredTerminate = func() {
-				// no-op as we already terminated
-			}
-
-			// verify the debug dump of Envoy state has been taken
-			files, err := os.ReadDir(debugDir)
-			require.NoError(t, err, `error reading %s after stopping [%v]`, debugDir, c)
-			require.Equal(t, 1, len(files), `expected 1 file in %s after stopping [%v]`, debugDir, c)
-			defer func() {
-				e := os.RemoveAll(debugDir)
-				require.NoError(t, e, `error removing debug dir %s after stopping [%v]`, debugDir, c)
-			}()
+			deferredTerminate = nil
 
-			// get a listing of the debug archive
-			debugArchive := filepath.Join(debugDir, files[0].Name())
-			var dumpFiles []string
-			err = archiver.Walk(filepath.Join(debugDir, files[0].Name()), func(f archiver.File) error {
-				dumpFiles = append(dumpFiles, f.Name())
-				return nil
-			})
-			require.NoError(t, err, `error reading debug archive %s after stopping [%v]`, debugArchive, c)
-
-			// ensure the minimum contents exist
-			for _, file := range []string{"config_dump.json", "stats.json"} {
-				require.Contains(t, dumpFiles, file, `debug archive %s doesn't contain %s after stopping [%v]`, debugArchive, file, c)
-			}
+			verifyDebugDump(t, debugDir, c)
 		})
 	}
 }
-
-// backupDebugDir backs up ${GETENVOY_HOME}/debug in case the test hasn't overridden it and the developer has existing
-// data there. The function returned reverts this directory.
-//
-// Typically, this will run in the default ~/.getenvoy directory, as a means to avoid re-downloads of files such as
-// .getenvoy/builds/standard/1.17.1/darwin/bin/envoy (~100MB)
-//
-// While CI usually overrides the `HOME` variable with E2E_TOOLCHAIN_CONTAINER_OPTIONS, a developer may be
-// running this on their laptop. To avoid clobbering their old debug data, backup the
-func backupDebugDir(t *testing.T) (string, func()) {
-	debugDir := filepath.Join(common.HomeDir, "debug")
-
-	if _, err := os.Lstat(debugDir); err != nil && os.IsNotExist(err) {
-		return debugDir, func() {} // do nothing on remove, if there was no debug backup
-	}
-
-	// get a name of a new temp directory, which we'll rename the existing debug to
-	backupDir, _ := RequireNewTempDir(t)
-	err := os.RemoveAll(backupDir)
-	require.NoError(t, err, `error removing temp directory: %s`, backupDir)
-
-	err = os.Rename(debugDir, backupDir)
-	require.NoError(t, err, `error renaming debug dir %s to %s`, debugDir, backupDir)
-
-	return debugDir, func() {
-		err = os.Rename(backupDir, debugDir)
-		require.NoError(t, err, `error renaming backup dir %s to %s`, debugDir, backupDir)
-	}
-}
diff --git a/test/e2e/getenvoy_run_test.go b/test/e2e/getenvoy_run_test.go
new file mode 100644
index 0000000..37fa4ca
--- /dev/null
+++ b/test/e2e/getenvoy_run_test.go
@@ -0,0 +1,142 @@
+// Copyright 2020 Tetrate
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package e2e_test
+
+import (
+	"io"
+	"os"
+	"path/filepath"
+	"regexp"
+	"testing"
+	"time"
+
+	"github.com/mholt/archiver/v3"
+	"github.com/stretchr/testify/require"
+	"github.com/tetratelabs/log"
+
+	"github.com/tetratelabs/getenvoy/pkg/common"
+	. "github.com/tetratelabs/getenvoy/pkg/test/morerequire"
+	e2e "github.com/tetratelabs/getenvoy/test/e2e/util"
+	utilenvoy "github.com/tetratelabs/getenvoy/test/e2e/util/envoy"
+)
+
+// TestGetEnvoyRun runs the equivalent of "getenvoy run"
+//
+// See TestMain for general notes on about the test runtime.
+func TestGetEnvoyRun(t *testing.T) {
+	debugDir, revertOriginalDebugDir := backupDebugDir(t)
+	defer revertOriginalDebugDir()
+
+	c := getEnvoy(`run standard:1.17.1`). // TODO #106
+						Args(`--`, `--config-yaml`). // Below is the minimal config needed to run envoy
+						Arg(`admin: {access_log_path: '/dev/stdout', address: {socket_address: {address: '127.0.0.1', port_value: 0}}}`)
+
+	stdout, stderr, terminate := c.Start(t, terminateTimeout)
+
+	// The underlying call is conditional to ensure errors that raise before we stop the server, stop it.
+	deferredTerminate := terminate
+	defer func() {
+		if deferredTerminate != nil {
+			deferredTerminate()
+		}
+	}()
+
+	requireEnvoyReady(t, stdout, stderr, c)
+
+	log.Infof(`stopping Envoy after running [%v]`, c)
+	terminate()
+	deferredTerminate = nil
+
+	verifyDebugDump(t, debugDir, c)
+}
+
+// backupDebugDir backs up ${GETENVOY_HOME}/debug in case the test hasn't overridden it and the developer has existing
+// data there. The function returned reverts this directory.
+//
+// Typically, this will run in the default ~/.getenvoy directory, as a means to avoid re-downloads of files such as
+// .getenvoy/builds/standard/1.17.1/darwin/bin/envoy (~100MB)
+//
+// While CI usually overrides the `HOME` variable with E2E_TOOLCHAIN_CONTAINER_OPTIONS, a developer may be
+// running this on their laptop. To avoid clobbering their old debug data, backup the
+func backupDebugDir(t *testing.T) (string, func()) {
+	debugDir := filepath.Join(common.HomeDir, "debug")
+
+	if _, err := os.Lstat(debugDir); err != nil && os.IsNotExist(err) {
+		return debugDir, func() {} // do nothing on remove, if there was no debug backup
+	}
+
+	// get a name of a new temp directory, which we'll rename the existing debug to
+	backupDir, _ := RequireNewTempDir(t)
+	err := os.RemoveAll(backupDir)
+	require.NoError(t, err, `error removing temp directory: %s`, backupDir)
+
+	err = os.Rename(debugDir, backupDir)
+	require.NoError(t, err, `error renaming debug dir %s to %s`, debugDir, backupDir)
+
+	return debugDir, func() {
+		err = os.Rename(backupDir, debugDir)
+		require.NoError(t, err, `error renaming backup dir %s to %s`, debugDir, backupDir)
+	}
+}
+
+func requireEnvoyReady(t *testing.T, stdout, stderr io.Reader, c interface{}) utilenvoy.AdminAPI {
+	stdoutLines := e2e.StreamLines(stdout).Named("stdout")
+	stderrLines := e2e.StreamLines(stderr).Named("stderr")
+
+	log.Infof(`waiting for Envoy Admin address to get logged after running [%v]`, c)
+	adminAddressPattern := regexp.MustCompile(`discovered admin address: ([^:]+:[0-9]+)`)
+	line, err := stdoutLines.FirstMatch(adminAddressPattern).Wait(10 * time.Minute) // give time to compile the extension
+	require.NoError(t, err, `error parsing admin address from stdout of [%v]`, c)
+	adminAddress := adminAddressPattern.FindStringSubmatch(line)[1]
+
+	log.Infof(`waiting for Envoy start-up to complete after running [%v]`, c)
+	_, err = stderrLines.FirstMatch(regexp.MustCompile(`starting main dispatch loop`)).Wait(1 * time.Minute)
+	require.NoError(t, err, `error parsing startup from stderr of [%v]`, c)
+
+	log.Infof(`waiting for Envoy client to connect after running [%v]`, c)
+	envoyClient, err := utilenvoy.NewClient(adminAddress)
+	require.NoError(t, err, `error from envoy client %s after running [%v]`, adminAddress, c)
+	require.Eventually(t, func() bool {
+		ready, e := envoyClient.IsReady()
+		return e == nil && ready
+	}, 1*time.Minute, 100*time.Millisecond, `envoy client %s never ready after running [%v]`, adminAddress, c)
+
+	return envoyClient
+}
+
+func verifyDebugDump(t *testing.T, debugDir string, c interface{}) {
+	// verify the debug dump of Envoy state has been taken
+	files, err := os.ReadDir(debugDir)
+	require.NoError(t, err, `error reading %s after stopping [%v]`, debugDir, c)
+	require.Equal(t, 1, len(files), `expected 1 file in %s after stopping [%v]`, debugDir, c)
+	defer func() {
+		e := os.RemoveAll(debugDir)
+		require.NoError(t, e, `error removing debug dir %s after stopping [%v]`, debugDir, c)
+	}()
+
+	// get a listing of the debug archive
+	debugArchive := filepath.Join(debugDir, files[0].Name())
+	var dumpFiles []string
+	err = archiver.Walk(filepath.Join(debugDir, files[0].Name()), func(f archiver.File) error {
+		dumpFiles = append(dumpFiles, f.Name())
+		return nil
+	})
+	require.NoError(t, err, `error reading debug archive %s after stopping [%v]`, debugArchive, c)
+
+	// ensure the minimum contents exist
+	for _, file := range []string{"config_dump.json", "stats.json"} {
+		require.Contains(t, dumpFiles, file, `debug archive %s doesn't contain %s after stopping [%v]`, debugArchive, file, c)
+	}
+}