opentofu/builtin/providers/aws/resource_aws_route_table.go
Phil Frost 9545f26fa0 Correct AWS VPC or route table read functions
If the state file contained a VPC or a route table which no longer
exists, Terraform would fail to create the correct plan, which is to
recreate them.

In the case of VPCs, this was due to incorrect error handling. The AWS
SDK returns a aws.APIError, not a *aws.APIError on error.  When the VPC
no longer exists, upon attempting to refresh state Terraform would
simply exit with an error.

For route tables, the provider would recognize that the route table no
longer existed, but would not make the appropriate call to update the
state as such. Thus there'd be no crash, but also no plan to re-create
the route table.
2015-03-20 13:38:41 -04:00

321 lines
8.1 KiB
Go

package aws
import (
"bytes"
"fmt"
"log"
"time"
"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/ec2"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsRouteTable() *schema.Resource {
return &schema.Resource{
Create: resourceAwsRouteTableCreate,
Read: resourceAwsRouteTableRead,
Update: resourceAwsRouteTableUpdate,
Delete: resourceAwsRouteTableDelete,
Schema: map[string]*schema.Schema{
"vpc_id": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"tags": tagsSchema(),
"route": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"cidr_block": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"gateway_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"instance_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"vpc_peering_connection_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
},
Set: resourceAwsRouteTableHash,
},
},
}
}
func resourceAwsRouteTableCreate(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
// Create the routing table
createOpts := &ec2.CreateRouteTableRequest{
VPCID: aws.String(d.Get("vpc_id").(string)),
}
log.Printf("[DEBUG] RouteTable create config: %#v", createOpts)
resp, err := ec2conn.CreateRouteTable(createOpts)
if err != nil {
return fmt.Errorf("Error creating route table: %s", err)
}
// Get the ID and store it
rt := resp.RouteTable
d.SetId(*rt.RouteTableID)
log.Printf("[INFO] Route Table ID: %s", d.Id())
// Wait for the route table to become available
log.Printf(
"[DEBUG] Waiting for route table (%s) to become available",
d.Id())
stateConf := &resource.StateChangeConf{
Pending: []string{"pending"},
Target: "ready",
Refresh: resourceAwsRouteTableStateRefreshFunc(ec2conn, d.Id()),
Timeout: 1 * time.Minute,
}
if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf(
"Error waiting for route table (%s) to become available: %s",
d.Id(), err)
}
return resourceAwsRouteTableUpdate(d, meta)
}
func resourceAwsRouteTableRead(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
rtRaw, _, err := resourceAwsRouteTableStateRefreshFunc(ec2conn, d.Id())()
if err != nil {
return err
}
if rtRaw == nil {
d.SetId("")
return nil
}
rt := rtRaw.(*ec2.RouteTable)
d.Set("vpc_id", rt.VPCID)
// Create an empty schema.Set to hold all routes
route := &schema.Set{F: resourceAwsRouteTableHash}
// Loop through the routes and add them to the set
for _, r := range rt.Routes {
if r.GatewayID != nil && *r.GatewayID == "local" {
continue
}
if r.Origin != nil && *r.Origin == "EnableVgwRoutePropagation" {
continue
}
m := make(map[string]interface{})
if r.DestinationCIDRBlock != nil {
m["cidr_block"] = *r.DestinationCIDRBlock
}
if r.GatewayID != nil {
m["gateway_id"] = *r.GatewayID
}
if r.InstanceID != nil {
m["instance_id"] = *r.InstanceID
}
if r.VPCPeeringConnectionID != nil {
m["vpc_peering_connection_id"] = *r.VPCPeeringConnectionID
}
route.Add(m)
}
d.Set("route", route)
// Tags
d.Set("tags", tagsToMap(rt.Tags))
return nil
}
func resourceAwsRouteTableUpdate(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
// Check if the route set as a whole has changed
if d.HasChange("route") {
o, n := d.GetChange("route")
ors := o.(*schema.Set).Difference(n.(*schema.Set))
nrs := n.(*schema.Set).Difference(o.(*schema.Set))
// Now first loop through all the old routes and delete any obsolete ones
for _, route := range ors.List() {
m := route.(map[string]interface{})
// Delete the route as it no longer exists in the config
log.Printf(
"[INFO] Deleting route from %s: %s",
d.Id(), m["cidr_block"].(string))
err := ec2conn.DeleteRoute(&ec2.DeleteRouteRequest{
RouteTableID: aws.String(d.Id()),
DestinationCIDRBlock: aws.String(m["cidr_block"].(string)),
})
if err != nil {
return err
}
}
// Make sure we save the state of the currently configured rules
routes := o.(*schema.Set).Intersection(n.(*schema.Set))
d.Set("route", routes)
// Then loop through al the newly configured routes and create them
for _, route := range nrs.List() {
m := route.(map[string]interface{})
opts := ec2.CreateRouteRequest{
RouteTableID: aws.String(d.Id()),
DestinationCIDRBlock: aws.String(m["cidr_block"].(string)),
GatewayID: aws.String(m["gateway_id"].(string)),
InstanceID: aws.String(m["instance_id"].(string)),
VPCPeeringConnectionID: aws.String(m["vpc_peering_connection_id"].(string)),
}
log.Printf("[INFO] Creating route for %s: %#v", d.Id(), opts)
if err := ec2conn.CreateRoute(&opts); err != nil {
return err
}
routes.Add(route)
d.Set("route", routes)
}
}
if err := setTags(ec2conn, d); err != nil {
return err
} else {
d.SetPartial("tags")
}
return resourceAwsRouteTableRead(d, meta)
}
func resourceAwsRouteTableDelete(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
// First request the routing table since we'll have to disassociate
// all the subnets first.
rtRaw, _, err := resourceAwsRouteTableStateRefreshFunc(ec2conn, d.Id())()
if err != nil {
return err
}
if rtRaw == nil {
return nil
}
rt := rtRaw.(*ec2.RouteTable)
// Do all the disassociations
for _, a := range rt.Associations {
log.Printf("[INFO] Disassociating association: %s", *a.RouteTableAssociationID)
err := ec2conn.DisassociateRouteTable(&ec2.DisassociateRouteTableRequest{
AssociationID: a.RouteTableAssociationID,
})
if err != nil {
return err
}
}
// Delete the route table
log.Printf("[INFO] Deleting Route Table: %s", d.Id())
err = ec2conn.DeleteRouteTable(&ec2.DeleteRouteTableRequest{
RouteTableID: aws.String(d.Id()),
})
if err != nil {
ec2err, ok := err.(aws.APIError)
if ok && ec2err.Code == "InvalidRouteTableID.NotFound" {
return nil
}
return fmt.Errorf("Error deleting route table: %s", err)
}
// Wait for the route table to really destroy
log.Printf(
"[DEBUG] Waiting for route table (%s) to become destroyed",
d.Id())
stateConf := &resource.StateChangeConf{
Pending: []string{"ready"},
Target: "",
Refresh: resourceAwsRouteTableStateRefreshFunc(ec2conn, d.Id()),
Timeout: 1 * time.Minute,
}
if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf(
"Error waiting for route table (%s) to become destroyed: %s",
d.Id(), err)
}
return nil
}
func resourceAwsRouteTableHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["cidr_block"].(string)))
if v, ok := m["gateway_id"]; ok {
buf.WriteString(fmt.Sprintf("%s-", v.(string)))
}
if v, ok := m["instance_id"]; ok {
buf.WriteString(fmt.Sprintf("%s-", v.(string)))
}
if v, ok := m["vpc_peering_connection_id"]; ok {
buf.WriteString(fmt.Sprintf("%s-", v.(string)))
}
return hashcode.String(buf.String())
}
// resourceAwsRouteTableStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
// a RouteTable.
func resourceAwsRouteTableStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
resp, err := conn.DescribeRouteTables(&ec2.DescribeRouteTablesRequest{
RouteTableIDs: []string{id},
})
if err != nil {
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidRouteTableID.NotFound" {
resp = nil
} else {
log.Printf("Error on RouteTableStateRefresh: %s", err)
return nil, "", err
}
}
if resp == nil {
// Sometimes AWS just has consistency issues and doesn't see
// our instance yet. Return an empty state.
return nil, "", nil
}
rt := &resp.RouteTables[0]
return rt, "ready", nil
}
}