diff --git a/.changelog/26274.txt b/.changelog/26274.txt new file mode 100644 index 000000000000..88a61a564baa --- /dev/null +++ b/.changelog/26274.txt @@ -0,0 +1,11 @@ +```release-note:new-resource +aws_dx_macsec_key_association +``` + +```release-note:enhancement +resource/aws_dx_connection: Add `encryption_mode` and `request_macsec` arguments and `macsec_capable` and `port_encryption_status` attributes in support of [MACsec](https://docs.aws.amazon.com/directconnect/latest/UserGuide/MACsec.html) +``` + +```release-note:enhancement +resource/aws_dx_connection: Add `skip_destroy` argument +``` \ No newline at end of file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 40226965b8c2..9d2e42792e82 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1304,6 +1304,7 @@ func New(_ context.Context) (*schema.Provider, error) { "aws_dx_hosted_transit_virtual_interface": directconnect.ResourceHostedTransitVirtualInterface(), "aws_dx_hosted_transit_virtual_interface_accepter": directconnect.ResourceHostedTransitVirtualInterfaceAccepter(), "aws_dx_lag": directconnect.ResourceLag(), + "aws_dx_macsec_key_association": directconnect.ResourceMacSecKeyAssociation(), "aws_dx_private_virtual_interface": directconnect.ResourcePrivateVirtualInterface(), "aws_dx_public_virtual_interface": directconnect.ResourcePublicVirtualInterface(), "aws_dx_transit_virtual_interface": directconnect.ResourceTransitVirtualInterface(), diff --git a/internal/service/directconnect/connection.go b/internal/service/directconnect/connection.go index 2ffaacc2352e..e68906013696 100644 --- a/internal/service/directconnect/connection.go +++ b/internal/service/directconnect/connection.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go/service/directconnect" "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" @@ -40,6 +41,13 @@ func ResourceConnection() *schema.Resource { ForceNew: true, ValidateFunc: validConnectionBandWidth(), }, + // The MAC Security (MACsec) connection encryption mode. + "encryption_mode": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"no_encrypt", "should_encrypt", "must_encrypt"}, false), + }, "has_logical_redundancy": { Type: schema.TypeString, Computed: true, @@ -53,6 +61,18 @@ func ResourceConnection() *schema.Resource { Required: true, ForceNew: true, }, + // Indicates whether the connection supports MAC Security (MACsec). + "macsec_capable": { + Type: schema.TypeBool, + Computed: true, + }, + // Enable or disable MAC Security (MACsec) on this connection. + "request_macsec": { + Type: schema.TypeBool, + Optional: true, + Default: false, + ForceNew: true, + }, "name": { Type: schema.TypeString, Required: true, @@ -62,12 +82,22 @@ func ResourceConnection() *schema.Resource { Type: schema.TypeString, Computed: true, }, + // The MAC Security (MACsec) port link status of the connection. + "port_encryption_status": { + Type: schema.TypeString, + Computed: true, + }, "provider_name": { Type: schema.TypeString, Optional: true, Computed: true, ForceNew: true, }, + "skip_destroy": { + Type: schema.TypeBool, + Default: false, + Optional: true, + }, "tags": tftags.TagsSchema(), "tags_all": tftags.TagsSchemaComputed(), "vlan_id": { @@ -90,6 +120,7 @@ func resourceConnectionCreate(d *schema.ResourceData, meta interface{}) error { Bandwidth: aws.String(d.Get("bandwidth").(string)), ConnectionName: aws.String(name), Location: aws.String(d.Get("location").(string)), + RequestMACSec: aws.Bool(d.Get("request_macsec").(bool)), } if v, ok := d.GetOk("provider_name"); ok { @@ -139,14 +170,23 @@ func resourceConnectionRead(d *schema.ResourceData, meta interface{}) error { d.Set("arn", arn) d.Set("aws_device", connection.AwsDeviceV2) d.Set("bandwidth", connection.Bandwidth) + d.Set("encryption_mode", connection.EncryptionMode) d.Set("has_logical_redundancy", connection.HasLogicalRedundancy) d.Set("jumbo_frame_capable", connection.JumboFrameCapable) d.Set("location", connection.Location) + d.Set("macsec_capable", connection.MacSecCapable) d.Set("name", connection.ConnectionName) d.Set("owner_account_id", connection.OwnerAccount) + d.Set("port_encryption_status", connection.PortEncryptionStatus) d.Set("provider_name", connection.ProviderName) d.Set("vlan_id", connection.Vlan) + // d.Set("request_macsec", d.Get("request_macsec").(bool)) + + if !d.IsNewResource() && !d.Get("request_macsec").(bool) { + d.Set("request_macsec", aws.Bool(false)) + } + tags, err := ListTags(conn, arn) if err != nil { @@ -170,9 +210,26 @@ func resourceConnectionRead(d *schema.ResourceData, meta interface{}) error { func resourceConnectionUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).DirectConnectConn - arn := d.Get("arn").(string) + // Update encryption mode + if d.HasChange("encryption_mode") { + input := &directconnect.UpdateConnectionInput{ + ConnectionId: aws.String(d.Id()), + EncryptionMode: aws.String(d.Get("encryption_mode").(string)), + } + log.Printf("[DEBUG] Modifying Direct Connect connection attributes: %s", input) + _, err := conn.UpdateConnection(input) + if err != nil { + return fmt.Errorf("error modifying Direct Connect connection (%s) attributes: %s", d.Id(), err) + } + + if _, err := waitConnectionConfirmed(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for Direct Connect connection (%s) to become available: %w", d.Id(), err) + } + } + if d.HasChange("tags_all") { o, n := d.GetChange("tags_all") + arn := d.Get("arn").(string) if err := UpdateTags(conn, arn, o, n); err != nil { return fmt.Errorf("error updating Direct Connect Connection (%s) tags: %w", arn, err) @@ -183,6 +240,11 @@ func resourceConnectionUpdate(d *schema.ResourceData, meta interface{}) error { } func resourceConnectionDelete(d *schema.ResourceData, meta interface{}) error { + if v, ok := d.GetOk("skip_destroy"); ok && v.(bool) { + log.Printf("[DEBUG] Retaining Direct Connect Connection: %s", d.Id()) + return nil + } + conn := meta.(*conns.AWSClient).DirectConnectConn return deleteConnection(conn, d.Id(), waitConnectionDeleted) diff --git a/internal/service/directconnect/connection_test.go b/internal/service/directconnect/connection_test.go index 2758a88e81eb..fa4c606ab238 100644 --- a/internal/service/directconnect/connection_test.go +++ b/internal/service/directconnect/connection_test.go @@ -2,6 +2,7 @@ package directconnect_test import ( "fmt" + "os" "regexp" "testing" @@ -42,9 +43,10 @@ func TestAccDirectConnectConnection_basic(t *testing.T) { }, // Test import. { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"request_macsec", "skip_destroy"}, }, }, }) @@ -73,6 +75,98 @@ func TestAccDirectConnectConnection_disappears(t *testing.T) { }) } +func TestAccDirectConnectConnection_encryptionMode(t *testing.T) { + dxKey := "DX_CONNECTION_ID" + connectionId := os.Getenv(dxKey) + if connectionId == "" { + t.Skipf("Environment variable %s is not set", dxKey) + } + + dxName := "DX_CONNECTION_NAME" + connectionName := os.Getenv(dxName) + if connectionName == "" { + t.Skipf("Environment variable %s is not set", dxName) + } + + var connection directconnect.Connection + resourceName := "aws_dx_connection.test" + ckn := testAccDirecConnectMacSecGenerateHex() + cak := testAccDirecConnectMacSecGenerateHex() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, directconnect.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccConnectionConfig_encryptionModeShouldEncrypt(connectionName, ckn, cak), + ResourceName: resourceName, + ImportState: true, + ImportStateId: connectionId, + ImportStatePersist: true, + }, + { + Config: testAccConnectionConfig_encryptionModeNoEncrypt(connectionName), + Check: resource.ComposeTestCheckFunc( + testAccCheckConnectionExists(resourceName, &connection), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "directconnect", regexp.MustCompile(`dxcon/.+`)), + resource.TestCheckResourceAttr(resourceName, "encryption_mode", "no_encrypt"), + resource.TestCheckResourceAttrSet(resourceName, "location"), + resource.TestCheckResourceAttr(resourceName, "name", connectionName), + resource.TestCheckResourceAttr(resourceName, "skip_destroy", "true"), + ), + }, + { + Config: testAccConnectionConfig_encryptionModeShouldEncrypt(connectionName, ckn, cak), + Check: resource.ComposeTestCheckFunc( + testAccCheckConnectionExists(resourceName, &connection), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "directconnect", regexp.MustCompile(`dxcon/.+`)), + resource.TestCheckResourceAttr(resourceName, "encryption_mode", "should_encrypt"), + resource.TestCheckResourceAttrSet(resourceName, "location"), + resource.TestCheckResourceAttr(resourceName, "name", connectionName), + resource.TestCheckResourceAttr(resourceName, "skip_destroy", "true"), + ), + }, + }, + }) +} + +func TestAccDirectConnectConnection_macsecRequested(t *testing.T) { + var connection directconnect.Connection + resourceName := "aws_dx_connection.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, directconnect.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckConnectionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccConnectionConfig_macsecEnabled(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckConnectionExists(resourceName, &connection), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "directconnect", regexp.MustCompile(`dxcon/.+`)), + resource.TestCheckResourceAttr(resourceName, "bandwidth", "100Gbps"), + resource.TestCheckResourceAttrSet(resourceName, "location"), + resource.TestCheckResourceAttr(resourceName, "request_macsec", "true"), + acctest.CheckResourceAttrAccountID(resourceName, "owner_account_id"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrSet(resourceName, "provider_name"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"request_macsec", "skip_destroy"}, + }, + }, + }) +} + func TestAccDirectConnectConnection_providerName(t *testing.T) { var connection directconnect.Connection resourceName := "aws_dx_connection.test" @@ -99,9 +193,32 @@ func TestAccDirectConnectConnection_providerName(t *testing.T) { }, // Test import. { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"request_macsec", "skip_destroy"}, + }, + }, + }) +} + +func TestAccDirectConnectConnection_skipDestroy(t *testing.T) { + var connection directconnect.Connection + resourceName := "aws_dx_connection.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, directconnect.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckConnectionNoDestroy, + Steps: []resource.TestStep{ + { + Config: testAccConnectionConfig_skipDestroy(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckConnectionExists(resourceName, &connection), + resource.TestCheckResourceAttr(resourceName, "skip_destroy", "true"), + ), }, }, }) @@ -129,9 +246,10 @@ func TestAccDirectConnectConnection_tags(t *testing.T) { }, // Test import. { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"request_macsec", "skip_destroy"}, }, { Config: testAccConnectionConfig_tags2(rName, "key1", "value1updated", "key2", "value2"), @@ -205,6 +323,22 @@ func testAccCheckConnectionExists(name string, v *directconnect.Connection) reso } } +func testAccCheckConnectionNoDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).DirectConnectConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_dx_connection" { + continue + } + + _, err := tfdirectconnect.FindConnectionByID(conn, rs.Primary.ID) + + return err + } + + return nil +} + func testAccConnectionConfig_basic(rName string) string { return fmt.Sprintf(` data "aws_dx_locations" "test" {} @@ -222,6 +356,60 @@ resource "aws_dx_connection" "test" { `, rName) } +func testAccConnectionConfig_encryptionModeNoEncrypt(rName string) string { + return fmt.Sprintf(` +resource "aws_dx_connection" "test" { + name = %[1]q + location = "CSOW" + bandwidth = "100Gbps" + encryption_mode = "no_encrypt" + skip_destroy = true +} +`, rName) +} + +func testAccConnectionConfig_encryptionModeShouldEncrypt(rName, ckn, cak string) string { + return fmt.Sprintf(` +resource "aws_dx_connection" "test" { + name = %[1]q + location = "CSOW" + bandwidth = "100Gbps" + encryption_mode = "should_encrypt" + skip_destroy = true +} + +resource "aws_dx_macsec_key_association" "test" { + connection_id = aws_dx_connection.test.id + ckn = %[2]q + cak = %[3]q +} +`, rName, ckn, cak) +} + +func testAccConnectionConfig_macsecEnabled(rName string) string { + return fmt.Sprintf(` +data "aws_dx_locations" "test" {} + +locals { + location_codes = tolist(data.aws_dx_locations.test.location_codes) + idx = min(2, length(local.location_codes) - 1) +} + +data "aws_dx_location" "test" { + location_code = local.location_codes[local.idx] +} + +resource "aws_dx_connection" "test" { + name = %[1]q + bandwidth = "100Gbps" + location = data.aws_dx_location.test.location_code + request_macsec = true + + provider_name = data.aws_dx_location.test.available_providers[0] +} +`, rName) +} + func testAccConnectionConfig_providerName(rName string) string { return fmt.Sprintf(` data "aws_dx_locations" "test" {} @@ -245,6 +433,24 @@ resource "aws_dx_connection" "test" { `, rName) } +func testAccConnectionConfig_skipDestroy(rName string) string { + return fmt.Sprintf(` +data "aws_dx_locations" "test" {} + +locals { + location_codes = tolist(data.aws_dx_locations.test.location_codes) + idx = min(2, length(local.location_codes) - 1) +} + +resource "aws_dx_connection" "test" { + name = %[1]q + bandwidth = "1Gbps" + location = local.location_codes[local.idx] + skip_destroy = true +} + `, rName) +} + func testAccConnectionConfig_tags1(rName, tagKey1, tagValue1 string) string { return fmt.Sprintf(` data "aws_dx_locations" "test" {} diff --git a/internal/service/directconnect/macsec_key.go b/internal/service/directconnect/macsec_key.go new file mode 100644 index 000000000000..61c1293c70d2 --- /dev/null +++ b/internal/service/directconnect/macsec_key.go @@ -0,0 +1,171 @@ +package directconnect + +import ( + "fmt" + "log" + "regexp" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directconnect" + "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" + "github.com/hashicorp/terraform-provider-aws/internal/conns" +) + +func ResourceMacSecKeyAssociation() *schema.Resource { + return &schema.Resource{ + // MacSecKey resource only supports create (Associate), read (Describe) and delete (Disassociate) + Create: resourceMacSecKeyCreate, + Read: resourceMacSecKeyRead, + Delete: resourceMacSecKeyDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "cak": { + Type: schema.TypeString, + Optional: true, + // CAK requires CKN + RequiredWith: []string{"ckn"}, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`[a-fA-F0-9]{64}$`), "Must be 64-character hex code string"), + ForceNew: true, + }, + "ckn": { + Type: schema.TypeString, + Computed: true, + Optional: true, + AtLeastOneOf: []string{"ckn", "secret_arn"}, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`[a-fA-F0-9]{64}$`), "Must be 64-character hex code string"), + ForceNew: true, + }, + "connection_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "secret_arn": { + Type: schema.TypeString, + Optional: true, + Computed: true, + AtLeastOneOf: []string{"ckn", "secret_arn"}, + ForceNew: true, + }, + "start_on": { + Type: schema.TypeString, + Computed: true, + }, + "state": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceMacSecKeyCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).DirectConnectConn + + input := &directconnect.AssociateMacSecKeyInput{ + ConnectionId: aws.String(d.Get("connection_id").(string)), + } + + if d.Get("ckn").(string) != "" { + input.Cak = aws.String(d.Get("cak").(string)) + input.Ckn = aws.String(d.Get("ckn").(string)) + } + + if d.Get("secret_arn").(string) != "" { + input.SecretARN = aws.String(d.Get("secret_arn").(string)) + } + + log.Printf("[DEBUG] Creating MACSec secret key on Direct Connect Connection: %s", *input.ConnectionId) + output, err := conn.AssociateMacSecKey(input) + + if err != nil { + return fmt.Errorf("error creating MACSec secret key on Direct Connect Connection (%s): %w", *input.ConnectionId, err) + } + + secret_arn := MacSecKeyParseSecretARN(output) + + // Create a composite ID based on connection ID and secret ARN + d.SetId(fmt.Sprintf("%s_%s", secret_arn, aws.StringValue(output.ConnectionId))) + + d.Set("secret_arn", secret_arn) + + return resourceMacSecKeyRead(d, meta) +} + +func resourceMacSecKeyRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).DirectConnectConn + + secretArn, connId, err := MacSecKeyParseID(d.Id()) + if err != nil { + return fmt.Errorf("unexpected format of ID (%s), expected secretArn_connectionId", d.Id()) + } + + connection, err := FindConnectionByID(conn, connId) + if err != nil { + return fmt.Errorf("error reading Direct Connect Connection (%s): %w", d.Id(), err) + } + + if connection.MacSecKeys == nil { + return fmt.Errorf("no MACSec keys found on Direct Connect Connection (%s)", d.Id()) + } + + for _, key := range connection.MacSecKeys { + if aws.StringValue(key.SecretARN) == aws.StringValue(&secretArn) { + d.Set("ckn", key.Ckn) + d.Set("connection_id", connId) + d.Set("secret_arn", key.SecretARN) + d.Set("start_on", key.StartOn) + d.Set("state", key.State) + } + } + + return nil +} + +func resourceMacSecKeyDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).DirectConnectConn + + input := &directconnect.DisassociateMacSecKeyInput{ + ConnectionId: aws.String(d.Get("connection_id").(string)), + SecretARN: aws.String(d.Get("secret_arn").(string)), + } + + log.Printf("[DEBUG] Disassociating MACSec secret key on Direct Connect Connection: %s", *input.ConnectionId) + _, err := conn.DisassociateMacSecKey(input) + + if err != nil { + return fmt.Errorf("Unable to disassociate MACSec secret key on Direct Connect Connection (%s): %w", *input.ConnectionId, err) + } + + return nil +} + +// MacSecKeyParseSecretARN parses the secret ARN returned from a CMK or secret_arn +func MacSecKeyParseSecretARN(output *directconnect.AssociateMacSecKeyOutput) string { + var result string + + for _, key := range output.MacSecKeys { + if key != nil { + result = aws.StringValue(key.SecretARN) + } + } + + return result +} + +// MacSecKeyParseID parses the resource ID and returns the secret ARN and connection ID +func MacSecKeyParseID(id string) (string, string, error) { + parts := strings.SplitN(id, "_", 2) + + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", &resource.NotFoundError{} + } + + return parts[0], parts[1], nil +} diff --git a/internal/service/directconnect/macsec_key_test.go b/internal/service/directconnect/macsec_key_test.go new file mode 100644 index 000000000000..ca50f1df2ccb --- /dev/null +++ b/internal/service/directconnect/macsec_key_test.go @@ -0,0 +1,124 @@ +package directconnect_test + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "os" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/service/directconnect" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" +) + +func TestAccDirectConnectMacSecKey_withCkn(t *testing.T) { + // Requires an existing MACsec-capable DX connection set as environmental variable + key := "DX_CONNECTION_ID" + connectionId := os.Getenv(key) + if connectionId == "" { + t.Skipf("Environment variable %s is not set", key) + } + resourceName := "aws_dx_macsec_key_association.test" + ckn := testAccDirecConnectMacSecGenerateHex() + cak := testAccDirecConnectMacSecGenerateHex() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, directconnect.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccMacSecConfig_withCkn(ckn, cak, connectionId), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "connection_id", connectionId), + resource.TestMatchResourceAttr(resourceName, "ckn", regexp.MustCompile(ckn)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + // Ignore the "cak" attribute as isn't returned by the API during read/refresh + ImportStateVerifyIgnore: []string{"cak"}, + }, + }, + }) +} + +func TestAccDirectConnectMacSecKey_withSecret(t *testing.T) { + // Requires an existing MACsec-capable DX connection set as environmental variable + dxKey := "DX_CONNECTION_ID" + connectionId := os.Getenv(dxKey) + if connectionId == "" { + t.Skipf("Environment variable %s is not set", dxKey) + } + + secretKey := "SECRET_ARN" + secretArn := os.Getenv(secretKey) + if secretArn == "" { + t.Skipf("Environment variable %s is not set", secretKey) + } + + resourceName := "aws_dx_macsec_key_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, directconnect.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccMacSecConfig_withSecret(secretArn, connectionId), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "connection_id", connectionId), + resource.TestCheckResourceAttr(resourceName, "secret_arn", secretArn), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +// testAccDirecConnectMacSecGenerateKey generates a 64-character hex string to be used as CKN or CAK +func testAccDirecConnectMacSecGenerateHex() string { + s := make([]byte, 32) + if _, err := rand.Read(s); err != nil { + return "" + } + return hex.EncodeToString(s) +} + +func testAccMacSecConfig_withCkn(ckn, cak, connectionId string) string { + return fmt.Sprintf(` +resource "aws_dx_macsec_key_association" "test" { + connection_id = %[3]q + ckn = %[1]q + cak = %[2]q +} + + +`, ckn, cak, connectionId) +} + +// Can only be used with an EXISTING secrets created by previous association - cannot create secrets from scratch +func testAccMacSecConfig_withSecret(secretArn, connectionId string) string { + return fmt.Sprintf(` +data "aws_secretsmanager_secret" "test" { + arn = %[1]q +} + +resource "aws_dx_macsec_key_association" "test" { + connection_id = %[2]q + secret_arn = data.aws_secretsmanager_secret.test.arn +} + + +`, secretArn, connectionId) +} diff --git a/internal/service/directconnect/sweep.go b/internal/service/directconnect/sweep.go index c76dc024189d..607e5195c3bb 100644 --- a/internal/service/directconnect/sweep.go +++ b/internal/service/directconnect/sweep.go @@ -10,6 +10,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/directconnect" "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-provider-aws/internal/conns" @@ -48,6 +49,12 @@ func init() { F: sweepLags, Dependencies: []string{"aws_dx_connection"}, }) + + resource.AddTestSweepers("aws_dx_macsec_key", &resource.Sweeper{ + Name: "aws_dx_macsec_key", + F: sweepMacSecKeys, + Dependencies: []string{}, + }) } func sweepConnections(region string) error { @@ -439,3 +446,61 @@ func sweepLags(region string) error { return sweeperErrs.ErrorOrNil() } + +func sweepMacSecKeys(region string) error { + client, err := sweep.SharedRegionalSweepClient(region) + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + + dxConn := client.(*conns.AWSClient).DirectConnectConn + + // Clean up leaked Secrets Manager resources created by Direct Connect. + // Direct Connect does not remove the corresponding Secrets Manager + // key when deleting the MACsec key association. The only option to + // clean up the dangling resource is to use Secrets Manager to delete + // the MACsec key secret. + smConn := client.(*conns.AWSClient).SecretsManagerConn + dxInput := &directconnect.DescribeConnectionsInput{} + var sweeperErrs *multierror.Error + + output, err := dxConn.DescribeConnections(dxInput) + + if err != nil { + sweeperErr := fmt.Errorf("error listing Direct Connect Connections for %s: %w", region, err) + log.Printf("[ERROR] %s", sweeperErr) + sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) + return sweeperErrs.ErrorOrNil() + } + + if output == nil { + log.Printf("[WARN] Skipping Direct Connect MACsec Keys sweep for %s: empty response", region) + return sweeperErrs.ErrorOrNil() + } + + for _, connection := range output.Connections { + if connection.MacSecKeys == nil { + continue + } + + for _, key := range connection.MacSecKeys { + arn := aws.StringValue(key.SecretARN) + + input := &secretsmanager.DeleteSecretInput{ + SecretId: aws.String(arn), + } + + log.Printf("[DEBUG] Deleting MACSec secret key: %s", *input.SecretId) + _, err := smConn.DeleteSecret(input) + + if err != nil { + sweeperErr := fmt.Errorf("error deleting MACsec Secret (%s): %w", arn, err) + log.Printf("[ERROR] %s", sweeperErr) + sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) + continue + } + } + } + + return sweeperErrs.ErrorOrNil() +} diff --git a/internal/service/directconnect/wait.go b/internal/service/directconnect/wait.go index f680f8f55268..f0f1e4d9320c 100644 --- a/internal/service/directconnect/wait.go +++ b/internal/service/directconnect/wait.go @@ -18,7 +18,7 @@ const ( lagDeletedTimeout = 10 * time.Minute ) -func waitConnectionConfirmed(conn *directconnect.DirectConnect, id string) (*directconnect.Connection, error) { +func waitConnectionConfirmed(conn *directconnect.DirectConnect, id string) (*directconnect.Connection, error) { //nolint:unparam stateConf := &resource.StateChangeConf{ Pending: []string{directconnect.ConnectionStatePending, directconnect.ConnectionStateOrdering, directconnect.ConnectionStateRequested}, Target: []string{directconnect.ConnectionStateAvailable}, diff --git a/website/docs/r/dx_connection.html.markdown b/website/docs/r/dx_connection.html.markdown index fa1993fbf3cc..df6e423ce588 100644 --- a/website/docs/r/dx_connection.html.markdown +++ b/website/docs/r/dx_connection.html.markdown @@ -12,6 +12,8 @@ Provides a Connection of Direct Connect. ## Example Usage +### Create a connection + ```terraform resource "aws_dx_connection" "hoge" { name = "tf-dx-connection" @@ -20,14 +22,44 @@ resource "aws_dx_connection" "hoge" { } ``` +### Request a MACsec-capable connection + +```terraform +resource "aws_dx_connection" "example" { + name = "tf-dx-connection" + bandwidth = "10Gbps" + location = "EqDA2" + request_macsec = true +} +``` + +### Configure encryption mode for MACsec-capable connections +-> **NOTE:** You can only specify the `encryption_mode` argument once the connection is in an `Available` state. + +```terraform +resource "aws_dx_connection" "example" { + name = "tf-dx-connection" + bandwidth = "10Gbps" + location = "EqDC2" + request_macsec = true + encryption_mode = "must_encrypt" +} +``` + ## Argument Reference The following arguments are supported: * `bandwidth` - (Required) The bandwidth of the connection. Valid values for dedicated connections: 1Gbps, 10Gbps. Valid values for hosted connections: 50Mbps, 100Mbps, 200Mbps, 300Mbps, 400Mbps, 500Mbps, 1Gbps, 2Gbps, 5Gbps, 10Gbps and 100Gbps. Case sensitive. +* `encryption_mode` - (Optional) The connection MAC Security (MACsec) encryption mode. MAC Security (MACsec) is only available on dedicated connections. Valid values are `no_encrypt`, `should_encrypt`, and `must_encrypt`. * `location` - (Required) The AWS Direct Connect location where the connection is located. See [DescribeLocations](https://docs.aws.amazon.com/directconnect/latest/APIReference/API_DescribeLocations.html) for the list of AWS Direct Connect locations. Use `locationCode`. * `name` - (Required) The name of the connection. * `provider_name` - (Optional) The name of the service provider associated with the connection. +* `request_macsec` - (Optional) Boolean value indicating whether you want the connection to support MAC Security (MACsec). MAC Security (MACsec) is only available on dedicated connections. See [MACsec prerequisites](https://docs.aws.amazon.com/directconnect/latest/UserGuide/direct-connect-mac-sec-getting-started.html#mac-sec-prerequisites) for more information about MAC Security (MACsec) prerequisites. Default value: `false`. + +~> **NOTE:** Changing the value of `request_macsec` will cause the resource to be destroyed and re-created. + +* `skip_destroy` - (Optional) Set to true if you do not wish the connection to be deleted at destroy time, and instead just removed from the Terraform state. * `tags` - (Optional) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. ## Attributes Reference @@ -39,7 +71,9 @@ In addition to all arguments above, the following attributes are exported: * `has_logical_redundancy` - Indicates whether the connection supports a secondary BGP peer in the same address family (IPv4/IPv6). * `id` - The ID of the connection. * `jumbo_frame_capable` - Boolean value representing if jumbo frames have been enabled for this connection. +* `macsec_capable` - Boolean value indicating whether the connection supports MAC Security (MACsec). * `owner_account_id` - The ID of the AWS account that owns the connection. +* `port_encryption_status` - The MAC Security (MACsec) port link status of the connection. * `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). * `vlan_id` - The VLAN ID. diff --git a/website/docs/r/dx_macsec_key_association.html.markdown b/website/docs/r/dx_macsec_key_association.html.markdown new file mode 100644 index 000000000000..505d3f461ba7 --- /dev/null +++ b/website/docs/r/dx_macsec_key_association.html.markdown @@ -0,0 +1,70 @@ +--- +subcategory: "Direct Connect" +layout: "aws" +page_title: "AWS: aws_dx_macsec_key_association" +description: |- + Provides a MAC Security (MACSec) secret key resource for use with Direct Connect. +--- + +# Resource: aws_dx_macsec_key_association + +Provides a MAC Security (MACSec) secret key resource for use with Direct Connect. See [MACsec prerequisites](https://docs.aws.amazon.com/directconnect/latest/UserGuide/direct-connect-mac-sec-getting-started.html#mac-sec-prerequisites) for information about MAC Security (MACsec) prerequisites. + +Creating this resource will also create a resource of type [`aws_secretsmanager_secret`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret) which is managed by Direct Connect. While you can import this resource into your Terraform state, because this secret is managed by Direct Connect, you will not be able to make any modifications to it. See [How AWS Direct Connect uses AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/integrating_how-services-use-secrets_directconnect.html) for details. + +~> **Note:** All arguments including `ckn` and `cak` will be stored in the raw state as plain-text. +[Read more about sensitive data in state](https://www.terraform.io/docs/state/sensitive-data.html). + +~> **Note:** The `secret_arn` argument can only be used to reference a previously created MACSec key. You cannot associate a Secrets Manager secret created outside of the `aws_dx_macsec_key` resource. + +## Example Usage + +### Create MACSec key with CKN and CAK + +```terraform +data "aws_dx_connection" "example" { + name = "tf-dx-connection" +} + +resource "aws_dx_macsec_key_association" "test" { + connection_id = data.aws_dx_connection.example.id + ckn = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + cak = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" +} +``` + +### Create MACSec key with existing Secrets Manager secret + +```terraform +data "aws_dx_connection" "example" { + name = "tf-dx-connection" +} + +data "aws_secretsmanager_secret" "example" { + name = "directconnect!prod/us-east-1/directconnect/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +} + +resource "aws_dx_macsec_key_association" "test" { + connection_id = data.aws_dx_connection.example.id + secret_arn = data.aws_secretsmanager_secret.example.arn +} +``` + +## Argument Reference + +The following arguments are supported: + +* `cak` - (Optional) The MAC Security (MACsec) CAK to associate with the dedicated connection. The valid values are 64 hexadecimal characters (0-9, A-E). Required if using `ckn`. +* `ckn` - (Optional) The MAC Security (MACsec) CKN to associate with the dedicated connection. The valid values are 64 hexadecimal characters (0-9, A-E). Required if using `cak`. +* `connection_id` - (Required) The ID of the dedicated Direct Connect connection. The connection must be a dedicated connection in the `AVAILABLE` state. +* `secret_arn` - (Optional) The Amazon Resource Name (ARN) of the MAC Security (MACsec) secret key to associate with the dedicated connection. + +~> **Note:** `ckn` and `cak` are mutually exclusive with `secret_arn` - these arguments cannot be used together. If you use `ckn` and `cak`, you should not use `secret_arn`. If you use the `secret_arn` argument to reference an existing MAC Security (MACSec) secret key, you should not use `ckn` or `cak`. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - ID of the MAC Security (MACSec) secret key resource. +* `start_on` - The date in UTC format that the MAC Security (MACsec) secret key takes effect. +* `state` - The state of the MAC Security (MACsec) secret key. The possible values are: associating, associated, disassociating, disassociated. See [MacSecKey](https://docs.aws.amazon.com/directconnect/latest/APIReference/API_MacSecKey.html#DX-Type-MacSecKey-state) for descriptions of each state.