Skip to content

Add transit_gateway_attachment_id to aws_dx_gateway_association #43436

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jul 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/43436.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_dx_gateway_association: Add `transit_gateway_attachment_id` attribute. This functionality requires the `ec2:DescribeTransitGatewayAttachments` IAM permission
```
1 change: 1 addition & 0 deletions internal/service/directconnect/exports_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ var (
FindMacSecKeyByTwoPartKey = findMacSecKeyByTwoPartKey
FindVirtualInterfaceByID = findVirtualInterfaceByID
GatewayAssociationStateUpgradeV0 = gatewayAssociationStateUpgradeV0
GatewayAssociationStateUpgradeV1 = gatewayAssociationStateUpgradeV1
)
60 changes: 55 additions & 5 deletions internal/service/directconnect/gateway_association.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,21 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/directconnect"
awstypes "github.com/aws/aws-sdk-go-v2/service/directconnect/types"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/hashicorp/aws-sdk-go-base/v2/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
"github.com/hashicorp/terraform-provider-aws/internal/enum"
"github.com/hashicorp/terraform-provider-aws/internal/errs"
"github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag"
tfec2 "github.com/hashicorp/terraform-provider-aws/internal/service/ec2"
tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/hashicorp/terraform-provider-aws/internal/verify"
"github.com/hashicorp/terraform-provider-aws/names"
)

// @SDKResource("aws_dx_gateway_association", name="Gateway Association")
Expand All @@ -38,13 +43,18 @@ func resourceGatewayAssociation() *schema.Resource {
StateContext: resourceGatewayAssociationImport,
},

SchemaVersion: 1,
SchemaVersion: 2,
StateUpgraders: []schema.StateUpgrader{
{
Type: resourceGatewayAssociationResourceV0().CoreConfigSchema().ImpliedType(),
Upgrade: gatewayAssociationStateUpgradeV0,
Version: 0,
},
{
Type: resourceGatewayAssociationResourceV1().CoreConfigSchema().ImpliedType(),
Upgrade: gatewayAssociationStateUpgradeV1,
Version: 1,
},
},

Schema: map[string]*schema.Schema{
Expand Down Expand Up @@ -95,6 +105,10 @@ func resourceGatewayAssociation() *schema.Resource {
ConflictsWith: []string{"associated_gateway_id"},
AtLeastOneOf: []string{"associated_gateway_id", "associated_gateway_owner_account_id", "proposal_id"},
},
names.AttrTransitGatewayAttachmentID: {
Type: schema.TypeString,
Computed: true,
},
},

Timeouts: &schema.ResourceTimeout{
Expand Down Expand Up @@ -166,10 +180,10 @@ func resourceGatewayAssociationCreate(ctx context.Context, d *schema.ResourceDat

func resourceGatewayAssociationRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
var diags diag.Diagnostics
conn := meta.(*conns.AWSClient).DirectConnectClient(ctx)
c := meta.(*conns.AWSClient)
conn := c.DirectConnectClient(ctx)

associationID := d.Get("dx_gateway_association_id").(string)

output, err := findGatewayAssociationByID(ctx, conn, associationID)

if !d.IsNewResource() && tfresource.NotFound(err) {
Expand All @@ -182,15 +196,30 @@ func resourceGatewayAssociationRead(ctx context.Context, d *schema.ResourceData,
return sdkdiag.AppendErrorf(diags, "reading Direct Connect Gateway Association (%s): %s", d.Id(), err)
}

associatedGatewayID, dxGatewayID := aws.ToString(output.AssociatedGateway.Id), aws.ToString(output.DirectConnectGatewayId)
if err := d.Set("allowed_prefixes", flattenRouteFilterPrefixes(output.AllowedPrefixesToDirectConnectGateway)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting allowed_prefixes: %s", err)
}
d.Set("associated_gateway_id", output.AssociatedGateway.Id)
d.Set("associated_gateway_id", associatedGatewayID)
d.Set("associated_gateway_owner_account_id", output.AssociatedGateway.OwnerAccount)
d.Set("associated_gateway_type", output.AssociatedGateway.Type)
d.Set("dx_gateway_association_id", output.AssociationId)
d.Set("dx_gateway_id", output.DirectConnectGatewayId)
d.Set("dx_gateway_id", dxGatewayID)
d.Set("dx_gateway_owner_account_id", output.DirectConnectGatewayOwnerAccount)
if output.AssociatedGateway.Type == awstypes.GatewayTypeTransitGateway {
transitGatewayAttachment, err := findTransitGatewayAttachmentForGateway(ctx, c.EC2Client(ctx), associatedGatewayID, dxGatewayID)

switch {
case tfawserr.ErrCodeEquals(err, "UnauthorizedOperation"):
d.Set(names.AttrTransitGatewayAttachmentID, nil)
case err != nil:
return sdkdiag.AppendErrorf(diags, "reading EC2 Transit Gateway (%s) Attachment (%s): %s", associatedGatewayID, dxGatewayID, err)
default:
d.Set(names.AttrTransitGatewayAttachmentID, transitGatewayAttachment.TransitGatewayAttachmentId)
}
} else {
d.Set(names.AttrTransitGatewayAttachmentID, nil)
}

return diags
}
Expand Down Expand Up @@ -361,6 +390,27 @@ func findGatewayAssociations(ctx context.Context, conn *directconnect.Client, in
return output, nil
}

func findTransitGatewayAttachmentForGateway(ctx context.Context, conn *ec2.Client, tgwID, dxGatewayID string) (*ec2types.TransitGatewayAttachment, error) {
input := ec2.DescribeTransitGatewayAttachmentsInput{
Filters: []ec2types.Filter{
{
Name: aws.String("resource-type"),
Values: enum.Slice(ec2types.TransitGatewayAttachmentResourceTypeDirectConnectGateway),
},
{
Name: aws.String("resource-id"),
Values: []string{dxGatewayID},
},
{
Name: aws.String("transit-gateway-id"),
Values: []string{tgwID},
},
},
}

return tfec2.FindTransitGatewayAttachment(ctx, conn, &input)
}

func statusGatewayAssociation(ctx context.Context, conn *directconnect.Client, id string) retry.StateRefreshFunc {
return func() (any, string, error) {
output, err := findGatewayAssociationByID(ctx, conn, id)
Expand Down
113 changes: 79 additions & 34 deletions internal/service/directconnect/gateway_association_migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import (
"log"

"github.com/aws/aws-sdk-go-v2/aws"
awstypes "github.com/aws/aws-sdk-go-v2/service/directconnect/types"
"github.com/hashicorp/aws-sdk-go-base/v2/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
"github.com/hashicorp/terraform-provider-aws/internal/verify"
"github.com/hashicorp/terraform-provider-aws/names"
)

func resourceGatewayAssociationResourceV0() *schema.Resource {
Expand All @@ -19,60 +21,79 @@ func resourceGatewayAssociationResourceV0() *schema.Resource {
"allowed_prefixes": {
Type: schema.TypeSet,
Optional: true,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
},

"associated_gateway_id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ConflictsWith: []string{"associated_gateway_owner_account_id", "proposal_id", "vpn_gateway_id"},
Type: schema.TypeString,
Optional: true,
},

"associated_gateway_owner_account_id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: verify.ValidAccountID,
ConflictsWith: []string{"associated_gateway_id", "vpn_gateway_id"},
Type: schema.TypeString,
Optional: true,
},

"associated_gateway_type": {
Type: schema.TypeString,
Computed: true,
Optional: true,
},

"dx_gateway_association_id": {
Type: schema.TypeString,
Computed: true,
Optional: true,
},

"dx_gateway_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Optional: true,
},

"dx_gateway_owner_account_id": {
Type: schema.TypeString,
Computed: true,
Optional: true,
},

"proposal_id": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ConflictsWith: []string{"associated_gateway_id", "vpn_gateway_id"},
Type: schema.TypeString,
Optional: true,
},

"vpn_gateway_id": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ConflictsWith: []string{"associated_gateway_id", "associated_gateway_owner_account_id", "proposal_id"},
Type: schema.TypeString,
Optional: true,
},
},
}
}

func resourceGatewayAssociationResourceV1() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"allowed_prefixes": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"associated_gateway_id": {
Type: schema.TypeString,
Optional: true,
},
"associated_gateway_owner_account_id": {
Type: schema.TypeString,
Optional: true,
},
"associated_gateway_type": {
Type: schema.TypeString,
Optional: true,
},
"dx_gateway_association_id": {
Type: schema.TypeString,
Optional: true,
},
"dx_gateway_id": {
Type: schema.TypeString,
Optional: true,
},
"dx_gateway_owner_account_id": {
Type: schema.TypeString,
Optional: true,
},
"proposal_id": {
Type: schema.TypeString,
Optional: true,
},
},
}
Expand All @@ -96,3 +117,27 @@ func gatewayAssociationStateUpgradeV0(ctx context.Context, rawState map[string]a

return rawState, nil
}

func gatewayAssociationStateUpgradeV1(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) {
conn := meta.(*conns.AWSClient).EC2Client(ctx)

log.Println("[INFO] Found Direct Connect Gateway Association state v1; migrating to v2")

// transit_gateway_attachment_id was introduced in v6.5.0, handle the case where it's not yet present.
if rawState["associated_gateway_type"].(string) == string(awstypes.GatewayTypeTransitGateway) {
if v, ok := rawState[names.AttrTransitGatewayAttachmentID]; !ok || v == nil {
output, err := findTransitGatewayAttachmentForGateway(ctx, conn, rawState["associated_gateway_id"].(string), rawState["dx_gateway_id"].(string))

switch {
case tfawserr.ErrCodeEquals(err, "UnauthorizedOperation"):
rawState[names.AttrTransitGatewayAttachmentID] = nil
case err != nil:
return nil, err
default:
rawState[names.AttrTransitGatewayAttachmentID] = aws.ToString(output.TransitGatewayAttachmentId)
}
}
}

return rawState, nil
}
62 changes: 62 additions & 0 deletions internal/service/directconnect/gateway_association_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ import (
awstypes "github.com/aws/aws-sdk-go-v2/service/directconnect/types"
sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/plancheck"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
"github.com/hashicorp/terraform-provider-aws/internal/acctest"
tfstatecheck "github.com/hashicorp/terraform-provider-aws/internal/acctest/statecheck"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
tfdirectconnect "github.com/hashicorp/terraform-provider-aws/internal/service/directconnect"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
Expand Down Expand Up @@ -46,6 +51,61 @@ func TestAccDirectConnectGatewayAssociation_v0StateUpgrade(t *testing.T) {
})
}

func TestAccDirectConnectGatewayAssociation_upgradeFromV6_4_0(t *testing.T) {
ctx := acctest.Context(t)
resourceName := "aws_dx_gateway_association.test"
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
rBgpAsn := sdkacctest.RandIntRange(64512, 65534)
var ga awstypes.DirectConnectGatewayAssociation
var gap awstypes.DirectConnectGatewayAssociationProposal

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.DirectConnectServiceID),
CheckDestroy: testAccCheckGatewayAssociationDestroy(ctx),
Steps: []resource.TestStep{
{
ExternalProviders: map[string]resource.ExternalProvider{
"aws": {
Source: "hashicorp/aws",
VersionConstraint: "6.4.0",
},
},
Config: testAccGatewayAssociationConfig_basicTransitSingleAccount(rName, rBgpAsn),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckGatewayAssociationExists(ctx, resourceName, &ga, &gap),
),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate),
},
},
ConfigStateChecks: []statecheck.StateCheck{
tfstatecheck.ExpectNoValue(resourceName, tfjsonpath.New(names.AttrTransitGatewayAttachmentID)),
},
},
{
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Config: testAccGatewayAssociationConfig_basicTransitSingleAccount(rName, rBgpAsn),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckGatewayAssociationExists(ctx, resourceName, &ga, &gap),
),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop),
},
PostApplyPostRefresh: []plancheck.PlanCheck{
plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop),
},
},
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTransitGatewayAttachmentID), knownvalue.NotNull()),
},
},
},
})
}

func TestAccDirectConnectGatewayAssociation_basicVPNGatewaySingleAccount(t *testing.T) {
ctx := acctest.Context(t)
resourceName := "aws_dx_gateway_association.test"
Expand Down Expand Up @@ -150,6 +210,7 @@ func TestAccDirectConnectGatewayAssociation_basicTransitGatewaySingleAccount(t *
resource.TestCheckResourceAttrSet(resourceName, "dx_gateway_association_id"),
resource.TestCheckResourceAttrPair(resourceName, "dx_gateway_id", resourceNameDxGw, names.AttrID),
acctest.CheckResourceAttrAccountID(ctx, resourceName, "dx_gateway_owner_account_id"),
resource.TestCheckResourceAttrSet(resourceName, names.AttrTransitGatewayAttachmentID),
),
},
{
Expand Down Expand Up @@ -192,6 +253,7 @@ func TestAccDirectConnectGatewayAssociation_basicTransitGatewayCrossAccount(t *t
resource.TestCheckResourceAttrPair(resourceName, "dx_gateway_id", resourceNameDxGw, names.AttrID),
// dx_gateway_owner_account_id is the "awsalternate" provider's account ID.
// acctest.CheckResourceAttrAccountID(ctx, resourceName, "dx_gateway_owner_account_id"),
resource.TestCheckResourceAttrSet(resourceName, names.AttrTransitGatewayAttachmentID),
),
},
},
Expand Down
1 change: 1 addition & 0 deletions internal/service/ec2/exports.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var (
FindSecurityGroupByNameAndVPCIDAndOwnerID = findSecurityGroupByNameAndVPCIDAndOwnerID
FindSecurityGroups = findSecurityGroups
FindSubnetByID = findSubnetByID
FindTransitGatewayAttachment = findTransitGatewayAttachment
FindVPCByID = findVPCByID
FindVPCEndpointByID = findVPCEndpointByID
NetworkInterfaceDetachedTimeout = networkInterfaceDetachedTimeout
Expand Down
1 change: 1 addition & 0 deletions website/docs/r/dx_gateway_association.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ This resource exports the following attributes in addition to the arguments abov
* `associated_gateway_type` - The type of the associated gateway, `transitGateway` or `virtualPrivateGateway`.
* `dx_gateway_association_id` - The ID of the Direct Connect gateway association.
* `dx_gateway_owner_account_id` - The ID of the AWS account that owns the Direct Connect gateway.
* `transit_gateway_attachment_id` - The ID of the Transit Gateway Attachment when the type is `transitGateway`.

## Timeouts

Expand Down
Loading