mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
371 lines
9.5 KiB
371 lines
9.5 KiB
//go:build linux || darwin |
|
// +build linux darwin |
|
|
|
package envoy |
|
|
|
import ( |
|
"bytes" |
|
"encoding/json" |
|
"fmt" |
|
"io" |
|
"os" |
|
"os/exec" |
|
"strings" |
|
"testing" |
|
"time" |
|
|
|
"github.com/stretchr/testify/assert" |
|
"github.com/stretchr/testify/require" |
|
) |
|
|
|
func TestExecEnvoy(t *testing.T) { |
|
if testing.Short() { |
|
t.Skip("too slow for testing.Short") |
|
} |
|
|
|
cases := []struct { |
|
Name string |
|
Args []string |
|
WantArgs []string |
|
}{ |
|
{ |
|
Name: "default", |
|
Args: []string{}, |
|
WantArgs: []string{ |
|
"--config-path", |
|
"{{ got.ConfigPath }}", |
|
"--disable-hot-restart", |
|
"--fake-envoy-arg", |
|
}, |
|
}, |
|
{ |
|
Name: "hot-restart-epoch", |
|
Args: []string{"--restart-epoch", "1"}, |
|
WantArgs: []string{ |
|
"--config-path", |
|
// Different platforms produce different file descriptors here so we use the |
|
// value we got back. This is somewhat tautological but we do sanity check |
|
// that value further below. |
|
"{{ got.ConfigPath }}", |
|
// No --disable-hot-restart |
|
"--fake-envoy-arg", |
|
"--restart-epoch", |
|
"1", |
|
}, |
|
}, |
|
{ |
|
Name: "hot-restart-version", |
|
Args: []string{"--drain-time-s", "10"}, |
|
WantArgs: []string{ |
|
"--config-path", |
|
// Different platforms produce different file descriptors here so we use the |
|
// value we got back. This is somewhat tautological but we do sanity check |
|
// that value further below. |
|
"{{ got.ConfigPath }}", |
|
// No --disable-hot-restart |
|
"--fake-envoy-arg", |
|
// Restart epoch defaults to 0 if not given and not disabled. |
|
"--drain-time-s", |
|
"10", |
|
}, |
|
}, |
|
{ |
|
Name: "hot-restart-version", |
|
Args: []string{"--parent-shutdown-time-s", "20"}, |
|
WantArgs: []string{ |
|
"--config-path", |
|
// Different platforms produce different file descriptors here so we use the |
|
// value we got back. This is somewhat tautological but we do sanity check |
|
// that value further below. |
|
"{{ got.ConfigPath }}", |
|
// No --disable-hot-restart |
|
"--fake-envoy-arg", |
|
// Restart epoch defaults to 0 if not given and not disabled. |
|
"--parent-shutdown-time-s", |
|
"20", |
|
}, |
|
}, |
|
{ |
|
Name: "hot-restart-version", |
|
Args: []string{"--hot-restart-version", "foobar1"}, |
|
WantArgs: []string{ |
|
"--config-path", |
|
// Different platforms produce different file descriptors here so we use the |
|
// value we got back. This is somewhat tautological but we do sanity check |
|
// that value further below. |
|
"{{ got.ConfigPath }}", |
|
// No --disable-hot-restart |
|
"--fake-envoy-arg", |
|
// Restart epoch defaults to 0 if not given and not disabled. |
|
"--hot-restart-version", |
|
"foobar1", |
|
}, |
|
}, |
|
} |
|
|
|
for _, tc := range cases { |
|
t.Run(tc.Name, func(t *testing.T) { |
|
|
|
args := append([]string{"exec-fake-envoy"}, tc.Args...) |
|
cmd, destroy := helperProcess(args...) |
|
defer destroy() |
|
|
|
cmd.Stderr = os.Stderr |
|
outBytes, err := cmd.Output() |
|
require.NoError(t, err) |
|
|
|
var got FakeEnvoyExecData |
|
require.NoError(t, json.Unmarshal(outBytes, &got)) |
|
|
|
expectConfigData := fakeEnvoyTestData |
|
|
|
// Substitute the right FD path |
|
for idx := range tc.WantArgs { |
|
tc.WantArgs[idx] = strings.Replace(tc.WantArgs[idx], |
|
"{{ got.ConfigPath }}", got.ConfigPath, 1) |
|
} |
|
|
|
require.Equal(t, tc.WantArgs, got.Args) |
|
require.Equal(t, expectConfigData, got.ConfigData) |
|
// Sanity check the config path in a non-brittle way since we used it to |
|
// generate expectation for the args. |
|
require.Regexp(t, `-bootstrap.json$`, got.ConfigPath) |
|
}) |
|
} |
|
} |
|
|
|
func TestExecEnvoyVersion(t *testing.T) { |
|
if testing.Short() { |
|
t.Skip("too slow for testing.Short") |
|
} |
|
|
|
tests := []struct { |
|
name string |
|
cmdOutput string |
|
expectedOutput string |
|
}{ |
|
{ |
|
name: "actual-version-output-1-24-1", |
|
cmdOutput: `envoy version: 69958e4fe32da561376d8b1d367b5e6942dfba24/1.24.1/Distribution/RELEASE/BoringSSL`, |
|
expectedOutput: "1.24.1", |
|
}, |
|
{ |
|
name: "format-change", |
|
cmdOutput: `envoy version: (69958e4fe32da561376d8b1d367b5e6942dfba24)__(1.24.1)/Distribution/RELEASE/BoringSSL`, |
|
expectedOutput: "1.24.1", |
|
}, |
|
{ |
|
name: "zeroes", |
|
cmdOutput: `envoy version: 69958e4fe32da561376d8b1d367b5e6942dfba24/0.0.0/Distribution/RELEASE/BoringSSL`, |
|
expectedOutput: "0.0.0", |
|
}, |
|
{ |
|
name: "test-multi-digit", |
|
cmdOutput: `envoy version: 69958e4fe32da561376d8b1d367b5e6942dfba24/1246390.9401081.1238495/Distribution/RELEASE/BoringSSL`, |
|
expectedOutput: "1246390.9401081.1238495", |
|
}, |
|
} |
|
|
|
for _, tc := range tests { |
|
t.Run(tc.name, func(t *testing.T) { |
|
fe := fakeEnvoy{ |
|
desiredOutput: tc.cmdOutput, |
|
} |
|
execCommand = fe.ExecCommand |
|
// Reset back to base exec.Command |
|
defer func() { execCommand = exec.Command }() |
|
version, err := execEnvoyVersion("fake-envoy") |
|
|
|
require.NoError(t, err) |
|
|
|
assert.Equal(t, tc.expectedOutput, version) |
|
}) |
|
} |
|
} |
|
|
|
type fakeEnvoy struct { |
|
desiredOutput string |
|
} |
|
|
|
func (fe fakeEnvoy) ExecCommand(command string, args ...string) *exec.Cmd { |
|
cs := []string{"-test.run=TestEnvoyExecHelperProcess", "--", command} |
|
cs = append(cs, args...) |
|
// last argument will be the output |
|
cs = append(cs, fe.desiredOutput) |
|
cmd := exec.Command(os.Args[0], cs...) |
|
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} |
|
return cmd |
|
} |
|
|
|
func TestEnvoyExecHelperProcess(t *testing.T) { |
|
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { |
|
return |
|
} |
|
|
|
output := os.Args[len(os.Args)-1] |
|
fmt.Fprint(os.Stdout, output) |
|
os.Exit(0) |
|
} |
|
|
|
type FakeEnvoyExecData struct { |
|
Args []string `json:"args"` |
|
ConfigPath string `json:"configPath"` |
|
ConfigData string `json:"configData"` |
|
} |
|
|
|
// helperProcessSentinel is a sentinel value that is put as the first |
|
// argument following "--" and is used to determine if TestHelperProcess |
|
// should run. |
|
const helperProcessSentinel = "GO_WANT_HELPER_PROCESS" |
|
|
|
// helperProcess returns an *exec.Cmd that can be used to execute the |
|
// TestHelperProcess function below. This can be used to test multi-process |
|
// interactions. |
|
func helperProcess(s ...string) (*exec.Cmd, func()) { |
|
cs := []string{"-test.run=TestHelperProcess", "--", helperProcessSentinel} |
|
cs = append(cs, s...) |
|
|
|
cmd := exec.Command(os.Args[0], cs...) |
|
destroy := func() { |
|
if p := cmd.Process; p != nil { |
|
p.Kill() |
|
} |
|
} |
|
|
|
return cmd, destroy |
|
} |
|
|
|
const fakeEnvoyTestData = "pahx9eiPoogheb4haeb2abeem1QuireWahtah1Udi5ae4fuD0c" |
|
|
|
// This is not a real test. This is just a helper process kicked off by tests |
|
// using the helperProcess helper function. |
|
func TestHelperProcess(t *testing.T) { |
|
args := os.Args |
|
for len(args) > 0 { |
|
if args[0] == "--" { |
|
args = args[1:] |
|
break |
|
} |
|
|
|
args = args[1:] |
|
} |
|
|
|
if len(args) == 0 || args[0] != helperProcessSentinel { |
|
return |
|
} |
|
|
|
defer os.Exit(0) |
|
args = args[1:] // strip sentinel value |
|
cmd, args := args[0], args[1:] |
|
|
|
switch cmd { |
|
case "exec-fake-envoy": |
|
// this will just exec the "fake-envoy" flavor below |
|
|
|
limitProcessLifetime(2 * time.Minute) |
|
|
|
patchExecArgs(t) |
|
err := execEnvoy( |
|
os.Args[0], |
|
[]string{ |
|
"-test.run=TestHelperProcess", |
|
"--", |
|
helperProcessSentinel, |
|
"fake-envoy", |
|
}, |
|
append([]string{"--fake-envoy-arg"}, args...), |
|
[]byte(fakeEnvoyTestData), |
|
) |
|
if err != nil { |
|
fmt.Fprintf(os.Stderr, "fake envoy process failed to exec: %v\n", err) |
|
os.Exit(1) |
|
} |
|
|
|
case "fake-envoy": |
|
// This subcommand is instrumented to verify some settings |
|
// survived an exec. |
|
|
|
limitProcessLifetime(2 * time.Minute) |
|
|
|
data := FakeEnvoyExecData{ |
|
Args: args, |
|
} |
|
|
|
// Dump all of the args. |
|
var captureNext bool |
|
for _, arg := range args { |
|
if arg == "--config-path" { |
|
captureNext = true |
|
} else if captureNext { |
|
data.ConfigPath = arg |
|
captureNext = false |
|
} |
|
} |
|
|
|
if data.ConfigPath == "" { |
|
fmt.Fprintf(os.Stderr, "did not detect a --config-path argument passed through\n") |
|
os.Exit(1) |
|
} |
|
|
|
d, err := os.ReadFile(data.ConfigPath) |
|
if err != nil { |
|
fmt.Fprintf(os.Stderr, "could not read provided --config-path file %q: %v\n", data.ConfigPath, err) |
|
os.Exit(1) |
|
} |
|
data.ConfigData = string(d) |
|
|
|
enc := json.NewEncoder(os.Stdout) |
|
if err := enc.Encode(&data); err != nil { |
|
fmt.Fprintf(os.Stderr, "could not dump results to stdout: %v", err) |
|
os.Exit(1) |
|
|
|
} |
|
|
|
default: |
|
fmt.Fprintf(os.Stderr, "Unknown command: %q\n", cmd) |
|
os.Exit(2) |
|
} |
|
} |
|
|
|
// limitProcessLifetime installs a background goroutine that self-exits after |
|
// the specified duration elapses to prevent leaking processes from tests that |
|
// may spawn them. |
|
func limitProcessLifetime(dur time.Duration) { |
|
go time.AfterFunc(dur, func() { |
|
os.Exit(99) |
|
}) |
|
} |
|
|
|
// patchExecArgs to use a version that will execute the commands using 'go run'. |
|
// Also sets up a cleanup function to revert the patch when the test exits. |
|
func patchExecArgs(t *testing.T) { |
|
orig := execArgs |
|
// go run will run the consul source from the root of the repo. The relative |
|
// path is necessary because `go test` always sets the working directory to |
|
// the directory of the package being tested. |
|
execArgs = func(args ...string) (string, []string, error) { |
|
args = append([]string{"run", "../../.."}, args...) |
|
return "go", args, nil |
|
} |
|
t.Cleanup(func() { |
|
execArgs = orig |
|
}) |
|
} |
|
|
|
func TestMakeBootstrapPipe_DoesNotBlockOnAFullPipe(t *testing.T) { |
|
// A named pipe can buffer up to 64k, use a value larger than that |
|
bootstrap := bytes.Repeat([]byte("a"), 66000) |
|
|
|
patchExecArgs(t) |
|
pipe, err := makeBootstrapPipe(bootstrap) |
|
require.NoError(t, err) |
|
|
|
// Read everything from the named pipe, to allow the sub-process to exit |
|
f, err := os.Open(pipe) |
|
require.NoError(t, err) |
|
|
|
var buf bytes.Buffer |
|
_, err = io.Copy(&buf, f) |
|
require.NoError(t, err) |
|
require.Equal(t, bootstrap, buf.Bytes()) |
|
}
|
|
|