diff --git a/builtin/bins/provider-dyn/main.go b/builtin/bins/provider-dyn/main.go new file mode 100644 index 000000000..22809f46a --- /dev/null +++ b/builtin/bins/provider-dyn/main.go @@ -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, + }) +} diff --git a/builtin/bins/provider-dyn/main_test.go b/builtin/bins/provider-dyn/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/builtin/bins/provider-dyn/main_test.go @@ -0,0 +1 @@ +package main diff --git a/builtin/providers/dyn/config.go b/builtin/providers/dyn/config.go new file mode 100644 index 000000000..091c929d9 --- /dev/null +++ b/builtin/providers/dyn/config.go @@ -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 +} diff --git a/builtin/providers/dyn/provider.go b/builtin/providers/dyn/provider.go new file mode 100644 index 000000000..c591745ae --- /dev/null +++ b/builtin/providers/dyn/provider.go @@ -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() +} diff --git a/builtin/providers/dyn/provider_test.go b/builtin/providers/dyn/provider_test.go new file mode 100644 index 000000000..da148ff2f --- /dev/null +++ b/builtin/providers/dyn/provider_test.go @@ -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.") + } +} diff --git a/builtin/providers/dyn/resource_dyn_record.go b/builtin/providers/dyn/resource_dyn_record.go new file mode 100644 index 000000000..7f7b66fd5 --- /dev/null +++ b/builtin/providers/dyn/resource_dyn_record.go @@ -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 +} diff --git a/builtin/providers/dyn/resource_dyn_record_test.go b/builtin/providers/dyn/resource_dyn_record_test.go new file mode 100644 index 000000000..e23367283 --- /dev/null +++ b/builtin/providers/dyn/resource_dyn_record_test.go @@ -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 +}` diff --git a/website/source/docs/providers/dyn/index.html.markdown b/website/source/docs/providers/dyn/index.html.markdown new file mode 100644 index 000000000..700bb0087 --- /dev/null +++ b/website/source/docs/providers/dyn/index.html.markdown @@ -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. diff --git a/website/source/docs/providers/dyn/r/record.html.markdown b/website/source/docs/providers/dyn/r/record.html.markdown new file mode 100644 index 000000000..6094c27de --- /dev/null +++ b/website/source/docs/providers/dyn/r/record.html.markdown @@ -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`. diff --git a/website/source/layouts/dyn.erb b/website/source/layouts/dyn.erb new file mode 100644 index 000000000..ee66e7270 --- /dev/null +++ b/website/source/layouts/dyn.erb @@ -0,0 +1,24 @@ +<% wrap_layout :inner do %> + <% content_for :sidebar do %> +
+ <% end %> + + <%= yield %> +<% end %>