diff --git a/.changelog/27787.txt b/.changelog/27787.txt new file mode 100644 index 000000000000..86c0e3b517cc --- /dev/null +++ b/.changelog/27787.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_networkmanager_connect_attachment +``` \ No newline at end of file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index c2e0e840d4ff..edc9891284ca 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1809,6 +1809,7 @@ func New(_ context.Context) (*schema.Provider, error) { "aws_networkfirewall_rule_group": networkfirewall.ResourceRuleGroup(), "aws_networkmanager_attachment_accepter": networkmanager.ResourceAttachmentAccepter(), + "aws_networkmanager_connect_attachment": networkmanager.ResourceConnectAttachment(), "aws_networkmanager_connection": networkmanager.ResourceConnection(), "aws_networkmanager_customer_gateway_association": networkmanager.ResourceCustomerGatewayAssociation(), "aws_networkmanager_device": networkmanager.ResourceDevice(), diff --git a/internal/service/networkmanager/attachment_accepter.go b/internal/service/networkmanager/attachment_accepter.go index 8ee07c39baec..9bba59ebc5ee 100644 --- a/internal/service/networkmanager/attachment_accepter.go +++ b/internal/service/networkmanager/attachment_accepter.go @@ -46,6 +46,7 @@ func ResourceAttachmentAccepter() *schema.Resource { ValidateFunc: validation.StringInSlice([]string{ networkmanager.AttachmentTypeVpc, networkmanager.AttachmentTypeSiteToSiteVpn, + networkmanager.AttachmentTypeConnect, }, false), }, "core_network_arn": { @@ -110,6 +111,17 @@ func resourceAttachmentAccepterCreate(ctx context.Context, d *schema.ResourceDat d.SetId(attachmentID) + case networkmanager.AttachmentTypeConnect: + connectAttachment, err := FindConnectAttachmentByID(ctx, conn, attachmentID) + + if err != nil { + return diag.Errorf("reading Network Manager Connect Attachment (%s): %s", attachmentID, err) + } + + state = aws.StringValue(connectAttachment.Attachment.State) + + d.SetId(attachmentID) + default: return diag.Errorf("unsupported Network Manager Attachment type: %s", attachmentType) } @@ -135,6 +147,11 @@ func resourceAttachmentAccepterCreate(ctx context.Context, d *schema.ResourceDat if _, err := waitSiteToSiteVPNAttachmentAvailable(ctx, conn, attachmentID, d.Timeout(schema.TimeoutCreate)); err != nil { return diag.Errorf("waiting for Network Manager VPN Attachment (%s) create: %s", attachmentID, err) } + + case networkmanager.AttachmentTypeConnect: + if _, err := waitConnectAttachmentAvailable(ctx, conn, attachmentID, d.Timeout(schema.TimeoutCreate)); err != nil { + return diag.Errorf("waiting for Network Manager Connect Attachment (%s) create: %s", attachmentID, err) + } } } @@ -144,6 +161,8 @@ func resourceAttachmentAccepterCreate(ctx context.Context, d *schema.ResourceDat func resourceAttachmentAccepterRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).NetworkManagerConn + var a *networkmanager.Attachment + switch aType := d.Get("attachment_type"); aType { case networkmanager.AttachmentTypeVpc: vpcAttachment, err := FindVPCAttachmentByID(ctx, conn, d.Id()) @@ -158,15 +177,7 @@ func resourceAttachmentAccepterRead(ctx context.Context, d *schema.ResourceData, return diag.Errorf("reading Network Manager VPC Attachment (%s): %s", d.Id(), err) } - a := vpcAttachment.Attachment - d.Set("attachment_policy_rule_number", a.AttachmentPolicyRuleNumber) - d.Set("core_network_arn", a.CoreNetworkArn) - d.Set("core_network_id", a.CoreNetworkId) - d.Set("edge_location", a.EdgeLocation) - d.Set("owner_account_id", a.OwnerAccountId) - d.Set("resource_arn", a.ResourceArn) - d.Set("segment_name", a.SegmentName) - d.Set("state", a.State) + a = vpcAttachment.Attachment case networkmanager.AttachmentTypeSiteToSiteVpn: vpnAttachment, err := FindSiteToSiteVPNAttachmentByID(ctx, conn, d.Id()) @@ -181,16 +192,32 @@ func resourceAttachmentAccepterRead(ctx context.Context, d *schema.ResourceData, return diag.Errorf("reading Network Manager Site To Site VPN Attachment (%s): %s", d.Id(), err) } - a := vpnAttachment.Attachment - d.Set("attachment_policy_rule_number", a.AttachmentPolicyRuleNumber) - d.Set("core_network_arn", a.CoreNetworkArn) - d.Set("core_network_id", a.CoreNetworkId) - d.Set("edge_location", a.EdgeLocation) - d.Set("owner_account_id", a.OwnerAccountId) - d.Set("resource_arn", a.ResourceArn) - d.Set("segment_name", a.SegmentName) - d.Set("state", a.State) + a = vpnAttachment.Attachment + + case networkmanager.AttachmentTypeConnect: + connectAttachment, err := FindConnectAttachmentByID(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Network Manager Connect Attachment %s not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.Errorf("reading Network Manager Connect Attachment (%s): %s", d.Id(), err) + } + + a = connectAttachment.Attachment } + d.Set("attachment_policy_rule_number", a.AttachmentPolicyRuleNumber) + d.Set("core_network_arn", a.CoreNetworkArn) + d.Set("core_network_id", a.CoreNetworkId) + d.Set("edge_location", a.EdgeLocation) + d.Set("owner_account_id", a.OwnerAccountId) + d.Set("resource_arn", a.ResourceArn) + d.Set("segment_name", a.SegmentName) + d.Set("state", a.State) + return nil } diff --git a/internal/service/networkmanager/connect_attachment.go b/internal/service/networkmanager/connect_attachment.go new file mode 100644 index 000000000000..0607c8d0c043 --- /dev/null +++ b/internal/service/networkmanager/connect_attachment.go @@ -0,0 +1,422 @@ +package networkmanager + +import ( + "context" + "fmt" + "log" + "regexp" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/networkmanager" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "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" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +func ResourceConnectAttachment() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceConnectAttachmentCreate, + ReadWithoutTimeout: resourceConnectAttachmentRead, + UpdateWithoutTimeout: resourceConnectAttachmentUpdate, + DeleteWithoutTimeout: resourceConnectAttachmentDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + CustomizeDiff: verify.SetTagsDiff, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "attachment_id": { + Type: schema.TypeString, + Computed: true, + }, + "attachment_policy_rule_number": { + Type: schema.TypeInt, + Computed: true, + }, + "attachment_type": { + Type: schema.TypeString, + Computed: true, + }, + "core_network_arn": { + Type: schema.TypeString, + Computed: true, + }, + "core_network_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(0, 50), + validation.StringMatch(regexp.MustCompile(`^core-network-([0-9a-f]{8,17})$`), "Must start with core-network and then have 8 to 17 characters"), + ), + }, + "edge_location": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 63), + validation.StringMatch(regexp.MustCompile(`[\s\S]*`), "Anything but whitespace"), + ), + }, + "options": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "protocol": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"GRE"}, false), + }, + }, + }, + }, + "owner_account_id": { + Type: schema.TypeString, + Computed: true, + }, + "resource_arn": { + Type: schema.TypeString, + Computed: true, + }, + "segment_name": { + Type: schema.TypeString, + Computed: true, + }, + "state": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tftags.TagsSchema(), + "tags_all": tftags.TagsSchemaComputed(), + "transport_attachment_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(0, 50), + validation.StringMatch(regexp.MustCompile(`^attachment-([0-9a-f]{8,17})$`), "Must start with attachment- and then have 8 to 17 characters"), + ), + }, + }, + } +} + +func resourceConnectAttachmentCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).NetworkManagerConn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) + + coreNetworkID := d.Get("core_network_id").(string) + edgeLocation := d.Get("edge_location").(string) + transportAttachmentID := d.Get("transport_attachment_id").(string) + options := &networkmanager.ConnectAttachmentOptions{} + if v, ok := d.GetOk("options"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + options = expandConnectOptions(v.([]interface{})[0].(map[string]interface{})) + } + + input := &networkmanager.CreateConnectAttachmentInput{ + CoreNetworkId: aws.String(coreNetworkID), + EdgeLocation: aws.String(edgeLocation), + Options: options, + TransportAttachmentId: aws.String(transportAttachmentID), + } + + if len(tags) > 0 { + input.Tags = Tags(tags.IgnoreAWS()) + } + + outputRaw, err := tfresource.RetryWhenContext(ctx, d.Timeout(schema.TimeoutCreate), + func() (interface{}, error) { + return conn.CreateConnectAttachmentWithContext(ctx, input) + }, + func(err error) (bool, error) { + // Connect attachment doesn't have direct dependency to VPC attachment state when using Attachment Accepter. + // Waiting for Create Timeout period for VPC Attachment to come available state. + // Only needed if depends_on statement is not used in Connect attachment. + // + // ValidationException: Incorrect input. + // { + // RespMetadata: { + // StatusCode: 400, + // RequestID: "0a711cf7-2210-40c9-a170-a4c42134e195" + // }, + // Fields: [{ + // Message: "Transport attachment state is invalid.", + // Name: "transportAttachmentId" + // }], + // Message_: "Incorrect input.", + // Reason: "FieldValidationFailed" + // } + if validationExceptionMessageContains(err, networkmanager.ValidationExceptionReasonFieldValidationFailed, "Transport attachment state is invalid.") { + return true, err + } + + return false, err + }) + + if err != nil { + return diag.Errorf("creating Network Manager Connect Attachment (%s) (%s): %s", transportAttachmentID, coreNetworkID, err) + } + + d.SetId(aws.StringValue(outputRaw.(*networkmanager.CreateConnectAttachmentOutput).ConnectAttachment.Attachment.AttachmentId)) + + if _, err := waitConnectAttachmentCreated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + return diag.Errorf("waiting for Network Manager Connect Attachment (%s) create: %s", d.Id(), err) + } + + return resourceConnectAttachmentRead(ctx, d, meta) +} + +func resourceConnectAttachmentRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).NetworkManagerConn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + + connectAttachment, err := FindConnectAttachmentByID(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Network Manager Connect Attachment %s not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.Errorf("reading Network Manager Connect Attachment (%s): %s", d.Id(), err) + } + + a := connectAttachment.Attachment + arn := arn.ARN{ + Partition: meta.(*conns.AWSClient).Partition, + Service: "networkmanager", + AccountID: meta.(*conns.AWSClient).AccountID, + Resource: fmt.Sprintf("attachment/%s", d.Id()), + }.String() + d.Set("arn", arn) + d.Set("attachment_policy_rule_number", a.AttachmentPolicyRuleNumber) + d.Set("attachment_id", a.AttachmentId) + d.Set("attachment_type", a.AttachmentType) + d.Set("core_network_arn", a.CoreNetworkArn) + d.Set("core_network_id", a.CoreNetworkId) + d.Set("edge_location", a.EdgeLocation) + if connectAttachment.Options != nil { + if err := d.Set("options", []interface{}{flattenConnectOptions(connectAttachment.Options)}); err != nil { + return diag.Errorf("setting options: %s", err) + } + } else { + d.Set("options", nil) + } + d.Set("owner_account_id", a.OwnerAccountId) + d.Set("resource_arn", a.ResourceArn) + d.Set("segment_name", a.SegmentName) + d.Set("state", a.State) + d.Set("transport_attachment_id", connectAttachment.TransportAttachmentId) + + tags := KeyValueTags(a.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return diag.Errorf("setting tags: %s", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return diag.Errorf("setting tags_all: %s", err) + } + + return nil +} + +func resourceConnectAttachmentUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).NetworkManagerConn + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + + if err := UpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return diag.FromErr(fmt.Errorf("updating Network Manager Connect Attachment (%s) tags: %s", d.Get("arn").(string), err)) + } + } + + return resourceConnectAttachmentRead(ctx, d, meta) +} + +func resourceConnectAttachmentDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).NetworkManagerConn + + // If ResourceAttachmentAccepter is used, then Connect Attachment state + // is never updated from StatePendingAttachmentAcceptance and the delete fails + output, err := FindConnectAttachmentByID(ctx, conn, d.Id()) + + if tfawserr.ErrCodeEquals(err, networkmanager.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return diag.Errorf("reading Network Manager Connect Attachment (%s): %s", d.Id(), err) + } + + if state := aws.StringValue(output.Attachment.State); state == networkmanager.AttachmentStatePendingAttachmentAcceptance || state == networkmanager.AttachmentStatePendingTagAcceptance { + return diag.Errorf("cannot delete Network Manager Connect Attachment (%s) in state: %s", d.Id(), state) + } + + log.Printf("[DEBUG] Deleting Network Manager Connect Attachment: %s", d.Id()) + _, err = conn.DeleteAttachmentWithContext(ctx, &networkmanager.DeleteAttachmentInput{ + AttachmentId: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, networkmanager.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return diag.Errorf("deleting Network Manager Connect Attachment (%s): %s", d.Id(), err) + } + + if _, err := waitConnectAttachmentDeleted(ctx, conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil { + return diag.Errorf("waiting for Network Manager Connect Attachment (%s) delete: %s", d.Id(), err) + } + + return nil +} + +func FindConnectAttachmentByID(ctx context.Context, conn *networkmanager.NetworkManager, id string) (*networkmanager.ConnectAttachment, error) { + input := &networkmanager.GetConnectAttachmentInput{ + AttachmentId: aws.String(id), + } + + output, err := conn.GetConnectAttachmentWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, networkmanager.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.ConnectAttachment == nil || output.ConnectAttachment.Attachment == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.ConnectAttachment, nil +} + +func statusConnectAttachmentState(ctx context.Context, conn *networkmanager.NetworkManager, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := FindConnectAttachmentByID(ctx, conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Attachment.State), nil + } +} + +func waitConnectAttachmentCreated(ctx context.Context, conn *networkmanager.NetworkManager, id string, timeout time.Duration) (*networkmanager.ConnectAttachment, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{networkmanager.AttachmentStateCreating, networkmanager.AttachmentStatePendingNetworkUpdate}, + Target: []string{networkmanager.AttachmentStateAvailable, networkmanager.AttachmentStatePendingAttachmentAcceptance}, + Timeout: timeout, + Refresh: statusConnectAttachmentState(ctx, conn, id), + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*networkmanager.ConnectAttachment); ok { + return output, err + } + + return nil, err +} + +func waitConnectAttachmentDeleted(ctx context.Context, conn *networkmanager.NetworkManager, id string, timeout time.Duration) (*networkmanager.ConnectAttachment, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{networkmanager.AttachmentStateDeleting}, + Target: []string{}, + Timeout: timeout, + Refresh: statusConnectAttachmentState(ctx, conn, id), + NotFoundChecks: 1, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*networkmanager.ConnectAttachment); ok { + return output, err + } + + return nil, err +} + +func waitConnectAttachmentAvailable(ctx context.Context, conn *networkmanager.NetworkManager, id string, timeout time.Duration) (*networkmanager.ConnectAttachment, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{networkmanager.AttachmentStateCreating, networkmanager.AttachmentStatePendingNetworkUpdate, networkmanager.AttachmentStatePendingAttachmentAcceptance}, + Target: []string{networkmanager.AttachmentStateAvailable}, + Timeout: timeout, + Refresh: statusConnectAttachmentState(ctx, conn, id), + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*networkmanager.ConnectAttachment); ok { + return output, err + } + + return nil, err +} + +func expandConnectOptions(o map[string]interface{}) *networkmanager.ConnectAttachmentOptions { + if o == nil { + return nil + } + + object := &networkmanager.ConnectAttachmentOptions{} + + if v, ok := o["protocol"].(string); ok { + object.Protocol = aws.String(v) + } + + return object +} + +func flattenConnectOptions(apiObject *networkmanager.ConnectAttachmentOptions) map[string]interface{} { // nosemgrep:ci.caps5-in-func-name + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.Protocol; v != nil { + tfMap["protocol"] = aws.StringValue(v) + } + + return tfMap +} diff --git a/internal/service/networkmanager/connect_attachment_test.go b/internal/service/networkmanager/connect_attachment_test.go new file mode 100644 index 000000000000..0142240eda74 --- /dev/null +++ b/internal/service/networkmanager/connect_attachment_test.go @@ -0,0 +1,459 @@ +package networkmanager_test + +import ( + "context" + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/service/networkmanager" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfnetworkmanager "github.com/hashicorp/terraform-provider-aws/internal/service/networkmanager" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func TestAccNetworkManagerConnectAttachment_basic(t *testing.T) { + var v networkmanager.ConnectAttachment + resourceName := "aws_networkmanager_connect_attachment.test" + testExternalProviders := map[string]resource.ExternalProvider{ + "awscc": { + Source: "hashicorp/awscc", + VersionConstraint: "0.29.0", + }, + } + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, networkmanager.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ExternalProviders: testExternalProviders, + CheckDestroy: testAccCheckConnectAttachmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccConnectAttachmentConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckConnectAttachmentExists(resourceName, &v), + acctest.MatchResourceAttrGlobalARN(resourceName, "arn", "networkmanager", regexp.MustCompile(`attachment/.+`)), + resource.TestCheckResourceAttr(resourceName, "attachment_type", "CONNECT"), + resource.TestCheckResourceAttrSet(resourceName, "core_network_id"), + resource.TestCheckResourceAttr(resourceName, "edge_location", acctest.Region()), + acctest.CheckResourceAttrAccountID(resourceName, "owner_account_id"), + resource.TestCheckResourceAttr(resourceName, "segment_name", "shared"), + resource.TestCheckResourceAttrSet(resourceName, "state"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccNetworkManagerConnectAttachment_basic_NoDependsOn(t *testing.T) { + var v networkmanager.ConnectAttachment + resourceName := "aws_networkmanager_connect_attachment.test" + testExternalProviders := map[string]resource.ExternalProvider{ + "awscc": { + Source: "hashicorp/awscc", + VersionConstraint: "0.29.0", + }, + } + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, networkmanager.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ExternalProviders: testExternalProviders, + CheckDestroy: testAccCheckConnectAttachmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccConnectAttachmentConfig_basic_NoDependsOn(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckConnectAttachmentExists(resourceName, &v), + acctest.MatchResourceAttrGlobalARN(resourceName, "arn", "networkmanager", regexp.MustCompile(`attachment/.+`)), + resource.TestCheckResourceAttr(resourceName, "attachment_type", "CONNECT"), + resource.TestCheckResourceAttrSet(resourceName, "core_network_id"), + resource.TestCheckResourceAttr(resourceName, "edge_location", acctest.Region()), + acctest.CheckResourceAttrAccountID(resourceName, "owner_account_id"), + resource.TestCheckResourceAttr(resourceName, "segment_name", "shared"), + resource.TestCheckResourceAttrSet(resourceName, "state"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccNetworkManagerConnectAttachment_disappears(t *testing.T) { + var v networkmanager.ConnectAttachment + resourceName := "aws_networkmanager_connect_attachment.test" + testExternalProviders := map[string]resource.ExternalProvider{ + "awscc": { + Source: "hashicorp/awscc", + VersionConstraint: "0.29.0", + }, + } + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, networkmanager.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ExternalProviders: testExternalProviders, + CheckDestroy: testAccCheckConnectAttachmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccConnectAttachmentConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckConnectAttachmentExists(resourceName, &v), + acctest.CheckResourceDisappears(acctest.Provider, tfnetworkmanager.ResourceConnectAttachment(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccNetworkManagerConnectAttachment_tags(t *testing.T) { + var v networkmanager.ConnectAttachment + resourceName := "aws_networkmanager_connect_attachment.test" + testExternalProviders := map[string]resource.ExternalProvider{ + "awscc": { + Source: "hashicorp/awscc", + VersionConstraint: "0.29.0", + }, + } + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, networkmanager.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ExternalProviders: testExternalProviders, + CheckDestroy: testAccCheckConnectAttachmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccConnectAttachmentConfig_tags1(rName, "segment", "shared"), + Check: resource.ComposeTestCheckFunc( + testAccCheckConnectAttachmentExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.segment", "shared"), + ), + }, + { + Config: testAccConnectAttachmentConfig_tags2(rName, "segment", "shared", "Name", "test"), + Check: resource.ComposeTestCheckFunc( + testAccCheckConnectAttachmentExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.segment", "shared"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", "test"), + ), + }, + { + Config: testAccConnectAttachmentConfig_tags1(rName, "segment", "shared"), + Check: resource.ComposeTestCheckFunc( + testAccCheckConnectAttachmentExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.segment", "shared"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckConnectAttachmentExists(n string, v *networkmanager.ConnectAttachment) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Network Manager Connect Attachment ID is set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).NetworkManagerConn + + output, err := tfnetworkmanager.FindConnectAttachmentByID(context.Background(), conn, rs.Primary.ID) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccCheckConnectAttachmentDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).NetworkManagerConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_networkmanager_connect_attachment" { + continue + } + + _, err := tfnetworkmanager.FindConnectAttachmentByID(context.Background(), conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Network Manager Connect Attachment %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccConnectAttachmentConfig_base(rName string) string { + return acctest.ConfigCompose(acctest.ConfigAvailableAZsNoOptIn(), fmt.Sprintf(` +data "aws_region" "current" {} + +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + + assign_generated_ipv6_cidr_block = true + + tags = { + Name = %[1]q + } +} + +resource "aws_subnet" "test" { + count = 2 + + vpc_id = aws_vpc.test.id + availability_zone = data.aws_availability_zones.available.names[count.index] + cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 8, count.index) + + ipv6_cidr_block = cidrsubnet(aws_vpc.test.ipv6_cidr_block, 8, count.index) + assign_ipv6_address_on_creation = true + + tags = { + Name = %[1]q + } +} + +resource "aws_networkmanager_global_network" "test" { + tags = { + Name = %[1]q + } +} + +resource "awscc_networkmanager_core_network" "test" { + global_network_id = aws_networkmanager_global_network.test.id + policy_document = jsonencode(jsondecode(data.aws_networkmanager_core_network_policy_document.test.json)) +} + +data "aws_networkmanager_core_network_policy_document" "test" { + core_network_configuration { + vpn_ecmp_support = false + asn_ranges = ["64512-64555"] + edge_locations { + location = data.aws_region.current.name + asn = 64512 + } + } + segments { + name = "shared" + description = "SegmentForSharedServices" + require_attachment_acceptance = true + } + segment_actions { + action = "share" + mode = "attachment-route" + segment = "shared" + share_with = ["*"] + } + attachment_policies { + rule_number = 1 + condition_logic = "or" + conditions { + type = "tag-value" + operator = "equals" + key = "segment" + value = "shared" + } + action { + association_method = "constant" + segment = "shared" + } + } +} + +`, rName)) +} + +func testAccConnectAttachmentConfig_basic(rName string) string { + return acctest.ConfigCompose(testAccConnectAttachmentConfig_base(rName), ` +resource "aws_networkmanager_vpc_attachment" "test" { + subnet_arns = aws_subnet.test[*].arn + core_network_id = awscc_networkmanager_core_network.test.id + vpc_arn = aws_vpc.test.arn + tags = { + segment = "shared" + } +} + +resource "aws_networkmanager_attachment_accepter" "test" { + attachment_id = aws_networkmanager_vpc_attachment.test.id + attachment_type = aws_networkmanager_vpc_attachment.test.attachment_type +} + +resource "aws_networkmanager_connect_attachment" "test" { + core_network_id = awscc_networkmanager_core_network.test.id + transport_attachment_id = aws_networkmanager_vpc_attachment.test.id + edge_location = aws_networkmanager_vpc_attachment.test.edge_location + options { + protocol = "GRE" + } + tags = { + segment = "shared" + } + depends_on = [ + "aws_networkmanager_attachment_accepter.test" + ] +} + +resource "aws_networkmanager_attachment_accepter" "test2" { + attachment_id = aws_networkmanager_connect_attachment.test.id + attachment_type = aws_networkmanager_connect_attachment.test.attachment_type +} +`) +} + +func testAccConnectAttachmentConfig_basic_NoDependsOn(rName string) string { + return acctest.ConfigCompose(testAccConnectAttachmentConfig_base(rName), ` +resource "aws_networkmanager_vpc_attachment" "test" { + subnet_arns = aws_subnet.test[*].arn + core_network_id = awscc_networkmanager_core_network.test.id + vpc_arn = aws_vpc.test.arn + tags = { + segment = "shared" + } +} + +resource "aws_networkmanager_attachment_accepter" "test" { + attachment_id = aws_networkmanager_vpc_attachment.test.id + attachment_type = aws_networkmanager_vpc_attachment.test.attachment_type +} + +resource "aws_networkmanager_connect_attachment" "test" { + core_network_id = awscc_networkmanager_core_network.test.id + transport_attachment_id = aws_networkmanager_vpc_attachment.test.id + edge_location = aws_networkmanager_vpc_attachment.test.edge_location + options { + protocol = "GRE" + } + tags = { + segment = "shared" + } +} + +resource "aws_networkmanager_attachment_accepter" "test2" { + attachment_id = aws_networkmanager_connect_attachment.test.id + attachment_type = aws_networkmanager_connect_attachment.test.attachment_type +} +`) +} + +func testAccConnectAttachmentConfig_tags1(rName, tagKey1, tagValue1 string) string { + return acctest.ConfigCompose(testAccConnectAttachmentConfig_base(rName), fmt.Sprintf(` +resource "aws_networkmanager_vpc_attachment" "test" { + subnet_arns = [aws_subnet.test[0].arn] + core_network_id = awscc_networkmanager_core_network.test.id + vpc_arn = aws_vpc.test.arn + tags = { + segment = "shared" + } +} + +resource "aws_networkmanager_attachment_accepter" "test" { + attachment_id = aws_networkmanager_vpc_attachment.test.id + attachment_type = aws_networkmanager_vpc_attachment.test.attachment_type +} + +resource "aws_networkmanager_connect_attachment" "test" { + core_network_id = awscc_networkmanager_core_network.test.id + transport_attachment_id = aws_networkmanager_vpc_attachment.test.id + edge_location = aws_networkmanager_vpc_attachment.test.edge_location + options { + protocol = "GRE" + } + depends_on = [ + "aws_networkmanager_attachment_accepter.test" + ] + tags = { + %[1]q = %[2]q + } +} + +resource "aws_networkmanager_attachment_accepter" "test2" { + attachment_id = aws_networkmanager_connect_attachment.test.id + attachment_type = aws_networkmanager_connect_attachment.test.attachment_type +} +`, tagKey1, tagValue1)) +} + +func testAccConnectAttachmentConfig_tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return acctest.ConfigCompose(testAccConnectAttachmentConfig_base(rName), fmt.Sprintf(` +resource "aws_networkmanager_vpc_attachment" "test" { + subnet_arns = [aws_subnet.test[0].arn] + core_network_id = awscc_networkmanager_core_network.test.id + vpc_arn = aws_vpc.test.arn + tags = { + segment = "shared" + } +} + +resource "aws_networkmanager_attachment_accepter" "test" { + attachment_id = aws_networkmanager_vpc_attachment.test.id + attachment_type = aws_networkmanager_vpc_attachment.test.attachment_type +} + +resource "aws_networkmanager_connect_attachment" "test" { + core_network_id = awscc_networkmanager_core_network.test.id + transport_attachment_id = aws_networkmanager_vpc_attachment.test.id + edge_location = aws_networkmanager_vpc_attachment.test.edge_location + options { + protocol = "GRE" + } + depends_on = [ + "aws_networkmanager_attachment_accepter.test" + ] + tags = { + %[1]q = %[2]q + %[3]q = %[4]q + } +} + +resource "aws_networkmanager_attachment_accepter" "test2" { + attachment_id = aws_networkmanager_connect_attachment.test.id + attachment_type = aws_networkmanager_connect_attachment.test.attachment_type +} +`, tagKey1, tagValue1, tagKey2, tagValue2)) +} diff --git a/internal/service/networkmanager/vpc_attachment.go b/internal/service/networkmanager/vpc_attachment.go index a4ca51928c48..1cbc7f01d927 100644 --- a/internal/service/networkmanager/vpc_attachment.go +++ b/internal/service/networkmanager/vpc_attachment.go @@ -269,6 +269,20 @@ func resourceVPCAttachmentUpdate(ctx context.Context, d *schema.ResourceData, me func resourceVPCAttachmentDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).NetworkManagerConn + // If ResourceAttachmentAccepter is used, then VPC Attachment state + // is not updated from StatePendingAttachmentAcceptance and the delete fails if deleted immediately after create + output, sErr := FindVPCAttachmentByID(ctx, conn, d.Id()) + + if tfawserr.ErrCodeEquals(sErr, networkmanager.ErrCodeResourceNotFoundException) { + return nil + } + + if sErr != nil { + return diag.Errorf("reading Network Manager VPC Attachment (%s): %s", d.Id(), sErr) + } + + d.Set("state", output.Attachment.State) + if state := d.Get("state").(string); state == networkmanager.AttachmentStatePendingAttachmentAcceptance || state == networkmanager.AttachmentStatePendingTagAcceptance { return diag.Errorf("cannot delete Network Manager VPC Attachment (%s) in %s state", d.Id(), state) } diff --git a/website/docs/r/networkmanager_connect_attachment.html.markdown b/website/docs/r/networkmanager_connect_attachment.html.markdown new file mode 100644 index 000000000000..e70c363c2e69 --- /dev/null +++ b/website/docs/r/networkmanager_connect_attachment.html.markdown @@ -0,0 +1,102 @@ +--- +subcategory: "Network Manager" +layout: "aws" +page_title: "AWS: aws_networkmanager_connect_attachment" +description: |- + Terraform resource for managing an AWS NetworkManager ConnectAttachment. +--- + +# Resource: aws_networkmanager_connect_attachment + +Terraform resource for managing an AWS NetworkManager ConnectAttachment. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_networkmanager_vpc_attachment" "example" { + subnet_arns = aws_subnet.example[*].arn + core_network_id = awscc_networkmanager_core_network.example.id + vpc_arn = aws_vpc.example.arn +} + +resource "aws_networkmanager_connect_attachment" "example" { + core_network_id = awscc_networkmanager_core_network.example.id + transport_attachment_id = aws_networkmanager_vpc_attachment.example.id + edge_location = aws_networkmanager_vpc_attachment.example.edge_location + options { + protocol = "GRE" + } +} +``` + +### Usage with attachment accepter + +```terraform +resource "aws_networkmanager_vpc_attachment" "example" { + subnet_arns = aws_subnet.example[*].arn + core_network_id = awscc_networkmanager_core_network.example.id + vpc_arn = aws_vpc.example.arn +} + +resource "aws_networkmanager_attachment_accepter" "example" { + attachment_id = aws_networkmanager_vpc_attachment.example.id + attachment_type = aws_networkmanager_vpc_attachment.example.attachment_type +} + +resource "aws_networkmanager_connect_attachment" "example" { + core_network_id = awscc_networkmanager_core_network.example.id + transport_attachment_id = aws_networkmanager_vpc_attachment.example.id + edge_location = aws_networkmanager_vpc_attachment.example.edge_location + options { + protocol = "GRE" + } + depends_on = [ + "aws_networkmanager_attachment_accepter.test" + ] +} + +resource "aws_networkmanager_attachment_accepter" "example2" { + attachment_id = aws_networkmanager_connect_attachment.example.id + attachment_type = aws_networkmanager_connect_attachment.example.attachment_type +} +``` + +## Argument Reference + +The following arguments are required: + +- `core_network_id` - (Required) The ID of a core network where you want to create the attachment. +- `transport_attachment_id` - (Required) The ID of the attachment between the two connections. +- `edge_location` - (Required) The Region where the edge is located. +- `options` - (Required) Options for creating an attachment. + +The following arguments are optional: + +- `tags` - (Optional) Key-value tags for the attachment. 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 + +In addition to all arguments above, the following attributes are exported: + +- `arn` - The ARN of the attachment. +- `attachment_policy_rule_number` - The policy rule number associated with the attachment. +- `attachment_type` - The type of attachment. +- `core_network_arn` - The ARN of a core network. +- `core_network_id` - The ID of a core network +- `edge_location` - The Region where the edge is located. +- `id` - The ID of the attachment. +- `owner_account_id` - The ID of the attachment account owner. +- `resource_arn` - The attachment resource ARN. +- `segment_name` - The name of the segment attachment. +- `state` - The state of the attachment. +- `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). + +## Import + +`aws_networkmanager_connect_attachment` can be imported using the attachment ID, e.g. + +``` +$ terraform import aws_networkmanager_connect_attachment.example attachment-0f8fa60d2238d1bd8 +```