github.com/smithx10/nomad@v0.9.1-rc1/client/allocrunner/taskrunner/template/template_test.go (about) 1 package template 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "strings" 11 "testing" 12 "time" 13 14 ctestutil "github.com/hashicorp/consul/testutil" 15 "github.com/hashicorp/nomad/client/config" 16 "github.com/hashicorp/nomad/client/taskenv" 17 "github.com/hashicorp/nomad/helper" 18 "github.com/hashicorp/nomad/nomad/mock" 19 "github.com/hashicorp/nomad/nomad/structs" 20 sconfig "github.com/hashicorp/nomad/nomad/structs/config" 21 "github.com/hashicorp/nomad/testutil" 22 "github.com/stretchr/testify/assert" 23 "github.com/stretchr/testify/require" 24 ) 25 26 const ( 27 // TestTaskName is the name of the injected task. It should appear in the 28 // environment variable $NOMAD_TASK_NAME 29 TestTaskName = "test-task" 30 ) 31 32 // MockTaskHooks is a mock of the TaskHooks interface useful for testing 33 type MockTaskHooks struct { 34 Restarts int 35 RestartCh chan struct{} 36 37 Signals []string 38 SignalCh chan struct{} 39 40 // SignalError is returned when Signal is called on the mock hook 41 SignalError error 42 43 UnblockCh chan struct{} 44 45 KillEvent *structs.TaskEvent 46 KillCh chan struct{} 47 48 Events []*structs.TaskEvent 49 EmitEventCh chan struct{} 50 } 51 52 func NewMockTaskHooks() *MockTaskHooks { 53 return &MockTaskHooks{ 54 UnblockCh: make(chan struct{}, 1), 55 RestartCh: make(chan struct{}, 1), 56 SignalCh: make(chan struct{}, 1), 57 KillCh: make(chan struct{}, 1), 58 EmitEventCh: make(chan struct{}, 1), 59 } 60 } 61 func (m *MockTaskHooks) Restart(ctx context.Context, event *structs.TaskEvent, failure bool) error { 62 m.Restarts++ 63 select { 64 case m.RestartCh <- struct{}{}: 65 default: 66 } 67 return nil 68 } 69 70 func (m *MockTaskHooks) Signal(event *structs.TaskEvent, s string) error { 71 m.Signals = append(m.Signals, s) 72 select { 73 case m.SignalCh <- struct{}{}: 74 default: 75 } 76 77 return m.SignalError 78 } 79 80 func (m *MockTaskHooks) Kill(ctx context.Context, event *structs.TaskEvent) error { 81 m.KillEvent = event 82 select { 83 case m.KillCh <- struct{}{}: 84 default: 85 } 86 return nil 87 } 88 89 func (m *MockTaskHooks) EmitEvent(event *structs.TaskEvent) { 90 m.Events = append(m.Events, event) 91 select { 92 case m.EmitEventCh <- struct{}{}: 93 default: 94 } 95 } 96 97 func (m *MockTaskHooks) SetState(state string, event *structs.TaskEvent) {} 98 99 // testHarness is used to test the TaskTemplateManager by spinning up 100 // Consul/Vault as needed 101 type testHarness struct { 102 manager *TaskTemplateManager 103 mockHooks *MockTaskHooks 104 templates []*structs.Template 105 envBuilder *taskenv.Builder 106 node *structs.Node 107 config *config.Config 108 vaultToken string 109 taskDir string 110 vault *testutil.TestVault 111 consul *ctestutil.TestServer 112 emitRate time.Duration 113 } 114 115 // newTestHarness returns a harness starting a dev consul and vault server, 116 // building the appropriate config and creating a TaskTemplateManager 117 func newTestHarness(t *testing.T, templates []*structs.Template, consul, vault bool) *testHarness { 118 region := "global" 119 harness := &testHarness{ 120 mockHooks: NewMockTaskHooks(), 121 templates: templates, 122 node: mock.Node(), 123 config: &config.Config{Region: region}, 124 emitRate: DefaultMaxTemplateEventRate, 125 } 126 127 // Build the task environment 128 a := mock.Alloc() 129 task := a.Job.TaskGroups[0].Tasks[0] 130 task.Name = TestTaskName 131 harness.envBuilder = taskenv.NewBuilder(harness.node, a, task, region) 132 133 // Make a tempdir 134 d, err := ioutil.TempDir("", "ct_test") 135 if err != nil { 136 t.Fatalf("Failed to make tmpdir: %v", err) 137 } 138 harness.taskDir = d 139 140 if consul { 141 harness.consul, err = ctestutil.NewTestServer() 142 if err != nil { 143 t.Fatalf("error starting test Consul server: %v", err) 144 } 145 harness.config.ConsulConfig = &sconfig.ConsulConfig{ 146 Addr: harness.consul.HTTPAddr, 147 } 148 } 149 150 if vault { 151 harness.vault = testutil.NewTestVault(t) 152 harness.config.VaultConfig = harness.vault.Config 153 harness.vaultToken = harness.vault.RootToken 154 } 155 156 return harness 157 } 158 159 func (h *testHarness) start(t *testing.T) { 160 if err := h.startWithErr(); err != nil { 161 t.Fatalf("failed to build task template manager: %v", err) 162 } 163 } 164 165 func (h *testHarness) startWithErr() error { 166 var err error 167 h.manager, err = NewTaskTemplateManager(&TaskTemplateManagerConfig{ 168 UnblockCh: h.mockHooks.UnblockCh, 169 Lifecycle: h.mockHooks, 170 Events: h.mockHooks, 171 Templates: h.templates, 172 ClientConfig: h.config, 173 VaultToken: h.vaultToken, 174 TaskDir: h.taskDir, 175 EnvBuilder: h.envBuilder, 176 MaxTemplateEventRate: h.emitRate, 177 retryRate: 10 * time.Millisecond, 178 }) 179 180 return err 181 } 182 183 func (h *testHarness) setEmitRate(d time.Duration) { 184 h.emitRate = d 185 } 186 187 // stop is used to stop any running Vault or Consul server plus the task manager 188 func (h *testHarness) stop() { 189 if h.vault != nil { 190 h.vault.Stop() 191 } 192 if h.consul != nil { 193 h.consul.Stop() 194 } 195 if h.manager != nil { 196 h.manager.Stop() 197 } 198 if h.taskDir != "" { 199 os.RemoveAll(h.taskDir) 200 } 201 } 202 203 func TestTaskTemplateManager_InvalidConfig(t *testing.T) { 204 t.Parallel() 205 hooks := NewMockTaskHooks() 206 clientConfig := &config.Config{Region: "global"} 207 taskDir := "foo" 208 a := mock.Alloc() 209 envBuilder := taskenv.NewBuilder(mock.Node(), a, a.Job.TaskGroups[0].Tasks[0], clientConfig.Region) 210 211 cases := []struct { 212 name string 213 config *TaskTemplateManagerConfig 214 expectedErr string 215 }{ 216 { 217 name: "nil config", 218 config: nil, 219 expectedErr: "Nil config passed", 220 }, 221 { 222 name: "bad lifecycle hooks", 223 config: &TaskTemplateManagerConfig{ 224 UnblockCh: hooks.UnblockCh, 225 Events: hooks, 226 ClientConfig: clientConfig, 227 TaskDir: taskDir, 228 EnvBuilder: envBuilder, 229 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 230 }, 231 expectedErr: "lifecycle hooks", 232 }, 233 { 234 name: "bad event hooks", 235 config: &TaskTemplateManagerConfig{ 236 UnblockCh: hooks.UnblockCh, 237 Lifecycle: hooks, 238 ClientConfig: clientConfig, 239 TaskDir: taskDir, 240 EnvBuilder: envBuilder, 241 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 242 }, 243 expectedErr: "event hook", 244 }, 245 { 246 name: "bad client config", 247 config: &TaskTemplateManagerConfig{ 248 UnblockCh: hooks.UnblockCh, 249 Lifecycle: hooks, 250 Events: hooks, 251 TaskDir: taskDir, 252 EnvBuilder: envBuilder, 253 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 254 }, 255 expectedErr: "client config", 256 }, 257 { 258 name: "bad task dir", 259 config: &TaskTemplateManagerConfig{ 260 UnblockCh: hooks.UnblockCh, 261 ClientConfig: clientConfig, 262 Lifecycle: hooks, 263 Events: hooks, 264 EnvBuilder: envBuilder, 265 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 266 }, 267 expectedErr: "task directory", 268 }, 269 { 270 name: "bad env builder", 271 config: &TaskTemplateManagerConfig{ 272 UnblockCh: hooks.UnblockCh, 273 ClientConfig: clientConfig, 274 Lifecycle: hooks, 275 Events: hooks, 276 TaskDir: taskDir, 277 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 278 }, 279 expectedErr: "task environment", 280 }, 281 { 282 name: "bad max event rate", 283 config: &TaskTemplateManagerConfig{ 284 UnblockCh: hooks.UnblockCh, 285 ClientConfig: clientConfig, 286 Lifecycle: hooks, 287 Events: hooks, 288 TaskDir: taskDir, 289 EnvBuilder: envBuilder, 290 }, 291 expectedErr: "template event rate", 292 }, 293 { 294 name: "valid", 295 config: &TaskTemplateManagerConfig{ 296 UnblockCh: hooks.UnblockCh, 297 ClientConfig: clientConfig, 298 Lifecycle: hooks, 299 Events: hooks, 300 TaskDir: taskDir, 301 EnvBuilder: envBuilder, 302 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 303 }, 304 }, 305 { 306 name: "invalid signal", 307 config: &TaskTemplateManagerConfig{ 308 UnblockCh: hooks.UnblockCh, 309 Templates: []*structs.Template{ 310 { 311 DestPath: "foo", 312 EmbeddedTmpl: "hello, world", 313 ChangeMode: structs.TemplateChangeModeSignal, 314 ChangeSignal: "foobarbaz", 315 }, 316 }, 317 ClientConfig: clientConfig, 318 Lifecycle: hooks, 319 Events: hooks, 320 TaskDir: taskDir, 321 EnvBuilder: envBuilder, 322 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 323 }, 324 expectedErr: "parse signal", 325 }, 326 } 327 328 for _, c := range cases { 329 t.Run(c.name, func(t *testing.T) { 330 _, err := NewTaskTemplateManager(c.config) 331 if err != nil { 332 if c.expectedErr == "" { 333 t.Fatalf("unexpected error: %v", err) 334 } else if !strings.Contains(err.Error(), c.expectedErr) { 335 t.Fatalf("expected error to contain %q; got %q", c.expectedErr, err.Error()) 336 } 337 } else if c.expectedErr != "" { 338 t.Fatalf("expected an error to contain %q", c.expectedErr) 339 } 340 }) 341 } 342 } 343 344 func TestTaskTemplateManager_HostPath(t *testing.T) { 345 t.Parallel() 346 // Make a template that will render immediately and write it to a tmp file 347 f, err := ioutil.TempFile("", "") 348 if err != nil { 349 t.Fatalf("Bad: %v", err) 350 } 351 defer f.Close() 352 defer os.Remove(f.Name()) 353 354 content := "hello, world!" 355 if _, err := io.WriteString(f, content); err != nil { 356 t.Fatalf("Bad: %v", err) 357 } 358 359 file := "my.tmpl" 360 template := &structs.Template{ 361 SourcePath: f.Name(), 362 DestPath: file, 363 ChangeMode: structs.TemplateChangeModeNoop, 364 } 365 366 harness := newTestHarness(t, []*structs.Template{template}, false, false) 367 harness.start(t) 368 defer harness.stop() 369 370 // Wait for the unblock 371 select { 372 case <-harness.mockHooks.UnblockCh: 373 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 374 t.Fatalf("Task unblock should have been called") 375 } 376 377 // Check the file is there 378 path := filepath.Join(harness.taskDir, file) 379 raw, err := ioutil.ReadFile(path) 380 if err != nil { 381 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 382 } 383 384 if s := string(raw); s != content { 385 t.Fatalf("Unexpected template data; got %q, want %q", s, content) 386 } 387 388 // Change the config to disallow host sources 389 harness = newTestHarness(t, []*structs.Template{template}, false, false) 390 harness.config.Options = map[string]string{ 391 hostSrcOption: "false", 392 } 393 if err := harness.startWithErr(); err == nil || !strings.Contains(err.Error(), "absolute") { 394 t.Fatalf("Expected absolute template path disallowed: %v", err) 395 } 396 } 397 398 func TestTaskTemplateManager_Unblock_Static(t *testing.T) { 399 t.Parallel() 400 // Make a template that will render immediately 401 content := "hello, world!" 402 file := "my.tmpl" 403 template := &structs.Template{ 404 EmbeddedTmpl: content, 405 DestPath: file, 406 ChangeMode: structs.TemplateChangeModeNoop, 407 } 408 409 harness := newTestHarness(t, []*structs.Template{template}, false, false) 410 harness.start(t) 411 defer harness.stop() 412 413 // Wait for the unblock 414 select { 415 case <-harness.mockHooks.UnblockCh: 416 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 417 t.Fatalf("Task unblock should have been called") 418 } 419 420 // Check the file is there 421 path := filepath.Join(harness.taskDir, file) 422 raw, err := ioutil.ReadFile(path) 423 if err != nil { 424 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 425 } 426 427 if s := string(raw); s != content { 428 t.Fatalf("Unexpected template data; got %q, want %q", s, content) 429 } 430 } 431 432 func TestTaskTemplateManager_Permissions(t *testing.T) { 433 t.Parallel() 434 // Make a template that will render immediately 435 content := "hello, world!" 436 file := "my.tmpl" 437 template := &structs.Template{ 438 EmbeddedTmpl: content, 439 DestPath: file, 440 ChangeMode: structs.TemplateChangeModeNoop, 441 Perms: "777", 442 } 443 444 harness := newTestHarness(t, []*structs.Template{template}, false, false) 445 harness.start(t) 446 defer harness.stop() 447 448 // Wait for the unblock 449 select { 450 case <-harness.mockHooks.UnblockCh: 451 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 452 t.Fatalf("Task unblock should have been called") 453 } 454 455 // Check the file is there 456 path := filepath.Join(harness.taskDir, file) 457 fi, err := os.Stat(path) 458 if err != nil { 459 t.Fatalf("Failed to stat file: %v", err) 460 } 461 462 if m := fi.Mode(); m != os.ModePerm { 463 t.Fatalf("Got mode %v; want %v", m, os.ModePerm) 464 } 465 } 466 467 func TestTaskTemplateManager_Unblock_Static_NomadEnv(t *testing.T) { 468 t.Parallel() 469 // Make a template that will render immediately 470 content := `Hello Nomad Task: {{env "NOMAD_TASK_NAME"}}` 471 expected := fmt.Sprintf("Hello Nomad Task: %s", TestTaskName) 472 file := "my.tmpl" 473 template := &structs.Template{ 474 EmbeddedTmpl: content, 475 DestPath: file, 476 ChangeMode: structs.TemplateChangeModeNoop, 477 } 478 479 harness := newTestHarness(t, []*structs.Template{template}, false, false) 480 harness.start(t) 481 defer harness.stop() 482 483 // Wait for the unblock 484 select { 485 case <-harness.mockHooks.UnblockCh: 486 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 487 t.Fatalf("Task unblock should have been called") 488 } 489 490 // Check the file is there 491 path := filepath.Join(harness.taskDir, file) 492 raw, err := ioutil.ReadFile(path) 493 if err != nil { 494 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 495 } 496 497 if s := string(raw); s != expected { 498 t.Fatalf("Unexpected template data; got %q, want %q", s, expected) 499 } 500 } 501 502 func TestTaskTemplateManager_Unblock_Static_AlreadyRendered(t *testing.T) { 503 t.Parallel() 504 // Make a template that will render immediately 505 content := "hello, world!" 506 file := "my.tmpl" 507 template := &structs.Template{ 508 EmbeddedTmpl: content, 509 DestPath: file, 510 ChangeMode: structs.TemplateChangeModeNoop, 511 } 512 513 harness := newTestHarness(t, []*structs.Template{template}, false, false) 514 515 // Write the contents 516 path := filepath.Join(harness.taskDir, file) 517 if err := ioutil.WriteFile(path, []byte(content), 0777); err != nil { 518 t.Fatalf("Failed to write data: %v", err) 519 } 520 521 harness.start(t) 522 defer harness.stop() 523 524 // Wait for the unblock 525 select { 526 case <-harness.mockHooks.UnblockCh: 527 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 528 t.Fatalf("Task unblock should have been called") 529 } 530 531 // Check the file is there 532 path = filepath.Join(harness.taskDir, file) 533 raw, err := ioutil.ReadFile(path) 534 if err != nil { 535 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 536 } 537 538 if s := string(raw); s != content { 539 t.Fatalf("Unexpected template data; got %q, want %q", s, content) 540 } 541 } 542 543 func TestTaskTemplateManager_Unblock_Consul(t *testing.T) { 544 t.Parallel() 545 // Make a template that will render based on a key in Consul 546 key := "foo" 547 content := "barbaz" 548 embedded := fmt.Sprintf(`{{key "%s"}}`, key) 549 file := "my.tmpl" 550 template := &structs.Template{ 551 EmbeddedTmpl: embedded, 552 DestPath: file, 553 ChangeMode: structs.TemplateChangeModeNoop, 554 } 555 556 harness := newTestHarness(t, []*structs.Template{template}, true, false) 557 harness.start(t) 558 defer harness.stop() 559 560 // Ensure no unblock 561 select { 562 case <-harness.mockHooks.UnblockCh: 563 t.Fatalf("Task unblock should have not have been called") 564 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 565 } 566 567 // Write the key to Consul 568 harness.consul.SetKV(t, key, []byte(content)) 569 570 // Wait for the unblock 571 select { 572 case <-harness.mockHooks.UnblockCh: 573 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 574 t.Fatalf("Task unblock should have been called") 575 } 576 577 // Check the file is there 578 path := filepath.Join(harness.taskDir, file) 579 raw, err := ioutil.ReadFile(path) 580 if err != nil { 581 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 582 } 583 584 if s := string(raw); s != content { 585 t.Fatalf("Unexpected template data; got %q, want %q", s, content) 586 } 587 } 588 589 func TestTaskTemplateManager_Unblock_Vault(t *testing.T) { 590 t.Parallel() 591 require := require.New(t) 592 // Make a template that will render based on a key in Vault 593 vaultPath := "secret/data/password" 594 key := "password" 595 content := "barbaz" 596 embedded := fmt.Sprintf(`{{with secret "%s"}}{{.Data.data.%s}}{{end}}`, vaultPath, key) 597 file := "my.tmpl" 598 template := &structs.Template{ 599 EmbeddedTmpl: embedded, 600 DestPath: file, 601 ChangeMode: structs.TemplateChangeModeNoop, 602 } 603 604 harness := newTestHarness(t, []*structs.Template{template}, false, true) 605 harness.start(t) 606 defer harness.stop() 607 608 // Ensure no unblock 609 select { 610 case <-harness.mockHooks.UnblockCh: 611 t.Fatalf("Task unblock should not have been called") 612 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 613 } 614 615 // Write the secret to Vault 616 logical := harness.vault.Client.Logical() 617 _, err := logical.Write(vaultPath, map[string]interface{}{"data": map[string]interface{}{key: content}}) 618 require.NoError(err) 619 620 // Wait for the unblock 621 select { 622 case <-harness.mockHooks.UnblockCh: 623 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 624 t.Fatalf("Task unblock should have been called") 625 } 626 627 // Check the file is there 628 path := filepath.Join(harness.taskDir, file) 629 raw, err := ioutil.ReadFile(path) 630 if err != nil { 631 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 632 } 633 634 if s := string(raw); s != content { 635 t.Fatalf("Unexpected template data; got %q, want %q", s, content) 636 } 637 } 638 639 func TestTaskTemplateManager_Unblock_Multi_Template(t *testing.T) { 640 t.Parallel() 641 // Make a template that will render immediately 642 staticContent := "hello, world!" 643 staticFile := "my.tmpl" 644 template := &structs.Template{ 645 EmbeddedTmpl: staticContent, 646 DestPath: staticFile, 647 ChangeMode: structs.TemplateChangeModeNoop, 648 } 649 650 // Make a template that will render based on a key in Consul 651 consulKey := "foo" 652 consulContent := "barbaz" 653 consulEmbedded := fmt.Sprintf(`{{key "%s"}}`, consulKey) 654 consulFile := "consul.tmpl" 655 template2 := &structs.Template{ 656 EmbeddedTmpl: consulEmbedded, 657 DestPath: consulFile, 658 ChangeMode: structs.TemplateChangeModeNoop, 659 } 660 661 harness := newTestHarness(t, []*structs.Template{template, template2}, true, false) 662 harness.start(t) 663 defer harness.stop() 664 665 // Ensure no unblock 666 select { 667 case <-harness.mockHooks.UnblockCh: 668 t.Fatalf("Task unblock should have not have been called") 669 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 670 } 671 672 // Check that the static file has been rendered 673 path := filepath.Join(harness.taskDir, staticFile) 674 raw, err := ioutil.ReadFile(path) 675 if err != nil { 676 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 677 } 678 679 if s := string(raw); s != staticContent { 680 t.Fatalf("Unexpected template data; got %q, want %q", s, staticContent) 681 } 682 683 // Write the key to Consul 684 harness.consul.SetKV(t, consulKey, []byte(consulContent)) 685 686 // Wait for the unblock 687 select { 688 case <-harness.mockHooks.UnblockCh: 689 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 690 t.Fatalf("Task unblock should have been called") 691 } 692 693 // Check the consul file is there 694 path = filepath.Join(harness.taskDir, consulFile) 695 raw, err = ioutil.ReadFile(path) 696 if err != nil { 697 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 698 } 699 700 if s := string(raw); s != consulContent { 701 t.Fatalf("Unexpected template data; got %q, want %q", s, consulContent) 702 } 703 } 704 705 func TestTaskTemplateManager_Rerender_Noop(t *testing.T) { 706 t.Parallel() 707 // Make a template that will render based on a key in Consul 708 key := "foo" 709 content1 := "bar" 710 content2 := "baz" 711 embedded := fmt.Sprintf(`{{key "%s"}}`, key) 712 file := "my.tmpl" 713 template := &structs.Template{ 714 EmbeddedTmpl: embedded, 715 DestPath: file, 716 ChangeMode: structs.TemplateChangeModeNoop, 717 } 718 719 harness := newTestHarness(t, []*structs.Template{template}, true, false) 720 harness.start(t) 721 defer harness.stop() 722 723 // Ensure no unblock 724 select { 725 case <-harness.mockHooks.UnblockCh: 726 t.Fatalf("Task unblock should have not have been called") 727 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 728 } 729 730 // Write the key to Consul 731 harness.consul.SetKV(t, key, []byte(content1)) 732 733 // Wait for the unblock 734 select { 735 case <-harness.mockHooks.UnblockCh: 736 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 737 t.Fatalf("Task unblock should have been called") 738 } 739 740 // Check the file is there 741 path := filepath.Join(harness.taskDir, file) 742 raw, err := ioutil.ReadFile(path) 743 if err != nil { 744 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 745 } 746 747 if s := string(raw); s != content1 { 748 t.Fatalf("Unexpected template data; got %q, want %q", s, content1) 749 } 750 751 // Update the key in Consul 752 harness.consul.SetKV(t, key, []byte(content2)) 753 754 select { 755 case <-harness.mockHooks.RestartCh: 756 t.Fatalf("Noop ignored: %+v", harness.mockHooks) 757 case <-harness.mockHooks.SignalCh: 758 t.Fatalf("Noop ignored: %+v", harness.mockHooks) 759 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 760 } 761 762 // Check the file has been updated 763 path = filepath.Join(harness.taskDir, file) 764 raw, err = ioutil.ReadFile(path) 765 if err != nil { 766 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 767 } 768 769 if s := string(raw); s != content2 { 770 t.Fatalf("Unexpected template data; got %q, want %q", s, content2) 771 } 772 } 773 774 func TestTaskTemplateManager_Rerender_Signal(t *testing.T) { 775 t.Parallel() 776 // Make a template that renders based on a key in Consul and sends SIGALRM 777 key1 := "foo" 778 content1_1 := "bar" 779 content1_2 := "baz" 780 embedded1 := fmt.Sprintf(`{{key "%s"}}`, key1) 781 file1 := "my.tmpl" 782 template := &structs.Template{ 783 EmbeddedTmpl: embedded1, 784 DestPath: file1, 785 ChangeMode: structs.TemplateChangeModeSignal, 786 ChangeSignal: "SIGALRM", 787 } 788 789 // Make a template that renders based on a key in Consul and sends SIGBUS 790 key2 := "bam" 791 content2_1 := "cat" 792 content2_2 := "dog" 793 embedded2 := fmt.Sprintf(`{{key "%s"}}`, key2) 794 file2 := "my-second.tmpl" 795 template2 := &structs.Template{ 796 EmbeddedTmpl: embedded2, 797 DestPath: file2, 798 ChangeMode: structs.TemplateChangeModeSignal, 799 ChangeSignal: "SIGBUS", 800 } 801 802 harness := newTestHarness(t, []*structs.Template{template, template2}, true, false) 803 harness.start(t) 804 defer harness.stop() 805 806 // Ensure no unblock 807 select { 808 case <-harness.mockHooks.UnblockCh: 809 t.Fatalf("Task unblock should have not have been called") 810 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 811 } 812 813 // Write the key to Consul 814 harness.consul.SetKV(t, key1, []byte(content1_1)) 815 harness.consul.SetKV(t, key2, []byte(content2_1)) 816 817 // Wait for the unblock 818 select { 819 case <-harness.mockHooks.UnblockCh: 820 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 821 t.Fatalf("Task unblock should have been called") 822 } 823 824 if len(harness.mockHooks.Signals) != 0 { 825 t.Fatalf("Should not have received any signals: %+v", harness.mockHooks) 826 } 827 828 // Update the keys in Consul 829 harness.consul.SetKV(t, key1, []byte(content1_2)) 830 harness.consul.SetKV(t, key2, []byte(content2_2)) 831 832 // Wait for signals 833 timeout := time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second) 834 OUTER: 835 for { 836 select { 837 case <-harness.mockHooks.RestartCh: 838 t.Fatalf("Restart with signal policy: %+v", harness.mockHooks) 839 case <-harness.mockHooks.SignalCh: 840 if len(harness.mockHooks.Signals) != 2 { 841 continue 842 } 843 break OUTER 844 case <-timeout: 845 t.Fatalf("Should have received two signals: %+v", harness.mockHooks) 846 } 847 } 848 849 // Check the files have been updated 850 path := filepath.Join(harness.taskDir, file1) 851 raw, err := ioutil.ReadFile(path) 852 if err != nil { 853 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 854 } 855 856 if s := string(raw); s != content1_2 { 857 t.Fatalf("Unexpected template data; got %q, want %q", s, content1_2) 858 } 859 860 path = filepath.Join(harness.taskDir, file2) 861 raw, err = ioutil.ReadFile(path) 862 if err != nil { 863 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 864 } 865 866 if s := string(raw); s != content2_2 { 867 t.Fatalf("Unexpected template data; got %q, want %q", s, content2_2) 868 } 869 } 870 871 func TestTaskTemplateManager_Rerender_Restart(t *testing.T) { 872 t.Parallel() 873 // Make a template that renders based on a key in Consul and sends restart 874 key1 := "bam" 875 content1_1 := "cat" 876 content1_2 := "dog" 877 embedded1 := fmt.Sprintf(`{{key "%s"}}`, key1) 878 file1 := "my.tmpl" 879 template := &structs.Template{ 880 EmbeddedTmpl: embedded1, 881 DestPath: file1, 882 ChangeMode: structs.TemplateChangeModeRestart, 883 } 884 885 harness := newTestHarness(t, []*structs.Template{template}, true, false) 886 harness.start(t) 887 defer harness.stop() 888 889 // Ensure no unblock 890 select { 891 case <-harness.mockHooks.UnblockCh: 892 t.Fatalf("Task unblock should have not have been called") 893 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 894 } 895 896 // Write the key to Consul 897 harness.consul.SetKV(t, key1, []byte(content1_1)) 898 899 // Wait for the unblock 900 select { 901 case <-harness.mockHooks.UnblockCh: 902 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 903 t.Fatalf("Task unblock should have been called") 904 } 905 906 // Update the keys in Consul 907 harness.consul.SetKV(t, key1, []byte(content1_2)) 908 909 // Wait for restart 910 timeout := time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second) 911 OUTER: 912 for { 913 select { 914 case <-harness.mockHooks.RestartCh: 915 break OUTER 916 case <-harness.mockHooks.SignalCh: 917 t.Fatalf("Signal with restart policy: %+v", harness.mockHooks) 918 case <-timeout: 919 t.Fatalf("Should have received a restart: %+v", harness.mockHooks) 920 } 921 } 922 923 // Check the files have been updated 924 path := filepath.Join(harness.taskDir, file1) 925 raw, err := ioutil.ReadFile(path) 926 if err != nil { 927 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 928 } 929 930 if s := string(raw); s != content1_2 { 931 t.Fatalf("Unexpected template data; got %q, want %q", s, content1_2) 932 } 933 } 934 935 func TestTaskTemplateManager_Interpolate_Destination(t *testing.T) { 936 t.Parallel() 937 // Make a template that will have its destination interpolated 938 content := "hello, world!" 939 file := "${node.unique.id}.tmpl" 940 template := &structs.Template{ 941 EmbeddedTmpl: content, 942 DestPath: file, 943 ChangeMode: structs.TemplateChangeModeNoop, 944 } 945 946 harness := newTestHarness(t, []*structs.Template{template}, false, false) 947 harness.start(t) 948 defer harness.stop() 949 950 // Ensure unblock 951 select { 952 case <-harness.mockHooks.UnblockCh: 953 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 954 t.Fatalf("Task unblock should have been called") 955 } 956 957 // Check the file is there 958 actual := fmt.Sprintf("%s.tmpl", harness.node.ID) 959 path := filepath.Join(harness.taskDir, actual) 960 raw, err := ioutil.ReadFile(path) 961 if err != nil { 962 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 963 } 964 965 if s := string(raw); s != content { 966 t.Fatalf("Unexpected template data; got %q, want %q", s, content) 967 } 968 } 969 970 func TestTaskTemplateManager_Signal_Error(t *testing.T) { 971 t.Parallel() 972 require := require.New(t) 973 974 // Make a template that renders based on a key in Consul and sends SIGALRM 975 key1 := "foo" 976 content1 := "bar" 977 content2 := "baz" 978 embedded1 := fmt.Sprintf(`{{key "%s"}}`, key1) 979 file1 := "my.tmpl" 980 template := &structs.Template{ 981 EmbeddedTmpl: embedded1, 982 DestPath: file1, 983 ChangeMode: structs.TemplateChangeModeSignal, 984 ChangeSignal: "SIGALRM", 985 } 986 987 harness := newTestHarness(t, []*structs.Template{template}, true, false) 988 harness.start(t) 989 defer harness.stop() 990 991 harness.mockHooks.SignalError = fmt.Errorf("test error") 992 993 // Write the key to Consul 994 harness.consul.SetKV(t, key1, []byte(content1)) 995 996 // Wait a little 997 select { 998 case <-harness.mockHooks.UnblockCh: 999 case <-time.After(time.Duration(2*testutil.TestMultiplier()) * time.Second): 1000 t.Fatalf("Should have received unblock: %+v", harness.mockHooks) 1001 } 1002 1003 // Write the key to Consul 1004 harness.consul.SetKV(t, key1, []byte(content2)) 1005 1006 // Wait for kill channel 1007 select { 1008 case <-harness.mockHooks.KillCh: 1009 break 1010 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 1011 t.Fatalf("Should have received a signals: %+v", harness.mockHooks) 1012 } 1013 1014 require.NotNil(harness.mockHooks.KillEvent) 1015 require.Contains(harness.mockHooks.KillEvent.DisplayMessage, "failed to send signals") 1016 } 1017 1018 // TestTaskTemplateManager_Env asserts templates with the env flag set are read 1019 // into the task's environment. 1020 func TestTaskTemplateManager_Env(t *testing.T) { 1021 t.Parallel() 1022 template := &structs.Template{ 1023 EmbeddedTmpl: ` 1024 # Comment lines are ok 1025 1026 FOO=bar 1027 foo=123 1028 ANYTHING_goes=Spaces are=ok! 1029 `, 1030 DestPath: "test.env", 1031 ChangeMode: structs.TemplateChangeModeNoop, 1032 Envvars: true, 1033 } 1034 harness := newTestHarness(t, []*structs.Template{template}, true, false) 1035 harness.start(t) 1036 defer harness.stop() 1037 1038 // Wait a little 1039 select { 1040 case <-harness.mockHooks.UnblockCh: 1041 case <-time.After(time.Duration(2*testutil.TestMultiplier()) * time.Second): 1042 t.Fatalf("Should have received unblock: %+v", harness.mockHooks) 1043 } 1044 1045 // Validate environment 1046 env := harness.envBuilder.Build().Map() 1047 if len(env) < 3 { 1048 t.Fatalf("expected at least 3 env vars but found %d:\n%#v\n", len(env), env) 1049 } 1050 if env["FOO"] != "bar" { 1051 t.Errorf("expected FOO=bar but found %q", env["FOO"]) 1052 } 1053 if env["foo"] != "123" { 1054 t.Errorf("expected foo=123 but found %q", env["foo"]) 1055 } 1056 if env["ANYTHING_goes"] != "Spaces are=ok!" { 1057 t.Errorf("expected ANYTHING_GOES='Spaces are ok!' but found %q", env["ANYTHING_goes"]) 1058 } 1059 } 1060 1061 // TestTaskTemplateManager_Env_Missing asserts the core env 1062 // template processing function returns errors when files don't exist 1063 func TestTaskTemplateManager_Env_Missing(t *testing.T) { 1064 t.Parallel() 1065 d, err := ioutil.TempDir("", "ct_env_missing") 1066 if err != nil { 1067 t.Fatalf("err: %v", err) 1068 } 1069 defer os.RemoveAll(d) 1070 1071 // Fake writing the file so we don't have to run the whole template manager 1072 err = ioutil.WriteFile(filepath.Join(d, "exists.env"), []byte("FOO=bar\n"), 0644) 1073 if err != nil { 1074 t.Fatalf("error writing template file: %v", err) 1075 } 1076 1077 templates := []*structs.Template{ 1078 { 1079 EmbeddedTmpl: "FOO=bar\n", 1080 DestPath: "exists.env", 1081 Envvars: true, 1082 }, 1083 { 1084 EmbeddedTmpl: "WHAT=ever\n", 1085 DestPath: "missing.env", 1086 Envvars: true, 1087 }, 1088 } 1089 1090 if vars, err := loadTemplateEnv(templates, d, taskenv.NewEmptyTaskEnv()); err == nil { 1091 t.Fatalf("expected an error but instead got env vars: %#v", vars) 1092 } 1093 } 1094 1095 // TestTaskTemplateManager_Env_InterpolatedDest asserts the core env 1096 // template processing function handles interpolated destinations 1097 func TestTaskTemplateManager_Env_InterpolatedDest(t *testing.T) { 1098 t.Parallel() 1099 require := require.New(t) 1100 1101 d, err := ioutil.TempDir("", "ct_env_interpolated") 1102 if err != nil { 1103 t.Fatalf("err: %v", err) 1104 } 1105 defer os.RemoveAll(d) 1106 1107 // Fake writing the file so we don't have to run the whole template manager 1108 err = ioutil.WriteFile(filepath.Join(d, "exists.env"), []byte("FOO=bar\n"), 0644) 1109 if err != nil { 1110 t.Fatalf("error writing template file: %v", err) 1111 } 1112 1113 templates := []*structs.Template{ 1114 { 1115 EmbeddedTmpl: "FOO=bar\n", 1116 DestPath: "${NOMAD_META_path}.env", 1117 Envvars: true, 1118 }, 1119 } 1120 1121 // Build the env 1122 taskEnv := taskenv.NewTaskEnv( 1123 map[string]string{"NOMAD_META_path": "exists"}, 1124 map[string]string{}, map[string]string{}) 1125 1126 vars, err := loadTemplateEnv(templates, d, taskEnv) 1127 require.NoError(err) 1128 require.Contains(vars, "FOO") 1129 require.Equal(vars["FOO"], "bar") 1130 } 1131 1132 // TestTaskTemplateManager_Env_Multi asserts the core env 1133 // template processing function returns combined env vars from multiple 1134 // templates correctly. 1135 func TestTaskTemplateManager_Env_Multi(t *testing.T) { 1136 t.Parallel() 1137 d, err := ioutil.TempDir("", "ct_env_missing") 1138 if err != nil { 1139 t.Fatalf("err: %v", err) 1140 } 1141 defer os.RemoveAll(d) 1142 1143 // Fake writing the files so we don't have to run the whole template manager 1144 err = ioutil.WriteFile(filepath.Join(d, "zzz.env"), []byte("FOO=bar\nSHARED=nope\n"), 0644) 1145 if err != nil { 1146 t.Fatalf("error writing template file 1: %v", err) 1147 } 1148 err = ioutil.WriteFile(filepath.Join(d, "aaa.env"), []byte("BAR=foo\nSHARED=yup\n"), 0644) 1149 if err != nil { 1150 t.Fatalf("error writing template file 2: %v", err) 1151 } 1152 1153 // Templates will get loaded in order (not alpha sorted) 1154 templates := []*structs.Template{ 1155 { 1156 DestPath: "zzz.env", 1157 Envvars: true, 1158 }, 1159 { 1160 DestPath: "aaa.env", 1161 Envvars: true, 1162 }, 1163 } 1164 1165 vars, err := loadTemplateEnv(templates, d, taskenv.NewEmptyTaskEnv()) 1166 if err != nil { 1167 t.Fatalf("expected no error: %v", err) 1168 } 1169 if vars["FOO"] != "bar" { 1170 t.Errorf("expected FOO=bar but found %q", vars["FOO"]) 1171 } 1172 if vars["BAR"] != "foo" { 1173 t.Errorf("expected BAR=foo but found %q", vars["BAR"]) 1174 } 1175 if vars["SHARED"] != "yup" { 1176 t.Errorf("expected FOO=bar but found %q", vars["yup"]) 1177 } 1178 } 1179 1180 func TestTaskTemplateManager_Rerender_Env(t *testing.T) { 1181 t.Parallel() 1182 // Make a template that renders based on a key in Consul and sends restart 1183 key1 := "bam" 1184 key2 := "bar" 1185 content1_1 := "cat" 1186 content1_2 := "dog" 1187 t1 := &structs.Template{ 1188 EmbeddedTmpl: ` 1189 FOO={{key "bam"}} 1190 `, 1191 DestPath: "test.env", 1192 ChangeMode: structs.TemplateChangeModeRestart, 1193 Envvars: true, 1194 } 1195 t2 := &structs.Template{ 1196 EmbeddedTmpl: ` 1197 BAR={{key "bar"}} 1198 `, 1199 DestPath: "test2.env", 1200 ChangeMode: structs.TemplateChangeModeRestart, 1201 Envvars: true, 1202 } 1203 1204 harness := newTestHarness(t, []*structs.Template{t1, t2}, true, false) 1205 harness.start(t) 1206 defer harness.stop() 1207 1208 // Ensure no unblock 1209 select { 1210 case <-harness.mockHooks.UnblockCh: 1211 t.Fatalf("Task unblock should have not have been called") 1212 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 1213 } 1214 1215 // Write the key to Consul 1216 harness.consul.SetKV(t, key1, []byte(content1_1)) 1217 harness.consul.SetKV(t, key2, []byte(content1_1)) 1218 1219 // Wait for the unblock 1220 select { 1221 case <-harness.mockHooks.UnblockCh: 1222 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 1223 t.Fatalf("Task unblock should have been called") 1224 } 1225 1226 env := harness.envBuilder.Build().Map() 1227 if v, ok := env["FOO"]; !ok || v != content1_1 { 1228 t.Fatalf("Bad env for FOO: %v %v", v, ok) 1229 } 1230 if v, ok := env["BAR"]; !ok || v != content1_1 { 1231 t.Fatalf("Bad env for BAR: %v %v", v, ok) 1232 } 1233 1234 // Update the keys in Consul 1235 harness.consul.SetKV(t, key1, []byte(content1_2)) 1236 1237 // Wait for restart 1238 timeout := time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second) 1239 OUTER: 1240 for { 1241 select { 1242 case <-harness.mockHooks.RestartCh: 1243 break OUTER 1244 case <-harness.mockHooks.SignalCh: 1245 t.Fatalf("Signal with restart policy: %+v", harness.mockHooks) 1246 case <-timeout: 1247 t.Fatalf("Should have received a restart: %+v", harness.mockHooks) 1248 } 1249 } 1250 1251 env = harness.envBuilder.Build().Map() 1252 if v, ok := env["FOO"]; !ok || v != content1_2 { 1253 t.Fatalf("Bad env for FOO: %v %v", v, ok) 1254 } 1255 if v, ok := env["BAR"]; !ok || v != content1_1 { 1256 t.Fatalf("Bad env for BAR: %v %v", v, ok) 1257 } 1258 } 1259 1260 // TestTaskTemplateManager_Config_ServerName asserts the tls_server_name 1261 // setting is propagated to consul-template's configuration. See #2776 1262 func TestTaskTemplateManager_Config_ServerName(t *testing.T) { 1263 t.Parallel() 1264 c := config.DefaultConfig() 1265 c.VaultConfig = &sconfig.VaultConfig{ 1266 Enabled: helper.BoolToPtr(true), 1267 Addr: "https://localhost/", 1268 TLSServerName: "notlocalhost", 1269 } 1270 config := &TaskTemplateManagerConfig{ 1271 ClientConfig: c, 1272 VaultToken: "token", 1273 } 1274 ctconf, err := newRunnerConfig(config, nil) 1275 if err != nil { 1276 t.Fatalf("unexpected error: %v", err) 1277 } 1278 1279 if *ctconf.Vault.SSL.ServerName != c.VaultConfig.TLSServerName { 1280 t.Fatalf("expected %q but found %q", c.VaultConfig.TLSServerName, *ctconf.Vault.SSL.ServerName) 1281 } 1282 } 1283 1284 // TestTaskTemplateManager_Config_VaultGrace asserts the vault_grace setting is 1285 // propagated to consul-template's configuration. 1286 func TestTaskTemplateManager_Config_VaultGrace(t *testing.T) { 1287 t.Parallel() 1288 assert := assert.New(t) 1289 c := config.DefaultConfig() 1290 c.Node = mock.Node() 1291 c.VaultConfig = &sconfig.VaultConfig{ 1292 Enabled: helper.BoolToPtr(true), 1293 Addr: "https://localhost/", 1294 TLSServerName: "notlocalhost", 1295 } 1296 1297 alloc := mock.Alloc() 1298 config := &TaskTemplateManagerConfig{ 1299 ClientConfig: c, 1300 VaultToken: "token", 1301 1302 // Make a template that will render immediately 1303 Templates: []*structs.Template{ 1304 { 1305 EmbeddedTmpl: "bar", 1306 DestPath: "foo", 1307 ChangeMode: structs.TemplateChangeModeNoop, 1308 VaultGrace: 10 * time.Second, 1309 }, 1310 { 1311 EmbeddedTmpl: "baz", 1312 DestPath: "bam", 1313 ChangeMode: structs.TemplateChangeModeNoop, 1314 VaultGrace: 100 * time.Second, 1315 }, 1316 }, 1317 EnvBuilder: taskenv.NewBuilder(c.Node, alloc, alloc.Job.TaskGroups[0].Tasks[0], c.Region), 1318 } 1319 1320 ctmplMapping, err := parseTemplateConfigs(config) 1321 assert.Nil(err, "Parsing Templates") 1322 1323 ctconf, err := newRunnerConfig(config, ctmplMapping) 1324 assert.Nil(err, "Building Runner Config") 1325 assert.NotNil(ctconf.Vault.Grace, "Vault Grace Pointer") 1326 assert.Equal(10*time.Second, *ctconf.Vault.Grace, "Vault Grace Value") 1327 } 1328 1329 func TestTaskTemplateManager_BlockedEvents(t *testing.T) { 1330 t.Parallel() 1331 require := require.New(t) 1332 1333 // Make a template that will render based on a key in Consul 1334 var embedded string 1335 for i := 0; i < 5; i++ { 1336 embedded += fmt.Sprintf(`{{key "%d"}}`, i) 1337 } 1338 1339 file := "my.tmpl" 1340 template := &structs.Template{ 1341 EmbeddedTmpl: embedded, 1342 DestPath: file, 1343 ChangeMode: structs.TemplateChangeModeNoop, 1344 } 1345 1346 harness := newTestHarness(t, []*structs.Template{template}, true, false) 1347 harness.setEmitRate(100 * time.Millisecond) 1348 harness.start(t) 1349 defer harness.stop() 1350 1351 // Ensure that we get a blocked event 1352 select { 1353 case <-harness.mockHooks.UnblockCh: 1354 t.Fatalf("Task unblock should have not have been called") 1355 case <-harness.mockHooks.EmitEventCh: 1356 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 1357 t.Fatalf("timeout") 1358 } 1359 1360 // Check to see we got a correct message 1361 require.Len(harness.mockHooks.Events, 1) 1362 require.Contains(harness.mockHooks.Events[0].DisplayMessage, "and 2 more") 1363 1364 // Write 3 keys to Consul 1365 for i := 0; i < 3; i++ { 1366 harness.consul.SetKV(t, fmt.Sprintf("%d", i), []byte{0xa}) 1367 } 1368 1369 // Ensure that we get a blocked event 1370 select { 1371 case <-harness.mockHooks.UnblockCh: 1372 t.Fatalf("Task unblock should have not have been called") 1373 case <-harness.mockHooks.EmitEventCh: 1374 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 1375 t.Fatalf("timeout") 1376 } 1377 1378 // TODO 1379 // Check to see we got a correct message 1380 eventMsg := harness.mockHooks.Events[len(harness.mockHooks.Events)-1].DisplayMessage 1381 if !strings.Contains(eventMsg, "Missing") || strings.Contains(eventMsg, "more") { 1382 t.Fatalf("bad event: %q", eventMsg) 1383 } 1384 }