Repository: tetratelabs/getenvoy (go)

Issue:
Revert `getenvoy run` to excute envoy in the CWD
Right now, `getenvoy run` executes envoy in a different directory (the "run" directory). This was a mistake in hindsight because it makes running envoy more difficult as the config file must be an absolute path.

Ex. this won't work
```
$ getenvoy run -c ./my-config.yml
```

This will
```bash
$ getenvoy run -c $PWD/my-config.yml
```

However, the $PWD trick won't work with included files, as those are resolved by envoy based on the cwd, not relative to the path of the config file.

Ex. relative paths like this:
```yaml
                          vm_config:
                            vm_id: "my_vm_id"
                            runtime: "envoy.wasm.runtime.v8"
                            code:
                              local:
                                filename: "./examples/http_body/main.go.wasm"
```

We need to change the behavior to execute in the current working directory and backfill an E2E test that proves file resolution relative to the config file works.

Pull request:
Runs Envoy in the current working directory
This makes sure Envoy is run in the current working directory, so that
it is easier to use and configuration can locate relative paths.

Fixes #263

Patch:
diff --git a/.licenserignore b/.licenserignore
index a72a569..1e2b10d 100644
--- a/.licenserignore
+++ b/.licenserignore
@@ -16,3 +16,4 @@ last_known_envoy.txt
 
 testdata/
 site/*.json
+e2e/*.yaml
diff --git a/internal/cmd/run.go b/internal/cmd/run.go
index 9d5ceff..79648c3 100644
--- a/internal/cmd/run.go
+++ b/internal/cmd/run.go
@@ -37,17 +37,16 @@ func NewRunCmd(o *globals.GlobalOpts) *cli.Command {
 		Usage:           "Run Envoy with the given [arguments...], collecting process state on termination",
 		ArgsUsage:       "[arguments...]",
 		SkipFlagParsing: true,
-		Description: fmt.Sprintf(`
-Envoy interprets the '[arguments...]'.
+		Description: `To run Envoy, execute ` + "`getenvoy run -c your_envoy_config.yaml`" + `. This
+downloads and installs the latest version of Envoy for you.
 
-The "use" command primarily controls the version. The first version in %s is
-run, installing on-demand as needed.
+Envoy runs in the current directory and interprets the '[arguments...]'.
+The first version in the below is run, controllable by the "use" command:
+` + fmt.Sprintf("```\n%s\n```", envoy.VersionUsageList()) + `
 
-Envoy uses $GETENVOY_HOME/runs/$epochtime as the working directory.
-Upon termination, this is archived as $GETENVOY_HOME/runs/$epochtime.tar.gz.
-
-Example:
-$ getenvoy run -c ./bootstrap.yaml`, envoy.VersionUsageList()),
+getenvoy generates files, e.g. logs, into ` + "`$GETENVOY_HOME/runs/$epochtime`" + `.
+These archive into ` + "`$GETENVOY_HOME/runs/$epochtime.tar.gz`" + ` upon termination.
+`,
 		Before: func(context *cli.Context) error {
 			if err := os.MkdirAll(o.HomeDir, 0750); err != nil {
 				return NewValidationError(err.Error())
@@ -97,16 +96,15 @@ func initializeRunOpts(o *globals.GlobalOpts, p, v string) error {
 		}
 		o.EnvoyPath = envoyPath
 	}
-	if runOpts.WorkingDir == "" { // not overridden for tests
-		// Historically, the directory run files wrote to was called DebugStore
+	if runOpts.RunDir == "" { // not overridden for tests
 		runID := strconv.FormatInt(time.Now().UnixNano(), 10)
-		workingDir := filepath.Join(filepath.Join(o.HomeDir, "runs"), runID)
+		runDir := filepath.Join(filepath.Join(o.HomeDir, "runs"), runID)
 
 		// When the directory is implicitly generated, we should create it to avoid late errors.
-		if err := os.MkdirAll(workingDir, 0750); err != nil {
-			return NewValidationError("unable to create working directory %q, so we cannot run envoy", workingDir)
+		if err := os.MkdirAll(runDir, 0750); err != nil {
+			return NewValidationError("unable to create working directory %q, so we cannot run envoy", runDir)
 		}
-		runOpts.WorkingDir = workingDir
+		runOpts.RunDir = runDir
 	}
 	return nil
 }
diff --git a/internal/envoy/debug/admin.go b/internal/envoy/debug/admin.go
index d886b64..d1811b9 100644
--- a/internal/envoy/debug/admin.go
+++ b/internal/envoy/debug/admin.go
@@ -38,7 +38,7 @@ var adminAPIPaths = map[string]string{
 
 // enableEnvoyAdminDataCollection is a preset option that registers collection of Envoy Admin API information
 func enableEnvoyAdminDataCollection(r *envoy.Runtime) error {
-	e := envoyAdminDataCollection{r.GetAdminAddress, r.GetWorkingDir()}
+	e := envoyAdminDataCollection{r.GetAdminAddress, r.GetRunDir()}
 	r.RegisterPreTermination(e.retrieveAdminAPIData)
 	return nil
 }
diff --git a/internal/envoy/debug/log.go b/internal/envoy/debug/log.go
index 0038b6a..7521816 100644
--- a/internal/envoy/debug/log.go
+++ b/internal/envoy/debug/log.go
@@ -25,7 +25,7 @@ import (
 
 // enableEnvoyLogCollection is a preset option that registers collection of Envoy access logs and stderr
 func enableEnvoyLogCollection(r *envoy.Runtime) error {
-	logsDir := filepath.Join(r.GetWorkingDir(), "logs")
+	logsDir := filepath.Join(r.GetRunDir(), "logs")
 	if err := os.MkdirAll(logsDir, 0750); err != nil {
 		return fmt.Errorf("unable to create directory %q, so no logs will be captured: %w", logsDir, err)
 	}
diff --git a/internal/envoy/debug/lsof.go b/internal/envoy/debug/lsof.go
index 689288d..2a07a45 100644
--- a/internal/envoy/debug/lsof.go
+++ b/internal/envoy/debug/lsof.go
@@ -46,7 +46,7 @@ type OpenFileStat struct {
 // enableOpenFilesDataCollection is a preset option that registers collection of statistics of files opened by envoy
 // instance(s).
 func enableOpenFilesDataCollection(r *envoy.Runtime) error {
-	lsofDir := filepath.Join(r.GetWorkingDir(), "lsof")
+	lsofDir := filepath.Join(r.GetRunDir(), "lsof")
 	if err := os.MkdirAll(lsofDir, 0750); err != nil {
 		return fmt.Errorf("unable to create directory %q, so lsof will not be captured: %w", lsofDir, err)
 	}
diff --git a/internal/envoy/debug/node.go b/internal/envoy/debug/node.go
index e67e77c..7e347d7 100644
--- a/internal/envoy/debug/node.go
+++ b/internal/envoy/debug/node.go
@@ -32,7 +32,7 @@ import (
 
 // enableNodeCollection is a preset option that registers collection of node level information for debugging
 func enableNodeCollection(r *envoy.Runtime) error {
-	nodeDir := filepath.Join(r.GetWorkingDir(), "node")
+	nodeDir := filepath.Join(r.GetRunDir(), "node")
 	if err := os.MkdirAll(nodeDir, 0750); err != nil {
 		return fmt.Errorf("unable to create directory %q, so node data will not be captured: %w", nodeDir, err)
 	}
diff --git a/internal/envoy/run.go b/internal/envoy/run.go
index 30313e6..4710d4a 100644
--- a/internal/envoy/run.go
+++ b/internal/envoy/run.go
@@ -30,7 +30,6 @@ func (r *Runtime) Run(ctx context.Context, args []string) (err error) {
 	// We can't use CommandContext even if that seems correct here. The reason is that we need to invoke preTerminate
 	// handlers, and they expect the process to still be running. For example, this allows admin API hooks.
 	cmd := exec.Command(r.opts.EnvoyPath, args...) // #nosec -> users can run whatever binary they like!
-	cmd.Dir = r.opts.WorkingDir
 	cmd.Stdout = r.Out
 	cmd.Stderr = r.Err
 	cmd.SysProcAttr = sysProcAttr()
@@ -52,9 +51,7 @@ func (r *Runtime) Run(ctx context.Context, args []string) (err error) {
 	}
 
 	// Print the process line to the console for user knowledge and parsing convenience
-	a := strings.Join(append([]string{r.opts.EnvoyPath}, args...), " ") // ensures no trailing space on empty args
-	fmt.Fprintln(r.Out, "starting:", a)                                 //nolint
-	fmt.Fprintln(r.Out, "working directory:", cmd.Dir)                  //nolint
+	fmt.Fprintln(r.Out, "starting:", strings.Join(r.cmd.Args, " ")) //nolint
 	if err := cmd.Start(); err != nil {
 		return fmt.Errorf("unable to start Envoy process: %w", err)
 	}
diff --git a/internal/envoy/runtime.go b/internal/envoy/runtime.go
index e2d76f9..2d1c023 100644
--- a/internal/envoy/runtime.go
+++ b/internal/envoy/runtime.go
@@ -26,7 +26,7 @@ import (
 	"github.com/tetratelabs/getenvoy/internal/globals"
 )
 
-// NewRuntime creates a new Runtime that runs envoy in globals.RunOpts WorkingDir
+// NewRuntime creates a new Runtime that runs envoy in globals.RunOpts RunDir
 // opts allows a user running envoy to control the working directory by ID or path, allowing explicit cleanup.
 func NewRuntime(opts *globals.RunOpts) *Runtime {
 	return &Runtime{opts: opts}
@@ -49,9 +49,9 @@ type Runtime struct {
 	preStart, preTermination, postTermination []func() error
 }
 
-// GetWorkingDir returns the run-specific directory files can be written to.
-func (r *Runtime) GetWorkingDir() string {
-	return r.opts.WorkingDir
+// GetRunDir returns the run-specific directory files can be written to.
+func (r *Runtime) GetRunDir() string {
+	return r.opts.RunDir
 }
 
 // GetAdminAddress returns the current admin address in host:port format, or empty if not yet available.
diff --git a/internal/envoy/start.go b/internal/envoy/start.go
index e5f0dda..493ce61 100644
--- a/internal/envoy/start.go
+++ b/internal/envoy/start.go
@@ -55,7 +55,7 @@ func (r *Runtime) ensureAdminAddressPath() error {
 		}
 	}
 	// Envoy's run directory is mutable, so it is fine to write the admin address there.
-	r.cmd.Args = append(r.cmd.Args, flag, "admin-address.txt")
-	r.adminAddressPath = filepath.Join(r.opts.WorkingDir, "admin-address.txt")
+	r.adminAddressPath = filepath.Join(r.opts.RunDir, "admin-address.txt")
+	r.cmd.Args = append(r.cmd.Args, flag, r.adminAddressPath)
 	return nil
 }
diff --git a/internal/envoy/termination.go b/internal/envoy/termination.go
index 08204f0..29d5d04 100644
--- a/internal/envoy/termination.go
+++ b/internal/envoy/termination.go
@@ -48,19 +48,19 @@ func (r *Runtime) handlePostTermination() error {
 		}
 	}
 
-	if r.opts.DontArchiveWorkingDir {
+	if r.opts.DontArchiveRunDir {
 		return nil
 	}
 
 	// Given ~/.getenvoy/debug/1620955405964267000
-	dirName := filepath.Dir(r.GetWorkingDir())              // ~/.getenvoy/debug
-	baseName := filepath.Base(r.GetWorkingDir())            // 1620955405964267000
+	dirName := filepath.Dir(r.GetRunDir())                  // ~/.getenvoy/debug
+	baseName := filepath.Base(r.GetRunDir())                // 1620955405964267000
 	targzName := filepath.Join(dirName, baseName+".tar.gz") // ~/.getenvoy/debug/1620955405964267000.tar.gz
 
-	if err := tar.TarGz(targzName, r.GetWorkingDir()); err != nil {
-		return fmt.Errorf("unable to archive run directory %v: %w", r.GetWorkingDir(), err)
+	if err := tar.TarGz(targzName, r.GetRunDir()); err != nil {
+		return fmt.Errorf("unable to archive run directory %v: %w", r.GetRunDir(), err)
 	}
-	return os.RemoveAll(r.GetWorkingDir())
+	return os.RemoveAll(r.GetRunDir())
 }
 
 // RegisterPreTermination registers the passed functions to be run after Envoy has started
diff --git a/internal/globals/globals.go b/internal/globals/globals.go
index 38b8bec..c05b767 100644
--- a/internal/globals/globals.go
+++ b/internal/globals/globals.go
@@ -24,12 +24,14 @@ import (
 type RunOpts struct {
 	// EnvoyPath is the exec.Cmd path to "envoy". Defaults to "$HomeDir/versions/$version/bin/envoy"
 	EnvoyPath string
-	// WorkingDir is the working directory of EnvoyPath and includes any generated configuration or debug files.
-	// Upon termination, this directory is archived as "../$(basename $WorkingDir).tar.gz"
+	// RunDir is the location any generated configuration or debug files are written.
+	// This is not Envoy's working directory, which remains the same as the $PWD of getenvoy.
+	//
+	// Upon termination, this directory is archived as "../$(basename $RunDir).tar.gz"
 	// Defaults to "$HomeDir/runs/$epochtime"
-	WorkingDir string
-	// DontArchiveWorkingDir is used in testing and prevents archiving the WorkingDir
-	DontArchiveWorkingDir bool
+	RunDir string
+	// DontArchiveRunDir is used in testing and prevents archiving the RunDir
+	DontArchiveRunDir bool
 }
 
 // GlobalOpts represents options that affect more than one getenvoy commands.


Test patch:
diff --git a/e2e/admin_test.go b/e2e/admin_test.go
index ef6bc4c..47ded23 100644
--- a/e2e/admin_test.go
+++ b/e2e/admin_test.go
@@ -22,37 +22,20 @@ import (
 	"net/http"
 )
 
-// adminAPI represents Envoy Admin API.
-type adminAPI interface {
-	isReady() (bool, error)
-	getStats() (*stats, error)
-}
-
-// stats represents Envoy response to `/stats?format=json` endpoint.
-type stats struct {
-	Metrics []metric `json:"stats"`
-}
-
-// metric represents recorded value of a single metric.
-type metric struct {
-	Name  string  `json:"name"`
-	Value float64 `json:"value"`
-}
-
-// newClient returns a new client for Envoy Admin API.
-func newClient(address string) (adminAPI, error) {
+// newAdminClient returns a new client for Envoy Admin API.
+func newAdminClient(address string) (*adminClient, error) {
 	host, port, err := net.SplitHostPort(address)
 	if err != nil {
 		return nil, err
 	}
-	return &client{baseURL: fmt.Sprintf("http://%s:%s", host, port)}, nil
+	return &adminClient{baseURL: fmt.Sprintf("http://%s:%s", host, port)}, nil
 }
 
-type client struct {
+type adminClient struct {
 	baseURL string
 }
 
-func (c *client) isReady() (bool, error) {
+func (c *adminClient) isReady() (bool, error) {
 	resp, err := http.Get(c.baseURL + "/ready")
 	if err != nil {
 		return false, err
@@ -61,23 +44,40 @@ func (c *client) isReady() (bool, error) {
 	return resp.StatusCode == http.StatusOK, nil
 }
 
-func (c *client) getStats() (*stats, error) {
-	resp, err := http.Get(c.baseURL + "/stats?format=json")
-	if err != nil {
-		return nil, err
+func (c *adminClient) getMainListenerURL() (string, error) {
+	var s map[string]interface{}
+	if err := c.getJSON("/listeners", &s); err != nil {
+		return "", err
 	}
-	defer resp.Body.Close() //nolint:errcheck
-	if resp.StatusCode != http.StatusOK {
-		return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+
+	// The json structure is deep, so parsing instead of many nested structs
+	for _, s := range s["listener_statuses"].([]interface{}) {
+		l := s.(map[string]interface{})
+		if l["name"] != "main" {
+			continue
+		}
+		port := l["local_address"].(map[string]interface{})["socket_address"].(map[string]interface{})["port_value"]
+		return fmt.Sprintf("http://127.0.0.1:%d", int(port.(float64))), nil
 	}
-	body, err := io.ReadAll(resp.Body)
+	return "", fmt.Errorf("didn't find main port in %+v", s)
+}
+
+func (c *adminClient) getJSON(path string, v interface{}) error {
+	body, err := httpGet(c.baseURL + path + "?format=json")
 	if err != nil {
-		return nil, err
+		return err
 	}
-	var s stats
-	err = json.Unmarshal(body, &s)
+	return json.Unmarshal(body, v)
+}
+
+func httpGet(url string) ([]byte, error) {
+	resp, err := http.Get(url)
 	if err != nil {
 		return nil, err
 	}
-	return &s, nil
+	defer resp.Body.Close() //nolint:errcheck
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+	}
+	return io.ReadAll(resp.Body)
 }
diff --git a/e2e/command_test.go b/e2e/command_test.go
index 1a54352..61f1197 100644
--- a/e2e/command_test.go
+++ b/e2e/command_test.go
@@ -40,11 +40,6 @@ func getEnvoy(args ...string) *cmdBuilder { //nolint:golint
 	return &cmdBuilder{exec.Command(getEnvoyPath, args...)} //nolint:gosec
 }
 
-func (b *cmdBuilder) args(args ...string) *cmdBuilder {
-	b.cmd.Args = append(b.cmd.Args, args...)
-	return b
-}
-
 func (b *cmdBuilder) String() string {
 	return fmt.Sprintf("%s: %s", b.cmd.Dir, strings.Join(b.cmd.Args, " "))
 }
diff --git a/e2e/getenvoy_run_test.go b/e2e/getenvoy_run_test.go
index 20bc77c..bf36d8a 100644
--- a/e2e/getenvoy_run_test.go
+++ b/e2e/getenvoy_run_test.go
@@ -15,6 +15,7 @@
 package e2e
 
 import (
+	_ "embed" // We embed the config files to make them easier to copy
 	"io"
 	"log"
 	"os"
@@ -26,6 +27,7 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/tetratelabs/getenvoy/internal/tar"
+	"github.com/tetratelabs/getenvoy/internal/test/morerequire"
 )
 
 const terminateTimeout = 2 * time.Minute
@@ -36,9 +38,8 @@ const terminateTimeout = 2 * time.Minute
 func TestGetEnvoyRun(t *testing.T) {
 	t.Parallel() // uses random ports so safe to run parallel
 
-	c := getEnvoy(`run`)
 	// Below is the minimal config needed to run envoy
-	c.args("--config-yaml", "admin: {access_log_path: '/dev/stdout', address: {socket_address: {address: '127.0.0.1', port_value: 0}}}")
+	c := getEnvoy(`run`, "--config-yaml", "admin: {access_log_path: '/dev/stdout', address: {socket_address: {address: '127.0.0.1', port_value: 0}}}")
 
 	stdout, stderr, terminate := c.start(t, terminateTimeout)
 
@@ -50,43 +51,74 @@ func TestGetEnvoyRun(t *testing.T) {
 		}
 	}()
 
-	envoyWorkingDir := requireEnvoyWorkingDir(t, stdout, c)
-	requireEnvoyReady(t, envoyWorkingDir, stderr, c)
+	runDir := requireRunDir(t, stdout, c)
+	requireEnvoyReady(t, runDir, stderr, c)
 
 	log.Printf(`stopping Envoy after running [%v]`, c)
 	terminate()
 	deferredTerminate = nil
 
-	verifyDebugDump(t, envoyWorkingDir, c)
+	verifyDebugDump(t, runDir, c)
 }
 
-func requireEnvoyWorkingDir(t *testing.T, stdout io.Reader, c interface{}) string {
+// staticFilesystemConfig shows envoy reading a file referenced from the CWD
+//go:embed static-filesystem.yaml
+var staticFilesystemConfig []byte
+
+func TestGetEnvoyRun_StaticFilesystem(t *testing.T) {
+	t.Parallel() // uses random ports so safe to run parallel
+
+	revertTempWd := morerequire.RequireChdirIntoTemp(t)
+	defer revertTempWd()
+
+	require.NoError(t, os.WriteFile("envoy.yaml", staticFilesystemConfig, 0600))
+	responseFromWorkingDirectory := []byte("foo")
+	require.NoError(t, os.WriteFile("response.txt", responseFromWorkingDirectory, 0600))
+	c := getEnvoy(`run`, "-c", "envoy.yaml")
+
+	stdout, stderr, terminate := c.start(t, terminateTimeout)
+	defer terminate()
+
+	runDir := requireRunDir(t, stdout, c)
+	admin := requireEnvoyReady(t, runDir, stderr, c)
+
+	mainURL, err := admin.getMainListenerURL()
+	require.NoError(t, err, `couldn't read mainURL after running [%v]`, c)
+
+	body, err := httpGet(mainURL)
+	require.NoError(t, err, `couldn't read %s after running [%v]`, mainURL, c)
+
+	// If this passes, we know Envoy is running in the current directory, so can resolve relative configuration.
+	require.Equal(t, responseFromWorkingDirectory, body, `unexpected content in %s after running [%v]`, mainURL, c)
+}
+
+func requireRunDir(t *testing.T, stdout io.Reader, c interface{}) string {
 	stdoutLines := streamLines(stdout).named("stdout")
-	log.Printf(`waiting for GetEnvoy to log working directory after running [%v]`, c)
-	workingDirectoryPattern := regexp.MustCompile(`working directory: (.*)`)
-	line, err := stdoutLines.FirstMatch(workingDirectoryPattern).Wait(1 * time.Minute)
-	require.NoError(t, err, `error parsing working directory from stdout of [%v]`, c)
-	return workingDirectoryPattern.FindStringSubmatch(line)[1]
+	log.Printf(`waiting for GetEnvoy to log admin address path after running [%v]`, c)
+	adminAddressPathPattern := regexp.MustCompile(`--admin-address-path ([^ ]+)`)
+	line, err := stdoutLines.FirstMatch(adminAddressPathPattern).Wait(1 * time.Minute)
+	require.NoError(t, err, `error parsing admin address path from stdout of [%v]`, c)
+	return filepath.Dir(adminAddressPathPattern.FindStringSubmatch(line)[1])
 }
 
-func requireEnvoyReady(t *testing.T, envoyWorkingDir string, stderr io.Reader, c interface{}) adminAPI {
+func requireEnvoyReady(t *testing.T, runDir string, stderr io.Reader, c interface{}) *adminClient {
 	stderrLines := streamLines(stderr).named("stderr")
 
 	log.Printf(`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)
 
-	adminAddressPath := filepath.Join(envoyWorkingDir, "admin-address.txt")
+	adminAddressPath := filepath.Join(runDir, "admin-address.txt")
 	adminAddress, err := os.ReadFile(adminAddressPath) //nolint:gosec
 	require.NoError(t, err, `error reading admin address file %q after running [%v]`, adminAddressPath, c)
 
-	log.Printf(`waiting for Envoy client to connect after running [%v]`, c)
-	envoyClient, err := newClient(string(adminAddress))
-	require.NoError(t, err, `error from envoy client %s after running [%v]`, adminAddress, c)
+	log.Printf(`waiting for Envoy adminClient to connect after running [%v]`, c)
+	envoyClient, err := newAdminClient(string(adminAddress))
+	require.NoError(t, err, `error from envoy adminClient %s after running [%v]`, adminAddress, c)
 	require.Eventually(t, func() bool {
 		ready, err := envoyClient.isReady()
 		return err == nil && ready
-	}, 1*time.Minute, 100*time.Millisecond, `envoy client %s never ready after running [%v]`, adminAddress, c)
+	}, 1*time.Minute, 100*time.Millisecond, `envoy adminClient %s never ready after running [%v]`, adminAddress, c)
 
 	return envoyClient
 }
diff --git a/e2e/static-filesystem.yaml b/e2e/static-filesystem.yaml
new file mode 100644
index 0000000..9f7e9a8
--- /dev/null
+++ b/e2e/static-filesystem.yaml
@@ -0,0 +1,37 @@
+admin:
+  access_log_path: '/dev/stdout'
+  address:
+    socket_address:
+      address: '127.0.0.1'
+      port_value: 0
+
+static_resources:
+  listeners:
+    - name: main
+      address:
+        socket_address:
+          address: '127.0.0.1'
+          port_value: 0
+      filter_chains:
+        - filters:
+            - name: envoy.http_connection_manager
+              typed_config:
+                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                stat_prefix: ingress_http
+                codec_type: auto
+                route_config:
+                  name: local_route
+                  virtual_hosts:
+                    - name: local_service
+                      domains:
+                        - "*"
+                      routes:
+                        - match:
+                            prefix: "/"
+                          direct_response:
+                            status: 200
+                            body:
+                              filename: "response.txt"
+                http_filters:
+                  - name: envoy.filters.http.router
+                    typed_config: { }
diff --git a/internal/cmd/run_test.go b/internal/cmd/run_test.go
index 20f518a..d2b6521 100644
--- a/internal/cmd/run_test.go
+++ b/internal/cmd/run_test.go
@@ -42,31 +42,30 @@ func TestGetEnvoyRun(t *testing.T) {
 		{
 			name:              "envoy args",
 			args:              []string{"getenvoy", "run", "-c", "envoy.yaml"},
-			expectedEnvoyArgs: ` -c envoy.yaml`,
+			expectedEnvoyArgs: `-c envoy.yaml `,
 		},
 	}
 
-	for _, test := range tests {
-		test := test // pin! see https://github.com/kyoh86/scopelint for why
+	for _, tc := range tests {
+		tc := tc // pin! see https://github.com/kyoh86/scopelint for why
 
-		t.Run(test.name, func(t *testing.T) {
+		t.Run(tc.name, func(t *testing.T) {
 			o, cleanup := setupTest(t)
 			defer cleanup()
 
 			c, stdout, stderr := newApp(o)
 			o.Out = io.Discard // don't verify logging
-			err := c.Run(test.args)
+			err := c.Run(tc.args)
 
 			// Verify the command invoked, passing the correct default commandline
 			require.NoError(t, err)
 
 			// We expect getenvoy to print the context it will run, and Envoy to execute the same, except adding the
 			// --admin-address-path flag
-			expectedStdout := fmt.Sprintf(`starting: %[2]s%[3]s
-working directory: %[1]s
-envoy wd: %[1]s
-envoy bin: %[2]s
-envoy args:%[3]s --admin-address-path admin-address.txt`, o.WorkingDir, o.EnvoyPath, test.expectedEnvoyArgs)
+			expectedEnvoyArgs := fmt.Sprint(tc.expectedEnvoyArgs, "--admin-address-path ", filepath.Join(o.RunDir, "admin-address.txt"))
+			expectedStdout := fmt.Sprintf(`starting: %[1]s %[2]s
+envoy bin: %[1]s
+envoy args: %[2]s`, o.EnvoyPath, expectedEnvoyArgs)
 			require.Equal(t, expectedStdout+"\n", stdout.String())
 			require.Equal(t, "envoy stderr\n", stderr.String())
 		})
diff --git a/internal/cmd/testdata/getenvoy_run_help.txt b/internal/cmd/testdata/getenvoy_run_help.txt
index 1387f82..1229b11 100644
--- a/internal/cmd/testdata/getenvoy_run_help.txt
+++ b/internal/cmd/testdata/getenvoy_run_help.txt
@@ -5,13 +5,14 @@ USAGE:
    getenvoy run [arguments...]
 
 DESCRIPTION:
-   Envoy interprets the '[arguments...]'.
+   To run Envoy, execute `getenvoy run -c your_envoy_config.yaml`. This
+   downloads and installs the latest version of Envoy for you.
    
-   The "use" command primarily controls the version. The first version in $ENVOY_VERSION, $PWD/.envoy-version, $GETENVOY_HOME/version is
-   run, installing on-demand as needed.
+   Envoy runs in the current directory and interprets the '[arguments...]'.
+   The first version in the below is run, controllable by the "use" command:
+   ```
+   $ENVOY_VERSION, $PWD/.envoy-version, $GETENVOY_HOME/version
+   ```
    
-   Envoy uses $GETENVOY_HOME/runs/$epochtime as the working directory.
-   Upon termination, this is archived as $GETENVOY_HOME/runs/$epochtime.tar.gz.
-   
-   Example:
-   $ getenvoy run -c ./bootstrap.yaml
+   getenvoy generates files, e.g. logs, into `$GETENVOY_HOME/runs/$epochtime`.
+   These archive into `$GETENVOY_HOME/runs/$epochtime.tar.gz` upon termination.
diff --git a/internal/envoy/debug/admin_test.go b/internal/envoy/debug/admin_test.go
index c35be53..cff03c2 100644
--- a/internal/envoy/debug/admin_test.go
+++ b/internal/envoy/debug/admin_test.go
@@ -32,8 +32,8 @@ import (
 )
 
 func TestEnableEnvoyAdminDataCollection(t *testing.T) {
-	workingDir, removeWorkingDir := morerequire.RequireNewTempDir(t)
-	defer removeWorkingDir()
+	runDir, removeRunDir := morerequire.RequireNewTempDir(t)
+	defer removeRunDir()
 
 	mockAdmin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.WriteHeader(http.StatusOK)
@@ -41,14 +41,14 @@ func TestEnableEnvoyAdminDataCollection(t *testing.T) {
 	}))
 	defer mockAdmin.Close()
 
-	adminPath := filepath.Join(workingDir, "admin-address.txt")
+	adminPath := filepath.Join(runDir, "admin-address.txt")
 	err := os.WriteFile(adminPath, []byte(mockAdmin.Listener.Addr().String()), 0600)
 	require.NoError(t, err)
 
-	runAndTerminateWithDebug(t, workingDir, enableEnvoyAdminDataCollection, `--admin-address-path`, adminPath)
+	runAndTerminateWithDebug(t, runDir, enableEnvoyAdminDataCollection, `--admin-address-path`, adminPath)
 
 	for _, filename := range adminAPIPaths {
-		path := filepath.Join(workingDir, filename)
+		path := filepath.Join(runDir, filename)
 		f, err := os.Stat(path)
 		require.NoError(t, err, "error stating %v", path)
 		require.NotEmpty(t, f.Size(), "file %v was empty", path)
@@ -56,11 +56,11 @@ func TestEnableEnvoyAdminDataCollection(t *testing.T) {
 }
 
 // runAndTerminateWithDebug is like RequireRunTerminate, except returns a directory populated by the debug plugin.
-func runAndTerminateWithDebug(t *testing.T, workingDir string, debug func(r *envoy.Runtime) error, args ...string) error {
-	fakeEnvoy := filepath.Join(workingDir, "envoy")
+func runAndTerminateWithDebug(t *testing.T, runDir string, debug func(r *envoy.Runtime) error, args ...string) error {
+	fakeEnvoy := filepath.Join(runDir, "envoy")
 	morerequire.RequireCaptureScript(t, fakeEnvoy)
 
-	o := &globals.RunOpts{EnvoyPath: fakeEnvoy, WorkingDir: workingDir, DontArchiveWorkingDir: true}
+	o := &globals.RunOpts{EnvoyPath: fakeEnvoy, RunDir: runDir, DontArchiveRunDir: true}
 
 	stderr := new(bytes.Buffer)
 	r := envoy.NewRuntime(o)
diff --git a/internal/envoy/run_test.go b/internal/envoy/run_test.go
index 3e43ad6..4b4f9c7 100644
--- a/internal/envoy/run_test.go
+++ b/internal/envoy/run_test.go
@@ -36,8 +36,8 @@ func TestRuntime_Run(t *testing.T) {
 	defer removeTempDir()
 
 	runsDir := filepath.Join(tempDir, "runs")
-	fakeTimestamp := "1619574747231823000"
-	workingDir := filepath.Join(runsDir, fakeTimestamp)
+	runDir := filepath.Join(runsDir, "1619574747231823000") // fake a realistic value
+	adminFlag := fmt.Sprintf("--admin-address-path %s/admin-address.txt", runDir)
 
 	// "quiet" as we aren't testing the environment envoy runs in
 	fakeEnvoy := filepath.Join(tempDir, "quiet")
@@ -54,21 +54,17 @@ func TestRuntime_Run(t *testing.T) {
 		{
 			name: "GetEnvoy Ctrl-C",
 			// Don't warn the user when they exited the process
-			expectedStdout: fmt.Sprintf(`starting: %s
-working directory: %s
-`, fakeEnvoy, workingDir),
+			expectedStdout: fmt.Sprintln("starting:", fakeEnvoy, adminFlag),
 			expectedStderr: "started\ncaught SIGINT\n",
 			expectedHooks:  []string{"preStart", "preTermination", "postTermination"},
 		},
 		// We don't test envoy dying from an external signal as it isn't reported back to the getenvoy process and
 		// Envoy returns exit status zero on anything except kill -9. We can't test kill -9 with a fake shell script.
 		{
-			name:      "Envoy exited with error",
-			terminate: func() { time.Sleep(time.Millisecond * 100) },
-			args:      []string{"quiet_exit=3"},
-			expectedStdout: fmt.Sprintf(`starting: %s quiet_exit=3
-working directory: %s
-`, fakeEnvoy, workingDir),
+			name:           "Envoy exited with error",
+			terminate:      func() { time.Sleep(time.Millisecond * 100) },
+			args:           []string{"quiet_exit=3"},
+			expectedStdout: fmt.Sprintln("starting:", fakeEnvoy, "quiet_exit=3", adminFlag),
 			expectedStderr: "started\n",
 			expectedErr:    "envoy exited with status: 3",
 			expectedHooks:  []string{"preStart"},
@@ -78,8 +74,8 @@ working directory: %s
 	for _, tt := range tests {
 		tc := tt
 		t.Run(tc.name, func(t *testing.T) {
-			o := &globals.RunOpts{EnvoyPath: fakeEnvoy, WorkingDir: workingDir}
-			require.NoError(t, os.MkdirAll(workingDir, 0750))
+			o := &globals.RunOpts{EnvoyPath: fakeEnvoy, RunDir: runDir}
+			require.NoError(t, os.MkdirAll(runDir, 0750))
 
 			stdout := new(bytes.Buffer)
 			stderr := new(bytes.Buffer)
@@ -113,7 +109,7 @@ working directory: %s
 			require.NoError(t, err)
 			require.Equal(t, 1, len(files))
 			archive := filepath.Join(runsDir, files[0].Name())
-			require.Equal(t, filepath.Join(runsDir, fakeTimestamp+".tar.gz"), archive)
+			require.Equal(t, runDir+".tar.gz", archive)
 
 			// Cleanup for the next run
 			require.NoError(t, os.Remove(archive))
diff --git a/internal/envoy/start_test.go b/internal/envoy/start_test.go
index 138c7d4..85ba1a3 100644
--- a/internal/envoy/start_test.go
+++ b/internal/envoy/start_test.go
@@ -58,16 +58,16 @@ func TestHandlePreStartReturnsError(t *testing.T) {
 }
 
 func TestHandlePreStartEnsuresAdminAddressPath(t *testing.T) {
-	tempDir, removeTempDir := morerequire.RequireNewTempDir(t)
-	defer removeTempDir()
+	runDir, removeRunDir := morerequire.RequireNewTempDir(t)
+	defer removeRunDir()
 
-	r := NewRuntime(&globals.RunOpts{WorkingDir: tempDir})
+	r := NewRuntime(&globals.RunOpts{RunDir: runDir})
 	r.cmd = exec.Command("envoy")
 
 	// Verify the admin address path set (same assertion as ensureAdminAddressPath)
 	actualErr := r.handlePreStart()
 	require.NoError(t, actualErr)
-	require.Equal(t, []string{"envoy", "--admin-address-path", "admin-address.txt"}, r.cmd.Args)
+	require.Equal(t, []string{"envoy", "--admin-address-path", filepath.Join(runDir, "admin-address.txt")}, r.cmd.Args)
 }
 
 func TestHandlePreStartEnsuresAdminAddressPathLast(t *testing.T) {
@@ -86,9 +86,10 @@ func TestHandlePreStartEnsuresAdminAddressPathLast(t *testing.T) {
 }
 
 func TestEnsureAdminAddressPath(t *testing.T) {
-	workingDir, removeWorkingDir := morerequire.RequireNewTempDir(t)
-	defer removeWorkingDir()
+	runDir, removeRunDir := morerequire.RequireNewTempDir(t)
+	defer removeRunDir()
 
+	runAdminAddressPath := filepath.Join(runDir, "admin-address.txt")
 	tests := []struct {
 		name                 string
 		args                 []string
@@ -98,14 +99,14 @@ func TestEnsureAdminAddressPath(t *testing.T) {
 		{
 			name:                 "no args",
 			args:                 []string{"envoy"},
-			wantAdminAddressPath: filepath.Join(workingDir, "admin-address.txt"),
-			wantArgs:             []string{"envoy", "--admin-address-path", "admin-address.txt"},
+			wantAdminAddressPath: runAdminAddressPath,
+			wantArgs:             []string{"envoy", "--admin-address-path", runAdminAddressPath},
 		},
 		{
 			name:                 "args",
 			args:                 []string{"envoy", "-c", "/tmp/google_com_proxy.v2.yaml"},
-			wantAdminAddressPath: filepath.Join(workingDir, "admin-address.txt"),
-			wantArgs:             []string{"envoy", "-c", "/tmp/google_com_proxy.v2.yaml", "--admin-address-path", "admin-address.txt"},
+			wantAdminAddressPath: runAdminAddressPath,
+			wantArgs:             []string{"envoy", "-c", "/tmp/google_com_proxy.v2.yaml", "--admin-address-path", runAdminAddressPath},
 		},
 		{
 			name:                 "already",
@@ -118,7 +119,7 @@ func TestEnsureAdminAddressPath(t *testing.T) {
 	for _, tt := range tests {
 		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
-			r := NewRuntime(&globals.RunOpts{WorkingDir: workingDir})
+			r := NewRuntime(&globals.RunOpts{RunDir: runDir})
 			r.cmd = exec.Command(tt.args[0], tt.args[1:]...)
 
 			err := r.ensureAdminAddressPath()
diff --git a/internal/test/server.go b/internal/test/server.go
index 3c0fa77..38f459d 100644
--- a/internal/test/server.go
+++ b/internal/test/server.go
@@ -110,7 +110,6 @@ func requireFakeEnvoyTarGz(t *testing.T, v string) []byte {
 	fakeEnvoy := []byte(`#!/bin/sh
 set -ue
 # Echo invocation context to stdout and fake stderr to ensure it is not combined into stdout.
-echo envoy wd: $PWD
 echo envoy bin: $0
 echo envoy args: $@
 echo >&2 envoy stderr