Files
nomad/client/allocrunner/taskrunner/template/template_test.go
Michael Smithhisler 0714353324 fix: handle template re-renders on client restart (#24399)
When multiple templates with api functions are included in a task, it's
possible for consul-template to re-render templates as it creates
watchers, overwriting render event data. This change uses event fields
that do not get overwritten, and only executes the change mode for
templates that were actually written to disk.

---------

Co-authored-by: Tim Gross <tgross@hashicorp.com>
2024-11-08 12:49:38 -05:00

2615 lines
78 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package template
import (
"bytes"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"regexp"
"slices"
"sort"
"strconv"
"strings"
"testing"
"time"
templateconfig "github.com/hashicorp/consul-template/config"
ctestutil "github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/client/allocdir"
trtesting "github.com/hashicorp/nomad/client/allocrunner/taskrunner/testing"
"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/client/taskenv"
clienttestutil "github.com/hashicorp/nomad/client/testutil"
"github.com/hashicorp/nomad/helper/pointer"
"github.com/hashicorp/nomad/helper/testlog"
"github.com/hashicorp/nomad/helper/users"
"github.com/hashicorp/nomad/helper/uuid"
"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/kr/pretty"
"github.com/shoenig/test"
"github.com/shoenig/test/must"
)
// TestMain overrides the normal top-level test runner for this package. When
// template-render subprocesses are run, they use os.Executable to find their
// own binary, which is the template.test binary when these tests are
// running. That causes the template-render subprocess to run all these tests!
// Bail out early if we know we're in the subprocess.
func TestMain(m *testing.M) {
flag.Parse()
if slices.Contains(flag.Args(), "template-render") {
return
}
os.Exit(m.Run())
}
const (
// TestTaskName is the name of the injected task. It should appear in the
// environment variable $NOMAD_TASK_NAME
TestTaskName = "test-task"
)
// testHarness is used to test the TaskTemplateManager by spinning up
// Consul/Vault as needed
type testHarness struct {
manager *TaskTemplateManager
mockHooks *trtesting.MockTaskHooks
templates []*structs.Template
envBuilder *taskenv.Builder
node *structs.Node
config *config.Config
vaultToken string
taskDir string
vault *testutil.TestVault
consul *ctestutil.TestServer
emitRate time.Duration
nomadNamespace string
}
// 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 {
t.Helper()
region := "global"
mockNode := mock.Node()
harness := &testHarness{
mockHooks: trtesting.NewMockTaskHooks(),
templates: templates,
node: mockNode,
config: &config.Config{
Node: mockNode,
Region: region,
TemplateConfig: &config.ClientTemplateConfig{
FunctionDenylist: config.DefaultTemplateFunctionDenylist,
DisableSandbox: false,
ConsulRetry: &config.RetryConfig{Backoff: pointer.Of(10 * time.Millisecond)},
}},
emitRate: DefaultMaxTemplateEventRate,
}
// Build the task environment
a := mock.Alloc()
task := a.Job.TaskGroups[0].Tasks[0]
task.Name = TestTaskName
harness.envBuilder = taskenv.NewBuilder(harness.node, a, task, region)
harness.nomadNamespace = a.Namespace
// Make a tempdir
harness.taskDir = t.TempDir()
harness.envBuilder.SetClientTaskRoot(harness.taskDir)
if consul {
var err error
harness.consul, err = ctestutil.NewTestServerConfigT(t, func(c *ctestutil.TestServerConfig) {
c.Peering = nil // fix for older versions of Consul (<1.13.0) that don't support peering
})
if err != nil {
t.Fatalf("error starting test Consul server: %v", err)
}
harness.config.ConsulConfigs = map[string]*sconfig.ConsulConfig{
structs.ConsulDefaultCluster: {
Addr: harness.consul.HTTPAddr,
}}
}
if vault {
harness.vault = testutil.NewTestVault(t)
harness.config.VaultConfigs = map[string]*sconfig.VaultConfig{
structs.VaultDefaultCluster: harness.vault.Config,
}
harness.vaultToken = harness.vault.RootToken
}
return harness
}
func (h *testHarness) start(t *testing.T) {
t.Helper()
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{
UnblockCh: h.mockHooks.UnblockCh,
Lifecycle: h.mockHooks,
Events: h.mockHooks,
Templates: h.templates,
ClientConfig: h.config,
ConsulConfig: h.config.GetDefaultConsul(),
VaultToken: h.vaultToken,
VaultConfig: h.config.GetDefaultVault(),
TaskDir: h.taskDir,
EnvBuilder: h.envBuilder,
MaxTemplateEventRate: h.emitRate,
TaskID: uuid.Generate(),
})
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) {
ci.Parallel(t)
hooks := trtesting.NewMockTaskHooks()
clientConfig := &config.Config{Region: "global"}
taskDir := t.TempDir()
a := mock.Alloc()
envBuilder := taskenv.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 lifecycle hooks",
config: &TaskTemplateManagerConfig{
UnblockCh: hooks.UnblockCh,
Events: hooks,
ClientConfig: clientConfig,
TaskDir: taskDir,
EnvBuilder: envBuilder,
MaxTemplateEventRate: DefaultMaxTemplateEventRate,
Logger: testlog.HCLogger(t),
},
expectedErr: "lifecycle hooks",
},
{
name: "bad event hooks",
config: &TaskTemplateManagerConfig{
UnblockCh: hooks.UnblockCh,
Lifecycle: hooks,
ClientConfig: clientConfig,
TaskDir: taskDir,
EnvBuilder: envBuilder,
MaxTemplateEventRate: DefaultMaxTemplateEventRate,
},
expectedErr: "event hook",
},
{
name: "bad client config",
config: &TaskTemplateManagerConfig{
UnblockCh: hooks.UnblockCh,
Lifecycle: hooks,
Events: hooks,
TaskDir: taskDir,
EnvBuilder: envBuilder,
MaxTemplateEventRate: DefaultMaxTemplateEventRate,
},
expectedErr: "client config",
},
{
name: "bad task dir",
config: &TaskTemplateManagerConfig{
UnblockCh: hooks.UnblockCh,
ClientConfig: clientConfig,
Lifecycle: hooks,
Events: hooks,
EnvBuilder: envBuilder,
MaxTemplateEventRate: DefaultMaxTemplateEventRate,
},
expectedErr: "task directory",
},
{
name: "bad env builder",
config: &TaskTemplateManagerConfig{
UnblockCh: hooks.UnblockCh,
ClientConfig: clientConfig,
Lifecycle: hooks,
Events: hooks,
TaskDir: taskDir,
MaxTemplateEventRate: DefaultMaxTemplateEventRate,
},
expectedErr: "task environment",
},
{
name: "bad max event rate",
config: &TaskTemplateManagerConfig{
UnblockCh: hooks.UnblockCh,
ClientConfig: clientConfig,
Lifecycle: hooks,
Events: hooks,
TaskDir: taskDir,
EnvBuilder: envBuilder,
},
expectedErr: "template event rate",
},
{
name: "valid",
config: &TaskTemplateManagerConfig{
UnblockCh: hooks.UnblockCh,
ClientConfig: clientConfig,
Lifecycle: hooks,
Events: hooks,
TaskDir: taskDir,
EnvBuilder: envBuilder,
MaxTemplateEventRate: DefaultMaxTemplateEventRate,
},
},
{
name: "invalid signal",
config: &TaskTemplateManagerConfig{
UnblockCh: hooks.UnblockCh,
Templates: []*structs.Template{
{
DestPath: "foo",
EmbeddedTmpl: "hello, world",
ChangeMode: structs.TemplateChangeModeSignal,
ChangeSignal: "foobarbaz",
},
},
ClientConfig: clientConfig,
Lifecycle: hooks,
Events: hooks,
TaskDir: taskDir,
EnvBuilder: envBuilder,
MaxTemplateEventRate: DefaultMaxTemplateEventRate,
},
expectedErr: "parse signal",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if c.config != nil {
c.config.TaskID = c.name
c.config.Logger = testlog.HCLogger(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) {
ci.Parallel(t)
// Make a template that will render immediately and write it to a tmp file
f, err := os.CreateTemp("", "")
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.config.TemplateConfig.DisableSandbox = true
err = harness.startWithErr()
if err != nil {
t.Fatalf("couldn't setup initial harness: %v", err)
}
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 := os.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)
err = harness.startWithErr()
if err == nil || !strings.Contains(err.Error(), "escapes alloc directory") {
t.Fatalf("Expected absolute template path disallowed for %q: %v",
template.SourcePath, err)
}
template.SourcePath = "../../../../../../" + file
harness = newTestHarness(t, []*structs.Template{template}, false, false)
err = harness.startWithErr()
if err == nil || !strings.Contains(err.Error(), "escapes alloc directory") {
t.Fatalf("Expected directory traversal out of %q disallowed for %q: %v",
harness.taskDir, template.SourcePath, err)
}
// Build a new task environment
a := mock.Alloc()
task := a.Job.TaskGroups[0].Tasks[0]
task.Name = TestTaskName
task.Meta = map[string]string{"ESCAPE": "../"}
template.SourcePath = "${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}" + file
harness = newTestHarness(t, []*structs.Template{template}, false, false)
harness.envBuilder = taskenv.NewBuilder(harness.node, a, task, "global")
err = harness.startWithErr()
if err == nil || !strings.Contains(err.Error(), "escapes alloc directory") {
t.Fatalf("Expected directory traversal out of %q via interpolation disallowed for %q: %v",
harness.taskDir, template.SourcePath, err)
}
// Test with destination too
template.SourcePath = f.Name()
template.DestPath = "../../../../../../" + file
harness = newTestHarness(t, []*structs.Template{template}, false, false)
harness.envBuilder = taskenv.NewBuilder(harness.node, a, task, "global")
err = harness.startWithErr()
must.ErrorContains(t, err, "escapes alloc directory", must.Sprintf(
"Expected directory traversal out of %q via interpolation disallowed for %q: %v",
harness.taskDir, template.SourcePath, err))
}
func TestTaskTemplateManager_Unblock_Static(t *testing.T) {
ci.Parallel(t)
// Make a template that will render immediately
content := "hello, world!"
file := "my.tmpl"
template := &structs.Template{
EmbeddedTmpl: content,
DestPath: file,
ChangeMode: structs.TemplateChangeModeNoop,
}
a := mock.Alloc()
task := a.Job.TaskGroups[0].Tasks[0]
task.Name = TestTaskName
harness := newTestHarness(t, []*structs.Template{template}, false, false)
harness.envBuilder = taskenv.NewBuilder(harness.node, a, task, "global")
harness.envBuilder.SetClientTaskRoot(harness.taskDir)
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 := os.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_Static_NomadEnv(t *testing.T) {
ci.Parallel(t)
// 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,
}
a := mock.Alloc()
task := a.Job.TaskGroups[0].Tasks[0]
task.Name = TestTaskName
harness := newTestHarness(t, []*structs.Template{template}, false, false)
harness.envBuilder = taskenv.NewBuilder(harness.node, a, task, "global")
harness.envBuilder.SetClientTaskRoot(harness.taskDir)
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 := os.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) {
ci.Parallel(t)
// Make a template that will render immediately
content := "hello, world!"
file := "my.tmpl"
template := &structs.Template{
EmbeddedTmpl: content,
DestPath: file,
ChangeMode: structs.TemplateChangeModeNoop,
}
a := mock.Alloc()
task := a.Job.TaskGroups[0].Tasks[0]
task.Name = TestTaskName
harness := newTestHarness(t, []*structs.Template{template}, false, false)
harness.envBuilder = taskenv.NewBuilder(harness.node, a, task, "global")
harness.envBuilder.SetClientTaskRoot(harness.taskDir)
// Write the contents
path := filepath.Join(harness.taskDir, file)
must.NoError(t, os.WriteFile(path, []byte(content), 0777))
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 := os.ReadFile(path)
must.NoError(t, err, must.Sprintf(
"Failed to read rendered template from %q", path))
must.Eq(t, content, string(raw), must.Sprint("Unexpected template data"))
}
func TestTaskTemplateManager_Unblock_Consul(t *testing.T) {
ci.Parallel(t)
clienttestutil.RequireConsul(t)
// 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 := os.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) {
ci.Parallel(t)
clienttestutil.RequireVault(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}})
must.NoError(t, 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 := os.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) {
ci.Parallel(t)
clienttestutil.RequireConsul(t)
// 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 := os.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 = os.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)
}
}
// TestTaskTemplateManager_FirstRender_Restored tests that a task that's been
// restored renders and triggers its change mode if the template has changed
func TestTaskTemplateManager_FirstRender_Restored(t *testing.T) {
ci.Parallel(t)
clienttestutil.RequireVault(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.TemplateChangeModeRestart,
}
harness := newTestHarness(t, []*structs.Template{template}, false, true)
harness.start(t)
defer harness.stop()
// Ensure no unblock
select {
case <-harness.mockHooks.UnblockCh:
t.Fatal("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}})
must.NoError(t, err)
// Wait for the unblock
select {
case <-harness.mockHooks.UnblockCh:
case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second):
t.Fatal("Task unblock should have been called")
}
// Check the file is there
path := filepath.Join(harness.taskDir, file)
raw, err := os.ReadFile(path)
must.NoError(t, err, must.Sprintf("Failed to read rendered template from %q", path))
must.Eq(t, content, string(raw), must.Sprintf("Unexpected template data; got %s, want %q", raw, content))
// task is now running
harness.mockHooks.HasHandle = true
// simulate a client restart
harness.manager.Stop()
harness.mockHooks.UnblockCh = make(chan struct{}, 1)
harness.start(t)
// Wait for the unblock
select {
case <-harness.mockHooks.UnblockCh:
case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second):
t.Fatal("Task unblock should have been called")
}
select {
case <-harness.mockHooks.RestartCh:
t.Fatal("should not have restarted", harness.mockHooks)
case <-harness.mockHooks.SignalCh:
t.Fatal("should not have received signal", harness.mockHooks)
case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second):
}
// simulate a client restart and TTL expiry
harness.manager.Stop()
content = "bazbar"
_, err = logical.Write(vaultPath, map[string]interface{}{"data": map[string]interface{}{key: content}})
must.NoError(t, err)
harness.mockHooks.UnblockCh = make(chan struct{}, 1)
harness.start(t)
// Wait for the unblock
select {
case <-harness.mockHooks.UnblockCh:
case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second):
t.Fatal("Task unblock should have been called")
}
// 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.Fatal("Signal with restart policy", harness.mockHooks)
case <-timeout:
t.Fatal("Should have received a restart", harness.mockHooks)
}
}
}
// Tests an edge case where a task has multiple templates and the client is restarted.
// In this case, consul-template may re-render and overwrite some fields in the first
// render event but we still want to make sure it causes a restart.
// We cannot control the order in which these templates are rendered, so this test will
// exhibit flakiness if this edge case is not properly handled.
func TestTaskTemplateManager_FirstRender_MultiSecret(t *testing.T) {
ci.Parallel(t)
clienttestutil.RequireVault(t)
// Make a template that will render based on a key in Vault
vaultPath := "secret/data/restart"
key := "shouldRestart"
content := "shouldRestart"
embedded := fmt.Sprintf(`{{with secret "%s"}}{{.Data.data.%s}}{{end}}`, vaultPath, key)
file := "my.tmpl"
template := &structs.Template{
EmbeddedTmpl: embedded,
DestPath: file,
ChangeMode: structs.TemplateChangeModeRestart,
}
vaultPath2 := "secret/data/noop"
key2 := "noop"
content2 := "noop"
embedded2 := fmt.Sprintf(`{{with secret "%s"}}{{.Data.data.%s}}{{end}}`, vaultPath2, key2)
file2 := "my.tmpl2"
template2 := &structs.Template{
EmbeddedTmpl: embedded2,
DestPath: file2,
ChangeMode: structs.TemplateChangeModeNoop,
}
harness := newTestHarness(t, []*structs.Template{template, template2}, false, true)
// Write the secret to Vault
logical := harness.vault.Client.Logical()
_, err := logical.Write(vaultPath, map[string]interface{}{"data": map[string]interface{}{key: content}})
must.NoError(t, err)
_, err = logical.Write(vaultPath2, map[string]interface{}{"data": map[string]interface{}{key2: content2}})
must.NoError(t, err)
// simulate task is running already
harness.mockHooks.HasHandle = true
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.Fatal("Task unblock should have been called")
}
select {
case <-harness.mockHooks.RestartCh:
case <-harness.mockHooks.SignalCh:
t.Fatal("should not have received signal", harness.mockHooks)
case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second):
t.Fatal("should have restarted")
}
}
func TestTaskTemplateManager_Rerender_Noop(t *testing.T) {
ci.Parallel(t)
clienttestutil.RequireConsul(t)
// 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 := os.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 = os.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) {
ci.Parallel(t)
clienttestutil.RequireConsul(t)
// 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:
s := harness.mockHooks.Signals()
if len(s) != 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 := os.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 = os.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) {
ci.Parallel(t)
clienttestutil.RequireConsul(t)
// 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 := os.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) {
ci.Parallel(t)
// 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.config.TemplateConfig.DisableSandbox = true // no real alloc in this test
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 := os.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) {
ci.Parallel(t)
clienttestutil.RequireConsul(t)
// 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)
}
must.NotNil(t, harness.mockHooks.KillEvent)
must.StrContains(t, harness.mockHooks.KillEvent().DisplayMessage, "failed to send signals")
}
func TestTaskTemplateManager_ScriptExecution(t *testing.T) {
ci.Parallel(t)
clienttestutil.RequireConsul(t)
// Make a template that renders based on a key in Consul and triggers script
key1 := "bam"
key2 := "bar"
content1_1 := "cat"
content1_2 := "dog"
t1 := &structs.Template{
EmbeddedTmpl: `
FOO={{key "bam"}}
`,
DestPath: "test.env",
ChangeMode: structs.TemplateChangeModeScript,
ChangeScript: &structs.ChangeScript{
Command: "/bin/foo",
Args: []string{},
Timeout: 5 * time.Second,
FailOnError: false,
},
Envvars: true,
}
t2 := &structs.Template{
EmbeddedTmpl: `
BAR={{key "bar"}}
`,
DestPath: "test2.env",
ChangeMode: structs.TemplateChangeModeScript,
ChangeScript: &structs.ChangeScript{
Command: "/bin/foo",
Args: []string{},
Timeout: 5 * time.Second,
FailOnError: false,
},
Envvars: true,
}
harness := newTestHarness(t, []*structs.Template{t1, t2}, true, false)
harness.mockHooks.SetupExecTest(0, nil)
harness.start(t)
defer harness.stop()
// Ensure no unblock
select {
case <-harness.mockHooks.UnblockCh:
t.Fatal(t, "Task unblock should 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.Fatal(t, "Task unblock should have been called")
}
// Update the keys in Consul
harness.consul.SetKV(t, key1, []byte(content1_2))
// Wait for script execution
timeout := time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second)
OUTER:
for {
select {
case <-harness.mockHooks.RestartCh:
t.Fatal(t, "restart not expected")
case ev := <-harness.mockHooks.EmitEventCh:
if strings.Contains(ev.DisplayMessage, t1.ChangeScript.Command) {
break OUTER
}
case <-harness.mockHooks.SignalCh:
t.Fatal(t, "signal not expected")
case <-timeout:
t.Fatal(t, "should have received an event")
}
}
}
// TestTaskTemplateManager_ScriptExecutionFailTask tests whether we fail the
// task upon script execution failure if that's how it's configured.
func TestTaskTemplateManager_ScriptExecutionFailTask(t *testing.T) {
ci.Parallel(t)
clienttestutil.RequireConsul(t)
// Make a template that renders based on a key in Consul and triggers script
key1 := "bam"
key2 := "bar"
content1_1 := "cat"
content1_2 := "dog"
t1 := &structs.Template{
EmbeddedTmpl: `
FOO={{key "bam"}}
`,
DestPath: "test.env",
ChangeMode: structs.TemplateChangeModeScript,
ChangeScript: &structs.ChangeScript{
Command: "/bin/foo",
Args: []string{},
Timeout: 5 * time.Second,
FailOnError: true,
},
Envvars: true,
}
t2 := &structs.Template{
EmbeddedTmpl: `
BAR={{key "bar"}}
`,
DestPath: "test2.env",
ChangeMode: structs.TemplateChangeModeScript,
ChangeScript: &structs.ChangeScript{
Command: "/bin/foo",
Args: []string{},
Timeout: 5 * time.Second,
FailOnError: false,
},
Envvars: true,
}
harness := newTestHarness(t, []*structs.Template{t1, t2}, true, false)
harness.mockHooks.SetupExecTest(1, fmt.Errorf("Script failed"))
harness.start(t)
defer harness.stop()
// Ensure no unblock
select {
case <-harness.mockHooks.UnblockCh:
t.Fatal("Task unblock should 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.Fatal("Task unblock should have been called")
}
// Update the keys in Consul
harness.consul.SetKV(t, key1, []byte(content1_2))
// 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)
}
must.NotNil(t, harness.mockHooks.KillEvent)
must.StrContains(t, harness.mockHooks.KillEvent().DisplayMessage, "task is being killed")
}
func TestTaskTemplateManager_ChangeModeMixed(t *testing.T) {
ci.Parallel(t)
clienttestutil.RequireConsul(t)
templateRestart := &structs.Template{
EmbeddedTmpl: `
RESTART={{key "restart"}}
COMMON={{key "common"}}
`,
DestPath: "restart",
ChangeMode: structs.TemplateChangeModeRestart,
}
templateSignal := &structs.Template{
EmbeddedTmpl: `
SIGNAL={{key "signal"}}
COMMON={{key "common"}}
`,
DestPath: "signal",
ChangeMode: structs.TemplateChangeModeSignal,
ChangeSignal: "SIGALRM",
}
templateScript := &structs.Template{
EmbeddedTmpl: `
SCRIPT={{key "script"}}
COMMON={{key "common"}}
`,
DestPath: "script",
ChangeMode: structs.TemplateChangeModeScript,
ChangeScript: &structs.ChangeScript{
Command: "/bin/foo",
Args: []string{},
Timeout: 5 * time.Second,
FailOnError: true,
},
}
templates := []*structs.Template{
templateRestart,
templateSignal,
templateScript,
}
harness := newTestHarness(t, templates, true, false)
harness.mockHooks.SetupExecTest(0, nil)
harness.start(t)
defer harness.stop()
// Ensure no unblock
select {
case <-harness.mockHooks.UnblockCh:
t.Fatal(t, "Task unblock should not have been called")
case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second):
}
// Write the key to Consul
harness.consul.SetKV(t, "common", []byte(fmt.Sprintf("%v", time.Now())))
harness.consul.SetKV(t, "restart", []byte(fmt.Sprintf("%v", time.Now())))
harness.consul.SetKV(t, "signal", []byte(fmt.Sprintf("%v", time.Now())))
harness.consul.SetKV(t, "script", []byte(fmt.Sprintf("%v", time.Now())))
// Wait for the unblock
select {
case <-harness.mockHooks.UnblockCh:
case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second):
t.Fatal(t, "Task unblock should have been called")
}
t.Run("restart takes precedence", func(t *testing.T) {
// Update the common Consul key.
harness.consul.SetKV(t, "common", []byte(fmt.Sprintf("%v", time.Now())))
// Collect some events.
timeout := time.After(time.Duration(3*testutil.TestMultiplier()) * time.Second)
events := []*structs.TaskEvent{}
OUTER:
for {
select {
case <-harness.mockHooks.RestartCh:
// Consume restarts so the channel is clean for other tests.
case <-harness.mockHooks.SignalCh:
t.Fatal(t, "signal not expected")
case ev := <-harness.mockHooks.EmitEventCh:
events = append(events, ev)
case <-timeout:
break OUTER
}
}
for _, ev := range events {
must.StrNotContains(t, ev.DisplayMessage, templateScript.ChangeScript.Command)
must.StrNotContains(t, ev.Type, structs.TaskSignaling)
}
})
t.Run("signal and script", func(t *testing.T) {
// Update the signal and script Consul keys.
harness.consul.SetKV(t, "signal", []byte(fmt.Sprintf("%v", time.Now())))
harness.consul.SetKV(t, "script", []byte(fmt.Sprintf("%v", time.Now())))
// Wait for a events.
var gotSignal, gotScript bool
timeout := time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second)
for {
select {
case <-harness.mockHooks.RestartCh:
t.Fatal(t, "restart not expected")
case ev := <-harness.mockHooks.EmitEventCh:
if strings.Contains(ev.DisplayMessage, templateScript.ChangeScript.Command) {
// Make sure we only run script once.
must.False(t, gotScript)
gotScript = true
}
case <-harness.mockHooks.SignalCh:
// Make sure we only signal once.
must.False(t, gotSignal)
gotSignal = true
case <-timeout:
t.Fatal(t, "timeout waiting for script and signal")
}
if gotScript && gotSignal {
break
}
}
})
}
// TestTaskTemplateManager_FiltersEnvVars asserts that we only render
// environment variables found in task env-vars and not read the nomad host
// process environment variables. nomad host process environment variables are
// to be treated the same as not found environment variables.
func TestTaskTemplateManager_FiltersEnvVars(t *testing.T) {
t.Setenv("NOMAD_TASK_NAME", "should be overridden by task")
testenv := "TESTENV_" + strings.ReplaceAll(uuid.Generate(), "-", "")
t.Setenv(testenv, "MY_TEST_VALUE")
// Make a template that will render immediately
content := `Hello Nomad Task: {{env "NOMAD_TASK_NAME"}}
TEST_ENV: {{ env "` + testenv + `" }}
TEST_ENV_NOT_FOUND: {{env "` + testenv + `_NOTFOUND" }}`
expected := fmt.Sprintf("Hello Nomad Task: %s\nTEST_ENV: \nTEST_ENV_NOT_FOUND: ", TestTaskName)
file := "my.tmpl"
template := &structs.Template{
EmbeddedTmpl: content,
DestPath: file,
ChangeMode: structs.TemplateChangeModeNoop,
}
harness := newTestHarness(t, []*structs.Template{template}, false, false)
harness.config.TemplateConfig.DisableSandbox = true // no real alloc in this test
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.Fatal(t, "Task unblock should have been called")
}
// Check the file is there
path := filepath.Join(harness.taskDir, file)
raw, err := os.ReadFile(path)
must.NoError(t, err)
must.Eq(t, expected, string(raw))
}
// TestTaskTemplateManager_Env asserts templates with the env flag set are read
// into the task's environment.
func TestTaskTemplateManager_Env(t *testing.T) {
ci.Parallel(t)
clienttestutil.RequireConsul(t)
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.config.TemplateConfig.DisableSandbox = true // no real alloc in this test
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) {
ci.Parallel(t)
d := t.TempDir()
// Fake writing the file so we don't have to run the whole template manager
err := os.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,
},
}
taskEnv := taskenv.NewEmptyBuilder().SetClientTaskRoot(d).Build()
if vars, err := loadTemplateEnv(templates, taskEnv); err == nil {
t.Fatalf("expected an error but instead got env vars: %#v", vars)
}
}
// TestTaskTemplateManager_Env_InterpolatedDest asserts the core env
// template processing function handles interpolated destinations
func TestTaskTemplateManager_Env_InterpolatedDest(t *testing.T) {
ci.Parallel(t)
d := t.TempDir()
// Fake writing the file so we don't have to run the whole template manager
err := os.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: "${NOMAD_META_path}.env",
Envvars: true,
},
}
// Build the env
taskEnv := taskenv.NewTaskEnv(
map[string]string{"NOMAD_META_path": "exists"},
map[string]string{"NOMAD_META_path": "exists"},
map[string]string{},
map[string]string{},
d, "")
vars, err := loadTemplateEnv(templates, taskEnv)
must.NoError(t, err)
must.MapContainsKey(t, vars, "FOO")
must.Eq(t, "bar", vars["FOO"])
}
// 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) {
ci.Parallel(t)
d := t.TempDir()
// Fake writing the files so we don't have to run the whole template manager
err := os.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 = os.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,
},
}
taskEnv := taskenv.NewEmptyBuilder().SetClientTaskRoot(d).Build()
vars, err := loadTemplateEnv(templates, taskEnv)
if err != nil {
t.Fatalf("expected no error: %v", err)
}
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) {
ci.Parallel(t)
clienttestutil.RequireConsul(t)
// 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) {
ci.Parallel(t)
c := config.DefaultConfig()
c.Node = mock.Node()
c.VaultConfigs = map[string]*sconfig.VaultConfig{
structs.VaultDefaultCluster: {
Enabled: pointer.Of(true),
Addr: "https://localhost/",
TLSServerName: "notlocalhost",
},
}
config := &TaskTemplateManagerConfig{
ClientConfig: c,
VaultToken: "token",
VaultConfig: c.GetDefaultVault(),
TaskID: uuid.Generate(),
}
ctconf, err := newRunnerConfig(config, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if *ctconf.Vault.SSL.ServerName != c.GetDefaultVault().TLSServerName {
t.Fatalf("expected %q but found %q", c.GetDefaultVault().TLSServerName, *ctconf.Vault.SSL.ServerName)
}
}
// TestTaskTemplateManager_Config_VaultNamespace asserts the Vault namespace setting is
// propagated to consul-template's configuration.
func TestTaskTemplateManager_Config_VaultNamespace(t *testing.T) {
ci.Parallel(t)
testNS := "test-namespace"
c := config.DefaultConfig()
c.Node = mock.Node()
c.VaultConfigs = map[string]*sconfig.VaultConfig{
structs.VaultDefaultCluster: {
Enabled: pointer.Of(true),
Addr: "https://localhost/",
TLSServerName: "notlocalhost",
Namespace: testNS,
},
}
alloc := mock.Alloc()
config := &TaskTemplateManagerConfig{
ClientConfig: c,
VaultToken: "token",
VaultConfig: c.GetDefaultVault(),
EnvBuilder: taskenv.NewBuilder(c.Node, alloc, alloc.Job.TaskGroups[0].Tasks[0], c.Region),
TaskID: uuid.Generate(),
}
ctmplMapping, err := parseTemplateConfigs(config)
must.NoError(t, err, must.Sprint("parsing templates"))
ctconf, err := newRunnerConfig(config, ctmplMapping)
must.NoError(t, err, must.Sprint("building runner config"))
must.Eq(t, testNS, *ctconf.Vault.Namespace, must.Sprintf("Vault Namespace Value"))
}
// TestTaskTemplateManager_Config_VaultNamespace asserts the Vault namespace setting is
// propagated to consul-template's configuration.
func TestTaskTemplateManager_Config_VaultNamespace_TaskOverride(t *testing.T) {
ci.Parallel(t)
testNS := "test-namespace"
c := config.DefaultConfig()
c.Node = mock.Node()
c.VaultConfigs = map[string]*sconfig.VaultConfig{
structs.VaultDefaultCluster: {
Enabled: pointer.Of(true),
Addr: "https://localhost/",
TLSServerName: "notlocalhost",
Namespace: testNS,
},
}
alloc := mock.Alloc()
overriddenNS := "new-namespace"
// Set the template manager config vault namespace
config := &TaskTemplateManagerConfig{
ClientConfig: c,
VaultToken: "token",
VaultConfig: c.GetDefaultVault(),
VaultNamespace: overriddenNS,
EnvBuilder: taskenv.NewBuilder(c.Node, alloc, alloc.Job.TaskGroups[0].Tasks[0], c.Region),
TaskID: uuid.Generate(),
}
ctmplMapping, err := parseTemplateConfigs(config)
must.NoError(t, err, must.Sprint("parsing templates"))
ctconf, err := newRunnerConfig(config, ctmplMapping)
must.NoError(t, err, must.Sprint("building runner config"))
must.Eq(t, overriddenNS, *ctconf.Vault.Namespace, must.Sprintf("Vault Namespace Value"))
}
// TestTaskTemplateManager_Escapes asserts that when sandboxing is enabled
// interpolated paths are not incorrectly treated as escaping the alloc dir.
func TestTaskTemplateManager_Escapes(t *testing.T) {
ci.Parallel(t)
// the specific files paths are different on Linux vs Windows
// TODO(tgross): rewrite this test to allow for platform-specific paths
clienttestutil.RequireNotWindows(t)
clientConf := config.DefaultConfig()
must.False(t, clientConf.TemplateConfig.DisableSandbox, must.Sprint("expected sandbox to be enabled"))
// Set a fake alloc dir to make test output more realistic
clientConf.AllocDir = "/fake/allocdir"
clientConf.Node = mock.Node()
alloc := mock.Alloc()
task := alloc.Job.TaskGroups[0].Tasks[0]
logger := testlog.HCLogger(t)
allocDir := allocdir.NewAllocDir(logger, clientConf.AllocDir, clientConf.AllocMountsDir, alloc.ID)
taskDir := allocDir.NewTaskDir(task)
containerEnv := func() *taskenv.Builder {
// To emulate a Docker or exec tasks we must copy the
// Set{Alloc,Task,Secrets}Dir logic in taskrunner/task_dir_hook.go
b := taskenv.NewBuilder(clientConf.Node, alloc, task, clientConf.Region)
b.SetAllocDir(allocdir.SharedAllocContainerPath)
b.SetTaskLocalDir(allocdir.TaskLocalContainerPath)
b.SetSecretsDir(allocdir.TaskSecretsContainerPath)
b.SetClientTaskRoot(taskDir.Dir)
b.SetClientSharedAllocDir(taskDir.SharedAllocDir)
b.SetClientTaskLocalDir(taskDir.LocalDir)
b.SetClientTaskSecretsDir(taskDir.SecretsDir)
return b
}
rawExecEnv := func() *taskenv.Builder {
// To emulate a unisolated tasks we must copy the
// Set{Alloc,Task,Secrets}Dir logic in taskrunner/task_dir_hook.go
b := taskenv.NewBuilder(clientConf.Node, alloc, task, clientConf.Region)
b.SetAllocDir(taskDir.SharedAllocDir)
b.SetTaskLocalDir(taskDir.LocalDir)
b.SetSecretsDir(taskDir.SecretsDir)
b.SetClientTaskRoot(taskDir.Dir)
b.SetClientSharedAllocDir(taskDir.SharedAllocDir)
b.SetClientTaskLocalDir(taskDir.LocalDir)
b.SetClientTaskSecretsDir(taskDir.SecretsDir)
return b
}
cases := []struct {
Name string
Config func() *TaskTemplateManagerConfig
// Expected paths to be returned if Err is nil
SourcePath string
DestPath string
// Err is the expected error to be returned or nil
Err error
}{
{
Name: "ContainerOk",
Config: func() *TaskTemplateManagerConfig {
return &TaskTemplateManagerConfig{
ClientConfig: clientConf,
TaskDir: taskDir.Dir,
EnvBuilder: containerEnv(),
Templates: []*structs.Template{
{
SourcePath: "${NOMAD_TASK_DIR}/src",
DestPath: "${NOMAD_SECRETS_DIR}/dst",
},
},
}
},
SourcePath: filepath.Join(taskDir.Dir, "local/src"),
DestPath: filepath.Join(taskDir.Dir, "secrets/dst"),
},
{
Name: "ContainerSrcEscapesErr",
Config: func() *TaskTemplateManagerConfig {
return &TaskTemplateManagerConfig{
ClientConfig: clientConf,
TaskDir: taskDir.Dir,
EnvBuilder: containerEnv(),
Templates: []*structs.Template{
{
SourcePath: "/etc/src_escapes",
DestPath: "${NOMAD_SECRETS_DIR}/dst",
},
},
}
},
Err: sourceEscapesErr,
},
{
Name: "ContainerSrcEscapesOk",
Config: func() *TaskTemplateManagerConfig {
unsafeConf := clientConf.Copy()
unsafeConf.TemplateConfig.DisableSandbox = true
return &TaskTemplateManagerConfig{
ClientConfig: unsafeConf,
TaskDir: taskDir.Dir,
EnvBuilder: containerEnv(),
Templates: []*structs.Template{
{
SourcePath: "/etc/src_escapes_ok",
DestPath: "${NOMAD_SECRETS_DIR}/dst",
},
},
}
},
SourcePath: "/etc/src_escapes_ok",
DestPath: filepath.Join(taskDir.Dir, "secrets/dst"),
},
{
Name: "ContainerDstAbsoluteOk",
Config: func() *TaskTemplateManagerConfig {
return &TaskTemplateManagerConfig{
ClientConfig: clientConf,
TaskDir: taskDir.Dir,
EnvBuilder: containerEnv(),
Templates: []*structs.Template{
{
SourcePath: "${NOMAD_TASK_DIR}/src",
DestPath: "/etc/absolutely_relative",
},
},
}
},
SourcePath: filepath.Join(taskDir.Dir, "local/src"),
DestPath: filepath.Join(taskDir.Dir, "etc/absolutely_relative"),
},
{
Name: "ContainerDstAbsoluteEscapesErr",
Config: func() *TaskTemplateManagerConfig {
return &TaskTemplateManagerConfig{
ClientConfig: clientConf,
TaskDir: taskDir.Dir,
EnvBuilder: containerEnv(),
Templates: []*structs.Template{
{
SourcePath: "${NOMAD_TASK_DIR}/src",
DestPath: "../escapes",
},
},
}
},
Err: destEscapesErr,
},
{
Name: "ContainerDstAbsoluteEscapesOk",
Config: func() *TaskTemplateManagerConfig {
unsafeConf := clientConf.Copy()
unsafeConf.TemplateConfig.DisableSandbox = true
return &TaskTemplateManagerConfig{
ClientConfig: unsafeConf,
TaskDir: taskDir.Dir,
EnvBuilder: containerEnv(),
Templates: []*structs.Template{
{
SourcePath: "${NOMAD_TASK_DIR}/src",
DestPath: "../escapes",
},
},
}
},
SourcePath: filepath.Join(taskDir.Dir, "local/src"),
DestPath: filepath.Join(taskDir.Dir, "..", "escapes"),
},
//TODO: Fix this test. I *think* it should pass. The double
// joining of the task dir onto the destination seems like
// a bug. https://github.com/hashicorp/nomad/issues/9389
{
Name: "RawExecOk",
Config: func() *TaskTemplateManagerConfig {
return &TaskTemplateManagerConfig{
ClientConfig: clientConf,
TaskDir: taskDir.Dir,
EnvBuilder: rawExecEnv(),
Templates: []*structs.Template{
{
SourcePath: "${NOMAD_TASK_DIR}/src",
DestPath: "${NOMAD_SECRETS_DIR}/dst",
},
},
}
},
SourcePath: filepath.Join(taskDir.Dir, "local/src"),
DestPath: filepath.Join(taskDir.Dir, "secrets/dst"),
},
{
Name: "RawExecSrcEscapesErr",
Config: func() *TaskTemplateManagerConfig {
return &TaskTemplateManagerConfig{
ClientConfig: clientConf,
TaskDir: taskDir.Dir,
EnvBuilder: rawExecEnv(),
Templates: []*structs.Template{
{
SourcePath: "/etc/src_escapes",
DestPath: "${NOMAD_SECRETS_DIR}/dst",
},
},
}
},
Err: sourceEscapesErr,
},
{
Name: "RawExecDstAbsoluteOk",
Config: func() *TaskTemplateManagerConfig {
return &TaskTemplateManagerConfig{
ClientConfig: clientConf,
TaskDir: taskDir.Dir,
EnvBuilder: rawExecEnv(),
Templates: []*structs.Template{
{
SourcePath: "${NOMAD_TASK_DIR}/src",
DestPath: "/etc/absolutely_relative",
},
},
}
},
SourcePath: filepath.Join(taskDir.Dir, "local/src"),
DestPath: filepath.Join(taskDir.Dir, "etc/absolutely_relative"),
},
}
for i := range cases {
tc := cases[i]
t.Run(tc.Name, func(t *testing.T) {
config := tc.Config()
config.TaskID = uuid.Generate()
mapping, err := parseTemplateConfigs(config)
if tc.Err == nil {
// Ok path
must.NoError(t, err)
must.NotNil(t, mapping)
must.MapLen(t, 1, mapping)
for k := range mapping {
must.Eq(t, tc.SourcePath, *k.Source)
must.Eq(t, tc.DestPath, *k.Destination)
t.Logf("Rendering %s => %s", *k.Source, *k.Destination)
}
} else {
// Err path
test.EqError(t, err, tc.Err.Error())
must.Nil(t, mapping)
}
})
}
}
func TestTaskTemplateManager_BlockedEvents(t *testing.T) {
// The tests sets a template that need keys 0, 1, 2, 3, 4,
// then subsequently sets 0, 1, 2 keys
// then asserts that templates are still blocked on 3 and 4,
// and check that we got the relevant task events
ci.Parallel(t)
clienttestutil.RequireConsul(t)
// 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()
missingKeys := func(e *structs.TaskEvent) ([]string, int) {
missingRexp := regexp.MustCompile(`kv.block\(([0-9]*)\)`)
moreRexp := regexp.MustCompile(`and ([0-9]*) more`)
missingMatch := missingRexp.FindAllStringSubmatch(e.DisplayMessage, -1)
moreMatch := moreRexp.FindAllStringSubmatch(e.DisplayMessage, -1)
missing := make([]string, len(missingMatch))
for i, v := range missingMatch {
missing[i] = v[1]
}
sort.Strings(missing)
more := 0
if len(moreMatch) != 0 {
more, _ = strconv.Atoi(moreMatch[0][1])
}
return missing, more
}
// 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
// assert that all 0-4 keys are missing
must.Len(t, 1, harness.mockHooks.Events())
t.Logf("first message: %v", harness.mockHooks.Events()[0])
missing, more := missingKeys(harness.mockHooks.Events()[0])
must.Eq(t, 5, len(missing)+more)
must.StrContains(t, harness.mockHooks.Events()[0].DisplayMessage, "and 2 more")
// Write 0-2 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
isExpectedFinalEvent := func(e *structs.TaskEvent) bool {
missing, more := missingKeys(e)
return reflect.DeepEqual(missing, []string{"3", "4"}) && more == 0
}
timeout := time.After(time.Second * time.Duration(testutil.TestMultiplier()))
WAIT_LOOP:
for {
select {
case <-harness.mockHooks.UnblockCh:
t.Errorf("Task unblock should have not have been called")
case e := <-harness.mockHooks.EmitEventCh:
t.Logf("received event: %v", e.DisplayMessage)
if isExpectedFinalEvent(e) {
break WAIT_LOOP
}
case <-timeout:
t.Errorf("timeout")
}
}
// Check to see we got a correct message
event := harness.mockHooks.Events()[len(harness.mockHooks.Events())-1]
if !isExpectedFinalEvent(event) {
t.Logf("received all events: %v", pretty.Sprint(harness.mockHooks.Events))
t.Fatalf("bad event, expected only 3 and 5 blocked got: %q", event.DisplayMessage)
}
}
// TestTaskTemplateManager_ClientTemplateConfig_Set asserts that all client level
// configuration is accurately mapped from the client to the TaskTemplateManager
// and that any operator defined boundaries are enforced.
func TestTaskTemplateManager_ClientTemplateConfig_Set(t *testing.T) {
ci.Parallel(t)
testNS := "test-namespace"
clientConfig := config.DefaultConfig()
clientConfig.Node = mock.Node()
clientConfig.VaultConfigs = map[string]*sconfig.VaultConfig{
structs.VaultDefaultCluster: {
Enabled: pointer.Of(true),
Namespace: testNS,
},
}
clientConfig.ConsulConfigs = map[string]*sconfig.ConsulConfig{
structs.ConsulDefaultCluster: {
Namespace: testNS,
}}
// helper to reduce boilerplate
waitConfig := &config.WaitConfig{
Min: pointer.Of(5 * time.Second),
Max: pointer.Of(10 * time.Second),
}
// helper to reduce boilerplate
retryConfig := &config.RetryConfig{
Attempts: pointer.Of(5),
Backoff: pointer.Of(5 * time.Second),
MaxBackoff: pointer.Of(20 * time.Second),
}
clientConfig.TemplateConfig.MaxStale = pointer.Of(5 * time.Second)
clientConfig.TemplateConfig.BlockQueryWaitTime = pointer.Of(60 * time.Second)
clientConfig.TemplateConfig.Wait = waitConfig.Copy()
clientConfig.TemplateConfig.ConsulRetry = retryConfig.Copy()
clientConfig.TemplateConfig.VaultRetry = retryConfig.Copy()
clientConfig.TemplateConfig.NomadRetry = retryConfig.Copy()
alloc := mock.Alloc()
allocWithOverride := mock.Alloc()
allocWithOverride.Job.TaskGroups[0].Tasks[0].Templates = []*structs.Template{
{
Wait: &structs.WaitConfig{
Min: pointer.Of(2 * time.Second),
Max: pointer.Of(12 * time.Second),
},
},
}
cases := []struct {
Name string
ClientTemplateConfig *config.ClientTemplateConfig
TTMConfig *TaskTemplateManagerConfig
ExpectedRunnerConfig *config.Config
ExpectedTemplateConfig *templateconfig.TemplateConfig
}{
{
"basic-wait-config",
&config.ClientTemplateConfig{
MaxStale: pointer.Of(5 * time.Second),
BlockQueryWaitTime: pointer.Of(60 * time.Second),
Wait: waitConfig.Copy(),
ConsulRetry: retryConfig.Copy(),
VaultRetry: retryConfig.Copy(),
NomadRetry: retryConfig.Copy(),
},
&TaskTemplateManagerConfig{
ClientConfig: clientConfig,
ConsulConfig: clientConfig.GetDefaultConsul(),
VaultToken: "token",
VaultConfig: clientConfig.GetDefaultVault(),
EnvBuilder: taskenv.NewBuilder(clientConfig.Node, alloc, alloc.Job.TaskGroups[0].Tasks[0], clientConfig.Region),
},
&config.Config{
TemplateConfig: &config.ClientTemplateConfig{
MaxStale: pointer.Of(5 * time.Second),
BlockQueryWaitTime: pointer.Of(60 * time.Second),
Wait: waitConfig.Copy(),
ConsulRetry: retryConfig.Copy(),
VaultRetry: retryConfig.Copy(),
NomadRetry: retryConfig.Copy(),
},
},
&templateconfig.TemplateConfig{
Wait: &templateconfig.WaitConfig{
Enabled: pointer.Of(true),
Min: pointer.Of(5 * time.Second),
Max: pointer.Of(10 * time.Second),
},
},
},
{
"template-override",
&config.ClientTemplateConfig{
MaxStale: pointer.Of(5 * time.Second),
BlockQueryWaitTime: pointer.Of(60 * time.Second),
Wait: waitConfig.Copy(),
ConsulRetry: retryConfig.Copy(),
VaultRetry: retryConfig.Copy(),
NomadRetry: retryConfig.Copy(),
},
&TaskTemplateManagerConfig{
ClientConfig: clientConfig,
ConsulConfig: clientConfig.GetDefaultConsul(),
VaultToken: "token",
VaultConfig: clientConfig.GetDefaultVault(),
EnvBuilder: taskenv.NewBuilder(clientConfig.Node, allocWithOverride, allocWithOverride.Job.TaskGroups[0].Tasks[0], clientConfig.Region),
},
&config.Config{
TemplateConfig: &config.ClientTemplateConfig{
MaxStale: pointer.Of(5 * time.Second),
BlockQueryWaitTime: pointer.Of(60 * time.Second),
Wait: waitConfig.Copy(),
ConsulRetry: retryConfig.Copy(),
VaultRetry: retryConfig.Copy(),
NomadRetry: retryConfig.Copy(),
},
},
&templateconfig.TemplateConfig{
Wait: &templateconfig.WaitConfig{
Enabled: pointer.Of(true),
Min: pointer.Of(2 * time.Second),
Max: pointer.Of(12 * time.Second),
},
},
},
{
"bounds-override",
&config.ClientTemplateConfig{
MaxStale: pointer.Of(5 * time.Second),
BlockQueryWaitTime: pointer.Of(60 * time.Second),
Wait: waitConfig.Copy(),
WaitBounds: &config.WaitConfig{
Min: pointer.Of(3 * time.Second),
Max: pointer.Of(11 * time.Second),
},
ConsulRetry: retryConfig.Copy(),
VaultRetry: retryConfig.Copy(),
NomadRetry: retryConfig.Copy(),
},
&TaskTemplateManagerConfig{
ClientConfig: clientConfig,
ConsulConfig: clientConfig.GetDefaultConsul(),
VaultToken: "token",
VaultConfig: clientConfig.GetDefaultVault(),
EnvBuilder: taskenv.NewBuilder(clientConfig.Node, allocWithOverride, allocWithOverride.Job.TaskGroups[0].Tasks[0], clientConfig.Region),
Templates: []*structs.Template{
{
Wait: &structs.WaitConfig{
Min: pointer.Of(2 * time.Second),
Max: pointer.Of(12 * time.Second),
},
},
},
},
&config.Config{
TemplateConfig: &config.ClientTemplateConfig{
MaxStale: pointer.Of(5 * time.Second),
BlockQueryWaitTime: pointer.Of(60 * time.Second),
Wait: waitConfig.Copy(),
WaitBounds: &config.WaitConfig{
Min: pointer.Of(3 * time.Second),
Max: pointer.Of(11 * time.Second),
},
ConsulRetry: retryConfig.Copy(),
VaultRetry: retryConfig.Copy(),
NomadRetry: retryConfig.Copy(),
},
},
&templateconfig.TemplateConfig{
Wait: &templateconfig.WaitConfig{
Enabled: pointer.Of(true),
Min: pointer.Of(3 * time.Second),
Max: pointer.Of(11 * time.Second),
},
},
},
}
for _, _case := range cases {
t.Run(_case.Name, func(t *testing.T) {
// monkey patch the client config with the version of the ClientTemplateConfig we want to test.
_case.TTMConfig.ClientConfig.TemplateConfig = _case.ClientTemplateConfig
_case.TTMConfig.TaskID = uuid.Generate()
templateMapping, err := parseTemplateConfigs(_case.TTMConfig)
must.NoError(t, err)
runnerConfig, err := newRunnerConfig(_case.TTMConfig, templateMapping)
must.NoError(t, err)
// Direct properties
must.Eq(t, *_case.ExpectedRunnerConfig.TemplateConfig.MaxStale, *runnerConfig.MaxStale)
must.Eq(t, *_case.ExpectedRunnerConfig.TemplateConfig.BlockQueryWaitTime, *runnerConfig.BlockQueryWaitTime)
// WaitConfig
must.Eq(t, *_case.ExpectedRunnerConfig.TemplateConfig.Wait.Min, *runnerConfig.Wait.Min)
must.Eq(t, *_case.ExpectedRunnerConfig.TemplateConfig.Wait.Max, *runnerConfig.Wait.Max)
// Consul Retry
must.NotNil(t, runnerConfig.Consul)
must.NotNil(t, runnerConfig.Consul.Retry)
must.Eq(t, *_case.ExpectedRunnerConfig.TemplateConfig.ConsulRetry.Attempts, *runnerConfig.Consul.Retry.Attempts)
must.Eq(t, *_case.ExpectedRunnerConfig.TemplateConfig.ConsulRetry.Backoff, *runnerConfig.Consul.Retry.Backoff)
must.Eq(t, *_case.ExpectedRunnerConfig.TemplateConfig.ConsulRetry.MaxBackoff, *runnerConfig.Consul.Retry.MaxBackoff)
// Vault Retry
must.NotNil(t, runnerConfig.Vault)
must.NotNil(t, runnerConfig.Vault.Retry)
must.Eq(t, *_case.ExpectedRunnerConfig.TemplateConfig.VaultRetry.Attempts, *runnerConfig.Vault.Retry.Attempts)
must.Eq(t, *_case.ExpectedRunnerConfig.TemplateConfig.VaultRetry.Backoff, *runnerConfig.Vault.Retry.Backoff)
must.Eq(t, *_case.ExpectedRunnerConfig.TemplateConfig.VaultRetry.MaxBackoff, *runnerConfig.Vault.Retry.MaxBackoff)
// Nomad Retry
must.NotNil(t, runnerConfig.Nomad)
must.NotNil(t, runnerConfig.Nomad.Retry)
must.Eq(t, *_case.ExpectedRunnerConfig.TemplateConfig.NomadRetry.Attempts, *runnerConfig.Nomad.Retry.Attempts)
must.Eq(t, *_case.ExpectedRunnerConfig.TemplateConfig.NomadRetry.Backoff, *runnerConfig.Nomad.Retry.Backoff)
must.Eq(t, *_case.ExpectedRunnerConfig.TemplateConfig.NomadRetry.MaxBackoff, *runnerConfig.Nomad.Retry.MaxBackoff)
// Test that wait_bounds are enforced
for _, tmpl := range *runnerConfig.Templates {
must.Eq(t, *_case.ExpectedTemplateConfig.Wait.Enabled, *tmpl.Wait.Enabled)
must.Eq(t, *_case.ExpectedTemplateConfig.Wait.Min, *tmpl.Wait.Min)
must.Eq(t, *_case.ExpectedTemplateConfig.Wait.Max, *tmpl.Wait.Max)
}
})
}
}
// TestTaskTemplateManager_Template_Wait_Set asserts that all template level
// configuration is accurately mapped from the template to the TaskTemplateManager's
// template config.
func TestTaskTemplateManager_Template_Wait_Set(t *testing.T) {
ci.Parallel(t)
c := config.DefaultConfig()
c.Node = mock.Node()
alloc := mock.Alloc()
ttmConfig := &TaskTemplateManagerConfig{
ClientConfig: c,
VaultToken: "token",
EnvBuilder: taskenv.NewBuilder(c.Node, alloc, alloc.Job.TaskGroups[0].Tasks[0], c.Region),
Templates: []*structs.Template{
{
Wait: &structs.WaitConfig{
Min: pointer.Of(5 * time.Second),
Max: pointer.Of(10 * time.Second),
},
},
},
TaskID: uuid.Generate(),
}
templateMapping, err := parseTemplateConfigs(ttmConfig)
must.NoError(t, err)
for k, _ := range templateMapping {
must.True(t, *k.Wait.Enabled)
must.Eq(t, 5*time.Second, *k.Wait.Min)
must.Eq(t, 10*time.Second, *k.Wait.Max)
}
}
// TestTaskTemplateManager_Template_ErrMissingKey_Set asserts that all template level
// configuration is accurately mapped from the template to the TaskTemplateManager's
// template config.
func TestTaskTemplateManager_Template_ErrMissingKey_Set(t *testing.T) {
ci.Parallel(t)
c := config.DefaultConfig()
c.Node = mock.Node()
alloc := mock.Alloc()
ttmConfig := &TaskTemplateManagerConfig{
ClientConfig: c,
VaultToken: "token",
EnvBuilder: taskenv.NewBuilder(c.Node, alloc, alloc.Job.TaskGroups[0].Tasks[0], c.Region),
Templates: []*structs.Template{
{
EmbeddedTmpl: "test-false",
ErrMissingKey: false,
},
{
EmbeddedTmpl: "test-true",
ErrMissingKey: true,
},
},
TaskID: uuid.Generate(),
}
templateMapping, err := parseTemplateConfigs(ttmConfig)
must.NoError(t, err)
for k, tmpl := range templateMapping {
if tmpl.EmbeddedTmpl == "test-false" {
must.False(t, *k.ErrMissingKey)
}
if tmpl.EmbeddedTmpl == "test-true" {
must.True(t, *k.ErrMissingKey)
}
}
}
// TestTaskTemplateManager_writeToFile_Disabled asserts the consul-template function
// writeToFile is disabled by default.
func TestTaskTemplateManager_writeToFile_Disabled(t *testing.T) {
ci.Parallel(t)
file := "my.tmpl"
template := &structs.Template{
EmbeddedTmpl: `Testing writeToFile...
{{ "if i exist writeToFile is enabled" | writeToFile "/tmp/NOMAD-TEST-SHOULD-NOT-EXIST" "" "" "0644" }}
...done
`,
DestPath: file,
ChangeMode: structs.TemplateChangeModeNoop,
}
harness := newTestHarness(t, []*structs.Template{template}, false, false)
must.NoError(t, harness.startWithErr(), must.Sprint("couldn't setup initial harness"))
defer harness.stop()
// Using writeToFile should cause a kill
select {
case <-harness.mockHooks.UnblockCh:
t.Fatalf("Task unblock should have not have been called")
case <-harness.mockHooks.EmitEventCh:
t.Fatalf("Task event should not have been emitted")
case e := <-harness.mockHooks.KillCh:
must.StrContains(t, e.DisplayMessage, "writeToFile: function is disabled")
case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second):
t.Fatalf("timeout")
}
// Check the file is not there
path := filepath.Join(harness.taskDir, file)
_, err := os.ReadFile(path)
must.Error(t, err)
}
// TestTaskTemplateManager_writeToFile asserts the consul-template function
// writeToFile can be enabled.
func TestTaskTemplateManager_writeToFile(t *testing.T) {
ci.Parallel(t)
clienttestutil.RequireLinux(t)
cu, err := users.Current()
must.NoError(t, err)
file := "my.tmpl"
template := &structs.Template{
// EmbeddedTmpl set below as it needs the taskDir
DestPath: file,
ChangeMode: structs.TemplateChangeModeNoop,
}
harness := newTestHarness(t, []*structs.Template{template}, false, false)
// Add template now that we know the taskDir
harness.templates[0].EmbeddedTmpl = fmt.Sprintf(`Testing writeToFile...
{{ "hello" | writeToFile "%s" "`+cu.Username+`" "`+cu.Gid+`" "0644" }}
...done
`, filepath.Join(harness.taskDir, "writetofile.out"))
// Enable all funcs
harness.config.TemplateConfig.FunctionDenylist = []string{}
must.NoError(t, harness.startWithErr(), must.Sprint("couldn't setup initial harness"))
defer harness.stop()
// Using writeToFile should not cause a kill
select {
case <-harness.mockHooks.UnblockCh:
case <-harness.mockHooks.EmitEventCh:
t.Fatalf("Task event should not have been emitted")
case e := <-harness.mockHooks.KillCh:
t.Fatalf("Task should not have been killed: %v", e.DisplayMessage)
case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second):
t.Fatalf("timeout")
}
// Check the templated file is there
path := filepath.Join(harness.taskDir, file)
r, err := os.ReadFile(path)
must.NoError(t, err)
must.True(t, bytes.HasSuffix(r, []byte("...done\n")), must.Sprint(string(r)))
// Check that writeToFile was allowed
path = filepath.Join(harness.taskDir, "writetofile.out")
r, err = os.ReadFile(path)
must.NoError(t, err)
must.Eq(t, "hello", string(r))
}