github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/client/allocrunner/taskrunner/template/template_test.go (about) 1 package template 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "reflect" 11 "regexp" 12 "runtime" 13 "sort" 14 "strconv" 15 "strings" 16 "sync" 17 "syscall" 18 "testing" 19 "time" 20 21 templateconfig "github.com/hashicorp/consul-template/config" 22 ctestutil "github.com/hashicorp/consul/sdk/testutil" 23 "github.com/hashicorp/nomad/ci" 24 "github.com/hashicorp/nomad/client/allocdir" 25 "github.com/hashicorp/nomad/client/config" 26 "github.com/hashicorp/nomad/client/taskenv" 27 clienttestutil "github.com/hashicorp/nomad/client/testutil" 28 "github.com/hashicorp/nomad/helper/pointer" 29 "github.com/hashicorp/nomad/helper/testlog" 30 "github.com/hashicorp/nomad/helper/users" 31 "github.com/hashicorp/nomad/helper/uuid" 32 "github.com/hashicorp/nomad/nomad/mock" 33 "github.com/hashicorp/nomad/nomad/structs" 34 sconfig "github.com/hashicorp/nomad/nomad/structs/config" 35 "github.com/hashicorp/nomad/testutil" 36 "github.com/kr/pretty" 37 "github.com/shoenig/test/must" 38 "github.com/stretchr/testify/assert" 39 "github.com/stretchr/testify/require" 40 ) 41 42 const ( 43 // TestTaskName is the name of the injected task. It should appear in the 44 // environment variable $NOMAD_TASK_NAME 45 TestTaskName = "test-task" 46 ) 47 48 // MockTaskHooks is a mock of the TaskHooks interface useful for testing 49 type MockTaskHooks struct { 50 Restarts int 51 RestartCh chan struct{} 52 53 Signals []string 54 SignalCh chan struct{} 55 signalLock sync.Mutex 56 57 // SignalError is returned when Signal is called on the mock hook 58 SignalError error 59 60 UnblockCh chan struct{} 61 62 KillEvent *structs.TaskEvent 63 KillCh chan *structs.TaskEvent 64 65 Events []*structs.TaskEvent 66 EmitEventCh chan *structs.TaskEvent 67 68 // hasHandle can be set to simulate restoring a task after client restart 69 hasHandle bool 70 } 71 72 func NewMockTaskHooks() *MockTaskHooks { 73 return &MockTaskHooks{ 74 UnblockCh: make(chan struct{}, 1), 75 RestartCh: make(chan struct{}, 1), 76 SignalCh: make(chan struct{}, 1), 77 KillCh: make(chan *structs.TaskEvent, 1), 78 EmitEventCh: make(chan *structs.TaskEvent, 1), 79 } 80 } 81 func (m *MockTaskHooks) Restart(ctx context.Context, event *structs.TaskEvent, failure bool) error { 82 m.Restarts++ 83 select { 84 case m.RestartCh <- struct{}{}: 85 default: 86 } 87 return nil 88 } 89 90 func (m *MockTaskHooks) Signal(event *structs.TaskEvent, s string) error { 91 m.signalLock.Lock() 92 m.Signals = append(m.Signals, s) 93 m.signalLock.Unlock() 94 select { 95 case m.SignalCh <- struct{}{}: 96 default: 97 } 98 99 return m.SignalError 100 } 101 102 func (m *MockTaskHooks) Kill(ctx context.Context, event *structs.TaskEvent) error { 103 m.KillEvent = event 104 select { 105 case m.KillCh <- event: 106 default: 107 } 108 return nil 109 } 110 111 func (m *MockTaskHooks) IsRunning() bool { 112 return m.hasHandle 113 } 114 115 func (m *MockTaskHooks) EmitEvent(event *structs.TaskEvent) { 116 m.Events = append(m.Events, event) 117 select { 118 case m.EmitEventCh <- event: 119 case <-m.EmitEventCh: 120 m.EmitEventCh <- event 121 } 122 } 123 124 func (m *MockTaskHooks) SetState(state string, event *structs.TaskEvent) {} 125 126 // mockExecutor implements script executor interface 127 type mockExecutor struct { 128 DesiredExit int 129 DesiredErr error 130 } 131 132 func (m *mockExecutor) Exec(timeout time.Duration, cmd string, args []string) ([]byte, int, error) { 133 return []byte{}, m.DesiredExit, m.DesiredErr 134 } 135 136 // testHarness is used to test the TaskTemplateManager by spinning up 137 // Consul/Vault as needed 138 type testHarness struct { 139 manager *TaskTemplateManager 140 mockHooks *MockTaskHooks 141 templates []*structs.Template 142 envBuilder *taskenv.Builder 143 node *structs.Node 144 config *config.Config 145 vaultToken string 146 taskDir string 147 vault *testutil.TestVault 148 consul *ctestutil.TestServer 149 emitRate time.Duration 150 nomadNamespace string 151 } 152 153 // newTestHarness returns a harness starting a dev consul and vault server, 154 // building the appropriate config and creating a TaskTemplateManager 155 func newTestHarness(t *testing.T, templates []*structs.Template, consul, vault bool) *testHarness { 156 region := "global" 157 mockNode := mock.Node() 158 159 harness := &testHarness{ 160 mockHooks: NewMockTaskHooks(), 161 templates: templates, 162 node: mockNode, 163 config: &config.Config{ 164 Node: mockNode, 165 Region: region, 166 TemplateConfig: &config.ClientTemplateConfig{ 167 FunctionDenylist: config.DefaultTemplateFunctionDenylist, 168 DisableSandbox: false, 169 ConsulRetry: &config.RetryConfig{Backoff: pointer.Of(10 * time.Millisecond)}, 170 }}, 171 emitRate: DefaultMaxTemplateEventRate, 172 } 173 174 // Build the task environment 175 a := mock.Alloc() 176 task := a.Job.TaskGroups[0].Tasks[0] 177 task.Name = TestTaskName 178 harness.envBuilder = taskenv.NewBuilder(harness.node, a, task, region) 179 harness.nomadNamespace = a.Namespace 180 181 // Make a tempdir 182 harness.taskDir = t.TempDir() 183 harness.envBuilder.SetClientTaskRoot(harness.taskDir) 184 185 if consul { 186 var err error 187 harness.consul, err = ctestutil.NewTestServerConfigT(t, func(c *ctestutil.TestServerConfig) { 188 c.Peering = nil // fix for older versions of Consul (<1.13.0) that don't support peering 189 }) 190 if err != nil { 191 t.Fatalf("error starting test Consul server: %v", err) 192 } 193 harness.config.ConsulConfig = &sconfig.ConsulConfig{ 194 Addr: harness.consul.HTTPAddr, 195 } 196 } 197 198 if vault { 199 harness.vault = testutil.NewTestVault(t) 200 harness.config.VaultConfig = harness.vault.Config 201 harness.vaultToken = harness.vault.RootToken 202 } 203 204 return harness 205 } 206 207 func (h *testHarness) start(t *testing.T) { 208 if err := h.startWithErr(); err != nil { 209 t.Fatalf("failed to build task template manager: %v", err) 210 } 211 } 212 213 func (h *testHarness) startWithErr() error { 214 var err error 215 h.manager, err = NewTaskTemplateManager(&TaskTemplateManagerConfig{ 216 UnblockCh: h.mockHooks.UnblockCh, 217 Lifecycle: h.mockHooks, 218 Events: h.mockHooks, 219 Templates: h.templates, 220 ClientConfig: h.config, 221 VaultToken: h.vaultToken, 222 TaskDir: h.taskDir, 223 EnvBuilder: h.envBuilder, 224 MaxTemplateEventRate: h.emitRate, 225 }) 226 return err 227 } 228 229 func (h *testHarness) setEmitRate(d time.Duration) { 230 h.emitRate = d 231 } 232 233 // stop is used to stop any running Vault or Consul server plus the task manager 234 func (h *testHarness) stop() { 235 if h.vault != nil { 236 h.vault.Stop() 237 } 238 if h.consul != nil { 239 h.consul.Stop() 240 } 241 if h.manager != nil { 242 h.manager.Stop() 243 } 244 if h.taskDir != "" { 245 os.RemoveAll(h.taskDir) 246 } 247 } 248 249 func TestTaskTemplateManager_InvalidConfig(t *testing.T) { 250 ci.Parallel(t) 251 hooks := NewMockTaskHooks() 252 clientConfig := &config.Config{Region: "global"} 253 taskDir := "foo" 254 a := mock.Alloc() 255 envBuilder := taskenv.NewBuilder(mock.Node(), a, a.Job.TaskGroups[0].Tasks[0], clientConfig.Region) 256 257 cases := []struct { 258 name string 259 config *TaskTemplateManagerConfig 260 expectedErr string 261 }{ 262 { 263 name: "nil config", 264 config: nil, 265 expectedErr: "Nil config passed", 266 }, 267 { 268 name: "bad lifecycle hooks", 269 config: &TaskTemplateManagerConfig{ 270 UnblockCh: hooks.UnblockCh, 271 Events: hooks, 272 ClientConfig: clientConfig, 273 TaskDir: taskDir, 274 EnvBuilder: envBuilder, 275 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 276 }, 277 expectedErr: "lifecycle hooks", 278 }, 279 { 280 name: "bad event hooks", 281 config: &TaskTemplateManagerConfig{ 282 UnblockCh: hooks.UnblockCh, 283 Lifecycle: hooks, 284 ClientConfig: clientConfig, 285 TaskDir: taskDir, 286 EnvBuilder: envBuilder, 287 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 288 }, 289 expectedErr: "event hook", 290 }, 291 { 292 name: "bad client config", 293 config: &TaskTemplateManagerConfig{ 294 UnblockCh: hooks.UnblockCh, 295 Lifecycle: hooks, 296 Events: hooks, 297 TaskDir: taskDir, 298 EnvBuilder: envBuilder, 299 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 300 }, 301 expectedErr: "client config", 302 }, 303 { 304 name: "bad task dir", 305 config: &TaskTemplateManagerConfig{ 306 UnblockCh: hooks.UnblockCh, 307 ClientConfig: clientConfig, 308 Lifecycle: hooks, 309 Events: hooks, 310 EnvBuilder: envBuilder, 311 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 312 }, 313 expectedErr: "task directory", 314 }, 315 { 316 name: "bad env builder", 317 config: &TaskTemplateManagerConfig{ 318 UnblockCh: hooks.UnblockCh, 319 ClientConfig: clientConfig, 320 Lifecycle: hooks, 321 Events: hooks, 322 TaskDir: taskDir, 323 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 324 }, 325 expectedErr: "task environment", 326 }, 327 { 328 name: "bad max event rate", 329 config: &TaskTemplateManagerConfig{ 330 UnblockCh: hooks.UnblockCh, 331 ClientConfig: clientConfig, 332 Lifecycle: hooks, 333 Events: hooks, 334 TaskDir: taskDir, 335 EnvBuilder: envBuilder, 336 }, 337 expectedErr: "template event rate", 338 }, 339 { 340 name: "valid", 341 config: &TaskTemplateManagerConfig{ 342 UnblockCh: hooks.UnblockCh, 343 ClientConfig: clientConfig, 344 Lifecycle: hooks, 345 Events: hooks, 346 TaskDir: taskDir, 347 EnvBuilder: envBuilder, 348 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 349 }, 350 }, 351 { 352 name: "invalid signal", 353 config: &TaskTemplateManagerConfig{ 354 UnblockCh: hooks.UnblockCh, 355 Templates: []*structs.Template{ 356 { 357 DestPath: "foo", 358 EmbeddedTmpl: "hello, world", 359 ChangeMode: structs.TemplateChangeModeSignal, 360 ChangeSignal: "foobarbaz", 361 }, 362 }, 363 ClientConfig: clientConfig, 364 Lifecycle: hooks, 365 Events: hooks, 366 TaskDir: taskDir, 367 EnvBuilder: envBuilder, 368 MaxTemplateEventRate: DefaultMaxTemplateEventRate, 369 }, 370 expectedErr: "parse signal", 371 }, 372 } 373 374 for _, c := range cases { 375 t.Run(c.name, func(t *testing.T) { 376 _, err := NewTaskTemplateManager(c.config) 377 if err != nil { 378 if c.expectedErr == "" { 379 t.Fatalf("unexpected error: %v", err) 380 } else if !strings.Contains(err.Error(), c.expectedErr) { 381 t.Fatalf("expected error to contain %q; got %q", c.expectedErr, err.Error()) 382 } 383 } else if c.expectedErr != "" { 384 t.Fatalf("expected an error to contain %q", c.expectedErr) 385 } 386 }) 387 } 388 } 389 390 func TestTaskTemplateManager_HostPath(t *testing.T) { 391 ci.Parallel(t) 392 // Make a template that will render immediately and write it to a tmp file 393 f, err := os.CreateTemp("", "") 394 if err != nil { 395 t.Fatalf("Bad: %v", err) 396 } 397 defer f.Close() 398 defer os.Remove(f.Name()) 399 400 content := "hello, world!" 401 if _, err := io.WriteString(f, content); err != nil { 402 t.Fatalf("Bad: %v", err) 403 } 404 405 file := "my.tmpl" 406 template := &structs.Template{ 407 SourcePath: f.Name(), 408 DestPath: file, 409 ChangeMode: structs.TemplateChangeModeNoop, 410 } 411 412 harness := newTestHarness(t, []*structs.Template{template}, false, false) 413 harness.config.TemplateConfig.DisableSandbox = true 414 err = harness.startWithErr() 415 if err != nil { 416 t.Fatalf("couldn't setup initial harness: %v", err) 417 } 418 defer harness.stop() 419 420 // Wait for the unblock 421 select { 422 case <-harness.mockHooks.UnblockCh: 423 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 424 t.Fatalf("Task unblock should have been called") 425 } 426 427 // Check the file is there 428 path := filepath.Join(harness.taskDir, file) 429 raw, err := os.ReadFile(path) 430 if err != nil { 431 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 432 } 433 434 if s := string(raw); s != content { 435 t.Fatalf("Unexpected template data; got %q, want %q", s, content) 436 } 437 438 // Change the config to disallow host sources 439 harness = newTestHarness(t, []*structs.Template{template}, false, false) 440 err = harness.startWithErr() 441 if err == nil || !strings.Contains(err.Error(), "escapes alloc directory") { 442 t.Fatalf("Expected absolute template path disallowed for %q: %v", 443 template.SourcePath, err) 444 } 445 446 template.SourcePath = "../../../../../../" + file 447 harness = newTestHarness(t, []*structs.Template{template}, false, false) 448 err = harness.startWithErr() 449 if err == nil || !strings.Contains(err.Error(), "escapes alloc directory") { 450 t.Fatalf("Expected directory traversal out of %q disallowed for %q: %v", 451 harness.taskDir, template.SourcePath, err) 452 } 453 454 // Build a new task environment 455 a := mock.Alloc() 456 task := a.Job.TaskGroups[0].Tasks[0] 457 task.Name = TestTaskName 458 task.Meta = map[string]string{"ESCAPE": "../"} 459 460 template.SourcePath = "${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}" + file 461 harness = newTestHarness(t, []*structs.Template{template}, false, false) 462 harness.envBuilder = taskenv.NewBuilder(harness.node, a, task, "global") 463 err = harness.startWithErr() 464 if err == nil || !strings.Contains(err.Error(), "escapes alloc directory") { 465 t.Fatalf("Expected directory traversal out of %q via interpolation disallowed for %q: %v", 466 harness.taskDir, template.SourcePath, err) 467 } 468 469 // Test with desination too 470 template.SourcePath = f.Name() 471 template.DestPath = "../../../../../../" + file 472 harness = newTestHarness(t, []*structs.Template{template}, false, false) 473 harness.envBuilder = taskenv.NewBuilder(harness.node, a, task, "global") 474 err = harness.startWithErr() 475 if err == nil || !strings.Contains(err.Error(), "escapes alloc directory") { 476 t.Fatalf("Expected directory traversal out of %q via interpolation disallowed for %q: %v", 477 harness.taskDir, template.SourcePath, err) 478 } 479 480 } 481 482 func TestTaskTemplateManager_Unblock_Static(t *testing.T) { 483 ci.Parallel(t) 484 // Make a template that will render immediately 485 content := "hello, world!" 486 file := "my.tmpl" 487 template := &structs.Template{ 488 EmbeddedTmpl: content, 489 DestPath: file, 490 ChangeMode: structs.TemplateChangeModeNoop, 491 } 492 493 harness := newTestHarness(t, []*structs.Template{template}, false, false) 494 harness.start(t) 495 defer harness.stop() 496 497 // Wait for the unblock 498 select { 499 case <-harness.mockHooks.UnblockCh: 500 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 501 t.Fatalf("Task unblock should have been called") 502 } 503 504 // Check the file is there 505 path := filepath.Join(harness.taskDir, file) 506 raw, err := os.ReadFile(path) 507 if err != nil { 508 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 509 } 510 511 if s := string(raw); s != content { 512 t.Fatalf("Unexpected template data; got %q, want %q", s, content) 513 } 514 } 515 516 func TestTaskTemplateManager_Permissions(t *testing.T) { 517 clienttestutil.RequireRoot(t) 518 ci.Parallel(t) 519 // Make a template that will render immediately 520 content := "hello, world!" 521 file := "my.tmpl" 522 template := &structs.Template{ 523 EmbeddedTmpl: content, 524 DestPath: file, 525 ChangeMode: structs.TemplateChangeModeNoop, 526 Perms: "777", 527 Uid: pointer.Of(503), 528 Gid: pointer.Of(20), 529 } 530 531 harness := newTestHarness(t, []*structs.Template{template}, false, false) 532 harness.start(t) 533 defer harness.stop() 534 535 // Wait for the unblock 536 select { 537 case <-harness.mockHooks.UnblockCh: 538 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 539 t.Fatalf("Task unblock should have been called") 540 } 541 542 // Check the file is there 543 path := filepath.Join(harness.taskDir, file) 544 fi, err := os.Stat(path) 545 if err != nil { 546 t.Fatalf("Failed to stat file: %v", err) 547 } 548 549 if m := fi.Mode(); m != os.ModePerm { 550 t.Fatalf("Got mode %v; want %v", m, os.ModePerm) 551 } 552 553 sys := fi.Sys() 554 uid := pointer.Of(int(sys.(*syscall.Stat_t).Uid)) 555 gid := pointer.Of(int(sys.(*syscall.Stat_t).Gid)) 556 557 must.Eq(t, template.Uid, uid) 558 must.Eq(t, template.Gid, gid) 559 } 560 561 func TestTaskTemplateManager_Unblock_Static_NomadEnv(t *testing.T) { 562 ci.Parallel(t) 563 // Make a template that will render immediately 564 content := `Hello Nomad Task: {{env "NOMAD_TASK_NAME"}}` 565 expected := fmt.Sprintf("Hello Nomad Task: %s", TestTaskName) 566 file := "my.tmpl" 567 template := &structs.Template{ 568 EmbeddedTmpl: content, 569 DestPath: file, 570 ChangeMode: structs.TemplateChangeModeNoop, 571 } 572 573 harness := newTestHarness(t, []*structs.Template{template}, false, false) 574 harness.start(t) 575 defer harness.stop() 576 577 // Wait for the unblock 578 select { 579 case <-harness.mockHooks.UnblockCh: 580 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 581 t.Fatalf("Task unblock should have been called") 582 } 583 584 // Check the file is there 585 path := filepath.Join(harness.taskDir, file) 586 raw, err := os.ReadFile(path) 587 if err != nil { 588 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 589 } 590 591 if s := string(raw); s != expected { 592 t.Fatalf("Unexpected template data; got %q, want %q", s, expected) 593 } 594 } 595 596 func TestTaskTemplateManager_Unblock_Static_AlreadyRendered(t *testing.T) { 597 ci.Parallel(t) 598 // Make a template that will render immediately 599 content := "hello, world!" 600 file := "my.tmpl" 601 template := &structs.Template{ 602 EmbeddedTmpl: content, 603 DestPath: file, 604 ChangeMode: structs.TemplateChangeModeNoop, 605 } 606 607 harness := newTestHarness(t, []*structs.Template{template}, false, false) 608 609 // Write the contents 610 path := filepath.Join(harness.taskDir, file) 611 if err := os.WriteFile(path, []byte(content), 0777); err != nil { 612 t.Fatalf("Failed to write data: %v", err) 613 } 614 615 harness.start(t) 616 defer harness.stop() 617 618 // Wait for the unblock 619 select { 620 case <-harness.mockHooks.UnblockCh: 621 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 622 t.Fatalf("Task unblock should have been called") 623 } 624 625 // Check the file is there 626 path = filepath.Join(harness.taskDir, file) 627 raw, err := os.ReadFile(path) 628 if err != nil { 629 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 630 } 631 632 if s := string(raw); s != content { 633 t.Fatalf("Unexpected template data; got %q, want %q", s, content) 634 } 635 } 636 637 func TestTaskTemplateManager_Unblock_Consul(t *testing.T) { 638 ci.Parallel(t) 639 // Make a template that will render based on a key in Consul 640 key := "foo" 641 content := "barbaz" 642 embedded := fmt.Sprintf(`{{key "%s"}}`, key) 643 file := "my.tmpl" 644 template := &structs.Template{ 645 EmbeddedTmpl: embedded, 646 DestPath: file, 647 ChangeMode: structs.TemplateChangeModeNoop, 648 } 649 650 harness := newTestHarness(t, []*structs.Template{template}, true, false) 651 harness.start(t) 652 defer harness.stop() 653 654 // Ensure no unblock 655 select { 656 case <-harness.mockHooks.UnblockCh: 657 t.Fatalf("Task unblock should have not have been called") 658 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 659 } 660 661 // Write the key to Consul 662 harness.consul.SetKV(t, key, []byte(content)) 663 664 // Wait for the unblock 665 select { 666 case <-harness.mockHooks.UnblockCh: 667 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 668 t.Fatalf("Task unblock should have been called") 669 } 670 671 // Check the file is there 672 path := filepath.Join(harness.taskDir, file) 673 raw, err := os.ReadFile(path) 674 if err != nil { 675 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 676 } 677 678 if s := string(raw); s != content { 679 t.Fatalf("Unexpected template data; got %q, want %q", s, content) 680 } 681 } 682 683 func TestTaskTemplateManager_Unblock_Vault(t *testing.T) { 684 ci.Parallel(t) 685 require := require.New(t) 686 // Make a template that will render based on a key in Vault 687 vaultPath := "secret/data/password" 688 key := "password" 689 content := "barbaz" 690 embedded := fmt.Sprintf(`{{with secret "%s"}}{{.Data.data.%s}}{{end}}`, vaultPath, key) 691 file := "my.tmpl" 692 template := &structs.Template{ 693 EmbeddedTmpl: embedded, 694 DestPath: file, 695 ChangeMode: structs.TemplateChangeModeNoop, 696 } 697 698 harness := newTestHarness(t, []*structs.Template{template}, false, true) 699 harness.start(t) 700 defer harness.stop() 701 702 // Ensure no unblock 703 select { 704 case <-harness.mockHooks.UnblockCh: 705 t.Fatalf("Task unblock should not have been called") 706 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 707 } 708 709 // Write the secret to Vault 710 logical := harness.vault.Client.Logical() 711 _, err := logical.Write(vaultPath, map[string]interface{}{"data": map[string]interface{}{key: content}}) 712 require.NoError(err) 713 714 // Wait for the unblock 715 select { 716 case <-harness.mockHooks.UnblockCh: 717 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 718 t.Fatalf("Task unblock should have been called") 719 } 720 721 // Check the file is there 722 path := filepath.Join(harness.taskDir, file) 723 raw, err := os.ReadFile(path) 724 if err != nil { 725 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 726 } 727 728 if s := string(raw); s != content { 729 t.Fatalf("Unexpected template data; got %q, want %q", s, content) 730 } 731 } 732 733 func TestTaskTemplateManager_Unblock_Multi_Template(t *testing.T) { 734 ci.Parallel(t) 735 // Make a template that will render immediately 736 staticContent := "hello, world!" 737 staticFile := "my.tmpl" 738 template := &structs.Template{ 739 EmbeddedTmpl: staticContent, 740 DestPath: staticFile, 741 ChangeMode: structs.TemplateChangeModeNoop, 742 } 743 744 // Make a template that will render based on a key in Consul 745 consulKey := "foo" 746 consulContent := "barbaz" 747 consulEmbedded := fmt.Sprintf(`{{key "%s"}}`, consulKey) 748 consulFile := "consul.tmpl" 749 template2 := &structs.Template{ 750 EmbeddedTmpl: consulEmbedded, 751 DestPath: consulFile, 752 ChangeMode: structs.TemplateChangeModeNoop, 753 } 754 755 harness := newTestHarness(t, []*structs.Template{template, template2}, true, false) 756 harness.start(t) 757 defer harness.stop() 758 759 // Ensure no unblock 760 select { 761 case <-harness.mockHooks.UnblockCh: 762 t.Fatalf("Task unblock should have not have been called") 763 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 764 } 765 766 // Check that the static file has been rendered 767 path := filepath.Join(harness.taskDir, staticFile) 768 raw, err := os.ReadFile(path) 769 if err != nil { 770 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 771 } 772 773 if s := string(raw); s != staticContent { 774 t.Fatalf("Unexpected template data; got %q, want %q", s, staticContent) 775 } 776 777 // Write the key to Consul 778 harness.consul.SetKV(t, consulKey, []byte(consulContent)) 779 780 // Wait for the unblock 781 select { 782 case <-harness.mockHooks.UnblockCh: 783 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 784 t.Fatalf("Task unblock should have been called") 785 } 786 787 // Check the consul file is there 788 path = filepath.Join(harness.taskDir, consulFile) 789 raw, err = os.ReadFile(path) 790 if err != nil { 791 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 792 } 793 794 if s := string(raw); s != consulContent { 795 t.Fatalf("Unexpected template data; got %q, want %q", s, consulContent) 796 } 797 } 798 799 // TestTaskTemplateManager_FirstRender_Restored tests that a task that's been 800 // restored renders and triggers its change mode if the template has changed 801 func TestTaskTemplateManager_FirstRender_Restored(t *testing.T) { 802 ci.Parallel(t) 803 require := require.New(t) 804 // Make a template that will render based on a key in Vault 805 vaultPath := "secret/data/password" 806 key := "password" 807 content := "barbaz" 808 embedded := fmt.Sprintf(`{{with secret "%s"}}{{.Data.data.%s}}{{end}}`, vaultPath, key) 809 file := "my.tmpl" 810 template := &structs.Template{ 811 EmbeddedTmpl: embedded, 812 DestPath: file, 813 ChangeMode: structs.TemplateChangeModeRestart, 814 } 815 816 harness := newTestHarness(t, []*structs.Template{template}, false, true) 817 harness.start(t) 818 defer harness.stop() 819 820 // Ensure no unblock 821 select { 822 case <-harness.mockHooks.UnblockCh: 823 require.Fail("Task unblock should not have been called") 824 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 825 } 826 827 // Write the secret to Vault 828 logical := harness.vault.Client.Logical() 829 _, err := logical.Write(vaultPath, map[string]interface{}{"data": map[string]interface{}{key: content}}) 830 require.NoError(err) 831 832 // Wait for the unblock 833 select { 834 case <-harness.mockHooks.UnblockCh: 835 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 836 require.Fail("Task unblock should have been called") 837 } 838 839 // Check the file is there 840 path := filepath.Join(harness.taskDir, file) 841 raw, err := os.ReadFile(path) 842 require.NoError(err, "Failed to read rendered template from %q", path) 843 require.Equal(content, string(raw), "Unexpected template data; got %s, want %q", raw, content) 844 845 // task is now running 846 harness.mockHooks.hasHandle = true 847 848 // simulate a client restart 849 harness.manager.Stop() 850 harness.mockHooks.UnblockCh = make(chan struct{}, 1) 851 harness.start(t) 852 853 // Wait for the unblock 854 select { 855 case <-harness.mockHooks.UnblockCh: 856 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 857 require.Fail("Task unblock should have been called") 858 } 859 860 select { 861 case <-harness.mockHooks.RestartCh: 862 require.Fail("should not have restarted", harness.mockHooks) 863 case <-harness.mockHooks.SignalCh: 864 require.Fail("should not have restarted", harness.mockHooks) 865 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 866 } 867 868 // simulate a client restart and TTL expiry 869 harness.manager.Stop() 870 content = "bazbar" 871 _, err = logical.Write(vaultPath, map[string]interface{}{"data": map[string]interface{}{key: content}}) 872 require.NoError(err) 873 harness.mockHooks.UnblockCh = make(chan struct{}, 1) 874 harness.start(t) 875 876 // Wait for the unblock 877 select { 878 case <-harness.mockHooks.UnblockCh: 879 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 880 require.Fail("Task unblock should have been called") 881 } 882 883 // Wait for restart 884 timeout := time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second) 885 OUTER: 886 for { 887 select { 888 case <-harness.mockHooks.RestartCh: 889 break OUTER 890 case <-harness.mockHooks.SignalCh: 891 require.Fail("Signal with restart policy", harness.mockHooks) 892 case <-timeout: 893 require.Fail("Should have received a restart", harness.mockHooks) 894 } 895 } 896 } 897 898 func TestTaskTemplateManager_Rerender_Noop(t *testing.T) { 899 ci.Parallel(t) 900 // Make a template that will render based on a key in Consul 901 key := "foo" 902 content1 := "bar" 903 content2 := "baz" 904 embedded := fmt.Sprintf(`{{key "%s"}}`, key) 905 file := "my.tmpl" 906 template := &structs.Template{ 907 EmbeddedTmpl: embedded, 908 DestPath: file, 909 ChangeMode: structs.TemplateChangeModeNoop, 910 } 911 912 harness := newTestHarness(t, []*structs.Template{template}, true, false) 913 harness.start(t) 914 defer harness.stop() 915 916 // Ensure no unblock 917 select { 918 case <-harness.mockHooks.UnblockCh: 919 t.Fatalf("Task unblock should have not have been called") 920 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 921 } 922 923 // Write the key to Consul 924 harness.consul.SetKV(t, key, []byte(content1)) 925 926 // Wait for the 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 path := filepath.Join(harness.taskDir, file) 935 raw, err := os.ReadFile(path) 936 if err != nil { 937 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 938 } 939 940 if s := string(raw); s != content1 { 941 t.Fatalf("Unexpected template data; got %q, want %q", s, content1) 942 } 943 944 // Update the key in Consul 945 harness.consul.SetKV(t, key, []byte(content2)) 946 947 select { 948 case <-harness.mockHooks.RestartCh: 949 t.Fatalf("Noop ignored: %+v", harness.mockHooks) 950 case <-harness.mockHooks.SignalCh: 951 t.Fatalf("Noop ignored: %+v", harness.mockHooks) 952 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 953 } 954 955 // Check the file has been updated 956 path = filepath.Join(harness.taskDir, file) 957 raw, err = os.ReadFile(path) 958 if err != nil { 959 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 960 } 961 962 if s := string(raw); s != content2 { 963 t.Fatalf("Unexpected template data; got %q, want %q", s, content2) 964 } 965 } 966 967 func TestTaskTemplateManager_Rerender_Signal(t *testing.T) { 968 ci.Parallel(t) 969 // Make a template that renders based on a key in Consul and sends SIGALRM 970 key1 := "foo" 971 content1_1 := "bar" 972 content1_2 := "baz" 973 embedded1 := fmt.Sprintf(`{{key "%s"}}`, key1) 974 file1 := "my.tmpl" 975 template := &structs.Template{ 976 EmbeddedTmpl: embedded1, 977 DestPath: file1, 978 ChangeMode: structs.TemplateChangeModeSignal, 979 ChangeSignal: "SIGALRM", 980 } 981 982 // Make a template that renders based on a key in Consul and sends SIGBUS 983 key2 := "bam" 984 content2_1 := "cat" 985 content2_2 := "dog" 986 embedded2 := fmt.Sprintf(`{{key "%s"}}`, key2) 987 file2 := "my-second.tmpl" 988 template2 := &structs.Template{ 989 EmbeddedTmpl: embedded2, 990 DestPath: file2, 991 ChangeMode: structs.TemplateChangeModeSignal, 992 ChangeSignal: "SIGBUS", 993 } 994 995 harness := newTestHarness(t, []*structs.Template{template, template2}, true, false) 996 harness.start(t) 997 defer harness.stop() 998 999 // Ensure no unblock 1000 select { 1001 case <-harness.mockHooks.UnblockCh: 1002 t.Fatalf("Task unblock should have not have been called") 1003 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 1004 } 1005 1006 // Write the key to Consul 1007 harness.consul.SetKV(t, key1, []byte(content1_1)) 1008 harness.consul.SetKV(t, key2, []byte(content2_1)) 1009 1010 // Wait for the unblock 1011 select { 1012 case <-harness.mockHooks.UnblockCh: 1013 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 1014 t.Fatalf("Task unblock should have been called") 1015 } 1016 1017 if len(harness.mockHooks.Signals) != 0 { 1018 t.Fatalf("Should not have received any signals: %+v", harness.mockHooks) 1019 } 1020 1021 // Update the keys in Consul 1022 harness.consul.SetKV(t, key1, []byte(content1_2)) 1023 harness.consul.SetKV(t, key2, []byte(content2_2)) 1024 1025 // Wait for signals 1026 timeout := time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second) 1027 OUTER: 1028 for { 1029 select { 1030 case <-harness.mockHooks.RestartCh: 1031 t.Fatalf("Restart with signal policy: %+v", harness.mockHooks) 1032 case <-harness.mockHooks.SignalCh: 1033 harness.mockHooks.signalLock.Lock() 1034 s := harness.mockHooks.Signals 1035 harness.mockHooks.signalLock.Unlock() 1036 if len(s) != 2 { 1037 continue 1038 } 1039 break OUTER 1040 case <-timeout: 1041 t.Fatalf("Should have received two signals: %+v", harness.mockHooks) 1042 } 1043 } 1044 1045 // Check the files have been updated 1046 path := filepath.Join(harness.taskDir, file1) 1047 raw, err := os.ReadFile(path) 1048 if err != nil { 1049 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 1050 } 1051 1052 if s := string(raw); s != content1_2 { 1053 t.Fatalf("Unexpected template data; got %q, want %q", s, content1_2) 1054 } 1055 1056 path = filepath.Join(harness.taskDir, file2) 1057 raw, err = os.ReadFile(path) 1058 if err != nil { 1059 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 1060 } 1061 1062 if s := string(raw); s != content2_2 { 1063 t.Fatalf("Unexpected template data; got %q, want %q", s, content2_2) 1064 } 1065 } 1066 1067 func TestTaskTemplateManager_Rerender_Restart(t *testing.T) { 1068 ci.Parallel(t) 1069 // Make a template that renders based on a key in Consul and sends restart 1070 key1 := "bam" 1071 content1_1 := "cat" 1072 content1_2 := "dog" 1073 embedded1 := fmt.Sprintf(`{{key "%s"}}`, key1) 1074 file1 := "my.tmpl" 1075 template := &structs.Template{ 1076 EmbeddedTmpl: embedded1, 1077 DestPath: file1, 1078 ChangeMode: structs.TemplateChangeModeRestart, 1079 } 1080 1081 harness := newTestHarness(t, []*structs.Template{template}, true, false) 1082 harness.start(t) 1083 defer harness.stop() 1084 1085 // Ensure no unblock 1086 select { 1087 case <-harness.mockHooks.UnblockCh: 1088 t.Fatalf("Task unblock should have not have been called") 1089 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 1090 } 1091 1092 // Write the key to Consul 1093 harness.consul.SetKV(t, key1, []byte(content1_1)) 1094 1095 // Wait for the unblock 1096 select { 1097 case <-harness.mockHooks.UnblockCh: 1098 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 1099 t.Fatalf("Task unblock should have been called") 1100 } 1101 1102 // Update the keys in Consul 1103 harness.consul.SetKV(t, key1, []byte(content1_2)) 1104 1105 // Wait for restart 1106 timeout := time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second) 1107 OUTER: 1108 for { 1109 select { 1110 case <-harness.mockHooks.RestartCh: 1111 break OUTER 1112 case <-harness.mockHooks.SignalCh: 1113 t.Fatalf("Signal with restart policy: %+v", harness.mockHooks) 1114 case <-timeout: 1115 t.Fatalf("Should have received a restart: %+v", harness.mockHooks) 1116 } 1117 } 1118 1119 // Check the files have been updated 1120 path := filepath.Join(harness.taskDir, file1) 1121 raw, err := os.ReadFile(path) 1122 if err != nil { 1123 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 1124 } 1125 1126 if s := string(raw); s != content1_2 { 1127 t.Fatalf("Unexpected template data; got %q, want %q", s, content1_2) 1128 } 1129 } 1130 1131 func TestTaskTemplateManager_Interpolate_Destination(t *testing.T) { 1132 ci.Parallel(t) 1133 // Make a template that will have its destination interpolated 1134 content := "hello, world!" 1135 file := "${node.unique.id}.tmpl" 1136 template := &structs.Template{ 1137 EmbeddedTmpl: content, 1138 DestPath: file, 1139 ChangeMode: structs.TemplateChangeModeNoop, 1140 } 1141 1142 harness := newTestHarness(t, []*structs.Template{template}, false, false) 1143 harness.start(t) 1144 defer harness.stop() 1145 1146 // Ensure unblock 1147 select { 1148 case <-harness.mockHooks.UnblockCh: 1149 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 1150 t.Fatalf("Task unblock should have been called") 1151 } 1152 1153 // Check the file is there 1154 actual := fmt.Sprintf("%s.tmpl", harness.node.ID) 1155 path := filepath.Join(harness.taskDir, actual) 1156 raw, err := os.ReadFile(path) 1157 if err != nil { 1158 t.Fatalf("Failed to read rendered template from %q: %v", path, err) 1159 } 1160 1161 if s := string(raw); s != content { 1162 t.Fatalf("Unexpected template data; got %q, want %q", s, content) 1163 } 1164 } 1165 1166 func TestTaskTemplateManager_Signal_Error(t *testing.T) { 1167 ci.Parallel(t) 1168 require := require.New(t) 1169 1170 // Make a template that renders based on a key in Consul and sends SIGALRM 1171 key1 := "foo" 1172 content1 := "bar" 1173 content2 := "baz" 1174 embedded1 := fmt.Sprintf(`{{key "%s"}}`, key1) 1175 file1 := "my.tmpl" 1176 template := &structs.Template{ 1177 EmbeddedTmpl: embedded1, 1178 DestPath: file1, 1179 ChangeMode: structs.TemplateChangeModeSignal, 1180 ChangeSignal: "SIGALRM", 1181 } 1182 1183 harness := newTestHarness(t, []*structs.Template{template}, true, false) 1184 harness.start(t) 1185 defer harness.stop() 1186 1187 harness.mockHooks.SignalError = fmt.Errorf("test error") 1188 1189 // Write the key to Consul 1190 harness.consul.SetKV(t, key1, []byte(content1)) 1191 1192 // Wait a little 1193 select { 1194 case <-harness.mockHooks.UnblockCh: 1195 case <-time.After(time.Duration(2*testutil.TestMultiplier()) * time.Second): 1196 t.Fatalf("Should have received unblock: %+v", harness.mockHooks) 1197 } 1198 1199 // Write the key to Consul 1200 harness.consul.SetKV(t, key1, []byte(content2)) 1201 1202 // Wait for kill channel 1203 select { 1204 case <-harness.mockHooks.KillCh: 1205 break 1206 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 1207 t.Fatalf("Should have received a signals: %+v", harness.mockHooks) 1208 } 1209 1210 require.NotNil(harness.mockHooks.KillEvent) 1211 require.Contains(harness.mockHooks.KillEvent.DisplayMessage, "failed to send signals") 1212 } 1213 1214 func TestTaskTemplateManager_ScriptExecution(t *testing.T) { 1215 ci.Parallel(t) 1216 1217 // Make a template that renders based on a key in Consul and triggers script 1218 key1 := "bam" 1219 key2 := "bar" 1220 content1_1 := "cat" 1221 content1_2 := "dog" 1222 t1 := &structs.Template{ 1223 EmbeddedTmpl: ` 1224 FOO={{key "bam"}} 1225 `, 1226 DestPath: "test.env", 1227 ChangeMode: structs.TemplateChangeModeScript, 1228 ChangeScript: &structs.ChangeScript{ 1229 Command: "/bin/foo", 1230 Args: []string{}, 1231 Timeout: 5 * time.Second, 1232 FailOnError: false, 1233 }, 1234 Envvars: true, 1235 } 1236 t2 := &structs.Template{ 1237 EmbeddedTmpl: ` 1238 BAR={{key "bar"}} 1239 `, 1240 DestPath: "test2.env", 1241 ChangeMode: structs.TemplateChangeModeScript, 1242 ChangeScript: &structs.ChangeScript{ 1243 Command: "/bin/foo", 1244 Args: []string{}, 1245 Timeout: 5 * time.Second, 1246 FailOnError: false, 1247 }, 1248 Envvars: true, 1249 } 1250 1251 me := mockExecutor{DesiredExit: 0, DesiredErr: nil} 1252 harness := newTestHarness(t, []*structs.Template{t1, t2}, true, false) 1253 harness.start(t) 1254 harness.manager.SetDriverHandle(&me) 1255 defer harness.stop() 1256 1257 // Ensure no unblock 1258 select { 1259 case <-harness.mockHooks.UnblockCh: 1260 require.Fail(t, "Task unblock should not have been called") 1261 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 1262 } 1263 1264 // Write the key to Consul 1265 harness.consul.SetKV(t, key1, []byte(content1_1)) 1266 harness.consul.SetKV(t, key2, []byte(content1_1)) 1267 1268 // Wait for the unblock 1269 select { 1270 case <-harness.mockHooks.UnblockCh: 1271 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 1272 require.Fail(t, "Task unblock should have been called") 1273 } 1274 1275 // Update the keys in Consul 1276 harness.consul.SetKV(t, key1, []byte(content1_2)) 1277 1278 // Wait for script execution 1279 timeout := time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second) 1280 OUTER: 1281 for { 1282 select { 1283 case <-harness.mockHooks.RestartCh: 1284 require.Fail(t, "restart not expected") 1285 case ev := <-harness.mockHooks.EmitEventCh: 1286 if strings.Contains(ev.DisplayMessage, t1.ChangeScript.Command) { 1287 break OUTER 1288 } 1289 case <-harness.mockHooks.SignalCh: 1290 require.Fail(t, "signal not expected") 1291 case <-timeout: 1292 require.Fail(t, "should have received an event") 1293 } 1294 } 1295 } 1296 1297 // TestTaskTemplateManager_ScriptExecutionFailTask tests whether we fail the 1298 // task upon script execution failure if that's how it's configured. 1299 func TestTaskTemplateManager_ScriptExecutionFailTask(t *testing.T) { 1300 ci.Parallel(t) 1301 require := require.New(t) 1302 1303 // Make a template that renders based on a key in Consul and triggers script 1304 key1 := "bam" 1305 key2 := "bar" 1306 content1_1 := "cat" 1307 content1_2 := "dog" 1308 t1 := &structs.Template{ 1309 EmbeddedTmpl: ` 1310 FOO={{key "bam"}} 1311 `, 1312 DestPath: "test.env", 1313 ChangeMode: structs.TemplateChangeModeScript, 1314 ChangeScript: &structs.ChangeScript{ 1315 Command: "/bin/foo", 1316 Args: []string{}, 1317 Timeout: 5 * time.Second, 1318 FailOnError: true, 1319 }, 1320 Envvars: true, 1321 } 1322 t2 := &structs.Template{ 1323 EmbeddedTmpl: ` 1324 BAR={{key "bar"}} 1325 `, 1326 DestPath: "test2.env", 1327 ChangeMode: structs.TemplateChangeModeScript, 1328 ChangeScript: &structs.ChangeScript{ 1329 Command: "/bin/foo", 1330 Args: []string{}, 1331 Timeout: 5 * time.Second, 1332 FailOnError: false, 1333 }, 1334 Envvars: true, 1335 } 1336 1337 me := mockExecutor{DesiredExit: 1, DesiredErr: fmt.Errorf("Script failed")} 1338 harness := newTestHarness(t, []*structs.Template{t1, t2}, true, false) 1339 harness.start(t) 1340 harness.manager.SetDriverHandle(&me) 1341 defer harness.stop() 1342 1343 // Ensure no unblock 1344 select { 1345 case <-harness.mockHooks.UnblockCh: 1346 require.Fail("Task unblock should not have been called") 1347 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 1348 } 1349 1350 // Write the key to Consul 1351 harness.consul.SetKV(t, key1, []byte(content1_1)) 1352 harness.consul.SetKV(t, key2, []byte(content1_1)) 1353 1354 // Wait for the unblock 1355 select { 1356 case <-harness.mockHooks.UnblockCh: 1357 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 1358 require.Fail("Task unblock should have been called") 1359 } 1360 1361 // Update the keys in Consul 1362 harness.consul.SetKV(t, key1, []byte(content1_2)) 1363 1364 // Wait for kill channel 1365 select { 1366 case <-harness.mockHooks.KillCh: 1367 break 1368 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 1369 require.Fail("Should have received a signals: %+v", harness.mockHooks) 1370 } 1371 1372 require.NotNil(harness.mockHooks.KillEvent) 1373 require.Contains(harness.mockHooks.KillEvent.DisplayMessage, "task is being killed") 1374 } 1375 1376 func TestTaskTemplateManager_ChangeModeMixed(t *testing.T) { 1377 ci.Parallel(t) 1378 1379 templateRestart := &structs.Template{ 1380 EmbeddedTmpl: ` 1381 RESTART={{key "restart"}} 1382 COMMON={{key "common"}} 1383 `, 1384 DestPath: "restart", 1385 ChangeMode: structs.TemplateChangeModeRestart, 1386 } 1387 templateSignal := &structs.Template{ 1388 EmbeddedTmpl: ` 1389 SIGNAL={{key "signal"}} 1390 COMMON={{key "common"}} 1391 `, 1392 DestPath: "signal", 1393 ChangeMode: structs.TemplateChangeModeSignal, 1394 ChangeSignal: "SIGALRM", 1395 } 1396 templateScript := &structs.Template{ 1397 EmbeddedTmpl: ` 1398 SCRIPT={{key "script"}} 1399 COMMON={{key "common"}} 1400 `, 1401 DestPath: "script", 1402 ChangeMode: structs.TemplateChangeModeScript, 1403 ChangeScript: &structs.ChangeScript{ 1404 Command: "/bin/foo", 1405 Args: []string{}, 1406 Timeout: 5 * time.Second, 1407 FailOnError: true, 1408 }, 1409 } 1410 templates := []*structs.Template{ 1411 templateRestart, 1412 templateSignal, 1413 templateScript, 1414 } 1415 1416 me := mockExecutor{DesiredExit: 0, DesiredErr: nil} 1417 harness := newTestHarness(t, templates, true, false) 1418 harness.start(t) 1419 harness.manager.SetDriverHandle(&me) 1420 defer harness.stop() 1421 1422 // Ensure no unblock 1423 select { 1424 case <-harness.mockHooks.UnblockCh: 1425 require.Fail(t, "Task unblock should not have been called") 1426 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 1427 } 1428 1429 // Write the key to Consul 1430 harness.consul.SetKV(t, "common", []byte(fmt.Sprintf("%v", time.Now()))) 1431 harness.consul.SetKV(t, "restart", []byte(fmt.Sprintf("%v", time.Now()))) 1432 harness.consul.SetKV(t, "signal", []byte(fmt.Sprintf("%v", time.Now()))) 1433 harness.consul.SetKV(t, "script", []byte(fmt.Sprintf("%v", time.Now()))) 1434 1435 // Wait for the unblock 1436 select { 1437 case <-harness.mockHooks.UnblockCh: 1438 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 1439 require.Fail(t, "Task unblock should have been called") 1440 } 1441 1442 t.Run("restart takes precedence", func(t *testing.T) { 1443 // Update the common Consul key. 1444 harness.consul.SetKV(t, "common", []byte(fmt.Sprintf("%v", time.Now()))) 1445 1446 // Collect some events. 1447 timeout := time.After(time.Duration(3*testutil.TestMultiplier()) * time.Second) 1448 events := []*structs.TaskEvent{} 1449 OUTER: 1450 for { 1451 select { 1452 case <-harness.mockHooks.RestartCh: 1453 // Consume restarts so the channel is clean for other tests. 1454 case <-harness.mockHooks.SignalCh: 1455 require.Fail(t, "signal not expected") 1456 case ev := <-harness.mockHooks.EmitEventCh: 1457 events = append(events, ev) 1458 case <-timeout: 1459 break OUTER 1460 } 1461 } 1462 1463 for _, ev := range events { 1464 require.NotContains(t, ev.DisplayMessage, templateScript.ChangeScript.Command) 1465 require.NotContains(t, ev.Type, structs.TaskSignaling) 1466 } 1467 }) 1468 1469 t.Run("signal and script", func(t *testing.T) { 1470 // Update the signal and script Consul keys. 1471 harness.consul.SetKV(t, "signal", []byte(fmt.Sprintf("%v", time.Now()))) 1472 harness.consul.SetKV(t, "script", []byte(fmt.Sprintf("%v", time.Now()))) 1473 1474 // Wait for a events. 1475 var gotSignal, gotScript bool 1476 timeout := time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second) 1477 for { 1478 select { 1479 case <-harness.mockHooks.RestartCh: 1480 require.Fail(t, "restart not expected") 1481 case ev := <-harness.mockHooks.EmitEventCh: 1482 if strings.Contains(ev.DisplayMessage, templateScript.ChangeScript.Command) { 1483 // Make sure we only run script once. 1484 require.False(t, gotScript) 1485 gotScript = true 1486 } 1487 case <-harness.mockHooks.SignalCh: 1488 // Make sure we only signal once. 1489 require.False(t, gotSignal) 1490 gotSignal = true 1491 case <-timeout: 1492 require.Fail(t, "timeout waiting for script and signal") 1493 } 1494 1495 if gotScript && gotSignal { 1496 break 1497 } 1498 } 1499 }) 1500 } 1501 1502 // TestTaskTemplateManager_FiltersProcessEnvVars asserts that we only render 1503 // environment variables found in task env-vars and not read the nomad host 1504 // process environment variables. nomad host process environment variables 1505 // are to be treated the same as not found environment variables. 1506 func TestTaskTemplateManager_FiltersEnvVars(t *testing.T) { 1507 1508 t.Setenv("NOMAD_TASK_NAME", "should be overridden by task") 1509 1510 testenv := "TESTENV_" + strings.ReplaceAll(uuid.Generate(), "-", "") 1511 t.Setenv(testenv, "MY_TEST_VALUE") 1512 1513 // Make a template that will render immediately 1514 content := `Hello Nomad Task: {{env "NOMAD_TASK_NAME"}} 1515 TEST_ENV: {{ env "` + testenv + `" }} 1516 TEST_ENV_NOT_FOUND: {{env "` + testenv + `_NOTFOUND" }}` 1517 expected := fmt.Sprintf("Hello Nomad Task: %s\nTEST_ENV: \nTEST_ENV_NOT_FOUND: ", TestTaskName) 1518 1519 file := "my.tmpl" 1520 template := &structs.Template{ 1521 EmbeddedTmpl: content, 1522 DestPath: file, 1523 ChangeMode: structs.TemplateChangeModeNoop, 1524 } 1525 1526 harness := newTestHarness(t, []*structs.Template{template}, false, false) 1527 harness.start(t) 1528 defer harness.stop() 1529 1530 // Wait for the unblock 1531 select { 1532 case <-harness.mockHooks.UnblockCh: 1533 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 1534 require.Fail(t, "Task unblock should have been called") 1535 } 1536 1537 // Check the file is there 1538 path := filepath.Join(harness.taskDir, file) 1539 raw, err := os.ReadFile(path) 1540 require.NoError(t, err) 1541 1542 require.Equal(t, expected, string(raw)) 1543 } 1544 1545 // TestTaskTemplateManager_Env asserts templates with the env flag set are read 1546 // into the task's environment. 1547 func TestTaskTemplateManager_Env(t *testing.T) { 1548 ci.Parallel(t) 1549 template := &structs.Template{ 1550 EmbeddedTmpl: ` 1551 # Comment lines are ok 1552 1553 FOO=bar 1554 foo=123 1555 ANYTHING_goes=Spaces are=ok! 1556 `, 1557 DestPath: "test.env", 1558 ChangeMode: structs.TemplateChangeModeNoop, 1559 Envvars: true, 1560 } 1561 harness := newTestHarness(t, []*structs.Template{template}, true, false) 1562 harness.start(t) 1563 defer harness.stop() 1564 1565 // Wait a little 1566 select { 1567 case <-harness.mockHooks.UnblockCh: 1568 case <-time.After(time.Duration(2*testutil.TestMultiplier()) * time.Second): 1569 t.Fatalf("Should have received unblock: %+v", harness.mockHooks) 1570 } 1571 1572 // Validate environment 1573 env := harness.envBuilder.Build().Map() 1574 if len(env) < 3 { 1575 t.Fatalf("expected at least 3 env vars but found %d:\n%#v\n", len(env), env) 1576 } 1577 if env["FOO"] != "bar" { 1578 t.Errorf("expected FOO=bar but found %q", env["FOO"]) 1579 } 1580 if env["foo"] != "123" { 1581 t.Errorf("expected foo=123 but found %q", env["foo"]) 1582 } 1583 if env["ANYTHING_goes"] != "Spaces are=ok!" { 1584 t.Errorf("expected ANYTHING_GOES='Spaces are ok!' but found %q", env["ANYTHING_goes"]) 1585 } 1586 } 1587 1588 // TestTaskTemplateManager_Env_Missing asserts the core env 1589 // template processing function returns errors when files don't exist 1590 func TestTaskTemplateManager_Env_Missing(t *testing.T) { 1591 ci.Parallel(t) 1592 d := t.TempDir() 1593 1594 // Fake writing the file so we don't have to run the whole template manager 1595 err := os.WriteFile(filepath.Join(d, "exists.env"), []byte("FOO=bar\n"), 0644) 1596 if err != nil { 1597 t.Fatalf("error writing template file: %v", err) 1598 } 1599 1600 templates := []*structs.Template{ 1601 { 1602 EmbeddedTmpl: "FOO=bar\n", 1603 DestPath: "exists.env", 1604 Envvars: true, 1605 }, 1606 { 1607 EmbeddedTmpl: "WHAT=ever\n", 1608 DestPath: "missing.env", 1609 Envvars: true, 1610 }, 1611 } 1612 1613 taskEnv := taskenv.NewEmptyBuilder().SetClientTaskRoot(d).Build() 1614 if vars, err := loadTemplateEnv(templates, taskEnv); err == nil { 1615 t.Fatalf("expected an error but instead got env vars: %#v", vars) 1616 } 1617 } 1618 1619 // TestTaskTemplateManager_Env_InterpolatedDest asserts the core env 1620 // template processing function handles interpolated destinations 1621 func TestTaskTemplateManager_Env_InterpolatedDest(t *testing.T) { 1622 ci.Parallel(t) 1623 require := require.New(t) 1624 1625 d := t.TempDir() 1626 1627 // Fake writing the file so we don't have to run the whole template manager 1628 err := os.WriteFile(filepath.Join(d, "exists.env"), []byte("FOO=bar\n"), 0644) 1629 if err != nil { 1630 t.Fatalf("error writing template file: %v", err) 1631 } 1632 1633 templates := []*structs.Template{ 1634 { 1635 EmbeddedTmpl: "FOO=bar\n", 1636 DestPath: "${NOMAD_META_path}.env", 1637 Envvars: true, 1638 }, 1639 } 1640 1641 // Build the env 1642 taskEnv := taskenv.NewTaskEnv( 1643 map[string]string{"NOMAD_META_path": "exists"}, 1644 map[string]string{"NOMAD_META_path": "exists"}, 1645 map[string]string{}, 1646 map[string]string{}, 1647 d, "") 1648 1649 vars, err := loadTemplateEnv(templates, taskEnv) 1650 require.NoError(err) 1651 require.Contains(vars, "FOO") 1652 require.Equal(vars["FOO"], "bar") 1653 } 1654 1655 // TestTaskTemplateManager_Env_Multi asserts the core env 1656 // template processing function returns combined env vars from multiple 1657 // templates correctly. 1658 func TestTaskTemplateManager_Env_Multi(t *testing.T) { 1659 ci.Parallel(t) 1660 d := t.TempDir() 1661 1662 // Fake writing the files so we don't have to run the whole template manager 1663 err := os.WriteFile(filepath.Join(d, "zzz.env"), []byte("FOO=bar\nSHARED=nope\n"), 0644) 1664 if err != nil { 1665 t.Fatalf("error writing template file 1: %v", err) 1666 } 1667 err = os.WriteFile(filepath.Join(d, "aaa.env"), []byte("BAR=foo\nSHARED=yup\n"), 0644) 1668 if err != nil { 1669 t.Fatalf("error writing template file 2: %v", err) 1670 } 1671 1672 // Templates will get loaded in order (not alpha sorted) 1673 templates := []*structs.Template{ 1674 { 1675 DestPath: "zzz.env", 1676 Envvars: true, 1677 }, 1678 { 1679 DestPath: "aaa.env", 1680 Envvars: true, 1681 }, 1682 } 1683 1684 taskEnv := taskenv.NewEmptyBuilder().SetClientTaskRoot(d).Build() 1685 vars, err := loadTemplateEnv(templates, taskEnv) 1686 if err != nil { 1687 t.Fatalf("expected no error: %v", err) 1688 } 1689 if vars["FOO"] != "bar" { 1690 t.Errorf("expected FOO=bar but found %q", vars["FOO"]) 1691 } 1692 if vars["BAR"] != "foo" { 1693 t.Errorf("expected BAR=foo but found %q", vars["BAR"]) 1694 } 1695 if vars["SHARED"] != "yup" { 1696 t.Errorf("expected FOO=bar but found %q", vars["yup"]) 1697 } 1698 } 1699 1700 func TestTaskTemplateManager_Rerender_Env(t *testing.T) { 1701 ci.Parallel(t) 1702 // Make a template that renders based on a key in Consul and sends restart 1703 key1 := "bam" 1704 key2 := "bar" 1705 content1_1 := "cat" 1706 content1_2 := "dog" 1707 t1 := &structs.Template{ 1708 EmbeddedTmpl: ` 1709 FOO={{key "bam"}} 1710 `, 1711 DestPath: "test.env", 1712 ChangeMode: structs.TemplateChangeModeRestart, 1713 Envvars: true, 1714 } 1715 t2 := &structs.Template{ 1716 EmbeddedTmpl: ` 1717 BAR={{key "bar"}} 1718 `, 1719 DestPath: "test2.env", 1720 ChangeMode: structs.TemplateChangeModeRestart, 1721 Envvars: true, 1722 } 1723 1724 harness := newTestHarness(t, []*structs.Template{t1, t2}, true, false) 1725 harness.start(t) 1726 defer harness.stop() 1727 1728 // Ensure no unblock 1729 select { 1730 case <-harness.mockHooks.UnblockCh: 1731 t.Fatalf("Task unblock should have not have been called") 1732 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 1733 } 1734 1735 // Write the key to Consul 1736 harness.consul.SetKV(t, key1, []byte(content1_1)) 1737 harness.consul.SetKV(t, key2, []byte(content1_1)) 1738 1739 // Wait for the unblock 1740 select { 1741 case <-harness.mockHooks.UnblockCh: 1742 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 1743 t.Fatalf("Task unblock should have been called") 1744 } 1745 1746 env := harness.envBuilder.Build().Map() 1747 if v, ok := env["FOO"]; !ok || v != content1_1 { 1748 t.Fatalf("Bad env for FOO: %v %v", v, ok) 1749 } 1750 if v, ok := env["BAR"]; !ok || v != content1_1 { 1751 t.Fatalf("Bad env for BAR: %v %v", v, ok) 1752 } 1753 1754 // Update the keys in Consul 1755 harness.consul.SetKV(t, key1, []byte(content1_2)) 1756 1757 // Wait for restart 1758 timeout := time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second) 1759 OUTER: 1760 for { 1761 select { 1762 case <-harness.mockHooks.RestartCh: 1763 break OUTER 1764 case <-harness.mockHooks.SignalCh: 1765 t.Fatalf("Signal with restart policy: %+v", harness.mockHooks) 1766 case <-timeout: 1767 t.Fatalf("Should have received a restart: %+v", harness.mockHooks) 1768 } 1769 } 1770 1771 env = harness.envBuilder.Build().Map() 1772 if v, ok := env["FOO"]; !ok || v != content1_2 { 1773 t.Fatalf("Bad env for FOO: %v %v", v, ok) 1774 } 1775 if v, ok := env["BAR"]; !ok || v != content1_1 { 1776 t.Fatalf("Bad env for BAR: %v %v", v, ok) 1777 } 1778 } 1779 1780 // TestTaskTemplateManager_Config_ServerName asserts the tls_server_name 1781 // setting is propagated to consul-template's configuration. See #2776 1782 func TestTaskTemplateManager_Config_ServerName(t *testing.T) { 1783 ci.Parallel(t) 1784 c := config.DefaultConfig() 1785 c.Node = mock.Node() 1786 c.VaultConfig = &sconfig.VaultConfig{ 1787 Enabled: pointer.Of(true), 1788 Addr: "https://localhost/", 1789 TLSServerName: "notlocalhost", 1790 } 1791 config := &TaskTemplateManagerConfig{ 1792 ClientConfig: c, 1793 VaultToken: "token", 1794 } 1795 ctconf, err := newRunnerConfig(config, nil) 1796 if err != nil { 1797 t.Fatalf("unexpected error: %v", err) 1798 } 1799 1800 if *ctconf.Vault.SSL.ServerName != c.VaultConfig.TLSServerName { 1801 t.Fatalf("expected %q but found %q", c.VaultConfig.TLSServerName, *ctconf.Vault.SSL.ServerName) 1802 } 1803 } 1804 1805 // TestTaskTemplateManager_Config_VaultNamespace asserts the Vault namespace setting is 1806 // propagated to consul-template's configuration. 1807 func TestTaskTemplateManager_Config_VaultNamespace(t *testing.T) { 1808 ci.Parallel(t) 1809 assert := assert.New(t) 1810 1811 testNS := "test-namespace" 1812 c := config.DefaultConfig() 1813 c.Node = mock.Node() 1814 c.VaultConfig = &sconfig.VaultConfig{ 1815 Enabled: pointer.Of(true), 1816 Addr: "https://localhost/", 1817 TLSServerName: "notlocalhost", 1818 Namespace: testNS, 1819 } 1820 1821 alloc := mock.Alloc() 1822 config := &TaskTemplateManagerConfig{ 1823 ClientConfig: c, 1824 VaultToken: "token", 1825 EnvBuilder: taskenv.NewBuilder(c.Node, alloc, alloc.Job.TaskGroups[0].Tasks[0], c.Region), 1826 } 1827 1828 ctmplMapping, err := parseTemplateConfigs(config) 1829 assert.Nil(err, "Parsing Templates") 1830 1831 ctconf, err := newRunnerConfig(config, ctmplMapping) 1832 assert.Nil(err, "Building Runner Config") 1833 assert.Equal(testNS, *ctconf.Vault.Namespace, "Vault Namespace Value") 1834 } 1835 1836 // TestTaskTemplateManager_Config_VaultNamespace asserts the Vault namespace setting is 1837 // propagated to consul-template's configuration. 1838 func TestTaskTemplateManager_Config_VaultNamespace_TaskOverride(t *testing.T) { 1839 ci.Parallel(t) 1840 assert := assert.New(t) 1841 1842 testNS := "test-namespace" 1843 c := config.DefaultConfig() 1844 c.Node = mock.Node() 1845 c.VaultConfig = &sconfig.VaultConfig{ 1846 Enabled: pointer.Of(true), 1847 Addr: "https://localhost/", 1848 TLSServerName: "notlocalhost", 1849 Namespace: testNS, 1850 } 1851 1852 alloc := mock.Alloc() 1853 overriddenNS := "new-namespace" 1854 1855 // Set the template manager config vault namespace 1856 config := &TaskTemplateManagerConfig{ 1857 ClientConfig: c, 1858 VaultToken: "token", 1859 VaultNamespace: overriddenNS, 1860 EnvBuilder: taskenv.NewBuilder(c.Node, alloc, alloc.Job.TaskGroups[0].Tasks[0], c.Region), 1861 } 1862 1863 ctmplMapping, err := parseTemplateConfigs(config) 1864 assert.Nil(err, "Parsing Templates") 1865 1866 ctconf, err := newRunnerConfig(config, ctmplMapping) 1867 assert.Nil(err, "Building Runner Config") 1868 assert.Equal(overriddenNS, *ctconf.Vault.Namespace, "Vault Namespace Value") 1869 } 1870 1871 // TestTaskTemplateManager_Escapes asserts that when sandboxing is enabled 1872 // interpolated paths are not incorrectly treated as escaping the alloc dir. 1873 func TestTaskTemplateManager_Escapes(t *testing.T) { 1874 ci.Parallel(t) 1875 1876 clientConf := config.DefaultConfig() 1877 require.False(t, clientConf.TemplateConfig.DisableSandbox, "expected sandbox to be disabled") 1878 1879 // Set a fake alloc dir to make test output more realistic 1880 clientConf.AllocDir = "/fake/allocdir" 1881 1882 clientConf.Node = mock.Node() 1883 alloc := mock.Alloc() 1884 task := alloc.Job.TaskGroups[0].Tasks[0] 1885 logger := testlog.HCLogger(t) 1886 allocDir := allocdir.NewAllocDir(logger, clientConf.AllocDir, alloc.ID) 1887 taskDir := allocDir.NewTaskDir(task.Name) 1888 1889 containerEnv := func() *taskenv.Builder { 1890 // To emulate a Docker or exec tasks we must copy the 1891 // Set{Alloc,Task,Secrets}Dir logic in taskrunner/task_dir_hook.go 1892 b := taskenv.NewBuilder(clientConf.Node, alloc, task, clientConf.Region) 1893 b.SetAllocDir(allocdir.SharedAllocContainerPath) 1894 b.SetTaskLocalDir(allocdir.TaskLocalContainerPath) 1895 b.SetSecretsDir(allocdir.TaskSecretsContainerPath) 1896 b.SetClientTaskRoot(taskDir.Dir) 1897 b.SetClientSharedAllocDir(taskDir.SharedAllocDir) 1898 b.SetClientTaskLocalDir(taskDir.LocalDir) 1899 b.SetClientTaskSecretsDir(taskDir.SecretsDir) 1900 return b 1901 } 1902 1903 rawExecEnv := func() *taskenv.Builder { 1904 // To emulate a unisolated tasks we must copy the 1905 // Set{Alloc,Task,Secrets}Dir logic in taskrunner/task_dir_hook.go 1906 b := taskenv.NewBuilder(clientConf.Node, alloc, task, clientConf.Region) 1907 b.SetAllocDir(taskDir.SharedAllocDir) 1908 b.SetTaskLocalDir(taskDir.LocalDir) 1909 b.SetSecretsDir(taskDir.SecretsDir) 1910 b.SetClientTaskRoot(taskDir.Dir) 1911 b.SetClientSharedAllocDir(taskDir.SharedAllocDir) 1912 b.SetClientTaskLocalDir(taskDir.LocalDir) 1913 b.SetClientTaskSecretsDir(taskDir.SecretsDir) 1914 return b 1915 } 1916 1917 cases := []struct { 1918 Name string 1919 Config func() *TaskTemplateManagerConfig 1920 1921 // Expected paths to be returned if Err is nil 1922 SourcePath string 1923 DestPath string 1924 1925 // Err is the expected error to be returned or nil 1926 Err error 1927 }{ 1928 { 1929 Name: "ContainerOk", 1930 Config: func() *TaskTemplateManagerConfig { 1931 return &TaskTemplateManagerConfig{ 1932 ClientConfig: clientConf, 1933 TaskDir: taskDir.Dir, 1934 EnvBuilder: containerEnv(), 1935 Templates: []*structs.Template{ 1936 { 1937 SourcePath: "${NOMAD_TASK_DIR}/src", 1938 DestPath: "${NOMAD_SECRETS_DIR}/dst", 1939 }, 1940 }, 1941 } 1942 }, 1943 SourcePath: filepath.Join(taskDir.Dir, "local/src"), 1944 DestPath: filepath.Join(taskDir.Dir, "secrets/dst"), 1945 }, 1946 { 1947 Name: "ContainerSrcEscapesErr", 1948 Config: func() *TaskTemplateManagerConfig { 1949 return &TaskTemplateManagerConfig{ 1950 ClientConfig: clientConf, 1951 TaskDir: taskDir.Dir, 1952 EnvBuilder: containerEnv(), 1953 Templates: []*structs.Template{ 1954 { 1955 SourcePath: "/etc/src_escapes", 1956 DestPath: "${NOMAD_SECRETS_DIR}/dst", 1957 }, 1958 }, 1959 } 1960 }, 1961 Err: sourceEscapesErr, 1962 }, 1963 { 1964 Name: "ContainerSrcEscapesOk", 1965 Config: func() *TaskTemplateManagerConfig { 1966 unsafeConf := clientConf.Copy() 1967 unsafeConf.TemplateConfig.DisableSandbox = true 1968 return &TaskTemplateManagerConfig{ 1969 ClientConfig: unsafeConf, 1970 TaskDir: taskDir.Dir, 1971 EnvBuilder: containerEnv(), 1972 Templates: []*structs.Template{ 1973 { 1974 SourcePath: "/etc/src_escapes_ok", 1975 DestPath: "${NOMAD_SECRETS_DIR}/dst", 1976 }, 1977 }, 1978 } 1979 }, 1980 SourcePath: "/etc/src_escapes_ok", 1981 DestPath: filepath.Join(taskDir.Dir, "secrets/dst"), 1982 }, 1983 { 1984 Name: "ContainerDstAbsoluteOk", 1985 Config: func() *TaskTemplateManagerConfig { 1986 return &TaskTemplateManagerConfig{ 1987 ClientConfig: clientConf, 1988 TaskDir: taskDir.Dir, 1989 EnvBuilder: containerEnv(), 1990 Templates: []*structs.Template{ 1991 { 1992 SourcePath: "${NOMAD_TASK_DIR}/src", 1993 DestPath: "/etc/absolutely_relative", 1994 }, 1995 }, 1996 } 1997 }, 1998 SourcePath: filepath.Join(taskDir.Dir, "local/src"), 1999 DestPath: filepath.Join(taskDir.Dir, "etc/absolutely_relative"), 2000 }, 2001 { 2002 Name: "ContainerDstAbsoluteEscapesErr", 2003 Config: func() *TaskTemplateManagerConfig { 2004 return &TaskTemplateManagerConfig{ 2005 ClientConfig: clientConf, 2006 TaskDir: taskDir.Dir, 2007 EnvBuilder: containerEnv(), 2008 Templates: []*structs.Template{ 2009 { 2010 SourcePath: "${NOMAD_TASK_DIR}/src", 2011 DestPath: "../escapes", 2012 }, 2013 }, 2014 } 2015 }, 2016 Err: destEscapesErr, 2017 }, 2018 { 2019 Name: "ContainerDstAbsoluteEscapesOk", 2020 Config: func() *TaskTemplateManagerConfig { 2021 unsafeConf := clientConf.Copy() 2022 unsafeConf.TemplateConfig.DisableSandbox = true 2023 return &TaskTemplateManagerConfig{ 2024 ClientConfig: unsafeConf, 2025 TaskDir: taskDir.Dir, 2026 EnvBuilder: containerEnv(), 2027 Templates: []*structs.Template{ 2028 { 2029 SourcePath: "${NOMAD_TASK_DIR}/src", 2030 DestPath: "../escapes", 2031 }, 2032 }, 2033 } 2034 }, 2035 SourcePath: filepath.Join(taskDir.Dir, "local/src"), 2036 DestPath: filepath.Join(taskDir.Dir, "..", "escapes"), 2037 }, 2038 //TODO: Fix this test. I *think* it should pass. The double 2039 // joining of the task dir onto the destination seems like 2040 // a bug. https://github.com/hashicorp/nomad/issues/9389 2041 { 2042 Name: "RawExecOk", 2043 Config: func() *TaskTemplateManagerConfig { 2044 return &TaskTemplateManagerConfig{ 2045 ClientConfig: clientConf, 2046 TaskDir: taskDir.Dir, 2047 EnvBuilder: rawExecEnv(), 2048 Templates: []*structs.Template{ 2049 { 2050 SourcePath: "${NOMAD_TASK_DIR}/src", 2051 DestPath: "${NOMAD_SECRETS_DIR}/dst", 2052 }, 2053 }, 2054 } 2055 }, 2056 SourcePath: filepath.Join(taskDir.Dir, "local/src"), 2057 DestPath: filepath.Join(taskDir.Dir, "secrets/dst"), 2058 }, 2059 { 2060 Name: "RawExecSrcEscapesErr", 2061 Config: func() *TaskTemplateManagerConfig { 2062 return &TaskTemplateManagerConfig{ 2063 ClientConfig: clientConf, 2064 TaskDir: taskDir.Dir, 2065 EnvBuilder: rawExecEnv(), 2066 Templates: []*structs.Template{ 2067 { 2068 SourcePath: "/etc/src_escapes", 2069 DestPath: "${NOMAD_SECRETS_DIR}/dst", 2070 }, 2071 }, 2072 } 2073 }, 2074 Err: sourceEscapesErr, 2075 }, 2076 { 2077 Name: "RawExecDstAbsoluteOk", 2078 Config: func() *TaskTemplateManagerConfig { 2079 return &TaskTemplateManagerConfig{ 2080 ClientConfig: clientConf, 2081 TaskDir: taskDir.Dir, 2082 EnvBuilder: rawExecEnv(), 2083 Templates: []*structs.Template{ 2084 { 2085 SourcePath: "${NOMAD_TASK_DIR}/src", 2086 DestPath: "/etc/absolutely_relative", 2087 }, 2088 }, 2089 } 2090 }, 2091 SourcePath: filepath.Join(taskDir.Dir, "local/src"), 2092 DestPath: filepath.Join(taskDir.Dir, "etc/absolutely_relative"), 2093 }, 2094 } 2095 2096 for i := range cases { 2097 tc := cases[i] 2098 t.Run(tc.Name, func(t *testing.T) { 2099 config := tc.Config() 2100 mapping, err := parseTemplateConfigs(config) 2101 if tc.Err == nil { 2102 // Ok path 2103 require.NoError(t, err) 2104 require.NotNil(t, mapping) 2105 require.Len(t, mapping, 1) 2106 for k := range mapping { 2107 require.Equal(t, tc.SourcePath, *k.Source) 2108 require.Equal(t, tc.DestPath, *k.Destination) 2109 t.Logf("Rendering %s => %s", *k.Source, *k.Destination) 2110 } 2111 } else { 2112 // Err path 2113 assert.EqualError(t, err, tc.Err.Error()) 2114 require.Nil(t, mapping) 2115 } 2116 2117 }) 2118 } 2119 } 2120 2121 func TestTaskTemplateManager_BlockedEvents(t *testing.T) { 2122 // The tests sets a template that need keys 0, 1, 2, 3, 4, 2123 // then subsequently sets 0, 1, 2 keys 2124 // then asserts that templates are still blocked on 3 and 4, 2125 // and check that we got the relevant task events 2126 ci.Parallel(t) 2127 require := require.New(t) 2128 2129 // Make a template that will render based on a key in Consul 2130 var embedded string 2131 for i := 0; i < 5; i++ { 2132 embedded += fmt.Sprintf(`{{key "%d"}}`, i) 2133 } 2134 2135 file := "my.tmpl" 2136 template := &structs.Template{ 2137 EmbeddedTmpl: embedded, 2138 DestPath: file, 2139 ChangeMode: structs.TemplateChangeModeNoop, 2140 } 2141 2142 harness := newTestHarness(t, []*structs.Template{template}, true, false) 2143 harness.setEmitRate(100 * time.Millisecond) 2144 harness.start(t) 2145 defer harness.stop() 2146 2147 missingKeys := func(e *structs.TaskEvent) ([]string, int) { 2148 missingRexp := regexp.MustCompile(`kv.block\(([0-9]*)\)`) 2149 moreRexp := regexp.MustCompile(`and ([0-9]*) more`) 2150 2151 missingMatch := missingRexp.FindAllStringSubmatch(e.DisplayMessage, -1) 2152 moreMatch := moreRexp.FindAllStringSubmatch(e.DisplayMessage, -1) 2153 2154 missing := make([]string, len(missingMatch)) 2155 for i, v := range missingMatch { 2156 missing[i] = v[1] 2157 } 2158 sort.Strings(missing) 2159 2160 more := 0 2161 if len(moreMatch) != 0 { 2162 more, _ = strconv.Atoi(moreMatch[0][1]) 2163 } 2164 return missing, more 2165 2166 } 2167 2168 // Ensure that we get a blocked event 2169 select { 2170 case <-harness.mockHooks.UnblockCh: 2171 t.Fatalf("Task unblock should have not have been called") 2172 case <-harness.mockHooks.EmitEventCh: 2173 case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): 2174 t.Fatalf("timeout") 2175 } 2176 2177 // Check to see we got a correct message 2178 // assert that all 0-4 keys are missing 2179 require.Len(harness.mockHooks.Events, 1) 2180 t.Logf("first message: %v", harness.mockHooks.Events[0]) 2181 missing, more := missingKeys(harness.mockHooks.Events[0]) 2182 require.Equal(5, len(missing)+more) 2183 require.Contains(harness.mockHooks.Events[0].DisplayMessage, "and 2 more") 2184 2185 // Write 0-2 keys to Consul 2186 for i := 0; i < 3; i++ { 2187 harness.consul.SetKV(t, fmt.Sprintf("%d", i), []byte{0xa}) 2188 } 2189 2190 // Ensure that we get a blocked event 2191 isExpectedFinalEvent := func(e *structs.TaskEvent) bool { 2192 missing, more := missingKeys(e) 2193 return reflect.DeepEqual(missing, []string{"3", "4"}) && more == 0 2194 } 2195 timeout := time.After(time.Second * time.Duration(testutil.TestMultiplier())) 2196 WAIT_LOOP: 2197 for { 2198 select { 2199 case <-harness.mockHooks.UnblockCh: 2200 t.Errorf("Task unblock should have not have been called") 2201 case e := <-harness.mockHooks.EmitEventCh: 2202 t.Logf("received event: %v", e.DisplayMessage) 2203 if isExpectedFinalEvent(e) { 2204 break WAIT_LOOP 2205 } 2206 case <-timeout: 2207 t.Errorf("timeout") 2208 } 2209 } 2210 2211 // Check to see we got a correct message 2212 event := harness.mockHooks.Events[len(harness.mockHooks.Events)-1] 2213 if !isExpectedFinalEvent(event) { 2214 t.Logf("received all events: %v", pretty.Sprint(harness.mockHooks.Events)) 2215 2216 t.Fatalf("bad event, expected only 3 and 5 blocked got: %q", event.DisplayMessage) 2217 } 2218 } 2219 2220 // TestTaskTemplateManager_ClientTemplateConfig_Set asserts that all client level 2221 // configuration is accurately mapped from the client to the TaskTemplateManager 2222 // and that any operator defined boundaries are enforced. 2223 func TestTaskTemplateManager_ClientTemplateConfig_Set(t *testing.T) { 2224 ci.Parallel(t) 2225 2226 testNS := "test-namespace" 2227 2228 clientConfig := config.DefaultConfig() 2229 clientConfig.Node = mock.Node() 2230 2231 clientConfig.VaultConfig = &sconfig.VaultConfig{ 2232 Enabled: pointer.Of(true), 2233 Namespace: testNS, 2234 } 2235 2236 clientConfig.ConsulConfig = &sconfig.ConsulConfig{ 2237 Namespace: testNS, 2238 } 2239 2240 // helper to reduce boilerplate 2241 waitConfig := &config.WaitConfig{ 2242 Min: pointer.Of(5 * time.Second), 2243 Max: pointer.Of(10 * time.Second), 2244 } 2245 // helper to reduce boilerplate 2246 retryConfig := &config.RetryConfig{ 2247 Attempts: pointer.Of(5), 2248 Backoff: pointer.Of(5 * time.Second), 2249 MaxBackoff: pointer.Of(20 * time.Second), 2250 } 2251 2252 clientConfig.TemplateConfig.MaxStale = pointer.Of(5 * time.Second) 2253 clientConfig.TemplateConfig.BlockQueryWaitTime = pointer.Of(60 * time.Second) 2254 clientConfig.TemplateConfig.Wait = waitConfig.Copy() 2255 clientConfig.TemplateConfig.ConsulRetry = retryConfig.Copy() 2256 clientConfig.TemplateConfig.VaultRetry = retryConfig.Copy() 2257 clientConfig.TemplateConfig.NomadRetry = retryConfig.Copy() 2258 2259 alloc := mock.Alloc() 2260 allocWithOverride := mock.Alloc() 2261 allocWithOverride.Job.TaskGroups[0].Tasks[0].Templates = []*structs.Template{ 2262 { 2263 Wait: &structs.WaitConfig{ 2264 Min: pointer.Of(2 * time.Second), 2265 Max: pointer.Of(12 * time.Second), 2266 }, 2267 }, 2268 } 2269 2270 cases := []struct { 2271 Name string 2272 ClientTemplateConfig *config.ClientTemplateConfig 2273 TTMConfig *TaskTemplateManagerConfig 2274 ExpectedRunnerConfig *config.Config 2275 ExpectedTemplateConfig *templateconfig.TemplateConfig 2276 }{ 2277 { 2278 "basic-wait-config", 2279 &config.ClientTemplateConfig{ 2280 MaxStale: pointer.Of(5 * time.Second), 2281 BlockQueryWaitTime: pointer.Of(60 * time.Second), 2282 Wait: waitConfig.Copy(), 2283 ConsulRetry: retryConfig.Copy(), 2284 VaultRetry: retryConfig.Copy(), 2285 NomadRetry: retryConfig.Copy(), 2286 }, 2287 &TaskTemplateManagerConfig{ 2288 ClientConfig: clientConfig, 2289 VaultToken: "token", 2290 EnvBuilder: taskenv.NewBuilder(clientConfig.Node, alloc, alloc.Job.TaskGroups[0].Tasks[0], clientConfig.Region), 2291 }, 2292 &config.Config{ 2293 TemplateConfig: &config.ClientTemplateConfig{ 2294 MaxStale: pointer.Of(5 * time.Second), 2295 BlockQueryWaitTime: pointer.Of(60 * time.Second), 2296 Wait: waitConfig.Copy(), 2297 ConsulRetry: retryConfig.Copy(), 2298 VaultRetry: retryConfig.Copy(), 2299 NomadRetry: retryConfig.Copy(), 2300 }, 2301 }, 2302 &templateconfig.TemplateConfig{ 2303 Wait: &templateconfig.WaitConfig{ 2304 Enabled: pointer.Of(true), 2305 Min: pointer.Of(5 * time.Second), 2306 Max: pointer.Of(10 * time.Second), 2307 }, 2308 }, 2309 }, 2310 { 2311 "template-override", 2312 &config.ClientTemplateConfig{ 2313 MaxStale: pointer.Of(5 * time.Second), 2314 BlockQueryWaitTime: pointer.Of(60 * time.Second), 2315 Wait: waitConfig.Copy(), 2316 ConsulRetry: retryConfig.Copy(), 2317 VaultRetry: retryConfig.Copy(), 2318 NomadRetry: retryConfig.Copy(), 2319 }, 2320 &TaskTemplateManagerConfig{ 2321 ClientConfig: clientConfig, 2322 VaultToken: "token", 2323 EnvBuilder: taskenv.NewBuilder(clientConfig.Node, allocWithOverride, allocWithOverride.Job.TaskGroups[0].Tasks[0], clientConfig.Region), 2324 }, 2325 &config.Config{ 2326 TemplateConfig: &config.ClientTemplateConfig{ 2327 MaxStale: pointer.Of(5 * time.Second), 2328 BlockQueryWaitTime: pointer.Of(60 * time.Second), 2329 Wait: waitConfig.Copy(), 2330 ConsulRetry: retryConfig.Copy(), 2331 VaultRetry: retryConfig.Copy(), 2332 NomadRetry: retryConfig.Copy(), 2333 }, 2334 }, 2335 &templateconfig.TemplateConfig{ 2336 Wait: &templateconfig.WaitConfig{ 2337 Enabled: pointer.Of(true), 2338 Min: pointer.Of(2 * time.Second), 2339 Max: pointer.Of(12 * time.Second), 2340 }, 2341 }, 2342 }, 2343 { 2344 "bounds-override", 2345 &config.ClientTemplateConfig{ 2346 MaxStale: pointer.Of(5 * time.Second), 2347 BlockQueryWaitTime: pointer.Of(60 * time.Second), 2348 Wait: waitConfig.Copy(), 2349 WaitBounds: &config.WaitConfig{ 2350 Min: pointer.Of(3 * time.Second), 2351 Max: pointer.Of(11 * time.Second), 2352 }, 2353 ConsulRetry: retryConfig.Copy(), 2354 VaultRetry: retryConfig.Copy(), 2355 NomadRetry: retryConfig.Copy(), 2356 }, 2357 &TaskTemplateManagerConfig{ 2358 ClientConfig: clientConfig, 2359 VaultToken: "token", 2360 EnvBuilder: taskenv.NewBuilder(clientConfig.Node, allocWithOverride, allocWithOverride.Job.TaskGroups[0].Tasks[0], clientConfig.Region), 2361 Templates: []*structs.Template{ 2362 { 2363 Wait: &structs.WaitConfig{ 2364 Min: pointer.Of(2 * time.Second), 2365 Max: pointer.Of(12 * time.Second), 2366 }, 2367 }, 2368 }, 2369 }, 2370 &config.Config{ 2371 TemplateConfig: &config.ClientTemplateConfig{ 2372 MaxStale: pointer.Of(5 * time.Second), 2373 BlockQueryWaitTime: pointer.Of(60 * time.Second), 2374 Wait: waitConfig.Copy(), 2375 WaitBounds: &config.WaitConfig{ 2376 Min: pointer.Of(3 * time.Second), 2377 Max: pointer.Of(11 * time.Second), 2378 }, 2379 ConsulRetry: retryConfig.Copy(), 2380 VaultRetry: retryConfig.Copy(), 2381 NomadRetry: retryConfig.Copy(), 2382 }, 2383 }, 2384 &templateconfig.TemplateConfig{ 2385 Wait: &templateconfig.WaitConfig{ 2386 Enabled: pointer.Of(true), 2387 Min: pointer.Of(3 * time.Second), 2388 Max: pointer.Of(11 * time.Second), 2389 }, 2390 }, 2391 }, 2392 } 2393 2394 for _, _case := range cases { 2395 t.Run(_case.Name, func(t *testing.T) { 2396 // monkey patch the client config with the version of the ClientTemplateConfig we want to test. 2397 _case.TTMConfig.ClientConfig.TemplateConfig = _case.ClientTemplateConfig 2398 templateMapping, err := parseTemplateConfigs(_case.TTMConfig) 2399 require.NoError(t, err) 2400 2401 runnerConfig, err := newRunnerConfig(_case.TTMConfig, templateMapping) 2402 require.NoError(t, err) 2403 2404 // Direct properties 2405 require.Equal(t, *_case.ExpectedRunnerConfig.TemplateConfig.MaxStale, *runnerConfig.MaxStale) 2406 require.Equal(t, *_case.ExpectedRunnerConfig.TemplateConfig.BlockQueryWaitTime, *runnerConfig.BlockQueryWaitTime) 2407 // WaitConfig 2408 require.Equal(t, *_case.ExpectedRunnerConfig.TemplateConfig.Wait.Min, *runnerConfig.Wait.Min) 2409 require.Equal(t, *_case.ExpectedRunnerConfig.TemplateConfig.Wait.Max, *runnerConfig.Wait.Max) 2410 // Consul Retry 2411 require.NotNil(t, runnerConfig.Consul) 2412 require.NotNil(t, runnerConfig.Consul.Retry) 2413 require.Equal(t, *_case.ExpectedRunnerConfig.TemplateConfig.ConsulRetry.Attempts, *runnerConfig.Consul.Retry.Attempts) 2414 require.Equal(t, *_case.ExpectedRunnerConfig.TemplateConfig.ConsulRetry.Backoff, *runnerConfig.Consul.Retry.Backoff) 2415 require.Equal(t, *_case.ExpectedRunnerConfig.TemplateConfig.ConsulRetry.MaxBackoff, *runnerConfig.Consul.Retry.MaxBackoff) 2416 // Vault Retry 2417 require.NotNil(t, runnerConfig.Vault) 2418 require.NotNil(t, runnerConfig.Vault.Retry) 2419 require.Equal(t, *_case.ExpectedRunnerConfig.TemplateConfig.VaultRetry.Attempts, *runnerConfig.Vault.Retry.Attempts) 2420 require.Equal(t, *_case.ExpectedRunnerConfig.TemplateConfig.VaultRetry.Backoff, *runnerConfig.Vault.Retry.Backoff) 2421 require.Equal(t, *_case.ExpectedRunnerConfig.TemplateConfig.VaultRetry.MaxBackoff, *runnerConfig.Vault.Retry.MaxBackoff) 2422 // Nomad Retry 2423 require.NotNil(t, runnerConfig.Nomad) 2424 require.NotNil(t, runnerConfig.Nomad.Retry) 2425 require.Equal(t, *_case.ExpectedRunnerConfig.TemplateConfig.NomadRetry.Attempts, *runnerConfig.Nomad.Retry.Attempts) 2426 require.Equal(t, *_case.ExpectedRunnerConfig.TemplateConfig.NomadRetry.Backoff, *runnerConfig.Nomad.Retry.Backoff) 2427 require.Equal(t, *_case.ExpectedRunnerConfig.TemplateConfig.NomadRetry.MaxBackoff, *runnerConfig.Nomad.Retry.MaxBackoff) 2428 2429 // Test that wait_bounds are enforced 2430 for _, tmpl := range *runnerConfig.Templates { 2431 require.Equal(t, *_case.ExpectedTemplateConfig.Wait.Enabled, *tmpl.Wait.Enabled) 2432 require.Equal(t, *_case.ExpectedTemplateConfig.Wait.Min, *tmpl.Wait.Min) 2433 require.Equal(t, *_case.ExpectedTemplateConfig.Wait.Max, *tmpl.Wait.Max) 2434 } 2435 }) 2436 } 2437 } 2438 2439 // TestTaskTemplateManager_Template_Wait_Set asserts that all template level 2440 // configuration is accurately mapped from the template to the TaskTemplateManager's 2441 // template config. 2442 func TestTaskTemplateManager_Template_Wait_Set(t *testing.T) { 2443 ci.Parallel(t) 2444 2445 c := config.DefaultConfig() 2446 c.Node = mock.Node() 2447 2448 alloc := mock.Alloc() 2449 2450 ttmConfig := &TaskTemplateManagerConfig{ 2451 ClientConfig: c, 2452 VaultToken: "token", 2453 EnvBuilder: taskenv.NewBuilder(c.Node, alloc, alloc.Job.TaskGroups[0].Tasks[0], c.Region), 2454 Templates: []*structs.Template{ 2455 { 2456 Wait: &structs.WaitConfig{ 2457 Min: pointer.Of(5 * time.Second), 2458 Max: pointer.Of(10 * time.Second), 2459 }, 2460 }, 2461 }, 2462 } 2463 2464 templateMapping, err := parseTemplateConfigs(ttmConfig) 2465 require.NoError(t, err) 2466 2467 for k, _ := range templateMapping { 2468 require.True(t, *k.Wait.Enabled) 2469 require.Equal(t, 5*time.Second, *k.Wait.Min) 2470 require.Equal(t, 10*time.Second, *k.Wait.Max) 2471 } 2472 } 2473 2474 // TestTaskTemplateManager_Template_ErrMissingKey_Set asserts that all template level 2475 // configuration is accurately mapped from the template to the TaskTemplateManager's 2476 // template config. 2477 func TestTaskTemplateManager_Template_ErrMissingKey_Set(t *testing.T) { 2478 ci.Parallel(t) 2479 2480 c := config.DefaultConfig() 2481 c.Node = mock.Node() 2482 2483 alloc := mock.Alloc() 2484 2485 ttmConfig := &TaskTemplateManagerConfig{ 2486 ClientConfig: c, 2487 VaultToken: "token", 2488 EnvBuilder: taskenv.NewBuilder(c.Node, alloc, alloc.Job.TaskGroups[0].Tasks[0], c.Region), 2489 Templates: []*structs.Template{ 2490 { 2491 EmbeddedTmpl: "test-false", 2492 ErrMissingKey: false, 2493 }, 2494 { 2495 EmbeddedTmpl: "test-true", 2496 ErrMissingKey: true, 2497 }, 2498 }, 2499 } 2500 2501 templateMapping, err := parseTemplateConfigs(ttmConfig) 2502 require.NoError(t, err) 2503 2504 for k, tmpl := range templateMapping { 2505 if tmpl.EmbeddedTmpl == "test-false" { 2506 require.False(t, *k.ErrMissingKey) 2507 } 2508 if tmpl.EmbeddedTmpl == "test-true" { 2509 require.True(t, *k.ErrMissingKey) 2510 } 2511 } 2512 } 2513 2514 // TestTaskTemplateManager_writeToFile_Disabled asserts the consul-template function 2515 // writeToFile is disabled by default. 2516 func TestTaskTemplateManager_writeToFile_Disabled(t *testing.T) { 2517 ci.Parallel(t) 2518 2519 file := "my.tmpl" 2520 template := &structs.Template{ 2521 EmbeddedTmpl: `Testing writeToFile... 2522 {{ "if i exist writeToFile is enabled" | writeToFile "/tmp/NOMAD-TEST-SHOULD-NOT-EXIST" "" "" "0644" }} 2523 ...done 2524 `, 2525 DestPath: file, 2526 ChangeMode: structs.TemplateChangeModeNoop, 2527 } 2528 2529 harness := newTestHarness(t, []*structs.Template{template}, false, false) 2530 require.NoError(t, harness.startWithErr(), "couldn't setup initial harness") 2531 defer harness.stop() 2532 2533 // Using writeToFile should cause a kill 2534 select { 2535 case <-harness.mockHooks.UnblockCh: 2536 t.Fatalf("Task unblock should have not have been called") 2537 case <-harness.mockHooks.EmitEventCh: 2538 t.Fatalf("Task event should not have been emitted") 2539 case e := <-harness.mockHooks.KillCh: 2540 require.Contains(t, e.DisplayMessage, "writeToFile: function is disabled") 2541 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 2542 t.Fatalf("timeout") 2543 } 2544 2545 // Check the file is not there 2546 path := filepath.Join(harness.taskDir, file) 2547 _, err := os.ReadFile(path) 2548 require.Error(t, err) 2549 } 2550 2551 // TestTaskTemplateManager_writeToFile asserts the consul-template function 2552 // writeToFile can be enabled. 2553 func TestTaskTemplateManager_writeToFile(t *testing.T) { 2554 if runtime.GOOS != "linux" { 2555 t.Skip("username and group lookup assume linux platform") 2556 } 2557 2558 ci.Parallel(t) 2559 2560 cu, err := users.Current() 2561 require.NoError(t, err) 2562 2563 cg, err := users.LookupGroupId(cu.Gid) 2564 require.NoError(t, err) 2565 2566 file := "my.tmpl" 2567 template := &structs.Template{ 2568 // EmbeddedTmpl set below as it needs the taskDir 2569 DestPath: file, 2570 ChangeMode: structs.TemplateChangeModeNoop, 2571 } 2572 2573 harness := newTestHarness(t, []*structs.Template{template}, false, false) 2574 2575 // Add template now that we know the taskDir 2576 harness.templates[0].EmbeddedTmpl = fmt.Sprintf(`Testing writeToFile... 2577 {{ "hello" | writeToFile "%s" "`+cu.Username+`" "`+cg.Name+`" "0644" }} 2578 ...done 2579 `, filepath.Join(harness.taskDir, "writetofile.out")) 2580 2581 // Enable all funcs 2582 harness.config.TemplateConfig.FunctionDenylist = []string{} 2583 2584 require.NoError(t, harness.startWithErr(), "couldn't setup initial harness") 2585 defer harness.stop() 2586 2587 // Using writeToFile should not cause a kill 2588 select { 2589 case <-harness.mockHooks.UnblockCh: 2590 case <-harness.mockHooks.EmitEventCh: 2591 t.Fatalf("Task event should not have been emitted") 2592 case e := <-harness.mockHooks.KillCh: 2593 t.Fatalf("Task should not have been killed: %v", e.DisplayMessage) 2594 case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): 2595 t.Fatalf("timeout") 2596 } 2597 2598 // Check the templated file is there 2599 path := filepath.Join(harness.taskDir, file) 2600 r, err := os.ReadFile(path) 2601 require.NoError(t, err) 2602 require.True(t, bytes.HasSuffix(r, []byte("...done\n")), string(r)) 2603 2604 // Check that writeToFile was allowed 2605 path = filepath.Join(harness.taskDir, "writetofile.out") 2606 r, err = os.ReadFile(path) 2607 require.NoError(t, err) 2608 require.Equal(t, "hello", string(r)) 2609 }