diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 1c8ec528f..908f726e2 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -125,8 +125,9 @@ func Provider() terraform.ResourceProvider { "aws_route53_record": resourceAwsRoute53Record(), "aws_route53_zone_association": resourceAwsRoute53ZoneAssociation(), "aws_route53_zone": resourceAwsRoute53Zone(), - "aws_route_table_association": resourceAwsRouteTableAssociation(), + "aws_route53_health_check": resourceAwsRoute53HealthCheck(), "aws_route_table": resourceAwsRouteTable(), + "aws_route_table_association": resourceAwsRouteTableAssociation(), "aws_s3_bucket": resourceAwsS3Bucket(), "aws_security_group": resourceAwsSecurityGroup(), "aws_security_group_rule": resourceAwsSecurityGroupRule(), diff --git a/builtin/providers/aws/resource_aws_route53_health_check.go b/builtin/providers/aws/resource_aws_route53_health_check.go new file mode 100644 index 000000000..cd48a30b7 --- /dev/null +++ b/builtin/providers/aws/resource_aws_route53_health_check.go @@ -0,0 +1,211 @@ +package aws + +import ( + "log" + "time" + + "github.com/hashicorp/terraform/helper/schema" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/route53" +) + +func resourceAwsRoute53HealthCheck() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsRoute53HealthCheckCreate, + Read: resourceAwsRoute53HealthCheckRead, + Update: resourceAwsRoute53HealthCheckUpdate, + Delete: resourceAwsRoute53HealthCheckDelete, + + Schema: map[string]*schema.Schema{ + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "failure_threshold": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + "request_interval": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: true, // todo this should be updateable but the awslabs route53 service doesnt have the ability + }, + "ip_address": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "fqdn": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "port": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + + "resource_path": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "search_string": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "tags": tagsSchema(), + }, + } +} + +func resourceAwsRoute53HealthCheckUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).r53conn + + updateHealthCheck := &route53.UpdateHealthCheckInput{ + HealthCheckID: aws.String(d.Id()), + } + + if d.HasChange("failure_threshold") { + updateHealthCheck.FailureThreshold = aws.Long(int64(d.Get("failure_threshold").(int))) + } + + if d.HasChange("fqdn") { + updateHealthCheck.FullyQualifiedDomainName = aws.String(d.Get("fqdn").(string)) + } + + if d.HasChange("port") { + updateHealthCheck.Port = aws.Long(int64(d.Get("port").(int))) + } + + if d.HasChange("resource_path") { + updateHealthCheck.ResourcePath = aws.String(d.Get("resource_path").(string)) + } + + if d.HasChange("search_string") { + updateHealthCheck.SearchString = aws.String(d.Get("search_string").(string)) + } + + _, err := conn.UpdateHealthCheck(updateHealthCheck) + if err != nil { + return err + } + + if err := setTagsR53(conn, d, "healthcheck"); err != nil { + return err + } + + return resourceAwsRoute53HealthCheckRead(d, meta) +} + +func resourceAwsRoute53HealthCheckCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).r53conn + + healthConfig := &route53.HealthCheckConfig{ + Type: aws.String(d.Get("type").(string)), + FailureThreshold: aws.Long(int64(d.Get("failure_threshold").(int))), + RequestInterval: aws.Long(int64(d.Get("request_interval").(int))), + } + + if v, ok := d.GetOk("fqdn"); ok { + healthConfig.FullyQualifiedDomainName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("search_string"); ok { + healthConfig.SearchString = aws.String(v.(string)) + } + + if v, ok := d.GetOk("ip_address"); ok { + healthConfig.IPAddress = aws.String(v.(string)) + } + + if v, ok := d.GetOk("port"); ok { + healthConfig.Port = aws.Long(int64(v.(int))) + } + + if v, ok := d.GetOk("resource_path"); ok { + healthConfig.ResourcePath = aws.String(v.(string)) + } + + input := &route53.CreateHealthCheckInput{ + CallerReference: aws.String(time.Now().Format(time.RFC3339Nano)), + HealthCheckConfig: healthConfig, + } + + resp, err := conn.CreateHealthCheck(input) + + if err != nil { + return err + } + + d.SetId(*resp.HealthCheck.ID) + + if err := setTagsR53(conn, d, "healthcheck"); err != nil { + return err + } + + return resourceAwsRoute53HealthCheckRead(d, meta) +} + +func resourceAwsRoute53HealthCheckRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).r53conn + + read, err := conn.GetHealthCheck(&route53.GetHealthCheckInput{HealthCheckID: aws.String(d.Id())}) + if err != nil { + if r53err, ok := err.(awserr.Error); ok && r53err.Code() == "NoSuchHealthCheck" { + d.SetId("") + return nil + + } + return err + } + + if read == nil { + return nil + } + + updated := read.HealthCheck.HealthCheckConfig + d.Set("type", updated.Type) + d.Set("failure_threshold", updated.FailureThreshold) + d.Set("request_interval", updated.RequestInterval) + d.Set("fqdn", updated.FullyQualifiedDomainName) + d.Set("search_string", updated.SearchString) + d.Set("ip_address", updated.IPAddress) + d.Set("port", updated.Port) + d.Set("resource_path", updated.ResourcePath) + + // read the tags + req := &route53.ListTagsForResourceInput{ + ResourceID: aws.String(d.Id()), + ResourceType: aws.String("healthcheck"), + } + + resp, err := conn.ListTagsForResource(req) + if err != nil { + return err + } + + var tags []*route53.Tag + if resp.ResourceTagSet != nil { + tags = resp.ResourceTagSet.Tags + } + + if err := d.Set("tags", tagsToMapR53(tags)); err != nil { + return err + } + + return nil +} + +func resourceAwsRoute53HealthCheckDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).r53conn + + log.Printf("[DEBUG] Deleteing Route53 health check: %s", d.Id()) + _, err := conn.DeleteHealthCheck(&route53.DeleteHealthCheckInput{HealthCheckID: aws.String(d.Id())}) + if err != nil { + return err + } + + return nil +} diff --git a/builtin/providers/aws/resource_aws_route53_health_check_test.go b/builtin/providers/aws/resource_aws_route53_health_check_test.go new file mode 100644 index 000000000..32286cc41 --- /dev/null +++ b/builtin/providers/aws/resource_aws_route53_health_check_test.go @@ -0,0 +1,162 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/aws/aws-sdk-go/service/route53" +) + +func TestAccRoute53HealthCheck_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckRoute53HealthCheckDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccRoute53HealthCheckConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckRoute53HealthCheckExists("aws_route53_health_check.foo"), + ), + }, + resource.TestStep{ + Config: testAccRoute53HealthCheckConfigUpdate, + Check: resource.ComposeTestCheckFunc( + testAccCheckRoute53HealthCheckExists("aws_route53_health_check.foo"), + resource.TestCheckResourceAttr( + "aws_route53_health_check.foo", "failure_threshold", "5"), + ), + }, + }, + }) +} + +func TestAccRoute53HealthCheck_IpConfig(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckRoute53HealthCheckDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccRoute53HealthCheckIpConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckRoute53HealthCheckExists("aws_route53_health_check.bar"), + ), + }, + }, + }) +} + +func testAccCheckRoute53HealthCheckDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).r53conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_route53_health_check" { + continue + } + + lopts := &route53.ListHealthChecksInput{} + resp, err := conn.ListHealthChecks(lopts) + if err != nil { + return err + } + if len(resp.HealthChecks) == 0 { + return nil + } + + for _, check := range resp.HealthChecks { + if *check.ID == rs.Primary.ID { + return fmt.Errorf("Record still exists: %#v", check) + } + + } + + } + return nil +} + +func testAccCheckRoute53HealthCheckExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).r53conn + + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + fmt.Print(rs.Primary.ID) + + if rs.Primary.ID == "" { + return fmt.Errorf("No health check ID is set") + } + + lopts := &route53.ListHealthChecksInput{} + resp, err := conn.ListHealthChecks(lopts) + if err != nil { + return err + } + if len(resp.HealthChecks) == 0 { + return fmt.Errorf("Health Check does not exist") + } + + for _, check := range resp.HealthChecks { + if *check.ID == rs.Primary.ID { + return nil + } + + } + return fmt.Errorf("Health Check does not exist") + } +} + +func testUpdateHappened(n string) resource.TestCheckFunc { + return nil +} + +const testAccRoute53HealthCheckConfig = ` +resource "aws_route53_health_check" "foo" { + fqdn = "dev.notexample.com" + port = 80 + type = "HTTP" + resource_path = "/" + failure_threshold = "2" + request_interval = "30" + + tags = { + Name = "tf-test-health-check" + } +} +` + +const testAccRoute53HealthCheckConfigUpdate = ` +resource "aws_route53_health_check" "foo" { + fqdn = "dev.notexample.com" + port = 80 + type = "HTTP" + resource_path = "/" + failure_threshold = "5" + request_interval = "30" + + tags = { + Name = "tf-test-health-check" + } +} +` + +const testAccRoute53HealthCheckIpConfig = ` +resource "aws_route53_health_check" "bar" { + ip_address = "1.2.3.4" + port = 80 + type = "HTTP" + resource_path = "/" + failure_threshold = "2" + request_interval = "30" + + tags = { + Name = "tf-test-health-check" + } +} +` diff --git a/builtin/providers/aws/resource_aws_route53_record.go b/builtin/providers/aws/resource_aws_route53_record.go index 7796163bf..480133ad1 100644 --- a/builtin/providers/aws/resource_aws_route53_record.go +++ b/builtin/providers/aws/resource_aws_route53_record.go @@ -89,6 +89,16 @@ func resourceAwsRoute53Record() *schema.Resource { Set: resourceAwsRoute53AliasRecordHash, }, + "failover": &schema.Schema{ // PRIMARY | SECONDARY + Type: schema.TypeString, + Optional: true, + }, + + "health_check_id": &schema.Schema{ // ID of health check + Type: schema.TypeString, + Optional: true, + }, + "records": &schema.Schema{ Type: schema.TypeSet, ConflictsWith: []string{"alias"}, @@ -232,10 +242,6 @@ func resourceAwsRoute53RecordRead(d *schema.ResourceData, meta interface{}) erro StartRecordType: aws.String(d.Get("type").(string)), } - if v, ok := d.GetOk("set_identifier"); ok { - lopts.StartRecordIdentifier = aws.String(v.(string)) - } - resp, err := conn.ListResourceRecordSets(lopts) if err != nil { return err @@ -252,7 +258,7 @@ func resourceAwsRoute53RecordRead(d *schema.ResourceData, meta interface{}) erro continue } - if lopts.StartRecordIdentifier != nil && *record.SetIdentifier != *lopts.StartRecordIdentifier { + if record.SetIdentifier != nil && *record.SetIdentifier != d.Get("set_identifier") { continue } @@ -262,9 +268,12 @@ func resourceAwsRoute53RecordRead(d *schema.ResourceData, meta interface{}) erro if err != nil { return fmt.Errorf("[DEBUG] Error setting records for: %s, error: %#v", en, err) } + d.Set("ttl", record.TTL) d.Set("weight", record.Weight) d.Set("set_identifier", record.SetIdentifier) + d.Set("failover", record.Failover) + d.Set("health_check_id", record.HealthCheckID) break } @@ -390,6 +399,14 @@ func resourceAwsRoute53RecordBuildSet(d *schema.ResourceData, zoneName string) ( } } + if v, ok := d.GetOk("failover"); ok { + rec.Failover = aws.String(v.(string)) + } + + if v, ok := d.GetOk("health_check_id"); ok { + rec.HealthCheckID = aws.String(v.(string)) + } + if v, ok := d.GetOk("weight"); ok { rec.Weight = aws.Long(int64(v.(int))) } diff --git a/builtin/providers/aws/resource_aws_route53_zone.go b/builtin/providers/aws/resource_aws_route53_zone.go index 8310ca91a..dca775a1a 100644 --- a/builtin/providers/aws/resource_aws_route53_zone.go +++ b/builtin/providers/aws/resource_aws_route53_zone.go @@ -183,7 +183,7 @@ func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error func resourceAwsRoute53ZoneUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).r53conn - if err := setTagsR53(conn, d); err != nil { + if err := setTagsR53(conn, d, "hostedzone"); err != nil { return err } else { d.SetPartial("tags") diff --git a/builtin/providers/aws/tags_route53.go b/builtin/providers/aws/tags_route53.go index 93847669e..864ce3350 100644 --- a/builtin/providers/aws/tags_route53.go +++ b/builtin/providers/aws/tags_route53.go @@ -10,7 +10,7 @@ import ( // setTags is a helper to set the tags for a resource. It expects the // tags field to be named "tags" -func setTagsR53(conn *route53.Route53, d *schema.ResourceData) error { +func setTagsR53(conn *route53.Route53, d *schema.ResourceData, resourceType string) error { if d.HasChange("tags") { oraw, nraw := d.GetChange("tags") o := oraw.(map[string]interface{}) @@ -25,7 +25,7 @@ func setTagsR53(conn *route53.Route53, d *schema.ResourceData) error { log.Printf("[DEBUG] Changing tags: \n\tadding: %#v\n\tremoving:%#v", create, remove) req := &route53.ChangeTagsForResourceInput{ ResourceID: aws.String(d.Id()), - ResourceType: aws.String("hostedzone"), + ResourceType: aws.String(resourceType), } if len(create) > 0 { diff --git a/website/source/docs/providers/aws/r/route53_health_check.html.markdown b/website/source/docs/providers/aws/r/route53_health_check.html.markdown new file mode 100644 index 000000000..e7af166d4 --- /dev/null +++ b/website/source/docs/providers/aws/r/route53_health_check.html.markdown @@ -0,0 +1,42 @@ +--- +layout: "aws" +page_title: "AWS: aws_route53_health_check" +sidebar_current: "docs-aws-resource-health-check" +description: |- + Provides a Route53 health check. +--- +# aws\_route53\_health\_check + +Provides a Route53 health check. + +## Example Usage + +``` +resource "aws_route53_health_check" "foo" { + fqdn = "foobar.terraform.com" + port = 80 + type = "HTTP" + resource_path = "/" + failure_threshold = "5" + request_interval = "30" + + tags = { + Name = "tf-test-health-check" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `fqdn` - (Optional) The fully qualified domain name of the endpoint to be checked. +* `ip_address` - (Optional) The IP address of the endpoint to be checked. +* `failure_threshold` - (Required) The number of consecutive health checks that an endpoint must pass or fai. +* `request_interval` - (Required) The number of seconds between the time that Amazon Route 53 gets a response from your endpoint and the time that it sends the next health-check request. +* `resource_path` - (Optional) The path that you want Amazon Route 53 to request when performing health checks. +* `search_string` - (Optional) String searched in respoonse body for check to considered healthy. +* `tags` - (Optional) A mapping of tags to assign to the health check. + +Exactly one of `fqdn` or `ip_address` must be specified. + diff --git a/website/source/docs/providers/aws/r/route53_record.html.markdown b/website/source/docs/providers/aws/r/route53_record.html.markdown index 500608853..e7455ba13 100644 --- a/website/source/docs/providers/aws/r/route53_record.html.markdown +++ b/website/source/docs/providers/aws/r/route53_record.html.markdown @@ -93,7 +93,9 @@ The following arguments are supported: * `records` - (Required for non-alias records) A string list of records. * `weight` - (Optional) The weight of weighted record (0-255). * `set_identifier` - (Optional) Unique identifier to differentiate weighted - record from one another. Required for each weighted record. +record from one another. Required for each weighted record. +* `failover` - (Optional) The routing behavior when associated health check fails. Must be PRIMARY or SECONDARY. +* `health_check_id` - (Optional) The health check the record should be associated with. * `alias` - (Optional) An alias block. Conflicts with `ttl` & `records`. Alias record documented below.