template: error on missing key (#15141)

* Support error_on_missing_value for templates
* Update docs for template stanza
This commit is contained in:
Charlie Voiselle 2022-11-04 13:23:01 -04:00 committed by GitHub
parent d7aa37a5c9
commit 79c4478f5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 281 additions and 126 deletions

5
.changelog/14002.txt Normal file
View File

@ -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.
```

View File

@ -769,6 +769,7 @@ func TestJobs_Canonicalize(t *testing.T) {
RightDelim: pointerOf("}}"),
Envvars: pointerOf(false),
VaultGrace: pointerOf(time.Duration(0)),
ErrMissingKey: pointerOf(false),
},
{
SourcePath: pointerOf(""),
@ -782,6 +783,7 @@ func TestJobs_Canonicalize(t *testing.T) {
RightDelim: pointerOf("}}"),
Envvars: pointerOf(true),
VaultGrace: pointerOf(time.Duration(0)),
ErrMissingKey: pointerOf(false),
},
},
},

View File

@ -847,6 +847,7 @@ type Template struct {
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))

View File

@ -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

View File

@ -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) {

View File

@ -1237,6 +1237,7 @@ func ApiTaskToStructsTask(job *structs.Job, group *structs.TaskGroup,
Envvars: *template.Envvars,
VaultGrace: *template.VaultGrace,
Wait: apiWaitConfigToStructsWaitConfig(template.Wait),
ErrMissingKey: *template.ErrMissingKey,
})
}
}

View File

@ -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{

View File

@ -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
@ -473,6 +476,9 @@ func parseTemplates(result *[]*api.Template, list *ast.ObjectList) error {
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{

View File

@ -379,7 +379,10 @@ func TestParse(t *testing.T) {
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"),
@ -397,6 +400,7 @@ func TestParse(t *testing.T) {
Gid: intToPtr(20),
LeftDelim: stringToPtr("--"),
RightDelim: stringToPtr("__"),
ErrMissingKey: boolToPtr(false),
},
},
Leader: true,

View File

@ -312,6 +312,7 @@ job "binstore-storagelocker" {
splay = "10s"
env = true
vault_grace = "33s"
error_on_missing_key = true
}
template {

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -0,0 +1,9 @@
job "example" {
group "group" {
task "task" {
template {
error_on_missing_key = true
}
}
}
}

View File

@ -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",

View File

@ -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.

View File

@ -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) {

View File

@ -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` <code>([ChangeScript][]: nil)</code> - Configures the script
- `change_script` <code>([`ChangeScript`][]: nil)</code> - 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
"<no value\>".
- `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
@ -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.<br /><br /> 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.<br /><br /> 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