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