diff --git a/aws/internal/service/route53resolver/finder/finder.go b/aws/internal/service/route53resolver/finder/finder.go index ab246be9be3..2c056d947f6 100644 --- a/aws/internal/service/route53resolver/finder/finder.go +++ b/aws/internal/service/route53resolver/finder/finder.go @@ -42,3 +42,36 @@ func ResolverQueryLogConfigByID(conn *route53resolver.Route53Resolver, queryLogC return output.ResolverQueryLogConfig, nil } + +// ResolverDnssecConfigByID returns the dnssec configuration corresponding to the specified ID. +// Returns nil if no configuration is found. +func ResolverDnssecConfigByID(conn *route53resolver.Route53Resolver, dnssecConfigID string) (*route53resolver.ResolverDnssecConfig, error) { + input := &route53resolver.ListResolverDnssecConfigsInput{} + + var config *route53resolver.ResolverDnssecConfig + // GetResolverDnssecConfigs does not support query with id + err := conn.ListResolverDnssecConfigsPages(input, func(page *route53resolver.ListResolverDnssecConfigsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, c := range page.ResolverDnssecConfigs { + if aws.StringValue(c.Id) == dnssecConfigID { + config = c + return false + } + } + + return !lastPage + }) + + if err != nil { + return nil, err + } + + if config == nil { + return nil, nil + } + + return config, nil +} diff --git a/aws/internal/service/route53resolver/waiter/status.go b/aws/internal/service/route53resolver/waiter/status.go index 7c8c91dd5e1..2d902d562a7 100644 --- a/aws/internal/service/route53resolver/waiter/status.go +++ b/aws/internal/service/route53resolver/waiter/status.go @@ -14,6 +14,9 @@ const ( resolverQueryLogConfigStatusNotFound = "NotFound" resolverQueryLogConfigStatusUnknown = "Unknown" + + resolverDnssecConfigStatusNotFound = "NotFound" + resolverDnssecConfigStatusUnknown = "Unknown" ) // QueryLogConfigAssociationStatus fetches the QueryLogConfigAssociation and its Status @@ -57,3 +60,20 @@ func QueryLogConfigStatus(conn *route53resolver.Route53Resolver, queryLogConfigI return queryLogConfig, aws.StringValue(queryLogConfig.Status), nil } } + +// DnssecConfigStatus fetches the DnssecConfig and its Status +func DnssecConfigStatus(conn *route53resolver.Route53Resolver, dnssecConfigID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + dnssecConfig, err := finder.ResolverDnssecConfigByID(conn, dnssecConfigID) + + if err != nil { + return nil, resolverDnssecConfigStatusUnknown, err + } + + if dnssecConfig == nil { + return nil, resolverDnssecConfigStatusNotFound, nil + } + + return dnssecConfig, aws.StringValue(dnssecConfig.ValidationStatus), nil + } +} diff --git a/aws/internal/service/route53resolver/waiter/waiter.go b/aws/internal/service/route53resolver/waiter/waiter.go index 744ad94bac1..8bcfea75b9b 100644 --- a/aws/internal/service/route53resolver/waiter/waiter.go +++ b/aws/internal/service/route53resolver/waiter/waiter.go @@ -19,6 +19,12 @@ const ( // Maximum amount of time to wait for a QueryLogConfig to be deleted QueryLogConfigDeletedTimeout = 5 * time.Minute + + // Maximum amount of time to wait for a DnssecConfig to return ENABLED + DnssecConfigCreatedTimeout = 5 * time.Minute + + // Maximum amount of time to wait for a DnssecConfig to return DISABLED + DnssecConfigDeletedTimeout = 5 * time.Minute ) // QueryLogConfigAssociationCreated waits for a QueryLogConfig to return ACTIVE @@ -92,3 +98,39 @@ func QueryLogConfigDeleted(conn *route53resolver.Route53Resolver, queryLogConfig return nil, err } + +// DnssecConfigCreated waits for a DnssecConfig to return ENABLED +func DnssecConfigCreated(conn *route53resolver.Route53Resolver, dnssecConfigID string) (*route53resolver.ResolverDnssecConfig, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{route53resolver.ResolverDNSSECValidationStatusEnabling}, + Target: []string{route53resolver.ResolverDNSSECValidationStatusEnabled}, + Refresh: DnssecConfigStatus(conn, dnssecConfigID), + Timeout: DnssecConfigCreatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*route53resolver.ResolverDnssecConfig); ok { + return v, err + } + + return nil, err +} + +// DnssecConfigCreated waits for a DnssecConfig to return DELETED +func DnssecConfigDeleted(conn *route53resolver.Route53Resolver, dnssecConfigID string) (*route53resolver.ResolverDnssecConfig, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{route53resolver.ResolverDNSSECValidationStatusDisabling}, + Target: []string{route53resolver.ResolverDNSSECValidationStatusDisabled}, + Refresh: DnssecConfigStatus(conn, dnssecConfigID), + Timeout: DnssecConfigDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*route53resolver.ResolverDnssecConfig); ok { + return v, err + } + + return nil, err +} diff --git a/aws/provider.go b/aws/provider.go index 84de3c32fc4..b499c246cce 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -863,6 +863,7 @@ func Provider() *schema.Provider { "aws_route53_vpc_association_authorization": resourceAwsRoute53VPCAssociationAuthorization(), "aws_route53_zone": resourceAwsRoute53Zone(), "aws_route53_health_check": resourceAwsRoute53HealthCheck(), + "aws_route53_resolver_dnssec_config": resourceAwsRoute53ResolverDnssecConfig(), "aws_route53_resolver_endpoint": resourceAwsRoute53ResolverEndpoint(), "aws_route53_resolver_query_log_config": resourceAwsRoute53ResolverQueryLogConfig(), "aws_route53_resolver_query_log_config_association": resourceAwsRoute53ResolverQueryLogConfigAssociation(), diff --git a/aws/resource_aws_route53_resolver_dnssec_config.go b/aws/resource_aws_route53_resolver_dnssec_config.go new file mode 100644 index 00000000000..370f5d969a5 --- /dev/null +++ b/aws/resource_aws_route53_resolver_dnssec_config.go @@ -0,0 +1,131 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/route53resolver" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/route53resolver/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/route53resolver/waiter" +) + +func resourceAwsRoute53ResolverDnssecConfig() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsRoute53ResolverDnssecConfigCreate, + Read: resourceAwsRoute53ResolverDnssecConfigRead, + Delete: resourceAwsRoute53ResolverDnssecConfigDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + + "id": { + Type: schema.TypeString, + Computed: true, + }, + + "owner_id": { + Type: schema.TypeString, + Computed: true, + }, + + "resource_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "validation_status": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsRoute53ResolverDnssecConfigCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).route53resolverconn + + req := &route53resolver.UpdateResolverDnssecConfigInput{ + ResourceId: aws.String(d.Get("resource_id").(string)), + Validation: aws.String(route53resolver.ValidationEnable), + } + + log.Printf("[DEBUG] Creating Route53 Resolver DNSSEC config: %#v", req) + resp, err := conn.UpdateResolverDnssecConfig(req) + if err != nil { + return fmt.Errorf("error creating Route53 Resolver DNSSEC config: %w", err) + } + + d.SetId(aws.StringValue(resp.ResolverDNSSECConfig.Id)) + + _, err = waiter.DnssecConfigCreated(conn, d.Id()) + if err != nil { + return err + } + + return resourceAwsRoute53ResolverDnssecConfigRead(d, meta) +} + +func resourceAwsRoute53ResolverDnssecConfigRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).route53resolverconn + + config, err := finder.ResolverDnssecConfigByID(conn, d.Id()) + + if err != nil { + return fmt.Errorf("error getting Route53 Resolver DNSSEC config (%s): %w", d.Id(), err) + } + + if config == nil || aws.StringValue(config.ValidationStatus) == route53resolver.ResolverDNSSECValidationStatusDisabled { + log.Printf("[WARN] Route53 Resolver DNSSEC config (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + d.Set("id", config.Id) + d.Set("owner_id", config.OwnerId) + d.Set("resource_id", config.ResourceId) + d.Set("validation_status", config.ValidationStatus) + + configArn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Service: "route53resolver", + Region: meta.(*AWSClient).region, + AccountID: aws.StringValue(config.OwnerId), + Resource: fmt.Sprintf("resolver-dnssec-config/%s", aws.StringValue(config.ResourceId)), + }.String() + d.Set("arn", configArn) + + return nil +} + +func resourceAwsRoute53ResolverDnssecConfigDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).route53resolverconn + + log.Printf("[DEBUG] Deleting Route53 Resolver DNSSEC config: %s", d.Id()) + _, err := conn.UpdateResolverDnssecConfig(&route53resolver.UpdateResolverDnssecConfigInput{ + ResourceId: aws.String(d.Get("resource_id").(string)), + Validation: aws.String(route53resolver.ValidationDisable), + }) + if isAWSErr(err, route53resolver.ErrCodeResourceNotFoundException, "") { + return nil + } + if err != nil { + return fmt.Errorf("error deleting Route53 Resolver DNSSEC config (%s): %w", d.Id(), err) + } + + _, err = waiter.DnssecConfigDeleted(conn, d.Id()) + if err != nil { + return err + } + + return nil +} diff --git a/aws/resource_aws_route53_resolver_dnssec_config_test.go b/aws/resource_aws_route53_resolver_dnssec_config_test.go new file mode 100644 index 00000000000..ad864458467 --- /dev/null +++ b/aws/resource_aws_route53_resolver_dnssec_config_test.go @@ -0,0 +1,235 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/route53resolver" + "github.com/hashicorp/go-multierror" + "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" +) + +func init() { + resource.AddTestSweepers("aws_route53_resolver_dnssec_config", &resource.Sweeper{ + Name: "aws_route53_resolver_dnssec_config", + F: testSweepRoute53ResolverDnssecConfig, + }) +} + +func testSweepRoute53ResolverDnssecConfig(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + conn := client.(*AWSClient).route53resolverconn + + var errors error + err = conn.ListResolverDnssecConfigsPages(&route53resolver.ListResolverDnssecConfigsInput{}, func(page *route53resolver.ListResolverDnssecConfigsOutput, isLast bool) bool { + if page == nil { + return !isLast + } + + for _, resolverDnssecConfig := range page.ResolverDnssecConfigs { + id := aws.StringValue(resolverDnssecConfig.ResourceId) + + log.Printf("[INFO] Deleting Route53 Resolver Dnssec config: %s", id) + _, err := conn.UpdateResolverDnssecConfig(&route53resolver.UpdateResolverDnssecConfigInput{ + ResourceId: aws.String(id), + Validation: aws.String(route53resolver.ResolverDNSSECValidationStatusDisabled), + }) + if isAWSErr(err, route53resolver.ErrCodeResourceNotFoundException, "") { + continue + } + if err != nil { + errors = multierror.Append(errors, fmt.Errorf("error deleting Route53 Resolver Resolver Dnssec config (%s): %w", id, err)) + continue + } + + err = route53ResolverEndpointWaitUntilTargetState(conn, id, 10*time.Minute, + []string{route53resolver.ResolverDNSSECValidationStatusDisabling}, + []string{route53resolver.ResolverDNSSECValidationStatusDisabled}) + if err != nil { + errors = multierror.Append(errors, err) + continue + } + } + + return !isLast + }) + if err != nil { + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping Route53 Resolver Resolver Dnssec config sweep for %s: %s", region, err) + return nil + } + errors = multierror.Append(errors, fmt.Errorf("error retrieving Route53 Resolver Resolver Dnssec config: %w", err)) + } + + return errors +} + +func TestAccAWSRoute53ResolverDnssecConfig_basic(t *testing.T) { + var config route53resolver.ResolverDnssecConfig + resourceName := "aws_route53_resolver_dnssec_config.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheckSkipRoute53(t), + Providers: testAccProviders, + CheckDestroy: testAccCheckRoute53ResolverDnssecConfigDestroy, + Steps: []resource.TestStep{ + { + Config: testAccRoute53ResolverDnssecConfigConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoute53ResolverDnssecConfigExists(resourceName, &config), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "route53resolver", regexp.MustCompile(`resolver-dnssec-config/.+$`)), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "owner_id"), + resource.TestCheckResourceAttrSet(resourceName, "resource_id"), + resource.TestCheckResourceAttr(resourceName, "validation_status", "ENABLED"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSRoute53ResolverDnssecConfig_disappear(t *testing.T) { + var config route53resolver.ResolverDnssecConfig + resourceName := "aws_route53_resolver_dnssec_config.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheckSkipRoute53(t), + Providers: testAccProviders, + CheckDestroy: testAccCheckRoute53ResolverDnssecConfigDestroy, + Steps: []resource.TestStep{ + { + Config: testAccRoute53ResolverDnssecConfigConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoute53ResolverDnssecConfigExists(resourceName, &config), + testAccCheckResourceDisappears(testAccProvider, resourceAwsRoute53ResolverDnssecConfig(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckRoute53ResolverDnssecConfigDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).route53resolverconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_route53_resolver_dnssec_config" { + continue + } + + input := &route53resolver.ListResolverDnssecConfigsInput{} + + var config *route53resolver.ResolverDnssecConfig + err := conn.ListResolverDnssecConfigsPages(input, func(page *route53resolver.ListResolverDnssecConfigsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, c := range page.ResolverDnssecConfigs { + if aws.StringValue(c.Id) == rs.Primary.ID { + config = c + return false + } + } + + return !lastPage + }) + + if err != nil { + return err + } + + if config == nil || aws.StringValue(config.ValidationStatus) == route53resolver.ResolverDNSSECValidationStatusDisabled { + return nil + } + + return fmt.Errorf("Route 53 Resolver Dnssec config still exists: %s", rs.Primary.ID) + } + + return nil +} + +func testAccCheckRoute53ResolverDnssecConfigExists(n string, config *route53resolver.ResolverDnssecConfig) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Route 53 Resolver Dnssec config ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).route53resolverconn + input := &route53resolver.ListResolverDnssecConfigsInput{} + + err := conn.ListResolverDnssecConfigsPages(input, func(page *route53resolver.ListResolverDnssecConfigsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, c := range page.ResolverDnssecConfigs { + if aws.StringValue(c.Id) == rs.Primary.ID { + config = c + return false + } + } + + return !lastPage + }) + + if err != nil { + return err + } + + if config == nil { + return fmt.Errorf("No Route 53 Resolver Dnssec config found") + } + + if aws.StringValue(config.ValidationStatus) != route53resolver.ResolverDNSSECValidationStatusEnabled { + return fmt.Errorf("Route 53 Resolver Dnssec config (%s) is not enabled", aws.StringValue(config.Id)) + } + + return nil + } +} + +func testAccRoute53ResolverDnssecConfigBase(rName string) string { + return fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + enable_dns_support = true + enable_dns_hostnames = true + + tags = { + Name = %q + } +} +`, rName) +} + +func testAccRoute53ResolverDnssecConfigConfigBasic(rName string) string { + return testAccRoute53ResolverDnssecConfigBase(rName) + ` +resource "aws_route53_resolver_dnssec_config" "test" { + resource_id = aws_vpc.test.id +} +` +} diff --git a/website/docs/r/route53_resolver_dnssec_config.html.markdown b/website/docs/r/route53_resolver_dnssec_config.html.markdown new file mode 100644 index 00000000000..326efa867f0 --- /dev/null +++ b/website/docs/r/route53_resolver_dnssec_config.html.markdown @@ -0,0 +1,48 @@ +--- +subcategory: "Route53 Resolver" +layout: "aws" +page_title: "AWS: aws_route53_resolver_dnssec_config" +description: |- + Provides a Route 53 Resolver DNSSEC config resource. +--- + +# Resource: aws_route53_resolver_dnssec_config + +Provides a Route 53 Resolver DNSSEC config resource. + +## Example Usage + +```hcl +resource "aws_vpc" "example" { + cidr_block = "10.0.0.0/16" + enable_dns_support = true + enable_dns_hostnames = true +} + +resource "aws_route53_resolver_dnssec_config" "example" { + resource_id = aws_vpc.example.id +} +``` + +## Argument Reference + +The following argument is supported: + +* `resource_id` - (Required) The ID of the virtual private cloud (VPC) that you're updating the DNSSEC validation status for. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - The ARN for a configuration for DNSSEC validation. +* `id` - The ID for a configuration for DNSSEC validation. +* `owner_id` - The owner account ID of the virtual private cloud (VPC) for a configuration for DNSSEC validation. +* `validation_status` - The validation status for a DNSSEC configuration. The status can be one of the following: `ENABLING`, `ENABLED`, `DISABLING` and `DISABLED`. + +## Import + + Route 53 Resolver DNSSEC configs can be imported using the Route 53 Resolver DNSSEC config ID, e.g. + +``` +$ terraform import aws_route53_resolver_dnssec_config.example rdsc-be1866ecc1683e95 +```