Skip to content
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

vpc/route_table: Allow local inline routes #32794

Merged
merged 12 commits into from
Aug 3, 2023
3 changes: 3 additions & 0 deletions .changelog/32794.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_route_table: Allow an existing local route to be adopted or imported and the target to be updated
```
2 changes: 1 addition & 1 deletion internal/service/ec2/vpc_route_data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ resource "aws_instance" "test" {
resource "aws_route" "instance" {
route_table_id = aws_route_table.test.id
destination_cidr_block = "10.0.1.0/24"
instance_id = aws_instance.test.id
network_interface_id = aws_instance.test.primary_network_interface_id
}

data "aws_route" "by_peering_connection_id" {
Expand Down
66 changes: 62 additions & 4 deletions internal/service/ec2/vpc_route_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ func resourceRouteTableRead(ctx context.Context, d *schema.ResourceData, meta in
if err := d.Set("propagating_vgws", propagatingVGWs); err != nil {
return sdkdiag.AppendErrorf(diags, "setting propagating_vgws: %s", err)
}
if err := d.Set("route", flattenRoutes(ctx, conn, routeTable.Routes)); err != nil {
if err := d.Set("route", flattenRoutes(ctx, conn, d, routeTable.Routes)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting route: %s", err)
}
d.Set("vpc_id", routeTable.VpcId)
Expand Down Expand Up @@ -480,6 +480,25 @@ func routeTableAddRoute(ctx context.Context, conn *ec2.EC2, routeTableID string,

input.RouteTableId = aws.String(routeTableID)

_, target := routeTableRouteTargetAttribute(tfMap)

if target == gatewayIDLocal {
// created by AWS so probably doesn't need a retry but just to be sure
// we provide a small one
_, err := tfresource.RetryWhenAWSErrCodeEquals(ctx, time.Second*15,
func() (interface{}, error) {
return routeFinder(ctx, conn, routeTableID, destination)
},
errCodeInvalidRouteNotFound,
)

if tfresource.NotFound(err) {
return fmt.Errorf("local route cannot be created but must exist to be adopted, %s %s does not exist", target, destination)
}

return nil
}

_, err := tfresource.RetryWhenAWSErrCodeEquals(ctx, timeout,
func() (interface{}, error) {
return conn.CreateRouteWithContext(ctx, input)
Expand Down Expand Up @@ -719,7 +738,11 @@ func expandReplaceRouteInput(tfMap map[string]interface{}) *ec2.ReplaceRouteInpu
}

if v, ok := tfMap["gateway_id"].(string); ok && v != "" {
apiObject.GatewayId = aws.String(v)
if v == gatewayIDLocal {
apiObject.LocalTarget = aws.Bool(true)
} else {
apiObject.GatewayId = aws.String(v)
}
}

if v, ok := tfMap["local_gateway_id"].(string); ok && v != "" {
Expand Down Expand Up @@ -811,7 +834,7 @@ func flattenRoute(apiObject *ec2.Route) map[string]interface{} {
return tfMap
}

func flattenRoutes(ctx context.Context, conn *ec2.EC2, apiObjects []*ec2.Route) []interface{} {
func flattenRoutes(ctx context.Context, conn *ec2.EC2, d *schema.ResourceData, apiObjects []*ec2.Route) []interface{} {
if len(apiObjects) == 0 {
return nil
}
Expand All @@ -823,7 +846,13 @@ func flattenRoutes(ctx context.Context, conn *ec2.EC2, apiObjects []*ec2.Route)
continue
}

if gatewayID := aws.StringValue(apiObject.GatewayId); gatewayID == gatewayIDLocal || gatewayID == gatewayIDVPCLattice {
if gatewayID := aws.StringValue(apiObject.GatewayId); gatewayID == gatewayIDVPCLattice {
continue
}

// local routes from config need to be included but not default local routes, as determined by hasLocalConfig
// see local route tests
if gatewayID := aws.StringValue(apiObject.GatewayId); gatewayID == gatewayIDLocal && !hasLocalConfig(d, apiObject) {
continue
}

Expand Down Expand Up @@ -854,6 +883,35 @@ func flattenRoutes(ctx context.Context, conn *ec2.EC2, apiObjects []*ec2.Route)
return tfList
}

// hasLocalConfig along with flattenRoutes prevents default local routes from
// being stored in state but allows configured local routes to be stored in
// state. hasLocalConfig checks the ResourceData and flattenRoutes skips or
// includes the route. Normally, you can't count on ResourceData to represent
// config. However, in this case, a local gateway route in ResourceData must
// come from config because of the gatekeeping done by hasLocalConfig and
// flattenRoutes.
func hasLocalConfig(d *schema.ResourceData, apiObject *ec2.Route) bool {
if apiObject.GatewayId == nil {
return false
}
if v, ok := d.GetOk("route"); ok && v.(*schema.Set).Len() > 0 {
for _, v := range v.(*schema.Set).List() {
v := v.(map[string]interface{})
if v["cidr_block"].(string) != aws.StringValue(apiObject.DestinationCidrBlock) &&
v["destination_prefix_list_id"] != aws.StringValue(apiObject.DestinationPrefixListId) &&
v["ipv6_cidr_block"] != aws.StringValue(apiObject.DestinationIpv6CidrBlock) {
continue
}

if v["gateway_id"].(string) == gatewayIDLocal {
return true
}
}
}

return false
}

// routeTableRouteDestinationAttribute returns the attribute key and value of the route table route's destination.
func routeTableRouteDestinationAttribute(m map[string]interface{}) (string, string) {
for _, key := range routeTableValidDestinations {
Expand Down
Loading