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