Repository: tetratelabs/getenvoy
Pull request #270 (2021-06-07 05:07:03)

Centralizes use of go context
Before, we had many call paths that could accept a go context, but we
didn't give one. Since urfave supports context, we should use it.

This centralizes shutdown hook deadline as now+5s and logs accordingly.

Ex.

```
invoking shutdown hooks with deadline [2021-06-07 13:49:47.226]
[2021-06-07T05:49:42.233Z] "GET /contention HTTP/1.1" 200 - 0 95 1 - "192.168.0.116" "Go-http-client/1.1" "-" "127.0.0.1:62884" "-"
sending interrupt to envoy (pid=61302)
[2021-06-07 13:49:47.269][4272150][warning][main] [external/envoy/source/server/server.cc:622] caught SIGINT
```

This also makes fetching parallel per #271

codefromthecrypt: https://github.com/tetratelabs/getenvoy/pull/271 layers over this and does things in parallel

Patch:
diff --git a/go.mod b/go.mod
index 28c7a65..2a7340d 100644
--- a/go.mod
+++ b/go.mod
@@ -14,6 +14,7 @@ require (
 	github.com/tklauser/go-sysconf v0.3.6 // indirect
 	github.com/ulikunitz/xz v0.5.10
 	github.com/urfave/cli/v2 v2.3.0
+	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
 	golang.org/x/sys v0.0.0-20210601080250-7ecdf8ef093b // indirect
 	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
 )
diff --git a/go.sum b/go.sum
index 927f005..772e873 100644
--- a/go.sum
+++ b/go.sum
@@ -33,6 +33,8 @@ github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
 github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
 github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/internal/cmd/run.go b/internal/cmd/run.go
index d8376a9..263feb6 100644
--- a/internal/cmd/run.go
+++ b/internal/cmd/run.go
@@ -15,6 +15,7 @@
 package cmd
 
 import (
+	"context"
 	"fmt"
 	"io"
 	"os"
@@ -49,13 +50,13 @@ While Envoy is running, the run directory (` + "`$GETENVOY_HOME/runs/$epochtime`
 includes minimally "stdout.log" and "stderr.log". On Ctrl-C, shutdown hooks
 write troubleshooting files, including admin endpoints, network and process
 state. Upon exit, this archives as ` + "`$GETENVOY_HOME/runs/$epochtime.tar.gz`",
-		Before: func(context *cli.Context) error {
+		Before: func(c *cli.Context) error {
 			if err := os.MkdirAll(o.HomeDir, 0750); err != nil {
 				return NewValidationError(err.Error())
 			}
 
 			if o.EnvoyVersion == "" { // not overridden for tests
-				if err := setHomeEnvoyVersion(o); err != nil {
+				if err := setHomeEnvoyVersion(c.Context, o); err != nil {
 					return err
 				}
 				v, _, err := envoy.CurrentVersion(o.HomeDir)
@@ -68,7 +69,7 @@ state. Upon exit, this archives as ` + "`$GETENVOY_HOME/runs/$epochtime.tar.gz`"
 			return nil
 		},
 		Action: func(c *cli.Context) error {
-			if err := initializeRunOpts(o, globals.CurrentPlatform, envoyVersion); err != nil {
+			if err := initializeRunOpts(c.Context, o, globals.CurrentPlatform, envoyVersion); err != nil {
 				return err
 			}
 			r := envoy.NewRuntime(&o.RunOpts)
@@ -102,10 +103,10 @@ state. Upon exit, this archives as ` + "`$GETENVOY_HOME/runs/$epochtime.tar.gz`"
 // initializeRunOpts allows us to default values when not overridden for tests.
 // The version parameter correlates with the globals.GlobalOpts EnvoyPath which is installed if needed.
 // Notably, this creates and sets a globals.GlobalOpts WorkingDirectory for Envoy, and any files that precede it.
-func initializeRunOpts(o *globals.GlobalOpts, p, v string) error {
+func initializeRunOpts(ctx context.Context, o *globals.GlobalOpts, p, v string) error {
 	runOpts := &o.RunOpts
 	if o.EnvoyPath == "" { // not overridden for tests
-		envoyPath, err := envoy.InstallIfNeeded(o, p, v)
+		envoyPath, err := envoy.InstallIfNeeded(ctx, o, p, v)
 		if err != nil {
 			return err
 		}
@@ -125,7 +126,7 @@ func initializeRunOpts(o *globals.GlobalOpts, p, v string) error {
 }
 
 // setHomeEnvoyVersion makes sure the $GETENVOY_HOME/version exists.
-func setHomeEnvoyVersion(o *globals.GlobalOpts) error {
+func setHomeEnvoyVersion(ctx context.Context, o *globals.GlobalOpts) error {
 	v, homeVersionFile, err := envoy.GetHomeVersion(o.HomeDir)
 	if err != nil {
 		return NewValidationError(err.Error())
@@ -135,7 +136,7 @@ func setHomeEnvoyVersion(o *globals.GlobalOpts) error {
 
 	// First time install: look up the latest version, which may be newer than version.LastKnownEnvoy!
 	fmt.Fprintln(o.Out, "looking up latest version") //nolint
-	m, err := envoy.GetEnvoyVersions(o.EnvoyVersionsURL, globals.CurrentPlatform, version.GetEnvoy)
+	m, err := envoy.GetEnvoyVersions(ctx, o.EnvoyVersionsURL, globals.CurrentPlatform, version.GetEnvoy)
 	if err != nil {
 		return NewValidationError(`couldn't read latest version from %s: %s`, o.EnvoyVersionsURL, err)
 	}
diff --git a/internal/cmd/use.go b/internal/cmd/use.go
index 94eb74d..354abbf 100644
--- a/internal/cmd/use.go
+++ b/internal/cmd/use.go
@@ -42,7 +42,7 @@ $ getenvoy use %s`, envoy.CurrentVersionWorkingDirFile, envoy.CurrentVersionHome
 		Before: validateVersionArg,
 		Action: func(c *cli.Context) error {
 			v := c.Args().First()
-			if _, err := envoy.InstallIfNeeded(o, globals.CurrentPlatform, v); err != nil {
+			if _, err := envoy.InstallIfNeeded(c.Context, o, globals.CurrentPlatform, v); err != nil {
 				return err
 			}
 			return envoy.WriteCurrentVersion(v, o.HomeDir)
diff --git a/internal/cmd/versions.go b/internal/cmd/versions.go
index 3146474..5726304 100644
--- a/internal/cmd/versions.go
+++ b/internal/cmd/versions.go
@@ -50,7 +50,7 @@ func NewVersionsCmd(o *globals.GlobalOpts) *cli.Command {
 			currentVersion, currentVersionSource, _ := envoy.CurrentVersion(o.HomeDir)
 
 			if c.Bool("all") {
-				if ev, err := envoy.GetEnvoyVersions(o.EnvoyVersionsURL, globals.CurrentPlatform, version.GetEnvoy); err != nil {
+				if ev, err := envoy.GetEnvoyVersions(c.Context, o.EnvoyVersionsURL, globals.CurrentPlatform, version.GetEnvoy); err != nil {
 					return err
 				} else if err := addAvailableVersions(&rows, ev.Versions, globals.CurrentPlatform); err != nil {
 					return err
diff --git a/internal/envoy/http.go b/internal/envoy/http.go
index a0b0b53..965fd1a 100644
--- a/internal/envoy/http.go
+++ b/internal/envoy/http.go
@@ -15,15 +15,16 @@
 package envoy
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"strings"
 )
 
 // httpGet adds the userAgent header to the request, so that we can tell what is a dev build vs release.
-func httpGet(url, platform, version string) (*http.Response, error) {
+func httpGet(ctx context.Context, url, platform, version string) (*http.Response, error) {
 	// #nosec -> url can be anywhere by design
-	req, err := http.NewRequest("GET", url, nil)
+	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/envoy/install.go b/internal/envoy/install.go
index 2aa67c9..3630b0c 100644
--- a/internal/envoy/install.go
+++ b/internal/envoy/install.go
@@ -15,6 +15,7 @@
 package envoy
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"os"
@@ -29,14 +30,14 @@ import (
 const binEnvoy = "bin/envoy"
 
 // InstallIfNeeded downloads an Envoy binary corresponding to the given version and returns a path to it or an error.
-func InstallIfNeeded(o *globals.GlobalOpts, p, v string) (string, error) {
+func InstallIfNeeded(ctx context.Context, o *globals.GlobalOpts, p, v string) (string, error) {
 	installPath := filepath.Join(o.HomeDir, "versions", v)
 	envoyPath := filepath.Join(installPath, binEnvoy)
 	_, err := os.Stat(envoyPath)
 	switch {
 	case os.IsNotExist(err):
 		var ev version.EnvoyVersions // Get version metadata for what we will install
-		ev, err = GetEnvoyVersions(o.EnvoyVersionsURL, p, v)
+		ev, err = GetEnvoyVersions(ctx, o.EnvoyVersionsURL, p, v)
 		if err != nil {
 			return "", err
 		}
@@ -54,8 +55,8 @@ func InstallIfNeeded(o *globals.GlobalOpts, p, v string) (string, error) {
 			return "", fmt.Errorf("unable to create directory %q: %w", installPath, err)
 		}
 
-		fmt.Fprintln(o.Out, "downloading", tarballURL)                   //nolint
-		if err = untarEnvoy(installPath, tarballURL, p, v); err != nil { //nolint
+		fmt.Fprintln(o.Out, "downloading", tarballURL)                        //nolint
+		if err = untarEnvoy(ctx, installPath, tarballURL, p, v); err != nil { //nolint
 			return "", err
 		}
 		if err = os.Chtimes(installPath, mtime, mtime); err != nil { // overwrite the mtime to preserve it in the list
@@ -82,8 +83,8 @@ func verifyEnvoy(installPath string) (string, error) {
 	return envoyPath, nil
 }
 
-func untarEnvoy(dst, url, p, v string) error { // dst, src order like io.Copy
-	resp, err := httpGet(url, p, v)
+func untarEnvoy(ctx context.Context, dst, url, p, v string) error { // dst, src order like io.Copy
+	resp, err := httpGet(ctx, url, p, v)
 	if err != nil {
 		return err
 	}
diff --git a/internal/envoy/run.go b/internal/envoy/run.go
index 38f64fe..ef3b212 100644
--- a/internal/envoy/run.go
+++ b/internal/envoy/run.go
@@ -73,7 +73,7 @@ func (r *Runtime) Run(ctx context.Context, args []string) (err error) {
 		return r.archiveRunDir()
 	}
 
-	r.handleShutdown()
+	r.handleShutdown(ctx)
 
 	// Block until the process is complete. This ensures file descriptors are closed.
 	<-waitCtx.Done()
diff --git a/internal/envoy/runtime.go b/internal/envoy/runtime.go
index 6d10d13..f1d2bf0 100644
--- a/internal/envoy/runtime.go
+++ b/internal/envoy/runtime.go
@@ -22,10 +22,18 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
+	"time"
 
 	"github.com/tetratelabs/getenvoy/internal/globals"
 )
 
+const (
+	// Don't wait forever. This has hung on macOS before
+	shutdownTimeout = 5 * time.Second
+	// Match envoy's log format field
+	dateFormat = "[2006-01-02 15:04:05.999]"
+)
+
 // 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 {
@@ -46,7 +54,7 @@ type Runtime struct {
 	// End-to-end tests should kill the getenvoy process to achieve the same.
 	FakeInterrupt context.CancelFunc
 
-	shutdownHooks []func() error
+	shutdownHooks []func(context.Context) error
 }
 
 // GetRunDir returns the run-specific directory files can be written to.
diff --git a/internal/envoy/shutdown.go b/internal/envoy/shutdown.go
index 3074e5b..998d0fb 100644
--- a/internal/envoy/shutdown.go
+++ b/internal/envoy/shutdown.go
@@ -15,34 +15,50 @@
 package envoy
 
 import (
+	"context"
 	"fmt"
 	"os"
 	"path/filepath"
+	"sync"
 	"syscall"
+	"time"
 
 	"github.com/tetratelabs/getenvoy/internal/tar"
 )
 
 // RegisterShutdownHook registers the passed functions to be run after Envoy has started
 // and just before GetEnvoy instructs Envoy to exit
-func (r *Runtime) RegisterShutdownHook(f ...func() error) {
+func (r *Runtime) RegisterShutdownHook(f ...func(context.Context) error) {
 	r.shutdownHooks = append(r.shutdownHooks, f...)
 }
 
-func (r *Runtime) handleShutdown() {
+func (r *Runtime) handleShutdown(ctx context.Context) {
 	defer r.interruptEnvoy() // Ensure the SIGINT forwards to Envoy even if a shutdown hook panics
 
-	fmt.Fprintln(r.Out, "invoking shutdown hooks") //nolint
+	deadline := time.Now().Add(shutdownTimeout)
+	timeout, cancel := context.WithDeadline(ctx, deadline)
+	defer cancel()
+
+	fmt.Fprintf(r.Out, "invoking shutdown hooks with deadline %s\n", deadline.Format(dateFormat)) //nolint
+
+	// Run each hook in parallel, logging each error
+	var wg sync.WaitGroup
+	wg.Add(len(r.shutdownHooks))
 	for _, f := range r.shutdownHooks {
-		if err := f(); err != nil {
-			fmt.Fprintln(r.Out, "failed shutdown hook:", err) //nolint
-		}
+		f := f // pin! see https://github.com/kyoh86/scopelint for why
+		go func() {
+			defer wg.Done()
+			if err := f(timeout); err != nil {
+				fmt.Fprintln(r.Out, "failed shutdown hook:", err) //nolint
+			}
+		}()
 	}
+	wg.Wait()
 }
 
 func (r *Runtime) interruptEnvoy() {
 	p := r.cmd.Process
-	fmt.Fprintln(r.Out, "shutting down envoy") //nolint
+	fmt.Fprintf(r.Out, "sending interrupt to envoy (pid=%d)\n", p.Pid) //nolint
 	_ = p.Signal(syscall.SIGINT)
 }
 
diff --git a/internal/envoy/shutdown/admin.go b/internal/envoy/shutdown/admin.go
index 04907be..10aacd9 100644
--- a/internal/envoy/shutdown/admin.go
+++ b/internal/envoy/shutdown/admin.go
@@ -15,12 +15,15 @@
 package shutdown
 
 import (
+	"context"
 	"fmt"
 	"io"
 	"net/http"
 	"os"
 	"path/filepath"
 
+	"golang.org/x/sync/errgroup"
+
 	"github.com/tetratelabs/getenvoy/internal/envoy"
 )
 
@@ -48,22 +51,27 @@ type envoyAdminDataCollection struct {
 	workingDir      string
 }
 
-func (e *envoyAdminDataCollection) retrieveAdminAPIData() error {
+func (e *envoyAdminDataCollection) retrieveAdminAPIData(ctx context.Context) error {
 	adminAddress, err := e.getAdminAddress()
 	if err != nil {
 		return fmt.Errorf("unable to capture Envoy configuration and metrics: %w", err)
 	}
+
+	// Save each admin API path to a file in parallel returning on first error
+	// Execute all admin fetches in parallel
+	g, ctx := errgroup.WithContext(ctx)
 	for p, f := range adminAPIPaths {
 		url := fmt.Sprintf("http://%s/%v", adminAddress, p)
 		file := filepath.Join(e.workingDir, f)
-		if err := copyURLToFile(url, file); err != nil {
-			return err
-		}
+
+		g.Go(func() error {
+			return copyURLToFile(ctx, url, file)
+		})
 	}
-	return nil
+	return g.Wait() // first error
 }
 
-func copyURLToFile(url, fullPath string) error {
+func copyURLToFile(ctx context.Context, url, fullPath string) error {
 	// #nosec -> e.workingDir is allowed to be anywhere
 	f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY, 0600)
 	if err != nil {
@@ -72,16 +80,20 @@ func copyURLToFile(url, fullPath string) error {
 	defer f.Close() //nolint
 
 	// #nosec -> adminAddress is written by Envoy and the paths are hard-coded
-	resp, err := http.Get(url)
+	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
 	if err != nil {
-		return fmt.Errorf("could not read %v: %w", url, err)
+		return fmt.Errorf("could not create request %v: %w", url, err)
 	}
-	if resp.StatusCode != http.StatusOK {
-		return fmt.Errorf("received %v from %v", resp.StatusCode, url)
+	res, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return fmt.Errorf("could not read %v: %w", url, err)
 	}
-	defer resp.Body.Close() //nolint
+	defer res.Body.Close() //nolint
 
-	if _, err := io.Copy(f, resp.Body); err != nil {
+	if res.StatusCode != http.StatusOK {
+		return fmt.Errorf("received %v from %v", res.StatusCode, url)
+	}
+	if _, err := io.Copy(f, res.Body); err != nil {
 		return fmt.Errorf("could not write response body of %v: %w", url, err)
 	}
 	return nil
diff --git a/internal/envoy/shutdown/node.go b/internal/envoy/shutdown/node.go
index 87a94bc..a5c6338 100644
--- a/internal/envoy/shutdown/node.go
+++ b/internal/envoy/shutdown/node.go
@@ -22,7 +22,6 @@ import (
 	"path/filepath"
 	"syscall"
 	"text/tabwriter"
-	"time"
 
 	"github.com/shirou/gopsutil/v3/net"
 	"github.com/shirou/gopsutil/v3/process"
@@ -47,19 +46,13 @@ type nodeCollection struct {
 	nodeDir string
 }
 
-// Don't wait forever. This has hung on macOS before
-const processTimeout = 3 * time.Second
-
-func (n *nodeCollection) ps() error {
+func (n *nodeCollection) ps(ctx context.Context) error {
 	f, err := os.Create(filepath.Join(n.nodeDir, "ps.txt"))
 	if err != nil {
 		return fmt.Errorf("unable to create file to write ps output to: %w", err)
 	}
 	defer f.Close() //nolint
 
-	ctx, cancel := context.WithTimeout(context.Background(), processTimeout)
-	defer cancel()
-
 	processes, err := process.ProcessesWithContext(ctx)
 	if err == nil {
 		err = ctx.Err()
@@ -183,8 +176,8 @@ func printProcessTable(out io.Writer, parsed []*proc) error {
 	return w.Flush()
 }
 
-func (n *nodeCollection) networkInterfaces() error {
-	result, err := net.Interfaces()
+func (n *nodeCollection) networkInterfaces(ctx context.Context) error {
+	result, err := net.InterfacesWithContext(ctx)
 	if err != nil {
 		return fmt.Errorf("unable to list network interfaces: %w", err)
 	}
@@ -217,8 +210,8 @@ var typeMap = map[uint32]string{
 	syscall.SOCK_DGRAM:  "SOCK_DGRAM",
 }
 
-func (n *nodeCollection) activeConnections() error {
-	cs, err := net.Connections("all")
+func (n *nodeCollection) activeConnections(ctx context.Context) error {
+	cs, err := net.ConnectionsWithContext(ctx, "all")
 	if err != nil {
 		return fmt.Errorf("unable to list network connections: %w", err)
 	}
diff --git a/internal/envoy/versions.go b/internal/envoy/versions.go
index 8ccb840..93be47b 100644
--- a/internal/envoy/versions.go
+++ b/internal/envoy/versions.go
@@ -15,6 +15,7 @@
 package envoy
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -24,10 +25,10 @@ import (
 )
 
 // GetEnvoyVersions returns a version map from a remote URL. eg globals.DefaultEnvoyVersionsURL.
-func GetEnvoyVersions(envoyVersionsURL, p, v string) (version.EnvoyVersions, error) {
+func GetEnvoyVersions(ctx context.Context, envoyVersionsURL, p, v string) (version.EnvoyVersions, error) {
 	result := version.EnvoyVersions{}
 	// #nosec => This is by design, users can call out to wherever they like!
-	resp, err := httpGet(envoyVersionsURL, p, v)
+	resp, err := httpGet(ctx, envoyVersionsURL, p, v)
 	if err != nil {
 		return result, err
 	}


Test patch:
diff --git a/internal/envoy/http_test.go b/internal/envoy/http_test.go
index 5c45db0..2a4b3cf 100644
--- a/internal/envoy/http_test.go
+++ b/internal/envoy/http_test.go
@@ -15,6 +15,7 @@
 package envoy
 
 import (
+	"context"
 	"net/http"
 	"net/http/httptest"
 	"testing"
@@ -33,7 +34,7 @@ func TestHttpGet_AddsDefaultHeaders(t *testing.T) {
 	}))
 	defer ts.Close()
 
-	res, err := httpGet(ts.URL, globals.CurrentPlatform, version.GetEnvoy)
+	res, err := httpGet(context.Background(), ts.URL, globals.CurrentPlatform, version.GetEnvoy)
 	require.NoError(t, err)
 
 	defer res.Body.Close()
diff --git a/internal/envoy/install_test.go b/internal/envoy/install_test.go
index 5fba8b5..55c361f 100644
--- a/internal/envoy/install_test.go
+++ b/internal/envoy/install_test.go
@@ -16,6 +16,7 @@ package envoy
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -35,6 +36,7 @@ import (
 )
 
 func TestUntarEnvoyError(t *testing.T) {
+	ctx := context.Background()
 	tempDir, removeTempDir := morerequire.RequireNewTempDir(t)
 	dst := filepath.Join(tempDir, "dst")
 	defer removeTempDir()
@@ -51,7 +53,7 @@ func TestUntarEnvoyError(t *testing.T) {
 
 	url := server.URL + "/file.tar.gz"
 	t.Run("error on incorrect URL", func(t *testing.T) {
-		err := untarEnvoy(dst, url, globals.CurrentPlatform, version.GetEnvoy)
+		err := untarEnvoy(ctx, dst, url, globals.CurrentPlatform, version.GetEnvoy)
 		require.EqualError(t, err, fmt.Sprintf(`received 404 status code from %s`, url))
 	})
 
@@ -59,7 +61,7 @@ func TestUntarEnvoyError(t *testing.T) {
 		w.WriteHeader(200)
 	}
 	t.Run("error on empty", func(t *testing.T) {
-		err := untarEnvoy(dst, url, globals.CurrentPlatform, version.GetEnvoy)
+		err := untarEnvoy(ctx, dst, url, globals.CurrentPlatform, version.GetEnvoy)
 		require.EqualError(t, err, fmt.Sprintf(`error untarring %s: EOF`, url))
 	})
 
@@ -68,7 +70,7 @@ func TestUntarEnvoyError(t *testing.T) {
 		w.Write([]byte("mary had a little lamb")) //nolint
 	}
 	t.Run("error on not a tar", func(t *testing.T) {
-		err := untarEnvoy(dst, url, globals.CurrentPlatform, version.GetEnvoy)
+		err := untarEnvoy(ctx, dst, url, globals.CurrentPlatform, version.GetEnvoy)
 		require.EqualError(t, err, fmt.Sprintf(`error untarring %s: gzip: invalid header`, url))
 	})
 }
@@ -78,7 +80,7 @@ func TestUntarEnvoy(t *testing.T) {
 	o, cleanup := setupInstallTest(t)
 	defer cleanup()
 
-	err := untarEnvoy(o.tempDir, o.tarballURL, globals.CurrentPlatform, version.GetEnvoy)
+	err := untarEnvoy(o.ctx, o.tempDir, o.tarballURL, globals.CurrentPlatform, version.GetEnvoy)
 	require.NoError(t, err)
 	require.FileExists(t, filepath.Join(o.tempDir, binEnvoy))
 }
@@ -89,7 +91,7 @@ func TestInstallIfNeeded_ErrorOnIncorrectURL(t *testing.T) {
 
 	o.EnvoyVersionsURL += "/varsionz.json"
 
-	_, err := InstallIfNeeded(&o.GlobalOpts, runtime.GOOS, version.LastKnownEnvoy)
+	_, err := InstallIfNeeded(o.ctx, &o.GlobalOpts, runtime.GOOS, version.LastKnownEnvoy)
 	require.EqualError(t, err, "received 404 status code from "+o.EnvoyVersionsURL)
 	require.Empty(t, o.Out.(*bytes.Buffer))
 }
@@ -117,7 +119,7 @@ func TestInstallIfNeeded_Validates(t *testing.T) {
 		tc := tt
 		t.Run(tc.name, func(t *testing.T) {
 			o.Out = new(bytes.Buffer)
-			_, e := InstallIfNeeded(&o.GlobalOpts, tc.goos, tc.version)
+			_, e := InstallIfNeeded(o.ctx, &o.GlobalOpts, tc.goos, tc.version)
 			require.EqualError(t, e, tc.expectedErr)
 			require.Empty(t, o.Out.(*bytes.Buffer))
 		})
@@ -129,7 +131,7 @@ func TestInstallIfNeeded(t *testing.T) {
 	defer cleanup()
 	out := o.Out.(*bytes.Buffer)
 
-	envoyPath, e := InstallIfNeeded(&o.GlobalOpts, globals.CurrentPlatform, version.LastKnownEnvoy)
+	envoyPath, e := InstallIfNeeded(o.ctx, &o.GlobalOpts, globals.CurrentPlatform, version.LastKnownEnvoy)
 	require.NoError(t, e)
 	require.Equal(t, o.EnvoyPath, envoyPath)
 	require.FileExists(t, envoyPath)
@@ -148,11 +150,11 @@ func TestInstallIfNeeded_NotFound(t *testing.T) {
 	defer cleanup()
 
 	t.Run("unknown version", func(t *testing.T) {
-		_, e := InstallIfNeeded(&o.GlobalOpts, "darwin/amd64", "1.1.1")
+		_, e := InstallIfNeeded(o.ctx, &o.GlobalOpts, "darwin/amd64", "1.1.1")
 		require.EqualError(t, e, `couldn't find version "1.1.1" for platform "darwin/amd64"`)
 	})
 	t.Run("unknown platform", func(t *testing.T) {
-		_, e := InstallIfNeeded(&o.GlobalOpts, "windows/arm64", version.LastKnownEnvoy)
+		_, e := InstallIfNeeded(o.ctx, &o.GlobalOpts, "windows/arm64", version.LastKnownEnvoy)
 		require.EqualError(t, e, fmt.Sprintf(`couldn't find version "%s" for platform "windows/arm64"`, version.LastKnownEnvoy))
 	})
 }
@@ -168,7 +170,7 @@ func TestInstallIfNeeded_AlreadyExists(t *testing.T) {
 	envoyStat, err := os.Stat(o.EnvoyPath)
 	require.NoError(t, err)
 
-	envoyPath, e := InstallIfNeeded(&o.GlobalOpts, runtime.GOOS, version.LastKnownEnvoy)
+	envoyPath, e := InstallIfNeeded(o.ctx, &o.GlobalOpts, runtime.GOOS, version.LastKnownEnvoy)
 	require.NoError(t, e)
 	require.Equal(t, fmt.Sprintln(version.LastKnownEnvoy, "is already downloaded"), out.String())
 
@@ -208,6 +210,7 @@ func TestVerifyEnvoy(t *testing.T) {
 }
 
 type installTest struct {
+	ctx context.Context
 	globals.GlobalOpts
 	tempDir, tarballURL string
 }
@@ -222,6 +225,7 @@ func setupInstallTest(t *testing.T) (*installTest, func()) {
 	tearDown = append(tearDown, versionsServer.Close)
 
 	return &installTest{
+			ctx:        context.Background(),
 			tempDir:    tempDir,
 			tarballURL: test.TarballURL(versionsServer.URL, runtime.GOOS, runtime.GOARCH, version.LastKnownEnvoy),
 			GlobalOpts: globals.GlobalOpts{
diff --git a/internal/envoy/run_test.go b/internal/envoy/run_test.go
index 4bc479a..c40abec 100644
--- a/internal/envoy/run_test.go
+++ b/internal/envoy/run_test.go
@@ -16,6 +16,7 @@ package envoy_test
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"io"
 	"os"
@@ -83,10 +84,10 @@ func TestRuntime_Run(t *testing.T) {
 			r.Out = stdout
 			r.Err = stderr
 			var haveShutdownHook bool
-			r.RegisterShutdownHook(func() error {
+			r.RegisterShutdownHook(func(_ context.Context) error {
 				pid := envoy.RequireEnvoyPid(t, r)
 				_, err := os.FindProcess(pid)
-				require.NoError(t, err, "shutdownHooks called after process shutdown")
+				require.NoError(t, err, "shutdownHook called after process shutdown")
 				haveShutdownHook = true
 				return nil
 			})