provider/nomad

This commit is contained in:
Mitchell Hashimoto 2016-10-23 23:18:04 -07:00
parent 8af241e501
commit bb5f6498e2
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
10 changed files with 693 additions and 0 deletions

View File

@ -0,0 +1,49 @@
package nomad
import (
"fmt"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
func Provider() terraform.ResourceProvider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"address": &schema.Schema{
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("NOMAD_ADDR", nil),
Description: "URL of the root of the target Nomad agent.",
},
"region": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("NOMAD_REGION", ""),
Description: "Region of the target Nomad agent.",
},
},
ConfigureFunc: providerConfigure,
ResourcesMap: map[string]*schema.Resource{
"nomad_job": resourceJob(),
},
}
}
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := &api.Config{
Address: d.Get("address").(string),
Region: d.Get("region").(string),
}
client, err := api.NewClient(config)
if err != nil {
return nil, fmt.Errorf("failed to configure Nomad API: %s", err)
}
return client, nil
}

View File

@ -0,0 +1,44 @@
package nomad
import (
"os"
"testing"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
// How to run the acceptance tests for this provider:
//
// - Obtain an official Nomad release from https://nomadproject.io
// and extract the "nomad" binary
//
// - Run the following to start the Nomad agent in development mode:
// nomad agent -dev
//
// - Run the Terraform acceptance tests as usual:
// make testacc TEST=./builtin/providers/nomad
//
// The tests expect to be run in a fresh, empty Nomad server.
func TestProvider(t *testing.T) {
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}
var testProvider *schema.Provider
var testProviders map[string]terraform.ResourceProvider
func init() {
testProvider = Provider().(*schema.Provider)
testProviders = map[string]terraform.ResourceProvider{
"nomad": testProvider,
}
}
func testAccPreCheck(t *testing.T) {
if v := os.Getenv("NOMAD_ADDR"); v == "" {
os.Setenv("NOMAD_ADDR", "http://127.0.0.1:4646")
}
}

View File

@ -0,0 +1,171 @@
package nomad
import (
"bytes"
"encoding/gob"
"fmt"
"log"
"strings"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/jobspec"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceJob() *schema.Resource {
return &schema.Resource{
Create: resourceJobRegister,
Update: resourceJobRegister,
Delete: resourceJobDeregister,
Read: resourceJobRead,
Exists: resourceJobExists,
Schema: map[string]*schema.Schema{
"jobspec": {
Description: "Job specification. If you want to point to a file use the file() function.",
Required: true,
Type: schema.TypeString,
},
"deregister_on_destroy": {
Description: "If true, the job will be deregistered on destroy.",
Optional: true,
Default: true,
Type: schema.TypeBool,
},
"deregister_on_id_change": {
Description: "If true, the job will be deregistered when the job ID changes.",
Optional: true,
Default: true,
Type: schema.TypeBool,
},
},
}
}
func resourceJobRegister(d *schema.ResourceData, meta interface{}) error {
client := meta.(*api.Client)
// Get the jobspec itself
jobspecRaw := d.Get("jobspec").(string)
// Parse it
jobspecStruct, err := jobspec.Parse(strings.NewReader(jobspecRaw))
if err != nil {
return fmt.Errorf("error parsing jobspec: %s", err)
}
// Initialize and validate
jobspecStruct.Canonicalize()
if err := jobspecStruct.Validate(); err != nil {
return fmt.Errorf("Error validating job: %v", err)
}
// If we have an ID and its not equal to this jobspec, then we
// have to deregister the old job before we register the new job.
prevId := d.Id()
if !d.Get("deregister_on_id_change").(bool) {
// If we aren't deregistering on ID change, just pretend we
// don't have a prior ID.
prevId = ""
}
if prevId != "" && prevId != jobspecStruct.ID {
log.Printf(
"[INFO] Deregistering %q before registering %q",
prevId, jobspecStruct.ID)
log.Printf("[DEBUG] Deregistering job: %q", prevId)
_, _, err := client.Jobs().Deregister(prevId, nil)
if err != nil {
return fmt.Errorf(
"error deregistering previous job %q "+
"before registering new job %q: %s",
prevId, jobspecStruct.ID, err)
}
// Success! Clear our state.
d.SetId("")
}
// Convert it so that we can use it with the API
jobspecAPI, err := convertStructJob(jobspecStruct)
if err != nil {
return fmt.Errorf("error converting jobspec: %s", err)
}
// Register the job
_, _, err = client.Jobs().Register(jobspecAPI, nil)
if err != nil {
return fmt.Errorf("error applying jobspec: %s", err)
}
d.SetId(jobspecAPI.ID)
return nil
}
func resourceJobDeregister(d *schema.ResourceData, meta interface{}) error {
client := meta.(*api.Client)
// If deregistration is disabled, then do nothing
if !d.Get("deregister_on_destroy").(bool) {
log.Printf(
"[WARN] Job %q will not deregister since 'deregister_on_destroy'"+
" is false", d.Id())
return nil
}
id := d.Id()
log.Printf("[DEBUG] Deregistering job: %q", id)
_, _, err := client.Jobs().Deregister(id, nil)
if err != nil {
return fmt.Errorf("error deregistering job: %s", err)
}
return nil
}
func resourceJobExists(d *schema.ResourceData, meta interface{}) (bool, error) {
client := meta.(*api.Client)
id := d.Id()
log.Printf("[DEBUG] Checking if job exists: %q", id)
_, _, err := client.Jobs().Info(id, nil)
if err != nil {
// As of Nomad 0.4.1, the API client returns an error for 404
// rather than a nil result, so we must check this way.
if strings.Contains(err.Error(), "404") {
return false, nil
}
return true, fmt.Errorf("error checking for job: %#v", err)
}
return true, nil
}
func resourceJobRead(d *schema.ResourceData, meta interface{}) error {
// We don't do anything at the moment. Exists is used to
// remove non-existent jobs but read doesn't have to do anything.
return nil
}
// convertStructJob is used to take a *structs.Job and convert it to an *api.Job.
//
// This is unfortunate but it is how Nomad itself does it (this is copied
// line for line from Nomad). We'll mimic them exactly to get this done.
func convertStructJob(in *structs.Job) (*api.Job, error) {
gob.Register([]map[string]interface{}{})
gob.Register([]interface{}{})
var apiJob *api.Job
buf := new(bytes.Buffer)
if err := gob.NewEncoder(buf).Encode(in); err != nil {
return nil, err
}
if err := gob.NewDecoder(buf).Decode(&apiJob); err != nil {
return nil, err
}
return apiJob, nil
}

View File

@ -0,0 +1,283 @@
package nomad
import (
"fmt"
"strings"
"testing"
r "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/nomad/api"
)
func TestResourceJob_basic(t *testing.T) {
r.Test(t, r.TestCase{
Providers: testProviders,
PreCheck: func() { testAccPreCheck(t) },
Steps: []r.TestStep{
r.TestStep{
Config: testResourceJob_initialConfig,
Check: testResourceJob_initialCheck,
},
},
CheckDestroy: testResourceJob_checkDestroy("foo"),
})
}
func TestResourceJob_refresh(t *testing.T) {
r.Test(t, r.TestCase{
Providers: testProviders,
PreCheck: func() { testAccPreCheck(t) },
Steps: []r.TestStep{
r.TestStep{
Config: testResourceJob_initialConfig,
Check: testResourceJob_initialCheck,
},
// This should successfully cause the job to be recreated,
// testing the Exists function.
r.TestStep{
PreConfig: testResourceJob_deregister(t, "foo"),
Config: testResourceJob_initialConfig,
},
},
})
}
func TestResourceJob_disableDestroyDeregister(t *testing.T) {
r.Test(t, r.TestCase{
Providers: testProviders,
PreCheck: func() { testAccPreCheck(t) },
Steps: []r.TestStep{
r.TestStep{
Config: testResourceJob_noDestroy,
Check: testResourceJob_initialCheck,
},
// Destroy with our setting set
r.TestStep{
Destroy: true,
Config: testResourceJob_noDestroy,
Check: testResourceJob_checkExists,
},
// Re-apply without the setting set
r.TestStep{
Config: testResourceJob_initialConfig,
Check: testResourceJob_checkExists,
},
},
})
}
func TestResourceJob_idChange(t *testing.T) {
r.Test(t, r.TestCase{
Providers: testProviders,
PreCheck: func() { testAccPreCheck(t) },
Steps: []r.TestStep{
r.TestStep{
Config: testResourceJob_initialConfig,
Check: testResourceJob_initialCheck,
},
// Change our ID
r.TestStep{
Config: testResourceJob_updateConfig,
Check: testResourceJob_updateCheck,
},
},
})
}
var testResourceJob_initialConfig = `
resource "nomad_job" "test" {
jobspec = <<EOT
job "foo" {
datacenters = ["dc1"]
type = "service"
group "foo" {
task "foo" {
driver = "raw_exec"
config {
command = "/bin/sleep"
args = ["1"]
}
resources {
cpu = 20
memory = 10
disk = 100
}
logs {
max_files = 3
max_file_size = 10
}
}
}
}
EOT
}
`
var testResourceJob_noDestroy = `
resource "nomad_job" "test" {
deregister_on_destroy = false
jobspec = <<EOT
job "foo" {
datacenters = ["dc1"]
type = "service"
group "foo" {
task "foo" {
driver = "raw_exec"
config {
command = "/bin/sleep"
args = ["1"]
}
resources {
cpu = 20
memory = 10
disk = 100
}
logs {
max_files = 3
max_file_size = 10
}
}
}
}
EOT
}
`
func testResourceJob_initialCheck(s *terraform.State) error {
resourceState := s.Modules[0].Resources["nomad_job.test"]
if resourceState == nil {
return fmt.Errorf("resource not found in state")
}
instanceState := resourceState.Primary
if instanceState == nil {
return fmt.Errorf("resource has no primary instance")
}
jobID := instanceState.ID
client := testProvider.Meta().(*api.Client)
job, _, err := client.Jobs().Info(jobID, nil)
if err != nil {
return fmt.Errorf("error reading back job: %s", err)
}
if got, want := job.ID, jobID; got != want {
return fmt.Errorf("jobID is %q; want %q", got, want)
}
return nil
}
func testResourceJob_checkExists(s *terraform.State) error {
jobID := "foo"
client := testProvider.Meta().(*api.Client)
_, _, err := client.Jobs().Info(jobID, nil)
if err != nil {
return fmt.Errorf("error reading back job: %s", err)
}
return nil
}
func testResourceJob_checkDestroy(jobID string) r.TestCheckFunc {
return func(*terraform.State) error {
client := testProvider.Meta().(*api.Client)
_, _, err := client.Jobs().Info(jobID, nil)
if err != nil && strings.Contains(err.Error(), "404") {
return nil
}
if err == nil {
err = fmt.Errorf("not destroyed")
}
return err
}
}
func testResourceJob_deregister(t *testing.T, jobID string) func() {
return func() {
client := testProvider.Meta().(*api.Client)
_, _, err := client.Jobs().Deregister(jobID, nil)
if err != nil {
t.Fatalf("error deregistering job: %s", err)
}
}
}
var testResourceJob_updateConfig = `
resource "nomad_job" "test" {
jobspec = <<EOT
job "bar" {
datacenters = ["dc1"]
type = "service"
group "foo" {
task "foo" {
driver = "raw_exec"
config {
command = "/bin/sleep"
args = ["1"]
}
resources {
cpu = 20
memory = 10
disk = 100
}
logs {
max_files = 3
max_file_size = 10
}
}
}
}
EOT
}
`
func testResourceJob_updateCheck(s *terraform.State) error {
resourceState := s.Modules[0].Resources["nomad_job.test"]
if resourceState == nil {
return fmt.Errorf("resource not found in state")
}
instanceState := resourceState.Primary
if instanceState == nil {
return fmt.Errorf("resource has no primary instance")
}
jobID := instanceState.ID
client := testProvider.Meta().(*api.Client)
job, _, err := client.Jobs().Info(jobID, nil)
if err != nil {
return fmt.Errorf("error reading back job: %s", err)
}
if got, want := job.ID, jobID; got != want {
return fmt.Errorf("jobID is %q; want %q", got, want)
}
{
// Verify foo doesn't exist
_, _, err := client.Jobs().Info("foo", nil)
if err == nil {
return fmt.Errorf("reading foo success")
}
}
return nil
}

View File

@ -34,6 +34,7 @@ import (
logentriesprovider "github.com/hashicorp/terraform/builtin/providers/logentries"
mailgunprovider "github.com/hashicorp/terraform/builtin/providers/mailgun"
mysqlprovider "github.com/hashicorp/terraform/builtin/providers/mysql"
nomadprovider "github.com/hashicorp/terraform/builtin/providers/nomad"
nullprovider "github.com/hashicorp/terraform/builtin/providers/null"
openstackprovider "github.com/hashicorp/terraform/builtin/providers/openstack"
packetprovider "github.com/hashicorp/terraform/builtin/providers/packet"
@ -91,6 +92,7 @@ var InternalProviders = map[string]plugin.ProviderFunc{
"logentries": logentriesprovider.Provider,
"mailgun": mailgunprovider.Provider,
"mysql": mysqlprovider.Provider,
"nomad": nomadprovider.Provider,
"null": nullprovider.Provider,
"openstack": openstackprovider.Provider,
"packet": packetprovider.Provider,

View File

@ -35,6 +35,7 @@ body.layout-librato,
body.layout-logentries,
body.layout-mailgun,
body.layout-mysql,
body.layout-nomad,
body.layout-openstack,
body.layout-packet,
body.layout-postgresql,

View File

@ -0,0 +1,36 @@
---
layout: "nomad"
page_title: "Provider: Nomad"
sidebar_current: "docs-nomad-index"
description: |-
Nomad is a cluster scheduler. The Nomad provider exposes resources to interact with a Nomad cluster.
---
# Nomad Provider
[Nomad](https://www.nomadproject.io) is a cluster scheduler. The Nomad
provider exposes resources to interact with a Nomad cluster.
Use the navigation to the left to read about the available resources.
## Example Usage
```
# Configure the Nomad provider
provider "nomad" {
address = "nomad.mycompany.com"
region = "us-east-2"
}
# Register a job
resource "nomad_job" "monitoring" {
jobspec = "${file("${path.module}/jobspec.hcl")}"
}
```
## Argument Reference
The following arguments are supported:
* `address` - (Optional) The HTTP(S) API address of the Nomad agent to use. Defaults to `127.0.0.1:4646`. The `NOMAD_ADDR` environment variable can also be used.
* `region` - (Optional) The Nomad region to target. The `NOMAD_REGION` environment variable can also be used.

View File

@ -0,0 +1,77 @@
---
layout: "nomad"
page_title: "Nomad: nomad_job"
sidebar_current: "docs-nomad-resource-job"
description: |-
Manages a job registered in Nomad.
---
# nomad\_job
Manages a job registered in Nomad.
This can be used to initialize your cluster with system jobs, common services,
and more. In day to day Nomad use it is common for developers to submit
jobs to Nomad directly, such as for general app deployment. In addition to
these apps, a Nomad cluster often runs core system services that are ideally
setup during infrastructure creation. This resource is ideal for the latter
type of job, but can be used to manage any job within Nomad.
## Example Usage
Registering a job from a jobspec file:
```
resource "nomad_job" "app" {
jobspec = "${file("${path.module}/job.hcl")}"
}
```
Registering a job from an inline jobspec. This is less realistic but
is an example of how it is possible. More likely, the contents will
be paired with something such as the
[template_file](https://www.terraform.io/docs/providers/template/d/file.html)
resource to render parameterized jobspecs.
```
resource "nomad_job" "app" {
jobspec = <<EOT
job "foo" {
datacenters = ["dc1"]
type = "service"
group "foo" {
task "foo" {
driver = "raw_exec"
config {
command = "/bin/sleep"
args = ["1"]
}
resources {
cpu = 20
memory = 10
disk = 100
}
logs {
max_files = 3
max_file_size = 10
}
}
}
}
EOT
}
```
## Argument Reference
The following arguments are supported:
* `jobspec` - (Required) The contents of the jobspec to register.
* `deregister_on_destroy` - (Optional) If true, the job will be deregistered
when this resource is destroyed in Terraform. Defaults to true.
* `deregister_on_id_change` - (Optional) If true, the job will be deregistered
if the ID of the job in the jobspec changes. Defaults to true.

View File

@ -262,6 +262,10 @@
<a href="/docs/providers/mailgun/index.html">Mailgun</a>
</li>
<li<%= sidebar_current("docs-providers-nomad") %>>
<a href="/docs/providers/nomad/index.html">Nomad</a>
</li>
<li<%= sidebar_current("docs-providers-azurerm") %>>
<a href="/docs/providers/azurerm/index.html">Microsoft Azure</a>
</li>

View File

@ -0,0 +1,26 @@
<% wrap_layout :inner do %>
<% content_for :sidebar do %>
<div class="docs-sidebar hidden-print affix-top" role="complementary">
<ul class="nav docs-sidenav">
<li<%= sidebar_current("docs-home") %>>
<a href="/docs/providers/index.html">&laquo; Documentation Home</a>
</li>
<li<%= sidebar_current("docs-nomad-index") %>>
<a href="/docs/providers/nomad/index.html">Nomad Provider</a>
</li>
<li<%= sidebar_current(/^docs-nomad-resource/) %>>
<a href="#">Resources</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-nomad-resource-job") %>>
<a href="/docs/providers/nomad/r/job.html">nomad_job</a>
</li>
</ul>
</li>
</ul>
</div>
<% end %>
<%= yield %>
<% end %>