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