diff --git a/command/deploy.go b/command/deploy.go index c3817d226..a21ad4334 100644 --- a/command/deploy.go +++ b/command/deploy.go @@ -102,6 +102,7 @@ func (c *DeployCommand) Run(args []string) int { var err error var level, format string + var strictMode bool config := &levant.DeployConfig{ Client: &structs.ClientConfig{}, @@ -125,6 +126,7 @@ func (c *DeployCommand) Run(args []string) int { flags.StringVar(&format, "log-format", "HUMAN", "") flags.StringVar(&config.Deploy.VaultToken, "vault-token", "", "") flags.BoolVar(&config.Deploy.EnvVault, "vault", false, "") + flags.BoolVar(&strictMode, "strict", false, "") flags.Var((*helper.FlagStringSlice)(&config.Template.VariableFiles), "var-file", "") @@ -159,7 +161,7 @@ func (c *DeployCommand) Run(args []string) int { } config.Template.Job, err = template.RenderJob(config.Template.TemplateFile, - config.Template.VariableFiles, config.Client.ConsulAddr, &c.Meta.flagVars) + config.Template.VariableFiles, config.Client.ConsulAddr, strictMode, &c.Meta.flagVars) if err != nil { c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err)) return 1 diff --git a/command/deploy_test.go b/command/deploy_test.go index 97e7825a3..39df769ae 100644 --- a/command/deploy_test.go +++ b/command/deploy_test.go @@ -30,7 +30,7 @@ func TestDeploy_checkCanaryAutoPromote(t *testing.T) { } for i, c := range cases { - job, err := template.RenderJob(c.File, []string{}, "", &fVars) + job, err := template.RenderJob(c.File, []string{}, "", false, &fVars) if err != nil { t.Fatalf("case %d failed: %v", i, err) } @@ -61,7 +61,7 @@ func TestDeploy_checkForceBatch(t *testing.T) { } for i, c := range cases { - job, err := template.RenderJob(c.File, []string{}, "", &fVars) + job, err := template.RenderJob(c.File, []string{}, "", false, &fVars) if err != nil { t.Fatalf("case %d failed: %v", i, err) } diff --git a/command/plan.go b/command/plan.go index caee6d907..b068e23ab 100644 --- a/command/plan.go +++ b/command/plan.go @@ -88,6 +88,7 @@ func (c *PlanCommand) Run(args []string) int { Template: &structs.TemplateConfig{}, } + var strictMode bool flags := c.Meta.FlagSet("plan", FlagSetVars) flags.Usage = func() { c.UI.Output(c.Help()) } @@ -98,6 +99,7 @@ func (c *PlanCommand) Run(args []string) int { flags.StringVar(&level, "log-level", "INFO", "") flags.StringVar(&format, "log-format", "HUMAN", "") flags.Var((*helper.FlagStringSlice)(&config.Template.VariableFiles), "var-file", "") + flags.BoolVar(&strictMode, "strict", false, "") if err = flags.Parse(args); err != nil { return 1 @@ -124,7 +126,7 @@ func (c *PlanCommand) Run(args []string) int { } config.Template.Job, err = template.RenderJob(config.Template.TemplateFile, - config.Template.VariableFiles, config.Client.ConsulAddr, &c.Meta.flagVars) + config.Template.VariableFiles, config.Client.ConsulAddr, strictMode, &c.Meta.flagVars) if err != nil { c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err)) diff --git a/command/render.go b/command/render.go index 03066c0cc..30cd1d5a5 100644 --- a/command/render.go +++ b/command/render.go @@ -69,6 +69,7 @@ func (c *RenderCommand) Run(args []string) int { var addr, outPath, templateFile string var variables []string + var strictMode bool var err error var tpl *bytes.Buffer var level, format string @@ -81,6 +82,7 @@ func (c *RenderCommand) Run(args []string) int { flags.StringVar(&format, "log-format", "HUMAN", "") flags.Var((*helper.FlagStringSlice)(&variables), "var-file", "") flags.StringVar(&outPath, "out", "", "") + flags.BoolVar(&strictMode, "strict", false, "") if err = flags.Parse(args); err != nil { return 1 @@ -106,7 +108,7 @@ func (c *RenderCommand) Run(args []string) int { return 1 } - tpl, err = template.RenderTemplate(templateFile, variables, addr, &c.Meta.flagVars) + tpl, err = template.RenderTemplate(templateFile, variables, addr, strictMode, &c.Meta.flagVars) if err != nil { c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err)) return 1 diff --git a/template/render.go b/template/render.go index 9552d7e0e..52c43ea96 100644 --- a/template/render.go +++ b/template/render.go @@ -19,9 +19,9 @@ import ( // RenderJob takes in a template and variables performing a render of the // template followed by Nomad jobspec parse. -func RenderJob(templateFile string, variableFiles []string, addr string, flagVars *map[string]interface{}) (job *nomad.Job, err error) { +func RenderJob(templateFile string, variableFiles []string, addr string, strictMode bool, flagVars *map[string]interface{}) (job *nomad.Job, err error) { var tpl *bytes.Buffer - tpl, err = RenderTemplate(templateFile, variableFiles, addr, flagVars) + tpl, err = RenderTemplate(templateFile, variableFiles, addr, strictMode, flagVars) if err != nil { return } @@ -31,12 +31,13 @@ func RenderJob(templateFile string, variableFiles []string, addr string, flagVar // RenderTemplate is the main entry point to render the template based on the // passed variables file. -func RenderTemplate(templateFile string, variableFiles []string, addr string, flagVars *map[string]interface{}) (tpl *bytes.Buffer, err error) { +func RenderTemplate(templateFile string, variableFiles []string, addr string, strictMode bool, flagVars *map[string]interface{}) (tpl *bytes.Buffer, err error) { t := &tmpl{} t.flagVariables = flagVars t.jobTemplateFile = templateFile t.variableFiles = variableFiles + t.errMissingKey = strictMode c, err := client.NewConsulClient(addr) if err != nil { diff --git a/template/render_test.go b/template/render_test.go index 09bfd6853..c30adb138 100644 --- a/template/render_test.go +++ b/template/render_test.go @@ -25,7 +25,7 @@ func TestTemplater_RenderTemplate(t *testing.T) { fVars := make(map[string]interface{}) // Test basic TF template render. - job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.tf"}, "", &fVars) + job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.tf"}, "", false, &fVars) if err != nil { t.Fatal(err) } @@ -37,7 +37,7 @@ func TestTemplater_RenderTemplate(t *testing.T) { } // Test basic YAML template render. - job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.yaml"}, "", &fVars) + job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.yaml"}, "", false, &fVars) if err != nil { t.Fatal(err) } @@ -49,7 +49,7 @@ func TestTemplater_RenderTemplate(t *testing.T) { } // Test multiple var-files - job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.yaml", "test-fixtures/test-overwrite.yaml"}, "", &fVars) + job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.yaml", "test-fixtures/test-overwrite.yaml"}, "", false, &fVars) if err != nil { t.Fatal(err) } @@ -58,7 +58,7 @@ func TestTemplater_RenderTemplate(t *testing.T) { } // Test multiple var-files of different types - job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.tf", "test-fixtures/test-overwrite.yaml"}, "", &fVars) + job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.tf", "test-fixtures/test-overwrite.yaml"}, "", false, &fVars) if err != nil { t.Fatal(err) } @@ -68,7 +68,7 @@ func TestTemplater_RenderTemplate(t *testing.T) { // Test multiple var-files with var-args fVars["job_name"] = testJobNameOverwrite2 - job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.tf", "test-fixtures/test-overwrite.yaml"}, "", &fVars) + job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.tf", "test-fixtures/test-overwrite.yaml"}, "", false, &fVars) if err != nil { t.Fatal(err) } @@ -77,7 +77,7 @@ func TestTemplater_RenderTemplate(t *testing.T) { } // Test empty var-args and empty variable file render. - job, err = RenderJob("test-fixtures/none_templated.nomad", []string{}, "", &fVars) + job, err = RenderJob("test-fixtures/none_templated.nomad", []string{}, "", false, &fVars) if err != nil { t.Fatal(err) } @@ -87,7 +87,7 @@ func TestTemplater_RenderTemplate(t *testing.T) { // Test var-args only render. fVars = map[string]interface{}{"job_name": testJobName, "task_resource_cpu": "1313"} - job, err = RenderJob("test-fixtures/single_templated.nomad", []string{}, "", &fVars) + job, err = RenderJob("test-fixtures/single_templated.nomad", []string{}, "", false, &fVars) if err != nil { t.Fatal(err) } @@ -102,7 +102,7 @@ func TestTemplater_RenderTemplate(t *testing.T) { delete(fVars, "job_name") fVars["datacentre"] = testDCName os.Setenv(testEnvName, testEnvValue) - job, err = RenderJob("test-fixtures/multi_templated.nomad", []string{"test-fixtures/test.yaml"}, "", &fVars) + job, err = RenderJob("test-fixtures/multi_templated.nomad", []string{"test-fixtures/test.yaml"}, "", false, &fVars) if err != nil { t.Fatal(err) } @@ -116,3 +116,44 @@ func TestTemplater_RenderTemplate(t *testing.T) { t.Fatalf("expected %s but got %v", testEnvValue, *job.TaskGroups[0].Name) } } + +func TestTemplater_RenderTemplate_strict(t *testing.T) { + testCase := []struct { + description string + flagVars map[string]interface{} + errExpected bool + strictMode bool + }{ + { + description: "non-strict mode with job_name missing", + flagVars: map[string]interface{}{"task_resource_cpu": "1313"}, + strictMode: false, + errExpected: false, + }, + { + description: "strict mode with missing vars", + flagVars: map[string]interface{}{}, + strictMode: true, + errExpected: true, + }, + { + description: "strict mode with all vars", + flagVars: map[string]interface{}{"job_name": testJobName, "task_resource_cpu": "1313"}, + strictMode: true, + errExpected: false, + }, + } + + for _, tc := range testCase { + t.Run(tc.description, func(t *testing.T) { + _, err := RenderJob("test-fixtures/single_templated.nomad", []string{}, "", tc.strictMode, &tc.flagVars) + + if tc.errExpected && err == nil { + t.Fatalf("expected error but got nil") + } + if !tc.errExpected && err != nil { + t.Fatalf("expected no error but got %v", err) + } + }) + } +} diff --git a/template/template.go b/template/template.go index 6f767f5da..ddf502956 100644 --- a/template/template.go +++ b/template/template.go @@ -13,6 +13,7 @@ type tmpl struct { flagVariables *map[string]interface{} jobTemplateFile string variableFiles []string + errMissingKey bool } const ( @@ -28,7 +29,13 @@ const ( func (t *tmpl) newTemplate() *template.Template { tmpl := template.New("jobTemplate") tmpl.Delims(leftDelim, rightDelim) - tmpl.Option("missingkey=zero") + + if t.errMissingKey { + tmpl.Option("missingkey=error") + } else { + tmpl.Option("missingkey=zero") + } + tmpl.Funcs(funcMap(t.consulClient)) return tmpl } diff --git a/test/acctest/deploy.go b/test/acctest/deploy.go index 5e98f944e..2802f2985 100644 --- a/test/acctest/deploy.go +++ b/test/acctest/deploy.go @@ -26,7 +26,7 @@ func (c DeployTestStepRunner) Run(s *TestState) error { } c.Vars["job_name"] = s.JobName - job, err := template.RenderJob("fixtures/"+c.FixtureName, []string{}, "", &c.Vars) + job, err := template.RenderJob("fixtures/"+c.FixtureName, []string{}, "", false, &c.Vars) if err != nil { return fmt.Errorf("error rendering template: %s", err) }