Skip to content

Commit

Permalink
add JSON support
Browse files Browse the repository at this point in the history
  • Loading branch information
Seros committed May 31, 2024
1 parent 7e44fef commit bcd2dff
Show file tree
Hide file tree
Showing 12 changed files with 272 additions and 15 deletions.
10 changes: 9 additions & 1 deletion command/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,17 @@ General Options:
Specify the format of Levant's logs. Valid values are HUMAN or JSON. The
default is HUMAN.
-json
Process template file as JSON
-var-file=<file>
Path to a file containing user variables used when rendering the job
template. You can repeat this flag multiple times to supply multiple
var-files. Defaults to levant.(json|yaml|yml|tf).
[default: levant.(json|yaml|yml|tf)]
-log-on-error
Enables the return of log messages if a deployment error in nomad appears
`
return strings.TrimSpace(helpText)
}
Expand Down Expand Up @@ -129,7 +135,9 @@ 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(&config.Deploy.LogOnError, "log-on-error", false, "")

flags.BoolVar(&config.Template.IsJSON, "json", false, "")
flags.Var((*helper.FlagStringSlice)(&config.Template.VariableFiles), "var-file", "")

if err = flags.Parse(args); err != nil {
Expand Down Expand Up @@ -163,7 +171,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, &c.Meta.flagVars, config.Template.IsJSON)
if err != nil {
c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err))
return 1
Expand Down
4 changes: 2 additions & 2 deletions command/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,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{}, "", &fVars, false)
if err != nil {
t.Fatalf("case %d failed: %v", i, err)
}
Expand Down Expand Up @@ -64,7 +64,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{}, "", &fVars, false)
if err != nil {
t.Fatalf("case %d failed: %v", i, err)
}
Expand Down
6 changes: 5 additions & 1 deletion command/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ General Options:
Specify the format of Levant's logs. Valid values are HUMAN or JSON. The
default is HUMAN.
-json
Process template file as JSON
-var-file=<file>
Path to a file containing user variables used when rendering the job
template. You can repeat this flag multiple times to supply multiple
Expand Down Expand Up @@ -101,6 +104,7 @@ func (c *PlanCommand) Run(args []string) int {
flags.BoolVar(&config.Plan.IgnoreNoChanges, "ignore-no-changes", false, "")
flags.StringVar(&level, "log-level", "INFO", "")
flags.StringVar(&format, "log-format", "HUMAN", "")
flags.BoolVar(&config.Template.IsJSON, "json", false, "")
flags.Var((*helper.FlagStringSlice)(&config.Template.VariableFiles), "var-file", "")

if err = flags.Parse(args); err != nil {
Expand Down Expand Up @@ -128,7 +132,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, &c.Meta.flagVars, config.Template.IsJSON)

if err != nil {
c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err))
Expand Down
4 changes: 4 additions & 0 deletions levant/structs/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ type TemplateConfig struct {
// VariableFiles contains the variables which will be substituted into the
// templateFile before deployment.
VariableFiles []string

// VariableFiles contains the variables which will be substituted into the
// templateFile before deployment.
IsJSON bool
}

// ScaleConfig contains all the scaling specific configuration options.
Expand Down
26 changes: 25 additions & 1 deletion template/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,37 @@ 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, flagVars *map[string]interface{}, isJSON bool) (job *nomad.Job, err error) {
var tpl *bytes.Buffer
tpl, err = RenderTemplate(templateFile, variableFiles, addr, flagVars)
if err != nil {
return
}

if isJSON {
// Support JSON files with both a top-level Job key as well as
// ones without.
eitherJob := struct {
NestedJob *nomad.Job `json:"Job"`
nomad.Job
}{}

if err := json.NewDecoder(tpl).Decode(&eitherJob); err != nil {
return nil, fmt.Errorf("Failed to parse JSON job: %w", err)
}

if eitherJob.NestedJob != nil {
if eitherJob.NestedJob.Name == nil && eitherJob.NestedJob.ID != nil {
eitherJob.NestedJob.Name = eitherJob.NestedJob.ID
}
if eitherJob.NestedJob.ID == nil {
return nil, fmt.Errorf("JSON is missing ID field")
}
return eitherJob.NestedJob, nil
}
return &eitherJob.Job, nil
}

return jobspec.Parse(tpl)
}

Expand Down
16 changes: 8 additions & 8 deletions template/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,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"}, "", &fVars, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -40,7 +40,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"}, "", &fVars, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -52,7 +52,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"}, "", &fVars, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -61,7 +61,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"}, "", &fVars, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -71,7 +71,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"}, "", &fVars, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -80,7 +80,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{}, "", &fVars, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -90,7 +90,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{}, "", &fVars, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -105,7 +105,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"}, "", &fVars, false)
if err != nil {
t.Fatal(err)
}
Expand Down
3 changes: 3 additions & 0 deletions test/acctest/acctest.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ func Test(t *testing.T, c TestCase) {
break
}
}
} else if step.ExpectErr {
t.Errorf("step %d/%d failed not but was expected to fail", stepNum, len(c.Steps))
break
}
}

Expand Down
6 changes: 4 additions & 2 deletions test/acctest/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type DeployTestStepRunner struct {
Canary int
ForceBatch bool
ForceCount bool
IsJSON bool
}

// Run renders the job fixture and triggers a deployment
Expand All @@ -29,7 +30,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{}, "", &c.Vars, c.IsJSON)
if err != nil {
return fmt.Errorf("error rendering template: %s", err)
}
Expand All @@ -42,7 +43,8 @@ func (c DeployTestStepRunner) Run(s *TestState) error {
},
Client: &structs.ClientConfig{},
Template: &structs.TemplateConfig{
Job: job,
Job: job,
IsJSON: c.IsJSON,
},
}

Expand Down
69 changes: 69 additions & 0 deletions test/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,75 @@ func TestDeploy_basic(t *testing.T) {
})
}

func TestDeploy_basicJson(t *testing.T) {
acctest.Test(t, acctest.TestCase{
Steps: []acctest.TestStep{
{
Runner: acctest.DeployTestStepRunner{
FixtureName: "deploy_basic.nomad.json",
IsJSON: true,
},
Check: acctest.CheckDeploymentStatus("successful"),
},
},
CleanupFunc: acctest.CleanupPurgeJob,
})
}

func TestDeploy_invalidJson(t *testing.T) {
acctest.Test(t, acctest.TestCase{
Steps: []acctest.TestStep{
{
Runner: acctest.DeployTestStepRunner{
FixtureName: "deploy_basic_invalid.nomad.json",
IsJSON: true,
},
ExpectErr: true,
CheckErr: func(err error) bool {
return err.Error() == "error rendering template: Failed to parse JSON job: json: cannot unmarshal array into Go struct field Job.Job.Type of type string"
},
},
},
CleanupFunc: acctest.CleanupPurgeJob,
})
}

func TestDeploy_jsonWithoutIdName(t *testing.T) {
acctest.Test(t, acctest.TestCase{
Steps: []acctest.TestStep{
{
Runner: acctest.DeployTestStepRunner{
FixtureName: "deploy_jsonWithoutIdName.nomad.json",
IsJSON: true,
},
ExpectErr: true,
CheckErr: func(err error) bool {
return err.Error() == "error rendering template: JSON is missing ID field"
},
},
},
CleanupFunc: acctest.CleanupPurgeJob,
})
}

func TestDeploy_notJson(t *testing.T) {
acctest.Test(t, acctest.TestCase{
Steps: []acctest.TestStep{
{
Runner: acctest.DeployTestStepRunner{
FixtureName: "deploy_basic.nomad",
IsJSON: true,
},
ExpectErr: true,
CheckErr: func(err error) bool {
return err.Error() == "error rendering template: Detected JSON but failed to parse JSON job: invalid character '#' looking for beginning of value"
},
},
},
CleanupFunc: acctest.CleanupPurgeJob,
})
}

func TestDeploy_driverError(t *testing.T) {
acctest.Test(t, acctest.TestCase{
Steps: []acctest.TestStep{
Expand Down
48 changes: 48 additions & 0 deletions test/fixtures/deploy_basic.nomad.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"Job": {
"ID": "[[.job_name]]",
"Type": "service",
"Datacenters": [
"dc1"
],
"TaskGroups": [
{
"Name": "test",
"Count": 1,
"Tasks": [
{
"Name": "alpine",
"Driver": "docker",
"Config": {
"args": [
"-f",
"/dev/null"
],
"command": "tail",
"image": "alpine"
},
"Resources": {
"CPU": 100,
"MemoryMB": 128
}
}
],
"RestartPolicy": {
"Interval": 300000000000,
"Attempts": 10,
"Delay": 25000000000,
"Mode": "delay"
},
"EphemeralDisk": {
"SizeMB": 300
}
}
],
"Update": {
"MaxParallel": 1,
"MinHealthyTime": 10000000000,
"HealthyDeadline": 60000000000,
"AutoRevert": true
}
}
}
Loading

0 comments on commit bcd2dff

Please sign in to comment.