From 79c4478f5b4d6410bba56d7a3672d6e2ee5d5c2b Mon Sep 17 00:00:00 2001 From: Charlie Voiselle <464492+angrycub@users.noreply.github.com> Date: Fri, 4 Nov 2022 13:23:01 -0400 Subject: [PATCH] template: error on missing key (#15141) * Support error_on_missing_value for templates * Update docs for template stanza --- .changelog/14002.txt | 5 + api/jobs_test.go | 46 +++---- api/tasks.go | 35 +++--- .../taskrunner/template/template.go | 1 + .../taskrunner/template/template_test.go | 40 ++++++ command/agent/job_endpoint.go | 31 ++--- command/agent/job_endpoint_test.go | 2 + jobspec/parse_task.go | 12 +- jobspec/parse_test.go | 32 ++--- jobspec/test-fixtures/basic.hcl | 15 +-- jobspec2/parse_job.go | 3 + jobspec2/parse_test.go | 16 +++ .../template-err-missing-key.hcl | 9 ++ nomad/structs/diff_test.go | 21 ++++ nomad/structs/structs.go | 4 + scheduler/util_test.go | 19 +++ .../docs/job-specification/template.mdx | 116 ++++++++++-------- 17 files changed, 281 insertions(+), 126 deletions(-) create mode 100644 .changelog/14002.txt create mode 100644 jobspec2/test-fixtures/template-err-missing-key.hcl diff --git a/.changelog/14002.txt b/.changelog/14002.txt new file mode 100644 index 000000000..6d739a7ab --- /dev/null +++ b/.changelog/14002.txt @@ -0,0 +1,5 @@ +```release-note:improvement +template: Expose per-template configuration for `error_on_missing_key`. This allows jobspec authors to specify that a +template should fail if it references a struct or map key that does not exist. The default value is false and should be +fully backward compatible. +``` diff --git a/api/jobs_test.go b/api/jobs_test.go index f6e3b3094..f036954c4 100644 --- a/api/jobs_test.go +++ b/api/jobs_test.go @@ -758,30 +758,32 @@ func TestJobs_Canonicalize(t *testing.T) { LogConfig: DefaultLogConfig(), Templates: []*Template{ { - SourcePath: pointerOf(""), - DestPath: pointerOf("local/file.yml"), - EmbeddedTmpl: pointerOf("---"), - ChangeMode: pointerOf("restart"), - ChangeSignal: pointerOf(""), - Splay: pointerOf(5 * time.Second), - Perms: pointerOf("0644"), - LeftDelim: pointerOf("{{"), - RightDelim: pointerOf("}}"), - Envvars: pointerOf(false), - VaultGrace: pointerOf(time.Duration(0)), + SourcePath: pointerOf(""), + DestPath: pointerOf("local/file.yml"), + EmbeddedTmpl: pointerOf("---"), + ChangeMode: pointerOf("restart"), + ChangeSignal: pointerOf(""), + Splay: pointerOf(5 * time.Second), + Perms: pointerOf("0644"), + LeftDelim: pointerOf("{{"), + RightDelim: pointerOf("}}"), + Envvars: pointerOf(false), + VaultGrace: pointerOf(time.Duration(0)), + ErrMissingKey: pointerOf(false), }, { - SourcePath: pointerOf(""), - DestPath: pointerOf("local/file.env"), - EmbeddedTmpl: pointerOf("FOO=bar\n"), - ChangeMode: pointerOf("restart"), - ChangeSignal: pointerOf(""), - Splay: pointerOf(5 * time.Second), - Perms: pointerOf("0644"), - LeftDelim: pointerOf("{{"), - RightDelim: pointerOf("}}"), - Envvars: pointerOf(true), - VaultGrace: pointerOf(time.Duration(0)), + SourcePath: pointerOf(""), + DestPath: pointerOf("local/file.env"), + EmbeddedTmpl: pointerOf("FOO=bar\n"), + ChangeMode: pointerOf("restart"), + ChangeSignal: pointerOf(""), + Splay: pointerOf(5 * time.Second), + Perms: pointerOf("0644"), + LeftDelim: pointerOf("{{"), + RightDelim: pointerOf("}}"), + Envvars: pointerOf(true), + VaultGrace: pointerOf(time.Duration(0)), + ErrMissingKey: pointerOf(false), }, }, }, diff --git a/api/tasks.go b/api/tasks.go index c8a1c0b3f..a02e6901b 100644 --- a/api/tasks.go +++ b/api/tasks.go @@ -832,21 +832,22 @@ func (ch *ChangeScript) Canonicalize() { } type Template struct { - SourcePath *string `mapstructure:"source" hcl:"source,optional"` - DestPath *string `mapstructure:"destination" hcl:"destination,optional"` - EmbeddedTmpl *string `mapstructure:"data" hcl:"data,optional"` - ChangeMode *string `mapstructure:"change_mode" hcl:"change_mode,optional"` - ChangeScript *ChangeScript `mapstructure:"change_script" hcl:"change_script,block"` - ChangeSignal *string `mapstructure:"change_signal" hcl:"change_signal,optional"` - Splay *time.Duration `mapstructure:"splay" hcl:"splay,optional"` - Perms *string `mapstructure:"perms" hcl:"perms,optional"` - Uid *int `mapstructure:"uid" hcl:"uid,optional"` - Gid *int `mapstructure:"gid" hcl:"gid,optional"` - LeftDelim *string `mapstructure:"left_delimiter" hcl:"left_delimiter,optional"` - RightDelim *string `mapstructure:"right_delimiter" hcl:"right_delimiter,optional"` - Envvars *bool `mapstructure:"env" hcl:"env,optional"` - VaultGrace *time.Duration `mapstructure:"vault_grace" hcl:"vault_grace,optional"` - Wait *WaitConfig `mapstructure:"wait" hcl:"wait,block"` + SourcePath *string `mapstructure:"source" hcl:"source,optional"` + DestPath *string `mapstructure:"destination" hcl:"destination,optional"` + EmbeddedTmpl *string `mapstructure:"data" hcl:"data,optional"` + ChangeMode *string `mapstructure:"change_mode" hcl:"change_mode,optional"` + ChangeScript *ChangeScript `mapstructure:"change_script" hcl:"change_script,block"` + ChangeSignal *string `mapstructure:"change_signal" hcl:"change_signal,optional"` + Splay *time.Duration `mapstructure:"splay" hcl:"splay,optional"` + Perms *string `mapstructure:"perms" hcl:"perms,optional"` + Uid *int `mapstructure:"uid" hcl:"uid,optional"` + Gid *int `mapstructure:"gid" hcl:"gid,optional"` + LeftDelim *string `mapstructure:"left_delimiter" hcl:"left_delimiter,optional"` + RightDelim *string `mapstructure:"right_delimiter" hcl:"right_delimiter,optional"` + Envvars *bool `mapstructure:"env" hcl:"env,optional"` + VaultGrace *time.Duration `mapstructure:"vault_grace" hcl:"vault_grace,optional"` + Wait *WaitConfig `mapstructure:"wait" hcl:"wait,block"` + ErrMissingKey *bool `mapstructure:"error_on_missing_key" hcl:"error_on_missing_key,optional"` } func (tmpl *Template) Canonicalize() { @@ -890,7 +891,9 @@ func (tmpl *Template) Canonicalize() { if tmpl.Envvars == nil { tmpl.Envvars = pointerOf(false) } - + if tmpl.ErrMissingKey == nil { + tmpl.ErrMissingKey = pointerOf(false) + } //COMPAT(0.12) VaultGrace is deprecated and unused as of Vault 0.5 if tmpl.VaultGrace == nil { tmpl.VaultGrace = pointerOf(time.Duration(0)) diff --git a/client/allocrunner/taskrunner/template/template.go b/client/allocrunner/taskrunner/template/template.go index 91f87aa7b..8538908cc 100644 --- a/client/allocrunner/taskrunner/template/template.go +++ b/client/allocrunner/taskrunner/template/template.go @@ -694,6 +694,7 @@ func parseTemplateConfigs(config *TaskTemplateManagerConfig) (map[*ctconf.Templa ct.Contents = &tmpl.EmbeddedTmpl ct.LeftDelim = &tmpl.LeftDelim ct.RightDelim = &tmpl.RightDelim + ct.ErrMissingKey = &tmpl.ErrMissingKey ct.FunctionDenylist = config.ClientConfig.TemplateConfig.FunctionDenylist if sandboxEnabled { ct.SandboxPath = &config.TaskDir diff --git a/client/allocrunner/taskrunner/template/template_test.go b/client/allocrunner/taskrunner/template/template_test.go index 3e8816362..8cbd8347e 100644 --- a/client/allocrunner/taskrunner/template/template_test.go +++ b/client/allocrunner/taskrunner/template/template_test.go @@ -2471,6 +2471,46 @@ func TestTaskTemplateManager_Template_Wait_Set(t *testing.T) { } } +// 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, + }, + }, + } + + templateMapping, err := parseTemplateConfigs(ttmConfig) + require.NoError(t, err) + + for k, tmpl := range templateMapping { + if tmpl.EmbeddedTmpl == "test-false" { + require.False(t, *k.ErrMissingKey) + } + if tmpl.EmbeddedTmpl == "test-true" { + require.True(t, *k.ErrMissingKey) + } + } +} + // TestTaskTemplateManager_writeToFile_Disabled asserts the consul-template function // writeToFile is disabled by default. func TestTaskTemplateManager_writeToFile_Disabled(t *testing.T) { diff --git a/command/agent/job_endpoint.go b/command/agent/job_endpoint.go index 7a4977da8..a6440b924 100644 --- a/command/agent/job_endpoint.go +++ b/command/agent/job_endpoint.go @@ -1222,21 +1222,22 @@ func ApiTaskToStructsTask(job *structs.Job, group *structs.TaskGroup, for _, template := range apiTask.Templates { structsTask.Templates = append(structsTask.Templates, &structs.Template{ - SourcePath: *template.SourcePath, - DestPath: *template.DestPath, - EmbeddedTmpl: *template.EmbeddedTmpl, - ChangeMode: *template.ChangeMode, - ChangeSignal: *template.ChangeSignal, - ChangeScript: apiChangeScriptToStructsChangeScript(template.ChangeScript), - Splay: *template.Splay, - Perms: *template.Perms, - Uid: template.Uid, - Gid: template.Gid, - LeftDelim: *template.LeftDelim, - RightDelim: *template.RightDelim, - Envvars: *template.Envvars, - VaultGrace: *template.VaultGrace, - Wait: apiWaitConfigToStructsWaitConfig(template.Wait), + SourcePath: *template.SourcePath, + DestPath: *template.DestPath, + EmbeddedTmpl: *template.EmbeddedTmpl, + ChangeMode: *template.ChangeMode, + ChangeSignal: *template.ChangeSignal, + ChangeScript: apiChangeScriptToStructsChangeScript(template.ChangeScript), + Splay: *template.Splay, + Perms: *template.Perms, + Uid: template.Uid, + Gid: template.Gid, + LeftDelim: *template.LeftDelim, + RightDelim: *template.RightDelim, + Envvars: *template.Envvars, + VaultGrace: *template.VaultGrace, + Wait: apiWaitConfigToStructsWaitConfig(template.Wait), + ErrMissingKey: *template.ErrMissingKey, }) } } diff --git a/command/agent/job_endpoint_test.go b/command/agent/job_endpoint_test.go index a0d70423e..163fa5147 100644 --- a/command/agent/job_endpoint_test.go +++ b/command/agent/job_endpoint_test.go @@ -2747,6 +2747,7 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) { Min: pointer.Of(5 * time.Second), Max: pointer.Of(10 * time.Second), }, + ErrMissingKey: pointer.Of(true), }, }, DispatchPayload: &api.DispatchPayloadConfig{ @@ -3160,6 +3161,7 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) { Min: pointer.Of(5 * time.Second), Max: pointer.Of(10 * time.Second), }, + ErrMissingKey: true, }, }, DispatchPayload: &structs.DispatchPayloadConfig{ diff --git a/jobspec/parse_task.go b/jobspec/parse_task.go index 6260e9b67..4465b8f24 100644 --- a/jobspec/parse_task.go +++ b/jobspec/parse_task.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/hcl" "github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/nomad/api" + "github.com/hashicorp/nomad/helper/pointer" "github.com/mitchellh/mapstructure" ) @@ -458,6 +459,8 @@ func parseTemplates(result *[]*api.Template, list *ast.ObjectList) error { "splay", "env", "vault_grace", //COMPAT(0.12) not used; emits warning in 0.11. + "wait", + "error_on_missing_key", } if err := checkHCLKeys(o.Val, valid); err != nil { return err @@ -470,9 +473,12 @@ func parseTemplates(result *[]*api.Template, list *ast.ObjectList) error { delete(m, "change_script") // change_script is its own object templ := &api.Template{ - ChangeMode: stringToPtr("restart"), - Splay: timeToPtr(5 * time.Second), - Perms: stringToPtr("0644"), + ChangeMode: stringToPtr("restart"), + Splay: timeToPtr(5 * time.Second), + Perms: stringToPtr("0644"), + Uid: pointer.Of(-1), + Gid: pointer.Of(-1), + ErrMissingKey: pointer.Of(false), } dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ diff --git a/jobspec/parse_test.go b/jobspec/parse_test.go index 41b99e359..44fb8c196 100644 --- a/jobspec/parse_test.go +++ b/jobspec/parse_test.go @@ -372,14 +372,17 @@ func TestParse(t *testing.T) { }, Templates: []*api.Template{ { - SourcePath: stringToPtr("foo"), - DestPath: stringToPtr("foo"), - ChangeMode: stringToPtr("foo"), - ChangeSignal: stringToPtr("foo"), - Splay: timeToPtr(10 * time.Second), - Perms: stringToPtr("0644"), - Envvars: boolToPtr(true), - VaultGrace: timeToPtr(33 * time.Second), + SourcePath: stringToPtr("foo"), + DestPath: stringToPtr("foo"), + ChangeMode: stringToPtr("foo"), + ChangeSignal: stringToPtr("foo"), + Splay: timeToPtr(10 * time.Second), + Perms: stringToPtr("0644"), + Envvars: boolToPtr(true), + Uid: intToPtr(-1), + Gid: intToPtr(-1), + VaultGrace: timeToPtr(33 * time.Second), + ErrMissingKey: boolToPtr(true), }, { SourcePath: stringToPtr("bar"), @@ -391,12 +394,13 @@ func TestParse(t *testing.T) { Timeout: timeToPtr(5 * time.Second), FailOnError: boolToPtr(false), }, - Splay: timeToPtr(5 * time.Second), - Perms: stringToPtr("777"), - Uid: intToPtr(1001), - Gid: intToPtr(20), - LeftDelim: stringToPtr("--"), - RightDelim: stringToPtr("__"), + Splay: timeToPtr(5 * time.Second), + Perms: stringToPtr("777"), + Uid: intToPtr(1001), + Gid: intToPtr(20), + LeftDelim: stringToPtr("--"), + RightDelim: stringToPtr("__"), + ErrMissingKey: boolToPtr(false), }, }, Leader: true, diff --git a/jobspec/test-fixtures/basic.hcl b/jobspec/test-fixtures/basic.hcl index a749bf91d..e2d86843e 100644 --- a/jobspec/test-fixtures/basic.hcl +++ b/jobspec/test-fixtures/basic.hcl @@ -305,13 +305,14 @@ job "binstore-storagelocker" { } template { - source = "foo" - destination = "foo" - change_mode = "foo" - change_signal = "foo" - splay = "10s" - env = true - vault_grace = "33s" + source = "foo" + destination = "foo" + change_mode = "foo" + change_signal = "foo" + splay = "10s" + env = true + vault_grace = "33s" + error_on_missing_key = true } template { diff --git a/jobspec2/parse_job.go b/jobspec2/parse_job.go index a4ee50343..b3300a636 100644 --- a/jobspec2/parse_job.go +++ b/jobspec2/parse_job.go @@ -111,6 +111,9 @@ func normalizeTemplates(templates []*api.Template) { if t.Splay == nil { t.Splay = pointer.Of(5 * time.Second) } + if t.ErrMissingKey == nil { + t.ErrMissingKey = pointer.Of(false) + } normalizeChangeScript(t.ChangeScript) } } diff --git a/jobspec2/parse_test.go b/jobspec2/parse_test.go index 806412cad..7879b99a3 100644 --- a/jobspec2/parse_test.go +++ b/jobspec2/parse_test.go @@ -1052,3 +1052,19 @@ func TestWaitConfig(t *testing.T) { require.Equal(t, 5*time.Second, *tmpl.Wait.Min) require.Equal(t, 60*time.Second, *tmpl.Wait.Max) } + +func TestErrMissingKey(t *testing.T) { + ci.Parallel(t) + hclBytes, err := os.ReadFile("test-fixtures/template-err-missing-key.hcl") + require.NoError(t, err) + job, err := ParseWithConfig(&ParseConfig{ + Path: "test-fixtures/template-err-missing-key.hcl", + Body: hclBytes, + AllowFS: false, + }) + require.NoError(t, err) + tmpl := job.TaskGroups[0].Tasks[0].Templates[0] + require.NotNil(t, tmpl) + require.NotNil(t, tmpl.ErrMissingKey) + require.True(t, *tmpl.ErrMissingKey) +} diff --git a/jobspec2/test-fixtures/template-err-missing-key.hcl b/jobspec2/test-fixtures/template-err-missing-key.hcl new file mode 100644 index 000000000..60df303a1 --- /dev/null +++ b/jobspec2/test-fixtures/template-err-missing-key.hcl @@ -0,0 +1,9 @@ +job "example" { + group "group" { + task "task" { + template { + error_on_missing_key = true + } + } + } +} diff --git a/nomad/structs/diff_test.go b/nomad/structs/diff_test.go index 467d1f6f1..a7fc697c6 100644 --- a/nomad/structs/diff_test.go +++ b/nomad/structs/diff_test.go @@ -7070,6 +7070,7 @@ func TestTaskDiff(t *testing.T) { Min: pointer.Of(5 * time.Second), Max: pointer.Of(5 * time.Second), }, + ErrMissingKey: false, }, { SourcePath: "foo2", @@ -7113,6 +7114,7 @@ func TestTaskDiff(t *testing.T) { Min: pointer.Of(5 * time.Second), Max: pointer.Of(10 * time.Second), }, + ErrMissingKey: true, }, { SourcePath: "foo3", @@ -7134,6 +7136,7 @@ func TestTaskDiff(t *testing.T) { Min: pointer.Of(5 * time.Second), Max: pointer.Of(10 * time.Second), }, + ErrMissingKey: true, }, }, }, @@ -7150,6 +7153,12 @@ func TestTaskDiff(t *testing.T) { Old: "baz", New: "baz new", }, + { + Type: DiffTypeEdited, + Name: "ErrMissingKey", + Old: "false", + New: "true", + }, }, Objects: []*ObjectDiff{ { @@ -7200,6 +7209,12 @@ func TestTaskDiff(t *testing.T) { Old: "", New: "false", }, + { + Type: DiffTypeAdded, + Name: "ErrMissingKey", + Old: "", + New: "true", + }, { Type: DiffTypeAdded, Name: "Gid", @@ -7330,6 +7345,12 @@ func TestTaskDiff(t *testing.T) { Old: "true", New: "", }, + { + Type: DiffTypeDeleted, + Name: "ErrMissingKey", + Old: "false", + New: "", + }, { Type: DiffTypeDeleted, Name: "Gid", diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index c05f7e1bc..8b7d3f028 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -7739,6 +7739,10 @@ type Template struct { // WaitConfig is used to override the global WaitConfig on a per-template basis Wait *WaitConfig + + // ErrMissingKey is used to control how the template behaves when attempting + // to index a struct or map key that does not exist. + ErrMissingKey bool } // DefaultTemplate returns a default template. diff --git a/scheduler/util_test.go b/scheduler/util_test.go index 4133e9edc..f55f9b520 100644 --- a/scheduler/util_test.go +++ b/scheduler/util_test.go @@ -837,6 +837,25 @@ func TestTasksUpdated(t *testing.T) { j28 := j27.Copy() j28.TaskGroups[0].Tasks[0].CSIPluginConfig.Type = "monolith" require.True(t, tasksUpdated(j27, j28, name)) + + // Compare identical Template ErrMissingKey + j29 := mock.Job() + j29.TaskGroups[0].Tasks[0].Templates = []*structs.Template{ + { + ErrMissingKey: false, + }, + } + j30 := mock.Job() + j30.TaskGroups[0].Tasks[0].Templates = []*structs.Template{ + { + ErrMissingKey: false, + }, + } + require.False(t, tasksUpdated(j29, j30, name)) + + // Compare changed Template ErrMissingKey + j30.TaskGroups[0].Tasks[0].Templates[0].ErrMissingKey = true + require.True(t, tasksUpdated(j29, j30, name)) } func TestTasksUpdated_connectServiceUpdated(t *testing.T) { diff --git a/website/content/docs/job-specification/template.mdx b/website/content/docs/job-specification/template.mdx index 6303f9515..6891f2aea 100644 --- a/website/content/docs/job-specification/template.mdx +++ b/website/content/docs/job-specification/template.mdx @@ -34,10 +34,10 @@ job "docs" { Nomad utilizes [Go template][gt] and a tool called [Consul Template][ct], which adds a set of new functions that can be used to retrieve data from Consul and -Vault. Since Nomad v0.5.3, the template can reference [Nomad's runtime -environment variables][env], and since Nomad v0.5.6, the template can reference -[Node attributes and metadata][nodevars]. Since Nomad v0.6.0, templates can be -read as environment variables. +Vault. Nomad templates can reference [Nomad's runtime +environment variables][env], [node attributes and metadata][nodevars], +[Nomad service registrations][ct_api_nsvc], and [Nomad variables][nvars]. +Templates can also be used to provide environment variables to your workload. For a full list of the API template functions, please refer to the [Consul Template documentation][ct_api]. For a an introduction to Go templates, please @@ -47,7 +47,7 @@ refer to the [Learn Go Template Syntax][gt_learn] guide. - `change_mode` `(string: "restart")` - Specifies the behavior Nomad should take if the rendered template changes. Nomad will always write the new contents of - the template to the specified destination. The possible values below describe + the template to the specified destination. The following possible values describe Nomad's action after writing the template to disk. - `"noop"` - take no action (continue running the task) @@ -59,7 +59,7 @@ refer to the [Learn Go Template Syntax][gt_learn] guide. string like `"SIGUSR1"` or `"SIGINT"`. This option is required if the `change_mode` is `signal`. -- `change_script` ([ChangeScript][]: nil) - Configures the script +- `change_script` ([`ChangeScript`][]: nil) - Configures the script triggered on template change. This option is required if the `change_mode` is `script`. @@ -76,12 +76,22 @@ refer to the [Learn Go Template Syntax][gt_learn] guide. task drivers, see the [Filesystem internals] documentation. - `env` `(bool: false)` - Specifies the template should be read back in as - environment variables for the task ([see below](#environment-variables)). To + environment variables for the task ([example](#environment-variables)). To update the environment on changes, you must set `change_mode` to `restart`. Setting `env` when the `change_mode` is `signal` will return a validation error. Setting `env` when the `change_mode` is `noop` is permitted but will not update the environment variables in the task. +- `error_on_missing_key` `(bool: false)` - Specifies how the template behaves + when attempting to index a map key that does not exist in the map. + + - When `true`, the template engine will return an error, which will cause the + task to fail. + + - When `false`, the template engine will do nothing and continue executing the + template. If printed, the result of the index operation is the string + "". + - `left_delimiter` `(string: "{{")` - Specifies the left delimiter to use in the template. The default is "{{" for some templates, it may be easier to use a different delimiter that does not conflict with the output file itself. @@ -90,7 +100,7 @@ refer to the [Learn Go Template Syntax][gt_learn] guide. File permissions are given as octal of the Unix file permissions `rwxrwxrwx`. - `uid` `(int: nil)` - Specifies the rendered template owner's user ID. If - negative or not specified (`nil`) the ID of the Nomad agent user wil be used. + negative or not specified (`nil`) the ID of the Nomad agent user will be used. ~> **Caveat:** Works only on Unix-based systems. Be careful when using containerized drivers, such as `docker` or `podman`, as groups and users @@ -114,7 +124,7 @@ refer to the [Learn Go Template Syntax][gt_learn] guide. One of `source` or `data` must be specified, but not both. This source can optionally be fetched using an [`artifact`][artifact] resource. This template must exist on the machine prior to starting the task; it is not possible to - reference a template inside a Docker container, for example. + reference a template that's source is inside a Docker container, for example. - `splay` `(string: "5s")` - Specifies a random amount of time to wait between 0 ms and the given splay value before invoking the change mode. This is @@ -129,7 +139,8 @@ refer to the [Learn Go Template Syntax][gt_learn] guide. can be overridden by the [`client.template.wait_bounds`]. If the template configuration has a `min` lower than `client.template.wait_bounds.min` or a `max` greater than `client.template.wait_bounds.max`, the client's bounds will be enforced, - and the template `wait` will be adjusted before being sent to `consul-template`. + and the template `wait` will be adjusted before being sent to the template + engine. ```hcl wait { @@ -192,7 +203,8 @@ template { ### Node Variables -As of Nomad v0.5.6 it is possible to access the Node's attributes and metadata. +Use the `env` function to access the Node's attributes and metadata inside a +template. ```hcl template { @@ -209,10 +221,10 @@ template { ### Environment Variables -Since v0.6.0 templates may be used to create environment variables for tasks. -Env templates work exactly like other templates except once the templates are -written, they are parsed as `KEY=value` pairs. Those key value pairs are -included in the task's environment. +Templates may be used to create environment variables for tasks. These templates +work exactly like other templates except once the templates are written, they +are parsed as `KEY=value` pairs. Those key value pairs are included in the +task's environment. For example the following template stanza: @@ -234,13 +246,13 @@ EOH The task's environment would then have environment variables like the following: -``` +```text LOG_LEVEL=DEBUG API_KEY=12345678-1234-1234-1234-1234-123456789abc ``` This allows [12factor app](https://12factor.net/config) style environment -variable based configuration while keeping all of the familiar features and +variable based configuration while keeping all the familiar features and semantics of Nomad templates. Secrets or certificates may contain a wide variety of characters such as @@ -248,19 +260,19 @@ newlines, quotes, and backslashes which may be difficult to quote or escape properly. Whenever a templated variable may include special characters, use the `toJSON` -function to ensure special characters are properly parsed by Nomad: +function to ensure special characters are properly parsed by Nomad. -``` +```hcl CERT_PEM={{ file "path/to/cert.pem" | toJSON }} ``` The parser will read the JSON string, so the `$CERT_PEM` environment variable will be identical to the contents of the file. -Likewise when evaluating a password that may contain quotes or `#`, use the -`toJSON` function to ensure Nomad passes the password to task unchanged: +Likewise, when evaluating a password that may contain quotes or `#`, use the +`toJSON` function to ensure Nomad passes the password to the task unchanged. -``` +```hcl # Passwords may contain any character including special characters like: # \"'# # Use toJSON to ensure Nomad passes them to the environment unchanged. @@ -278,7 +290,7 @@ filesystem isolation (such as `raw_exec`) or drivers that build a chroot in the task working directory (such as `exec`) can have templates rendered to arbitrary paths in the task. But task drivers such as `docker` can only access templates rendered into the `NOMAD_ALLOC_DIR`, `NOMAD_TASK_DIR`, or -`NOMAD_SECRETS_DIR`. To workaround this restriction, you can create a mount +`NOMAD_SECRETS_DIR`. To work around this restriction, you can create a mount from the template `destination` to another location in the task. ```hcl @@ -380,9 +392,9 @@ The list functions return a slice of `NomadVarMeta` type: ```golang type NomadVarMeta struct { - Namespace, Path string - CreateIndex, ModifyIndex uint64 - CreateTime, ModifyTime nanoTime + Namespace, Path string + CreateIndex, ModifyIndex uint64 + CreateTime, ModifyTime nanoTime } ``` @@ -391,7 +403,7 @@ prints it out as a formatted date/time value. It also has a `Time` method that can be used to retrieve a Go [`time.Time`] for further manipulation. `NomadVarMeta` objects print their `Path` value when used as a string. For -example, these two template blocks produce identical output: +example, these two template blocks produce identical output. ```hcl template { @@ -427,7 +439,7 @@ EOH } ``` -By default the `nomadVarList` will list variables in the same namespace as the +By default, the `nomadVarList` will list variables in the same namespace as the task. The path filter can change the namespace by adding a suffix separated by the `@` character: @@ -445,7 +457,6 @@ EOH The `nomadVarListSafe` function works identically to `nomadVarList`, but refuses to render the template if the variable list query returns blank/empty data. - #### `nomadVar` These functions can be used to a read Nomad variable, assuming the task has @@ -454,12 +465,12 @@ path does not exist or the caller does not have access to it, the `template` renderer will block until the path is available. To avoid blocking, wrap `nomadVar` calls with [`nomadVarExists`](#nomadvarexists). -The `nomadVar` function returns a map of of `string` to `NomadVarItems` +The `nomadVar` function returns a map of `string` to `NomadVarItems` structs. Each member of the map corresponds to a member of the variable's `Items` collection. For example, given a variable exists at the path `nomad/jobs/redis`, with a -single key/value pair in its Items collection — `maxconns`:`15` +single key/value pair in its Items collection—`maxconns`:`15` ```hcl template { @@ -478,11 +489,15 @@ renders Each map value also contains helper methods to get the variable metadata and a link to the parent variable: -* `Keys`: produces a sorted list of keys to this `NomadVarItems` map. -* `Values`: produces a key-sorted list. -* `Tuples`: produces a key-sorted list of K,V tuple structs. -* `Metadata`: returns this collection's parent metadata as a `NomadVarMeta` -* `Parent`: returns a parent object that has a Metadata field referring to the +- `Keys`: produces a sorted list of keys to this `NomadVarItems` map. + +- `Values`: produces a key-sorted list. + +- `Tuples`: produces a key-sorted list of K,V tuple structs. + +- `Metadata`: returns this collection's parent metadata as a `NomadVarMeta` + +- `Parent`: returns a parent object that has a Metadata field referring to the `NomadVarMeta` and an Items field that refers to this `NomadVarItems` object. For example, given a variable exists at the path `nomad/jobs/redis`, you could @@ -501,9 +516,9 @@ EOH } ``` -By default the `nomadVar` will read a variable in the same namespace as the -task. The path filter can change the namespace by adding a suffix separated by -the `@` character: +By default, the `nomadVar` function reads a variable in the same namespace as +the task. The path filter can change the namespace by adding a suffix separated +by the `@` character. ```hcl template { @@ -602,11 +617,11 @@ When generating PKI certificates with Vault, the certificate, private key, and any intermediate certs are all returned as part of the same API call. Most software requires these files be placed in separate files on the system. -~> **Note**: `generate_lease` must be set to `true` (non-default) on the Vault PKI -role.

Failure to do so will cause the template to frequently render a new -certificate, approximately every minute. This creates a significant number of -certificates to be expired in Vault and could ultimately lead to Vault performance -impacts and failures. +~> **Note**: `generate_lease` must be set to `true` (non-default) on the Vault +PKI role.

Failure to do so will cause the template to frequently +render a new certificate, approximately every minute. This creates a significant +number of certificates to be expired in Vault and could ultimately lead to Vault +performance impacts and failures. #### As individual files @@ -711,8 +726,9 @@ Additionally, when using the Vault v2 API, the Vault policies applied to your Nomad jobs will need to grant permissions to `read` under `secret/data/...` rather than `secret/...`. -Similar to KV API v1, if the name of a secret includes the `-` character, you -must access it by index. This secret was set using `vault kv put secret/app db-password=somepassword`. +Like KV API v1, if the name of a secret includes the `-` character, you must +access it by index. This secret was set using +`vault kv put secret/app db-password=somepassword`. ```hcl template { @@ -728,15 +744,15 @@ The `template` block has the following [client configuration options](/docs/configuration/client#options): - `function_denylist` `([]string: ["plugin"])` - Specifies a list of template - rendering functions that should be disallowed in job specs. By default the + rendering functions that should be disallowed in job specs. By default, the `plugin` function is disallowed as it allows running arbitrary commands on the host as root (unless Nomad is configured to run as a non-root user). - `disable_file_sandbox` `(bool: false)` - Allows templates access to arbitrary - files on the client host via the `file` function. By default templates can + files on the client host via the `file` function. By default, templates can access files only within the [task working directory]. -[changescript]: /docs/job-specification/change_script 'Nomad change_script Job Specification' +[`changescript`]: /docs/job-specification/change_script 'Nomad change_script Job Specification' [ct]: https://github.com/hashicorp/consul-template 'Consul Template by HashiCorp' [ct_api]: https://github.com/hashicorp/consul-template/blob/master/docs/templating-language.md 'Consul Template API by HashiCorp' [ct_api_connect]: https://github.com/hashicorp/consul-template/blob/master/docs/templating-language.md#connect 'Consul Template API by HashiCorp - connect' @@ -744,6 +760,8 @@ options](/docs/configuration/client#options): [ct_api_ls]: https://github.com/hashicorp/consul-template/blob/master/docs/templating-language.md#ls 'Consul Template API by HashiCorp - ls' [ct_api_service]: https://github.com/hashicorp/consul-template/blob/master/docs/templating-language.md#service 'Consul Template API by HashiCorp - service' [ct_api_services]: https://github.com/hashicorp/consul-template/blob/master/docs/templating-language.md#services 'Consul Template API by HashiCorp - services' +[ct_api_nsvc]: https://github.com/hashicorp/consul-template/blob/master/docs/templating-language.md#nomadService 'Consul Template API by HashiCorp - nomadService' +[nvars]: /docs/concepts/variablesr 'Nomad Variables' [ct_api_tree]: https://github.com/hashicorp/consul-template/blob/master/docs/templating-language.md#tree 'Consul Template API by HashiCorp - tree' [gt]: https://pkg.go.dev/text/template 'Go template package' [gt_learn]: https://learn.hashicorp.com/tutorials/nomad/go-template-syntax