github.com/outbrain/consul@v1.4.5/agent/proxyprocess/manager_test.go (about) 1 package proxyprocess 2 3 import ( 4 "io/ioutil" 5 "os" 6 "os/exec" 7 "path/filepath" 8 "sort" 9 "strings" 10 "testing" 11 "time" 12 13 "github.com/hashicorp/consul/agent/local" 14 "github.com/hashicorp/consul/agent/structs" 15 "github.com/hashicorp/consul/testutil/retry" 16 "github.com/stretchr/testify/require" 17 ) 18 19 func TestManagerClose_noRun(t *testing.T) { 20 t.Parallel() 21 22 // Really we're testing that it doesn't deadlock here. 23 m, closer := testManager(t) 24 defer closer() 25 require.NoError(t, m.Close()) 26 27 // Close again for sanity 28 require.NoError(t, m.Close()) 29 } 30 31 // Test that Run performs an initial sync (if local.State is already set) 32 // rather than waiting for a notification from the local state. 33 func TestManagerRun_initialSync(t *testing.T) { 34 t.Parallel() 35 36 state := local.TestState(t) 37 m, closer := testManager(t) 38 defer closer() 39 m.State = state 40 defer m.Kill() 41 42 // Add the proxy before we start the manager to verify initial sync 43 td, closer := testTempDir(t) 44 defer closer() 45 path := filepath.Join(td, "file") 46 47 cmd, destroy := helperProcess("restart", path) 48 defer destroy() 49 50 testStateProxy(t, state, "web", cmd) 51 52 // Start the manager 53 go m.Run() 54 55 // We should see the path appear shortly 56 retry.Run(t, func(r *retry.R) { 57 _, err := os.Stat(path) 58 if err == nil { 59 return 60 } 61 r.Fatalf("error waiting for path: %s", err) 62 }) 63 } 64 65 func TestManagerRun_syncNew(t *testing.T) { 66 t.Parallel() 67 68 state := local.TestState(t) 69 m, closer := testManager(t) 70 defer closer() 71 m.State = state 72 defer m.Kill() 73 74 // Start the manager 75 go m.Run() 76 77 // Sleep a bit, this is just an attempt for Run to already be running. 78 // Its not a big deal if this sleep doesn't happen (slow CI). 79 time.Sleep(100 * time.Millisecond) 80 81 // Add the first proxy 82 td, closer := testTempDir(t) 83 defer closer() 84 path := filepath.Join(td, "file") 85 86 cmd, destroy := helperProcess("restart", path) 87 defer destroy() 88 89 testStateProxy(t, state, "web", cmd) 90 91 // We should see the path appear shortly 92 retry.Run(t, func(r *retry.R) { 93 _, err := os.Stat(path) 94 if err == nil { 95 return 96 } 97 r.Fatalf("error waiting for path: %s", err) 98 }) 99 100 // Add another proxy 101 path = path + "2" 102 103 cmd, destroy = helperProcess("restart", path) 104 defer destroy() 105 106 testStateProxy(t, state, "db", cmd) 107 retry.Run(t, func(r *retry.R) { 108 _, err := os.Stat(path) 109 if err == nil { 110 return 111 } 112 r.Fatalf("error waiting for path: %s", err) 113 }) 114 } 115 116 func TestManagerRun_syncDelete(t *testing.T) { 117 t.Parallel() 118 119 state := local.TestState(t) 120 m, closer := testManager(t) 121 defer closer() 122 m.State = state 123 defer m.Kill() 124 125 // Start the manager 126 go m.Run() 127 128 // Add the first proxy 129 td, closer := testTempDir(t) 130 defer closer() 131 path := filepath.Join(td, "file") 132 133 cmd, destroy := helperProcess("restart", path) 134 defer destroy() 135 136 id := testStateProxy(t, state, "web", cmd) 137 138 // We should see the path appear shortly 139 retry.Run(t, func(r *retry.R) { 140 _, err := os.Stat(path) 141 if err == nil { 142 return 143 } 144 r.Fatalf("error waiting for path: %s", err) 145 }) 146 147 // Remove the proxy 148 _, err := state.RemoveProxy(id) 149 require.NoError(t, err) 150 151 // File should disappear as process is killed 152 retry.Run(t, func(r *retry.R) { 153 _, err := os.Stat(path) 154 if err == nil { 155 r.Fatalf("path exists") 156 } 157 }) 158 } 159 160 func TestManagerRun_syncUpdate(t *testing.T) { 161 t.Parallel() 162 163 state := local.TestState(t) 164 m, closer := testManager(t) 165 defer closer() 166 m.State = state 167 defer m.Kill() 168 169 // Start the manager 170 go m.Run() 171 172 // Add the first proxy 173 td, closer := testTempDir(t) 174 defer closer() 175 path := filepath.Join(td, "file") 176 177 cmd, destroy := helperProcess("restart", path) 178 defer destroy() 179 180 testStateProxy(t, state, "web", cmd) 181 182 // We should see the path appear shortly 183 retry.Run(t, func(r *retry.R) { 184 _, err := os.Stat(path) 185 if err == nil { 186 return 187 } 188 r.Fatalf("error waiting for path: %s", err) 189 }) 190 191 // Update the proxy with a new path 192 oldPath := path 193 path = path + "2" 194 195 cmd, destroy = helperProcess("restart", path) 196 defer destroy() 197 198 testStateProxy(t, state, "web", cmd) 199 200 retry.Run(t, func(r *retry.R) { 201 _, err := os.Stat(path) 202 if err == nil { 203 return 204 } 205 r.Fatalf("error waiting for path: %s", err) 206 }) 207 208 // Old path should be gone 209 retry.Run(t, func(r *retry.R) { 210 _, err := os.Stat(oldPath) 211 if err == nil { 212 r.Fatalf("old path exists") 213 } 214 }) 215 } 216 217 func TestManagerRun_daemonLogs(t *testing.T) { 218 t.Parallel() 219 220 require := require.New(t) 221 state := local.TestState(t) 222 m, closer := testManager(t) 223 defer closer() 224 m.State = state 225 defer m.Kill() 226 227 // Configure a log dir so that we can read the logs 228 logDir := filepath.Join(m.DataDir, "logs") 229 230 // Create the service and calculate the log paths 231 path := filepath.Join(m.DataDir, "notify") 232 233 cmd, destroy := helperProcess("output", path) 234 defer destroy() 235 236 id := testStateProxy(t, state, "web", cmd) 237 stdoutPath := logPath(logDir, id, "stdout") 238 stderrPath := logPath(logDir, id, "stderr") 239 240 // Start the manager 241 go m.Run() 242 243 // We should see the path appear shortly 244 retry.Run(t, func(r *retry.R) { 245 if _, err := os.Stat(path); err != nil { 246 r.Fatalf("error waiting for stdout path: %s", err) 247 } 248 }) 249 250 expectedOut := "hello stdout\n" 251 actual, err := ioutil.ReadFile(stdoutPath) 252 require.NoError(err) 253 require.Equal([]byte(expectedOut), actual) 254 255 expectedErr := "hello stderr\n" 256 actual, err = ioutil.ReadFile(stderrPath) 257 require.NoError(err) 258 require.Equal([]byte(expectedErr), actual) 259 } 260 261 func TestManagerRun_daemonPid(t *testing.T) { 262 t.Parallel() 263 264 require := require.New(t) 265 state := local.TestState(t) 266 m, closer := testManager(t) 267 defer closer() 268 m.State = state 269 defer m.Kill() 270 271 // Configure a log dir so that we can read the logs 272 pidDir := filepath.Join(m.DataDir, "pids") 273 274 // Create the service and calculate the log paths 275 path := filepath.Join(m.DataDir, "notify") 276 277 cmd, destroy := helperProcess("output", path) 278 defer destroy() 279 280 id := testStateProxy(t, state, "web", cmd) 281 pidPath := pidPath(pidDir, id) 282 283 // Start the manager 284 go m.Run() 285 286 // We should see the path appear shortly 287 retry.Run(t, func(r *retry.R) { 288 if _, err := os.Stat(path); err != nil { 289 r.Fatalf("error waiting for stdout path: %s", err) 290 } 291 }) 292 293 // Verify the pid file is not empty 294 pidRaw, err := ioutil.ReadFile(pidPath) 295 require.NoError(err) 296 require.NotEmpty(pidRaw) 297 } 298 299 // Test to check if the parent and the child processes 300 // have the same environmental variables 301 302 func TestManagerPassesEnvironment(t *testing.T) { 303 t.Parallel() 304 305 require := require.New(t) 306 state := local.TestState(t) 307 m, closer := testManager(t) 308 defer closer() 309 m.State = state 310 defer m.Kill() 311 312 // Add Proxy for the test 313 td, closer := testTempDir(t) 314 defer closer() 315 path := filepath.Join(td, "env-variables") 316 317 cmd, destroy := helperProcess("environ", path) 318 defer destroy() 319 320 testStateProxy(t, state, "environTest", cmd) 321 322 //Run the manager 323 go m.Run() 324 325 //Get the environmental variables from the OS 326 var fileContent []byte 327 var err error 328 var data []byte 329 envData := os.Environ() 330 sort.Strings(envData) 331 for _, envVariable := range envData { 332 if strings.HasPrefix(envVariable, "CONSUL") || strings.HasPrefix(envVariable, "CONNECT") { 333 continue 334 } 335 data = append(data, envVariable...) 336 data = append(data, "\n"...) 337 } 338 339 // Check if the file written to from the spawned process 340 // has the necessary environmental variable data 341 retry.Run(t, func(r *retry.R) { 342 if fileContent, err = ioutil.ReadFile(path); err != nil { 343 r.Fatalf("No file ya dummy") 344 } 345 }) 346 347 require.Equal(data, fileContent) 348 } 349 350 // Test to check if the parent and the child processes 351 // have the same environmental variables 352 func TestManagerPassesProxyEnv(t *testing.T) { 353 t.Parallel() 354 355 require := require.New(t) 356 state := local.TestState(t) 357 m, closer := testManager(t) 358 defer closer() 359 m.State = state 360 defer m.Kill() 361 362 penv := make([]string, 0, 2) 363 penv = append(penv, "HTTP_ADDR=127.0.0.1:8500") 364 penv = append(penv, "HTTP_SSL=false") 365 m.ProxyEnv = penv 366 367 // Add Proxy for the test 368 td, closer := testTempDir(t) 369 defer closer() 370 path := filepath.Join(td, "env-variables") 371 372 cmd, destroy := helperProcess("environ", path) 373 defer destroy() 374 375 testStateProxy(t, state, "environTest", cmd) 376 377 //Run the manager 378 go m.Run() 379 380 //Get the environmental variables from the OS 381 var fileContent []byte 382 var err error 383 var data []byte 384 envData := os.Environ() 385 envData = append(envData, "HTTP_ADDR=127.0.0.1:8500") 386 envData = append(envData, "HTTP_SSL=false") 387 sort.Strings(envData) 388 for _, envVariable := range envData { 389 if strings.HasPrefix(envVariable, "CONSUL") || strings.HasPrefix(envVariable, "CONNECT") { 390 continue 391 } 392 data = append(data, envVariable...) 393 data = append(data, "\n"...) 394 } 395 396 // Check if the file written to from the spawned process 397 // has the necessary environmental variable data 398 retry.Run(t, func(r *retry.R) { 399 if fileContent, err = ioutil.ReadFile(path); err != nil { 400 r.Fatalf("No file ya dummy") 401 } 402 }) 403 404 require.Equal(data, fileContent) 405 } 406 407 // Test the Snapshot/Restore works. 408 func TestManagerRun_snapshotRestore(t *testing.T) { 409 t.Parallel() 410 411 require := require.New(t) 412 state := local.TestState(t) 413 m, closer := testManager(t) 414 defer closer() 415 m.State = state 416 defer m.Kill() 417 418 // Add the proxy 419 td, closer := testTempDir(t) 420 defer closer() 421 path := filepath.Join(td, "file") 422 423 cmd, destroy := helperProcess("start-stop", path) 424 defer destroy() 425 426 testStateProxy(t, state, "web", cmd) 427 428 // Set a low snapshot period so we get a snapshot 429 m.SnapshotPeriod = 10 * time.Millisecond 430 431 // Start the manager 432 go m.Run() 433 434 // We should see the path appear shortly 435 retry.Run(t, func(r *retry.R) { 436 _, err := os.Stat(path) 437 if err == nil { 438 return 439 } 440 r.Fatalf("error waiting for path: %s", err) 441 }) 442 443 // Wait for the snapshot 444 snapPath := m.SnapshotPath() 445 retry.Run(t, func(r *retry.R) { 446 raw, err := ioutil.ReadFile(snapPath) 447 if err != nil { 448 r.Fatalf("error waiting for path: %s", err) 449 } 450 if len(raw) < 30 { 451 r.Fatalf("snapshot too small") 452 } 453 }) 454 455 // Stop the sync 456 require.NoError(m.Close()) 457 458 // File should still exist 459 _, err := os.Stat(path) 460 require.NoError(err) 461 462 // Restore a manager from a snapshot 463 m2, closer := testManager(t) 464 m2.State = state 465 defer closer() 466 defer m2.Kill() 467 require.NoError(m2.Restore(snapPath)) 468 469 // Start 470 go m2.Run() 471 472 // Add a second proxy so that we can determine when we're up 473 // and running. 474 path2 := filepath.Join(td, "file2") 475 476 cmd, destroy = helperProcess("start-stop", path2) 477 defer destroy() 478 479 testStateProxy(t, state, "db", cmd) 480 481 retry.Run(t, func(r *retry.R) { 482 _, err := os.Stat(path2) 483 if err == nil { 484 return 485 } 486 r.Fatalf("error waiting for path: %s", err) 487 }) 488 489 // Kill m2, which should kill our main process 490 require.NoError(m2.Kill()) 491 492 // File should no longer exist 493 retry.Run(t, func(r *retry.R) { 494 _, err := os.Stat(path) 495 if err != nil { 496 return 497 } 498 r.Fatalf("file still exists: %s", path) 499 }) 500 } 501 502 // Manager should not run any proxies if we're running as root. Tests 503 // stub the value. 504 func TestManagerRun_rootDisallow(t *testing.T) { 505 // Pretend we are root 506 defer testSetRootValue(true)() 507 508 state := local.TestState(t) 509 m, closer := testManager(t) 510 defer closer() 511 m.State = state 512 defer m.Kill() 513 514 // Add the proxy before we start the manager to verify initial sync 515 td, closer := testTempDir(t) 516 defer closer() 517 path := filepath.Join(td, "file") 518 519 cmd, destroy := helperProcess("restart", path) 520 defer destroy() 521 522 testStateProxy(t, state, "web", cmd) 523 524 // Start the manager 525 go m.Run() 526 527 // Sleep a bit just to verify 528 time.Sleep(100 * time.Millisecond) 529 530 // We should see the path appear shortly 531 retry.Run(t, func(r *retry.R) { 532 _, err := os.Stat(path) 533 if err != nil { 534 return 535 } 536 537 r.Fatalf("path exists") 538 }) 539 } 540 541 func testManager(t *testing.T) (*Manager, func()) { 542 m := NewManager() 543 544 // Setup a default state 545 m.State = local.TestState(t) 546 547 // Set these periods low to speed up tests 548 m.CoalescePeriod = 1 * time.Millisecond 549 m.QuiescentPeriod = 1 * time.Millisecond 550 551 // Setup a temporary directory for logs 552 td, closer := testTempDir(t) 553 m.DataDir = td 554 555 return m, func() { closer() } 556 } 557 558 // testStateProxy registers a proxy with the given local state and the command 559 // (expected to be from the helperProcess function call). It returns the 560 // ID for deregistration. 561 func testStateProxy(t *testing.T, state *local.State, service string, cmd *exec.Cmd) string { 562 // *exec.Cmd must manually set args[0] to the binary. We automatically 563 // set this when constructing the command for the proxy, so we must strip 564 // the zero index. We do this unconditionally (anytime len is > 0) because 565 // index zero should ALWAYS be the binary. 566 if len(cmd.Args) > 0 { 567 cmd.Args = cmd.Args[1:] 568 } 569 570 command := []string{cmd.Path} 571 command = append(command, cmd.Args...) 572 573 require.NoError(t, state.AddService(&structs.NodeService{ 574 Service: service, 575 }, "token")) 576 577 p, err := state.AddProxy(&structs.ConnectManagedProxy{ 578 ExecMode: structs.ProxyExecModeDaemon, 579 Command: command, 580 TargetServiceID: service, 581 }, "token", "") 582 require.NoError(t, err) 583 584 return p.Proxy.ProxyService.ID 585 }