consul/command/connect/envoy/exec_test.go

377 lines
9.6 KiB
Go
Raw Permalink Normal View History

// Copyright (c) HashiCorp, Inc.
[COMPLIANCE] License changes (#18443) * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2023-08-11 13:12:13 +00:00
// SPDX-License-Identifier: BUSL-1.1
//go: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
2021-06-01 15:35:32 +00:00
// 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) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
// 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())
}