diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c47d107..85df1a9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ IMPROVEMENTS: * resource/nomad_csi_volume: update import key to be `@` 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 `@` 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 `@` to allow importing jobs from namespaces other than `default` ([#408](https://github.com/hashicorp/terraform-provider-nomad/pull/408)) diff --git a/nomad/resource_job.go b/nomad/resource_job.go index c5521502..251e6ec6 100644 --- a/nomad/resource_job.go +++ b/nomad/resource_job.go @@ -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" ) @@ -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. @@ -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, }) @@ -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 } @@ -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 { @@ -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 diff --git a/nomad/resource_job_test.go b/nomad/resource_job_test.go index 6ebceb2c..e6bd2ee7 100644 --- a/nomad/resource_job_test.go +++ b/nomad/resource_job_test.go @@ -7,7 +7,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "os" "reflect" "regexp" "strings" @@ -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) } @@ -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 } @@ -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 } } diff --git a/website/docs/r/job.html.markdown b/website/docs/r/job.html.markdown index 0840b60c..9489cfee 100644 --- a/website/docs/r/job.html.markdown +++ b/website/docs/r/job.html.markdown @@ -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: