From 9bbfb1d319481925ff6c2b2a428c2e32a81b7282 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 18 Dec 2020 11:29:20 -0500 Subject: [PATCH] New Resource: aws_route53_key_signing_key Reference: https://github.com/hashicorp/terraform-provider-aws/pull/16834 Reference: https://github.com/hashicorp/terraform-provider-aws/issues/16836 Output from acceptance testing in AWS Commercial: ``` --- PASS: TestAccAwsRoute53KeySigningKey_disappears (209.62s) --- PASS: TestAccAwsRoute53KeySigningKey_basic (233.38s) --- PASS: TestAccAwsRoute53KeySigningKey_Status (295.66s) ``` --- aws/internal/service/route53/enum.go | 9 + aws/internal/service/route53/finder/finder.go | 50 +++ aws/internal/service/route53/id.go | 25 ++ aws/internal/service/route53/waiter/status.go | 44 +++ aws/internal/service/route53/waiter/waiter.go | 67 ++++ aws/provider.go | 1 + aws/resource_aws_route53_key_signing_key.go | 300 ++++++++++++++++++ ...source_aws_route53_key_signing_key_test.go | 243 ++++++++++++++ aws/route53_key_signing_key_test.go | 87 +++++ .../r/route53_key_signing_key.html.markdown | 97 ++++++ 10 files changed, 923 insertions(+) create mode 100644 aws/internal/service/route53/enum.go create mode 100644 aws/internal/service/route53/finder/finder.go create mode 100644 aws/internal/service/route53/id.go create mode 100644 aws/internal/service/route53/waiter/status.go create mode 100644 aws/internal/service/route53/waiter/waiter.go create mode 100644 aws/resource_aws_route53_key_signing_key.go create mode 100644 aws/resource_aws_route53_key_signing_key_test.go create mode 100644 aws/route53_key_signing_key_test.go create mode 100644 website/docs/r/route53_key_signing_key.html.markdown diff --git a/aws/internal/service/route53/enum.go b/aws/internal/service/route53/enum.go new file mode 100644 index 00000000000..d10f69e9189 --- /dev/null +++ b/aws/internal/service/route53/enum.go @@ -0,0 +1,9 @@ +package route53 + +const ( + KeySigningKeyStatusActionNeeded = "ACTION_NEEDED" + KeySigningKeyStatusActive = "ACTIVE" + KeySigningKeyStatusDeleting = "DELETING" + KeySigningKeyStatusInactive = "INACTIVE" + KeySigningKeyStatusInternalFailure = "INTERNAL_FAILURE" +) diff --git a/aws/internal/service/route53/finder/finder.go b/aws/internal/service/route53/finder/finder.go new file mode 100644 index 00000000000..de40116d4e8 --- /dev/null +++ b/aws/internal/service/route53/finder/finder.go @@ -0,0 +1,50 @@ +package finder + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/route53" + tfroute53 "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/route53" +) + +func KeySigningKey(conn *route53.Route53, hostedZoneID string, name string) (*route53.KeySigningKey, error) { + input := &route53.GetDNSSECInput{ + HostedZoneId: aws.String(hostedZoneID), + } + + var result *route53.KeySigningKey + + output, err := conn.GetDNSSEC(input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + for _, keySigningKey := range output.KeySigningKeys { + if keySigningKey == nil { + continue + } + + if aws.StringValue(keySigningKey.Name) == name { + result = keySigningKey + break + } + } + + return result, err +} + +func KeySigningKeyByResourceID(conn *route53.Route53, resourceID string) (*route53.KeySigningKey, error) { + hostedZoneID, name, err := tfroute53.KeySigningKeyParseResourceID(resourceID) + + if err != nil { + return nil, fmt.Errorf("error parsing Route 53 Key Signing Key (%s) identifier: %w", resourceID, err) + } + + return KeySigningKey(conn, hostedZoneID, name) +} diff --git a/aws/internal/service/route53/id.go b/aws/internal/service/route53/id.go new file mode 100644 index 00000000000..a8748fa4eef --- /dev/null +++ b/aws/internal/service/route53/id.go @@ -0,0 +1,25 @@ +package route53 + +import ( + "fmt" + "strings" +) + +const KeySigningKeyResourceIDSeparator = "," + +func KeySigningKeyCreateResourceID(transitGatewayRouteTableID string, prefixListID string) string { + parts := []string{transitGatewayRouteTableID, prefixListID} + id := strings.Join(parts, KeySigningKeyResourceIDSeparator) + + return id +} + +func KeySigningKeyParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, KeySigningKeyResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected hosted-zone-id%[2]sname", id, KeySigningKeyResourceIDSeparator) +} diff --git a/aws/internal/service/route53/waiter/status.go b/aws/internal/service/route53/waiter/status.go new file mode 100644 index 00000000000..eb520ef2d1e --- /dev/null +++ b/aws/internal/service/route53/waiter/status.go @@ -0,0 +1,44 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/route53" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/route53/finder" +) + +func ChangeInfoStatus(conn *route53.Route53, changeID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &route53.GetChangeInput{ + Id: aws.String(changeID), + } + + output, err := conn.GetChange(input) + + if err != nil { + return nil, "", err + } + + if output == nil || output.ChangeInfo == nil { + return nil, "", nil + } + + return output.ChangeInfo, aws.StringValue(output.ChangeInfo.Status), nil + } +} + +func KeySigningKeyStatus(conn *route53.Route53, hostedZoneID string, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + keySigningKey, err := finder.KeySigningKey(conn, hostedZoneID, name) + + if err != nil { + return nil, "", err + } + + if keySigningKey == nil { + return nil, "", nil + } + + return keySigningKey, aws.StringValue(keySigningKey.Status), nil + } +} diff --git a/aws/internal/service/route53/waiter/waiter.go b/aws/internal/service/route53/waiter/waiter.go new file mode 100644 index 00000000000..0c9647f9ca3 --- /dev/null +++ b/aws/internal/service/route53/waiter/waiter.go @@ -0,0 +1,67 @@ +package waiter + +import ( + "fmt" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/route53" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + ChangeTimeout = 30 * time.Minute + + KeySigningKeyStatusTimeout = 5 * time.Minute +) + +func ChangeInfoStatusInsync(conn *route53.Route53, changeID string) (*route53.ChangeInfo, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{route53.ChangeStatusPending}, + Target: []string{route53.ChangeStatusInsync}, + Refresh: ChangeInfoStatus(conn, changeID), + Delay: 30 * time.Second, + MinTimeout: 5 * time.Second, + Timeout: ChangeTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*route53.ChangeInfo); ok { + return output, err + } + + return nil, err +} + +func KeySigningKeyStatusUpdated(conn *route53.Route53, hostedZoneID string, name string, status string) (*route53.KeySigningKey, error) { + stateConf := &resource.StateChangeConf{ + Target: []string{status}, + Refresh: KeySigningKeyStatus(conn, hostedZoneID, name), + MinTimeout: 5 * time.Second, + Timeout: KeySigningKeyStatusTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*route53.KeySigningKey); ok { + if err != nil && output != nil && output.Status != nil && output.StatusMessage != nil { + newErr := fmt.Errorf("%s: %s", aws.StringValue(output.Status), aws.StringValue(output.StatusMessage)) + + switch e := err.(type) { + case *resource.TimeoutError: + if e.LastError == nil { + e.LastError = newErr + } + case *resource.UnexpectedStateError: + if e.LastError == nil { + e.LastError = newErr + } + } + } + + return output, err + } + + return nil, err +} diff --git a/aws/provider.go b/aws/provider.go index 2d2fe3b412b..9308317e289 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -852,6 +852,7 @@ func Provider() *schema.Provider { "aws_redshift_event_subscription": resourceAwsRedshiftEventSubscription(), "aws_resourcegroups_group": resourceAwsResourceGroupsGroup(), "aws_route53_delegation_set": resourceAwsRoute53DelegationSet(), + "aws_route53_key_signing_key": resourceAwsRoute53KeySigningKey(), "aws_route53_query_log": resourceAwsRoute53QueryLog(), "aws_route53_record": resourceAwsRoute53Record(), "aws_route53_zone_association": resourceAwsRoute53ZoneAssociation(), diff --git a/aws/resource_aws_route53_key_signing_key.go b/aws/resource_aws_route53_key_signing_key.go new file mode 100644 index 00000000000..46f93d6fc07 --- /dev/null +++ b/aws/resource_aws_route53_key_signing_key.go @@ -0,0 +1,300 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/route53" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + tfroute53 "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/route53" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/route53/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/route53/waiter" +) + +func resourceAwsRoute53KeySigningKey() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsRoute53KeySigningKeyCreate, + Read: resourceAwsRoute53KeySigningKeyRead, + Update: resourceAwsRoute53KeySigningKeyUpdate, + Delete: resourceAwsRoute53KeySigningKeyDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "digest_algorithm_mnemonic": { + Type: schema.TypeString, + Computed: true, + }, + "digest_algorithm_type": { + Type: schema.TypeInt, + Computed: true, + }, + "digest_value": { + Type: schema.TypeString, + Computed: true, + }, + "dnskey_record": { + Type: schema.TypeString, + Computed: true, + }, + "ds_record": { + Type: schema.TypeString, + Computed: true, + }, + "flag": { + Type: schema.TypeInt, + Computed: true, + }, + "hosted_zone_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "key_management_service_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + "key_tag": { + Type: schema.TypeInt, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(3, 128), + validation.StringMatch(regexp.MustCompile("^[a-zA-Z0-9._-]"), "must contain only alphanumeric characters, periods, underscores, or hyphens"), + ), + }, + "public_key": { + Type: schema.TypeString, + Computed: true, + }, + "signing_algorithm_mnemonic": { + Type: schema.TypeString, + Computed: true, + }, + "signing_algorithm_type": { + Type: schema.TypeInt, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Optional: true, + Default: tfroute53.KeySigningKeyStatusActive, + ValidateFunc: validation.StringInSlice([]string{ + tfroute53.KeySigningKeyStatusActive, + tfroute53.KeySigningKeyStatusInactive, + }, false), + }, + }, + } +} + +func resourceAwsRoute53KeySigningKeyCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).r53conn + + hostedZoneID := d.Get("hosted_zone_id").(string) + name := d.Get("name").(string) + status := d.Get("status").(string) + + input := &route53.CreateKeySigningKeyInput{ + CallerReference: aws.String(resource.UniqueId()), + HostedZoneId: aws.String(hostedZoneID), + Name: aws.String(name), + Status: aws.String(status), + } + + if v, ok := d.GetOk("key_management_service_arn"); ok { + input.KeyManagementServiceArn = aws.String(v.(string)) + } + + output, err := conn.CreateKeySigningKey(input) + + if err != nil { + return fmt.Errorf("error creating Route 53 Key Signing Key: %w", err) + } + + d.SetId(tfroute53.KeySigningKeyCreateResourceID(hostedZoneID, name)) + + if output != nil && output.ChangeInfo != nil { + if _, err := waiter.ChangeInfoStatusInsync(conn, aws.StringValue(output.ChangeInfo.Id)); err != nil { + return fmt.Errorf("error waiting for Route 53 Key Signing Key (%s) creation: %w", d.Id(), err) + } + } + + if _, err := waiter.KeySigningKeyStatusUpdated(conn, hostedZoneID, name, status); err != nil { + return fmt.Errorf("error waiting for Route 53 Key Signing Key (%s) status (%s): %w", d.Id(), status, err) + } + + return resourceAwsRoute53KeySigningKeyRead(d, meta) +} + +func resourceAwsRoute53KeySigningKeyRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).r53conn + + hostedZoneID, name, err := tfroute53.KeySigningKeyParseResourceID(d.Id()) + + if err != nil { + return fmt.Errorf("error parsing Route 53 Key Signing Key (%s) identifier: %w", d.Id(), err) + } + + keySigningKey, err := finder.KeySigningKey(conn, hostedZoneID, name) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, route53.ErrCodeNoSuchHostedZone) { + log.Printf("[WARN] Route 53 Key Signing Key (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, route53.ErrCodeNoSuchKeySigningKey) { + log.Printf("[WARN] Route 53 Key Signing Key (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading Route 53 Key Signing Key (%s): %w", d.Id(), err) + } + + if keySigningKey == nil { + if d.IsNewResource() { + return fmt.Errorf("error reading Route 53 Key Signing Key (%s): not found", d.Id()) + } + + log.Printf("[WARN] Route 53 Key Signing Key (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + d.Set("digest_algorithm_mnemonic", keySigningKey.DigestAlgorithmMnemonic) + d.Set("digest_algorithm_type", keySigningKey.DigestAlgorithmType) + d.Set("digest_value", keySigningKey.DigestValue) + d.Set("dnskey_record", keySigningKey.DNSKEYRecord) + d.Set("ds_record", keySigningKey.DSRecord) + d.Set("flag", keySigningKey.Flag) + d.Set("hosted_zone_id", hostedZoneID) + d.Set("key_management_service_arn", keySigningKey.KmsArn) + d.Set("key_tag", keySigningKey.KeyTag) + d.Set("name", keySigningKey.Name) + d.Set("public_key", keySigningKey.PublicKey) + d.Set("signing_algorithm_mnemonic", keySigningKey.SigningAlgorithmMnemonic) + d.Set("signing_algorithm_type", keySigningKey.SigningAlgorithmType) + d.Set("status", keySigningKey.Status) + + return nil +} + +func resourceAwsRoute53KeySigningKeyUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).r53conn + + if d.HasChange("status") { + status := d.Get("status").(string) + + switch status { + default: + return fmt.Errorf("error updating Route 53 Key Signing Key (%s) status: unknown status (%s)", d.Id(), status) + case tfroute53.KeySigningKeyStatusActive: + input := &route53.ActivateKeySigningKeyInput{ + HostedZoneId: aws.String(d.Get("hosted_zone_id").(string)), + Name: aws.String(d.Get("name").(string)), + } + + output, err := conn.ActivateKeySigningKey(input) + + if err != nil { + return fmt.Errorf("error updating Route 53 Key Signing Key (%s) status (%s): %w", d.Id(), status, err) + } + + if output != nil && output.ChangeInfo != nil { + if _, err := waiter.ChangeInfoStatusInsync(conn, aws.StringValue(output.ChangeInfo.Id)); err != nil { + return fmt.Errorf("error waiting for Route 53 Key Signing Key (%s) status (%s) update: %w", d.Id(), status, err) + } + } + case tfroute53.KeySigningKeyStatusInactive: + input := &route53.DeactivateKeySigningKeyInput{ + HostedZoneId: aws.String(d.Get("hosted_zone_id").(string)), + Name: aws.String(d.Get("name").(string)), + } + + output, err := conn.DeactivateKeySigningKey(input) + + if err != nil { + return fmt.Errorf("error updating Route 53 Key Signing Key (%s) status (%s): %w", d.Id(), status, err) + } + + if output != nil && output.ChangeInfo != nil { + if _, err := waiter.ChangeInfoStatusInsync(conn, aws.StringValue(output.ChangeInfo.Id)); err != nil { + return fmt.Errorf("error waiting for Route 53 Key Signing Key (%s) status (%s) update: %w", d.Id(), status, err) + } + } + } + + if _, err := waiter.KeySigningKeyStatusUpdated(conn, d.Get("hosted_zone_id").(string), d.Get("name").(string), status); err != nil { + return fmt.Errorf("error waiting for Route 53 Key Signing Key (%s) status (%s): %w", d.Id(), status, err) + } + } + + return resourceAwsRoute53KeySigningKeyRead(d, meta) +} + +func resourceAwsRoute53KeySigningKeyDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).r53conn + + status := d.Get("status").(string) + + if status == tfroute53.KeySigningKeyStatusActive { + input := &route53.DeactivateKeySigningKeyInput{ + HostedZoneId: aws.String(d.Get("hosted_zone_id").(string)), + Name: aws.String(d.Get("name").(string)), + } + + output, err := conn.DeactivateKeySigningKey(input) + + if err != nil { + return fmt.Errorf("error updating Route 53 Key Signing Key (%s) status (%s): %w", d.Id(), status, err) + } + + if output != nil && output.ChangeInfo != nil { + if _, err := waiter.ChangeInfoStatusInsync(conn, aws.StringValue(output.ChangeInfo.Id)); err != nil { + return fmt.Errorf("error waiting for Route 53 Key Signing Key (%s) status (%s) update: %w", d.Id(), status, err) + } + } + } + + input := &route53.DeleteKeySigningKeyInput{ + HostedZoneId: aws.String(d.Get("hosted_zone_id").(string)), + Name: aws.String(d.Get("name").(string)), + } + + output, err := conn.DeleteKeySigningKey(input) + + if tfawserr.ErrCodeEquals(err, route53.ErrCodeNoSuchHostedZone) { + return nil + } + + if tfawserr.ErrCodeEquals(err, route53.ErrCodeNoSuchKeySigningKey) { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting Route 53 Key Signing Key (%s): %w", d.Id(), err) + } + + if output != nil && output.ChangeInfo != nil { + if _, err := waiter.ChangeInfoStatusInsync(conn, aws.StringValue(output.ChangeInfo.Id)); err != nil { + return fmt.Errorf("error waiting for Route 53 Key Signing Key (%s) deletion: %w", d.Id(), err) + } + } + + return nil +} diff --git a/aws/resource_aws_route53_key_signing_key_test.go b/aws/resource_aws_route53_key_signing_key_test.go new file mode 100644 index 00000000000..fdfc60b2ab1 --- /dev/null +++ b/aws/resource_aws_route53_key_signing_key_test.go @@ -0,0 +1,243 @@ +package aws + +import ( + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/service/route53" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "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" + tfroute53 "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/route53" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/route53/finder" +) + +func TestAccAwsRoute53KeySigningKey_basic(t *testing.T) { + kmsKeyResourceName := "aws_kms_key.test" + route53ZoneResourceName := "aws_route53_zone.test" + resourceName := "aws_route53_key_signing_key.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckRoute53KeySigningKey(t) }, + ErrorCheck: testAccErrorCheckSkipRoute53(t), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsRoute53KeySigningKeyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsRoute53KeySigningKeyConfig_Name(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsRoute53KeySigningKeyExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "digest_algorithm_mnemonic", "SHA-256"), + resource.TestCheckResourceAttr(resourceName, "digest_algorithm_type", "2"), + resource.TestMatchResourceAttr(resourceName, "digest_value", regexp.MustCompile(`^[0-9A-F]+$`)), + resource.TestMatchResourceAttr(resourceName, "dnskey_record", regexp.MustCompile(`^257 [0-9]+ [0-9]+ [a-zA-Z0-9+/]+={0,3}$`)), + resource.TestMatchResourceAttr(resourceName, "ds_record", regexp.MustCompile(`^[0-9]+ [0-9]+ [0-9]+ [0-9A-F]+$`)), + resource.TestCheckResourceAttr(resourceName, "flag", "257"), + resource.TestCheckResourceAttrPair(resourceName, "hosted_zone_id", route53ZoneResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "key_management_service_arn", kmsKeyResourceName, "arn"), + resource.TestMatchResourceAttr(resourceName, "key_tag", regexp.MustCompile(`^[0-9]+$`)), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestMatchResourceAttr(resourceName, "public_key", regexp.MustCompile(`^[a-zA-Z0-9+/]+={0,3}$`)), + resource.TestCheckResourceAttr(resourceName, "signing_algorithm_mnemonic", "ECDSAP256SHA256"), + resource.TestCheckResourceAttr(resourceName, "signing_algorithm_type", "13"), + resource.TestCheckResourceAttr(resourceName, "status", tfroute53.KeySigningKeyStatusActive), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsRoute53KeySigningKey_disappears(t *testing.T) { + resourceName := "aws_route53_key_signing_key.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckRoute53KeySigningKey(t) }, + ErrorCheck: testAccErrorCheckSkipRoute53(t), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsRoute53KeySigningKeyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsRoute53KeySigningKeyConfig_Name(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsRoute53KeySigningKeyExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsRoute53KeySigningKey(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAwsRoute53KeySigningKey_Status(t *testing.T) { + resourceName := "aws_route53_key_signing_key.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckRoute53KeySigningKey(t) }, + ErrorCheck: testAccErrorCheckSkipRoute53(t), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsRoute53KeySigningKeyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsRoute53KeySigningKeyConfig_Status(rName, tfroute53.KeySigningKeyStatusInactive), + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsRoute53KeySigningKeyExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "status", tfroute53.KeySigningKeyStatusInactive), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsRoute53KeySigningKeyConfig_Status(rName, tfroute53.KeySigningKeyStatusActive), + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsRoute53KeySigningKeyExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "status", tfroute53.KeySigningKeyStatusActive), + ), + }, + { + Config: testAccAwsRoute53KeySigningKeyConfig_Status(rName, tfroute53.KeySigningKeyStatusInactive), + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsRoute53KeySigningKeyExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "status", tfroute53.KeySigningKeyStatusInactive), + ), + }, + }, + }) +} + +func testAccCheckAwsRoute53KeySigningKeyDestroy(s *terraform.State) error { + conn := testAccProviderRoute53KeySigningKey.Meta().(*AWSClient).r53conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_route53_key_signing_key" { + continue + } + + keySigningKey, err := finder.KeySigningKeyByResourceID(conn, rs.Primary.ID) + + if tfawserr.ErrCodeEquals(err, route53.ErrCodeNoSuchHostedZone) { + continue + } + + if tfawserr.ErrCodeEquals(err, route53.ErrCodeNoSuchKeySigningKey) { + continue + } + + if err != nil { + return fmt.Errorf("error reading Route 53 Key Signing Key (%s): %w", rs.Primary.ID, err) + } + + if keySigningKey != nil { + return fmt.Errorf("Route 53 Key Signing Key (%s) still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccAwsRoute53KeySigningKeyExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + + if !ok { + return fmt.Errorf("resource %s not found", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("resource %s has not set its id", resourceName) + } + + conn := testAccProviderRoute53KeySigningKey.Meta().(*AWSClient).r53conn + + keySigningKey, err := finder.KeySigningKeyByResourceID(conn, rs.Primary.ID) + + if err != nil { + return fmt.Errorf("error reading Route 53 Key Signing Key (%s): %w", rs.Primary.ID, err) + } + + if keySigningKey == nil { + return fmt.Errorf("Route 53 Key Signing Key (%s) not found", rs.Primary.ID) + } + + return nil + } +} + +func testAccAwsRoute53KeySigningKeyConfig_Base(rName string) string { + return composeConfig( + testAccRoute53KeySigningKeyRegionProviderConfig(), + fmt.Sprintf(` +resource "aws_kms_key" "test" { + customer_master_key_spec = "ECC_NIST_P256" + deletion_window_in_days = 7 + key_usage = "SIGN_VERIFY" + policy = jsonencode({ + Statement = [ + { + Action = [ + "kms:DescribeKey", + "kms:GetPublicKey", + "kms:Sign", + ], + Effect = "Allow" + Principal = { + Service = "api-service.dnssec.route53.aws.internal" + } + Sid = "Allow Route 53 DNSSEC Service" + }, + { + Action = "kms:*" + Effect = "Allow" + Principal = { + AWS = "*" + } + Resource = "*" + Sid = "Enable IAM User Permissions" + }, + ] + Version = "2012-10-17" + }) +} + +resource "aws_route53_zone" "test" { + name = "%[1]s.terraformtest.com" +} +`, rName)) +} + +func testAccAwsRoute53KeySigningKeyConfig_Name(rName string) string { + return composeConfig( + testAccAwsRoute53KeySigningKeyConfig_Base(rName), + fmt.Sprintf(` +resource "aws_route53_key_signing_key" "test" { + hosted_zone_id = aws_route53_zone.test.id + key_management_service_arn = aws_kms_key.test.arn + name = %[1]q +} +`, rName)) +} + +func testAccAwsRoute53KeySigningKeyConfig_Status(rName string, status string) string { + return composeConfig( + testAccAwsRoute53KeySigningKeyConfig_Base(rName), + fmt.Sprintf(` +resource "aws_route53_key_signing_key" "test" { + hosted_zone_id = aws_route53_zone.test.id + key_management_service_arn = aws_kms_key.test.arn + name = %[1]q + status = %[2]q +} +`, rName, status)) +} diff --git a/aws/route53_key_signing_key_test.go b/aws/route53_key_signing_key_test.go new file mode 100644 index 00000000000..6ab08003000 --- /dev/null +++ b/aws/route53_key_signing_key_test.go @@ -0,0 +1,87 @@ +package aws + +import ( + "context" + "sync" + "testing" + + "github.com/aws/aws-sdk-go/aws/endpoints" + "github.com/aws/aws-sdk-go/service/route53" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +// Route 53 Key Signing Key can only be enabled with KMS Keys in specific regions, + +// testAccRoute53KeySigningKeyRegion is the chosen Route 53 Key Signing Key testing region +// +// Cached to prevent issues should multiple regions become available. +var testAccRoute53KeySigningKeyRegion string + +// testAccProviderRoute53KeySigningKey is the Route 53 Key Signing Key provider instance +// +// This Provider can be used in testing code for API calls without requiring +// the use of saving and referencing specific ProviderFactories instances. +// +// testAccPreCheckRoute53KeySigningKey(t) must be called before using this provider instance. +var testAccProviderRoute53KeySigningKey *schema.Provider + +// testAccProviderRoute53KeySigningKeyConfigure ensures the provider is only configured once +var testAccProviderRoute53KeySigningKeyConfigure sync.Once + +// testAccPreCheckRoute53KeySigningKey verifies AWS credentials and that Route 53 Key Signing Key is supported +func testAccPreCheckRoute53KeySigningKey(t *testing.T) { + testAccPartitionHasServicePreCheck(route53.EndpointsID, t) + + // Since we are outside the scope of the Terraform configuration we must + // call Configure() to properly initialize the provider configuration. + testAccProviderRoute53KeySigningKeyConfigure.Do(func() { + testAccProviderRoute53KeySigningKey = Provider() + + region := testAccGetRoute53KeySigningKeyRegion() + + if region == "" { + t.Skip("Route 53 Key Signing Key not available in this AWS Partition") + } + + config := map[string]interface{}{ + "region": region, + } + + diags := testAccProviderRoute53KeySigningKey.Configure(context.Background(), terraform.NewResourceConfigRaw(config)) + + if diags != nil && diags.HasError() { + for _, d := range diags { + if d.Severity == diag.Error { + t.Fatalf("error configuring Route 53 Key Signing Key provider: %s", d.Summary) + } + } + } + }) +} + +// testAccRoute53KeySigningKeyRegionProviderConfig is the Terraform provider configuration for Route 53 Key Signing Key region testing +// +// Testing Route 53 Key Signing Key assumes no other provider configurations +// are necessary and overwrites the "aws" provider configuration. +func testAccRoute53KeySigningKeyRegionProviderConfig() string { + return testAccRegionalProviderConfig(testAccGetRoute53KeySigningKeyRegion()) +} + +// testAccGetRoute53KeySigningKeyRegion returns the Route 53 Key Signing Key region for testing +func testAccGetRoute53KeySigningKeyRegion() string { + if testAccRoute53KeySigningKeyRegion != "" { + return testAccRoute53KeySigningKeyRegion + } + + // AWS Commercial: https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-configuring-dnssec-cmk-requirements.html + // AWS GovCloud (US) - not available yet: https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-r53.html + // AWS China - not available yet: https://docs.amazonaws.cn/en_us/aws/latest/userguide/route53.html + switch testAccGetPartition() { + case endpoints.AwsPartitionID: + testAccRoute53KeySigningKeyRegion = endpoints.UsEast1RegionID + } + + return testAccRoute53KeySigningKeyRegion +} diff --git a/website/docs/r/route53_key_signing_key.html.markdown b/website/docs/r/route53_key_signing_key.html.markdown new file mode 100644 index 00000000000..c0aa9d65356 --- /dev/null +++ b/website/docs/r/route53_key_signing_key.html.markdown @@ -0,0 +1,97 @@ +--- +subcategory: "Route53" +layout: "aws" +page_title: "AWS: aws_route53_key_signing_key" +description: |- + Manages an Route 53 Key Signing Key +--- + +# Resource: aws_route53_key_signing_key + +Manages an Route 53 Key Signing Key. For more information about managing Domain Name System Security Extensions (DNSSEC)in Route 53, see the [Route 53 Developer Guide](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-configuring-dnssec.html). + +## Example Usage + +```hcl +provider "aws" { + region = "us-east-1" +} + +resource "aws_kms_key" "example" { + customer_master_key_spec = "ECC_NIST_P256" + deletion_window_in_days = 7 + key_usage = "SIGN_VERIFY" + policy = jsonencode({ + Statement = [ + { + Action = [ + "kms:DescribeKey", + "kms:GetPublicKey", + "kms:Sign", + ], + Effect = "Allow" + Principal = { + Service = "api-service.dnssec.route53.aws.internal" + } + Sid = "Route 53 DNSSEC Permissions" + }, + { + Action = "kms:*" + Effect = "Allow" + Principal = { + AWS = "*" + } + Resource = "*" + Sid = "IAM User Permissions" + }, + ] + Version = "2012-10-17" + }) +} + +resource "aws_route53_zone" "example" { + name = "example.com" +} + +resource "aws_route53_key_signing_key" "example" { + hosted_zone_id = aws_route53_zone.test.id + key_management_service_arn = aws_kms_key.test.arn + name = "example" +} +``` + +## Argument Reference + +The following arguments are required: + +* `hosted_zone_id` - (Required) Identifier of the Route 53 Hosted Zone. +* `key_management_service_arn` - (Required) Amazon Resource Name (ARN) of the Key Management Service (KMS) Key. This must be unique for each key-signing key (KSK) in a single hosted zone. This key must be in the `us-east-1` Region and meet certain requirements, which are described in the [Route 53 Developer Guide](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-configuring-dnssec-cmk-requirements.html) and [Route 53 API Reference](https://docs.aws.amazon.com/Route53/latest/APIReference/API_CreateKeySigningKey.html). +* `name` - (Required) Name of the key-signing key (KSK). Must be unique for each key-singing key in the same hosted zone. + +The following arguments are optional: + +* `status` - (Optional) Status of the key-signing key (KSK). Valid values: `ACTIVE`, `INACTIVE`. Defaults to `ACTIVE`. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `digest_algorithm_mnemonic` - A string used to represent the delegation signer digest algorithm. This value must follow the guidelines provided by [RFC-8624 Section 3.3](https://tools.ietf.org/html/rfc8624#section-3.3). +* `digest_algorithm_type` - An integer used to represent the delegation signer digest algorithm. This value must follow the guidelines provided by [RFC-8624 Section 3.3](https://tools.ietf.org/html/rfc8624#section-3.3). +* `digest_value` - A cryptographic digest of a DNSKEY resource record (RR). DNSKEY records are used to publish the public key that resolvers can use to verify DNSSEC signatures that are used to secure certain kinds of information provided by the DNS system. +* `dnskey_record` - A string that represents a DNSKEY record. +* `ds_record` - A string that represents a delegation signer (DS) record. +* `flag` - An integer that specifies how the key is used. For key-signing key (KSK), this value is always 257. +* `id` - Route 53 Hosted Zone identifier and KMS Key identifier, separated by a comma (`,`). +* `key_tag` - An integer used to identify the DNSSEC record for the domain name. The process used to calculate the value is described in [RFC-4034 Appendix B](https://tools.ietf.org/rfc/rfc4034.txt). +* `public_key` - The public key, represented as a Base64 encoding, as required by [RFC-4034 Page 5](https://tools.ietf.org/rfc/rfc4034.txt). +* `signing_algorithm_mnemonic` - A string used to represent the signing algorithm. This value must follow the guidelines provided by [RFC-8624 Section 3.1](https://tools.ietf.org/html/rfc8624#section-3.1). +* `signing_algorithm_type` - An integer used to represent the signing algorithm. This value must follow the guidelines provided by [RFC-8624 Section 3.1](https://tools.ietf.org/html/rfc8624#section-3.1). + +## Import + +`aws_route53_key_signing_key` resources can be imported by using the Route 53 Hosted Zone identifier and KMS Key identifier, separated by a comma (`,`), e.g. + +``` +$ terraform import aws_route53_key_signing_key.example Z1D633PJN98FT9,example +```