diff --git a/builtin/providers/cloudflare/config.go b/builtin/providers/cloudflare/config.go new file mode 100644 index 000000000..2aa020b62 --- /dev/null +++ b/builtin/providers/cloudflare/config.go @@ -0,0 +1,27 @@ +package cloudflare + +import ( + "fmt" + "log" + + "github.com/pearkes/cloudflare" +) + +type Config struct { + Token string `mapstructure:"token"` + Email string `mapstructure:"email"` +} + +// Client() returns a new client for accessing cloudflare. +// +func (c *Config) Client() (*cloudflare.Client, error) { + client, err := cloudflare.NewClient(c.Email, c.Token) + + if err != nil { + return nil, fmt.Errorf("Error setting up client: %s", err) + } + + log.Printf("[INFO] CloudFlare Client configured for user: %s", client.Email) + + return client, nil +} diff --git a/builtin/providers/cloudflare/resource_cloudflare_record.go b/builtin/providers/cloudflare/resource_cloudflare_record.go new file mode 100644 index 000000000..6c6701633 --- /dev/null +++ b/builtin/providers/cloudflare/resource_cloudflare_record.go @@ -0,0 +1,183 @@ +package cloudflare + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/helper/diff" + "github.com/hashicorp/terraform/terraform" + "github.com/pearkes/cloudflare" +) + +func resource_cloudflare_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 := cloudflare.CreateRecord{ + Name: rs.Attributes["name"], + Priority: rs.Attributes["priority"], + Type: rs.Attributes["type"], + Content: rs.Attributes["value"], + Ttl: rs.Attributes["ttl"], + } + + 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 = rec.Id + log.Printf("[INFO] record ID: %s", rs.ID) + + record, err := resource_cloudflare_record_retrieve(rs.Attributes["domain"], rs.ID, client) + if err != nil { + return nil, fmt.Errorf("Couldn't find record: %s", err) + } + + return resource_cloudflare_record_update_state(rs, record) +} + +func resource_cloudflare_record_update( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + rs := s.MergeDiff(d) + + // Cloudflare requires we send all values + // for an update request, so we just + // merge out diff and send the current + // state of affairs to them + updateRecord := cloudflare.UpdateRecord{ + Name: rs.Attributes["name"], + Content: rs.Attributes["value"], + Type: rs.Attributes["type"], + Ttl: rs.Attributes["ttl"], + Priority: rs.Attributes["priority"], + } + + log.Printf("[DEBUG] record update configuration: %#v", updateRecord) + + err := client.UpdateRecord(rs.Attributes["domain"], rs.ID, &updateRecord) + if err != nil { + return rs, fmt.Errorf("Failed to update record: %s", err) + } + + record, err := resource_cloudflare_record_retrieve(rs.Attributes["domain"], rs.ID, client) + if err != nil { + return rs, fmt.Errorf("Couldn't find record: %s", err) + } + + return resource_cloudflare_record_update_state(rs, record) +} + +func resource_cloudflare_record_destroy( + s *terraform.ResourceState, + meta interface{}) error { + p := meta.(*ResourceProvider) + client := p.client + + log.Printf("[INFO] Deleting record: %s, %s", s.Attributes["domain"], s.ID) + + err := client.DestroyRecord(s.Attributes["domain"], s.ID) + + if err != nil { + return fmt.Errorf("Error deleting record: %s", err) + } + + return nil +} + +func resource_cloudflare_record_refresh( + s *terraform.ResourceState, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + + rec, err := resource_cloudflare_record_retrieve(s.Attributes["domain"], s.ID, client) + if err != nil { + return nil, err + } + + return resource_cloudflare_record_update_state(s, rec) +} + +func resource_cloudflare_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.AttrTypeUpdate, + "value": diff.AttrTypeUpdate, + "ttl": diff.AttrTypeUpdate, + "type": diff.AttrTypeUpdate, + "priority": diff.AttrTypeUpdate, + }, + + ComputedAttrs: []string{ + "priority", + "ttl", + "hostname", + "ttl", + }, + + ComputedAttrsUpdate: []string{}, + } + + return b.Diff(s, c) +} + +func resource_cloudflare_record_update_state( + s *terraform.ResourceState, + rec *cloudflare.Record) (*terraform.ResourceState, error) { + + s.Attributes["name"] = rec.Name + s.Attributes["value"] = rec.Value + s.Attributes["type"] = rec.Type + s.Attributes["ttl"] = rec.Ttl + s.Attributes["priority"] = rec.Priority + s.Attributes["hostname"] = rec.FullName + + return s, nil +} + +func resource_cloudflare_record_retrieve(domain string, id string, client *cloudflare.Client) (*cloudflare.Record, error) { + record, err := client.RetrieveRecord(domain, id) + if err != nil { + return nil, err + } + + return record, nil +} + +func resource_cloudflare_record_validation() *config.Validator { + return &config.Validator{ + Required: []string{ + "domain", + "name", + "value", + "type", + }, + Optional: []string{ + "ttl", + "priority", + }, + } +} diff --git a/builtin/providers/cloudflare/resource_cloudflare_record_test.go b/builtin/providers/cloudflare/resource_cloudflare_record_test.go new file mode 100644 index 000000000..40a537c3e --- /dev/null +++ b/builtin/providers/cloudflare/resource_cloudflare_record_test.go @@ -0,0 +1,166 @@ +package cloudflare + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/pearkes/cloudflare" +) + +func TestAccCLOudflareRecord_Basic(t *testing.T) { + var record cloudflare.Record + domain := os.Getenv("CLOUDFLARE_DOMAIN") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCLOudflareRecordDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: fmt.Sprintf(testAccCheckCLoudFlareRecordConfig_basic, domain), + Check: resource.ComposeTestCheckFunc( + testAccCheckCLOudflareRecordExists("cloudflare_record.foobar", &record), + testAccCheckCLOudflareRecordAttributes(&record), + resource.TestCheckResourceAttr( + "cloudflare_record.foobar", "name", "terraform"), + resource.TestCheckResourceAttr( + "cloudflare_record.foobar", "domain", domain), + resource.TestCheckResourceAttr( + "cloudflare_record.foobar", "value", "192.168.0.10"), + ), + }, + }, + }) +} + +func TestAccCLOudflareRecord_Updated(t *testing.T) { + var record cloudflare.Record + domain := os.Getenv("CLOUDFLARE_DOMAIN") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCLOudflareRecordDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: fmt.Sprintf(testAccCheckCLoudFlareRecordConfig_basic, domain), + Check: resource.ComposeTestCheckFunc( + testAccCheckCLOudflareRecordExists("cloudflare_record.foobar", &record), + testAccCheckCLOudflareRecordAttributes(&record), + resource.TestCheckResourceAttr( + "cloudflare_record.foobar", "name", "terraform"), + resource.TestCheckResourceAttr( + "cloudflare_record.foobar", "domain", domain), + resource.TestCheckResourceAttr( + "cloudflare_record.foobar", "value", "192.168.0.10"), + ), + }, + resource.TestStep{ + Config: fmt.Sprintf(testAccCheckCloudFlareRecordConfig_new_value, domain), + Check: resource.ComposeTestCheckFunc( + testAccCheckCLOudflareRecordExists("cloudflare_record.foobar", &record), + testAccCheckCLOudflareRecordAttributesUpdated(&record), + resource.TestCheckResourceAttr( + "cloudflare_record.foobar", "name", "terraform"), + resource.TestCheckResourceAttr( + "cloudflare_record.foobar", "domain", domain), + resource.TestCheckResourceAttr( + "cloudflare_record.foobar", "value", "192.168.0.11"), + ), + }, + }, + }) +} + +func testAccCheckCLOudflareRecordDestroy(s *terraform.State) error { + client := testAccProvider.client + + for _, rs := range s.Resources { + if rs.Type != "cloudflare_record" { + continue + } + + _, err := client.RetrieveRecord(rs.Attributes["domain"], rs.ID) + + if err == nil { + return fmt.Errorf("Record still exists") + } + } + + return nil +} + +func testAccCheckCLOudflareRecordAttributes(record *cloudflare.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 testAccCheckCLOudflareRecordAttributesUpdated(record *cloudflare.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 testAccCheckCLOudflareRecordExists(n string, record *cloudflare.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 + + foundRecord, err := client.RetrieveRecord(rs.Attributes["domain"], rs.ID) + + if err != nil { + return err + } + + if foundRecord.Id != rs.ID { + return fmt.Errorf("Record not found") + } + + *record = *foundRecord + + return nil + } +} + +const testAccCheckCLoudFlareRecordConfig_basic = ` +resource "cloudflare_record" "foobar" { + domain = "%s" + + name = "terraform" + value = "192.168.0.10" + type = "A" + ttl = 3600 +}` + +const testAccCheckCloudFlareRecordConfig_new_value = ` +resource "cloudflare_record" "foobar" { + domain = "%s" + + name = "terraform" + value = "192.168.0.11" + type = "A" + ttl = 3600 +}` diff --git a/builtin/providers/cloudflare/resource_provider.go b/builtin/providers/cloudflare/resource_provider.go new file mode 100644 index 000000000..808518254 --- /dev/null +++ b/builtin/providers/cloudflare/resource_provider.go @@ -0,0 +1,68 @@ +package cloudflare + +import ( + "log" + + "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/terraform" + "github.com/pearkes/cloudflare" +) + +type ResourceProvider struct { + Config Config + + client *cloudflare.Client +} + +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 CloudFlare 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/cloudflare/resource_provider_test.go b/builtin/providers/cloudflare/resource_provider_test.go new file mode 100644 index 000000000..ab2d7f995 --- /dev/null +++ b/builtin/providers/cloudflare/resource_provider_test.go @@ -0,0 +1,80 @@ +package cloudflare + +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{ + "cloudflare": 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("CLOUDFLARE_EMAIL"); v != "" { + expectedEmail = v + } else { + expectedEmail = "foo" + } + + if v := os.Getenv("CLOUDFLARE_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("CLOUDFLARE_EMAIL"); v == "" { + t.Fatal("CLOUDFLARE_EMAIL must be set for acceptance tests") + } + + if v := os.Getenv("CLOUDFLARE_TOKEN"); v == "" { + t.Fatal("CLOUDFLARE_TOKEN must be set for acceptance tests") + } + + if v := os.Getenv("CLOUDFLARE_DOMAIN"); v == "" { + t.Fatal("CLOUDFLARE_DOMAIN must be set for acceptance tests. The domain is used to ` and destroy record against.") + } +} diff --git a/builtin/providers/cloudflare/resources.go b/builtin/providers/cloudflare/resources.go new file mode 100644 index 000000000..3701f6273 --- /dev/null +++ b/builtin/providers/cloudflare/resources.go @@ -0,0 +1,24 @@ +package cloudflare + +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{ + "cloudflare_record": resource.Resource{ + ConfigValidator: resource_cloudflare_record_validation(), + Create: resource_cloudflare_record_create, + Destroy: resource_cloudflare_record_destroy, + Diff: resource_cloudflare_record_diff, + Update: resource_cloudflare_record_update, + Refresh: resource_cloudflare_record_refresh, + }, + }, + } +}