From 0c876dea92825ad8fb4057cfaa7b52e2c1931b69 Mon Sep 17 00:00:00 2001 From: Csaba Kollar <1803150+csabakollar@users.noreply.github.com> Date: Wed, 1 Dec 2021 20:17:55 +0000 Subject: [PATCH 1/4] New Resource: aws_shield_protection_health_check_association --- internal/provider/provider.go | 5 +- internal/service/shield/id.go | 25 +++ .../protection_health_check_association.go | 125 ++++++++++++ ...rotection_health_check_association_test.go | 192 ++++++++++++++++++ ...ion_health_check_association.html.markdown | 75 +++++++ 5 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 internal/service/shield/id.go create mode 100644 internal/service/shield/protection_health_check_association.go create mode 100644 internal/service/shield/protection_health_check_association_test.go create mode 100644 website/docs/r/shield_protection_health_check_association.html.markdown diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 7240ab1490b..9eeee7fbcd9 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1594,8 +1594,9 @@ func Provider() *schema.Provider { "aws_sfn_activity": sfn.ResourceActivity(), "aws_sfn_state_machine": sfn.ResourceStateMachine(), - "aws_shield_protection": shield.ResourceProtection(), - "aws_shield_protection_group": shield.ResourceProtectionGroup(), + "aws_shield_protection": shield.ResourceProtection(), + "aws_shield_protection_health_check_association": shield.ResourceProtectionHealthCheckAssociation(), + "aws_shield_protection_group": shield.ResourceProtectionGroup(), "aws_signer_signing_job": signer.ResourceSigningJob(), "aws_signer_signing_profile": signer.ResourceSigningProfile(), diff --git a/internal/service/shield/id.go b/internal/service/shield/id.go new file mode 100644 index 00000000000..7ee3e22eafe --- /dev/null +++ b/internal/service/shield/id.go @@ -0,0 +1,25 @@ +package shield + +import ( + "fmt" + "strings" +) + +const protectionHealthCheckAssociationResourceIDSeparator = "+" + +func ProtectionHealthCheckAssociationCreateResourceID(protectionId, healthCheckArn string) string { + parts := []string{protectionId, healthCheckArn} + id := strings.Join(parts, protectionHealthCheckAssociationResourceIDSeparator) + + return id +} + +func ProtectionHealthCheckAssociationParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, protectionHealthCheckAssociationResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected PROTECTIONID%[2]sHEALTHCHECKARN", id, protectionHealthCheckAssociationResourceIDSeparator) +} diff --git a/internal/service/shield/protection_health_check_association.go b/internal/service/shield/protection_health_check_association.go new file mode 100644 index 00000000000..1e1611b8845 --- /dev/null +++ b/internal/service/shield/protection_health_check_association.go @@ -0,0 +1,125 @@ +package shield + +import ( + "fmt" + "log" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/shield" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/conns" +) + +func ResourceProtectionHealthCheckAssociation() *schema.Resource { + return &schema.Resource{ + Create: ResourceProtectionHealthCheckAssociationCreate, + Read: ResourceProtectionHealthCheckAssociationRead, + Delete: ResourceProtectionHealthCheckAssociationDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "shield_protection_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "health_check_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func ResourceProtectionHealthCheckAssociationCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).ShieldConn + + protectionId := d.Get("shield_protection_id").(string) + healthCheckArn := d.Get("health_check_arn").(string) + id := ProtectionHealthCheckAssociationCreateResourceID(protectionId, healthCheckArn) + + input := &shield.AssociateHealthCheckInput{ + ProtectionId: aws.String(protectionId), + HealthCheckArn: aws.String(healthCheckArn), + } + + _, err := conn.AssociateHealthCheck(input) + if err != nil { + return fmt.Errorf("error associating Route53 Health Check (%s) with Shield Protected resource (%s): %s", d.Get("health_check_arn"), d.Get("shield_protection_id"), err) + } + d.SetId(id) + return ResourceProtectionHealthCheckAssociationRead(d, meta) +} + +func ResourceProtectionHealthCheckAssociationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).ShieldConn + + protectionId, healthCheckArn, err := ProtectionHealthCheckAssociationParseResourceID(d.Id()) + + if err != nil { + return fmt.Errorf("error parsing Shield Protection and Route53 Health Check Association ID: %w", err) + } + + input := &shield.DescribeProtectionInput{ + ProtectionId: aws.String(protectionId), + } + + resp, err := conn.DescribeProtection(input) + + if tfawserr.ErrMessageContains(err, shield.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] Shield Protection itself (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading Shield Protection Health Check Association (%s): %s", d.Id(), err) + } + + isHealthCheck := stringInSlice(strings.Split(healthCheckArn, "/")[1], aws.StringValueSlice(resp.Protection.HealthCheckIds)) + if !isHealthCheck { + log.Printf("[WARN] Shield Protection Health Check Association (%s) not found, removing from state", d.Id()) + d.SetId("") + } + + d.Set("health_check_arn", healthCheckArn) + d.Set("shield_protection_id", resp.Protection.Id) + + return nil +} + +func ResourceProtectionHealthCheckAssociationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).ShieldConn + + protectionId, healthCheckId, err := ProtectionHealthCheckAssociationParseResourceID(d.Id()) + + if err != nil { + return fmt.Errorf("error parsing Shield Protection and Route53 Health Check Association ID: %w", err) + } + + input := &shield.DisassociateHealthCheckInput{ + ProtectionId: aws.String(protectionId), + HealthCheckArn: aws.String(healthCheckId), + } + + _, err = conn.DisassociateHealthCheck(input) + + if err != nil { + return fmt.Errorf("error disassociating Route53 Health Check (%s) from Shield Protected resource (%s): %s", d.Get("health_check_arn"), d.Get("shield_protection_id"), err) + } + return nil +} + +func stringInSlice(expected string, list []string) bool { + for _, item := range list { + if item == expected { + return true + } + } + return false +} diff --git a/internal/service/shield/protection_health_check_association_test.go b/internal/service/shield/protection_health_check_association_test.go new file mode 100644 index 00000000000..a44012138f9 --- /dev/null +++ b/internal/service/shield/protection_health_check_association_test.go @@ -0,0 +1,192 @@ +package shield_test + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/shield" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfshield "github.com/hashicorp/terraform-provider-aws/internal/service/shield" +) + +func TestAccShieldProtectionHealthCheckAssociation_basic(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_shield_protection_health_check_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(shield.EndpointsID, t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, shield.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckAWSShieldProtectionHealthCheckAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccShieldProtectionaHealthCheckAssociationConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSShieldProtectionHealthCheckAssociationExists(resourceName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccShieldProtectionHealthCheckAssociation_disappears(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_shield_protection_health_check_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(shield.EndpointsID, t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, shield.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckAWSShieldProtectionHealthCheckAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccShieldProtectionaHealthCheckAssociationConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSShieldProtectionHealthCheckAssociationExists(resourceName), + acctest.CheckResourceDisappears(acctest.Provider, tfshield.ResourceProtectionHealthCheckAssociation(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckAWSShieldProtectionHealthCheckAssociationDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).ShieldConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_shield_protection_health_check_association" { + continue + } + + protectionId, _, err := tfshield.ProtectionHealthCheckAssociationParseResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + input := &shield.DescribeProtectionInput{ + ProtectionId: aws.String(protectionId), + } + + resp, err := conn.DescribeProtection(input) + + if tfawserr.ErrMessageContains(err, shield.ErrCodeResourceNotFoundException, "") { + continue + } + + if err != nil { + return err + } + + if resp != nil && resp.Protection != nil && len(aws.StringValueSlice(resp.Protection.HealthCheckIds)) == 0 { + return fmt.Errorf("The Shield protection HealthCheck with IDs %v still exists", aws.StringValueSlice(resp.Protection.HealthCheckIds)) + } + } + + return nil +} + +func testAccCheckAWSShieldProtectionHealthCheckAssociationExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Shield Protection and Route53 Health Check Association ID is set") + } + + protectionId, _, err := tfshield.ProtectionHealthCheckAssociationParseResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).ShieldConn + + input := &shield.DescribeProtectionInput{ + ProtectionId: aws.String(protectionId), + } + + resp, err := conn.DescribeProtection(input) + + if err != nil { + return err + } + + if resp == nil || resp.Protection == nil { + return fmt.Errorf("The Shield protection does not exist") + } + + if resp.Protection.HealthCheckIds == nil || len(aws.StringValueSlice(resp.Protection.HealthCheckIds)) != 1 { + return fmt.Errorf("The Shield protection HealthCheck does not exist") + } + + return nil + } +} + +func testAccShieldProtectionaHealthCheckAssociationConfig(rName string) string { + return fmt.Sprintf(` +data "aws_availability_zones" "available" { + state = "available" + + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } +} +data "aws_region" "current" {} +data "aws_caller_identity" "current" {} +data "aws_partition" "current" {} + +resource "aws_eip" "test" { + vpc = true + + tags = { + foo = "bar" + Name = %[1]q + } +} +resource "aws_shield_protection" "test" { + name = %[1]q + resource_arn = "arn:${data.aws_partition.current.partition}:ec2:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:eip-allocation/${aws_eip.test.id}" +} +resource "aws_route53_health_check" "test" { + fqdn = "example.com" + port = 80 + type = "HTTP" + resource_path = "/" + failure_threshold = "5" + request_interval = "30" + tags = { + Name = "tf-test-health-check" + } +} +resource "aws_shield_protection_health_check_association" "test" { + shield_protection_id = aws_shield_protection.test.id + health_check_arn = aws_route53_health_check.test.arn +} +`, rName) +} diff --git a/website/docs/r/shield_protection_health_check_association.html.markdown b/website/docs/r/shield_protection_health_check_association.html.markdown new file mode 100644 index 00000000000..6ce3ffcdb23 --- /dev/null +++ b/website/docs/r/shield_protection_health_check_association.html.markdown @@ -0,0 +1,75 @@ +--- +subcategory: "Shield" +layout: "aws" +page_title: "AWS: aws_shield_protection_health_check_association" +description: |- + Creates an association between a Route53 Health Check and a Shield Advanced protected resource. +--- + +# Resource: aws_shield_protection_health_check_association + +Creates an association between a Route53 Health Check and a Shield Advanced protected resource. +This association uses the health of your applications to improve responsiveness and accuracy in attack detection and mitigation. + +Blog post: [AWS Shield Advanced now supports Health Based Detection](https://aws.amazon.com/about-aws/whats-new/2020/02/aws-shield-advanced-now-supports-health-based-detection/) + +## Example Usage + +### Create an association between a protected EIP and a Route53 Health Check + +```terraform +data "aws_region" "current" {} +data "aws_caller_identity" "current" {} +data "aws_partition" "current" {} + +resource "aws_eip" "example" { + vpc = true + tags = { + Name = "example" + } +} + +resource "aws_shield_protection" "example" { + name = "example-protection" + resource_arn = "arn:${data.aws_partition.current.partition}:ec2:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:eip-allocation/${aws_eip.example.id}" +} + +resource "aws_route53_health_check" "example" { + ip_address = aws_eip.example.public_ip + port = 80 + type = "HTTP" + resource_path = "/ready" + failure_threshold = "3" + request_interval = "30" + + tags = { + Name = "tf-example-health-check" + } +} + +resource "aws_shield_protection_health_check_association" "example" { + health_check_arn = aws_route53_health_check.example.arn + shield_protection_id = aws_shield_protection.example.id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `health_check_arn` - (Required) The ARN (Amazon Resource Name) of the Route53 Health Check resource which will be associated to the protected resource. +* `shield_protection_id` - (Required) The ID of the protected resource. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The unique identifier (ID) for the Protection object that is created. + +## Import + +Shield protection health check association resources can be imported by specifying the `shield_protection_id` and `health_check_arn` e.g., + +``` +$ terraform import aws_shield_protection_health_check_association.example ff9592dc-22f3-4e88-afa1-7b29fde9669a+arn:aws:route53:::healthcheck/3742b175-edb9-46bc-9359-f53e3b794b1b +``` From 8c84e89dac2fd161dc54ef3ce361fe3fea4b037a Mon Sep 17 00:00:00 2001 From: Csaba Kollar <1803150+csabakollar@users.noreply.github.com> Date: Wed, 1 Dec 2021 20:40:17 +0000 Subject: [PATCH 2/4] fix: replacing tabs with spaces --- ...rotection_health_check_association_test.go | 24 +++++++++---------- ...ion_health_check_association.html.markdown | 12 +++++----- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/internal/service/shield/protection_health_check_association_test.go b/internal/service/shield/protection_health_check_association_test.go index a44012138f9..aec3b3014e2 100644 --- a/internal/service/shield/protection_health_check_association_test.go +++ b/internal/service/shield/protection_health_check_association_test.go @@ -150,28 +150,28 @@ func testAccCheckAWSShieldProtectionHealthCheckAssociationExists(resourceName st func testAccShieldProtectionaHealthCheckAssociationConfig(rName string) string { return fmt.Sprintf(` data "aws_availability_zones" "available" { - state = "available" + state = "available" - filter { - name = "opt-in-status" - values = ["opt-in-not-required"] - } + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } } data "aws_region" "current" {} data "aws_caller_identity" "current" {} data "aws_partition" "current" {} resource "aws_eip" "test" { - vpc = true + vpc = true - tags = { - foo = "bar" - Name = %[1]q - } + tags = { + foo = "bar" + Name = %[1]q + } } resource "aws_shield_protection" "test" { - name = %[1]q - resource_arn = "arn:${data.aws_partition.current.partition}:ec2:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:eip-allocation/${aws_eip.test.id}" + name = %[1]q + resource_arn = "arn:${data.aws_partition.current.partition}:ec2:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:eip-allocation/${aws_eip.test.id}" } resource "aws_route53_health_check" "test" { fqdn = "example.com" diff --git a/website/docs/r/shield_protection_health_check_association.html.markdown b/website/docs/r/shield_protection_health_check_association.html.markdown index 6ce3ffcdb23..69220b785f3 100644 --- a/website/docs/r/shield_protection_health_check_association.html.markdown +++ b/website/docs/r/shield_protection_health_check_association.html.markdown @@ -23,15 +23,15 @@ data "aws_caller_identity" "current" {} data "aws_partition" "current" {} resource "aws_eip" "example" { - vpc = true - tags = { - Name = "example" - } + vpc = true + tags = { + Name = "example" + } } resource "aws_shield_protection" "example" { - name = "example-protection" - resource_arn = "arn:${data.aws_partition.current.partition}:ec2:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:eip-allocation/${aws_eip.example.id}" + name = "example-protection" + resource_arn = "arn:${data.aws_partition.current.partition}:ec2:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:eip-allocation/${aws_eip.example.id}" } resource "aws_route53_health_check" "example" { From b10bc048fc0e425a1f1e82c641e585bc1fb4318c Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Wed, 12 Jan 2022 09:10:26 -0500 Subject: [PATCH 3/4] Add CHANGELOG entry. --- .changelog/21993.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/21993.txt diff --git a/.changelog/21993.txt b/.changelog/21993.txt new file mode 100644 index 00000000000..a40f6297cea --- /dev/null +++ b/.changelog/21993.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +resource/aws_shield_protection_health_check_association +``` \ No newline at end of file From 5e381d59c1321d7c0ebf2aeaf7ac026a41b37108 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Wed, 12 Jan 2022 09:28:22 -0500 Subject: [PATCH 4/4] Alphabetic order for resource names. --- internal/provider/provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 9eeee7fbcd9..9bd2dd5d319 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1595,8 +1595,8 @@ func Provider() *schema.Provider { "aws_sfn_state_machine": sfn.ResourceStateMachine(), "aws_shield_protection": shield.ResourceProtection(), - "aws_shield_protection_health_check_association": shield.ResourceProtectionHealthCheckAssociation(), "aws_shield_protection_group": shield.ResourceProtectionGroup(), + "aws_shield_protection_health_check_association": shield.ResourceProtectionHealthCheckAssociation(), "aws_signer_signing_job": signer.ResourceSigningJob(), "aws_signer_signing_profile": signer.ResourceSigningProfile(),