From e2da0b9decb59919233a3e31a041befd53f04628 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Wed, 2 Nov 2016 13:03:54 -0700 Subject: [PATCH] Allow absolute paths for template sources --- client/consul_template.go | 27 ++++++-- client/consul_template_test.go | 61 +++++++++++++++++++ .../docs/job-specification/template.html.md | 8 +++ 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/client/consul_template.go b/client/consul_template.go index 24824d9e1..19150d71b 100644 --- a/client/consul_template.go +++ b/client/consul_template.go @@ -18,6 +18,12 @@ import ( "github.com/hashicorp/nomad/nomad/structs" ) +const ( + // hostSrcOption is the Client option that determines whether the template + // source may be from the host + hostSrcOption = "template.allow_host_source" +) + var ( // testRetryRate is used to speed up tests by setting consul-templates retry // rate to something low @@ -319,7 +325,11 @@ func templateRunner(tmpls []*structs.Template, config *config.Config, } // Parse the templates - ctmplMapping := parseTemplateConfigs(tmpls, taskDir, taskEnv) + allowAbs := config.ReadBoolDefault(hostSrcOption, true) + ctmplMapping, err := parseTemplateConfigs(tmpls, taskDir, taskEnv, allowAbs) + if err != nil { + return nil, nil, err + } // Set the config flat := make([]*ctconf.ConfigTemplate, 0, len(ctmplMapping)) @@ -349,7 +359,8 @@ func templateRunner(tmpls []*structs.Template, config *config.Config, } // parseTemplateConfigs converts the tasks templates into consul-templates -func parseTemplateConfigs(tmpls []*structs.Template, taskDir string, taskEnv *env.TaskEnvironment) map[ctconf.ConfigTemplate]*structs.Template { +func parseTemplateConfigs(tmpls []*structs.Template, taskDir string, + taskEnv *env.TaskEnvironment, allowAbs bool) (map[ctconf.ConfigTemplate]*structs.Template, error) { // Build the task environment // TODO Should be able to inject the Nomad env vars into Consul-template for // rendering @@ -359,7 +370,15 @@ func parseTemplateConfigs(tmpls []*structs.Template, taskDir string, taskEnv *en for _, tmpl := range tmpls { var src, dest string if tmpl.SourcePath != "" { - src = filepath.Join(taskDir, taskEnv.ReplaceEnv(tmpl.SourcePath)) + if filepath.IsAbs(tmpl.SourcePath) { + if !allowAbs { + return nil, fmt.Errorf("Specifying absolute template paths disallowed by client config: %q", tmpl.SourcePath) + } + + src = tmpl.SourcePath + } else { + src = filepath.Join(taskDir, taskEnv.ReplaceEnv(tmpl.SourcePath)) + } } if tmpl.DestPath != "" { dest = filepath.Join(taskDir, taskEnv.ReplaceEnv(tmpl.DestPath)) @@ -376,7 +395,7 @@ func parseTemplateConfigs(tmpls []*structs.Template, taskDir string, taskEnv *en ctmpls[ct] = tmpl } - return ctmpls + return ctmpls, nil } // runnerConfig returns a consul-template runner configuration, setting the diff --git a/client/consul_template_test.go b/client/consul_template_test.go index f4c23b540..50d4a5437 100644 --- a/client/consul_template_test.go +++ b/client/consul_template_test.go @@ -2,6 +2,7 @@ package client import ( "fmt" + "io" "io/ioutil" "os" "path/filepath" @@ -139,6 +140,13 @@ func (h *testHarness) start(t *testing.T) { h.manager = manager } +func (h *testHarness) startWithErr() error { + manager, err := NewTaskTemplateManager(h.mockHooks, h.templates, + h.config, h.vaultToken, h.taskDir, h.taskEnv) + h.manager = manager + return err +} + // stop is used to stop any running Vault or Consul server plus the task manager func (h *testHarness) stop() { if h.vault != nil { @@ -210,6 +218,59 @@ func TestTaskTemplateManager_Invalid(t *testing.T) { } } +func TestTaskTemplateManager_HostPath(t *testing.T) { + // Make a template that will render immediately and write it to a tmp file + f, err := ioutil.TempFile("", "") + if err != nil { + t.Fatalf("Bad: %v", err) + } + defer f.Close() + defer os.Remove(f.Name()) + + content := "hello, world!" + if _, err := io.WriteString(f, content); err != nil { + t.Fatalf("Bad: %v", err) + } + + file := "my.tmpl" + template := &structs.Template{ + SourcePath: f.Name(), + DestPath: file, + ChangeMode: structs.TemplateChangeModeNoop, + } + + harness := newTestHarness(t, []*structs.Template{template}, false, false) + harness.start(t) + defer harness.stop() + + // Wait for the unblock + select { + case <-harness.mockHooks.UnblockCh: + case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second): + t.Fatalf("Task unblock should have been called") + } + + // Check the file is there + path := filepath.Join(harness.taskDir, file) + raw, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("Failed to read rendered template from %q: %v", path, err) + } + + if s := string(raw); s != content { + t.Fatalf("Unexpected template data; got %q, want %q", s, content) + } + + // Change the config to disallow host sources + harness = newTestHarness(t, []*structs.Template{template}, false, false) + harness.config.Options = map[string]string{ + hostSrcOption: "false", + } + if err := harness.startWithErr(); err == nil || !strings.Contains(err.Error(), "absolute") { + t.Fatalf("Expected absolute template path disallowed: %v", err) + } +} + func TestTaskTemplateManager_Unblock_Static(t *testing.T) { // Make a template that will render immediately content := "hello, world!" diff --git a/website/source/docs/job-specification/template.html.md b/website/source/docs/job-specification/template.html.md index 06fe392fb..a93569d7a 100644 --- a/website/source/docs/job-specification/template.html.md +++ b/website/source/docs/job-specification/template.html.md @@ -121,5 +121,13 @@ template { } ``` +### Client Configuration + +The `template` block has the following [client configuration +options](/docs/agent/config.html#options): + +* `template.allow_host_source` - Allows templates to specify their source + template as an absolute path referencing host directories. Defaults to `true`. + [ct]: https://github.com/hashicorp/consul-template "Consul Template by HashiCorp" [artifact]: /docs/job-specification/artifact.html "Nomad artifact Job Specification"