Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add JSON support #540

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion command/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,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 @@ -129,6 +132,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(&config.Template.IsJSON, "json", false, "")

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

Expand Down Expand Up @@ -163,7 +167,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