From dcfd137312ecbb229da23635aeeaef7625f97e3e Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 18 Dec 2020 11:29:20 -0500 Subject: [PATCH 1/3] 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 (134.60s) --- PASS: TestAccAwsRoute53KeySigningKey_basic (135.34s) --- PASS: TestAccAwsRoute53KeySigningKey_Status (180.85s) ``` --- 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 7cf44f61983..dd1d393e7ff 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -855,6 +855,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 +``` From 4e5b1311057abce639638f2e7cef6c82825cabd9 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Wed, 27 Jan 2021 11:10:29 -0500 Subject: [PATCH 2/3] Update CHANGELOG for #16840 --- .changelog/16840.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/16840.txt diff --git a/.changelog/16840.txt b/.changelog/16840.txt new file mode 100644 index 00000000000..cbcde6925b2 --- /dev/null +++ b/.changelog/16840.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_route53_key_signing_key +``` From 713a466586a43f3c50c3f3cdee723dd3de94a4c4 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 2 Feb 2021 16:57:12 -0500 Subject: [PATCH 3/3] tests/service/route53: Fix region lookup skipping to occur outside sync.Once Output from acceptance testing in AWS Commercial: ``` --- PASS: TestAccAwsRoute53KeySigningKey_disappears (157.73s) --- PASS: TestAccAwsRoute53KeySigningKey_basic (180.64s) --- PASS: TestAccAwsRoute53KeySigningKey_Status (218.81s) ``` Output from acceptance testing in AWS GovCloud (US): ``` === CONT TestAccAwsRoute53KeySigningKey_basic route53_key_signing_key_test.go:40: Route 53 Key Signing Key not available in this AWS Partition --- SKIP: TestAccAwsRoute53KeySigningKey_basic (2.52s) === CONT TestAccAwsRoute53KeySigningKey_Status route53_key_signing_key_test.go:40: Route 53 Key Signing Key not available in this AWS Partition === CONT TestAccAwsRoute53KeySigningKey_disappears route53_key_signing_key_test.go:40: Route 53 Key Signing Key not available in this AWS Partition --- SKIP: TestAccAwsRoute53KeySigningKey_Status (2.52s) --- SKIP: TestAccAwsRoute53KeySigningKey_disappears (2.52s) ``` --- aws/route53_key_signing_key_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/aws/route53_key_signing_key_test.go b/aws/route53_key_signing_key_test.go index 6ab08003000..ad2a87f7f00 100644 --- a/aws/route53_key_signing_key_test.go +++ b/aws/route53_key_signing_key_test.go @@ -34,17 +34,17 @@ var testAccProviderRoute53KeySigningKeyConfigure sync.Once func testAccPreCheckRoute53KeySigningKey(t *testing.T) { testAccPartitionHasServicePreCheck(route53.EndpointsID, t) + region := testAccGetRoute53KeySigningKeyRegion() + + if region == "" { + t.Skip("Route 53 Key Signing Key not available in this AWS Partition") + } + // 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, }