Skip to content

Commit fd498fb

Browse files
authored
Merge pull request hashicorp#9538 from hashicorp/f-nomad-provider
provider/nomad: Nomad provider for managing jobs
2 parents 695bd6d + f35996c commit fd498fb

956 files changed

Lines changed: 177005 additions & 475 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package nomad
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/hashicorp/nomad/api"
7+
"github.com/hashicorp/terraform/helper/schema"
8+
"github.com/hashicorp/terraform/terraform"
9+
)
10+
11+
func Provider() terraform.ResourceProvider {
12+
return &schema.Provider{
13+
Schema: map[string]*schema.Schema{
14+
"address": &schema.Schema{
15+
Type: schema.TypeString,
16+
Required: true,
17+
DefaultFunc: schema.EnvDefaultFunc("NOMAD_ADDR", nil),
18+
Description: "URL of the root of the target Nomad agent.",
19+
},
20+
21+
"region": &schema.Schema{
22+
Type: schema.TypeString,
23+
Optional: true,
24+
DefaultFunc: schema.EnvDefaultFunc("NOMAD_REGION", ""),
25+
Description: "Region of the target Nomad agent.",
26+
},
27+
},
28+
29+
ConfigureFunc: providerConfigure,
30+
31+
ResourcesMap: map[string]*schema.Resource{
32+
"nomad_job": resourceJob(),
33+
},
34+
}
35+
}
36+
37+
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
38+
config := &api.Config{
39+
Address: d.Get("address").(string),
40+
Region: d.Get("region").(string),
41+
}
42+
43+
client, err := api.NewClient(config)
44+
if err != nil {
45+
return nil, fmt.Errorf("failed to configure Nomad API: %s", err)
46+
}
47+
48+
return client, nil
49+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package nomad
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform/helper/schema"
8+
"github.com/hashicorp/terraform/terraform"
9+
)
10+
11+
// How to run the acceptance tests for this provider:
12+
//
13+
// - Obtain an official Nomad release from https://nomadproject.io
14+
// and extract the "nomad" binary
15+
//
16+
// - Run the following to start the Nomad agent in development mode:
17+
// nomad agent -dev
18+
//
19+
// - Run the Terraform acceptance tests as usual:
20+
// make testacc TEST=./builtin/providers/nomad
21+
//
22+
// The tests expect to be run in a fresh, empty Nomad server.
23+
24+
func TestProvider(t *testing.T) {
25+
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
26+
t.Fatalf("err: %s", err)
27+
}
28+
}
29+
30+
var testProvider *schema.Provider
31+
var testProviders map[string]terraform.ResourceProvider
32+
33+
func init() {
34+
testProvider = Provider().(*schema.Provider)
35+
testProviders = map[string]terraform.ResourceProvider{
36+
"nomad": testProvider,
37+
}
38+
}
39+
40+
func testAccPreCheck(t *testing.T) {
41+
if v := os.Getenv("NOMAD_ADDR"); v == "" {
42+
os.Setenv("NOMAD_ADDR", "http://127.0.0.1:4646")
43+
}
44+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package nomad
2+
3+
import (
4+
"bytes"
5+
"encoding/gob"
6+
"fmt"
7+
"log"
8+
"reflect"
9+
"strings"
10+
11+
"github.com/hashicorp/nomad/api"
12+
"github.com/hashicorp/nomad/jobspec"
13+
"github.com/hashicorp/nomad/nomad/structs"
14+
"github.com/hashicorp/terraform/helper/schema"
15+
)
16+
17+
func resourceJob() *schema.Resource {
18+
return &schema.Resource{
19+
Create: resourceJobRegister,
20+
Update: resourceJobRegister,
21+
Delete: resourceJobDeregister,
22+
Read: resourceJobRead,
23+
Exists: resourceJobExists,
24+
25+
Schema: map[string]*schema.Schema{
26+
"jobspec": {
27+
Description: "Job specification. If you want to point to a file use the file() function.",
28+
Required: true,
29+
Type: schema.TypeString,
30+
DiffSuppressFunc: jobspecDiffSuppress,
31+
},
32+
33+
"deregister_on_destroy": {
34+
Description: "If true, the job will be deregistered on destroy.",
35+
Optional: true,
36+
Default: true,
37+
Type: schema.TypeBool,
38+
},
39+
40+
"deregister_on_id_change": {
41+
Description: "If true, the job will be deregistered when the job ID changes.",
42+
Optional: true,
43+
Default: true,
44+
Type: schema.TypeBool,
45+
},
46+
},
47+
}
48+
}
49+
50+
func resourceJobRegister(d *schema.ResourceData, meta interface{}) error {
51+
client := meta.(*api.Client)
52+
53+
// Get the jobspec itself
54+
jobspecRaw := d.Get("jobspec").(string)
55+
56+
// Parse it
57+
jobspecStruct, err := jobspec.Parse(strings.NewReader(jobspecRaw))
58+
if err != nil {
59+
return fmt.Errorf("error parsing jobspec: %s", err)
60+
}
61+
62+
// Initialize and validate
63+
jobspecStruct.Canonicalize()
64+
if err := jobspecStruct.Validate(); err != nil {
65+
return fmt.Errorf("Error validating job: %v", err)
66+
}
67+
68+
// If we have an ID and its not equal to this jobspec, then we
69+
// have to deregister the old job before we register the new job.
70+
prevId := d.Id()
71+
if !d.Get("deregister_on_id_change").(bool) {
72+
// If we aren't deregistering on ID change, just pretend we
73+
// don't have a prior ID.
74+
prevId = ""
75+
}
76+
if prevId != "" && prevId != jobspecStruct.ID {
77+
log.Printf(
78+
"[INFO] Deregistering %q before registering %q",
79+
prevId, jobspecStruct.ID)
80+
81+
log.Printf("[DEBUG] Deregistering job: %q", prevId)
82+
_, _, err := client.Jobs().Deregister(prevId, nil)
83+
if err != nil {
84+
return fmt.Errorf(
85+
"error deregistering previous job %q "+
86+
"before registering new job %q: %s",
87+
prevId, jobspecStruct.ID, err)
88+
}
89+
90+
// Success! Clear our state.
91+
d.SetId("")
92+
}
93+
94+
// Convert it so that we can use it with the API
95+
jobspecAPI, err := convertStructJob(jobspecStruct)
96+
if err != nil {
97+
return fmt.Errorf("error converting jobspec: %s", err)
98+
}
99+
100+
// Register the job
101+
_, _, err = client.Jobs().Register(jobspecAPI, nil)
102+
if err != nil {
103+
return fmt.Errorf("error applying jobspec: %s", err)
104+
}
105+
106+
d.SetId(jobspecAPI.ID)
107+
108+
return nil
109+
}
110+
111+
func resourceJobDeregister(d *schema.ResourceData, meta interface{}) error {
112+
client := meta.(*api.Client)
113+
114+
// If deregistration is disabled, then do nothing
115+
if !d.Get("deregister_on_destroy").(bool) {
116+
log.Printf(
117+
"[WARN] Job %q will not deregister since 'deregister_on_destroy'"+
118+
" is false", d.Id())
119+
return nil
120+
}
121+
122+
id := d.Id()
123+
log.Printf("[DEBUG] Deregistering job: %q", id)
124+
_, _, err := client.Jobs().Deregister(id, nil)
125+
if err != nil {
126+
return fmt.Errorf("error deregistering job: %s", err)
127+
}
128+
129+
return nil
130+
}
131+
132+
func resourceJobExists(d *schema.ResourceData, meta interface{}) (bool, error) {
133+
client := meta.(*api.Client)
134+
135+
id := d.Id()
136+
log.Printf("[DEBUG] Checking if job exists: %q", id)
137+
_, _, err := client.Jobs().Info(id, nil)
138+
if err != nil {
139+
// As of Nomad 0.4.1, the API client returns an error for 404
140+
// rather than a nil result, so we must check this way.
141+
if strings.Contains(err.Error(), "404") {
142+
return false, nil
143+
}
144+
145+
return true, fmt.Errorf("error checking for job: %#v", err)
146+
}
147+
148+
return true, nil
149+
}
150+
151+
func resourceJobRead(d *schema.ResourceData, meta interface{}) error {
152+
// We don't do anything at the moment. Exists is used to
153+
// remove non-existent jobs but read doesn't have to do anything.
154+
return nil
155+
}
156+
157+
// convertStructJob is used to take a *structs.Job and convert it to an *api.Job.
158+
//
159+
// This is unfortunate but it is how Nomad itself does it (this is copied
160+
// line for line from Nomad). We'll mimic them exactly to get this done.
161+
func convertStructJob(in *structs.Job) (*api.Job, error) {
162+
gob.Register([]map[string]interface{}{})
163+
gob.Register([]interface{}{})
164+
var apiJob *api.Job
165+
buf := new(bytes.Buffer)
166+
if err := gob.NewEncoder(buf).Encode(in); err != nil {
167+
return nil, err
168+
}
169+
if err := gob.NewDecoder(buf).Decode(&apiJob); err != nil {
170+
return nil, err
171+
}
172+
return apiJob, nil
173+
}
174+
175+
// jobspecDiffSuppress is the DiffSuppressFunc used by the schema to
176+
// check if two jobspecs are equal.
177+
func jobspecDiffSuppress(k, old, new string, d *schema.ResourceData) bool {
178+
// Parse the old job
179+
oldJob, err := jobspec.Parse(strings.NewReader(old))
180+
if err != nil {
181+
return false
182+
}
183+
184+
// Parse the new job
185+
newJob, err := jobspec.Parse(strings.NewReader(new))
186+
if err != nil {
187+
return false
188+
}
189+
190+
// Init
191+
oldJob.Canonicalize()
192+
newJob.Canonicalize()
193+
194+
// Check for jobspec equality
195+
return reflect.DeepEqual(oldJob, newJob)
196+
}

0 commit comments

Comments
 (0)