github.com/DerekStrickland/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 }