package taskrunner import ( "fmt" "io" "io/ioutil" "os" "path/filepath" "strings" "testing" "time" ctestutil "github.com/hashicorp/consul/testutil" "github.com/hashicorp/nomad/client/config" "github.com/hashicorp/nomad/client/driver/env" "github.com/hashicorp/nomad/helper" "github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/structs" sconfig "github.com/hashicorp/nomad/nomad/structs/config" "github.com/hashicorp/nomad/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( // TestTaskName is the name of the injected task. It should appear in the // environment variable $NOMAD_TASK_NAME TestTaskName = "test-task" ) // MockTaskHooks is a mock of the TaskHooks interface useful for testing type MockTaskHooks struct { Restarts int RestartCh chan struct{} Signals []os.Signal SignalCh chan struct{} // SignalError is returned when Signal is called on the mock hook SignalError error UnblockCh chan struct{} Unblocked bool KillReason string KillCh chan struct{} Events []string EmitEventCh chan struct{} } func NewMockTaskHooks() *MockTaskHooks { return &MockTaskHooks{ UnblockCh: make(chan struct{}, 1), RestartCh: make(chan struct{}, 1), SignalCh: make(chan struct{}, 1), KillCh: make(chan struct{}, 1), EmitEventCh: make(chan struct{}, 1), } } func (m *MockTaskHooks) Restart(source, reason string, failure bool) { m.Restarts++ select { case m.RestartCh <- struct{}{}: default: } } func (m *MockTaskHooks) Signal(source, reason string, s os.Signal) error { m.Signals = append(m.Signals, s) select { case m.SignalCh <- struct{}{}: default: } return m.SignalError } func (m *MockTaskHooks) Kill(source, reason string, fail bool) { m.KillReason = reason select { case m.KillCh <- struct{}{}: default: } } func (m *MockTaskHooks) UnblockStart(source string) { if !m.Unblocked { close(m.UnblockCh) } m.Unblocked = true } func (m *MockTaskHooks) EmitEvent(source, message string) { m.Events = append(m.Events, message) select { case m.EmitEventCh <- struct{}{}: default: } } // testHarness is used to test the TaskTemplateManager by spinning up // Consul/Vault as needed type testHarness struct { manager *TaskTemplateManager mockHooks *MockTaskHooks templates []*structs.Template envBuilder *env.Builder node *structs.Node config *config.Config vaultToken string taskDir string vault *testutil.TestVault consul *ctestutil.TestServer emitRate time.Duration } // newTestHarness returns a harness starting a dev consul and vault server, // building the appropriate config and creating a TaskTemplateManager func newTestHarness(t *testing.T, templates []*structs.Template, consul, vault bool) *testHarness { region := "global" harness := &testHarness{ mockHooks: NewMockTaskHooks(), templates: templates, node: mock.Node(), config: &config.Config{Region: region}, emitRate: DefaultMaxTemplateEventRate, } // Build the task environment a := mock.Alloc() task := a.Job.TaskGroups[0].Tasks[0] task.Name = TestTaskName harness.envBuilder = env.NewBuilder(harness.node, a, task, region) // Make a tempdir d, err := ioutil.TempDir("", "ct_test") if err != nil { t.Fatalf("Failed to make tmpdir: %v", err) } harness.taskDir = d if consul { harness.consul, err = ctestutil.NewTestServer() if err != nil { t.Fatalf("error starting test Consul server: %v", err) } harness.config.ConsulConfig = &sconfig.ConsulConfig{ Addr: harness.consul.HTTPAddr, } } if vault { harness.vault = testutil.NewTestVault(t) harness.config.VaultConfig = harness.vault.Config harness.vaultToken = harness.vault.RootToken } return harness } func (h *testHarness) start(t *testing.T) { if err := h.startWithErr(); err != nil { t.Fatalf("failed to build task template manager: %v", err) } } func (h *testHarness) startWithErr() error { var err error h.manager, err = NewTaskTemplateManager(&TaskTemplateManagerConfig{ Hooks: h.mockHooks, Templates: h.templates, ClientConfig: h.config, VaultToken: h.vaultToken, TaskDir: h.taskDir, EnvBuilder: h.envBuilder, MaxTemplateEventRate: h.emitRate, retryRate: 10 * time.Millisecond, }) return err } func (h *testHarness) setEmitRate(d time.Duration) { h.emitRate = d } // stop is used to stop any running Vault or Consul server plus the task manager func (h *testHarness) stop() { if h.vault != nil { h.vault.Stop() } if h.consul != nil { h.consul.Stop() } if h.manager != nil { h.manager.Stop() } if h.taskDir != "" { os.RemoveAll(h.taskDir) } } func TestTaskTemplateManager_InvalidConfig(t *testing.T) { t.Parallel() hooks := NewMockTaskHooks() clientConfig := &config.Config{Region: "global"} taskDir := "foo" a := mock.Alloc() envBuilder := env.NewBuilder(mock.Node(), a, a.Job.TaskGroups[0].Tasks[0], clientConfig.Region) cases := []struct { name string config *TaskTemplateManagerConfig expectedErr string }{ { name: "nil config", config: nil, expectedErr: "Nil config passed", }, { name: "bad hooks", config: &TaskTemplateManagerConfig{ ClientConfig: clientConfig, TaskDir: taskDir, EnvBuilder: envBuilder, MaxTemplateEventRate: DefaultMaxTemplateEventRate, }, expectedErr: "task hooks", }, { name: "bad client config", config: &TaskTemplateManagerConfig{ Hooks: hooks, TaskDir: taskDir, EnvBuilder: envBuilder, MaxTemplateEventRate: DefaultMaxTemplateEventRate, }, expectedErr: "client config", }, { name: "bad task dir", config: &TaskTemplateManagerConfig{ ClientConfig: clientConfig, Hooks: hooks, EnvBuilder: envBuilder, MaxTemplateEventRate: DefaultMaxTemplateEventRate, }, expectedErr: "task directory", }, { name: "bad env builder", config: &TaskTemplateManagerConfig{ ClientConfig: clientConfig, Hooks: hooks, TaskDir: taskDir, MaxTemplateEventRate: DefaultMaxTemplateEventRate, }, expectedErr: "task environment", }, { name: "bad max event rate", config: &TaskTemplateManagerConfig{ ClientConfig: clientConfig, Hooks: hooks, TaskDir: taskDir, EnvBuilder: envBuilder, }, expectedErr: "template event rate", }, { name: "valid", config: &TaskTemplateManagerConfig{ ClientConfig: clientConfig, Hooks: hooks, TaskDir: taskDir, EnvBuilder: envBuilder, MaxTemplateEventRate: DefaultMaxTemplateEventRate, }, }, { name: "invalid signal", config: &TaskTemplateManagerConfig{ Templates: []*structs.Template{ { DestPath: "foo", EmbeddedTmpl: "hello, world", ChangeMode: structs.TemplateChangeModeSignal, ChangeSignal: "foobarbaz", }, }, ClientConfig: clientConfig, Hooks: hooks, TaskDir: taskDir, EnvBuilder: envBuilder, MaxTemplateEventRate: DefaultMaxTemplateEventRate, }, expectedErr: "parse signal", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { _, err := NewTaskTemplateManager(c.config) if err != nil { if c.expectedErr == "" { t.Fatalf("unexpected error: %v", err) } else if !strings.Contains(err.Error(), c.expectedErr) { t.Fatalf("expected error to contain %q; got %q", c.expectedErr, err.Error()) } } else if c.expectedErr != "" { t.Fatalf("expected an error to contain %q", c.expectedErr) } }) } } func TestTaskTemplateManager_HostPath(t *testing.T) { t.Parallel() // Make a template that will render immediately and write it to a tmp file f, err := ioutil.TempFile("", "") if err != nil { t.Fatalf("Bad: %v", err) } defer f.Close() defer os.Remove(f.Name()) content := "hello, world!" if _, err := io.WriteString(f, content); err != nil { t.Fatalf("Bad: %v", err) } file := "my.tmpl" template := &structs.Template{ SourcePath: f.Name(), DestPath: file, ChangeMode: structs.TemplateChangeModeNoop, } harness := newTestHarness(t, []*structs.Template{template}, false, false) harness.start(t) defer harness.stop() // Wait for the unblock select { case <-harness.mockHooks.UnblockCh: case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): t.Fatalf("Task unblock should have been called") } // Check the file is there path := filepath.Join(harness.taskDir, file) raw, err := ioutil.ReadFile(path) if err != nil { t.Fatalf("Failed to read rendered template from %q: %v", path, err) } if s := string(raw); s != content { t.Fatalf("Unexpected template data; got %q, want %q", s, content) } // Change the config to disallow host sources harness = newTestHarness(t, []*structs.Template{template}, false, false) harness.config.Options = map[string]string{ hostSrcOption: "false", } if err := harness.startWithErr(); err == nil || !strings.Contains(err.Error(), "absolute") { t.Fatalf("Expected absolute template path disallowed: %v", err) } } func TestTaskTemplateManager_Unblock_Static(t *testing.T) { t.Parallel() // Make a template that will render immediately content := "hello, world!" file := "my.tmpl" template := &structs.Template{ EmbeddedTmpl: content, DestPath: file, ChangeMode: structs.TemplateChangeModeNoop, } harness := newTestHarness(t, []*structs.Template{template}, false, false) harness.start(t) defer harness.stop() // Wait for the unblock select { case <-harness.mockHooks.UnblockCh: case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): t.Fatalf("Task unblock should have been called") } // Check the file is there path := filepath.Join(harness.taskDir, file) raw, err := ioutil.ReadFile(path) if err != nil { t.Fatalf("Failed to read rendered template from %q: %v", path, err) } if s := string(raw); s != content { t.Fatalf("Unexpected template data; got %q, want %q", s, content) } } func TestTaskTemplateManager_Permissions(t *testing.T) { t.Parallel() // Make a template that will render immediately content := "hello, world!" file := "my.tmpl" template := &structs.Template{ EmbeddedTmpl: content, DestPath: file, ChangeMode: structs.TemplateChangeModeNoop, Perms: "777", } harness := newTestHarness(t, []*structs.Template{template}, false, false) harness.start(t) defer harness.stop() // Wait for the unblock select { case <-harness.mockHooks.UnblockCh: case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): t.Fatalf("Task unblock should have been called") } // Check the file is there path := filepath.Join(harness.taskDir, file) fi, err := os.Stat(path) if err != nil { t.Fatalf("Failed to stat file: %v", err) } if m := fi.Mode(); m != os.ModePerm { t.Fatalf("Got mode %v; want %v", m, os.ModePerm) } } func TestTaskTemplateManager_Unblock_Static_NomadEnv(t *testing.T) { t.Parallel() // Make a template that will render immediately content := `Hello Nomad Task: {{env "NOMAD_TASK_NAME"}}` expected := fmt.Sprintf("Hello Nomad Task: %s", TestTaskName) file := "my.tmpl" template := &structs.Template{ EmbeddedTmpl: content, DestPath: file, ChangeMode: structs.TemplateChangeModeNoop, } harness := newTestHarness(t, []*structs.Template{template}, false, false) harness.start(t) defer harness.stop() // Wait for the unblock select { case <-harness.mockHooks.UnblockCh: case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): t.Fatalf("Task unblock should have been called") } // Check the file is there path := filepath.Join(harness.taskDir, file) raw, err := ioutil.ReadFile(path) if err != nil { t.Fatalf("Failed to read rendered template from %q: %v", path, err) } if s := string(raw); s != expected { t.Fatalf("Unexpected template data; got %q, want %q", s, expected) } } func TestTaskTemplateManager_Unblock_Static_AlreadyRendered(t *testing.T) { t.Parallel() // Make a template that will render immediately content := "hello, world!" file := "my.tmpl" template := &structs.Template{ EmbeddedTmpl: content, DestPath: file, ChangeMode: structs.TemplateChangeModeNoop, } harness := newTestHarness(t, []*structs.Template{template}, false, false) // Write the contents path := filepath.Join(harness.taskDir, file) if err := ioutil.WriteFile(path, []byte(content), 0777); err != nil { t.Fatalf("Failed to write data: %v", err) } harness.start(t) defer harness.stop() // Wait for the unblock select { case <-harness.mockHooks.UnblockCh: case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): t.Fatalf("Task unblock should have been called") } // Check the file is there path = filepath.Join(harness.taskDir, file) raw, err := ioutil.ReadFile(path) if err != nil { t.Fatalf("Failed to read rendered template from %q: %v", path, err) } if s := string(raw); s != content { t.Fatalf("Unexpected template data; got %q, want %q", s, content) } } func TestTaskTemplateManager_Unblock_Consul(t *testing.T) { t.Parallel() // Make a template that will render based on a key in Consul key := "foo" content := "barbaz" embedded := fmt.Sprintf(`{{key "%s"}}`, key) file := "my.tmpl" template := &structs.Template{ EmbeddedTmpl: embedded, DestPath: file, ChangeMode: structs.TemplateChangeModeNoop, } harness := newTestHarness(t, []*structs.Template{template}, true, false) harness.start(t) defer harness.stop() // Ensure no unblock select { case <-harness.mockHooks.UnblockCh: t.Fatalf("Task unblock should have not have been called") case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): } // Write the key to Consul harness.consul.SetKV(t, key, []byte(content)) // Wait for the unblock select { case <-harness.mockHooks.UnblockCh: case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): t.Fatalf("Task unblock should have been called") } // Check the file is there path := filepath.Join(harness.taskDir, file) raw, err := ioutil.ReadFile(path) if err != nil { t.Fatalf("Failed to read rendered template from %q: %v", path, err) } if s := string(raw); s != content { t.Fatalf("Unexpected template data; got %q, want %q", s, content) } } func TestTaskTemplateManager_Unblock_Vault(t *testing.T) { t.Parallel() require := require.New(t) // Make a template that will render based on a key in Vault vaultPath := "secret/data/password" key := "password" content := "barbaz" embedded := fmt.Sprintf(`{{with secret "%s"}}{{.Data.data.%s}}{{end}}`, vaultPath, key) file := "my.tmpl" template := &structs.Template{ EmbeddedTmpl: embedded, DestPath: file, ChangeMode: structs.TemplateChangeModeNoop, } harness := newTestHarness(t, []*structs.Template{template}, false, true) harness.start(t) defer harness.stop() // Ensure no unblock select { case <-harness.mockHooks.UnblockCh: t.Fatalf("Task unblock should not have been called") case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): } // Write the secret to Vault logical := harness.vault.Client.Logical() _, err := logical.Write(vaultPath, map[string]interface{}{"data": map[string]interface{}{key: content}}) require.NoError(err) // Wait for the unblock select { case <-harness.mockHooks.UnblockCh: case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): t.Fatalf("Task unblock should have been called") } // Check the file is there path := filepath.Join(harness.taskDir, file) raw, err := ioutil.ReadFile(path) if err != nil { t.Fatalf("Failed to read rendered template from %q: %v", path, err) } if s := string(raw); s != content { t.Fatalf("Unexpected template data; got %q, want %q", s, content) } } func TestTaskTemplateManager_Unblock_Multi_Template(t *testing.T) { t.Parallel() // Make a template that will render immediately staticContent := "hello, world!" staticFile := "my.tmpl" template := &structs.Template{ EmbeddedTmpl: staticContent, DestPath: staticFile, ChangeMode: structs.TemplateChangeModeNoop, } // Make a template that will render based on a key in Consul consulKey := "foo" consulContent := "barbaz" consulEmbedded := fmt.Sprintf(`{{key "%s"}}`, consulKey) consulFile := "consul.tmpl" template2 := &structs.Template{ EmbeddedTmpl: consulEmbedded, DestPath: consulFile, ChangeMode: structs.TemplateChangeModeNoop, } harness := newTestHarness(t, []*structs.Template{template, template2}, true, false) harness.start(t) defer harness.stop() // Ensure no unblock select { case <-harness.mockHooks.UnblockCh: t.Fatalf("Task unblock should have not have been called") case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): } // Check that the static file has been rendered path := filepath.Join(harness.taskDir, staticFile) raw, err := ioutil.ReadFile(path) if err != nil { t.Fatalf("Failed to read rendered template from %q: %v", path, err) } if s := string(raw); s != staticContent { t.Fatalf("Unexpected template data; got %q, want %q", s, staticContent) } // Write the key to Consul harness.consul.SetKV(t, consulKey, []byte(consulContent)) // Wait for the unblock select { case <-harness.mockHooks.UnblockCh: case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): t.Fatalf("Task unblock should have been called") } // Check the consul file is there path = filepath.Join(harness.taskDir, consulFile) raw, err = ioutil.ReadFile(path) if err != nil { t.Fatalf("Failed to read rendered template from %q: %v", path, err) } if s := string(raw); s != consulContent { t.Fatalf("Unexpected template data; got %q, want %q", s, consulContent) } } func TestTaskTemplateManager_Rerender_Noop(t *testing.T) { t.Parallel() // Make a template that will render based on a key in Consul key := "foo" content1 := "bar" content2 := "baz" embedded := fmt.Sprintf(`{{key "%s"}}`, key) file := "my.tmpl" template := &structs.Template{ EmbeddedTmpl: embedded, DestPath: file, ChangeMode: structs.TemplateChangeModeNoop, } harness := newTestHarness(t, []*structs.Template{template}, true, false) harness.start(t) defer harness.stop() // Ensure no unblock select { case <-harness.mockHooks.UnblockCh: t.Fatalf("Task unblock should have not have been called") case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): } // Write the key to Consul harness.consul.SetKV(t, key, []byte(content1)) // Wait for the unblock select { case <-harness.mockHooks.UnblockCh: case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): t.Fatalf("Task unblock should have been called") } // Check the file is there path := filepath.Join(harness.taskDir, file) raw, err := ioutil.ReadFile(path) if err != nil { t.Fatalf("Failed to read rendered template from %q: %v", path, err) } if s := string(raw); s != content1 { t.Fatalf("Unexpected template data; got %q, want %q", s, content1) } // Update the key in Consul harness.consul.SetKV(t, key, []byte(content2)) select { case <-harness.mockHooks.RestartCh: t.Fatalf("Noop ignored: %+v", harness.mockHooks) case <-harness.mockHooks.SignalCh: t.Fatalf("Noop ignored: %+v", harness.mockHooks) case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): } // Check the file has been updated path = filepath.Join(harness.taskDir, file) raw, err = ioutil.ReadFile(path) if err != nil { t.Fatalf("Failed to read rendered template from %q: %v", path, err) } if s := string(raw); s != content2 { t.Fatalf("Unexpected template data; got %q, want %q", s, content2) } } func TestTaskTemplateManager_Rerender_Signal(t *testing.T) { t.Parallel() // Make a template that renders based on a key in Consul and sends SIGALRM key1 := "foo" content1_1 := "bar" content1_2 := "baz" embedded1 := fmt.Sprintf(`{{key "%s"}}`, key1) file1 := "my.tmpl" template := &structs.Template{ EmbeddedTmpl: embedded1, DestPath: file1, ChangeMode: structs.TemplateChangeModeSignal, ChangeSignal: "SIGALRM", } // Make a template that renders based on a key in Consul and sends SIGBUS key2 := "bam" content2_1 := "cat" content2_2 := "dog" embedded2 := fmt.Sprintf(`{{key "%s"}}`, key2) file2 := "my-second.tmpl" template2 := &structs.Template{ EmbeddedTmpl: embedded2, DestPath: file2, ChangeMode: structs.TemplateChangeModeSignal, ChangeSignal: "SIGBUS", } harness := newTestHarness(t, []*structs.Template{template, template2}, true, false) harness.start(t) defer harness.stop() // Ensure no unblock select { case <-harness.mockHooks.UnblockCh: t.Fatalf("Task unblock should have not have been called") case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): } // Write the key to Consul harness.consul.SetKV(t, key1, []byte(content1_1)) harness.consul.SetKV(t, key2, []byte(content2_1)) // Wait for the unblock select { case <-harness.mockHooks.UnblockCh: case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): t.Fatalf("Task unblock should have been called") } if len(harness.mockHooks.Signals) != 0 { t.Fatalf("Should not have received any signals: %+v", harness.mockHooks) } // Update the keys in Consul harness.consul.SetKV(t, key1, []byte(content1_2)) harness.consul.SetKV(t, key2, []byte(content2_2)) // Wait for signals timeout := time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second) OUTER: for { select { case <-harness.mockHooks.RestartCh: t.Fatalf("Restart with signal policy: %+v", harness.mockHooks) case <-harness.mockHooks.SignalCh: if len(harness.mockHooks.Signals) != 2 { continue } break OUTER case <-timeout: t.Fatalf("Should have received two signals: %+v", harness.mockHooks) } } // Check the files have been updated path := filepath.Join(harness.taskDir, file1) raw, err := ioutil.ReadFile(path) if err != nil { t.Fatalf("Failed to read rendered template from %q: %v", path, err) } if s := string(raw); s != content1_2 { t.Fatalf("Unexpected template data; got %q, want %q", s, content1_2) } path = filepath.Join(harness.taskDir, file2) raw, err = ioutil.ReadFile(path) if err != nil { t.Fatalf("Failed to read rendered template from %q: %v", path, err) } if s := string(raw); s != content2_2 { t.Fatalf("Unexpected template data; got %q, want %q", s, content2_2) } } func TestTaskTemplateManager_Rerender_Restart(t *testing.T) { t.Parallel() // Make a template that renders based on a key in Consul and sends restart key1 := "bam" content1_1 := "cat" content1_2 := "dog" embedded1 := fmt.Sprintf(`{{key "%s"}}`, key1) file1 := "my.tmpl" template := &structs.Template{ EmbeddedTmpl: embedded1, DestPath: file1, ChangeMode: structs.TemplateChangeModeRestart, } harness := newTestHarness(t, []*structs.Template{template}, true, false) harness.start(t) defer harness.stop() // Ensure no unblock select { case <-harness.mockHooks.UnblockCh: t.Fatalf("Task unblock should have not have been called") case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): } // Write the key to Consul harness.consul.SetKV(t, key1, []byte(content1_1)) // Wait for the unblock select { case <-harness.mockHooks.UnblockCh: case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): t.Fatalf("Task unblock should have been called") } // Update the keys in Consul harness.consul.SetKV(t, key1, []byte(content1_2)) // Wait for restart timeout := time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second) OUTER: for { select { case <-harness.mockHooks.RestartCh: break OUTER case <-harness.mockHooks.SignalCh: t.Fatalf("Signal with restart policy: %+v", harness.mockHooks) case <-timeout: t.Fatalf("Should have received a restart: %+v", harness.mockHooks) } } // Check the files have been updated path := filepath.Join(harness.taskDir, file1) raw, err := ioutil.ReadFile(path) if err != nil { t.Fatalf("Failed to read rendered template from %q: %v", path, err) } if s := string(raw); s != content1_2 { t.Fatalf("Unexpected template data; got %q, want %q", s, content1_2) } } func TestTaskTemplateManager_Interpolate_Destination(t *testing.T) { t.Parallel() // Make a template that will have its destination interpolated content := "hello, world!" file := "${node.unique.id}.tmpl" template := &structs.Template{ EmbeddedTmpl: content, DestPath: file, ChangeMode: structs.TemplateChangeModeNoop, } harness := newTestHarness(t, []*structs.Template{template}, false, false) harness.start(t) defer harness.stop() // Ensure unblock select { case <-harness.mockHooks.UnblockCh: case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): t.Fatalf("Task unblock should have been called") } // Check the file is there actual := fmt.Sprintf("%s.tmpl", harness.node.ID) path := filepath.Join(harness.taskDir, actual) raw, err := ioutil.ReadFile(path) if err != nil { t.Fatalf("Failed to read rendered template from %q: %v", path, err) } if s := string(raw); s != content { t.Fatalf("Unexpected template data; got %q, want %q", s, content) } } func TestTaskTemplateManager_Signal_Error(t *testing.T) { t.Parallel() // Make a template that renders based on a key in Consul and sends SIGALRM key1 := "foo" content1 := "bar" content2 := "baz" embedded1 := fmt.Sprintf(`{{key "%s"}}`, key1) file1 := "my.tmpl" template := &structs.Template{ EmbeddedTmpl: embedded1, DestPath: file1, ChangeMode: structs.TemplateChangeModeSignal, ChangeSignal: "SIGALRM", } harness := newTestHarness(t, []*structs.Template{template}, true, false) harness.start(t) defer harness.stop() harness.mockHooks.SignalError = fmt.Errorf("test error") // Write the key to Consul harness.consul.SetKV(t, key1, []byte(content1)) // Wait a little select { case <-harness.mockHooks.UnblockCh: case <-time.After(time.Duration(2*testutil.TestMultiplier()) * time.Second): t.Fatalf("Should have received unblock: %+v", harness.mockHooks) } // Write the key to Consul harness.consul.SetKV(t, key1, []byte(content2)) // Wait for kill channel select { case <-harness.mockHooks.KillCh: break case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): t.Fatalf("Should have received a signals: %+v", harness.mockHooks) } if !strings.Contains(harness.mockHooks.KillReason, "Sending signals") { t.Fatalf("Unexpected error: %v", harness.mockHooks.KillReason) } } // TestTaskTemplateManager_Env asserts templates with the env flag set are read // into the task's environment. func TestTaskTemplateManager_Env(t *testing.T) { t.Parallel() template := &structs.Template{ EmbeddedTmpl: ` # Comment lines are ok FOO=bar foo=123 ANYTHING_goes=Spaces are=ok! `, DestPath: "test.env", ChangeMode: structs.TemplateChangeModeNoop, Envvars: true, } harness := newTestHarness(t, []*structs.Template{template}, true, false) harness.start(t) defer harness.stop() // Wait a little select { case <-harness.mockHooks.UnblockCh: case <-time.After(time.Duration(2*testutil.TestMultiplier()) * time.Second): t.Fatalf("Should have received unblock: %+v", harness.mockHooks) } // Validate environment env := harness.envBuilder.Build().Map() if len(env) < 3 { t.Fatalf("expected at least 3 env vars but found %d:\n%#v\n", len(env), env) } if env["FOO"] != "bar" { t.Errorf("expected FOO=bar but found %q", env["FOO"]) } if env["foo"] != "123" { t.Errorf("expected foo=123 but found %q", env["foo"]) } if env["ANYTHING_goes"] != "Spaces are=ok!" { t.Errorf("expected ANYTHING_GOES='Spaces are ok!' but found %q", env["ANYTHING_goes"]) } } // TestTaskTemplateManager_Env_Missing asserts the core env // template processing function returns errors when files don't exist func TestTaskTemplateManager_Env_Missing(t *testing.T) { t.Parallel() d, err := ioutil.TempDir("", "ct_env_missing") if err != nil { t.Fatalf("err: %v", err) } defer os.RemoveAll(d) // Fake writing the file so we don't have to run the whole template manager err = ioutil.WriteFile(filepath.Join(d, "exists.env"), []byte("FOO=bar\n"), 0644) if err != nil { t.Fatalf("error writing template file: %v", err) } templates := []*structs.Template{ { EmbeddedTmpl: "FOO=bar\n", DestPath: "exists.env", Envvars: true, }, { EmbeddedTmpl: "WHAT=ever\n", DestPath: "missing.env", Envvars: true, }, } if vars, err := loadTemplateEnv(templates, d); err == nil { t.Fatalf("expected an error but instead got env vars: %#v", vars) } } // TestTaskTemplateManager_Env_Multi asserts the core env // template processing function returns combined env vars from multiple // templates correctly. func TestTaskTemplateManager_Env_Multi(t *testing.T) { t.Parallel() d, err := ioutil.TempDir("", "ct_env_missing") if err != nil { t.Fatalf("err: %v", err) } defer os.RemoveAll(d) // Fake writing the files so we don't have to run the whole template manager err = ioutil.WriteFile(filepath.Join(d, "zzz.env"), []byte("FOO=bar\nSHARED=nope\n"), 0644) if err != nil { t.Fatalf("error writing template file 1: %v", err) } err = ioutil.WriteFile(filepath.Join(d, "aaa.env"), []byte("BAR=foo\nSHARED=yup\n"), 0644) if err != nil { t.Fatalf("error writing template file 2: %v", err) } // Templates will get loaded in order (not alpha sorted) templates := []*structs.Template{ { DestPath: "zzz.env", Envvars: true, }, { DestPath: "aaa.env", Envvars: true, }, } vars, err := loadTemplateEnv(templates, d) if err != nil { t.Fatalf("expected an error but instead got env vars: %#v", vars) } if vars["FOO"] != "bar" { t.Errorf("expected FOO=bar but found %q", vars["FOO"]) } if vars["BAR"] != "foo" { t.Errorf("expected BAR=foo but found %q", vars["BAR"]) } if vars["SHARED"] != "yup" { t.Errorf("expected FOO=bar but found %q", vars["yup"]) } } func TestTaskTemplateManager_Rerender_Env(t *testing.T) { t.Parallel() // Make a template that renders based on a key in Consul and sends restart key1 := "bam" key2 := "bar" content1_1 := "cat" content1_2 := "dog" t1 := &structs.Template{ EmbeddedTmpl: ` FOO={{key "bam"}} `, DestPath: "test.env", ChangeMode: structs.TemplateChangeModeRestart, Envvars: true, } t2 := &structs.Template{ EmbeddedTmpl: ` BAR={{key "bar"}} `, DestPath: "test2.env", ChangeMode: structs.TemplateChangeModeRestart, Envvars: true, } harness := newTestHarness(t, []*structs.Template{t1, t2}, true, false) harness.start(t) defer harness.stop() // Ensure no unblock select { case <-harness.mockHooks.UnblockCh: t.Fatalf("Task unblock should have not have been called") case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): } // Write the key to Consul harness.consul.SetKV(t, key1, []byte(content1_1)) harness.consul.SetKV(t, key2, []byte(content1_1)) // Wait for the unblock select { case <-harness.mockHooks.UnblockCh: case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): t.Fatalf("Task unblock should have been called") } env := harness.envBuilder.Build().Map() if v, ok := env["FOO"]; !ok || v != content1_1 { t.Fatalf("Bad env for FOO: %v %v", v, ok) } if v, ok := env["BAR"]; !ok || v != content1_1 { t.Fatalf("Bad env for BAR: %v %v", v, ok) } // Update the keys in Consul harness.consul.SetKV(t, key1, []byte(content1_2)) // Wait for restart timeout := time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second) OUTER: for { select { case <-harness.mockHooks.RestartCh: break OUTER case <-harness.mockHooks.SignalCh: t.Fatalf("Signal with restart policy: %+v", harness.mockHooks) case <-timeout: t.Fatalf("Should have received a restart: %+v", harness.mockHooks) } } env = harness.envBuilder.Build().Map() if v, ok := env["FOO"]; !ok || v != content1_2 { t.Fatalf("Bad env for FOO: %v %v", v, ok) } if v, ok := env["BAR"]; !ok || v != content1_1 { t.Fatalf("Bad env for BAR: %v %v", v, ok) } } // TestTaskTemplateManager_Config_ServerName asserts the tls_server_name // setting is propagated to consul-template's configuration. See #2776 func TestTaskTemplateManager_Config_ServerName(t *testing.T) { t.Parallel() c := config.DefaultConfig() c.VaultConfig = &sconfig.VaultConfig{ Enabled: helper.BoolToPtr(true), Addr: "https://localhost/", TLSServerName: "notlocalhost", } config := &TaskTemplateManagerConfig{ ClientConfig: c, VaultToken: "token", } ctconf, err := newRunnerConfig(config, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } if *ctconf.Vault.SSL.ServerName != c.VaultConfig.TLSServerName { t.Fatalf("expected %q but found %q", c.VaultConfig.TLSServerName, *ctconf.Vault.SSL.ServerName) } } // TestTaskTemplateManager_Config_VaultGrace asserts the vault_grace setting is // propagated to consul-template's configuration. func TestTaskTemplateManager_Config_VaultGrace(t *testing.T) { t.Parallel() assert := assert.New(t) c := config.DefaultConfig() c.Node = mock.Node() c.VaultConfig = &sconfig.VaultConfig{ Enabled: helper.BoolToPtr(true), Addr: "https://localhost/", TLSServerName: "notlocalhost", } alloc := mock.Alloc() config := &TaskTemplateManagerConfig{ ClientConfig: c, VaultToken: "token", // Make a template that will render immediately Templates: []*structs.Template{ { EmbeddedTmpl: "bar", DestPath: "foo", ChangeMode: structs.TemplateChangeModeNoop, VaultGrace: 10 * time.Second, }, { EmbeddedTmpl: "baz", DestPath: "bam", ChangeMode: structs.TemplateChangeModeNoop, VaultGrace: 100 * time.Second, }, }, EnvBuilder: env.NewBuilder(c.Node, alloc, alloc.Job.TaskGroups[0].Tasks[0], c.Region), } ctmplMapping, err := parseTemplateConfigs(config) assert.Nil(err, "Parsing Templates") ctconf, err := newRunnerConfig(config, ctmplMapping) assert.Nil(err, "Building Runner Config") assert.NotNil(ctconf.Vault.Grace, "Vault Grace Pointer") assert.Equal(10*time.Second, *ctconf.Vault.Grace, "Vault Grace Value") } func TestTaskTemplateManager_BlockedEvents(t *testing.T) { t.Parallel() // Make a template that will render based on a key in Consul var embedded string for i := 0; i < 5; i++ { embedded += fmt.Sprintf(`{{key "%d"}}`, i) } file := "my.tmpl" template := &structs.Template{ EmbeddedTmpl: embedded, DestPath: file, ChangeMode: structs.TemplateChangeModeNoop, } harness := newTestHarness(t, []*structs.Template{template}, true, false) harness.setEmitRate(100 * time.Millisecond) harness.start(t) defer harness.stop() // Ensure that we get a blocked event select { case <-harness.mockHooks.UnblockCh: t.Fatalf("Task unblock should have not have been called") case <-harness.mockHooks.EmitEventCh: case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): t.Fatalf("timeout") } // Check to see we got a correct message event := harness.mockHooks.Events[0] if !strings.Contains(event, "and 2 more") { t.Fatalf("bad event: %q", event) } // Write 3 keys to Consul for i := 0; i < 3; i++ { harness.consul.SetKV(t, fmt.Sprintf("%d", i), []byte{0xa}) } // Ensure that we get a blocked event select { case <-harness.mockHooks.UnblockCh: t.Fatalf("Task unblock should have not have been called") case <-harness.mockHooks.EmitEventCh: case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second): t.Fatalf("timeout") } // Check to see we got a correct message event = harness.mockHooks.Events[len(harness.mockHooks.Events)-1] if !strings.Contains(event, "Missing") || strings.Contains(event, "more") { t.Fatalf("bad event: %q", event) } }