From b6503a78105c8ffa71dd04f512fc1a0bb60c04c2 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 22 Jul 2014 23:08:39 -0400 Subject: [PATCH] provider/aws: Adding route53 records support --- .../aws/resource_aws_route53_record.go | 208 ++++++++++++++++++ .../aws/resource_aws_route53_record_test.go | 102 +++++++++ .../aws/resource_aws_route53_zone.go | 4 +- builtin/providers/aws/resources.go | 9 + 4 files changed, 321 insertions(+), 2 deletions(-) create mode 100644 builtin/providers/aws/resource_aws_route53_record.go create mode 100644 builtin/providers/aws/resource_aws_route53_record_test.go diff --git a/builtin/providers/aws/resource_aws_route53_record.go b/builtin/providers/aws/resource_aws_route53_record.go new file mode 100644 index 000000000..d868ac0a3 --- /dev/null +++ b/builtin/providers/aws/resource_aws_route53_record.go @@ -0,0 +1,208 @@ +package aws + +import ( + "fmt" + "log" + "strconv" + "strings" + "time" + + "github.com/hashicorp/terraform/flatmap" + "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/helper/diff" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/goamz/route53" +) + +func resource_aws_r53_record_validation() *config.Validator { + return &config.Validator{ + Required: []string{ + "zone_id", + "name", + "type", + "ttl", + "records.*", + }, + } +} + +func resource_aws_r53_record_create( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + conn := p.route53 + + // Merge the diff into the state so that we have all the attributes + // properly. + rs := s.MergeDiff(d) + + // Get the record + rec, err := resource_aws_r53_build_record_set(rs) + if err != nil { + return rs, err + } + + // Create the new records + req := &route53.ChangeResourceRecordSetsRequest{ + Comment: "Managed by Terraform", + Changes: []route53.Change{ + route53.Change{ + Action: "UPSERT", + Record: *rec, + }, + }, + } + zone := rs.Attributes["zone_id"] + log.Printf("[DEBUG] Creating resource records for zone: %s, name: %s", + zone, rs.Attributes["name"]) + resp, err := conn.ChangeResourceRecordSets(zone, req) + if err != nil { + return rs, err + } + + // Generate an ID + rs.ID = fmt.Sprintf("%s_%s_%s", zone, rs.Attributes["name"], rs.Attributes["type"]) + rs.Dependencies = []terraform.ResourceDependency{ + terraform.ResourceDependency{ID: zone}, + } + + // Wait until we are done + wait := resource.StateChangeConf{ + Delay: 30 * time.Second, + Pending: []string{"PENDING"}, + Target: "INSYNC", + Timeout: 10 * time.Minute, + MinTimeout: 5 * time.Second, + Refresh: func() (result interface{}, state string, err error) { + return resource_aws_r53_wait(conn, resp.ChangeInfo.ID) + }, + } + _, err = wait.WaitForState() + if err != nil { + return rs, err + } + return rs, nil +} + +func resource_aws_r53_build_record_set(s *terraform.ResourceState) (*route53.ResourceRecordSet, error) { + // Parse the TTL + ttl, err := strconv.ParseInt(s.Attributes["ttl"], 10, 32) + if err != nil { + return nil, err + } + + // Expand the records + recRaw := flatmap.Expand(s.Attributes, "records") + var records []string + for _, raw := range recRaw.([]interface{}) { + records = append(records, raw.(string)) + } + + rec := &route53.ResourceRecordSet{ + Name: s.Attributes["name"], + Type: s.Attributes["type"], + TTL: int(ttl), + Records: records, + } + return rec, nil +} + +func resource_aws_r53_record_destroy( + s *terraform.ResourceState, + meta interface{}) error { + p := meta.(*ResourceProvider) + conn := p.route53 + + // Get the record + rec, err := resource_aws_r53_build_record_set(s) + if err != nil { + return err + } + + // Create the new records + req := &route53.ChangeResourceRecordSetsRequest{ + Comment: "Deleted by Terraform", + Changes: []route53.Change{ + route53.Change{ + Action: "DELETE", + Record: *rec, + }, + }, + } + zone := s.Attributes["zone_id"] + log.Printf("[DEBUG] Deleting resource records for zone: %s, name: %s", + zone, s.Attributes["name"]) + _, err = conn.ChangeResourceRecordSets(zone, req) + if err != nil { + return err + } + return nil +} + +func resource_aws_r53_record_refresh( + s *terraform.ResourceState, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + conn := p.route53 + + zone := s.Attributes["zone_id"] + lopts := &route53.ListOpts{ + Name: s.Attributes["name"], + Type: s.Attributes["type"], + } + resp, err := conn.ListResourceRecordSets(zone, lopts) + if err != nil { + return s, err + } + + // Scan for a matching record + found := false + for _, record := range resp.Records { + if route53.FQDN(record.Name) != route53.FQDN(lopts.Name) { + continue + } + if strings.ToUpper(record.Type) != strings.ToUpper(lopts.Type) { + continue + } + + found = true + resource_aws_r53_record_update_state(s, &record) + break + } + if !found { + s.ID = "" + } + return s, nil +} + +func resource_aws_r53_record_update_state( + s *terraform.ResourceState, + rec *route53.ResourceRecordSet) { + + flatRec := flatmap.Flatten(map[string]interface{}{ + "records": rec.Records, + }) + for k, v := range flatRec { + s.Attributes[k] = v + } + + s.Attributes["ttl"] = strconv.FormatInt(int64(rec.TTL), 10) +} + +func resource_aws_r53_record_diff( + s *terraform.ResourceState, + c *terraform.ResourceConfig, + meta interface{}) (*terraform.ResourceDiff, error) { + b := &diff.ResourceBuilder{ + Attrs: map[string]diff.AttrType{ + "zone_id": diff.AttrTypeCreate, + "name": diff.AttrTypeCreate, + "type": diff.AttrTypeCreate, + "ttl": diff.AttrTypeUpdate, + "records": diff.AttrTypeUpdate, + }, + } + return b.Diff(s, c) +} diff --git a/builtin/providers/aws/resource_aws_route53_record_test.go b/builtin/providers/aws/resource_aws_route53_record_test.go new file mode 100644 index 000000000..08c551453 --- /dev/null +++ b/builtin/providers/aws/resource_aws_route53_record_test.go @@ -0,0 +1,102 @@ +package aws + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/goamz/route53" +) + +func TestAccRoute53Record(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckRoute53RecordDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccRoute53RecordConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckRoute53RecordExists("aws_route53_record.default"), + ), + }, + }, + }) +} + +func testAccCheckRoute53RecordDestroy(s *terraform.State) error { + conn := testAccProvider.route53 + for _, rs := range s.Resources { + if rs.Type != "aws_route53_record" { + continue + } + + parts := strings.Split(rs.ID, "_") + zone := parts[0] + name := parts[1] + rType := parts[2] + + lopts := &route53.ListOpts{Name: name, Type: rType} + resp, err := conn.ListResourceRecordSets(zone, lopts) + if err != nil { + return err + } + if len(resp.Records) == 0 { + return nil + } + rec := resp.Records[0] + if route53.FQDN(rec.Name) == route53.FQDN(name) && rec.Type == rType { + return fmt.Errorf("Record still exists: %#v", rec) + } + } + return nil +} + +func testAccCheckRoute53RecordExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.route53 + rs, ok := s.Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.ID == "" { + return fmt.Errorf("No hosted zone ID is set") + } + + parts := strings.Split(rs.ID, "_") + zone := parts[0] + name := parts[1] + rType := parts[2] + + lopts := &route53.ListOpts{Name: name, Type: rType} + resp, err := conn.ListResourceRecordSets(zone, lopts) + if err != nil { + return err + } + if len(resp.Records) == 0 { + return fmt.Errorf("Record does not exist") + } + rec := resp.Records[0] + if route53.FQDN(rec.Name) == route53.FQDN(name) && rec.Type == rType { + return nil + } + return fmt.Errorf("Record does not exist: %#v", rec) + } +} + +const testAccRoute53RecordConfig = ` +resource "aws_route53_zone" "main" { + name = "hashicorp.com" +} + +resource "aws_route53_record" "default" { + zone_id = "${aws_route53_zone.main.zone_id}" + name = "www.hashicorp.com" + type = "A" + ttl = "30" + records = ["127.0.0.1"] +} +` diff --git a/builtin/providers/aws/resource_aws_route53_zone.go b/builtin/providers/aws/resource_aws_route53_zone.go index 49e6cec76..c4f7dff46 100644 --- a/builtin/providers/aws/resource_aws_route53_zone.go +++ b/builtin/providers/aws/resource_aws_route53_zone.go @@ -47,11 +47,11 @@ func resource_aws_r53_zone_create( // Wait until we are done initializing wait := resource.StateChangeConf{ - Delay: 15 * time.Second, + Delay: 30 * time.Second, Pending: []string{"PENDING"}, Target: "INSYNC", Timeout: 10 * time.Minute, - MinTimeout: 3 * time.Second, + MinTimeout: 5 * time.Second, Refresh: func() (result interface{}, state string, err error) { return resource_aws_r53_wait(r53, resp.ChangeInfo.ID) }, diff --git a/builtin/providers/aws/resources.go b/builtin/providers/aws/resources.go index 6e80f8392..687cd6479 100644 --- a/builtin/providers/aws/resources.go +++ b/builtin/providers/aws/resources.go @@ -120,6 +120,15 @@ func init() { Refresh: resource_aws_r53_zone_refresh, }, + "aws_route53_record": resource.Resource{ + ConfigValidator: resource_aws_r53_record_validation(), + Create: resource_aws_r53_record_create, + Destroy: resource_aws_r53_record_destroy, + Diff: resource_aws_r53_record_diff, + Refresh: resource_aws_r53_record_refresh, + Update: resource_aws_r53_record_create, + }, + "aws_s3_bucket": resource.Resource{ ConfigValidator: resource_aws_s3_bucket_validation(), Create: resource_aws_s3_bucket_create,