diff --git a/builtin/bins/provider-dnsimple/main.go b/builtin/bins/provider-dnsimple/main.go new file mode 100644 index 000000000..44860d71b --- /dev/null +++ b/builtin/bins/provider-dnsimple/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/dnsimple" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(new(dnsimple.ResourceProvider)) +} diff --git a/builtin/bins/provider-dnsimple/main_test.go b/builtin/bins/provider-dnsimple/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/builtin/bins/provider-dnsimple/main_test.go @@ -0,0 +1 @@ +package main diff --git a/builtin/providers/dnsimple/config.go b/builtin/providers/dnsimple/config.go new file mode 100644 index 000000000..dfec0f2e1 --- /dev/null +++ b/builtin/providers/dnsimple/config.go @@ -0,0 +1,33 @@ +package dnsimple + +import ( + "log" + "os" + + "github.com/rubyist/go-dnsimple" +) + +type Config struct { + Token string `mapstructure:"token"` + Email string `mapstructure:"email"` +} + +// Client() returns a new client for accessing heroku. +// +func (c *Config) Client() (*dnsimple.DNSimpleClient, error) { + + // If we have env vars set (like in the acc) tests, + // we need to override the values passed in here. + if v := os.Getenv("DNSIMPLE_EMAIL"); v != "" { + c.Email = v + } + if v := os.Getenv("DNSIMPLE_TOKEN"); v != "" { + c.Token = v + } + + client := dnsimple.NewClient(c.Token, c.Email) + + log.Printf("[INFO] DNSimple Client configured for user: %s", client.Email) + + return client, nil +} diff --git a/builtin/providers/dnsimple/resource_dnsimple_record.go b/builtin/providers/dnsimple/resource_dnsimple_record.go new file mode 100644 index 000000000..881b28a71 --- /dev/null +++ b/builtin/providers/dnsimple/resource_dnsimple_record.go @@ -0,0 +1,163 @@ +package dnsimple + +import ( + "fmt" + "log" + "strconv" + + "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/helper/diff" + "github.com/hashicorp/terraform/terraform" + "github.com/rubyist/go-dnsimple" +) + +func resource_dnsimple_record_create( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + + // Merge the diff into the state so that we have all the attributes + // properly. + rs := s.MergeDiff(d) + + var err error + + newRecord := dnsimple.Record{ + Name: rs.Attributes["name"], + Content: rs.Attributes["value"], + RecordType: rs.Attributes["type"], + } + + if attr, ok := rs.Attributes["ttl"]; ok { + newRecord.TTL, err = strconv.Atoi(attr) + if err != nil { + return nil, err + } + } + + log.Printf("[DEBUG] record create configuration: %#v", newRecord) + + rec, err := client.CreateRecord(rs.Attributes["domain"], newRecord) + + if err != nil { + return nil, fmt.Errorf("Failed to create record: %s", err) + } + + rs.ID = strconv.Itoa(rec.Id) + + log.Printf("[INFO] record ID: %s", rs.ID) + + return resource_dnsimple_record_update_state(rs, &rec) +} + +func resource_dnsimple_record_update( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + + panic("Cannot update record") + + return nil, nil +} + +func resource_dnsimple_record_destroy( + s *terraform.ResourceState, + meta interface{}) error { + p := meta.(*ResourceProvider) + client := p.client + + log.Printf("[INFO] Deleting record: %s", s.ID) + + rec, err := resource_dnsimple_record_retrieve(s.Attributes["domain"], s.ID, client) + if err != nil { + return err + } + + err = rec.Delete(client) + if err != nil { + return fmt.Errorf("Error deleting record: %s", err) + } + + return nil +} + +func resource_dnsimple_record_refresh( + s *terraform.ResourceState, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + + rec, err := resource_dnsimple_record_retrieve(s.Attributes["app"], s.ID, client) + if err != nil { + return nil, err + } + + return resource_dnsimple_record_update_state(s, rec) +} + +func resource_dnsimple_record_diff( + s *terraform.ResourceState, + c *terraform.ResourceConfig, + meta interface{}) (*terraform.ResourceDiff, error) { + + b := &diff.ResourceBuilder{ + Attrs: map[string]diff.AttrType{ + "domain": diff.AttrTypeCreate, + "name": diff.AttrTypeCreate, + "value": diff.AttrTypeUpdate, + "ttl": diff.AttrTypeCreate, + "type": diff.AttrTypeUpdate, + }, + + ComputedAttrs: []string{ + "priority", + "domain_id", + }, + } + + return b.Diff(s, c) +} + +func resource_dnsimple_record_update_state( + s *terraform.ResourceState, + rec *dnsimple.Record) (*terraform.ResourceState, error) { + + s.Attributes["name"] = rec.Name + s.Attributes["value"] = rec.Content + s.Attributes["type"] = rec.RecordType + s.Attributes["ttl"] = strconv.Itoa(rec.TTL) + s.Attributes["priority"] = strconv.Itoa(rec.Priority) + s.Attributes["domain_id"] = strconv.Itoa(rec.DomainId) + + return s, nil +} + +func resource_dnsimple_record_retrieve(domain string, id string, client *dnsimple.DNSimpleClient) (*dnsimple.Record, error) { + intId, err := strconv.Atoi(id) + if err != nil { + return nil, err + } + + record, err := client.RetrieveRecord(domain, intId) + if err != nil { + return nil, fmt.Errorf("Error retrieving record: %s", err) + } + + return &record, nil +} + +func resource_dnsimple_record_validation() *config.Validator { + return &config.Validator{ + Required: []string{ + "domain", + "name", + "value", + "type", + }, + Optional: []string{ + "ttl", + }, + } +} diff --git a/builtin/providers/dnsimple/resource_dnsimple_record_test.go b/builtin/providers/dnsimple/resource_dnsimple_record_test.go new file mode 100644 index 000000000..eb91e7f6d --- /dev/null +++ b/builtin/providers/dnsimple/resource_dnsimple_record_test.go @@ -0,0 +1,115 @@ +package dnsimple + +import ( + "fmt" + "strconv" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/rubyist/go-dnsimple" +) + +func TestAccDNSimpleRecord_Basic(t *testing.T) { + var record dnsimple.Record + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDNSimpleRecordDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckDNSimpleRecordConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckDNSimpleRecordExists("dnsimple_record.foobar", &record), + testAccCheckDNSimpleRecordAttributes(&record), + resource.TestCheckResourceAttr( + "dnsimple_record.foobar", "name", "terraform"), + resource.TestCheckResourceAttr( + "dnsimple_record.foobar", "domain", "jack.ly"), + resource.TestCheckResourceAttr( + "dnsimple_record.foobar", "value", "192.168.0.10"), + ), + }, + }, + }) +} + +func testAccCheckDNSimpleRecordDestroy(s *terraform.State) error { + client := testAccProvider.client + + for _, rs := range s.Resources { + if rs.Type != "dnsimple_record" { + continue + } + + intId, err := strconv.Atoi(rs.ID) + if err != nil { + return err + } + + _, err = client.RetrieveRecord(rs.Attributes["domain"], intId) + + if err == nil { + return fmt.Errorf("Record still exists") + } + } + + return nil +} + +func testAccCheckDNSimpleRecordAttributes(record *dnsimple.Record) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if record.Content != "192.168.0.10" { + return fmt.Errorf("Bad content: %s", record.Content) + } + + return nil + } +} + +func testAccCheckDNSimpleRecordExists(n string, record *dnsimple.Record) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.ID == "" { + return fmt.Errorf("No Record ID is set") + } + + client := testAccProvider.client + + intId, err := strconv.Atoi(rs.ID) + if err != nil { + return err + } + + foundRecord, err := client.RetrieveRecord(rs.Attributes["domain"], intId) + + if err != nil { + return err + } + + if strconv.Itoa(foundRecord.Id) != rs.ID { + return fmt.Errorf("Record not found") + } + + *record = foundRecord + + return nil + } +} + +const testAccCheckDNSimpleRecordConfig_basic = ` +resource "dnsimple_record" "foobar" { + domain = "jack.ly" + + name = "terraform" + value = "192.168.0.10" + type = "A" + ttl = 3600 +}` diff --git a/builtin/providers/dnsimple/resource_provider.go b/builtin/providers/dnsimple/resource_provider.go new file mode 100644 index 000000000..478acb470 --- /dev/null +++ b/builtin/providers/dnsimple/resource_provider.go @@ -0,0 +1,68 @@ +package dnsimple + +import ( + "log" + + "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/terraform" + "github.com/rubyist/go-dnsimple" +) + +type ResourceProvider struct { + Config Config + + client *dnsimple.DNSimpleClient +} + +func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) { + v := &config.Validator{ + Required: []string{ + "token", + "email", + }, + } + + return v.Validate(c) +} + +func (p *ResourceProvider) ValidateResource( + t string, c *terraform.ResourceConfig) ([]string, []error) { + return resourceMap.Validate(t, c) +} + +func (p *ResourceProvider) Configure(c *terraform.ResourceConfig) error { + if _, err := config.Decode(&p.Config, c.Config); err != nil { + return err + } + + log.Println("[INFO] Initializing DNSimple client") + var err error + p.client, err = p.Config.Client() + + if err != nil { + return err + } + + return nil +} + +func (p *ResourceProvider) Apply( + s *terraform.ResourceState, + d *terraform.ResourceDiff) (*terraform.ResourceState, error) { + return resourceMap.Apply(s, d, p) +} + +func (p *ResourceProvider) Diff( + s *terraform.ResourceState, + c *terraform.ResourceConfig) (*terraform.ResourceDiff, error) { + return resourceMap.Diff(s, c, p) +} + +func (p *ResourceProvider) Refresh( + s *terraform.ResourceState) (*terraform.ResourceState, error) { + return resourceMap.Refresh(s, p) +} + +func (p *ResourceProvider) Resources() []terraform.ResourceType { + return resourceMap.Resources() +} diff --git a/builtin/providers/dnsimple/resource_provider_test.go b/builtin/providers/dnsimple/resource_provider_test.go new file mode 100644 index 000000000..63dd0b067 --- /dev/null +++ b/builtin/providers/dnsimple/resource_provider_test.go @@ -0,0 +1,76 @@ +package dnsimple + +import ( + "os" + "reflect" + "testing" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *ResourceProvider + +func init() { + testAccProvider = new(ResourceProvider) + testAccProviders = map[string]terraform.ResourceProvider{ + "dnsimple": testAccProvider, + } +} + +func TestResourceProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = new(ResourceProvider) +} + +func TestResourceProvider_Configure(t *testing.T) { + rp := new(ResourceProvider) + var expectedToken string + var expectedEmail string + + if v := os.Getenv("DNSIMPLE_EMAIL"); v != "" { + expectedEmail = v + } else { + expectedEmail = "foo" + } + + if v := os.Getenv("DNSIMPLE_TOKEN"); v != "" { + expectedToken = v + } else { + expectedToken = "foo" + } + + raw := map[string]interface{}{ + "token": expectedToken, + "email": expectedEmail, + } + + rawConfig, err := config.NewRawConfig(raw) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = rp.Configure(terraform.NewResourceConfig(rawConfig)) + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := Config{ + Token: expectedToken, + Email: expectedEmail, + } + + if !reflect.DeepEqual(rp.Config, expected) { + t.Fatalf("bad: %#v", rp.Config) + } +} + +func testAccPreCheck(t *testing.T) { + if v := os.Getenv("DNSIMPLE_EMAIL"); v == "" { + t.Fatal("DNSIMPLE_EMAIL must be set for acceptance tests") + } + + if v := os.Getenv("DNSIMPLE_TOKEN"); v == "" { + t.Fatal("DNSIMPLE_TOKEN must be set for acceptance tests") + } +} diff --git a/builtin/providers/dnsimple/resources.go b/builtin/providers/dnsimple/resources.go new file mode 100644 index 000000000..7cbd5db91 --- /dev/null +++ b/builtin/providers/dnsimple/resources.go @@ -0,0 +1,24 @@ +package dnsimple + +import ( + "github.com/hashicorp/terraform/helper/resource" +) + +// resourceMap is the mapping of resources we support to their basic +// operations. This makes it easy to implement new resource types. +var resourceMap *resource.Map + +func init() { + resourceMap = &resource.Map{ + Mapping: map[string]resource.Resource{ + "dnsimple_record": resource.Resource{ + ConfigValidator: resource_dnsimple_record_validation(), + Create: resource_dnsimple_record_create, + Destroy: resource_dnsimple_record_destroy, + Diff: resource_dnsimple_record_diff, + Update: resource_dnsimple_record_update, + Refresh: resource_dnsimple_record_refresh, + }, + }, + } +} diff --git a/config.go b/config.go index 3cad21c11..29a30e66c 100644 --- a/config.go +++ b/config.go @@ -36,6 +36,7 @@ func init() { "aws": "terraform-provider-aws", "digitalocean": "terraform-provider-digitalocean", "heroku": "terraform-provider-heroku", + "dnsimple": "terraform-provider-dnsimple", } BuiltinConfig.Provisioners = map[string]string{ "local-exec": "terraform-provisioner-local-exec",