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

Fix panic when using invalid JSON jobspec #111

Merged
merged 4 commits into from
May 29, 2020
Merged
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
45 changes: 37 additions & 8 deletions nomad/resource_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -548,17 +548,20 @@ func resourceJobCustomizeDiff(d *schema.ResourceDiff, meta interface{}) error {

func parseJobspec(raw string, is_json bool, vaultToken *string) (*api.Job, error) {
var job *api.Job
var err error

if is_json {
err := json.Unmarshal([]byte(raw), &job)
if err != nil {
return nil, fmt.Errorf("error parsing jobspec: %s", err)
}
job, err = parseJSONJobspec(raw)
} else {
var err error
job, err = jobspec.Parse(strings.NewReader(raw))
if err != nil {
return nil, fmt.Errorf("error parsing jobspec: %s", err)
}
}
if err != nil {
return nil, fmt.Errorf("error parsing jobspec: %s", err)
}

lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
// If job is empty after parsing, the input is not a valid Nomad job.
if job == nil || reflect.DeepEqual(job, &api.Job{}) {
return nil, fmt.Errorf("error parsing jobspec: input JSON is not a valid Nomad jobspec")
}

// Inject the Vault token
Expand All @@ -567,6 +570,32 @@ func parseJobspec(raw string, is_json bool, vaultToken *string) (*api.Job, error
return job, nil
}

func parseJSONJobspec(raw string) (*api.Job, error) {
// `nomad job run -output` returns a jobspec with a "Job" root, so
// partially parse the input JSON to detect if we have this root.
var root map[string]json.RawMessage

err := json.Unmarshal([]byte(raw), &root)
if err != nil {
return nil, err
}

jobBytes, ok := root["Job"]
if !ok {
// Parse the input as is if there's no "Job" root.
jobBytes = []byte(raw)
}

// Parse actual job.
var job api.Job
err = json.Unmarshal(jobBytes, &job)
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}

return &job, nil
}

func jobTaskGroupsRaw(tgs []*api.TaskGroup) []interface{} {
ret := make([]interface{}, 0, len(tgs))

Expand Down
91 changes: 91 additions & 0 deletions nomad/resource_job_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,40 @@ func TestResourceJob_consulConnect(t *testing.T) {
}

func TestResourceJob_json(t *testing.T) {
// Test invalid JSON inputs.
re := regexp.MustCompile("error parsing jobspec")
r.Test(t, r.TestCase{
Providers: testProviders,
PreCheck: func() { testAccPreCheck(t) },
Steps: []r.TestStep{
{
Config: testResourceJob_invalidJSONConfig,
ExpectError: re,
},
{
Config: testResourceJob_invalidJSONConfig_notJobspec,
ExpectError: re,
},
},

CheckDestroy: testResourceJob_checkDestroy("foo-json"),
})

// Test jobspec with "Job" root.
r.Test(t, r.TestCase{
Providers: testProviders,
PreCheck: func() { testAccPreCheck(t) },
Steps: []r.TestStep{
{
Config: testResourceJob_jsonConfigWithRoot,
Check: testResourceJob_initialCheck(t),
},
},

CheckDestroy: testResourceJob_checkDestroy("foo-json"),
})

// Test plain jobspec.
r.Test(t, r.TestCase{
Providers: testProviders,
PreCheck: func() { testAccPreCheck(t) },
Expand Down Expand Up @@ -468,6 +502,63 @@ resource "nomad_job" "test" {
}
`

var testResourceJob_invalidJSONConfig = `
resource "nomad_job" "test" {
json = true
jobspec = "not json"
}
`

var testResourceJob_invalidJSONConfig_notJobspec = `
resource "nomad_job" "test" {
json = true
jobspec = <<EOT
{
"not": "job"
}
EOT
}
`

var testResourceJob_jsonConfigWithRoot = `
resource "nomad_job" "test" {
json = true
jobspec = <<EOT
{
"Job": {
"Datacenters": [ "dc1" ],
"ID": "foo-json",
"Name": "foo-json",
"Type": "service",
"TaskGroups": [
{
"Name": "foo",
"Tasks": [{
"Config": {
"command": "/bin/sleep",
"args": [ "1" ]
},
"Driver": "raw_exec",
"Leader": true,
"LogConfig": {
"MaxFileSizeMB": 10,
"MaxFiles": 3
},
"Name": "foo",
"Resources": {
"CPU": 100,
"MemoryMB": 10
}
}
]
}
]
}
}
EOT
}
`

var testResourceJob_jsonConfig = `
resource "nomad_job" "test" {
json = true
Expand Down
23 changes: 23 additions & 0 deletions website/docs/r/job.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,29 @@ EOT
}
```

## JSON jobspec

The input jobspec can also be provided as JSON instead of HCL by setting the
argument `json` to `true`:

```hcl
resource "nomad_job" "app" {
jobspec = "${file("${path.module}/job.json")}"
json = true
}
```

When using JSON, the input jobspec should have the same structured used by the
[Nomad API](https://www.nomadproject.io/api-docs/json-jobs/). The Nomad CLI
can translate HCL jobs to JSON:

```shellsession
nomad job run -output my-job.nomad > my-job.json
```

Or you can also use the [`/v1/jobs/parse`](https://www.nomadproject.io/api-docs/jobs/#parse-job)
API endpoint.

## Argument Reference

The following arguments are supported:
Expand Down