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