github.com/outbrain/consul@v1.4.5/command/connect/envoy/exec_test.go (about)

     1  // +build linux darwin
     2  
     3  package envoy
     4  
     5  import (
     6  	"encoding/json"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"os"
    10  	"os/exec"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/stretchr/testify/require"
    16  )
    17  
    18  func TestExecEnvoy(t *testing.T) {
    19  	cases := []struct {
    20  		Name     string
    21  		Args     []string
    22  		WantArgs []string
    23  	}{
    24  		{
    25  			Name: "default",
    26  			Args: []string{},
    27  			WantArgs: []string{
    28  				"--v2-config-only",
    29  				"--config-path",
    30  				// Different platforms produce different file descriptors here so we use the
    31  				// value we got back. This is somewhat tautological but we do sanity check
    32  				// that value further below.
    33  				"{{ got.ConfigPath }}",
    34  				"--disable-hot-restart",
    35  				"--fake-envoy-arg",
    36  			},
    37  		},
    38  		{
    39  			Name: "hot-restart-epoch",
    40  			Args: []string{"--restart-epoch", "1"},
    41  			WantArgs: []string{
    42  				"--v2-config-only",
    43  				"--config-path",
    44  				// Different platforms produce different file descriptors here so we use the
    45  				// value we got back. This is somewhat tautological but we do sanity check
    46  				// that value further below.
    47  				"{{ got.ConfigPath }}",
    48  				// No --disable-hot-restart
    49  				"--fake-envoy-arg",
    50  				"--restart-epoch",
    51  				"1",
    52  			},
    53  		},
    54  		{
    55  			Name: "hot-restart-version",
    56  			Args: []string{"--drain-time-s", "10"},
    57  			WantArgs: []string{
    58  				"--v2-config-only",
    59  				"--config-path",
    60  				// Different platforms produce different file descriptors here so we use the
    61  				// value we got back. This is somewhat tautological but we do sanity check
    62  				// that value further below.
    63  				"{{ got.ConfigPath }}",
    64  				// No --disable-hot-restart
    65  				"--fake-envoy-arg",
    66  				// Restart epoch defaults to 0 if not given and not disabled.
    67  				"--drain-time-s",
    68  				"10",
    69  			},
    70  		},
    71  		{
    72  			Name: "hot-restart-version",
    73  			Args: []string{"--parent-shutdown-time-s", "20"},
    74  			WantArgs: []string{
    75  				"--v2-config-only",
    76  				"--config-path",
    77  				// Different platforms produce different file descriptors here so we use the
    78  				// value we got back. This is somewhat tautological but we do sanity check
    79  				// that value further below.
    80  				"{{ got.ConfigPath }}",
    81  				// No --disable-hot-restart
    82  				"--fake-envoy-arg",
    83  				// Restart epoch defaults to 0 if not given and not disabled.
    84  				"--parent-shutdown-time-s",
    85  				"20",
    86  			},
    87  		},
    88  		{
    89  			Name: "hot-restart-version",
    90  			Args: []string{"--hot-restart-version", "foobar1"},
    91  			WantArgs: []string{
    92  				"--v2-config-only",
    93  				"--config-path",
    94  				// Different platforms produce different file descriptors here so we use the
    95  				// value we got back. This is somewhat tautological but we do sanity check
    96  				// that value further below.
    97  				"{{ got.ConfigPath }}",
    98  				// No --disable-hot-restart
    99  				"--fake-envoy-arg",
   100  				// Restart epoch defaults to 0 if not given and not disabled.
   101  				"--hot-restart-version",
   102  				"foobar1",
   103  			},
   104  		},
   105  	}
   106  
   107  	for _, tc := range cases {
   108  		t.Run(tc.Name, func(t *testing.T) {
   109  			require := require.New(t)
   110  
   111  			args := append([]string{"exec-fake-envoy"}, tc.Args...)
   112  			cmd, destroy := helperProcess(args...)
   113  			defer destroy()
   114  
   115  			cmd.Stderr = os.Stderr
   116  			outBytes, err := cmd.Output()
   117  			require.NoError(err)
   118  
   119  			var got FakeEnvoyExecData
   120  			require.NoError(json.Unmarshal(outBytes, &got))
   121  
   122  			expectConfigData := fakeEnvoyTestData
   123  
   124  			// Substitute the right FD path
   125  			for idx := range tc.WantArgs {
   126  				tc.WantArgs[idx] = strings.Replace(tc.WantArgs[idx],
   127  					"{{ got.ConfigPath }}", got.ConfigPath, 1)
   128  			}
   129  
   130  			require.Equal(tc.WantArgs, got.Args)
   131  			require.Equal(expectConfigData, got.ConfigData)
   132  			// Sanity check the config path in a non-brittle way since we used it to
   133  			// generate expectation for the args.
   134  			require.Regexp(`^/dev/fd/\d+$`, got.ConfigPath)
   135  		})
   136  	}
   137  }
   138  
   139  type FakeEnvoyExecData struct {
   140  	Args       []string `json:"args"`
   141  	ConfigPath string   `json:"configPath"`
   142  	ConfigData string   `json:"configData"`
   143  }
   144  
   145  // helperProcessSentinel is a sentinel value that is put as the first
   146  // argument following "--" and is used to determine if TestHelperProcess
   147  // should run.
   148  const helperProcessSentinel = "GO_WANT_HELPER_PROCESS"
   149  
   150  // helperProcess returns an *exec.Cmd that can be used to execute the
   151  // TestHelperProcess function below. This can be used to test multi-process
   152  // interactions.
   153  func helperProcess(s ...string) (*exec.Cmd, func()) {
   154  	cs := []string{"-test.run=TestHelperProcess", "--", helperProcessSentinel}
   155  	cs = append(cs, s...)
   156  
   157  	cmd := exec.Command(os.Args[0], cs...)
   158  	destroy := func() {
   159  		if p := cmd.Process; p != nil {
   160  			p.Kill()
   161  		}
   162  	}
   163  
   164  	return cmd, destroy
   165  }
   166  
   167  const fakeEnvoyTestData = "pahx9eiPoogheb4haeb2abeem1QuireWahtah1Udi5ae4fuD0c"
   168  
   169  // This is not a real test. This is just a helper process kicked off by tests
   170  // using the helperProcess helper function.
   171  func TestHelperProcess(t *testing.T) {
   172  	args := os.Args
   173  	for len(args) > 0 {
   174  		if args[0] == "--" {
   175  			args = args[1:]
   176  			break
   177  		}
   178  
   179  		args = args[1:]
   180  	}
   181  
   182  	if len(args) == 0 || args[0] != helperProcessSentinel {
   183  		return
   184  	}
   185  
   186  	defer os.Exit(0)
   187  	args = args[1:] // strip sentinel value
   188  	cmd, args := args[0], args[1:]
   189  
   190  	switch cmd {
   191  	case "exec-fake-envoy":
   192  		// this will just exec the "fake-envoy" flavor below
   193  
   194  		limitProcessLifetime(2 * time.Minute)
   195  
   196  		err := execEnvoy(
   197  			os.Args[0],
   198  			[]string{
   199  				"-test.run=TestHelperProcess",
   200  				"--",
   201  				helperProcessSentinel,
   202  				"fake-envoy",
   203  			},
   204  			append([]string{"--fake-envoy-arg"}, args...),
   205  			[]byte(fakeEnvoyTestData),
   206  		)
   207  		if err != nil {
   208  			fmt.Fprintf(os.Stderr, "fake envoy process failed to exec: %v\n", err)
   209  			os.Exit(1)
   210  		}
   211  
   212  	case "fake-envoy":
   213  		// This subcommand is instrumented to verify some settings
   214  		// survived an exec.
   215  
   216  		limitProcessLifetime(2 * time.Minute)
   217  
   218  		data := FakeEnvoyExecData{
   219  			Args: args,
   220  		}
   221  
   222  		// Dump all of the args.
   223  		var captureNext bool
   224  		for _, arg := range args {
   225  			if arg == "--config-path" {
   226  				captureNext = true
   227  			} else if captureNext {
   228  				data.ConfigPath = arg
   229  				captureNext = false
   230  			}
   231  		}
   232  
   233  		if data.ConfigPath == "" {
   234  			fmt.Fprintf(os.Stderr, "did not detect a --config-path argument passed through\n")
   235  			os.Exit(1)
   236  		}
   237  
   238  		d, err := ioutil.ReadFile(data.ConfigPath)
   239  		if err != nil {
   240  			fmt.Fprintf(os.Stderr, "could not read provided --config-path file %q: %v\n", data.ConfigPath, err)
   241  			os.Exit(1)
   242  		}
   243  		data.ConfigData = string(d)
   244  
   245  		enc := json.NewEncoder(os.Stdout)
   246  		if err := enc.Encode(&data); err != nil {
   247  			fmt.Fprintf(os.Stderr, "could not dump results to stdout: %v", err)
   248  			os.Exit(1)
   249  
   250  		}
   251  
   252  	default:
   253  		fmt.Fprintf(os.Stderr, "Unknown command: %q\n", cmd)
   254  		os.Exit(2)
   255  	}
   256  }
   257  
   258  // limitProcessLifetime installs a background goroutine that self-exits after
   259  // the specified duration elapses to prevent leaking processes from tests that
   260  // may spawn them.
   261  func limitProcessLifetime(dur time.Duration) {
   262  	go time.AfterFunc(dur, func() {
   263  		os.Exit(99)
   264  	})
   265  }