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..24c2977d5 --- /dev/null +++ b/builtin/providers/dyn/resource_dyn_record.go @@ -0,0 +1,177 @@ +package dyn + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/nesv/go-dynect/dynect" +) + +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 { + 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 { + return fmt.Errorf("Failed to create 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) + } + + // get the record ID + err = client.GetRecordID(record) + if err != nil { + return fmt.Errorf("%s", err) + } + d.SetId(record.ID) + + return resourceDynRecordRead(d, meta) +} + +func resourceDynRecordRead(d *schema.ResourceData, meta interface{}) error { + 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 { + 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 { + return fmt.Errorf("Failed to update 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) + } + + // get the record ID + err = client.GetRecordID(record) + if err != nil { + return fmt.Errorf("%s", err) + } + d.SetId(record.ID) + + return resourceDynRecordRead(d, meta) +} + +func resourceDynRecordDelete(d *schema.ResourceData, meta interface{}) error { + 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..ccb0ccd98 --- /dev/null +++ b/builtin/providers/dyn/resource_dyn_record_test.go @@ -0,0 +1,178 @@ +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 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 +}`