Skip to content

Commit

Permalink
job: add support for job submissions (#405)
Browse files Browse the repository at this point in the history
If available, use the job submission source to detect changes to
`jobspec`. This can mitigate drift detection problems such as #1.

Read HCL2 variables from job submission even if the `nomad_job` resource
does not speify an `hcl2` block.
  • Loading branch information
lgfa29 authored Dec 19, 2023
1 parent 1b74304 commit 6788d0a
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ IMPROVEMENTS:
* resource/nomad_csi_volume: update import key to be `<volume id>@<namespace>` to allow importing volumes from namespaces other than `default` ([#408](https://github.com/hashicorp/terraform-provider-nomad/pull/408))
* resource/nomad_csi_volume_registration: update import key to be `<volume id>@<namespace>` to allow importing volume registrations from namespaces other than `default` ([#408](https://github.com/hashicorp/terraform-provider-nomad/pull/408))
on Nomad version 1.6.3 or later, if the CSI plugin supports it ([#382](https://github.com/hashicorp/terraform-provider-nomad/pull/382))
* resource/nomad_job: read and submit original jobspec on state refresh and job register ([#405](https://github.com/hashicorp/terraform-provider-nomad/pull/405))
* resource/nomad_job: Add `rerun_if_dead` attribute to allow forcing a job to run again if it's marked as `dead`. ([#407](https://github.com/hashicorp/terraform-provider-nomad/pull/407))
* resource/nomad_job: update import key to be `<job id>@<namespace>` to allow importing jobs from namespaces other than `default` ([#408](https://github.com/hashicorp/terraform-provider-nomad/pull/408))

Expand Down
82 changes: 82 additions & 0 deletions nomad/resource_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"github.com/hashicorp/nomad/jobspec2"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"golang.org/x/exp/maps"

"github.com/hashicorp/terraform-provider-nomad/nomad/helper"
)

Expand Down Expand Up @@ -351,6 +353,11 @@ type HCL1JobParserConfig struct {
type HCL2JobParserConfig struct {
AllowFS bool
Vars map[string]string

// Deprecated: Starting in v2.0.0 the provider assumes HCL2 parsing by
// default. This field should only be used to update the `hcl2` attribute
// in state without causing a diff.
Enabled bool
}

// ResourceFieldGetter are able to retrieve field values.
Expand Down Expand Up @@ -407,9 +414,23 @@ func resourceJobRegister(d *schema.ResourceData, meta interface{}) error {
if err != nil {
wantModifyIndex = 0
}

sub := &api.JobSubmission{
Source: jobspecRaw,
Format: "hcl2",
VariableFlags: jobParserConfig.HCL2.Vars,
}
switch {
case jobParserConfig.JSON.Enabled:
sub.Format = "json"
case jobParserConfig.HCL1.Enabled:
sub.Format = "hcl1"
}

resp, _, err := client.Jobs().RegisterOpts(job, &api.RegisterOptions{
PolicyOverride: d.Get("policy_override").(bool),
ModifyIndex: wantModifyIndex,
Submission: sub,
}, &api.WriteOptions{
Namespace: *job.Namespace,
})
Expand Down Expand Up @@ -641,6 +662,56 @@ func resourceJobRead(d *schema.ResourceData, meta interface{}) error {
d.Set("allocation_ids", nil)
}

// Update jobspec submission data if available.
// Safely ignore errors as this is an optional step.
sub, _, err := client.Jobs().Submission(*job.ID, int(*job.Version), opts)
if err != nil {
log.Printf("[WARN] failed to read job submission: %v", err)
} else {
err := resourceJobReadSubmission(sub, d, meta)
if err != nil {
log.Printf("[WARN] failed to update job submission: %v", err)
}
}

return nil
}

func resourceJobReadSubmission(sub *api.JobSubmission, d *schema.ResourceData, meta any) error {
if sub == nil {
return nil
}

if sub.Source != "" {
d.Set("jobspec", sub.Source)
}

if sub.Format == "hcl2" {
var err error
var hcl2Config HCL2JobParserConfig

hcl2, ok := d.GetOk("hcl2")
if ok {
hcl2Config, err = parseHCL2JobParserConfig(hcl2)
if err != nil {
return fmt.Errorf("failed to parse HCL2 config: %v", err)
}
} else {
// Use default values if hcl2 is not set.
hcl2Config = HCL2JobParserConfig{
AllowFS: false,
Enabled: true,
}
}

// Only update hcl2 if there are changes to variables to avoid
// unnecessary updates if hcl2 is not set.
if !maps.Equal(sub.VariableFlags, hcl2Config.Vars) {
hcl2Config.Vars = sub.VariableFlags
d.Set("hcl2", flattenHCL2JobParserConfig(hcl2Config))
}
}

return nil
}

Expand Down Expand Up @@ -827,6 +898,9 @@ func parseHCL2JobParserConfig(raw interface{}) (HCL2JobParserConfig, error) {
if allowFS, ok := hcl2Map["allow_fs"].(bool); ok {
config.AllowFS = allowFS
}
if enabled, ok := hcl2Map["enabled"].(bool); ok {
config.Enabled = enabled
}
if vars, ok := hcl2Map["vars"].(map[string]interface{}); ok {
config.Vars = make(map[string]string)
for k, v := range vars {
Expand All @@ -837,6 +911,14 @@ func parseHCL2JobParserConfig(raw interface{}) (HCL2JobParserConfig, error) {
return config, nil
}

func flattenHCL2JobParserConfig(c HCL2JobParserConfig) []any {
return []any{map[string]any{
"allow_fs": c.AllowFS,
"enabled": c.Enabled,
"vars": c.Vars,
}}
}

func parseJobspec(raw string, config JobParserConfig, vaultToken *string, consulToken *string) (*api.Job, error) {
var job *api.Job
var err error
Expand Down
36 changes: 34 additions & 2 deletions nomad/resource_job_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"reflect"
"regexp"
"strings"
Expand Down Expand Up @@ -641,7 +641,7 @@ func testResourceJob_hcl2Check(s *terraform.State) error {
}
got := *tpl.EmbeddedTmpl

want, err := ioutil.ReadFile("./test-fixtures/hello.txt")
want, err := os.ReadFile("./test-fixtures/hello.txt")
if err != nil {
return fmt.Errorf("failed to open template data: %v", err)
}
Expand All @@ -650,6 +650,28 @@ func testResourceJob_hcl2Check(s *terraform.State) error {
return fmt.Errorf("template content mismatch (-want +got):\n%s", diff)
}

sub, _, err := client.Jobs().Submission(jobID, int(*job.Version), &api.QueryOptions{
Namespace: *job.Namespace,
})
if err != nil {
return fmt.Errorf("error reading job submissions: %s", err)
}
if diff := cmp.Diff(instanceState.Attributes["jobspec"], sub.Source); diff != "" {
return fmt.Errorf("job source mismatch (-want +got):\n%s", diff)
}

wantVars := make(map[string]string)
for k, v := range instanceState.Attributes {
if !strings.HasPrefix(k, "hcl2.0.vars") || k == "hcl2.0.vars.%" {
continue
}
varKey := strings.TrimPrefix(k, "hcl2.0.vars.")
wantVars[varKey] = v
}
if diff := cmp.Diff(wantVars, sub.VariableFlags); diff != "" {
return fmt.Errorf("job hcl2 variables mismatch (-want +got):\n%s", diff)
}

return nil
}

Expand Down Expand Up @@ -1199,6 +1221,16 @@ func testResourceJob_initialCheckNS(t *testing.T, expectedNamespace string) r.Te
return fmt.Errorf("job namespace is %q; want %q", got, want)
}

sub, _, err := client.Jobs().Submission(jobID, int(*job.Version), &api.QueryOptions{
Namespace: expectedNamespace,
})
if err != nil {
return fmt.Errorf("error reading job submissions: %s", err)
}
if diff := cmp.Diff(instanceState.Attributes["jobspec"], sub.Source); diff != "" {
return fmt.Errorf("job source mismatch (-want +got):\n%s", diff)
}

return nil
}
}
Expand Down
7 changes: 7 additions & 0 deletions website/docs/r/job.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,13 @@ EOF
}
```

## Tracking Jobspec Changes

The Nomad API allows [submitting the raw jobspec when registering and updating
jobs](https://developer.hashicorp.com/nomad/api-docs/jobs#submission). If
available, the job submission source is used to detect changes to the `jobspec`
and `hcl2.vars` arguments.

## Argument Reference

The following arguments are supported:
Expand Down

0 comments on commit 6788d0a

Please sign in to comment.