diff --git a/api/tasks.go b/api/tasks.go index b6b1840723dc..b22938e1ea34 100644 --- a/api/tasks.go +++ b/api/tasks.go @@ -179,6 +179,7 @@ type Template struct { ChangeMode string ChangeSignal string Splay time.Duration + Perms string } type Vault struct { diff --git a/client/consul_template.go b/client/consul_template.go index 1335fd7e216b..0b00a070d7b7 100644 --- a/client/consul_template.go +++ b/client/consul_template.go @@ -5,6 +5,7 @@ import ( "math/rand" "os" "path/filepath" + "strconv" "strings" "sync" "time" @@ -393,6 +394,16 @@ func parseTemplateConfigs(tmpls []*structs.Template, taskDir string, ct.Source = &src ct.Destination = &dest ct.Contents = &tmpl.EmbeddedTmpl + + // Set the permissions + if tmpl.Perms != "" { + v, err := strconv.ParseUint(tmpl.Perms, 8, 12) + if err != nil { + return nil, fmt.Errorf("Failed to parse %q as octal: %v", tmpl.Perms, err) + } + m := os.FileMode(v) + ct.Perms = &m + } ct.Finalize() ctmpls[*ct] = tmpl diff --git a/client/consul_template_test.go b/client/consul_template_test.go index 14b8f077a681..d195b359b056 100644 --- a/client/consul_template_test.go +++ b/client/consul_template_test.go @@ -310,6 +310,40 @@ func TestTaskTemplateManager_Unblock_Static(t *testing.T) { } } +func TestTaskTemplateManager_Permissions(t *testing.T) { + // Make a template that will render immediately + content := "hello, world!" + file := "my.tmpl" + template := &structs.Template{ + EmbeddedTmpl: content, + DestPath: file, + ChangeMode: structs.TemplateChangeModeNoop, + Perms: "777", + } + + harness := newTestHarness(t, []*structs.Template{template}, false, false) + harness.start(t) + defer harness.stop() + + // Wait for the unblock + select { + case <-harness.mockHooks.UnblockCh: + case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): + t.Fatalf("Task unblock should have been called") + } + + // Check the file is there + path := filepath.Join(harness.taskDir, file) + fi, err := os.Stat(path) + if err != nil { + t.Fatalf("Failed to stat file: %v", err) + } + + if m := fi.Mode(); m != os.ModePerm { + t.Fatalf("Got mode %v; want %v", m, os.ModePerm) + } +} + func TestTaskTemplateManager_Unblock_Static_NomadEnv(t *testing.T) { // Make a template that will render immediately content := `Hello Nomad Task: {{env "NOMAD_TASK_NAME"}}` diff --git a/jobspec/parse.go b/jobspec/parse.go index 1bb6c2bd38b2..e109854c4ca9 100644 --- a/jobspec/parse.go +++ b/jobspec/parse.go @@ -854,13 +854,13 @@ func parseTemplates(result *[]*structs.Template, list *ast.ObjectList) error { for _, o := range list.Elem().Items { // Check for invalid keys valid := []string{ - "source", - "destination", - "data", "change_mode", "change_signal", + "data", + "destination", + "perms", + "source", "splay", - "once", } if err := checkHCLKeys(o.Val, valid); err != nil { return err diff --git a/jobspec/parse_test.go b/jobspec/parse_test.go index 7bd404a55ec7..1e3485b696a1 100644 --- a/jobspec/parse_test.go +++ b/jobspec/parse_test.go @@ -170,6 +170,7 @@ func TestParse(t *testing.T) { ChangeMode: "foo", ChangeSignal: "foo", Splay: 10 * time.Second, + Perms: "0644", }, { SourcePath: "bar", @@ -177,6 +178,7 @@ func TestParse(t *testing.T) { ChangeMode: structs.TemplateChangeModeRestart, ChangeSignal: "", Splay: 5 * time.Second, + Perms: "777", }, }, }, diff --git a/jobspec/test-fixtures/basic.hcl b/jobspec/test-fixtures/basic.hcl index f25d9a87b3d1..b70ad03dd072 100644 --- a/jobspec/test-fixtures/basic.hcl +++ b/jobspec/test-fixtures/basic.hcl @@ -145,6 +145,7 @@ job "binstore-storagelocker" { template { source = "bar" destination = "bar" + perms = "777" } } diff --git a/nomad/structs/diff_test.go b/nomad/structs/diff_test.go index b5ca58a78fca..852e91db51e5 100644 --- a/nomad/structs/diff_test.go +++ b/nomad/structs/diff_test.go @@ -3544,6 +3544,7 @@ func TestTaskDiff(t *testing.T) { ChangeMode: "bam", ChangeSignal: "SIGHUP", Splay: 1, + Perms: "0644", }, { SourcePath: "foo2", @@ -3552,6 +3553,7 @@ func TestTaskDiff(t *testing.T) { ChangeMode: "bam2", ChangeSignal: "SIGHUP2", Splay: 2, + Perms: "0666", }, }, }, @@ -3564,6 +3566,7 @@ func TestTaskDiff(t *testing.T) { ChangeMode: "bam", ChangeSignal: "SIGHUP", Splay: 1, + Perms: "0644", }, { SourcePath: "foo3", @@ -3572,6 +3575,7 @@ func TestTaskDiff(t *testing.T) { ChangeMode: "bam3", ChangeSignal: "SIGHUP3", Splay: 3, + Perms: "0776", }, }, }, @@ -3606,6 +3610,12 @@ func TestTaskDiff(t *testing.T) { Old: "", New: "baz3", }, + { + Type: DiffTypeAdded, + Name: "Perms", + Old: "", + New: "0776", + }, { Type: DiffTypeAdded, Name: "SourcePath", @@ -3648,6 +3658,12 @@ func TestTaskDiff(t *testing.T) { Old: "baz2", New: "", }, + { + Type: DiffTypeDeleted, + Name: "Perms", + Old: "0666", + New: "", + }, { Type: DiffTypeDeleted, Name: "SourcePath", diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index 582d0f3ba273..3fe27853e2da 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -2589,6 +2589,9 @@ type Template struct { // random wait between 0 and the given splay value before signalling the // application of a change Splay time.Duration `mapstructure:"splay"` + + // Perms is the permission the file should be written out with. + Perms string `mapstructure:"perms"` } // DefaultTemplate returns a default template. @@ -2596,6 +2599,7 @@ func DefaultTemplate() *Template { return &Template{ ChangeMode: TemplateChangeModeRestart, Splay: 5 * time.Second, + Perms: "0644", } } @@ -2651,6 +2655,13 @@ func (t *Template) Validate() error { multierror.Append(&mErr, fmt.Errorf("Must specify positive splay value")) } + // Verify the permissions + if t.Perms != "" { + if _, err := strconv.ParseUint(t.Perms, 8, 12); err != nil { + multierror.Append(&mErr, fmt.Errorf("Failed to parse %q as octal: %v", t.Perms, err)) + } + } + return mErr.ErrorOrNil() } diff --git a/nomad/structs/structs_test.go b/nomad/structs/structs_test.go index 24bbc83d1606..610a00044d59 100644 --- a/nomad/structs/structs_test.go +++ b/nomad/structs/structs_test.go @@ -733,6 +733,27 @@ func TestTemplate_Validate(t *testing.T) { }, Fail: false, }, + { + Tmpl: &Template{ + SourcePath: "foo", + DestPath: "local/foo", + ChangeMode: "noop", + Perms: "0444", + }, + Fail: false, + }, + { + Tmpl: &Template{ + SourcePath: "foo", + DestPath: "local/foo", + ChangeMode: "noop", + Perms: "zza", + }, + Fail: true, + ContainsErrs: []string{ + "as octal", + }, + }, } for i, c := range cases { diff --git a/website/source/docs/job-specification/template.html.md b/website/source/docs/job-specification/template.html.md index d1d6744929e4..ee567d367731 100644 --- a/website/source/docs/job-specification/template.html.md +++ b/website/source/docs/job-specification/template.html.md @@ -47,6 +47,13 @@ README][ct]. ## `template` Parameters +- `change_mode` `(string: "restart")` - Specifies the behavior Nomad should take + if the rendered template changes. The possible values are: + + - `"noop"` - take no action (continue running the task) + - `"restart"` - restart the task + - `"signal"` - send a configurable signal to the task + - `change_signal` `(string: "")` - Specifies the signal to send to the task as a string like `"SIGUSR1"` or `"SIGINT"`. This option is required if the `change_mode` is `signal`. @@ -58,12 +65,8 @@ README][ct]. - `destination` `(string: )` - Specifies the location where the resulting template should be rendered, relative to the task directory. -- `change_mode` `(string: "restart")` - Specifies the behavior Nomad should take - if the rendered template changes. The possible values are: - - - `"noop"` - take no action (continue running the task) - - `"restart"` - restart the task - - `"signal"` - send a configurable signal to the task +- `perms` `(string: "666")` - Specifies the rendered template's permissions. + File permissions are given as octal of the unix file permissions rwxrwxrwx. - `source` `(string: "")` - Specifies the path to the template to be rendered. One of `source` or `data` must be specified, but not both. This source can