github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/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/allocdir" 21 "github.com/hashicorp/nomad/client/config" 22 "github.com/hashicorp/nomad/client/taskenv" 23 "github.com/hashicorp/nomad/helper" 24 "github.com/hashicorp/nomad/helper/testlog" 25 "github.com/hashicorp/nomad/helper/uuid" 26 "github.com/hashicorp/nomad/nomad/mock" 27 "github.com/hashicorp/nomad/nomad/structs" 28 sconfig "github.com/hashicorp/nomad/nomad/structs/config" 29 "github.com/hashicorp/nomad/testutil" 30 "github.com/kr/pretty" 31 "github.com/stretchr/testify/assert" 32 "github.com/stretchr/testify/require" 33 ) 34 35 const ( 36 // TestTaskName is the name of the injected task. It should appear in the 37 // environment variable $NOMAD_TASK_NAME 38 TestTaskName = "test-task" 39 ) 40 41 // MockTaskHooks is a mock of the TaskHooks interface useful for testing 42 type MockTaskHooks struct { 43 Restarts int 44 RestartCh chan struct{} 45 46 Signals []string 47 SignalCh chan struct{} 48 signalLock sync.Mutex 49 50 // SignalError is returned when Signal is called on the mock hook 51 SignalError error 52 53 UnblockCh chan struct{} 54 55 KillEvent *structs.TaskEvent 56 KillCh chan struct{} 57 58 Events []*structs.TaskEvent 59 EmitEventCh chan *structs.TaskEvent 60 61 // hasHandle can be set to simulate restoring a task after client restart 62 hasHandle bool 63 } 64 65 func NewMockTaskHooks() *MockTaskHooks { 66 return &MockTaskHooks{ 67 UnblockCh: make(chan struct{}, 1), 68 RestartCh: make(chan struct{}, 1), 69 SignalCh: make(chan struct{}, 1), 70 KillCh: make(chan struct{}, 1), 71 EmitEventCh: make(chan *structs.TaskEvent, 1), 72 } 73 } 74 func (m *MockTaskHooks) Restart(ctx context.Context, event *structs.TaskEvent, failure bool) error { 75 m.Restarts++ 76 select { 77 case m.RestartCh <- struct{}{}: 78 default: 79 } 80 return nil 81 } 82 83 func (m *MockTaskHooks) Signal(event *structs.TaskEvent, s string) error { 84 m.signalLock.Lock() 85 m.Signals = append(m.Signals, s) 86 m.signalLock.Unlock() 87 select { 88 case m.SignalCh <- struct{}{}: 89 default: 90 } 91 92 return m.SignalError 93 } 94 95 func (m *MockTaskHooks) Kill(ctx context.Context, event *structs.TaskEvent) error { 96 m.KillEvent = event 97 select { 98 case m.KillCh <- struct{}{}: 99 default: 100 } 101 return nil 102 } 103 104 func (m *MockTaskHooks) IsRunning() bool { 105 return m.hasHandle 106 } 107 108 func (m *MockTaskHooks) EmitEvent(event *structs.TaskEvent) { 109 m.Events = append(m.Events, event) 110 select { 111 case m.EmitEventCh <- event: 112 case <-m.EmitEventCh: 113 m.EmitEventCh <- event 114 } 115 } 116 117 func (m *MockTaskHooks) SetState(state string, event *structs.TaskEvent) {} 118 119 // testHarness is used to test the TaskTemplateManager by spinning up 120 // Consul/Vault as needed 121 type testHarness struct { 122 manager *TaskTemplateManager 123 mockHooks *MockTaskHooks 124 templates []*structs.Template 125 envBuilder *taskenv.Builder 126 node *structs.Node 127 config *config.Config 128 vaultToken string 129 taskDir string 130 vault *testutil.TestVault 131 consul *ctestutil.TestServer 132 emitRate time.Duration 133 } 134 135 // newTestHarness returns a harness starting a dev consul and vault server, 136 // building the appropriate config and creating a TaskTemplateManager 137 func newTestHarness(t *testing.T, templates []*structs.Template, consul, vault bool) *testHarness { 138 region := "global" 139 harness := &testHarness{ 140 mockHooks: NewMockTaskHooks(), 141 templates: templates, 142 node: mock.Node(), 143 config: &config.Config{ 144 Region: region, 145 TemplateConfig: &config.ClientTemplateConfig{ 146 FunctionDenylist: []string{"plugin"}, 147 DisableSandbox: false, 148 }}, 149 emitRate: DefaultMaxTemplateEventRate, 150 } 151 152 // Build the task environment 153 a := mock.Alloc() 154 task := a.Job.TaskGroups[0].Tasks[0] 155 task.Name = TestTaskName 156 harness.envBuilder = taskenv.NewBuilder(harness.node, a, task, region) 157 158 // Make a tempdir 159 d, err := ioutil.TempDir("", "ct_test") 160 if err != nil { 161 t.Fatalf("Failed to make tmpdir: %v", err) 162 } 163 harness.taskDir = d 164 harness.envBuilder.SetClientTaskRoot(harness.taskDir) 165 166 if consul { 167 harness.consul, err = ctestutil.NewTestServerConfigT(t, func(c *ctestutil.TestServerConfig) { 168 // defaults 169 }) 170 if err != nil { 171 t.Fatalf("error starting test Consul server: %v", err) 172 } 173 harness.config.ConsulConfig = &sconfig.ConsulConfig{ 174 Addr: harness.consul.HTTPAddr, 175 } 176 } 177 178 if vault { 179 harness.vault = testutil.NewTestVault(t) 180 harness.config.VaultConfig = harness.vault.Config 181 harness.vaultToken = harness.vault.RootToken 182 } 183 184 return harness 185 } 186 187 func (h *testHarness) start(t *testing.T) { 188 if err := h.startWithErr(); err != nil { 189 t.Fatalf("failed to build task template manager: %v", err) 190 } 191 } 192 193 func (h *testHarness) startWithErr() error { 194 var err error 195 h.manager, err = NewTaskTemplateManager(&TaskTemplateManagerConfig{ 196 UnblockCh: h.mockHooks.UnblockCh, 197 Lifecycle: h.mockHooks, 198 Events: h.mockHooks, 199 Templates: h.templates, 200 ClientConfig: h.config, 201 VaultToken: h.vaultToken, 202 TaskDir: h.taskDir, 203 EnvBuilder: h.envBuilder, 204 MaxTemplateEventRate: h.emitRate, 205 retryRate: 10 * time.Millisecond, 206 }) 207 208 return err 209 } 210 211 func (h *testHarness) setEmitRate(d time.Duration) { 212 h.emitRate = d 213 } 214 215 // stop is used to stop any running Vault or Consul server plus the task manager 216 func (h *testHarness) stop() { 217 if h.vault != nil { 218 h.vault.Stop() 219 } 220 if h.consul != nil { 221 h.consul.Stop() 222 } 223 if h.manager != nil { 224 h.manager.Stop() 225 } 226 if h.taskDir != "" { 227 os.RemoveAll(h.taskDir) 228 } 229 } 230 231 func TestTaskTemplateManager_InvalidConfig(t *testing.T) { 232 t.Parallel() 233 hooks := NewMockTaskHooks() 234 clientConfig := &config.Config{Region: "global"} 235 taskDir := "foo" 236 a := mock.Alloc() 237 envBuilder := taskenv.NewBuilder(mock.Node(), a, a.Job.TaskGroups[0].Tasks[0], clientConfig.Region) 238 239 cases := []struct { 240 name string 241 config *TaskTemplateManagerConfig 242 expectedErr string 243 }{ 244 { 245 name: "nil config", 246 config: nil, 247 expectedErr: "Nil config passed", 248 }, 249 { 250 name: "bad lifecycle hooks", 251 config: &TaskTemplateManagerConfig{ 252 UnblockCh: hooks.UnblockCh, 253 Events: hooks, 254 ClientConfig: clientConfig, 255 TaskDir: taskDir, 256 EnvBuilder: envBuilder, 257 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 258 }, 259 expectedErr: "lifecycle hooks", 260 }, 261 { 262 name: "bad event hooks", 263 config: &TaskTemplateManagerConfig{ 264 UnblockCh: hooks.UnblockCh, 265 Lifecycle: hooks, 266 ClientConfig: clientConfig, 267 TaskDir: taskDir, 268 EnvBuilder: envBuilder, 269 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 270 }, 271 expectedErr: "event hook", 272 }, 273 { 274 name: "bad client config", 275 config: &TaskTemplateManagerConfig{ 276 UnblockCh: hooks.UnblockCh, 277 Lifecycle: hooks, 278 Events: hooks, 279 TaskDir: taskDir, 280 EnvBuilder: envBuilder, 281 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 282 }, 283 expectedErr: "client config", 284 }, 285 { 286 name: "bad task dir", 287 config: &TaskTemplateManagerConfig{ 288 UnblockCh: hooks.UnblockCh, 289 ClientConfig: clientConfig, 290 Lifecycle: hooks, 291 Events: hooks, 292 EnvBuilder: envBuilder, 293 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 294 }, 295 expectedErr: "task directory", 296 }, 297 { 298 name: "bad env builder", 299 config: &TaskTemplateManagerConfig{ 300 UnblockCh: hooks.UnblockCh, 301 ClientConfig: clientConfig, 302 Lifecycle: hooks, 303 Events: hooks, 304 TaskDir: taskDir, 305 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 306 }, 307 expectedErr: "task environment", 308 }, 309 { 310 name: "bad max event rate", 311 config: &TaskTemplateManagerConfig{ 312 UnblockCh: hooks.UnblockCh, 313 ClientConfig: clientConfig, 314 Lifecycle: hooks, 315 Events: hooks, 316 TaskDir: taskDir, 317 EnvBuilder: envBuilder, 318 }, 319 expectedErr: "template event rate", 320 }, 321 { 322 name: "valid", 323 config: &TaskTemplateManagerConfig{ 324 UnblockCh: hooks.UnblockCh, 325 ClientConfig: clientConfig, 326 Lifecycle: hooks, 327 Events: hooks, 328 TaskDir: taskDir, 329 EnvBuilder: envBuilder, 330 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 331 }, 332 }, 333 { 334 name: "invalid signal", 335 config: &TaskTemplateManagerConfig{ 336 UnblockCh: hooks.UnblockCh, 337 Templates: []*structs.Template{ 338 { 339 DestPath: "foo", 340 EmbeddedTmpl: "hello, world", 341 ChangeMode: structs.TemplateChangeModeSignal, 342 ChangeSignal: "foobarbaz", 343 }, 344 }, 345 ClientConfig: clientConfig, 346 Lifecycle: hooks, 347 Events: hooks, 348 TaskDir: taskDir, 349 EnvBuilder: envBuilder, 350 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 351 }, 352 expectedErr: "parse signal", 353 }, 354 } 355 356 for _, c := range cases { 357 t.Run(c.name, func(t *testing.T) { 358 _, err := NewTaskTemplateManager(c.config) 359 if err != nil { 360 if c.expectedErr == "" { 361 t.Fatalf("unexpected error: %v", err) 362 } else if !strings.Contains(err.Error(), c.expectedErr) { 363 t.Fatalf("expected error to contain %q; got %q", c.expectedErr, err.Error()) 364 } 365 } else if c.expectedErr != "" { 366 t.Fatalf("expected an error to contain %q", c.expectedErr) 367 } 368 }) 369 } 370 } 371 372 func TestTaskTemplateManager_HostPath(t *testing.T) { 373 t.Parallel() 374 // Make a template that will render immediately and write it to a tmp file 375 f, err := ioutil.TempFile("", "") 376 if err != nil { 377 t.Fatalf("Bad: %v", err) 378 } 379 defer f.Close() 380 defer os.Remove(f.Name()) 381 382 content := "hello, world!" 383 if _, err := io.WriteString(f, content); err != nil { 384 t.Fatalf("Bad: %v", err) 385 } 386 387 file := "my.tmpl" 388 template := &structs.Template{ 389 SourcePath: f.Name(), 390 DestPath: file, 391 ChangeMode: structs.TemplateChangeModeNoop, 392 } 393 394 harness := newTestHarness(t, []*structs.Template{template}, false, false) 395 harness.config.TemplateConfig.DisableSandbox = true 396 err = harness.startWithErr() 397 if err != nil { 398 t.Fatalf("couldn't setup initial harness: %v", err) 399 } 400 defer harness.stop() 401 402 // Wait for the unblock 403 select { 404 case <-harness.mockHooks.UnblockCh: 405 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 406 t.Fatalf("Task unblock should have been called") 407 } 408 409 // Check the file is there 410 path := filepath.Join(harness.taskDir, file) 411 raw, err := ioutil.ReadFile(path) 412 if err != nil { 413 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 414 } 415 416 if s := string(raw); s != content { 417 t.Fatalf("Unexpected template data; got %q, want %q", s, content) 418 } 419 420 // Change the config to disallow host sources 421 harness = newTestHarness(t, []*structs.Template{template}, false, false) 422 err = harness.startWithErr() 423 if err == nil || !strings.Contains(err.Error(), "escapes alloc directory") { 424 t.Fatalf("Expected absolute template path disallowed for %q: %v", 425 template.SourcePath, err) 426 } 427 428 template.SourcePath = "../../../../../../" + file 429 harness = newTestHarness(t, []*structs.Template{template}, false, false) 430 err = harness.startWithErr() 431 if err == nil || !strings.Contains(err.Error(), "escapes alloc directory") { 432 t.Fatalf("Expected directory traversal out of %q disallowed for %q: %v", 433 harness.taskDir, template.SourcePath, err) 434 } 435 436 // Build a new task environment 437 a := mock.Alloc() 438 task := a.Job.TaskGroups[0].Tasks[0] 439 task.Name = TestTaskName 440 task.Meta = map[string]string{"ESCAPE": "../"} 441 442 template.SourcePath = "${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}" + file 443 harness = newTestHarness(t, []*structs.Template{template}, false, false) 444 harness.envBuilder = taskenv.NewBuilder(harness.node, a, task, "global") 445 err = harness.startWithErr() 446 if err == nil || !strings.Contains(err.Error(), "escapes alloc directory") { 447 t.Fatalf("Expected directory traversal out of %q via interpolation disallowed for %q: %v", 448 harness.taskDir, template.SourcePath, err) 449 } 450 451 // Test with desination too 452 template.SourcePath = f.Name() 453 template.DestPath = "../../../../../../" + file 454 harness = newTestHarness(t, []*structs.Template{template}, false, false) 455 harness.envBuilder = taskenv.NewBuilder(harness.node, a, task, "global") 456 err = harness.startWithErr() 457 if err == nil || !strings.Contains(err.Error(), "escapes alloc directory") { 458 t.Fatalf("Expected directory traversal out of %q via interpolation disallowed for %q: %v", 459 harness.taskDir, template.SourcePath, err) 460 } 461 462 } 463 464 func TestTaskTemplateManager_Unblock_Static(t *testing.T) { 465 t.Parallel() 466 // Make a template that will render immediately 467 content := "hello, world!" 468 file := "my.tmpl" 469 template := &structs.Template{ 470 EmbeddedTmpl: content, 471 DestPath: file, 472 ChangeMode: structs.TemplateChangeModeNoop, 473 } 474 475 harness := newTestHarness(t, []*structs.Template{template}, false, false) 476 harness.start(t) 477 defer harness.stop() 478 479 // Wait for the unblock 480 select { 481 case <-harness.mockHooks.UnblockCh: 482 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 483 t.Fatalf("Task unblock should have been called") 484 } 485 486 // Check the file is there 487 path := filepath.Join(harness.taskDir, file) 488 raw, err := ioutil.ReadFile(path) 489 if err != nil { 490 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 491 } 492 493 if s := string(raw); s != content { 494 t.Fatalf("Unexpected template data; got %q, want %q", s, content) 495 } 496 } 497 498 func TestTaskTemplateManager_Permissions(t *testing.T) { 499 t.Parallel() 500 // Make a template that will render immediately 501 content := "hello, world!" 502 file := "my.tmpl" 503 template := &structs.Template{ 504 EmbeddedTmpl: content, 505 DestPath: file, 506 ChangeMode: structs.TemplateChangeModeNoop, 507 Perms: "777", 508 } 509 510 harness := newTestHarness(t, []*structs.Template{template}, false, false) 511 harness.start(t) 512 defer harness.stop() 513 514 // Wait for the unblock 515 select { 516 case <-harness.mockHooks.UnblockCh: 517 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 518 t.Fatalf("Task unblock should have been called") 519 } 520 521 // Check the file is there 522 path := filepath.Join(harness.taskDir, file) 523 fi, err := os.Stat(path) 524 if err != nil { 525 t.Fatalf("Failed to stat file: %v", err) 526 } 527 528 if m := fi.Mode(); m != os.ModePerm { 529 t.Fatalf("Got mode %v; want %v", m, os.ModePerm) 530 } 531 } 532 533 func TestTaskTemplateManager_Unblock_Static_NomadEnv(t *testing.T) { 534 t.Parallel() 535 // Make a template that will render immediately 536 content := `Hello Nomad Task: {{env "NOMAD_TASK_NAME"}}` 537 expected := fmt.Sprintf("Hello Nomad Task: %s", TestTaskName) 538 file := "my.tmpl" 539 template := &structs.Template{ 540 EmbeddedTmpl: content, 541 DestPath: file, 542 ChangeMode: structs.TemplateChangeModeNoop, 543 } 544 545 harness := newTestHarness(t, []*structs.Template{template}, false, false) 546 harness.start(t) 547 defer harness.stop() 548 549 // Wait for the unblock 550 select { 551 case <-harness.mockHooks.UnblockCh: 552 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 553 t.Fatalf("Task unblock should have been called") 554 } 555 556 // Check the file is there 557 path := filepath.Join(harness.taskDir, file) 558 raw, err := ioutil.ReadFile(path) 559 if err != nil { 560 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 561 } 562 563 if s := string(raw); s != expected { 564 t.Fatalf("Unexpected template data; got %q, want %q", s, expected) 565 } 566 } 567 568 func TestTaskTemplateManager_Unblock_Static_AlreadyRendered(t *testing.T) { 569 t.Parallel() 570 // Make a template that will render immediately 571 content := "hello, world!" 572 file := "my.tmpl" 573 template := &structs.Template{ 574 EmbeddedTmpl: content, 575 DestPath: file, 576 ChangeMode: structs.TemplateChangeModeNoop, 577 } 578 579 harness := newTestHarness(t, []*structs.Template{template}, false, false) 580 581 // Write the contents 582 path := filepath.Join(harness.taskDir, file) 583 if err := ioutil.WriteFile(path, []byte(content), 0777); err != nil { 584 t.Fatalf("Failed to write data: %v", err) 585 } 586 587 harness.start(t) 588 defer harness.stop() 589 590 // Wait for the unblock 591 select { 592 case <-harness.mockHooks.UnblockCh: 593 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 594 t.Fatalf("Task unblock should have been called") 595 } 596 597 // Check the file is there 598 path = filepath.Join(harness.taskDir, file) 599 raw, err := ioutil.ReadFile(path) 600 if err != nil { 601 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 602 } 603 604 if s := string(raw); s != content { 605 t.Fatalf("Unexpected template data; got %q, want %q", s, content) 606 } 607 } 608 609 func TestTaskTemplateManager_Unblock_Consul(t *testing.T) { 610 t.Parallel() 611 // Make a template that will render based on a key in Consul 612 key := "foo" 613 content := "barbaz" 614 embedded := fmt.Sprintf(`{{key "%s"}}`, key) 615 file := "my.tmpl" 616 template := &structs.Template{ 617 EmbeddedTmpl: embedded, 618 DestPath: file, 619 ChangeMode: structs.TemplateChangeModeNoop, 620 } 621 622 harness := newTestHarness(t, []*structs.Template{template}, true, false) 623 harness.start(t) 624 defer harness.stop() 625 626 // Ensure no unblock 627 select { 628 case <-harness.mockHooks.UnblockCh: 629 t.Fatalf("Task unblock should have not have been called") 630 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 631 } 632 633 // Write the key to Consul 634 harness.consul.SetKV(t, key, []byte(content)) 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_Vault(t *testing.T) { 656 t.Parallel() 657 require := require.New(t) 658 // Make a template that will render based on a key in Vault 659 vaultPath := "secret/data/password" 660 key := "password" 661 content := "barbaz" 662 embedded := fmt.Sprintf(`{{with secret "%s"}}{{.Data.data.%s}}{{end}}`, vaultPath, key) 663 file := "my.tmpl" 664 template := &structs.Template{ 665 EmbeddedTmpl: embedded, 666 DestPath: file, 667 ChangeMode: structs.TemplateChangeModeNoop, 668 } 669 670 harness := newTestHarness(t, []*structs.Template{template}, false, true) 671 harness.start(t) 672 defer harness.stop() 673 674 // Ensure no unblock 675 select { 676 case <-harness.mockHooks.UnblockCh: 677 t.Fatalf("Task unblock should not have been called") 678 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 679 } 680 681 // Write the secret to Vault 682 logical := harness.vault.Client.Logical() 683 _, err := logical.Write(vaultPath, map[string]interface{}{"data": map[string]interface{}{key: content}}) 684 require.NoError(err) 685 686 // Wait for the unblock 687 select { 688 case <-harness.mockHooks.UnblockCh: 689 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 690 t.Fatalf("Task unblock should have been called") 691 } 692 693 // Check the file is there 694 path := filepath.Join(harness.taskDir, file) 695 raw, err := ioutil.ReadFile(path) 696 if err != nil { 697 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 698 } 699 700 if s := string(raw); s != content { 701 t.Fatalf("Unexpected template data; got %q, want %q", s, content) 702 } 703 } 704 705 func TestTaskTemplateManager_Unblock_Multi_Template(t *testing.T) { 706 t.Parallel() 707 // Make a template that will render immediately 708 staticContent := "hello, world!" 709 staticFile := "my.tmpl" 710 template := &structs.Template{ 711 EmbeddedTmpl: staticContent, 712 DestPath: staticFile, 713 ChangeMode: structs.TemplateChangeModeNoop, 714 } 715 716 // Make a template that will render based on a key in Consul 717 consulKey := "foo" 718 consulContent := "barbaz" 719 consulEmbedded := fmt.Sprintf(`{{key "%s"}}`, consulKey) 720 consulFile := "consul.tmpl" 721 template2 := &structs.Template{ 722 EmbeddedTmpl: consulEmbedded, 723 DestPath: consulFile, 724 ChangeMode: structs.TemplateChangeModeNoop, 725 } 726 727 harness := newTestHarness(t, []*structs.Template{template, template2}, true, false) 728 harness.start(t) 729 defer harness.stop() 730 731 // Ensure no unblock 732 select { 733 case <-harness.mockHooks.UnblockCh: 734 t.Fatalf("Task unblock should have not have been called") 735 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 736 } 737 738 // Check that the static file has been rendered 739 path := filepath.Join(harness.taskDir, staticFile) 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 != staticContent { 746 t.Fatalf("Unexpected template data; got %q, want %q", s, staticContent) 747 } 748 749 // Write the key to Consul 750 harness.consul.SetKV(t, consulKey, []byte(consulContent)) 751 752 // Wait for the unblock 753 select { 754 case <-harness.mockHooks.UnblockCh: 755 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 756 t.Fatalf("Task unblock should have been called") 757 } 758 759 // Check the consul file is there 760 path = filepath.Join(harness.taskDir, consulFile) 761 raw, err = ioutil.ReadFile(path) 762 if err != nil { 763 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 764 } 765 766 if s := string(raw); s != consulContent { 767 t.Fatalf("Unexpected template data; got %q, want %q", s, consulContent) 768 } 769 } 770 771 // TestTaskTemplateManager_FirstRender_Restored tests that a task that's been 772 // restored renders and triggers its change mode if the template has changed 773 func TestTaskTemplateManager_FirstRender_Restored(t *testing.T) { 774 t.Parallel() 775 require := require.New(t) 776 // Make a template that will render based on a key in Vault 777 vaultPath := "secret/data/password" 778 key := "password" 779 content := "barbaz" 780 embedded := fmt.Sprintf(`{{with secret "%s"}}{{.Data.data.%s}}{{end}}`, vaultPath, key) 781 file := "my.tmpl" 782 template := &structs.Template{ 783 EmbeddedTmpl: embedded, 784 DestPath: file, 785 ChangeMode: structs.TemplateChangeModeRestart, 786 } 787 788 harness := newTestHarness(t, []*structs.Template{template}, false, true) 789 harness.start(t) 790 defer harness.stop() 791 792 // Ensure no unblock 793 select { 794 case <-harness.mockHooks.UnblockCh: 795 require.Fail("Task unblock should not have been called") 796 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 797 } 798 799 // Write the secret to Vault 800 logical := harness.vault.Client.Logical() 801 _, err := logical.Write(vaultPath, map[string]interface{}{"data": map[string]interface{}{key: content}}) 802 require.NoError(err) 803 804 // Wait for the unblock 805 select { 806 case <-harness.mockHooks.UnblockCh: 807 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 808 require.Fail("Task unblock should have been called") 809 } 810 811 // Check the file is there 812 path := filepath.Join(harness.taskDir, file) 813 raw, err := ioutil.ReadFile(path) 814 require.NoError(err, "Failed to read rendered template from %q", path) 815 require.Equal(content, string(raw), "Unexpected template data; got %s, want %q", raw, content) 816 817 // task is now running 818 harness.mockHooks.hasHandle = true 819 820 // simulate a client restart 821 harness.manager.Stop() 822 harness.mockHooks.UnblockCh = make(chan struct{}, 1) 823 harness.start(t) 824 825 // Wait for the unblock 826 select { 827 case <-harness.mockHooks.UnblockCh: 828 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 829 require.Fail("Task unblock should have been called") 830 } 831 832 select { 833 case <-harness.mockHooks.RestartCh: 834 require.Fail("should not have restarted", harness.mockHooks) 835 case <-harness.mockHooks.SignalCh: 836 require.Fail("should not have restarted", harness.mockHooks) 837 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 838 } 839 840 // simulate a client restart and TTL expiry 841 harness.manager.Stop() 842 content = "bazbar" 843 _, err = logical.Write(vaultPath, map[string]interface{}{"data": map[string]interface{}{key: content}}) 844 require.NoError(err) 845 harness.mockHooks.UnblockCh = make(chan struct{}, 1) 846 harness.start(t) 847 848 // Wait for the unblock 849 select { 850 case <-harness.mockHooks.UnblockCh: 851 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 852 require.Fail("Task unblock should have been called") 853 } 854 855 // Wait for restart 856 timeout := time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second) 857 OUTER: 858 for { 859 select { 860 case <-harness.mockHooks.RestartCh: 861 break OUTER 862 case <-harness.mockHooks.SignalCh: 863 require.Fail("Signal with restart policy", harness.mockHooks) 864 case <-timeout: 865 require.Fail("Should have received a restart", harness.mockHooks) 866 } 867 } 868 } 869 870 func TestTaskTemplateManager_Rerender_Noop(t *testing.T) { 871 t.Parallel() 872 // Make a template that will render based on a key in Consul 873 key := "foo" 874 content1 := "bar" 875 content2 := "baz" 876 embedded := fmt.Sprintf(`{{key "%s"}}`, key) 877 file := "my.tmpl" 878 template := &structs.Template{ 879 EmbeddedTmpl: embedded, 880 DestPath: file, 881 ChangeMode: structs.TemplateChangeModeNoop, 882 } 883 884 harness := newTestHarness(t, []*structs.Template{template}, true, false) 885 harness.start(t) 886 defer harness.stop() 887 888 // Ensure no unblock 889 select { 890 case <-harness.mockHooks.UnblockCh: 891 t.Fatalf("Task unblock should have not have been called") 892 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 893 } 894 895 // Write the key to Consul 896 harness.consul.SetKV(t, key, []byte(content1)) 897 898 // Wait for the unblock 899 select { 900 case <-harness.mockHooks.UnblockCh: 901 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 902 t.Fatalf("Task unblock should have been called") 903 } 904 905 // Check the file is there 906 path := filepath.Join(harness.taskDir, file) 907 raw, err := ioutil.ReadFile(path) 908 if err != nil { 909 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 910 } 911 912 if s := string(raw); s != content1 { 913 t.Fatalf("Unexpected template data; got %q, want %q", s, content1) 914 } 915 916 // Update the key in Consul 917 harness.consul.SetKV(t, key, []byte(content2)) 918 919 select { 920 case <-harness.mockHooks.RestartCh: 921 t.Fatalf("Noop ignored: %+v", harness.mockHooks) 922 case <-harness.mockHooks.SignalCh: 923 t.Fatalf("Noop ignored: %+v", harness.mockHooks) 924 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 925 } 926 927 // Check the file has been updated 928 path = filepath.Join(harness.taskDir, file) 929 raw, err = ioutil.ReadFile(path) 930 if err != nil { 931 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 932 } 933 934 if s := string(raw); s != content2 { 935 t.Fatalf("Unexpected template data; got %q, want %q", s, content2) 936 } 937 } 938 939 func TestTaskTemplateManager_Rerender_Signal(t *testing.T) { 940 t.Parallel() 941 // Make a template that renders based on a key in Consul and sends SIGALRM 942 key1 := "foo" 943 content1_1 := "bar" 944 content1_2 := "baz" 945 embedded1 := fmt.Sprintf(`{{key "%s"}}`, key1) 946 file1 := "my.tmpl" 947 template := &structs.Template{ 948 EmbeddedTmpl: embedded1, 949 DestPath: file1, 950 ChangeMode: structs.TemplateChangeModeSignal, 951 ChangeSignal: "SIGALRM", 952 } 953 954 // Make a template that renders based on a key in Consul and sends SIGBUS 955 key2 := "bam" 956 content2_1 := "cat" 957 content2_2 := "dog" 958 embedded2 := fmt.Sprintf(`{{key "%s"}}`, key2) 959 file2 := "my-second.tmpl" 960 template2 := &structs.Template{ 961 EmbeddedTmpl: embedded2, 962 DestPath: file2, 963 ChangeMode: structs.TemplateChangeModeSignal, 964 ChangeSignal: "SIGBUS", 965 } 966 967 harness := newTestHarness(t, []*structs.Template{template, template2}, true, false) 968 harness.start(t) 969 defer harness.stop() 970 971 // Ensure no unblock 972 select { 973 case <-harness.mockHooks.UnblockCh: 974 t.Fatalf("Task unblock should have not have been called") 975 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 976 } 977 978 // Write the key to Consul 979 harness.consul.SetKV(t, key1, []byte(content1_1)) 980 harness.consul.SetKV(t, key2, []byte(content2_1)) 981 982 // Wait for the unblock 983 select { 984 case <-harness.mockHooks.UnblockCh: 985 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 986 t.Fatalf("Task unblock should have been called") 987 } 988 989 if len(harness.mockHooks.Signals) != 0 { 990 t.Fatalf("Should not have received any signals: %+v", harness.mockHooks) 991 } 992 993 // Update the keys in Consul 994 harness.consul.SetKV(t, key1, []byte(content1_2)) 995 harness.consul.SetKV(t, key2, []byte(content2_2)) 996 997 // Wait for signals 998 timeout := time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second) 999 OUTER: 1000 for { 1001 select { 1002 case <-harness.mockHooks.RestartCh: 1003 t.Fatalf("Restart with signal policy: %+v", harness.mockHooks) 1004 case <-harness.mockHooks.SignalCh: 1005 harness.mockHooks.signalLock.Lock() 1006 s := harness.mockHooks.Signals 1007 harness.mockHooks.signalLock.Unlock() 1008 if len(s) != 2 { 1009 continue 1010 } 1011 break OUTER 1012 case <-timeout: 1013 t.Fatalf("Should have received two signals: %+v", harness.mockHooks) 1014 } 1015 } 1016 1017 // Check the files have been updated 1018 path := filepath.Join(harness.taskDir, file1) 1019 raw, err := ioutil.ReadFile(path) 1020 if err != nil { 1021 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 1022 } 1023 1024 if s := string(raw); s != content1_2 { 1025 t.Fatalf("Unexpected template data; got %q, want %q", s, content1_2) 1026 } 1027 1028 path = filepath.Join(harness.taskDir, file2) 1029 raw, err = ioutil.ReadFile(path) 1030 if err != nil { 1031 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 1032 } 1033 1034 if s := string(raw); s != content2_2 { 1035 t.Fatalf("Unexpected template data; got %q, want %q", s, content2_2) 1036 } 1037 } 1038 1039 func TestTaskTemplateManager_Rerender_Restart(t *testing.T) { 1040 t.Parallel() 1041 // Make a template that renders based on a key in Consul and sends restart 1042 key1 := "bam" 1043 content1_1 := "cat" 1044 content1_2 := "dog" 1045 embedded1 := fmt.Sprintf(`{{key "%s"}}`, key1) 1046 file1 := "my.tmpl" 1047 template := &structs.Template{ 1048 EmbeddedTmpl: embedded1, 1049 DestPath: file1, 1050 ChangeMode: structs.TemplateChangeModeRestart, 1051 } 1052 1053 harness := newTestHarness(t, []*structs.Template{template}, true, false) 1054 harness.start(t) 1055 defer harness.stop() 1056 1057 // Ensure no unblock 1058 select { 1059 case <-harness.mockHooks.UnblockCh: 1060 t.Fatalf("Task unblock should have not have been called") 1061 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 1062 } 1063 1064 // Write the key to Consul 1065 harness.consul.SetKV(t, key1, []byte(content1_1)) 1066 1067 // Wait for the unblock 1068 select { 1069 case <-harness.mockHooks.UnblockCh: 1070 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 1071 t.Fatalf("Task unblock should have been called") 1072 } 1073 1074 // Update the keys in Consul 1075 harness.consul.SetKV(t, key1, []byte(content1_2)) 1076 1077 // Wait for restart 1078 timeout := time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second) 1079 OUTER: 1080 for { 1081 select { 1082 case <-harness.mockHooks.RestartCh: 1083 break OUTER 1084 case <-harness.mockHooks.SignalCh: 1085 t.Fatalf("Signal with restart policy: %+v", harness.mockHooks) 1086 case <-timeout: 1087 t.Fatalf("Should have received a restart: %+v", harness.mockHooks) 1088 } 1089 } 1090 1091 // Check the files have been updated 1092 path := filepath.Join(harness.taskDir, file1) 1093 raw, err := ioutil.ReadFile(path) 1094 if err != nil { 1095 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 1096 } 1097 1098 if s := string(raw); s != content1_2 { 1099 t.Fatalf("Unexpected template data; got %q, want %q", s, content1_2) 1100 } 1101 } 1102 1103 func TestTaskTemplateManager_Interpolate_Destination(t *testing.T) { 1104 t.Parallel() 1105 // Make a template that will have its destination interpolated 1106 content := "hello, world!" 1107 file := "${node.unique.id}.tmpl" 1108 template := &structs.Template{ 1109 EmbeddedTmpl: content, 1110 DestPath: file, 1111 ChangeMode: structs.TemplateChangeModeNoop, 1112 } 1113 1114 harness := newTestHarness(t, []*structs.Template{template}, false, false) 1115 harness.start(t) 1116 defer harness.stop() 1117 1118 // Ensure unblock 1119 select { 1120 case <-harness.mockHooks.UnblockCh: 1121 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 1122 t.Fatalf("Task unblock should have been called") 1123 } 1124 1125 // Check the file is there 1126 actual := fmt.Sprintf("%s.tmpl", harness.node.ID) 1127 path := filepath.Join(harness.taskDir, actual) 1128 raw, err := ioutil.ReadFile(path) 1129 if err != nil { 1130 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 1131 } 1132 1133 if s := string(raw); s != content { 1134 t.Fatalf("Unexpected template data; got %q, want %q", s, content) 1135 } 1136 } 1137 1138 func TestTaskTemplateManager_Signal_Error(t *testing.T) { 1139 t.Parallel() 1140 require := require.New(t) 1141 1142 // Make a template that renders based on a key in Consul and sends SIGALRM 1143 key1 := "foo" 1144 content1 := "bar" 1145 content2 := "baz" 1146 embedded1 := fmt.Sprintf(`{{key "%s"}}`, key1) 1147 file1 := "my.tmpl" 1148 template := &structs.Template{ 1149 EmbeddedTmpl: embedded1, 1150 DestPath: file1, 1151 ChangeMode: structs.TemplateChangeModeSignal, 1152 ChangeSignal: "SIGALRM", 1153 } 1154 1155 harness := newTestHarness(t, []*structs.Template{template}, true, false) 1156 harness.start(t) 1157 defer harness.stop() 1158 1159 harness.mockHooks.SignalError = fmt.Errorf("test error") 1160 1161 // Write the key to Consul 1162 harness.consul.SetKV(t, key1, []byte(content1)) 1163 1164 // Wait a little 1165 select { 1166 case <-harness.mockHooks.UnblockCh: 1167 case <-time.After(time.Duration(2*testutil.TestMultiplier()) * time.Second): 1168 t.Fatalf("Should have received unblock: %+v", harness.mockHooks) 1169 } 1170 1171 // Write the key to Consul 1172 harness.consul.SetKV(t, key1, []byte(content2)) 1173 1174 // Wait for kill channel 1175 select { 1176 case <-harness.mockHooks.KillCh: 1177 break 1178 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 1179 t.Fatalf("Should have received a signals: %+v", harness.mockHooks) 1180 } 1181 1182 require.NotNil(harness.mockHooks.KillEvent) 1183 require.Contains(harness.mockHooks.KillEvent.DisplayMessage, "failed to send signals") 1184 } 1185 1186 // TestTaskTemplateManager_FiltersProcessEnvVars asserts that we only render 1187 // environment variables found in task env-vars and not read the nomad host 1188 // process environment variables. nomad host process environment variables 1189 // are to be treated the same as not found environment variables. 1190 func TestTaskTemplateManager_FiltersEnvVars(t *testing.T) { 1191 t.Parallel() 1192 1193 defer os.Setenv("NOMAD_TASK_NAME", os.Getenv("NOMAD_TASK_NAME")) 1194 os.Setenv("NOMAD_TASK_NAME", "should be overridden by task") 1195 1196 testenv := "TESTENV_" + strings.ReplaceAll(uuid.Generate(), "-", "") 1197 os.Setenv(testenv, "MY_TEST_VALUE") 1198 defer os.Unsetenv(testenv) 1199 1200 // Make a template that will render immediately 1201 content := `Hello Nomad Task: {{env "NOMAD_TASK_NAME"}} 1202 TEST_ENV: {{ env "` + testenv + `" }} 1203 TEST_ENV_NOT_FOUND: {{env "` + testenv + `_NOTFOUND" }}` 1204 expected := fmt.Sprintf("Hello Nomad Task: %s\nTEST_ENV: \nTEST_ENV_NOT_FOUND: ", TestTaskName) 1205 1206 file := "my.tmpl" 1207 template := &structs.Template{ 1208 EmbeddedTmpl: content, 1209 DestPath: file, 1210 ChangeMode: structs.TemplateChangeModeNoop, 1211 } 1212 1213 harness := newTestHarness(t, []*structs.Template{template}, false, false) 1214 harness.start(t) 1215 defer harness.stop() 1216 1217 // Wait for the unblock 1218 select { 1219 case <-harness.mockHooks.UnblockCh: 1220 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 1221 require.Fail(t, "Task unblock should have been called") 1222 } 1223 1224 // Check the file is there 1225 path := filepath.Join(harness.taskDir, file) 1226 raw, err := ioutil.ReadFile(path) 1227 require.NoError(t, err) 1228 1229 require.Equal(t, expected, string(raw)) 1230 } 1231 1232 // TestTaskTemplateManager_Env asserts templates with the env flag set are read 1233 // into the task's environment. 1234 func TestTaskTemplateManager_Env(t *testing.T) { 1235 t.Parallel() 1236 template := &structs.Template{ 1237 EmbeddedTmpl: ` 1238 # Comment lines are ok 1239 1240 FOO=bar 1241 foo=123 1242 ANYTHING_goes=Spaces are=ok! 1243 `, 1244 DestPath: "test.env", 1245 ChangeMode: structs.TemplateChangeModeNoop, 1246 Envvars: true, 1247 } 1248 harness := newTestHarness(t, []*structs.Template{template}, true, false) 1249 harness.start(t) 1250 defer harness.stop() 1251 1252 // Wait a little 1253 select { 1254 case <-harness.mockHooks.UnblockCh: 1255 case <-time.After(time.Duration(2*testutil.TestMultiplier()) * time.Second): 1256 t.Fatalf("Should have received unblock: %+v", harness.mockHooks) 1257 } 1258 1259 // Validate environment 1260 env := harness.envBuilder.Build().Map() 1261 if len(env) < 3 { 1262 t.Fatalf("expected at least 3 env vars but found %d:\n%#v\n", len(env), env) 1263 } 1264 if env["FOO"] != "bar" { 1265 t.Errorf("expected FOO=bar but found %q", env["FOO"]) 1266 } 1267 if env["foo"] != "123" { 1268 t.Errorf("expected foo=123 but found %q", env["foo"]) 1269 } 1270 if env["ANYTHING_goes"] != "Spaces are=ok!" { 1271 t.Errorf("expected ANYTHING_GOES='Spaces are ok!' but found %q", env["ANYTHING_goes"]) 1272 } 1273 } 1274 1275 // TestTaskTemplateManager_Env_Missing asserts the core env 1276 // template processing function returns errors when files don't exist 1277 func TestTaskTemplateManager_Env_Missing(t *testing.T) { 1278 t.Parallel() 1279 d, err := ioutil.TempDir("", "ct_env_missing") 1280 if err != nil { 1281 t.Fatalf("err: %v", err) 1282 } 1283 defer os.RemoveAll(d) 1284 1285 // Fake writing the file so we don't have to run the whole template manager 1286 err = ioutil.WriteFile(filepath.Join(d, "exists.env"), []byte("FOO=bar\n"), 0644) 1287 if err != nil { 1288 t.Fatalf("error writing template file: %v", err) 1289 } 1290 1291 templates := []*structs.Template{ 1292 { 1293 EmbeddedTmpl: "FOO=bar\n", 1294 DestPath: "exists.env", 1295 Envvars: true, 1296 }, 1297 { 1298 EmbeddedTmpl: "WHAT=ever\n", 1299 DestPath: "missing.env", 1300 Envvars: true, 1301 }, 1302 } 1303 1304 taskEnv := taskenv.NewEmptyBuilder().SetClientTaskRoot(d).Build() 1305 if vars, err := loadTemplateEnv(templates, taskEnv); err == nil { 1306 t.Fatalf("expected an error but instead got env vars: %#v", vars) 1307 } 1308 } 1309 1310 // TestTaskTemplateManager_Env_InterpolatedDest asserts the core env 1311 // template processing function handles interpolated destinations 1312 func TestTaskTemplateManager_Env_InterpolatedDest(t *testing.T) { 1313 t.Parallel() 1314 require := require.New(t) 1315 1316 d, err := ioutil.TempDir("", "ct_env_interpolated") 1317 if err != nil { 1318 t.Fatalf("err: %v", err) 1319 } 1320 defer os.RemoveAll(d) 1321 1322 // Fake writing the file so we don't have to run the whole template manager 1323 err = ioutil.WriteFile(filepath.Join(d, "exists.env"), []byte("FOO=bar\n"), 0644) 1324 if err != nil { 1325 t.Fatalf("error writing template file: %v", err) 1326 } 1327 1328 templates := []*structs.Template{ 1329 { 1330 EmbeddedTmpl: "FOO=bar\n", 1331 DestPath: "${NOMAD_META_path}.env", 1332 Envvars: true, 1333 }, 1334 } 1335 1336 // Build the env 1337 taskEnv := taskenv.NewTaskEnv( 1338 map[string]string{"NOMAD_META_path": "exists"}, 1339 map[string]string{"NOMAD_META_path": "exists"}, 1340 map[string]string{}, 1341 map[string]string{}, 1342 d, "") 1343 1344 vars, err := loadTemplateEnv(templates, taskEnv) 1345 require.NoError(err) 1346 require.Contains(vars, "FOO") 1347 require.Equal(vars["FOO"], "bar") 1348 } 1349 1350 // TestTaskTemplateManager_Env_Multi asserts the core env 1351 // template processing function returns combined env vars from multiple 1352 // templates correctly. 1353 func TestTaskTemplateManager_Env_Multi(t *testing.T) { 1354 t.Parallel() 1355 d, err := ioutil.TempDir("", "ct_env_missing") 1356 if err != nil { 1357 t.Fatalf("err: %v", err) 1358 } 1359 defer os.RemoveAll(d) 1360 1361 // Fake writing the files so we don't have to run the whole template manager 1362 err = ioutil.WriteFile(filepath.Join(d, "zzz.env"), []byte("FOO=bar\nSHARED=nope\n"), 0644) 1363 if err != nil { 1364 t.Fatalf("error writing template file 1: %v", err) 1365 } 1366 err = ioutil.WriteFile(filepath.Join(d, "aaa.env"), []byte("BAR=foo\nSHARED=yup\n"), 0644) 1367 if err != nil { 1368 t.Fatalf("error writing template file 2: %v", err) 1369 } 1370 1371 // Templates will get loaded in order (not alpha sorted) 1372 templates := []*structs.Template{ 1373 { 1374 DestPath: "zzz.env", 1375 Envvars: true, 1376 }, 1377 { 1378 DestPath: "aaa.env", 1379 Envvars: true, 1380 }, 1381 } 1382 1383 taskEnv := taskenv.NewEmptyBuilder().SetClientTaskRoot(d).Build() 1384 vars, err := loadTemplateEnv(templates, taskEnv) 1385 if err != nil { 1386 t.Fatalf("expected no error: %v", err) 1387 } 1388 if vars["FOO"] != "bar" { 1389 t.Errorf("expected FOO=bar but found %q", vars["FOO"]) 1390 } 1391 if vars["BAR"] != "foo" { 1392 t.Errorf("expected BAR=foo but found %q", vars["BAR"]) 1393 } 1394 if vars["SHARED"] != "yup" { 1395 t.Errorf("expected FOO=bar but found %q", vars["yup"]) 1396 } 1397 } 1398 1399 func TestTaskTemplateManager_Rerender_Env(t *testing.T) { 1400 t.Parallel() 1401 // Make a template that renders based on a key in Consul and sends restart 1402 key1 := "bam" 1403 key2 := "bar" 1404 content1_1 := "cat" 1405 content1_2 := "dog" 1406 t1 := &structs.Template{ 1407 EmbeddedTmpl: ` 1408 FOO={{key "bam"}} 1409 `, 1410 DestPath: "test.env", 1411 ChangeMode: structs.TemplateChangeModeRestart, 1412 Envvars: true, 1413 } 1414 t2 := &structs.Template{ 1415 EmbeddedTmpl: ` 1416 BAR={{key "bar"}} 1417 `, 1418 DestPath: "test2.env", 1419 ChangeMode: structs.TemplateChangeModeRestart, 1420 Envvars: true, 1421 } 1422 1423 harness := newTestHarness(t, []*structs.Template{t1, t2}, true, false) 1424 harness.start(t) 1425 defer harness.stop() 1426 1427 // Ensure no unblock 1428 select { 1429 case <-harness.mockHooks.UnblockCh: 1430 t.Fatalf("Task unblock should have not have been called") 1431 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 1432 } 1433 1434 // Write the key to Consul 1435 harness.consul.SetKV(t, key1, []byte(content1_1)) 1436 harness.consul.SetKV(t, key2, []byte(content1_1)) 1437 1438 // Wait for the unblock 1439 select { 1440 case <-harness.mockHooks.UnblockCh: 1441 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 1442 t.Fatalf("Task unblock should have been called") 1443 } 1444 1445 env := harness.envBuilder.Build().Map() 1446 if v, ok := env["FOO"]; !ok || v != content1_1 { 1447 t.Fatalf("Bad env for FOO: %v %v", v, ok) 1448 } 1449 if v, ok := env["BAR"]; !ok || v != content1_1 { 1450 t.Fatalf("Bad env for BAR: %v %v", v, ok) 1451 } 1452 1453 // Update the keys in Consul 1454 harness.consul.SetKV(t, key1, []byte(content1_2)) 1455 1456 // Wait for restart 1457 timeout := time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second) 1458 OUTER: 1459 for { 1460 select { 1461 case <-harness.mockHooks.RestartCh: 1462 break OUTER 1463 case <-harness.mockHooks.SignalCh: 1464 t.Fatalf("Signal with restart policy: %+v", harness.mockHooks) 1465 case <-timeout: 1466 t.Fatalf("Should have received a restart: %+v", harness.mockHooks) 1467 } 1468 } 1469 1470 env = harness.envBuilder.Build().Map() 1471 if v, ok := env["FOO"]; !ok || v != content1_2 { 1472 t.Fatalf("Bad env for FOO: %v %v", v, ok) 1473 } 1474 if v, ok := env["BAR"]; !ok || v != content1_1 { 1475 t.Fatalf("Bad env for BAR: %v %v", v, ok) 1476 } 1477 } 1478 1479 // TestTaskTemplateManager_Config_ServerName asserts the tls_server_name 1480 // setting is propagated to consul-template's configuration. See #2776 1481 func TestTaskTemplateManager_Config_ServerName(t *testing.T) { 1482 t.Parallel() 1483 c := config.DefaultConfig() 1484 c.VaultConfig = &sconfig.VaultConfig{ 1485 Enabled: helper.BoolToPtr(true), 1486 Addr: "https://localhost/", 1487 TLSServerName: "notlocalhost", 1488 } 1489 config := &TaskTemplateManagerConfig{ 1490 ClientConfig: c, 1491 VaultToken: "token", 1492 } 1493 ctconf, err := newRunnerConfig(config, nil) 1494 if err != nil { 1495 t.Fatalf("unexpected error: %v", err) 1496 } 1497 1498 if *ctconf.Vault.SSL.ServerName != c.VaultConfig.TLSServerName { 1499 t.Fatalf("expected %q but found %q", c.VaultConfig.TLSServerName, *ctconf.Vault.SSL.ServerName) 1500 } 1501 } 1502 1503 // TestTaskTemplateManager_Config_VaultNamespace asserts the Vault namespace setting is 1504 // propagated to consul-template's configuration. 1505 func TestTaskTemplateManager_Config_VaultNamespace(t *testing.T) { 1506 t.Parallel() 1507 assert := assert.New(t) 1508 1509 testNS := "test-namespace" 1510 c := config.DefaultConfig() 1511 c.Node = mock.Node() 1512 c.VaultConfig = &sconfig.VaultConfig{ 1513 Enabled: helper.BoolToPtr(true), 1514 Addr: "https://localhost/", 1515 TLSServerName: "notlocalhost", 1516 Namespace: testNS, 1517 } 1518 1519 alloc := mock.Alloc() 1520 config := &TaskTemplateManagerConfig{ 1521 ClientConfig: c, 1522 VaultToken: "token", 1523 EnvBuilder: taskenv.NewBuilder(c.Node, alloc, alloc.Job.TaskGroups[0].Tasks[0], c.Region), 1524 } 1525 1526 ctmplMapping, err := parseTemplateConfigs(config) 1527 assert.Nil(err, "Parsing Templates") 1528 1529 ctconf, err := newRunnerConfig(config, ctmplMapping) 1530 assert.Nil(err, "Building Runner Config") 1531 assert.Equal(testNS, *ctconf.Vault.Namespace, "Vault Namespace Value") 1532 } 1533 1534 // TestTaskTemplateManager_Config_VaultNamespace asserts the Vault namespace setting is 1535 // propagated to consul-template's configuration. 1536 func TestTaskTemplateManager_Config_VaultNamespace_TaskOverride(t *testing.T) { 1537 t.Parallel() 1538 assert := assert.New(t) 1539 1540 testNS := "test-namespace" 1541 c := config.DefaultConfig() 1542 c.Node = mock.Node() 1543 c.VaultConfig = &sconfig.VaultConfig{ 1544 Enabled: helper.BoolToPtr(true), 1545 Addr: "https://localhost/", 1546 TLSServerName: "notlocalhost", 1547 Namespace: testNS, 1548 } 1549 1550 alloc := mock.Alloc() 1551 overriddenNS := "new-namespace" 1552 1553 // Set the template manager config vault namespace 1554 config := &TaskTemplateManagerConfig{ 1555 ClientConfig: c, 1556 VaultToken: "token", 1557 VaultNamespace: overriddenNS, 1558 EnvBuilder: taskenv.NewBuilder(c.Node, alloc, alloc.Job.TaskGroups[0].Tasks[0], c.Region), 1559 } 1560 1561 ctmplMapping, err := parseTemplateConfigs(config) 1562 assert.Nil(err, "Parsing Templates") 1563 1564 ctconf, err := newRunnerConfig(config, ctmplMapping) 1565 assert.Nil(err, "Building Runner Config") 1566 assert.Equal(overriddenNS, *ctconf.Vault.Namespace, "Vault Namespace Value") 1567 } 1568 1569 // TestTaskTemplateManager_Escapes asserts that when sandboxing is enabled 1570 // interpolated paths are not incorrectly treated as escaping the alloc dir. 1571 func TestTaskTemplateManager_Escapes(t *testing.T) { 1572 t.Parallel() 1573 1574 clientConf := config.DefaultConfig() 1575 require.False(t, clientConf.TemplateConfig.DisableSandbox, "expected sandbox to be disabled") 1576 1577 // Set a fake alloc dir to make test output more realistic 1578 clientConf.AllocDir = "/fake/allocdir" 1579 1580 clientConf.Node = mock.Node() 1581 alloc := mock.Alloc() 1582 task := alloc.Job.TaskGroups[0].Tasks[0] 1583 logger := testlog.HCLogger(t) 1584 allocDir := allocdir.NewAllocDir(logger, filepath.Join(clientConf.AllocDir, alloc.ID)) 1585 taskDir := allocDir.NewTaskDir(task.Name) 1586 1587 containerEnv := func() *taskenv.Builder { 1588 // To emulate a Docker or exec tasks we must copy the 1589 // Set{Alloc,Task,Secrets}Dir logic in taskrunner/task_dir_hook.go 1590 b := taskenv.NewBuilder(clientConf.Node, alloc, task, clientConf.Region) 1591 b.SetAllocDir(allocdir.SharedAllocContainerPath) 1592 b.SetTaskLocalDir(allocdir.TaskLocalContainerPath) 1593 b.SetSecretsDir(allocdir.TaskSecretsContainerPath) 1594 b.SetClientTaskRoot(taskDir.Dir) 1595 b.SetClientSharedAllocDir(taskDir.SharedAllocDir) 1596 b.SetClientTaskLocalDir(taskDir.LocalDir) 1597 b.SetClientTaskSecretsDir(taskDir.SecretsDir) 1598 return b 1599 } 1600 1601 rawExecEnv := func() *taskenv.Builder { 1602 // To emulate a unisolated tasks we must copy the 1603 // Set{Alloc,Task,Secrets}Dir logic in taskrunner/task_dir_hook.go 1604 b := taskenv.NewBuilder(clientConf.Node, alloc, task, clientConf.Region) 1605 b.SetAllocDir(taskDir.SharedAllocDir) 1606 b.SetTaskLocalDir(taskDir.LocalDir) 1607 b.SetSecretsDir(taskDir.SecretsDir) 1608 b.SetClientTaskRoot(taskDir.Dir) 1609 b.SetClientSharedAllocDir(taskDir.SharedAllocDir) 1610 b.SetClientTaskLocalDir(taskDir.LocalDir) 1611 b.SetClientTaskSecretsDir(taskDir.SecretsDir) 1612 return b 1613 } 1614 1615 cases := []struct { 1616 Name string 1617 Config func() *TaskTemplateManagerConfig 1618 1619 // Expected paths to be returned if Err is nil 1620 SourcePath string 1621 DestPath string 1622 1623 // Err is the expected error to be returned or nil 1624 Err error 1625 }{ 1626 { 1627 Name: "ContainerOk", 1628 Config: func() *TaskTemplateManagerConfig { 1629 return &TaskTemplateManagerConfig{ 1630 ClientConfig: clientConf, 1631 TaskDir: taskDir.Dir, 1632 EnvBuilder: containerEnv(), 1633 Templates: []*structs.Template{ 1634 { 1635 SourcePath: "${NOMAD_TASK_DIR}/src", 1636 DestPath: "${NOMAD_SECRETS_DIR}/dst", 1637 }, 1638 }, 1639 } 1640 }, 1641 SourcePath: filepath.Join(taskDir.Dir, "local/src"), 1642 DestPath: filepath.Join(taskDir.Dir, "secrets/dst"), 1643 }, 1644 { 1645 Name: "ContainerSrcEscapesErr", 1646 Config: func() *TaskTemplateManagerConfig { 1647 return &TaskTemplateManagerConfig{ 1648 ClientConfig: clientConf, 1649 TaskDir: taskDir.Dir, 1650 EnvBuilder: containerEnv(), 1651 Templates: []*structs.Template{ 1652 { 1653 SourcePath: "/etc/src_escapes", 1654 DestPath: "${NOMAD_SECRETS_DIR}/dst", 1655 }, 1656 }, 1657 } 1658 }, 1659 Err: sourceEscapesErr, 1660 }, 1661 { 1662 Name: "ContainerSrcEscapesOk", 1663 Config: func() *TaskTemplateManagerConfig { 1664 unsafeConf := clientConf.Copy() 1665 unsafeConf.TemplateConfig.DisableSandbox = true 1666 return &TaskTemplateManagerConfig{ 1667 ClientConfig: unsafeConf, 1668 TaskDir: taskDir.Dir, 1669 EnvBuilder: containerEnv(), 1670 Templates: []*structs.Template{ 1671 { 1672 SourcePath: "/etc/src_escapes_ok", 1673 DestPath: "${NOMAD_SECRETS_DIR}/dst", 1674 }, 1675 }, 1676 } 1677 }, 1678 SourcePath: "/etc/src_escapes_ok", 1679 DestPath: filepath.Join(taskDir.Dir, "secrets/dst"), 1680 }, 1681 { 1682 Name: "ContainerDstAbsoluteOk", 1683 Config: func() *TaskTemplateManagerConfig { 1684 return &TaskTemplateManagerConfig{ 1685 ClientConfig: clientConf, 1686 TaskDir: taskDir.Dir, 1687 EnvBuilder: containerEnv(), 1688 Templates: []*structs.Template{ 1689 { 1690 SourcePath: "${NOMAD_TASK_DIR}/src", 1691 DestPath: "/etc/absolutely_relative", 1692 }, 1693 }, 1694 } 1695 }, 1696 SourcePath: filepath.Join(taskDir.Dir, "local/src"), 1697 DestPath: filepath.Join(taskDir.Dir, "etc/absolutely_relative"), 1698 }, 1699 { 1700 Name: "ContainerDstAbsoluteEscapesErr", 1701 Config: func() *TaskTemplateManagerConfig { 1702 return &TaskTemplateManagerConfig{ 1703 ClientConfig: clientConf, 1704 TaskDir: taskDir.Dir, 1705 EnvBuilder: containerEnv(), 1706 Templates: []*structs.Template{ 1707 { 1708 SourcePath: "${NOMAD_TASK_DIR}/src", 1709 DestPath: "../escapes", 1710 }, 1711 }, 1712 } 1713 }, 1714 Err: destEscapesErr, 1715 }, 1716 { 1717 Name: "ContainerDstAbsoluteEscapesOk", 1718 Config: func() *TaskTemplateManagerConfig { 1719 unsafeConf := clientConf.Copy() 1720 unsafeConf.TemplateConfig.DisableSandbox = true 1721 return &TaskTemplateManagerConfig{ 1722 ClientConfig: unsafeConf, 1723 TaskDir: taskDir.Dir, 1724 EnvBuilder: containerEnv(), 1725 Templates: []*structs.Template{ 1726 { 1727 SourcePath: "${NOMAD_TASK_DIR}/src", 1728 DestPath: "../escapes", 1729 }, 1730 }, 1731 } 1732 }, 1733 SourcePath: filepath.Join(taskDir.Dir, "local/src"), 1734 DestPath: filepath.Join(taskDir.Dir, "..", "escapes"), 1735 }, 1736 //TODO: Fix this test. I *think* it should pass. The double 1737 // joining of the task dir onto the destination seems like 1738 // a bug. https://github.com/hashicorp/nomad/issues/9389 1739 { 1740 Name: "RawExecOk", 1741 Config: func() *TaskTemplateManagerConfig { 1742 return &TaskTemplateManagerConfig{ 1743 ClientConfig: clientConf, 1744 TaskDir: taskDir.Dir, 1745 EnvBuilder: rawExecEnv(), 1746 Templates: []*structs.Template{ 1747 { 1748 SourcePath: "${NOMAD_TASK_DIR}/src", 1749 DestPath: "${NOMAD_SECRETS_DIR}/dst", 1750 }, 1751 }, 1752 } 1753 }, 1754 SourcePath: filepath.Join(taskDir.Dir, "local/src"), 1755 DestPath: filepath.Join(taskDir.Dir, "secrets/dst"), 1756 }, 1757 { 1758 Name: "RawExecSrcEscapesErr", 1759 Config: func() *TaskTemplateManagerConfig { 1760 return &TaskTemplateManagerConfig{ 1761 ClientConfig: clientConf, 1762 TaskDir: taskDir.Dir, 1763 EnvBuilder: rawExecEnv(), 1764 Templates: []*structs.Template{ 1765 { 1766 SourcePath: "/etc/src_escapes", 1767 DestPath: "${NOMAD_SECRETS_DIR}/dst", 1768 }, 1769 }, 1770 } 1771 }, 1772 Err: sourceEscapesErr, 1773 }, 1774 { 1775 Name: "RawExecDstAbsoluteOk", 1776 Config: func() *TaskTemplateManagerConfig { 1777 return &TaskTemplateManagerConfig{ 1778 ClientConfig: clientConf, 1779 TaskDir: taskDir.Dir, 1780 EnvBuilder: rawExecEnv(), 1781 Templates: []*structs.Template{ 1782 { 1783 SourcePath: "${NOMAD_TASK_DIR}/src", 1784 DestPath: "/etc/absolutely_relative", 1785 }, 1786 }, 1787 } 1788 }, 1789 SourcePath: filepath.Join(taskDir.Dir, "local/src"), 1790 DestPath: filepath.Join(taskDir.Dir, "etc/absolutely_relative"), 1791 }, 1792 } 1793 1794 for i := range cases { 1795 tc := cases[i] 1796 t.Run(tc.Name, func(t *testing.T) { 1797 config := tc.Config() 1798 mapping, err := parseTemplateConfigs(config) 1799 if tc.Err == nil { 1800 // Ok path 1801 require.NoError(t, err) 1802 require.NotNil(t, mapping) 1803 require.Len(t, mapping, 1) 1804 for k := range mapping { 1805 require.Equal(t, tc.SourcePath, *k.Source) 1806 require.Equal(t, tc.DestPath, *k.Destination) 1807 t.Logf("Rendering %s => %s", *k.Source, *k.Destination) 1808 } 1809 } else { 1810 // Err path 1811 assert.EqualError(t, err, tc.Err.Error()) 1812 require.Nil(t, mapping) 1813 } 1814 1815 }) 1816 } 1817 } 1818 1819 func TestTaskTemplateManager_BlockedEvents(t *testing.T) { 1820 // The tests sets a template that need keys 0, 1, 2, 3, 4, 1821 // then subsequently sets 0, 1, 2 keys 1822 // then asserts that templates are still blocked on 3 and 4, 1823 // and check that we got the relevant task events 1824 t.Parallel() 1825 require := require.New(t) 1826 1827 // Make a template that will render based on a key in Consul 1828 var embedded string 1829 for i := 0; i < 5; i++ { 1830 embedded += fmt.Sprintf(`{{key "%d"}}`, i) 1831 } 1832 1833 file := "my.tmpl" 1834 template := &structs.Template{ 1835 EmbeddedTmpl: embedded, 1836 DestPath: file, 1837 ChangeMode: structs.TemplateChangeModeNoop, 1838 } 1839 1840 harness := newTestHarness(t, []*structs.Template{template}, true, false) 1841 harness.setEmitRate(100 * time.Millisecond) 1842 harness.start(t) 1843 defer harness.stop() 1844 1845 missingKeys := func(e *structs.TaskEvent) ([]string, int) { 1846 missingRexp := regexp.MustCompile(`kv.block\(([0-9]*)\)`) 1847 moreRexp := regexp.MustCompile(`and ([0-9]*) more`) 1848 1849 missingMatch := missingRexp.FindAllStringSubmatch(e.DisplayMessage, -1) 1850 moreMatch := moreRexp.FindAllStringSubmatch(e.DisplayMessage, -1) 1851 1852 missing := make([]string, len(missingMatch)) 1853 for i, v := range missingMatch { 1854 missing[i] = v[1] 1855 } 1856 sort.Strings(missing) 1857 1858 more := 0 1859 if len(moreMatch) != 0 { 1860 more, _ = strconv.Atoi(moreMatch[0][1]) 1861 } 1862 return missing, more 1863 1864 } 1865 1866 // Ensure that we get a blocked event 1867 select { 1868 case <-harness.mockHooks.UnblockCh: 1869 t.Fatalf("Task unblock should have not have been called") 1870 case <-harness.mockHooks.EmitEventCh: 1871 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 1872 t.Fatalf("timeout") 1873 } 1874 1875 // Check to see we got a correct message 1876 // assert that all 0-4 keys are missing 1877 require.Len(harness.mockHooks.Events, 1) 1878 t.Logf("first message: %v", harness.mockHooks.Events[0]) 1879 missing, more := missingKeys(harness.mockHooks.Events[0]) 1880 require.Equal(5, len(missing)+more) 1881 require.Contains(harness.mockHooks.Events[0].DisplayMessage, "and 2 more") 1882 1883 // Write 0-2 keys to Consul 1884 for i := 0; i < 3; i++ { 1885 harness.consul.SetKV(t, fmt.Sprintf("%d", i), []byte{0xa}) 1886 } 1887 1888 // Ensure that we get a blocked event 1889 isExpectedFinalEvent := func(e *structs.TaskEvent) bool { 1890 missing, more := missingKeys(e) 1891 return reflect.DeepEqual(missing, []string{"3", "4"}) && more == 0 1892 } 1893 timeout := time.After(time.Second * time.Duration(testutil.TestMultiplier())) 1894 WAIT_LOOP: 1895 for { 1896 select { 1897 case <-harness.mockHooks.UnblockCh: 1898 t.Errorf("Task unblock should have not have been called") 1899 case e := <-harness.mockHooks.EmitEventCh: 1900 t.Logf("received event: %v", e.DisplayMessage) 1901 if isExpectedFinalEvent(e) { 1902 break WAIT_LOOP 1903 } 1904 case <-timeout: 1905 t.Errorf("timeout") 1906 } 1907 } 1908 1909 // Check to see we got a correct message 1910 event := harness.mockHooks.Events[len(harness.mockHooks.Events)-1] 1911 if !isExpectedFinalEvent(event) { 1912 t.Logf("received all events: %v", pretty.Sprint(harness.mockHooks.Events)) 1913 1914 t.Fatalf("bad event, expected only 3 and 5 blocked got: %q", event.DisplayMessage) 1915 } 1916 }