Merge pull request #2807 from dwradcliffe/f-dyn-provider

add Dyn provider
This commit is contained in:
Paul Hinze 2015-11-16 13:53:44 -06:00
commit afb416fba4
10 changed files with 679 additions and 0 deletions

View File

@ -0,0 +1,12 @@
package main
import (
"github.com/hashicorp/terraform/builtin/providers/dyn"
"github.com/hashicorp/terraform/plugin"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: dyn.Provider,
})
}

View File

@ -0,0 +1 @@
package main

View File

@ -0,0 +1,28 @@
package dyn
import (
"fmt"
"log"
"github.com/nesv/go-dynect/dynect"
)
type Config struct {
CustomerName string
Username string
Password string
}
// Client() returns a new client for accessing dyn.
func (c *Config) Client() (*dynect.ConvenientClient, error) {
client := dynect.NewConvenientClient(c.CustomerName)
err := client.Login(c.Username, c.Password)
if err != nil {
return nil, fmt.Errorf("Error setting up Dyn client: %s", err)
}
log.Printf("[INFO] Dyn client configured for customer: %s, user: %s", c.CustomerName, c.Username)
return client, nil
}

View File

@ -0,0 +1,50 @@
package dyn
import (
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
// Provider returns a terraform.ResourceProvider.
func Provider() terraform.ResourceProvider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"customer_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("DYN_CUSTOMER_NAME", nil),
Description: "A Dyn customer name.",
},
"username": &schema.Schema{
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("DYN_USERNAME", nil),
Description: "A Dyn username.",
},
"password": &schema.Schema{
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("DYN_PASSWORD", nil),
Description: "The Dyn password.",
},
},
ResourcesMap: map[string]*schema.Resource{
"dyn_record": resourceDynRecord(),
},
ConfigureFunc: providerConfigure,
}
}
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := Config{
CustomerName: d.Get("customer_name").(string),
Username: d.Get("username").(string),
Password: d.Get("password").(string),
}
return config.Client()
}

View File

@ -0,0 +1,47 @@
package dyn
import (
"os"
"testing"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
var testAccProviders map[string]terraform.ResourceProvider
var testAccProvider *schema.Provider
func init() {
testAccProvider = Provider().(*schema.Provider)
testAccProviders = map[string]terraform.ResourceProvider{
"dyn": testAccProvider,
}
}
func TestProvider(t *testing.T) {
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = Provider()
}
func testAccPreCheck(t *testing.T) {
if v := os.Getenv("DYN_CUSTOMER_NAME"); v == "" {
t.Fatal("DYN_CUSTOMER_NAME must be set for acceptance tests")
}
if v := os.Getenv("DYN_USERNAME"); v == "" {
t.Fatal("DYN_USERNAME must be set for acceptance tests")
}
if v := os.Getenv("DYN_PASSWORD"); v == "" {
t.Fatal("DYN_PASSWORD must be set for acceptance tests.")
}
if v := os.Getenv("DYN_ZONE"); v == "" {
t.Fatal("DYN_ZONE must be set for acceptance tests. The domain is used to ` and destroy record against.")
}
}

View File

@ -0,0 +1,198 @@
package dyn
import (
"fmt"
"log"
"sync"
"github.com/hashicorp/terraform/helper/schema"
"github.com/nesv/go-dynect/dynect"
)
var mutex = &sync.Mutex{}
func resourceDynRecord() *schema.Resource {
return &schema.Resource{
Create: resourceDynRecordCreate,
Read: resourceDynRecordRead,
Update: resourceDynRecordUpdate,
Delete: resourceDynRecordDelete,
Schema: map[string]*schema.Schema{
"zone": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"fqdn": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"type": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"value": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"ttl": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "0", // 0 means use zone default
},
},
}
}
func resourceDynRecordCreate(d *schema.ResourceData, meta interface{}) error {
mutex.Lock()
client := meta.(*dynect.ConvenientClient)
record := &dynect.Record{
Name: d.Get("name").(string),
Zone: d.Get("zone").(string),
Type: d.Get("type").(string),
TTL: d.Get("ttl").(string),
Value: d.Get("value").(string),
}
log.Printf("[DEBUG] Dyn record create configuration: %#v", record)
// create the record
err := client.CreateRecord(record)
if err != nil {
mutex.Unlock()
return fmt.Errorf("Failed to create Dyn record: %s", err)
}
// publish the zone
err = client.PublishZone(record.Zone)
if err != nil {
mutex.Unlock()
return fmt.Errorf("Failed to publish Dyn zone: %s", err)
}
// get the record ID
err = client.GetRecordID(record)
if err != nil {
mutex.Unlock()
return fmt.Errorf("%s", err)
}
d.SetId(record.ID)
mutex.Unlock()
return resourceDynRecordRead(d, meta)
}
func resourceDynRecordRead(d *schema.ResourceData, meta interface{}) error {
mutex.Lock()
defer mutex.Unlock()
client := meta.(*dynect.ConvenientClient)
record := &dynect.Record{
ID: d.Id(),
Name: d.Get("name").(string),
Zone: d.Get("zone").(string),
TTL: d.Get("ttl").(string),
FQDN: d.Get("fqdn").(string),
Type: d.Get("type").(string),
}
err := client.GetRecord(record)
if err != nil {
return fmt.Errorf("Couldn't find Dyn record: %s", err)
}
d.Set("zone", record.Zone)
d.Set("fqdn", record.FQDN)
d.Set("name", record.Name)
d.Set("type", record.Type)
d.Set("ttl", record.TTL)
d.Set("value", record.Value)
return nil
}
func resourceDynRecordUpdate(d *schema.ResourceData, meta interface{}) error {
mutex.Lock()
client := meta.(*dynect.ConvenientClient)
record := &dynect.Record{
Name: d.Get("name").(string),
Zone: d.Get("zone").(string),
TTL: d.Get("ttl").(string),
Type: d.Get("type").(string),
Value: d.Get("value").(string),
}
log.Printf("[DEBUG] Dyn record update configuration: %#v", record)
// update the record
err := client.UpdateRecord(record)
if err != nil {
mutex.Unlock()
return fmt.Errorf("Failed to update Dyn record: %s", err)
}
// publish the zone
err = client.PublishZone(record.Zone)
if err != nil {
mutex.Unlock()
return fmt.Errorf("Failed to publish Dyn zone: %s", err)
}
// get the record ID
err = client.GetRecordID(record)
if err != nil {
mutex.Unlock()
return fmt.Errorf("%s", err)
}
d.SetId(record.ID)
mutex.Unlock()
return resourceDynRecordRead(d, meta)
}
func resourceDynRecordDelete(d *schema.ResourceData, meta interface{}) error {
mutex.Lock()
defer mutex.Unlock()
client := meta.(*dynect.ConvenientClient)
record := &dynect.Record{
ID: d.Id(),
Name: d.Get("name").(string),
Zone: d.Get("zone").(string),
FQDN: d.Get("fqdn").(string),
Type: d.Get("type").(string),
}
log.Printf("[INFO] Deleting Dyn record: %s, %s", record.FQDN, record.ID)
// delete the record
err := client.DeleteRecord(record)
if err != nil {
return fmt.Errorf("Failed to delete Dyn record: %s", err)
}
// publish the zone
err = client.PublishZone(record.Zone)
if err != nil {
return fmt.Errorf("Failed to publish Dyn zone: %s", err)
}
return nil
}

View File

@ -0,0 +1,239 @@
package dyn
import (
"fmt"
"os"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/nesv/go-dynect/dynect"
)
func TestAccDynRecord_Basic(t *testing.T) {
var record dynect.Record
zone := os.Getenv("DYN_ZONE")
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDynRecordDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: fmt.Sprintf(testAccCheckDynRecordConfig_basic, zone),
Check: resource.ComposeTestCheckFunc(
testAccCheckDynRecordExists("dyn_record.foobar", &record),
testAccCheckDynRecordAttributes(&record),
resource.TestCheckResourceAttr(
"dyn_record.foobar", "name", "terraform"),
resource.TestCheckResourceAttr(
"dyn_record.foobar", "zone", zone),
resource.TestCheckResourceAttr(
"dyn_record.foobar", "value", "192.168.0.10"),
),
},
},
})
}
func TestAccDynRecord_Updated(t *testing.T) {
var record dynect.Record
zone := os.Getenv("DYN_ZONE")
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDynRecordDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: fmt.Sprintf(testAccCheckDynRecordConfig_basic, zone),
Check: resource.ComposeTestCheckFunc(
testAccCheckDynRecordExists("dyn_record.foobar", &record),
testAccCheckDynRecordAttributes(&record),
resource.TestCheckResourceAttr(
"dyn_record.foobar", "name", "terraform"),
resource.TestCheckResourceAttr(
"dyn_record.foobar", "zone", zone),
resource.TestCheckResourceAttr(
"dyn_record.foobar", "value", "192.168.0.10"),
),
},
resource.TestStep{
Config: fmt.Sprintf(testAccCheckDynRecordConfig_new_value, zone),
Check: resource.ComposeTestCheckFunc(
testAccCheckDynRecordExists("dyn_record.foobar", &record),
testAccCheckDynRecordAttributesUpdated(&record),
resource.TestCheckResourceAttr(
"dyn_record.foobar", "name", "terraform"),
resource.TestCheckResourceAttr(
"dyn_record.foobar", "zone", zone),
resource.TestCheckResourceAttr(
"dyn_record.foobar", "value", "192.168.0.11"),
),
},
},
})
}
func TestAccDynRecord_Multiple(t *testing.T) {
var record dynect.Record
zone := os.Getenv("DYN_ZONE")
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDynRecordDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: fmt.Sprintf(testAccCheckDynRecordConfig_multiple, zone, zone, zone),
Check: resource.ComposeTestCheckFunc(
testAccCheckDynRecordExists("dyn_record.foobar1", &record),
testAccCheckDynRecordAttributes(&record),
resource.TestCheckResourceAttr(
"dyn_record.foobar1", "name", "terraform1"),
resource.TestCheckResourceAttr(
"dyn_record.foobar1", "zone", zone),
resource.TestCheckResourceAttr(
"dyn_record.foobar1", "value", "192.168.0.10"),
resource.TestCheckResourceAttr(
"dyn_record.foobar2", "name", "terraform2"),
resource.TestCheckResourceAttr(
"dyn_record.foobar2", "zone", zone),
resource.TestCheckResourceAttr(
"dyn_record.foobar2", "value", "192.168.1.10"),
resource.TestCheckResourceAttr(
"dyn_record.foobar3", "name", "terraform3"),
resource.TestCheckResourceAttr(
"dyn_record.foobar3", "zone", zone),
resource.TestCheckResourceAttr(
"dyn_record.foobar3", "value", "192.168.2.10"),
),
},
},
})
}
func testAccCheckDynRecordDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*dynect.ConvenientClient)
for _, rs := range s.RootModule().Resources {
if rs.Type != "dyn_record" {
continue
}
foundRecord := &dynect.Record{
Zone: rs.Primary.Attributes["zone"],
ID: rs.Primary.ID,
FQDN: rs.Primary.Attributes["fqdn"],
Type: rs.Primary.Attributes["type"],
}
err := client.GetRecord(foundRecord)
if err != nil {
return fmt.Errorf("Record still exists")
}
}
return nil
}
func testAccCheckDynRecordAttributes(record *dynect.Record) resource.TestCheckFunc {
return func(s *terraform.State) error {
if record.Value != "192.168.0.10" {
return fmt.Errorf("Bad value: %s", record.Value)
}
return nil
}
}
func testAccCheckDynRecordAttributesUpdated(record *dynect.Record) resource.TestCheckFunc {
return func(s *terraform.State) error {
if record.Value != "192.168.0.11" {
return fmt.Errorf("Bad value: %s", record.Value)
}
return nil
}
}
func testAccCheckDynRecordExists(n string, record *dynect.Record) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
if rs.Primary.ID == "" {
return fmt.Errorf("No Record ID is set")
}
client := testAccProvider.Meta().(*dynect.ConvenientClient)
foundRecord := &dynect.Record{
Zone: rs.Primary.Attributes["zone"],
ID: rs.Primary.ID,
FQDN: rs.Primary.Attributes["fqdn"],
Type: rs.Primary.Attributes["type"],
}
err := client.GetRecord(foundRecord)
if err != nil {
return err
}
if foundRecord.ID != rs.Primary.ID {
return fmt.Errorf("Record not found")
}
*record = *foundRecord
return nil
}
}
const testAccCheckDynRecordConfig_basic = `
resource "dyn_record" "foobar" {
zone = "%s"
name = "terraform"
value = "192.168.0.10"
type = "A"
ttl = 3600
}`
const testAccCheckDynRecordConfig_new_value = `
resource "dyn_record" "foobar" {
zone = "%s"
name = "terraform"
value = "192.168.0.11"
type = "A"
ttl = 3600
}`
const testAccCheckDynRecordConfig_multiple = `
resource "dyn_record" "foobar1" {
zone = "%s"
name = "terraform1"
value = "192.168.0.10"
type = "A"
ttl = 3600
}
resource "dyn_record" "foobar2" {
zone = "%s"
name = "terraform2"
value = "192.168.1.10"
type = "A"
ttl = 3600
}
resource "dyn_record" "foobar3" {
zone = "%s"
name = "terraform3"
value = "192.168.2.10"
type = "A"
ttl = 3600
}`

View File

@ -0,0 +1,39 @@
---
layout: "dyn"
page_title: "Provider: Dyn"
sidebar_current: "docs-dyn-index"
description: |-
The Dyn provider is used to interact with the resources supported by Dyn. The provider needs to be configured with the proper credentials before it can be used.
---
# Dyn Provider
The Dyn provider is used to interact with the
resources supported by Dyn. The provider needs to be configured
with the proper credentials before it can be used.
Use the navigation to the left to read about the available resources.
## Example Usage
```
# Configure the Dyn provider
provider "dyn" {
customer_name = "${var.dyn_customer_name}"
username = "${var.dyn_username}"
password = "${var.dyn_password}"
}
# Create a record
resource "dyn_record" "www" {
...
}
```
## Argument Reference
The following arguments are supported:
* `customer_name` - (Required) The Dyn customer name. It must be provided, but it can also be sourced from the `DYN_CUSTOMER_NAME` environment variable.
* `username` - (Required) The Dyn username. It must be provided, but it can also be sourced from the `DYN_USERNAME` environment variable.
* `password` - (Required) The Dyn password. It must be provided, but it can also be sourced from the `DYN_PASSWORD` environment variable.

View File

@ -0,0 +1,41 @@
---
layout: "dyn"
page_title: "Dyn: dyn_record"
sidebar_current: "docs-dyn-resource-record"
description: |-
Provides a Dyn DNS record resource.
---
# dyn\_record
Provides a Dyn DNS record resource.
## Example Usage
```
# Add a record to the domain
resource "dyn_record" "foobar" {
zone = "${var.dyn_zone}"
name = "terraform"
value = "192.168.0.11"
type = "A"
ttl = 3600
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required) The name of the record.
* `type` - (Required) The type of the record.
* `value` - (Required) The value of the record.
* `zone` - (Required) The DNS zone to add the record to.
* `ttl` - (Optional) The TTL of the record. Default uses the zone default.
## Attributes Reference
The following attributes are exported:
* `id` - The record ID.
* `fqdn` - The FQDN of the record, built from the `name` and the `zone`.

View File

@ -0,0 +1,24 @@
<% 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-dyn-index") %>>
<a href="/docs/providers/dyn/index.html">Dyn Provider</a>
</li>
<li<%= sidebar_current(/^docs-dyn-resource/) %>>
<a href="#">Resources</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-dyn-resource-record") %>>
<a href="/docs/providers/dyn/r/record.html">dyn_record</a>
</li>
</ul>
</li>
</ul>
</div>
<% end %>
<%= yield %>
<% end %>