internal/sandbox: support appending to environment

Make it possible to append environment variables to
the sandbox environment, instead of replacing it.

Change-Id: If431bc34abb46ac239867bde7a32b750664a13bc
Reviewed-on: https://go-review.googlesource.com/c/pkgsite-metrics/+/475235
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/internal/sandbox/runner.go b/internal/sandbox/runner.go
index 77fa922..1b0f362 100644
--- a/internal/sandbox/runner.go
+++ b/internal/sandbox/runner.go
@@ -31,10 +31,16 @@
 		log.Fatal(err)
 	}
 	log.Printf("read %q", in)
-	var cmd exec.Cmd
+	var cmd struct {
+		exec.Cmd
+		AppendToEnv bool
+	}
 	if err := json.Unmarshal(in, &cmd); err != nil {
 		log.Fatal(err)
 	}
+	if cmd.AppendToEnv {
+		cmd.Env = append(os.Environ(), cmd.Env...)
+	}
 	log.Printf("cmd: %+v", cmd)
 	out, err := cmd.Output()
 	if err != nil {
diff --git a/internal/sandbox/sandbox.go b/internal/sandbox/sandbox.go
index c2b0855..a239cc2 100644
--- a/internal/sandbox/sandbox.go
+++ b/internal/sandbox/sandbox.go
@@ -60,6 +60,10 @@
 	// runsc provides by default.
 	Env []string
 
+	// If AppendToEnv is true, the contents of Env are appended
+	// to the sandbox's existing environment, instead of replacing it.
+	AppendToEnv bool
+
 	// Dir specifies the working directory of the command.
 	// If Dir is the empty string, Run runs the command in the
 	// root of the sandbox filesystem.
diff --git a/internal/sandbox/sandbox_test.go b/internal/sandbox/sandbox_test.go
index 60fa51f..8bb163c 100644
--- a/internal/sandbox/sandbox_test.go
+++ b/internal/sandbox/sandbox_test.go
@@ -25,33 +25,44 @@
 	sb := New("testdata/bundle")
 	sb.Runsc = "/usr/local/bin/runsc" // must match path in Makefile
 
-	t.Run("printargs", func(t *testing.T) {
-		out, err := sb.Command("printargs", "a", "b").Output()
+	check := func(t *testing.T, cmd *Cmd, want string) {
+		t.Helper()
+		out, err := cmd.Output()
 		if err != nil {
 			t.Fatal(derrors.IncludeStderr(err))
 		}
-
-		want := `args:
-0: "a"
-1: "b"`
 		got := string(out)
 		if got != want {
 			t.Fatalf("got\n%q\nwant\n%q", got, want)
 		}
+	}
+
+	t.Run("printargs", func(t *testing.T) {
+		check(t, sb.Command("printargs", "a", "b"), `args:
+0: "a"
+1: "b"`)
 	})
 
 	t.Run("space in arg", func(t *testing.T) {
-		out, err := sb.Command("printargs", "a", "b c\td").Output()
-		if err != nil {
-			t.Fatal(derrors.IncludeStderr(err))
-		}
-		want := `args:
+		check(t, sb.Command("printargs", "a", "b c\td"), `args:
 0: "a"
-1: "b c\td"`
-		got := string(out)
-		if got != want {
-			t.Fatalf("got\n%q\nwant\n%q", got, want)
-		}
+1: "b c\td"`)
+	})
+
+	t.Run("replace env", func(t *testing.T) {
+		cmd := sb.Command("printargs", "$HOME", "$FOO")
+		cmd.Env = []string{"FOO=17"}
+		check(t, cmd, `args:
+0: ""
+1: "17"`)
+	})
+	t.Run("append to env", func(t *testing.T) {
+		cmd := sb.Command("printargs", "$HOME", "$FOO")
+		cmd.Env = []string{"FOO=17"}
+		cmd.AppendToEnv = true
+		check(t, cmd, `args:
+0: "/"
+1: "17"`)
 	})
 	t.Run("no program", func(t *testing.T) {
 		_, err := sb.Command("foo").Output()
diff --git a/internal/sandbox/testdata/printargs.go b/internal/sandbox/testdata/printargs.go
index 2a03b5e..7f65410 100644
--- a/internal/sandbox/testdata/printargs.go
+++ b/internal/sandbox/testdata/printargs.go
@@ -5,8 +5,9 @@
 //go:build ignore
 
 // This program prints its arguments and exits.
+// If an argument begins with a "$", it prints
+// the value of the environment variable instead.
 // It is used for testing the sandbox package.
-
 package main
 
 import (
@@ -17,6 +18,10 @@
 func main() {
 	fmt.Printf("args:\n")
 	for i, arg := range os.Args[1:] {
-		fmt.Printf("%d: %q\n", i, arg)
+		val := arg
+		if len(arg) > 0 && arg[0] == '$' {
+			val = os.Getenv(arg[1:])
+		}
+		fmt.Printf("%d: %q\n", i, val)
 	}
 }