diff --git a/.changelog/22420.txt b/.changelog/22420.txt new file mode 100644 index 00000000000..dd0b2531f20 --- /dev/null +++ b/.changelog/22420.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_security_group: Ensure that the Security Group is found 3 times in a row before declaring that it has been created +``` \ No newline at end of file diff --git a/internal/acctest/acctest.go b/internal/acctest/acctest.go index 2fec60ca912..ccf398fc60c 100644 --- a/internal/acctest/acctest.go +++ b/internal/acctest/acctest.go @@ -29,6 +29,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/provider" + tfec2 "github.com/hashicorp/terraform-provider-aws/internal/service/ec2" tforganizations "github.com/hashicorp/terraform-provider-aws/internal/service/organizations" tfsts "github.com/hashicorp/terraform-provider-aws/internal/service/sts" ) @@ -1791,7 +1792,7 @@ resource "aws_subnet" "test" { ) } -func CheckVPCExists(n string, vpc *ec2.Vpc) resource.TestCheckFunc { +func CheckVPCExists(n string, v *ec2.Vpc) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -1803,18 +1804,14 @@ func CheckVPCExists(n string, vpc *ec2.Vpc) resource.TestCheckFunc { } conn := Provider.Meta().(*conns.AWSClient).EC2Conn - DescribeVpcOpts := &ec2.DescribeVpcsInput{ - VpcIds: []*string{aws.String(rs.Primary.ID)}, - } - resp, err := conn.DescribeVpcs(DescribeVpcOpts) + + output, err := tfec2.FindVPCByID(conn, rs.Primary.ID) + if err != nil { return err } - if len(resp.Vpcs) == 0 || resp.Vpcs[0] == nil { - return fmt.Errorf("VPC not found") - } - *vpc = *resp.Vpcs[0] + *v = *output return nil } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index b05196f8a35..2137fa33d6d 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -467,7 +467,7 @@ func Provider() *schema.Provider { "aws_internet_gateway": ec2.DataSourceInternetGateway(), "aws_key_pair": ec2.DataSourceKeyPair(), "aws_launch_template": ec2.DataSourceLaunchTemplate(), - "aws_nat_gateway": ec2.DataSourceNatGateway(), + "aws_nat_gateway": ec2.DataSourceNATGateway(), "aws_network_acls": ec2.DataSourceNetworkACLs(), "aws_network_interface": ec2.DataSourceNetworkInterface(), "aws_network_interfaces": ec2.DataSourceNetworkInterfaces(), @@ -1110,7 +1110,7 @@ func Provider() *schema.Provider { "aws_key_pair": ec2.ResourceKeyPair(), "aws_launch_template": ec2.ResourceLaunchTemplate(), "aws_main_route_table_association": ec2.ResourceMainRouteTableAssociation(), - "aws_nat_gateway": ec2.ResourceNatGateway(), + "aws_nat_gateway": ec2.ResourceNATGateway(), "aws_network_acl": ec2.ResourceNetworkACL(), "aws_network_acl_rule": ec2.ResourceNetworkACLRule(), "aws_network_interface": ec2.ResourceNetworkInterface(), diff --git a/internal/service/ec2/core_acc_test.go b/internal/service/ec2/core_acc_test.go deleted file mode 100644 index e2ccd6db9da..00000000000 --- a/internal/service/ec2/core_acc_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package ec2_test - -import ( - "testing" - - "github.com/aws/aws-sdk-go/service/ec2" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-provider-aws/internal/acctest" -) - -func TestAccEC2CoreAcc_VPC_coreMismatchedDiffs(t *testing.T) { - var vpc ec2.Vpc - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), - Providers: acctest.Providers, - CheckDestroy: testAccCheckVpcDestroy, - Steps: []resource.TestStep{ - { - Config: testMatchedDiffs, - Check: resource.ComposeTestCheckFunc( - acctest.CheckVPCExists("aws_vpc.test", &vpc), - testAccCheckVpcCidr(&vpc, "10.0.0.0/16"), - resource.TestCheckResourceAttr( - "aws_vpc.test", "cidr_block", "10.0.0.0/16"), - ), - }, - }, - }) -} - -const testMatchedDiffs = ` -resource "aws_vpc" "test" { - cidr_block = "10.0.0.0/16" - - tags = { - Name = "terraform-testacc-repro-GH-4965" - } - - lifecycle { - ignore_changes = ["tags"] - } -}` diff --git a/internal/service/ec2/default_security_group.go b/internal/service/ec2/default_security_group.go index f630b84ab18..1bbdfd62994 100644 --- a/internal/service/ec2/default_security_group.go +++ b/internal/service/ec2/default_security_group.go @@ -18,8 +18,6 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/verify" ) -const DefaultSecurityGroupName = "default" - func ResourceDefaultSecurityGroup() *schema.Resource { //lintignore:R011 return &schema.Resource{ diff --git a/internal/service/ec2/default_vpc_test.go b/internal/service/ec2/default_vpc_test.go index a0b43bc08ff..fa43dfb6cf9 100644 --- a/internal/service/ec2/default_vpc_test.go +++ b/internal/service/ec2/default_vpc_test.go @@ -22,7 +22,7 @@ func TestAccEC2DefaultVPC_basic(t *testing.T) { Config: testAccDefaultVPCBasicConfig, Check: resource.ComposeTestCheckFunc( acctest.CheckVPCExists("aws_default_vpc.foo", &vpc), - testAccCheckVpcCidr(&vpc, "172.31.0.0/16"), + resource.TestCheckResourceAttr("aws_default_vpc.foo", "cidr_block", "172.31.0.0/16"), resource.TestCheckResourceAttr( "aws_default_vpc.foo", "cidr_block", "172.31.0.0/16"), resource.TestCheckResourceAttr( diff --git a/internal/service/ec2/egress_only_internet_gateway.go b/internal/service/ec2/egress_only_internet_gateway.go index 6ed0b5e4965..c819e654257 100644 --- a/internal/service/ec2/egress_only_internet_gateway.go +++ b/internal/service/ec2/egress_only_internet_gateway.go @@ -3,11 +3,10 @@ package ec2 import ( "fmt" "log" - "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/conns" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" @@ -21,6 +20,7 @@ func ResourceEgressOnlyInternetGateway() *schema.Resource { Read: resourceEgressOnlyInternetGatewayRead, Update: resourceEgressOnlyInternetGatewayUpdate, Delete: resourceEgressOnlyInternetGatewayDelete, + Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, @@ -28,13 +28,13 @@ func ResourceEgressOnlyInternetGateway() *schema.Resource { CustomizeDiff: verify.SetTagsDiff, Schema: map[string]*schema.Schema{ + "tags": tftags.TagsSchema(), + "tags_all": tftags.TagsSchemaComputed(), "vpc_id": { Type: schema.TypeString, Required: true, ForceNew: true, }, - "tags": tftags.TagsSchema(), - "tags_all": tftags.TagsSchemaComputed(), }, } } @@ -44,15 +44,19 @@ func resourceEgressOnlyInternetGatewayCreate(d *schema.ResourceData, meta interf defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) - resp, err := conn.CreateEgressOnlyInternetGateway(&ec2.CreateEgressOnlyInternetGatewayInput{ - VpcId: aws.String(d.Get("vpc_id").(string)), + input := &ec2.CreateEgressOnlyInternetGatewayInput{ TagSpecifications: ec2TagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeEgressOnlyInternetGateway), - }) + VpcId: aws.String(d.Get("vpc_id").(string)), + } + + log.Printf("[DEBUG] Creating EC2 Egress-only Internet Gateway: %s", input) + output, err := conn.CreateEgressOnlyInternetGateway(input) + if err != nil { - return fmt.Errorf("Error creating egress internet gateway: %s", err) + return fmt.Errorf("error creating EC2 Egress-only Internet Gateway: %w", err) } - d.SetId(aws.StringValue(resp.EgressOnlyInternetGateway.EgressOnlyInternetGatewayId)) + d.SetId(aws.StringValue(output.EgressOnlyInternetGateway.EgressOnlyInternetGatewayId)) return resourceEgressOnlyInternetGatewayRead(d, meta) } @@ -62,44 +66,29 @@ func resourceEgressOnlyInternetGatewayRead(d *schema.ResourceData, meta interfac defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig - var req = &ec2.DescribeEgressOnlyInternetGatewaysInput{ - EgressOnlyInternetGatewayIds: []*string{aws.String(d.Id())}, - } + outputRaw, err := tfresource.RetryWhenNewResourceNotFound(PropagationTimeout, func() (interface{}, error) { + return FindEgressOnlyInternetGatewayByID(conn, d.Id()) + }, d.IsNewResource()) - var resp *ec2.DescribeEgressOnlyInternetGatewaysOutput - err := resource.Retry(1*time.Minute, func() *resource.RetryError { - var err error - resp, err = conn.DescribeEgressOnlyInternetGateways(req) - if err != nil { - return resource.NonRetryableError(err) - } - - igw := getEc2EgressOnlyInternetGateway(d.Id(), resp) - if d.IsNewResource() && igw == nil { - return resource.RetryableError(fmt.Errorf("Egress Only Internet Gateway (%s) not found.", d.Id())) - } + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] EC2 Egress-only Internet Gateway %s not found, removing from state", d.Id()) + d.SetId("") return nil - }) - if tfresource.TimedOut(err) { - resp, err = conn.DescribeEgressOnlyInternetGateways(req) } if err != nil { - return fmt.Errorf("Error describing egress internet gateway: %s", err) + return fmt.Errorf("error reading EC2 Egress-only Internet Gateway (%s): %w", d.Id(), err) } - igw := getEc2EgressOnlyInternetGateway(d.Id(), resp) - if igw == nil { - log.Printf("[Error] Cannot find Egress Only Internet Gateway: %q", d.Id()) - d.SetId("") - return nil - } + ig := outputRaw.(*ec2.EgressOnlyInternetGateway) - if len(igw.Attachments) == 1 && aws.StringValue(igw.Attachments[0].State) == ec2.AttachmentStatusAttached { - d.Set("vpc_id", igw.Attachments[0].VpcId) + if len(ig.Attachments) == 1 && aws.StringValue(ig.Attachments[0].State) == ec2.AttachmentStatusAttached { + d.Set("vpc_id", ig.Attachments[0].VpcId) + } else { + d.Set("vpc_id", nil) } - tags := KeyValueTags(igw.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + tags := KeyValueTags(ig.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) //lintignore:AWSR002 if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { @@ -113,17 +102,6 @@ func resourceEgressOnlyInternetGatewayRead(d *schema.ResourceData, meta interfac return nil } -func getEc2EgressOnlyInternetGateway(id string, resp *ec2.DescribeEgressOnlyInternetGatewaysOutput) *ec2.EgressOnlyInternetGateway { - if resp != nil && len(resp.EgressOnlyInternetGateways) > 0 { - for _, igw := range resp.EgressOnlyInternetGateways { - if aws.StringValue(igw.EgressOnlyInternetGatewayId) == id { - return igw - } - } - } - return nil -} - func resourceEgressOnlyInternetGatewayUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn @@ -131,7 +109,7 @@ func resourceEgressOnlyInternetGatewayUpdate(d *schema.ResourceData, meta interf o, n := d.GetChange("tags_all") if err := UpdateTags(conn, d.Id(), o, n); err != nil { - return fmt.Errorf("error updating Egress Only Internet Gateway (%s) tags: %s", d.Id(), err) + return fmt.Errorf("error updating EC2 Egress-only Internet Gateway (%s) tags: %w", d.Id(), err) } } @@ -141,11 +119,17 @@ func resourceEgressOnlyInternetGatewayUpdate(d *schema.ResourceData, meta interf func resourceEgressOnlyInternetGatewayDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn + log.Printf("[INFO] Deleting EC2 Egress-only Internet Gateway: %s", d.Id()) _, err := conn.DeleteEgressOnlyInternetGateway(&ec2.DeleteEgressOnlyInternetGatewayInput{ EgressOnlyInternetGatewayId: aws.String(d.Id()), }) + + if tfawserr.ErrCodeEquals(err, ErrCodeInvalidGatewayIDNotFound) { + return nil + } + if err != nil { - return fmt.Errorf("Error deleting egress internet gateway: %s", err) + return fmt.Errorf("error deleting EC2 Egress-only Internet Gateway (%s): %w", d.Id(), err) } return nil diff --git a/internal/service/ec2/egress_only_internet_gateway_test.go b/internal/service/ec2/egress_only_internet_gateway_test.go index 3095a4bdb93..4cf4bd39423 100644 --- a/internal/service/ec2/egress_only_internet_gateway_test.go +++ b/internal/service/ec2/egress_only_internet_gateway_test.go @@ -4,17 +4,20 @@ import ( "fmt" "testing" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" + 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" + tfec2 "github.com/hashicorp/terraform-provider-aws/internal/service/ec2" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) func TestAccEC2EgressOnlyInternetGateway_basic(t *testing.T) { - var igw ec2.EgressOnlyInternetGateway + var v ec2.EgressOnlyInternetGateway resourceName := "aws_egress_only_internet_gateway.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -23,9 +26,9 @@ func TestAccEC2EgressOnlyInternetGateway_basic(t *testing.T) { CheckDestroy: testAccCheckEgressOnlyInternetGatewayDestroy, Steps: []resource.TestStep{ { - Config: testAccEgressOnlyInternetGatewayConfig_basic, + Config: testAccEgressOnlyInternetGatewayConfig(rName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckEgressOnlyInternetGatewayExists(resourceName, &igw), + testAccCheckEgressOnlyInternetGatewayExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), ), }, @@ -41,6 +44,7 @@ func TestAccEC2EgressOnlyInternetGateway_basic(t *testing.T) { func TestAccEC2EgressOnlyInternetGateway_tags(t *testing.T) { var v ec2.EgressOnlyInternetGateway resourceName := "aws_egress_only_internet_gateway.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -49,7 +53,7 @@ func TestAccEC2EgressOnlyInternetGateway_tags(t *testing.T) { CheckDestroy: testAccCheckEgressOnlyInternetGatewayDestroy, Steps: []resource.TestStep{ { - Config: testAccEgressOnlyInternetGatewayTags1Config("key1", "value1"), + Config: testAccEgressOnlyInternetGatewayTags1Config(rName, "key1", "value1"), Check: resource.ComposeTestCheckFunc( testAccCheckEgressOnlyInternetGatewayExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), @@ -62,7 +66,7 @@ func TestAccEC2EgressOnlyInternetGateway_tags(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccEgressOnlyInternetGatewayTags2Config("key1", "value1updated", "key2", "value2"), + Config: testAccEgressOnlyInternetGatewayTags2Config(rName, "key1", "value1updated", "key2", "value2"), Check: resource.ComposeTestCheckFunc( testAccCheckEgressOnlyInternetGatewayExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), @@ -71,7 +75,7 @@ func TestAccEC2EgressOnlyInternetGateway_tags(t *testing.T) { ), }, { - Config: testAccEgressOnlyInternetGatewayTags1Config("key2", "value2"), + Config: testAccEgressOnlyInternetGatewayTags1Config(rName, "key2", "value2"), Check: resource.ComposeTestCheckFunc( testAccCheckEgressOnlyInternetGatewayExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), @@ -90,24 +94,23 @@ func testAccCheckEgressOnlyInternetGatewayDestroy(s *terraform.State) error { continue } - describe, err := conn.DescribeEgressOnlyInternetGateways(&ec2.DescribeEgressOnlyInternetGatewaysInput{ - EgressOnlyInternetGatewayIds: []*string{aws.String(rs.Primary.ID)}, - }) + _, err := tfec2.FindEgressOnlyInternetGatewayByID(conn, rs.Primary.ID) - if err == nil { - if len(describe.EgressOnlyInternetGateways) != 0 && - *describe.EgressOnlyInternetGateways[0].EgressOnlyInternetGatewayId == rs.Primary.ID { - return fmt.Errorf("Egress Only Internet Gateway %q still exists", rs.Primary.ID) - } + if tfresource.NotFound(err) { + continue } - return nil + if err != nil { + return err + } + + return fmt.Errorf("EC2 Egress-only Internet Gateway %s still exists", rs.Primary.ID) } return nil } -func testAccCheckEgressOnlyInternetGatewayExists(n string, igw *ec2.EgressOnlyInternetGateway) resource.TestCheckFunc { +func testAccCheckEgressOnlyInternetGatewayExists(n string, v *ec2.EgressOnlyInternetGateway) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -115,48 +118,47 @@ func testAccCheckEgressOnlyInternetGatewayExists(n string, igw *ec2.EgressOnlyIn } if rs.Primary.ID == "" { - return fmt.Errorf("No Egress Only IGW ID is set") + return fmt.Errorf("No EC2 Egress-only Internet Gateway ID is set") } conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn - resp, err := conn.DescribeEgressOnlyInternetGateways(&ec2.DescribeEgressOnlyInternetGatewaysInput{ - EgressOnlyInternetGatewayIds: []*string{aws.String(rs.Primary.ID)}, - }) + + output, err := tfec2.FindEgressOnlyInternetGatewayByID(conn, rs.Primary.ID) + if err != nil { return err } - if len(resp.EgressOnlyInternetGateways) == 0 { - return fmt.Errorf("Egress Only IGW not found") - } - *igw = *resp.EgressOnlyInternetGateways[0] + *v = *output return nil } } -const testAccEgressOnlyInternetGatewayConfig_basic = ` +func testAccEgressOnlyInternetGatewayConfig(rName string) string { + return fmt.Sprintf(` resource "aws_vpc" "test" { cidr_block = "10.1.0.0/16" assign_generated_ipv6_cidr_block = true tags = { - Name = "terraform-testacc-egress-only-igw-basic" + Name = %[1]q } } resource "aws_egress_only_internet_gateway" "test" { vpc_id = aws_vpc.test.id } -` +`, rName) +} -func testAccEgressOnlyInternetGatewayTags1Config(tagKey1, tagValue1 string) string { +func testAccEgressOnlyInternetGatewayTags1Config(rName, tagKey1, tagValue1 string) string { return fmt.Sprintf(` resource "aws_vpc" "test" { cidr_block = "10.1.0.0/16" tags = { - Name = "terraform-testacc-egress-only-igw-tags" + Name = %[1]q } } @@ -164,19 +166,19 @@ resource "aws_egress_only_internet_gateway" "test" { vpc_id = aws_vpc.test.id tags = { - %[1]q = %[2]q + %[2]q = %[3]q } } -`, tagKey1, tagValue1) +`, rName, tagKey1, tagValue1) } -func testAccEgressOnlyInternetGatewayTags2Config(tagKey1, tagValue1, tagKey2, tagValue2 string) string { +func testAccEgressOnlyInternetGatewayTags2Config(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { return fmt.Sprintf(` resource "aws_vpc" "test" { cidr_block = "10.1.0.0/16" tags = { - Name = "terraform-testacc-egress-only-igw-tags" + Name = %[1]q } } @@ -184,9 +186,9 @@ resource "aws_egress_only_internet_gateway" "test" { vpc_id = aws_vpc.test.id tags = { - %[1]q = %[2]q - %[3]q = %[4]q + %[2]q = %[3]q + %[4]q = %[5]q } } -`, tagKey1, tagValue1, tagKey2, tagValue2) +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) } diff --git a/internal/service/ec2/enum.go b/internal/service/ec2/enum.go index 5e0a4ee57a9..468f225cea4 100644 --- a/internal/service/ec2/enum.go +++ b/internal/service/ec2/enum.go @@ -67,3 +67,15 @@ func VpnConnectionType_Values() []string { VpnConnectionTypeIpsec1, } } + +const ( + AmazonIPv6PoolID = "Amazon" +) + +const ( + DefaultDHCPOptionsID = "default" +) + +const ( + DefaultSecurityGroupName = "default" +) diff --git a/internal/service/ec2/errors.go b/internal/service/ec2/errors.go index a394bfe59e1..07fcdc45706 100644 --- a/internal/service/ec2/errors.go +++ b/internal/service/ec2/errors.go @@ -10,6 +10,7 @@ import ( ) const ( + ErrCodeAuthFailure = "AuthFailure" ErrCodeClientInvalidHostIDNotFound = "Client.InvalidHostID.NotFound" ErrCodeClientVpnAssociationIdNotFound = "InvalidClientVpnAssociationId.NotFound" ErrCodeClientVpnAuthorizationRuleNotFound = "InvalidClientVpnEndpointAuthorizationRuleNotFound" @@ -22,12 +23,15 @@ const ( ErrCodeInvalidAttachmentIDNotFound = "InvalidAttachmentID.NotFound" ErrCodeInvalidCarrierGatewayIDNotFound = "InvalidCarrierGatewayID.NotFound" ErrCodeInvalidCustomerGatewayIDNotFound = "InvalidCustomerGatewayID.NotFound" + ErrCodeInvalidDhcpOptionIDNotFound = "InvalidDhcpOptionID.NotFound" ErrCodeInvalidFlowLogIdNotFound = "InvalidFlowLogId.NotFound" + ErrCodeInvalidGatewayIDNotFound = "InvalidGatewayID.NotFound" ErrCodeInvalidGroupNotFound = "InvalidGroup.NotFound" ErrCodeInvalidHostIDNotFound = "InvalidHostID.NotFound" ErrCodeInvalidInstanceIDNotFound = "InvalidInstanceID.NotFound" ErrCodeInvalidInternetGatewayIDNotFound = "InvalidInternetGatewayID.NotFound" ErrCodeInvalidKeyPairNotFound = "InvalidKeyPair.NotFound" + ErrCodeInvalidNetworkAclIDNotFound = "InvalidNetworkAclID.NotFound" ErrCodeInvalidNetworkInterfaceIDNotFound = "InvalidNetworkInterfaceID.NotFound" ErrCodeInvalidParameter = "InvalidParameter" ErrCodeInvalidParameterException = "InvalidParameterException" @@ -48,6 +52,7 @@ const ( ErrCodeInvalidSubnetIdNotFound = "InvalidSubnetId.NotFound" ErrCodeInvalidTransitGatewayAttachmentIDNotFound = "InvalidTransitGatewayAttachmentID.NotFound" ErrCodeInvalidTransitGatewayIDNotFound = "InvalidTransitGatewayID.NotFound" + ErrCodeInvalidVpcCidrBlockAssociationIDNotFound = "InvalidVpcCidrBlockAssociationID.NotFound" ErrCodeInvalidVpcEndpointIdNotFound = "InvalidVpcEndpointId.NotFound" ErrCodeInvalidVpcEndpointNotFound = "InvalidVpcEndpoint.NotFound" ErrCodeInvalidVpcEndpointServiceIdNotFound = "InvalidVpcEndpointServiceId.NotFound" @@ -56,6 +61,8 @@ const ( ErrCodeInvalidVpnConnectionIDNotFound = "InvalidVpnConnectionID.NotFound" ErrCodeInvalidVpnGatewayAttachmentNotFound = "InvalidVpnGatewayAttachment.NotFound" ErrCodeInvalidVpnGatewayIDNotFound = "InvalidVpnGatewayID.NotFound" + ErrCodeNatGatewayNotFound = "NatGatewayNotFound" + ErrCodeUnsupportedOperation = "UnsupportedOperation" ) func UnsuccessfulItemError(apiObject *ec2.UnsuccessfulItemError) error { diff --git a/internal/service/ec2/find.go b/internal/service/ec2/find.go index 830aab530a2..36d22abab64 100644 --- a/internal/service/ec2/find.go +++ b/internal/service/ec2/find.go @@ -154,6 +154,57 @@ func FindInstanceByID(conn *ec2.EC2, id string) (*ec2.Instance, error) { return output.Reservations[0].Instances[0], nil } +func FindNetworkACL(conn *ec2.EC2, input *ec2.DescribeNetworkAclsInput) (*ec2.NetworkAcl, error) { + output, err := FindNetworkACLs(conn, input) + + if err != nil { + return nil, err + } + + if len(output) == 0 || output[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + return output[0], nil +} + +func FindNetworkACLs(conn *ec2.EC2, input *ec2.DescribeNetworkAclsInput) ([]*ec2.NetworkAcl, error) { + var output []*ec2.NetworkAcl + + err := conn.DescribeNetworkAclsPages(input, func(page *ec2.DescribeNetworkAclsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.NetworkAcls { + if v == nil { + continue + } + + output = append(output, v) + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, ErrCodeInvalidNetworkAclIDNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return output, nil +} + // FindNetworkACLByID looks up a NetworkAcl by ID. When not found, returns nil and potentially an API error. func FindNetworkACLByID(conn *ec2.EC2, id string) (*ec2.NetworkAcl, error) { input := &ec2.DescribeNetworkAclsInput{ @@ -183,6 +234,8 @@ func FindNetworkACLByID(conn *ec2.EC2, id string) (*ec2.NetworkAcl, error) { } return nil, nil + + // TODO: Layer on top of FindNetworkACL and modify callers to handle NotFoundError. } // FindNetworkACLEntry looks up a FindNetworkACLEntry by Network ACL ID, Egress, and Rule Number. When not found, returns nil and potentially an API error. @@ -446,9 +499,6 @@ func FindRouteTableByID(conn *ec2.EC2, routeTableID string) (*ec2.RouteTable, er return FindRouteTable(conn, input) } -// FindRouteTable returns the route table corresponding to the specified input. -// Returns EmptyResultError if no route table is found or TooManyResultsError if more than 1 -// matching route table is found. func FindRouteTable(conn *ec2.EC2, input *ec2.DescribeRouteTablesInput) (*ec2.RouteTable, error) { output, err := FindRouteTables(conn, input) @@ -467,8 +517,6 @@ func FindRouteTable(conn *ec2.EC2, input *ec2.DescribeRouteTablesInput) (*ec2.Ro return output[0], nil } -// FindRouteTables returns an array of route tables for the specified input. -// Returns NotFoundError if no route table is found for a specified route table ID. func FindRouteTables(conn *ec2.EC2, input *ec2.DescribeRouteTablesInput) ([]*ec2.RouteTable, error) { var output []*ec2.RouteTable @@ -565,12 +613,25 @@ func FindRouteByPrefixListIDDestination(conn *ec2.EC2, routeTableID, prefixListI } } -// FindSecurityGroupByID looks up a security group by ID. Returns a resource.NotFoundError if not found. func FindSecurityGroupByID(conn *ec2.EC2, id string) (*ec2.SecurityGroup, error) { input := &ec2.DescribeSecurityGroupsInput{ GroupIds: aws.StringSlice([]string{id}), } - return FindSecurityGroup(conn, input) + + output, err := FindSecurityGroup(conn, input) + + if err != nil { + return nil, err + } + + // Eventual consistency check. + if aws.StringValue(output.GroupId) != id { + return nil, &resource.NotFoundError{ + LastRequest: input, + } + } + + return output, nil } // FindSecurityGroupByNameAndVPCID looks up a security group by name and VPC ID. Returns a resource.NotFoundError if not found. @@ -605,8 +666,6 @@ func FindSecurityGroup(conn *ec2.EC2, input *ec2.DescribeSecurityGroupsInput) (* return output[0], nil } -// FindSecurityGroups returns an array of security groups that match an ec2.DescribeSecurityGroupsInput. -// Returns a resource.NotFoundError if no group is found for a specified SecurityGroup or SecurityGroupId. func FindSecurityGroups(conn *ec2.EC2, input *ec2.DescribeSecurityGroupsInput) ([]*ec2.SecurityGroup, error) { var output []*ec2.SecurityGroup @@ -615,19 +674,18 @@ func FindSecurityGroups(conn *ec2.EC2, input *ec2.DescribeSecurityGroupsInput) ( return !lastPage } - for _, sg := range page.SecurityGroups { - if sg == nil { + for _, v := range page.SecurityGroups { + if v == nil { continue } - output = append(output, sg) + output = append(output, v) } return !lastPage }) - if tfawserr.ErrCodeEquals(err, ErrCodeInvalidSecurityGroupIDNotFound) || - tfawserr.ErrCodeEquals(err, ErrCodeInvalidGroupNotFound) { + if tfawserr.ErrCodeEquals(err, ErrCodeInvalidGroupNotFound, ErrCodeInvalidSecurityGroupIDNotFound) { return nil, &resource.NotFoundError{ LastError: err, LastRequest: input, @@ -893,8 +951,7 @@ func FindTransitGatewayRouteTablePropagation(conn *ec2.EC2, transitGatewayRouteT return result, nil } -// FindVPCAttribute looks up a VPC attribute. -func FindVPCAttribute(conn *ec2.EC2, vpcID string, attribute string) (*bool, error) { +func FindVPCAttribute(conn *ec2.EC2, vpcID string, attribute string) (bool, error) { input := &ec2.DescribeVpcAttributeInput{ Attribute: aws.String(attribute), VpcId: aws.String(vpcID), @@ -902,61 +959,284 @@ func FindVPCAttribute(conn *ec2.EC2, vpcID string, attribute string) (*bool, err output, err := conn.DescribeVpcAttribute(input) + if tfawserr.ErrCodeEquals(err, ErrCodeInvalidVpcIDNotFound) { + return false, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { - return nil, err + return false, err } if output == nil { - return nil, nil + return false, tfresource.NewEmptyResultError(input) } + var v *ec2.AttributeBooleanValue switch attribute { case ec2.VpcAttributeNameEnableDnsHostnames: - if output.EnableDnsHostnames == nil { - return nil, nil + v = output.EnableDnsHostnames + case ec2.VpcAttributeNameEnableDnsSupport: + v = output.EnableDnsSupport + default: + return false, fmt.Errorf("unsupported VPC attribute: %s", attribute) + } + + if v == nil { + return false, tfresource.NewEmptyResultError(input) + } + + return aws.BoolValue(v.Value), nil +} + +func FindVPCClassicLinkEnabled(conn *ec2.EC2, vpcID string) (bool, error) { + input := &ec2.DescribeVpcClassicLinkInput{ + VpcIds: aws.StringSlice([]string{vpcID}), + } + + output, err := conn.DescribeVpcClassicLink(input) + + if tfawserr.ErrCodeEquals(err, ErrCodeInvalidVpcIDNotFound, ErrCodeUnsupportedOperation) { + return false, &resource.NotFoundError{ + LastError: err, + LastRequest: input, } + } - return output.EnableDnsHostnames.Value, nil - case ec2.VpcAttributeNameEnableDnsSupport: - if output.EnableDnsSupport == nil { - return nil, nil + if err != nil { + return false, err + } + + if output == nil || len(output.Vpcs) == 0 || output.Vpcs[0] == nil { + return false, tfresource.NewEmptyResultError(input) + } + + if count := len(output.Vpcs); count > 1 { + return false, tfresource.NewTooManyResultsError(count, input) + } + + vpc := output.Vpcs[0] + + // Eventual consistency check. + if aws.StringValue(vpc.VpcId) != vpcID { + return false, &resource.NotFoundError{ + LastRequest: input, + } + } + + return aws.BoolValue(vpc.ClassicLinkEnabled), nil +} + +func FindVPCClassicLinkDnsSupported(conn *ec2.EC2, vpcID string) (bool, error) { + input := &ec2.DescribeVpcClassicLinkDnsSupportInput{ + VpcIds: aws.StringSlice([]string{vpcID}), + } + + output, err := conn.DescribeVpcClassicLinkDnsSupport(input) + + if tfawserr.ErrCodeEquals(err, ErrCodeInvalidVpcIDNotFound, ErrCodeUnsupportedOperation) || + tfawserr.ErrMessageContains(err, ErrCodeAuthFailure, "This request has been administratively disabled") { + return false, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return false, err + } + + if output == nil || len(output.Vpcs) == 0 || output.Vpcs[0] == nil { + return false, tfresource.NewEmptyResultError(input) + } + + if count := len(output.Vpcs); count > 1 { + return false, tfresource.NewTooManyResultsError(count, input) + } + + vpc := output.Vpcs[0] + + // Eventual consistency check. + if aws.StringValue(vpc.VpcId) != vpcID { + return false, &resource.NotFoundError{ + LastRequest: input, } + } - return output.EnableDnsSupport.Value, nil + return aws.BoolValue(vpc.ClassicLinkDnsSupported), nil +} + +func FindVPC(conn *ec2.EC2, input *ec2.DescribeVpcsInput) (*ec2.Vpc, error) { + output, err := FindVPCs(conn, input) + + if err != nil { + return nil, err + } + + if len(output) == 0 || output[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) } - return nil, fmt.Errorf("unimplemented VPC attribute: %s", attribute) + return output[0], nil +} + +func FindVPCs(conn *ec2.EC2, input *ec2.DescribeVpcsInput) ([]*ec2.Vpc, error) { + var output []*ec2.Vpc + + err := conn.DescribeVpcsPages(input, func(page *ec2.DescribeVpcsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.Vpcs { + if v != nil { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, ErrCodeInvalidVpcIDNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return output, nil } -// FindVPCByID looks up a Vpc by ID. When not found, returns nil and potentially an API error. func FindVPCByID(conn *ec2.EC2, id string) (*ec2.Vpc, error) { input := &ec2.DescribeVpcsInput{ VpcIds: aws.StringSlice([]string{id}), } - output, err := conn.DescribeVpcs(input) + output, err := FindVPC(conn, input) if err != nil { return nil, err } - if output == nil { - return nil, nil + // Eventual consistency check. + if aws.StringValue(output.VpcId) != id { + return nil, &resource.NotFoundError{ + LastRequest: input, + } } - for _, vpc := range output.Vpcs { - if vpc == nil { - continue + return output, nil +} + +func FindVPCDHCPOptionsAssociation(conn *ec2.EC2, vpcID string, dhcpOptionsID string) error { + vpc, err := FindVPCByID(conn, vpcID) + + if err != nil { + return err + } + + if aws.StringValue(vpc.DhcpOptionsId) != dhcpOptionsID { + return &resource.NotFoundError{ + LastError: fmt.Errorf("EC2 VPC (%s) DHCP Options Set (%s) Association not found", vpcID, dhcpOptionsID), } + } - if aws.StringValue(vpc.VpcId) != id { - continue + return nil +} + +func FindVPCCIDRBlockAssociationByID(conn *ec2.EC2, id string) (*ec2.VpcCidrBlockAssociation, *ec2.Vpc, error) { + input := &ec2.DescribeVpcsInput{ + Filters: BuildAttributeFilterList(map[string]string{ + "cidr-block-association.association-id": id, + }), + } + + vpc, err := FindVPC(conn, input) + + if err != nil { + return nil, nil, err + } + + for _, association := range vpc.CidrBlockAssociationSet { + if aws.StringValue(association.AssociationId) == id { + if state := aws.StringValue(association.CidrBlockState.State); state == ec2.VpcCidrBlockStateCodeDisassociated { + return nil, nil, &resource.NotFoundError{Message: state} + } + + return association, vpc, nil } + } + + return nil, nil, &resource.NotFoundError{} +} + +func FindVPCIPv6CIDRBlockAssociationByID(conn *ec2.EC2, id string) (*ec2.VpcIpv6CidrBlockAssociation, *ec2.Vpc, error) { + input := &ec2.DescribeVpcsInput{ + Filters: BuildAttributeFilterList(map[string]string{ + "ipv6-cidr-block-association.association-id": id, + }), + } + + vpc, err := FindVPC(conn, input) + + if err != nil { + return nil, nil, err + } + + for _, association := range vpc.Ipv6CidrBlockAssociationSet { + if aws.StringValue(association.AssociationId) == id { + if state := aws.StringValue(association.Ipv6CidrBlockState.State); state == ec2.VpcCidrBlockStateCodeDisassociated { + return nil, nil, &resource.NotFoundError{Message: state} + } - return vpc, nil + return association, vpc, nil + } } - return nil, nil + return nil, nil, &resource.NotFoundError{} +} + +func FindVPCDefaultNetworkACL(conn *ec2.EC2, id string) (*ec2.NetworkAcl, error) { + input := &ec2.DescribeNetworkAclsInput{ + Filters: BuildAttributeFilterList(map[string]string{ + "default": "true", + "vpc-id": id, + }), + } + + return FindNetworkACL(conn, input) +} + +func FindVPCDefaultSecurityGroup(conn *ec2.EC2, id string) (*ec2.SecurityGroup, error) { + input := &ec2.DescribeSecurityGroupsInput{ + Filters: BuildAttributeFilterList(map[string]string{ + "group-name": DefaultSecurityGroupName, + "vpc-id": id, + }), + } + + return FindSecurityGroup(conn, input) +} + +func FindVPCMainRouteTable(conn *ec2.EC2, id string) (*ec2.RouteTable, error) { + input := &ec2.DescribeRouteTablesInput{ + Filters: BuildAttributeFilterList(map[string]string{ + "association.main": "true", + "vpc-id": id, + }), + } + + return FindRouteTable(conn, input) } // FindVPCEndpointByID returns the VPC endpoint corresponding to the specified identifier. @@ -1363,6 +1643,139 @@ func FindTransitGatewayAttachmentByID(conn *ec2.EC2, id string) (*ec2.TransitGat return output, nil } +func FindDHCPOptions(conn *ec2.EC2, input *ec2.DescribeDhcpOptionsInput) (*ec2.DhcpOptions, error) { + output, err := FindDHCPOptionses(conn, input) + + if err != nil { + return nil, err + } + + if len(output) == 0 || output[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + return output[0], nil +} + +func FindDHCPOptionses(conn *ec2.EC2, input *ec2.DescribeDhcpOptionsInput) ([]*ec2.DhcpOptions, error) { + var output []*ec2.DhcpOptions + + err := conn.DescribeDhcpOptionsPages(input, func(page *ec2.DescribeDhcpOptionsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.DhcpOptions { + if v != nil { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, ErrCodeInvalidDhcpOptionIDNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return output, nil +} + +func FindDHCPOptionsByID(conn *ec2.EC2, id string) (*ec2.DhcpOptions, error) { + input := &ec2.DescribeDhcpOptionsInput{ + DhcpOptionsIds: aws.StringSlice([]string{id}), + } + + output, err := FindDHCPOptions(conn, input) + + if err != nil { + return nil, err + } + + // Eventual consistency check. + if aws.StringValue(output.DhcpOptionsId) != id { + return nil, &resource.NotFoundError{ + LastRequest: input, + } + } + + return output, nil +} + +func FindEgressOnlyInternetGateway(conn *ec2.EC2, input *ec2.DescribeEgressOnlyInternetGatewaysInput) (*ec2.EgressOnlyInternetGateway, error) { + output, err := FindEgressOnlyInternetGateways(conn, input) + + if err != nil { + return nil, err + } + + if len(output) == 0 || output[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + return output[0], nil +} + +func FindEgressOnlyInternetGateways(conn *ec2.EC2, input *ec2.DescribeEgressOnlyInternetGatewaysInput) ([]*ec2.EgressOnlyInternetGateway, error) { + var output []*ec2.EgressOnlyInternetGateway + + err := conn.DescribeEgressOnlyInternetGatewaysPages(input, func(page *ec2.DescribeEgressOnlyInternetGatewaysOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.EgressOnlyInternetGateways { + if v != nil { + output = append(output, v) + } + } + + return !lastPage + }) + + if err != nil { + return nil, err + } + + return output, nil +} + +func FindEgressOnlyInternetGatewayByID(conn *ec2.EC2, id string) (*ec2.EgressOnlyInternetGateway, error) { + input := &ec2.DescribeEgressOnlyInternetGatewaysInput{ + EgressOnlyInternetGatewayIds: aws.StringSlice([]string{id}), + } + + output, err := FindEgressOnlyInternetGateway(conn, input) + + if err != nil { + return nil, err + } + + // Eventual consistency check. + if aws.StringValue(output.EgressOnlyInternetGatewayId) != id { + return nil, &resource.NotFoundError{ + LastRequest: input, + } + } + + return output, nil +} + func FindFlowLogByID(conn *ec2.EC2, id string) (*ec2.FlowLog, error) { input := &ec2.DescribeFlowLogsInput{ FlowLogIds: aws.StringSlice([]string{id}), @@ -1626,6 +2039,83 @@ func FindManagedPrefixListEntryByIDAndCIDR(conn *ec2.EC2, id, cidr string) (*ec2 return nil, &resource.NotFoundError{} } +func FindNATGateway(conn *ec2.EC2, input *ec2.DescribeNatGatewaysInput) (*ec2.NatGateway, error) { + output, err := FindNATGateways(conn, input) + + if err != nil { + return nil, err + } + + if len(output) == 0 || output[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + return output[0], nil +} + +func FindNATGateways(conn *ec2.EC2, input *ec2.DescribeNatGatewaysInput) ([]*ec2.NatGateway, error) { + var output []*ec2.NatGateway + + err := conn.DescribeNatGatewaysPages(input, func(page *ec2.DescribeNatGatewaysOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.NatGateways { + if v != nil { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, ErrCodeNatGatewayNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return output, nil +} + +func FindNATGatewayByID(conn *ec2.EC2, id string) (*ec2.NatGateway, error) { + input := &ec2.DescribeNatGatewaysInput{ + NatGatewayIds: aws.StringSlice([]string{id}), + } + + output, err := FindNATGateway(conn, input) + + if err != nil { + return nil, err + } + + if state := aws.StringValue(output.State); state == ec2.NatGatewayStateDeleted { + return nil, &resource.NotFoundError{ + Message: state, + LastRequest: input, + } + } + + // Eventual consistency check. + if aws.StringValue(output.NatGatewayId) != id { + return nil, &resource.NotFoundError{ + LastRequest: input, + } + } + + return output, nil +} + func FindPlacementGroupByName(conn *ec2.EC2, name string) (*ec2.PlacementGroup, error) { input := &ec2.DescribePlacementGroupsInput{ GroupNames: aws.StringSlice([]string{name}), diff --git a/internal/service/ec2/flex.go b/internal/service/ec2/flex.go index 045b88bd2e6..b211b58dcff 100644 --- a/internal/service/ec2/flex.go +++ b/internal/service/ec2/flex.go @@ -9,14 +9,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -func flattenAttributeValues(l []*ec2.AttributeValue) []string { - values := make([]string, 0, len(l)) - for _, v := range l { - values = append(values, aws.StringValue(v.Value)) - } - return values -} - //Flattens security group identifiers into a []string, where the elements returned are the GroupIDs func FlattenGroupIdentifiers(dtos []*ec2.GroupIdentifier) []string { ids := make([]string, 0, len(dtos)) diff --git a/internal/service/ec2/instance.go b/internal/service/ec2/instance.go index 21add5d93c0..86852ce6874 100644 --- a/internal/service/ec2/instance.go +++ b/internal/service/ec2/instance.go @@ -2439,18 +2439,13 @@ func readSecurityGroups(d *schema.ResourceData, instance *ec2.Instance, conn *ec // If the instance is in a VPC, find out if that VPC is Default to determine // whether to store names. - if aws.StringValue(instance.VpcId) != "" { - out, err := conn.DescribeVpcs(&ec2.DescribeVpcsInput{ - VpcIds: []*string{instance.VpcId}, - }) + if vpcID := aws.StringValue(instance.VpcId); vpcID != "" { + vpc, err := FindVPCByID(conn, vpcID) + if err != nil { - log.Printf("[WARN] Unable to describe VPC %q: %s", aws.StringValue(instance.VpcId), err) - } else if len(out.Vpcs) == 0 { - // This may happen in Eucalyptus Cloud - log.Printf("[WARN] Unable to retrieve VPCs") + log.Printf("[WARN] error reading EC2 Instance (%s) VPC (%s): %s", d.Id(), vpcID, err) } else { - isInDefaultVpc := aws.BoolValue(out.Vpcs[0].IsDefault) - useName = isInDefaultVpc + useName = aws.BoolValue(vpc.IsDefault) } } diff --git a/internal/service/ec2/nat_gateway.go b/internal/service/ec2/nat_gateway.go index 6afd5b9fc8c..65943e08f98 100644 --- a/internal/service/ec2/nat_gateway.go +++ b/internal/service/ec2/nat_gateway.go @@ -3,27 +3,25 @@ package ec2 import ( "fmt" "log" - "strings" - "time" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/aws-sdk-go-base/tfawserr" - "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 ResourceNatGateway() *schema.Resource { +func ResourceNATGateway() *schema.Resource { return &schema.Resource{ - Create: resourceNatGatewayCreate, - Read: resourceNatGatewayRead, - Update: resourceNatGatewayUpdate, - Delete: resourceNatGatewayDelete, + Create: resourceNATGatewayCreate, + Read: resourceNATGatewayRead, + Update: resourceNATGatewayUpdate, + Delete: resourceNATGatewayDelete, + Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, @@ -34,13 +32,6 @@ func ResourceNatGateway() *schema.Resource { Optional: true, ForceNew: true, }, - - "subnet_id": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - "connectivity_type": { Type: schema.TypeString, Optional: true, @@ -48,22 +39,23 @@ func ResourceNatGateway() *schema.Resource { Default: ec2.ConnectivityTypePublic, ValidateFunc: validation.StringInSlice(ec2.ConnectivityType_Values(), false), }, - "network_interface_id": { Type: schema.TypeString, Computed: true, }, - "private_ip": { Type: schema.TypeString, Computed: true, }, - "public_ip": { Type: schema.TypeString, Computed: true, }, - + "subnet_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, "tags": tftags.TagsSchema(), "tags_all": tftags.TagsSchemaComputed(), }, @@ -72,89 +64,67 @@ func ResourceNatGateway() *schema.Resource { } } -func resourceNatGatewayCreate(d *schema.ResourceData, meta interface{}) error { +func resourceNATGatewayCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) - // Create the NAT Gateway - createOpts := &ec2.CreateNatGatewayInput{ + input := &ec2.CreateNatGatewayInput{ TagSpecifications: ec2TagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeNatgateway), } if v, ok := d.GetOk("allocation_id"); ok { - createOpts.AllocationId = aws.String(v.(string)) + input.AllocationId = aws.String(v.(string)) } if v, ok := d.GetOk("connectivity_type"); ok { - createOpts.ConnectivityType = aws.String(v.(string)) + input.ConnectivityType = aws.String(v.(string)) } if v, ok := d.GetOk("subnet_id"); ok { - createOpts.SubnetId = aws.String(v.(string)) + input.SubnetId = aws.String(v.(string)) } - log.Printf("[DEBUG] Create NAT Gateway: %s", *createOpts) - natResp, err := conn.CreateNatGateway(createOpts) + log.Printf("[DEBUG] Creating EC2 NAT Gateway: %s", input) + output, err := conn.CreateNatGateway(input) + if err != nil { - return fmt.Errorf("Error creating NAT Gateway: %s", err) + return fmt.Errorf("error creating EC2 NAT Gateway: %w", err) } - // Get the ID and store it - ng := natResp.NatGateway - d.SetId(aws.StringValue(ng.NatGatewayId)) - log.Printf("[INFO] NAT Gateway ID: %s", d.Id()) - - // Wait for the NAT Gateway to become available - log.Printf("[DEBUG] Waiting for NAT Gateway (%s) to become available", d.Id()) - stateConf := &resource.StateChangeConf{ - Pending: []string{ec2.NatGatewayStatePending}, - Target: []string{ec2.NatGatewayStateAvailable}, - Refresh: NGStateRefreshFunc(conn, d.Id()), - Timeout: 10 * time.Minute, - } + d.SetId(aws.StringValue(output.NatGateway.NatGatewayId)) - if _, err := stateConf.WaitForState(); err != nil { - return fmt.Errorf("Error waiting for NAT Gateway (%s) to become available: %s", d.Id(), err) + if _, err := WaitNATGatewayCreated(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for EC2 NAT Gateway (%s) create: %w", d.Id(), err) } - return resourceNatGatewayRead(d, meta) + return resourceNATGatewayRead(d, meta) } -func resourceNatGatewayRead(d *schema.ResourceData, meta interface{}) error { +func resourceNATGatewayRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig - // Refresh the NAT Gateway state - ngRaw, state, err := NGStateRefreshFunc(conn, d.Id())() - if err != nil { - return err - } - - status := map[string]bool{ - ec2.NatGatewayStateDeleted: true, - ec2.NatGatewayStateDeleting: true, - ec2.NatGatewayStateFailed: true, - } + ng, err := FindNATGatewayByID(conn, d.Id()) - if _, ok := status[strings.ToLower(state)]; ngRaw == nil || ok { - log.Printf("[INFO] Removing %s from Terraform state as it is not found or in the deleted state.", d.Id()) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] EC2 NAT Gateway (%s) not found, removing from state", d.Id()) d.SetId("") return nil } - // Set NAT Gateway attributes - ng := ngRaw.(*ec2.NatGateway) - d.Set("connectivity_type", ng.ConnectivityType) - d.Set("subnet_id", ng.SubnetId) + if err != nil { + return fmt.Errorf("error reading EC2 NAT Gateway (%s): %w", d.Id(), err) + } - // Address address := ng.NatGatewayAddresses[0] d.Set("allocation_id", address.AllocationId) + d.Set("connectivity_type", ng.ConnectivityType) d.Set("network_interface_id", address.NetworkInterfaceId) d.Set("private_ip", address.PrivateIp) d.Set("public_ip", address.PublicIp) + d.Set("subnet_id", ng.SubnetId) tags := KeyValueTags(ng.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) @@ -170,82 +140,39 @@ func resourceNatGatewayRead(d *schema.ResourceData, meta interface{}) error { return nil } -func resourceNatGatewayUpdate(d *schema.ResourceData, meta interface{}) error { +func resourceNATGatewayUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn if d.HasChange("tags_all") { o, n := d.GetChange("tags_all") if err := UpdateTags(conn, d.Id(), o, n); err != nil { - return fmt.Errorf("error updating EC2 NAT Gateway (%s) tags: %s", d.Id(), err) + return fmt.Errorf("error updating EC2 NAT Gateway (%s) tags: %w", d.Id(), err) } } - return resourceNatGatewayRead(d, meta) + return resourceNATGatewayRead(d, meta) } -func resourceNatGatewayDelete(d *schema.ResourceData, meta interface{}) error { +func resourceNATGatewayDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn - deleteOpts := &ec2.DeleteNatGatewayInput{ - NatGatewayId: aws.String(d.Id()), - } - log.Printf("[INFO] Deleting NAT Gateway: %s", d.Id()) - - _, err := conn.DeleteNatGateway(deleteOpts) - if err != nil { - ec2err, ok := err.(awserr.Error) - if !ok { - return err - } - if ec2err.Code() == "NatGatewayNotFound" { - return nil - } + log.Printf("[INFO] Deleting EC2 NAT Gateway: %s", d.Id()) + _, err := conn.DeleteNatGateway(&ec2.DeleteNatGatewayInput{ + NatGatewayId: aws.String(d.Id()), + }) - return err + if tfawserr.ErrCodeEquals(err, ErrCodeNatGatewayNotFound) { + return nil } - stateConf := &resource.StateChangeConf{ - Pending: []string{ec2.NatGatewayStateDeleting}, - Target: []string{ec2.NatGatewayStateDeleted}, - Refresh: NGStateRefreshFunc(conn, d.Id()), - Timeout: 30 * time.Minute, - Delay: 10 * time.Second, - MinTimeout: 10 * time.Second, + if err != nil { + return fmt.Errorf("error deleting EC2 NAT Gateway (%s): %w", d.Id(), err) } - _, stateErr := stateConf.WaitForState() - if stateErr != nil { - return fmt.Errorf("Error waiting for NAT Gateway (%s) to delete: %s", d.Id(), err) + if _, err := WaitNATGatewayDeleted(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for EC2 NAT Gateway (%s) delete: %w", d.Id(), err) } return nil } - -// NGStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch -// a NAT Gateway. -func NGStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc { - return func() (interface{}, string, error) { - opts := &ec2.DescribeNatGatewaysInput{ - NatGatewayIds: []*string{aws.String(id)}, - } - resp, err := conn.DescribeNatGateways(opts) - if err != nil { - if tfawserr.ErrMessageContains(err, "NatGatewayNotFound", "") { - resp = nil - } else { - log.Printf("Error on NGStateRefresh: %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 - } - - ng := resp.NatGateways[0] - return ng, *ng.State, nil - } -} diff --git a/internal/service/ec2/nat_gateway_data_source.go b/internal/service/ec2/nat_gateway_data_source.go index 296f3f8ab1f..5c626732fb7 100644 --- a/internal/service/ec2/nat_gateway_data_source.go +++ b/internal/service/ec2/nat_gateway_data_source.go @@ -2,128 +2,103 @@ package ec2 import ( "fmt" - "log" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "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" ) -func DataSourceNatGateway() *schema.Resource { +func DataSourceNATGateway() *schema.Resource { return &schema.Resource{ - Read: dataSourceNatGatewayRead, + Read: dataSourceNATGatewayRead, Schema: map[string]*schema.Schema{ - "id": { + "allocation_id": { Type: schema.TypeString, - Optional: true, Computed: true, }, - "state": { + "connectivity_type": { Type: schema.TypeString, - Optional: true, Computed: true, }, - "vpc_id": { + "filter": CustomFiltersSchema(), + "id": { Type: schema.TypeString, Optional: true, Computed: true, }, - "subnet_id": { + "network_interface_id": { Type: schema.TypeString, - Optional: true, Computed: true, }, - "allocation_id": { + "private_ip": { Type: schema.TypeString, Computed: true, }, - "connectivity_type": { + "public_ip": { Type: schema.TypeString, Computed: true, }, - "network_interface_id": { + "state": { Type: schema.TypeString, + Optional: true, Computed: true, }, - "public_ip": { + "subnet_id": { Type: schema.TypeString, + Optional: true, Computed: true, }, - "private_ip": { + "tags": tftags.TagsSchemaComputed(), + "vpc_id": { Type: schema.TypeString, + Optional: true, Computed: true, }, - "tags": tftags.TagsSchemaComputed(), - "filter": CustomFiltersSchema(), }, } } -func dataSourceNatGatewayRead(d *schema.ResourceData, meta interface{}) error { +func dataSourceNATGatewayRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig - req := &ec2.DescribeNatGatewaysInput{} - - if id, ok := d.GetOk("id"); ok { - req.NatGatewayIds = aws.StringSlice([]string{id.(string)}) - } - - if vpc_id, ok := d.GetOk("vpc_id"); ok { - req.Filter = append(req.Filter, BuildAttributeFilterList( + input := &ec2.DescribeNatGatewaysInput{ + Filter: BuildAttributeFilterList( map[string]string{ - "vpc-id": vpc_id.(string), + "state": d.Get("state").(string), + "subnet-id": d.Get("subnet_id").(string), + "vpc-id": d.Get("vpc_id").(string), }, - )...) + ), } - if state, ok := d.GetOk("state"); ok { - req.Filter = append(req.Filter, BuildAttributeFilterList( - map[string]string{ - "state": state.(string), - }, - )...) - } - - if subnet_id, ok := d.GetOk("subnet_id"); ok { - req.Filter = append(req.Filter, BuildAttributeFilterList( - map[string]string{ - "subnet-id": subnet_id.(string), - }, - )...) + if v, ok := d.GetOk("id"); ok { + input.NatGatewayIds = aws.StringSlice([]string{v.(string)}) } if tags, ok := d.GetOk("tags"); ok { - req.Filter = append(req.Filter, BuildTagFilterList( + input.Filter = append(input.Filter, BuildTagFilterList( Tags(tftags.New(tags.(map[string]interface{}))), )...) } - req.Filter = append(req.Filter, BuildCustomFilterList( + input.Filter = append(input.Filter, BuildCustomFilterList( d.Get("filter").(*schema.Set), )...) - if len(req.Filter) == 0 { + if len(input.Filter) == 0 { // Don't send an empty filters list; the EC2 API won't accept it. - req.Filter = nil - } - log.Printf("[DEBUG] Reading NAT Gateway: %s", req) - resp, err := conn.DescribeNatGateways(req) - if err != nil { - return err - } - if resp == nil || len(resp.NatGateways) == 0 { - return fmt.Errorf("no matching NAT gateway found: %s", req) - } - if len(resp.NatGateways) > 1 { - return fmt.Errorf("multiple NAT gateways matched; use additional constraints to reduce matches to a single NAT gateway") + input.Filter = nil } - ngw := resp.NatGateways[0] + ngw, err := FindNATGateway(conn, input) - log.Printf("[DEBUG] NAT Gateway response: %s", ngw) + if err != nil { + return tfresource.SingularDataSourceFindError("EC2 NAT Gateway", err) + } d.SetId(aws.StringValue(ngw.NatGatewayId)) d.Set("connectivity_type", ngw.ConnectivityType) @@ -131,10 +106,6 @@ func dataSourceNatGatewayRead(d *schema.ResourceData, meta interface{}) error { d.Set("subnet_id", ngw.SubnetId) d.Set("vpc_id", ngw.VpcId) - if err := d.Set("tags", KeyValueTags(ngw.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %w", err) - } - for _, address := range ngw.NatGatewayAddresses { if aws.StringValue(address.AllocationId) != "" { d.Set("allocation_id", address.AllocationId) @@ -145,5 +116,9 @@ func dataSourceNatGatewayRead(d *schema.ResourceData, meta interface{}) error { } } + if err := d.Set("tags", KeyValueTags(ngw.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + return nil } diff --git a/internal/service/ec2/nat_gateway_data_source_test.go b/internal/service/ec2/nat_gateway_data_source_test.go index 8632b81b28f..5def3988f06 100644 --- a/internal/service/ec2/nat_gateway_data_source_test.go +++ b/internal/service/ec2/nat_gateway_data_source_test.go @@ -10,9 +10,12 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/acctest" ) -func TestAccEC2NatGatewayDataSource_basic(t *testing.T) { - // This is used as a portion of CIDR network addresses. - rInt := sdkacctest.RandIntRange(4, 254) +func TestAccEC2NATGatewayDataSource_basic(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + dataSourceNameById := "data.aws_nat_gateway.test_by_id" + dataSourceNameBySubnetId := "data.aws_nat_gateway.test_by_subnet_id" + dataSourceNameByTags := "data.aws_nat_gateway.test_by_tags" + resourceName := "aws_nat_gateway.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -20,62 +23,58 @@ func TestAccEC2NatGatewayDataSource_basic(t *testing.T) { Providers: acctest.Providers, Steps: []resource.TestStep{ { - Config: testAccNatGatewayDataSourceConfig(rInt), + Config: testAccNATGatewayDataSourceConfig(rName), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrPair("data.aws_nat_gateway.test_by_id", "connectivity_type", "aws_nat_gateway.test", "connectivity_type"), - resource.TestCheckResourceAttrPair( - "data.aws_nat_gateway.test_by_id", "id", - "aws_nat_gateway.test", "id"), - resource.TestCheckResourceAttrPair( - "data.aws_nat_gateway.test_by_subnet_id", "subnet_id", - "aws_nat_gateway.test", "subnet_id"), - resource.TestCheckResourceAttrPair( - "data.aws_nat_gateway.test_by_tags", "tags.Name", - "aws_nat_gateway.test", "tags.Name"), - resource.TestCheckResourceAttrSet("data.aws_nat_gateway.test_by_id", "state"), - resource.TestCheckResourceAttrSet("data.aws_nat_gateway.test_by_id", "allocation_id"), - resource.TestCheckResourceAttrSet("data.aws_nat_gateway.test_by_id", "network_interface_id"), - resource.TestCheckResourceAttrSet("data.aws_nat_gateway.test_by_id", "public_ip"), - resource.TestCheckResourceAttrSet("data.aws_nat_gateway.test_by_id", "private_ip"), - resource.TestCheckNoResourceAttr("data.aws_nat_gateway.test_by_id", "attached_vpc_id"), - resource.TestCheckResourceAttrSet("data.aws_nat_gateway.test_by_id", "tags.OtherTag"), + resource.TestCheckResourceAttrPair(dataSourceNameById, "connectivity_type", resourceName, "connectivity_type"), + resource.TestCheckResourceAttrPair(dataSourceNameById, "id", resourceName, "id"), + resource.TestCheckResourceAttrPair(dataSourceNameBySubnetId, "subnet_id", resourceName, "subnet_id"), + resource.TestCheckResourceAttrPair(dataSourceNameByTags, "tags.Name", resourceName, "tags.Name"), + resource.TestCheckResourceAttrSet(dataSourceNameById, "state"), + resource.TestCheckResourceAttrSet(dataSourceNameById, "allocation_id"), + resource.TestCheckResourceAttrSet(dataSourceNameById, "network_interface_id"), + resource.TestCheckResourceAttrSet(dataSourceNameById, "public_ip"), + resource.TestCheckResourceAttrSet(dataSourceNameById, "private_ip"), + resource.TestCheckNoResourceAttr(dataSourceNameById, "attached_vpc_id"), + resource.TestCheckResourceAttrSet(dataSourceNameById, "tags.OtherTag"), ), }, }, }) } -func testAccNatGatewayDataSourceConfig(rInt int) string { +func testAccNATGatewayDataSourceConfig(rName string) string { return acctest.ConfigCompose(acctest.ConfigAvailableAZsNoOptIn(), fmt.Sprintf(` resource "aws_vpc" "test" { - cidr_block = "172.%[1]d.0.0/16" + cidr_block = "172.5.0.0/16" tags = { - Name = "terraform-testacc-nat-gw-data-source" + Name = %[1]q } } resource "aws_subnet" "test" { vpc_id = aws_vpc.test.id - cidr_block = "172.%[1]d.123.0/24" + cidr_block = "172.5.123.0/24" availability_zone = data.aws_availability_zones.available.names[0] tags = { - Name = "tf-acc-nat-gw-data-source" + Name = %[1]q } } -# EIPs are not taggable resource "aws_eip" "test" { vpc = true + + tags = { + Name = %[1]q + } } -# IGWs are required for an NGW to spin up; manual dependency resource "aws_internet_gateway" "test" { vpc_id = aws_vpc.test.id tags = { - Name = "terraform-testacc-nat-gateway-data-source-%[1]d" + Name = %[1]q } } @@ -84,7 +83,7 @@ resource "aws_nat_gateway" "test" { allocation_id = aws_eip.test.id tags = { - Name = "terraform-testacc-nat-gw-data-source-%[1]d" + Name = %[1]q OtherTag = "some-value" } @@ -104,5 +103,5 @@ data "aws_nat_gateway" "test_by_tags" { Name = aws_nat_gateway.test.tags["Name"] } } -`, rInt)) +`, rName)) } diff --git a/internal/service/ec2/nat_gateway_test.go b/internal/service/ec2/nat_gateway_test.go index 0263750b125..534d27cc4c4 100644 --- a/internal/service/ec2/nat_gateway_test.go +++ b/internal/service/ec2/nat_gateway_test.go @@ -2,33 +2,38 @@ package ec2_test import ( "fmt" - "strings" "testing" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" + 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" + tfec2 "github.com/hashicorp/terraform-provider-aws/internal/service/ec2" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) -func TestAccEC2NatGateway_basic(t *testing.T) { +func TestAccEC2NATGateway_basic(t *testing.T) { var natGateway ec2.NatGateway resourceName := "aws_nat_gateway.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), Providers: acctest.Providers, - CheckDestroy: testAccCheckNatGatewayDestroy, + CheckDestroy: testAccCheckNATGatewayDestroy, Steps: []resource.TestStep{ { - Config: testAccNatGatewayConfig, - Check: resource.ComposeTestCheckFunc( - testAccCheckNatGatewayExists(resourceName, &natGateway), + Config: testAccNATGatewayConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckNATGatewayExists(resourceName, &natGateway), + resource.TestCheckResourceAttrSet(resourceName, "allocation_id"), resource.TestCheckResourceAttr(resourceName, "connectivity_type", "public"), + resource.TestCheckResourceAttrSet(resourceName, "network_interface_id"), + resource.TestCheckResourceAttrSet(resourceName, "private_ip"), + resource.TestCheckResourceAttrSet(resourceName, "public_ip"), resource.TestCheckResourceAttr(resourceName, "tags.#", "0"), ), }, @@ -41,21 +46,51 @@ func TestAccEC2NatGateway_basic(t *testing.T) { }) } -func TestAccEC2NatGateway_ConnectivityType_private(t *testing.T) { +func TestAccEC2NATGateway_disappears(t *testing.T) { var natGateway ec2.NatGateway resourceName := "aws_nat_gateway.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), Providers: acctest.Providers, - CheckDestroy: testAccCheckNatGatewayDestroy, + CheckDestroy: testAccCheckNATGatewayDestroy, Steps: []resource.TestStep{ { - Config: testAccNatGatewayConfigConnectivityType("private"), + Config: testAccNATGatewayConfig(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckNatGatewayExists(resourceName, &natGateway), + testAccCheckNATGatewayExists(resourceName, &natGateway), + acctest.CheckResourceDisappears(acctest.Provider, tfec2.ResourceNATGateway(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccEC2NATGateway_ConnectivityType_private(t *testing.T) { + var natGateway ec2.NatGateway + resourceName := "aws_nat_gateway.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckNATGatewayDestroy, + Steps: []resource.TestStep{ + { + Config: testAccNATGatewayConfigConnectivityType(rName, "private"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckNATGatewayExists(resourceName, &natGateway), + resource.TestCheckResourceAttr(resourceName, "allocation_id", ""), resource.TestCheckResourceAttr(resourceName, "connectivity_type", "private"), + resource.TestCheckResourceAttrSet(resourceName, "network_interface_id"), + resource.TestCheckResourceAttrSet(resourceName, "private_ip"), + resource.TestCheckResourceAttr(resourceName, "public_ip", ""), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), ), }, { @@ -67,20 +102,21 @@ func TestAccEC2NatGateway_ConnectivityType_private(t *testing.T) { }) } -func TestAccEC2NatGateway_tags(t *testing.T) { +func TestAccEC2NATGateway_tags(t *testing.T) { var natGateway ec2.NatGateway resourceName := "aws_nat_gateway.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), Providers: acctest.Providers, - CheckDestroy: testAccCheckNatGatewayDestroy, + CheckDestroy: testAccCheckNATGatewayDestroy, Steps: []resource.TestStep{ { - Config: testAccNatGatewayConfigTags1("key1", "value1"), + Config: testAccNATGatewayConfigTags1(rName, "key1", "value1"), Check: resource.ComposeTestCheckFunc( - testAccCheckNatGatewayExists(resourceName, &natGateway), + testAccCheckNATGatewayExists(resourceName, &natGateway), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), ), @@ -91,18 +127,18 @@ func TestAccEC2NatGateway_tags(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccNatGatewayConfigTags2("key1", "value1updated", "key2", "value2"), + Config: testAccNATGatewayConfigTags2(rName, "key1", "value1updated", "key2", "value2"), Check: resource.ComposeTestCheckFunc( - testAccCheckNatGatewayExists(resourceName, &natGateway), + testAccCheckNATGatewayExists(resourceName, &natGateway), resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), ), }, { - Config: testAccNatGatewayConfigTags1("key2", "value2"), + Config: testAccNATGatewayConfigTags1(rName, "key2", "value2"), Check: resource.ComposeTestCheckFunc( - testAccCheckNatGatewayExists(resourceName, &natGateway), + testAccCheckNATGatewayExists(resourceName, &natGateway), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), ), @@ -111,7 +147,7 @@ func TestAccEC2NatGateway_tags(t *testing.T) { }) } -func testAccCheckNatGatewayDestroy(s *terraform.State) error { +func testAccCheckNATGatewayDestroy(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn for _, rs := range s.RootModule().Resources { @@ -119,37 +155,23 @@ func testAccCheckNatGatewayDestroy(s *terraform.State) error { continue } - // Try to find the resource - resp, err := conn.DescribeNatGateways(&ec2.DescribeNatGatewaysInput{ - NatGatewayIds: []*string{aws.String(rs.Primary.ID)}, - }) - if err == nil { - status := map[string]bool{ - ec2.NatGatewayStateDeleted: true, - ec2.NatGatewayStateDeleting: true, - ec2.NatGatewayStateFailed: true, - } - if _, ok := status[strings.ToLower(*resp.NatGateways[0].State)]; len(resp.NatGateways) > 0 && !ok { - return fmt.Errorf("still exists") - } - - return nil - } + _, err := tfec2.FindNATGatewayByID(conn, rs.Primary.ID) - // Verify the error is what we want - ec2err, ok := err.(awserr.Error) - if !ok { - return err + if tfresource.NotFound(err) { + continue } - if ec2err.Code() != "NatGatewayNotFound" { + + if err != nil { return err } + + return fmt.Errorf("EC2 NAT Gateway %s still exists", rs.Primary.ID) } return nil } -func testAccCheckNatGatewayExists(n string, ng *ec2.NatGateway) resource.TestCheckFunc { +func testAccCheckNATGatewayExists(n string, v *ec2.NatGateway) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -157,32 +179,30 @@ func testAccCheckNatGatewayExists(n string, ng *ec2.NatGateway) resource.TestChe } if rs.Primary.ID == "" { - return fmt.Errorf("No ID is set") + return fmt.Errorf("No EC2 NAT Gateway ID is set") } conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn - resp, err := conn.DescribeNatGateways(&ec2.DescribeNatGatewaysInput{ - NatGatewayIds: []*string{aws.String(rs.Primary.ID)}, - }) + + output, err := tfec2.FindNATGatewayByID(conn, rs.Primary.ID) + if err != nil { return err } - if len(resp.NatGateways) == 0 { - return fmt.Errorf("NatGateway not found") - } - *ng = *resp.NatGateways[0] + *v = *output return nil } } -const testAccNatGatewayConfigBase = ` +func testAccNATGatewayConfigBase(rName string) string { + return fmt.Sprintf(` resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-nat-gw-basic" + Name = %[1]q } } @@ -192,7 +212,7 @@ resource "aws_subnet" "private" { map_public_ip_on_launch = false tags = { - Name = "tf-acc-nat-gw-basic-private" + Name = %[1]q } } @@ -202,48 +222,71 @@ resource "aws_subnet" "public" { map_public_ip_on_launch = true tags = { - Name = "tf-acc-nat-gw-basic-public" + Name = %[1]q } } resource "aws_internet_gateway" "test" { vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } } resource "aws_eip" "test" { vpc = true + + tags = { + Name = %[1]q + } +} +`, rName) } -` -const testAccNatGatewayConfig = testAccNatGatewayConfigBase + ` +func testAccNATGatewayConfig(rName string) string { + return acctest.ConfigCompose(testAccNATGatewayConfigBase(rName), ` resource "aws_nat_gateway" "test" { allocation_id = aws_eip.test.id subnet_id = aws_subnet.public.id depends_on = [aws_internet_gateway.test] } -` +`) +} -func testAccNatGatewayConfigConnectivityType(connectivityType string) string { +func testAccNATGatewayConfigConnectivityType(rName, connectivityType string) string { return fmt.Sprintf(` resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" + + tags = { + Name = %[1]q + } } resource "aws_subnet" "test" { cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 8, 0) vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } } resource "aws_nat_gateway" "test" { - connectivity_type = %[1]q + connectivity_type = %[2]q subnet_id = aws_subnet.test.id + + tags = { + Name = %[1]q + } } -`, connectivityType) +`, rName, connectivityType) } -func testAccNatGatewayConfigTags1(tagKey1, tagValue1 string) string { - return testAccNatGatewayConfigBase + fmt.Sprintf(` +func testAccNATGatewayConfigTags1(rName, tagKey1, tagValue1 string) string { + return acctest.ConfigCompose(testAccNATGatewayConfigBase(rName), fmt.Sprintf(` resource "aws_nat_gateway" "test" { allocation_id = aws_eip.test.id subnet_id = aws_subnet.public.id @@ -254,11 +297,11 @@ resource "aws_nat_gateway" "test" { depends_on = [aws_internet_gateway.test] } -`, tagKey1, tagValue1) +`, tagKey1, tagValue1)) } -func testAccNatGatewayConfigTags2(tagKey1, tagValue1, tagKey2, tagValue2 string) string { - return testAccNatGatewayConfigBase + fmt.Sprintf(` +func testAccNATGatewayConfigTags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return acctest.ConfigCompose(testAccNATGatewayConfigBase(rName), fmt.Sprintf(` resource "aws_nat_gateway" "test" { allocation_id = aws_eip.test.id subnet_id = aws_subnet.public.id @@ -270,5 +313,5 @@ resource "aws_nat_gateway" "test" { depends_on = [aws_internet_gateway.test] } -`, tagKey1, tagValue1, tagKey2, tagValue2) +`, tagKey1, tagValue1, tagKey2, tagValue2)) } diff --git a/internal/service/ec2/security_group.go b/internal/service/ec2/security_group.go index aea4212c5fd..1f0f74da6bc 100644 --- a/internal/service/ec2/security_group.go +++ b/internal/service/ec2/security_group.go @@ -2,7 +2,6 @@ package ec2 import ( "bytes" - "errors" "fmt" "log" "sort" @@ -31,6 +30,7 @@ func ResourceSecurityGroup() *schema.Resource { Read: resourceSecurityGroupRead, Update: resourceSecurityGroupUpdate, Delete: resourceSecurityGroupDelete, + Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, @@ -246,49 +246,46 @@ func resourceSecurityGroupCreate(d *schema.ResourceData, meta interface{}) error defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) - securityGroupOpts := &ec2.CreateSecurityGroupInput{} + groupName := create.Name(d.Get("name").(string), d.Get("name_prefix").(string)) + input := &ec2.CreateSecurityGroupInput{ + GroupName: aws.String(groupName), + } - if v, ok := d.GetOk("vpc_id"); ok { - securityGroupOpts.VpcId = aws.String(v.(string)) + if v := d.Get("description"); v != nil { + input.Description = aws.String(v.(string)) } if len(tags) > 0 { - securityGroupOpts.TagSpecifications = ec2TagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeSecurityGroup) + input.TagSpecifications = ec2TagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeSecurityGroup) } - if v := d.Get("description"); v != nil { - securityGroupOpts.Description = aws.String(v.(string)) + if v, ok := d.GetOk("vpc_id"); ok { + input.VpcId = aws.String(v.(string)) } - groupName := create.Name(d.Get("name").(string), d.Get("name_prefix").(string)) - securityGroupOpts.GroupName = aws.String(groupName) + log.Printf("[DEBUG] Creating Security Group: %s", input) + output, err := conn.CreateSecurityGroup(input) - var err error - log.Printf("[DEBUG] Security Group create configuration: %s", securityGroupOpts) - createResp, err := conn.CreateSecurityGroup(securityGroupOpts) if err != nil { - return fmt.Errorf("Error creating Security Group: %w", err) + return fmt.Errorf("error creating Security Group (%s): %w", groupName, err) } - d.SetId(aws.StringValue(createResp.GroupId)) - - log.Printf("[INFO] Security Group ID: %s", d.Id()) + d.SetId(aws.StringValue(output.GroupId)) // Wait for the security group to truly exist group, err := WaitSecurityGroupCreated(conn, d.Id(), d.Timeout(schema.TimeoutCreate)) + if err != nil { - return fmt.Errorf( - "Error waiting for Security Group (%s) to become available: %w", - d.Id(), err) + return fmt.Errorf("error waiting for Security Group (%s) create: %w", d.Id(), err) } // AWS defaults all Security Groups to have an ALLOW ALL egress rule. Here we // revoke that rule, so users don't unknowingly have/use it. - if group.VpcId != nil && *group.VpcId != "" { + if aws.StringValue(group.VpcId) != "" { log.Printf("[DEBUG] Revoking default egress rule for Security Group for %s", d.Id()) - req := &ec2.RevokeSecurityGroupEgressInput{ - GroupId: createResp.GroupId, + input := &ec2.RevokeSecurityGroupEgressInput{ + GroupId: output.GroupId, IpPermissions: []*ec2.IpPermission{ { FromPort: aws.Int64(0), @@ -303,13 +300,13 @@ func resourceSecurityGroupCreate(d *schema.ResourceData, meta interface{}) error }, } - if _, err = conn.RevokeSecurityGroupEgress(req); err != nil { + if _, err = conn.RevokeSecurityGroupEgress(input); err != nil { return fmt.Errorf("Error revoking default egress rule for Security Group (%s): %w", d.Id(), err) } log.Printf("[DEBUG] Revoking default IPv6 egress rule for Security Group for %s", d.Id()) - req = &ec2.RevokeSecurityGroupEgressInput{ - GroupId: createResp.GroupId, + input = &ec2.RevokeSecurityGroupEgressInput{ + GroupId: output.GroupId, IpPermissions: []*ec2.IpPermission{ { FromPort: aws.Int64(0), @@ -324,7 +321,7 @@ func resourceSecurityGroupCreate(d *schema.ResourceData, meta interface{}) error }, } - _, err = conn.RevokeSecurityGroupEgress(req) + _, err = conn.RevokeSecurityGroupEgress(input) if err != nil { //If we have a NotFound or InvalidParameterValue, then we are trying to remove the default IPv6 egress of a non-IPv6 //enabled SG @@ -344,12 +341,13 @@ func resourceSecurityGroupRead(d *schema.ResourceData, meta interface{}) error { ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig sg, err := FindSecurityGroupByID(conn, d.Id()) - var nfe *resource.NotFoundError - if !d.IsNewResource() && errors.As(err, &nfe) { - log.Printf("[WARN] Security group (%s) not found, removing from state", d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Security Group %s not found, removing from state", d.Id()) d.SetId("") return nil } + if err != nil { return fmt.Errorf("error reading Security Group (%s): %w", d.Id(), err) } diff --git a/internal/service/ec2/security_group_test.go b/internal/service/ec2/security_group_test.go index 3f86777683d..f07470b5ad9 100644 --- a/internal/service/ec2/security_group_test.go +++ b/internal/service/ec2/security_group_test.go @@ -643,6 +643,28 @@ func TestAccEC2SecurityGroup_basic(t *testing.T) { }) } +func TestAccEC2SecurityGroup_disappears(t *testing.T) { + var group ec2.SecurityGroup + resourceName := "aws_security_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckSecurityGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSecurityGroupConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckSecurityGroupExists(resourceName, &group), + acctest.CheckResourceDisappears(acctest.Provider, tfec2.ResourceSecurityGroup(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + func TestAccEC2SecurityGroup_egressMode(t *testing.T) { var securityGroup1, securityGroup2, securityGroup3 ec2.SecurityGroup resourceName := "aws_security_group.test" diff --git a/internal/service/ec2/status.go b/internal/service/ec2/status.go index 2e6854f0736..cd84b6d420b 100644 --- a/internal/service/ec2/status.go +++ b/internal/service/ec2/status.go @@ -244,6 +244,22 @@ func StatusInstanceIAMInstanceProfile(conn *ec2.EC2, id string) resource.StateRe } } +func StatusNATGatewayState(conn *ec2.EC2, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := FindNATGatewayByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.State), nil + } +} + const ( RouteStatusReady = "ready" ) @@ -302,24 +318,21 @@ func StatusRouteTableAssociationState(conn *ec2.EC2, id string) resource.StateRe const ( SecurityGroupStatusCreated = "Created" - - SecurityGroupStatusNotFound = "NotFound" - - SecurityGroupStatusUnknown = "Unknown" ) -// StatusSecurityGroup fetches the security group and its status func StatusSecurityGroup(conn *ec2.EC2, id string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - group, err := FindSecurityGroupByID(conn, id) + output, err := FindSecurityGroupByID(conn, id) + if tfresource.NotFound(err) { - return nil, SecurityGroupStatusNotFound, nil + return nil, "", nil } + if err != nil { - return nil, SecurityGroupStatusUnknown, err + return nil, "", err } - return group, SecurityGroupStatusCreated, nil + return output, SecurityGroupStatusCreated, nil } } @@ -483,12 +496,27 @@ func StatusTransitGatewayRouteTablePropagationState(conn *ec2.EC2, transitGatewa } } -// StatusVPCAttribute fetches the Vpc and its attribute value -func StatusVPCAttribute(conn *ec2.EC2, id string, attribute string) resource.StateRefreshFunc { +func StatusVPCState(conn *ec2.EC2, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := FindVPCByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.State), nil + } +} + +func StatusVPCAttributeValue(conn *ec2.EC2, id string, attribute string) resource.StateRefreshFunc { return func() (interface{}, string, error) { attributeValue, err := FindVPCAttribute(conn, id, attribute) - if tfawserr.ErrCodeEquals(err, ErrCodeInvalidVpcIDNotFound) { + if tfresource.NotFound(err) { return nil, "", nil } @@ -496,11 +524,39 @@ func StatusVPCAttribute(conn *ec2.EC2, id string, attribute string) resource.Sta return nil, "", err } - if attributeValue == nil { + return attributeValue, strconv.FormatBool(attributeValue), nil + } +} + +func StatusVPCCIDRBlockAssociationState(conn *ec2.EC2, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, _, err := FindVPCCIDRBlockAssociationByID(conn, id) + + if tfresource.NotFound(err) { return nil, "", nil } - return attributeValue, strconv.FormatBool(aws.BoolValue(attributeValue)), nil + if err != nil { + return nil, "", err + } + + return output.CidrBlockState, aws.StringValue(output.CidrBlockState.State), nil + } +} + +func StatusVPCIPv6CIDRBlockAssociationState(conn *ec2.EC2, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, _, err := FindVPCIPv6CIDRBlockAssociationByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output.Ipv6CidrBlockState, aws.StringValue(output.Ipv6CidrBlockState.State), nil } } diff --git a/internal/service/ec2/sweep.go b/internal/service/ec2/sweep.go index ff4d919462a..ebc4c0785fb 100644 --- a/internal/service/ec2/sweep.go +++ b/internal/service/ec2/sweep.go @@ -128,7 +128,7 @@ func init() { resource.AddTestSweepers("aws_nat_gateway", &resource.Sweeper{ Name: "aws_nat_gateway", - F: sweepNatGateways, + F: sweepNATGateways, }) resource.AddTestSweepers("aws_network_acl", &resource.Sweeper{ @@ -614,39 +614,39 @@ func sweepEgressOnlyInternetGateways(region string) error { if err != nil { return fmt.Errorf("error getting client: %s", err) } + input := &ec2.DescribeEgressOnlyInternetGatewaysInput{} conn := client.(*conns.AWSClient).EC2Conn + sweepResources := make([]*sweep.SweepResource, 0) - input := &ec2.DescribeEgressOnlyInternetGatewaysInput{} err = conn.DescribeEgressOnlyInternetGatewaysPages(input, func(page *ec2.DescribeEgressOnlyInternetGatewaysOutput, lastPage bool) bool { if page == nil { return !lastPage } - for _, gateway := range page.EgressOnlyInternetGateways { - id := aws.StringValue(gateway.EgressOnlyInternetGatewayId) - input := &ec2.DeleteEgressOnlyInternetGatewayInput{ - EgressOnlyInternetGatewayId: gateway.EgressOnlyInternetGatewayId, - } - - log.Printf("[INFO] Deleting EC2 Egress Only Internet Gateway: %s", id) - - _, err := conn.DeleteEgressOnlyInternetGateway(input) + for _, v := range page.EgressOnlyInternetGateways { + r := ResourceEgressOnlyInternetGateway() + d := r.Data(nil) + d.SetId(aws.StringValue(v.EgressOnlyInternetGatewayId)) - if err != nil { - log.Printf("[ERROR] Error deleting EC2 Egress Only Internet Gateway (%s): %s", id, err) - } + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) } return !lastPage }) if sweep.SkipSweepError(err) { - log.Printf("[WARN] Skipping EC2 Egress Only Internet Gateway sweep for %s: %s", region, err) + log.Printf("[WARN] Skipping EC2 Egress-only Internet Gateway sweep for %s: %s", region, err) return nil } if err != nil { - return fmt.Errorf("Error describing EC2 Egress Only Internet Gateways: %s", err) + return fmt.Errorf("error listing EC2 Egress-only Internet Gateways (%s): %w", region, err) + } + + err = sweep.SweepOrchestrator(sweepResources) + + if err != nil { + return fmt.Errorf("error sweeping EC2 Egress-only Internet Gateways (%s): %w", region, err) } return nil @@ -1017,37 +1017,44 @@ func sweepLaunchTemplates(region string) error { return sweeperErrs.ErrorOrNil() } -func sweepNatGateways(region string) error { +func sweepNATGateways(region string) error { client, err := sweep.SharedRegionalSweepClient(region) if err != nil { return fmt.Errorf("error getting client: %s", err) } + input := &ec2.DescribeNatGatewaysInput{} conn := client.(*conns.AWSClient).EC2Conn + sweepResources := make([]*sweep.SweepResource, 0) - req := &ec2.DescribeNatGatewaysInput{} - resp, err := conn.DescribeNatGateways(req) - if err != nil { - if sweep.SkipSweepError(err) { - log.Printf("[WARN] Skipping EC2 NAT Gateway sweep for %s: %s", region, err) - return nil + err = conn.DescribeNatGatewaysPages(input, func(page *ec2.DescribeNatGatewaysOutput, lastPage bool) bool { + if page == nil { + return !lastPage } - return fmt.Errorf("Error describing NAT Gateways: %s", err) - } - if len(resp.NatGateways) == 0 { - log.Print("[DEBUG] No AWS NAT Gateways to sweep") + for _, v := range page.NatGateways { + r := ResourceNATGateway() + d := r.Data(nil) + d.SetId(aws.StringValue(v.NatGatewayId)) + + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) + } + + return !lastPage + }) + + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping EC2 NAT Gateway sweep for %s: %s", region, err) return nil } - for _, natGateway := range resp.NatGateways { - _, err := conn.DeleteNatGateway(&ec2.DeleteNatGatewayInput{ - NatGatewayId: natGateway.NatGatewayId, - }) - if err != nil { - return fmt.Errorf( - "Error deleting NAT Gateway (%s): %s", - *natGateway.NatGatewayId, err) - } + if err != nil { + return fmt.Errorf("error listing EC2 NAT Gateways (%s): %w", region, err) + } + + err = sweep.SweepOrchestrator(sweepResources) + + if err != nil { + return fmt.Errorf("error sweeping EC2 NAT Gateways (%s): %w", region, err) } return nil @@ -1739,30 +1746,34 @@ func sweepVPCDHCPOptions(region string) error { if err != nil { return fmt.Errorf("error getting client: %s", err) } - conn := client.(*conns.AWSClient).EC2Conn - input := &ec2.DescribeDhcpOptionsInput{} + conn := client.(*conns.AWSClient).EC2Conn + sweepResources := make([]*sweep.SweepResource, 0) err = conn.DescribeDhcpOptionsPages(input, func(page *ec2.DescribeDhcpOptionsOutput, lastPage bool) bool { - for _, dhcpOption := range page.DhcpOptions { + if page == nil { + return !lastPage + } + + for _, v := range page.DhcpOptions { + // Skip the default DHCP Options. var defaultDomainNameFound, defaultDomainNameServersFound bool - // This skips the default dhcp configurations so they don't get deleted - for _, dhcpConfiguration := range dhcpOption.DhcpConfigurations { - if aws.StringValue(dhcpConfiguration.Key) == "domain-name" { - if len(dhcpConfiguration.Values) != 1 || dhcpConfiguration.Values[0] == nil { + for _, v := range v.DhcpConfigurations { + if aws.StringValue(v.Key) == "domain-name" { + if len(v.Values) != 1 || v.Values[0] == nil { continue } - if aws.StringValue(dhcpConfiguration.Values[0].Value) == RegionalPrivateDNSSuffix(region) { + if aws.StringValue(v.Values[0].Value) == RegionalPrivateDNSSuffix(region) { defaultDomainNameFound = true } - } else if aws.StringValue(dhcpConfiguration.Key) == "domain-name-servers" { - if len(dhcpConfiguration.Values) != 1 || dhcpConfiguration.Values[0] == nil { + } else if aws.StringValue(v.Key) == "domain-name-servers" { + if len(v.Values) != 1 || v.Values[0] == nil { continue } - if aws.StringValue(dhcpConfiguration.Values[0].Value) == "AmazonProvidedDNS" { + if aws.StringValue(v.Values[0].Value) == "AmazonProvidedDNS" { defaultDomainNameServersFound = true } } @@ -1772,26 +1783,29 @@ func sweepVPCDHCPOptions(region string) error { continue } - input := &ec2.DeleteDhcpOptionsInput{ - DhcpOptionsId: dhcpOption.DhcpOptionsId, - } - - _, err := conn.DeleteDhcpOptions(input) + r := ResourceVPCDHCPOptions() + d := r.Data(nil) + d.SetId(aws.StringValue(v.DhcpOptionsId)) - if err != nil { - log.Printf("[ERROR] Error deleting EC2 DHCP Option (%s): %s", aws.StringValue(dhcpOption.DhcpOptionsId), err) - } + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) } + return !lastPage }) if sweep.SkipSweepError(err) { - log.Printf("[WARN] Skipping EC2 DHCP Option sweep for %s: %s", region, err) + log.Printf("[WARN] Skipping EC2 DHCP Options Set sweep for %s: %s", region, err) return nil } if err != nil { - return fmt.Errorf("error describing DHCP Options: %s", err) + return fmt.Errorf("error listing EC2 DHCP Options Sets (%s): %w", region, err) + } + + err = sweep.SweepOrchestrator(sweepResources) + + if err != nil { + return fmt.Errorf("error sweeping EC2 DHCP Options Sets (%s): %w", region, err) } return nil diff --git a/internal/service/ec2/vpc.go b/internal/service/ec2/vpc.go index 8ef4c9e09d3..4b2ba396078 100644 --- a/internal/service/ec2/vpc.go +++ b/internal/service/ec2/vpc.go @@ -4,15 +4,14 @@ import ( "context" "fmt" "log" - "time" + "strconv" + "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/arn" - "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" - "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" @@ -27,7 +26,6 @@ const ( VPCCIDRMaxIPv6 = 56 ) -// acceptance tests for byoip related tests are in vpc_byoip_test.go func ResourceVPC() *schema.Resource { //lintignore:R011 return &schema.Resource{ @@ -36,23 +34,12 @@ func ResourceVPC() *schema.Resource { Update: resourceVPCUpdate, Delete: resourceVPCDelete, Importer: &schema.ResourceImporter{ - State: resourceVPCInstanceImport, + State: resourceVPCImport, }, CustomizeDiff: customdiff.All( resourceVPCCustomizeDiff, verify.SetTagsDiff, - func(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { - // cidr_block can be set by a value returned from IPAM or explicitly in config - if diff.Id() != "" && diff.HasChange("cidr_block") { - // if netmask is set then cidr_block is derived from ipam, ignore changes - if diff.Get("ipv4_netmask_length") != 0 { - return diff.Clear("cidr_block") - } - return diff.ForceNew("cidr_block") - } - return nil - }, ), SchemaVersion: 1, @@ -80,7 +67,7 @@ func ResourceVPC() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "dhcp_options_id": { + "default_route_table_id": { Type: schema.TypeString, Computed: true, }, @@ -88,29 +75,29 @@ func ResourceVPC() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "default_route_table_id": { + "dhcp_options_id": { Type: schema.TypeString, Computed: true, }, - "enable_dns_hostnames": { + "enable_classiclink": { Type: schema.TypeBool, Optional: true, Computed: true, }, - "enable_dns_support": { + "enable_classiclink_dns_support": { Type: schema.TypeBool, Optional: true, - Default: true, + Computed: true, }, - "enable_classiclink": { + "enable_dns_hostnames": { Type: schema.TypeBool, Optional: true, Computed: true, }, - "enable_classiclink_dns_support": { + "enable_dns_support": { Type: schema.TypeBool, Optional: true, - Computed: true, + Default: true, }, "instance_tenancy": { Type: schema.TypeString, @@ -185,133 +172,75 @@ func resourceVPCCreate(d *schema.ResourceData, meta interface{}) error { defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) - // Create the VPC - createOpts := &ec2.CreateVpcInput{ - InstanceTenancy: aws.String(d.Get("instance_tenancy").(string)), + input := &ec2.CreateVpcInput{ AmazonProvidedIpv6CidrBlock: aws.Bool(d.Get("assign_generated_ipv6_cidr_block").(bool)), + InstanceTenancy: aws.String(d.Get("instance_tenancy").(string)), TagSpecifications: ec2TagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeVpc), } if v, ok := d.GetOk("cidr_block"); ok { - createOpts.CidrBlock = aws.String(v.(string)) + input.CidrBlock = aws.String(v.(string)) } if v, ok := d.GetOk("ipv4_ipam_pool_id"); ok { - createOpts.Ipv4IpamPoolId = aws.String(v.(string)) + input.Ipv4IpamPoolId = aws.String(v.(string)) } if v, ok := d.GetOk("ipv4_netmask_length"); ok { - createOpts.Ipv4NetmaskLength = aws.Int64(int64(v.(int))) - } - - if v, ok := d.GetOk("ipv6_ipam_pool_id"); ok { - createOpts.Ipv6IpamPoolId = aws.String(v.(string)) + input.Ipv4NetmaskLength = aws.Int64(int64(v.(int))) } if v, ok := d.GetOk("ipv6_cidr_block"); ok { - createOpts.Ipv6CidrBlock = aws.String(v.(string)) - } - - if v, ok := d.GetOk("ipv6_netmask_length"); ok { - createOpts.Ipv6NetmaskLength = aws.Int64(int64(v.(int))) + input.Ipv6CidrBlock = aws.String(v.(string)) } if v, ok := d.GetOk("ipv6_cidr_block_network_border_group"); ok { - createOpts.Ipv6CidrBlockNetworkBorderGroup = aws.String(v.(string)) + input.Ipv6CidrBlockNetworkBorderGroup = aws.String(v.(string)) } - log.Printf("[DEBUG] VPC create config: %#v", *createOpts) - vpcResp, err := conn.CreateVpc(createOpts) - if err != nil { - return fmt.Errorf("Error creating VPC: %s", err) + if v, ok := d.GetOk("ipv6_ipam_pool_id"); ok { + input.Ipv6IpamPoolId = aws.String(v.(string)) } - // Get the ID and store it - vpc := vpcResp.Vpc - d.SetId(aws.StringValue(vpc.VpcId)) - log.Printf("[INFO] VPC ID: %s", d.Id()) - - // Wait for the VPC to become available - log.Printf( - "[DEBUG] Waiting for VPC (%s) to become available", - d.Id()) - stateConf := &resource.StateChangeConf{ - Pending: []string{"pending"}, - Target: []string{"available"}, - Refresh: VPCStateRefreshFunc(conn, d.Id()), - Timeout: 10 * time.Minute, - } - if _, err := stateConf.WaitForState(); err != nil { - return fmt.Errorf( - "Error waiting for VPC (%s) to become available: %s", - d.Id(), err) + if v, ok := d.GetOk("ipv6_netmask_length"); ok { + input.Ipv6NetmaskLength = aws.Int64(int64(v.(int))) } - if len(vpc.Ipv6CidrBlockAssociationSet) > 0 && vpc.Ipv6CidrBlockAssociationSet[0] != nil { - log.Printf("[DEBUG] Waiting for EC2 VPC (%s) IPv6 CIDR to become associated", d.Id()) - if err := waitForEc2VpcIpv6CidrBlockAssociationCreate(conn, d.Id(), aws.StringValue(vpcResp.Vpc.Ipv6CidrBlockAssociationSet[0].AssociationId)); err != nil { - return fmt.Errorf("error waiting for EC2 VPC (%s) IPv6 CIDR to become associated: %s", d.Id(), err) - } - } + log.Printf("[DEBUG] Creating EC2 VPC: %s", input) + output, err := conn.CreateVpc(input) - // You cannot modify the DNS resolution and DNS hostnames attributes in the same request. Use separate requests for each attribute. - // Reference: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_ModifyVpcAttribute.html + if err != nil { + return fmt.Errorf("error creating EC2 VPC: %w", err) + } - if d.Get("enable_dns_hostnames").(bool) { - input := &ec2.ModifyVpcAttributeInput{ - EnableDnsHostnames: &ec2.AttributeBooleanValue{ - Value: aws.Bool(true), - }, - VpcId: aws.String(d.Id()), - } + d.SetId(aws.StringValue(output.Vpc.VpcId)) - if _, err := conn.ModifyVpcAttribute(input); err != nil { - return fmt.Errorf("error enabling EC2 VPC (%s) DNS Hostnames: %w", d.Id(), err) - } + vpc, err := WaitVPCCreated(conn, d.Id()) - if _, err := WaitVPCAttributeUpdated(conn, d.Id(), ec2.VpcAttributeNameEnableDnsHostnames, d.Get("enable_dns_hostnames").(bool)); err != nil { - return fmt.Errorf("error waiting for EC2 VPC (%s) DNS Hostnames to enable: %w", d.Id(), err) - } + if err != nil { + return fmt.Errorf("error waiting for EC2 VPC (%s) create: %w", d.Id(), err) } - // By default, only the enableDnsSupport attribute is set to true in a VPC created any other way. - // Reference: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-dns.html#vpc-dns-support - - if !d.Get("enable_dns_support").(bool) { - input := &ec2.ModifyVpcAttributeInput{ - EnableDnsSupport: &ec2.AttributeBooleanValue{ - Value: aws.Bool(false), - }, - VpcId: aws.String(d.Id()), - } + if len(vpc.Ipv6CidrBlockAssociationSet) > 0 && vpc.Ipv6CidrBlockAssociationSet[0] != nil { + associationID := aws.StringValue(output.Vpc.Ipv6CidrBlockAssociationSet[0].AssociationId) - if _, err := conn.ModifyVpcAttribute(input); err != nil { - return fmt.Errorf("error disabling EC2 VPC (%s) DNS Support: %w", d.Id(), err) - } + _, err = WaitVPCIPv6CIDRBlockAssociationCreated(conn, associationID, vpcIPv6CIDRBlockAssociationCreatedTimeout) - if _, err := WaitVPCAttributeUpdated(conn, d.Id(), ec2.VpcAttributeNameEnableDnsSupport, d.Get("enable_dns_support").(bool)); err != nil { - return fmt.Errorf("error waiting for EC2 VPC (%s) DNS Support to disable: %w", d.Id(), err) + if err != nil { + return fmt.Errorf("error waiting for EC2 VPC (%s) IPv6 CIDR block (%s) to become associated: %w", d.Id(), associationID, err) } } - if d.Get("enable_classiclink").(bool) { - input := &ec2.EnableVpcClassicLinkInput{ - VpcId: aws.String(d.Id()), - } - - if _, err := conn.EnableVpcClassicLink(input); err != nil { - return fmt.Errorf("error enabling VPC (%s) ClassicLink: %s", d.Id(), err) - } + vpcInfo := vpcInfo{ + vpc: vpc, + enableClassicLink: false, + enableClassicLinkDNSSupport: false, + enableDnsHostnames: false, + enableDnsSupport: true, } - if d.Get("enable_classiclink_dns_support").(bool) { - input := &ec2.EnableVpcClassicLinkDnsSupportInput{ - VpcId: aws.String(d.Id()), - } - - if _, err := conn.EnableVpcClassicLinkDnsSupport(input); err != nil { - return fmt.Errorf("error enabling VPC (%s) ClassicLink DNS support: %s", d.Id(), err) - } + if err := modifyVPCAttributesOnCreate(conn, d, &vpcInfo); err != nil { + return err } return resourceVPCRead(d, meta) @@ -322,36 +251,12 @@ func resourceVPCRead(d *schema.ResourceData, meta interface{}) error { defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig - var vpc *ec2.Vpc - - err := resource.Retry(VPCPropagationTimeout, func() *resource.RetryError { - var err error + outputRaw, err := tfresource.RetryWhenNewResourceNotFound(PropagationTimeout, func() (interface{}, error) { + return FindVPCByID(conn, d.Id()) + }, d.IsNewResource()) - vpc, err = FindVPCByID(conn, d.Id()) - - if d.IsNewResource() && tfawserr.ErrCodeEquals(err, "InvalidVpcID.NotFound") { - return resource.RetryableError(err) - } - - if err != nil { - return resource.NonRetryableError(err) - } - - if d.IsNewResource() && vpc == nil { - return resource.RetryableError(&resource.NotFoundError{ - LastError: fmt.Errorf("EC2 VPC (%s) not found", d.Id()), - }) - } - - return nil - }) - - if tfresource.TimedOut(err) { - vpc, err = FindVPCByID(conn, d.Id()) - } - - if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, "InvalidVpcID.NotFound") { - log.Printf("[WARN] EC2 VPC (%s) not found, removing from state", d.Id()) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] EC2 VPC %s not found, removing from state", d.Id()) d.SetId("") return nil } @@ -360,147 +265,140 @@ func resourceVPCRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error reading EC2 VPC (%s): %w", d.Id(), err) } - if vpc == nil { - if d.IsNewResource() { - return fmt.Errorf("error reading EC2 VPC (%s): not found after creation", d.Id()) - } - - log.Printf("[WARN] EC2 VPC (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil - } - - vpcid := d.Id() - d.Set("cidr_block", vpc.CidrBlock) - d.Set("dhcp_options_id", vpc.DhcpOptionsId) - d.Set("instance_tenancy", vpc.InstanceTenancy) + vpc := outputRaw.(*ec2.Vpc) - // ARN + ownerID := aws.StringValue(vpc.OwnerId) arn := arn.ARN{ Partition: meta.(*conns.AWSClient).Partition, Service: ec2.ServiceName, Region: meta.(*conns.AWSClient).Region, - AccountID: aws.StringValue(vpc.OwnerId), + AccountID: ownerID, Resource: fmt.Sprintf("vpc/%s", d.Id()), }.String() d.Set("arn", arn) + d.Set("cidr_block", vpc.CidrBlock) + d.Set("dhcp_options_id", vpc.DhcpOptionsId) + d.Set("instance_tenancy", vpc.InstanceTenancy) + d.Set("owner_id", ownerID) - tags := KeyValueTags(vpc.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) - - //lintignore:AWSR002 - if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + if v, err := FindVPCClassicLinkEnabled(conn, d.Id()); err != nil { + if tfresource.NotFound(err) { + d.Set("enable_classiclink", nil) + } else { + return fmt.Errorf("error reading EC2 VPC (%s) ClassicLinkEnabled: %w", d.Id(), err) + } + } else { + d.Set("enable_classiclink", v) } - if err := d.Set("tags_all", tags.Map()); err != nil { - return fmt.Errorf("error setting tags_all: %s", err) + if v, err := FindVPCClassicLinkDnsSupported(conn, d.Id()); err != nil { + if tfresource.NotFound(err) { + d.Set("enable_classiclink_dns_support", nil) + } else { + return fmt.Errorf("error reading EC2 VPC (%s) ClassicLinkDnsSupported: %w", d.Id(), err) + } + } else { + d.Set("enable_classiclink_dns_support", v) } - d.Set("owner_id", vpc.OwnerId) - - // Make sure those values are set, if an IPv6 block exists it'll be set in the loop - d.Set("ipv6_association_id", "") - d.Set("ipv6_cidr_block", "") - // assign_generated_ipv6_cidr_block is not returned by the API - // leave unassigned if not referenced - if v := d.Get("assign_generated_ipv6_cidr_block"); v != "" { - d.Set("assign_generated_ipv6_cidr_block", aws.Bool(v.(bool))) - } - for _, a := range vpc.Ipv6CidrBlockAssociationSet { - if aws.StringValue(a.Ipv6CidrBlockState.State) == ec2.VpcCidrBlockStateCodeAssociated { //we can only ever have 1 IPv6 block associated at once - d.Set("ipv6_association_id", a.AssociationId) - d.Set("ipv6_cidr_block", a.Ipv6CidrBlock) - d.Set("ipv6_cidr_block_network_border_group", a.NetworkBorderGroup) - } + if v, err := FindVPCAttribute(conn, d.Id(), ec2.VpcAttributeNameEnableDnsHostnames); err != nil { + return fmt.Errorf("error reading EC2 VPC (%s) Attribute (%s): %w", d.Id(), ec2.VpcAttributeNameEnableDnsHostnames, err) + } else { + d.Set("enable_dns_hostnames", v) } - // assign ipv6_cidr_block_network_border_group - if v := d.Get("ipv6_cidr_block_network_border_group"); v != "" { - d.Set("ipv6_cidr_block_network_border_group", v.(string)) + if v, err := FindVPCAttribute(conn, d.Id(), ec2.VpcAttributeNameEnableDnsSupport); err != nil { + return fmt.Errorf("error reading EC2 VPC (%s) Attribute (%s): %w", d.Id(), ec2.VpcAttributeNameEnableDnsSupport, err) + } else { + d.Set("enable_dns_support", v) } - enableDnsHostnames, err := FindVPCAttribute(conn, aws.StringValue(vpc.VpcId), ec2.VpcAttributeNameEnableDnsHostnames) + nacl, err := FindVPCDefaultNetworkACL(conn, d.Id()) if err != nil { - return fmt.Errorf("error reading EC2 VPC (%s) Attribute (%s): %w", aws.StringValue(vpc.VpcId), ec2.VpcAttributeNameEnableDnsHostnames, err) + return fmt.Errorf("error reading EC2 VPC (%s) default NACL: %w", d.Id(), err) } - d.Set("enable_dns_hostnames", enableDnsHostnames) + d.Set("default_network_acl_id", nacl.NetworkAclId) - enableDnsSupport, err := FindVPCAttribute(conn, aws.StringValue(vpc.VpcId), ec2.VpcAttributeNameEnableDnsSupport) + routeTable, err := FindVPCMainRouteTable(conn, d.Id()) if err != nil { - return fmt.Errorf("error reading EC2 VPC (%s) Attribute (%s): %w", aws.StringValue(vpc.VpcId), ec2.VpcAttributeNameEnableDnsSupport, err) + return fmt.Errorf("error reading EC2 VPC (%s) main Route Table: %w", d.Id(), err) } - d.Set("enable_dns_support", enableDnsSupport) + d.Set("default_route_table_id", routeTable.RouteTableId) + d.Set("main_route_table_id", routeTable.RouteTableId) - describeClassiclinkOpts := &ec2.DescribeVpcClassicLinkInput{ - VpcIds: []*string{&vpcid}, - } + securityGroup, err := FindVPCDefaultSecurityGroup(conn, d.Id()) - // Classic Link is only available in regions that support EC2 Classic - respClassiclink, err := conn.DescribeVpcClassicLink(describeClassiclinkOpts) if err != nil { - if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "UnsupportedOperation" { - log.Printf("[WARN] VPC Classic Link is not supported in this region") - } else if tfawserr.ErrMessageContains(err, "InvalidVpcID.NotFound", "") { - log.Printf("[WARN] VPC Classic Link functionality you requested is not available for this VPC") - } else { - return err - } - } else { - classiclink_enabled := false - for _, v := range respClassiclink.Vpcs { - if aws.StringValue(v.VpcId) == vpcid { - if v.ClassicLinkEnabled != nil { - classiclink_enabled = aws.BoolValue(v.ClassicLinkEnabled) - } + return fmt.Errorf("error reading EC2 VPC (%s) default Security Group: %w", d.Id(), err) + } + + d.Set("default_security_group_id", securityGroup.GroupId) + + d.Set("assign_generated_ipv6_cidr_block", nil) + d.Set("ipv6_cidr_block", nil) + d.Set("ipv6_cidr_block_network_border_group", nil) + d.Set("ipv6_ipam_pool_id", nil) + d.Set("ipv6_netmask_length", nil) + + // Try and find IPv6 CIDR block information, first by any stored association ID. + // Then if no IPv6 CIDR block information is available, use the first associated IPv6 CIDR block. + var ipv6CIDRBlockAssociation *ec2.VpcIpv6CidrBlockAssociation + if associationID := d.Get("ipv6_association_id").(string); associationID != "" { + for _, v := range vpc.Ipv6CidrBlockAssociationSet { + if state := aws.StringValue(v.Ipv6CidrBlockState.State); state == ec2.VpcCidrBlockStateCodeAssociated && aws.StringValue(v.AssociationId) == associationID { + ipv6CIDRBlockAssociation = v + break } } - d.Set("enable_classiclink", classiclink_enabled) } - - describeClassiclinkDnsOpts := &ec2.DescribeVpcClassicLinkDnsSupportInput{ - VpcIds: []*string{&vpcid}, - } - - respClassiclinkDnsSupport, err := conn.DescribeVpcClassicLinkDnsSupport(describeClassiclinkDnsOpts) - if err != nil { - if tfawserr.ErrMessageContains(err, "UnsupportedOperation", "The functionality you requested is not available in this region") || - tfawserr.ErrMessageContains(err, "AuthFailure", "This request has been administratively disabled") { - log.Printf("[WARN] VPC Classic Link DNS Support is not supported in this region") - } else { - return err + if ipv6CIDRBlockAssociation == nil { + for _, v := range vpc.Ipv6CidrBlockAssociationSet { + if aws.StringValue(v.Ipv6CidrBlockState.State) == ec2.VpcCidrBlockStateCodeAssociated { + ipv6CIDRBlockAssociation = v + } } + } + if ipv6CIDRBlockAssociation == nil { + d.Set("ipv6_association_id", nil) } else { - classiclinkdns_enabled := false - for _, v := range respClassiclinkDnsSupport.Vpcs { - if aws.StringValue(v.VpcId) == vpcid { - if v.ClassicLinkDnsSupported != nil { - classiclinkdns_enabled = aws.BoolValue(v.ClassicLinkDnsSupported) + cidrBlock := aws.StringValue(ipv6CIDRBlockAssociation.Ipv6CidrBlock) + ipv6PoolID := aws.StringValue(ipv6CIDRBlockAssociation.Ipv6Pool) + isAmazonIPv6Pool := ipv6PoolID == AmazonIPv6PoolID + d.Set("assign_generated_ipv6_cidr_block", isAmazonIPv6Pool) + d.Set("ipv6_association_id", ipv6CIDRBlockAssociation.AssociationId) + d.Set("ipv6_cidr_block", cidrBlock) + d.Set("ipv6_cidr_block_network_border_group", ipv6CIDRBlockAssociation.NetworkBorderGroup) + if !isAmazonIPv6Pool { + d.Set("ipv6_ipam_pool_id", ipv6PoolID) + } + if ipv6PoolID != "" && !isAmazonIPv6Pool { + parts := strings.Split(cidrBlock, "/") + if len(parts) == 2 { + if v, err := strconv.Atoi(parts[1]); err != nil { + d.Set("ipv6_netmask_length", v) + } else { + log.Printf("[WARN] Unable to parse CIDR (%s) netmask length: %s", cidrBlock, err) } - break + } else { + log.Printf("[WARN] Invalid CIDR block format: %s", cidrBlock) } } - d.Set("enable_classiclink_dns_support", classiclinkdns_enabled) } - routeTableId, err := resourceVPCSetMainRouteTable(conn, vpcid) - if err != nil { - log.Printf("[WARN] Unable to set Main Route Table: %s", err) - } - d.Set("main_route_table_id", routeTableId) + tags := KeyValueTags(vpc.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) - if err := resourceVPCSetDefaultNetworkACL(conn, d); err != nil { - log.Printf("[WARN] Unable to set Default Network ACL: %s", err) - } - if err := resourceVPCSetDefaultSecurityGroup(conn, d); err != nil { - log.Printf("[WARN] Unable to set Default Security Group: %s", err) + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) } - if err := resourceVPCSetDefaultRouteTable(conn, d); err != nil { - log.Printf("[WARN] Unable to set Default Route Table: %s", err) + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } return nil @@ -509,259 +407,73 @@ func resourceVPCRead(d *schema.ResourceData, meta interface{}) error { func resourceVPCUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn - vpcid := d.Id() if d.HasChange("enable_dns_hostnames") { - input := &ec2.ModifyVpcAttributeInput{ - VpcId: aws.String(d.Id()), - EnableDnsHostnames: &ec2.AttributeBooleanValue{ - Value: aws.Bool(d.Get("enable_dns_hostnames").(bool)), - }, - } - - if _, err := conn.ModifyVpcAttribute(input); err != nil { - return fmt.Errorf("error updating EC2 VPC (%s) DNS Hostnames: %w", d.Id(), err) - } - - if _, err := WaitVPCAttributeUpdated(conn, d.Id(), ec2.VpcAttributeNameEnableDnsHostnames, d.Get("enable_dns_hostnames").(bool)); err != nil { - return fmt.Errorf("error waiting for EC2 VPC (%s) DNS Hostnames update: %w", d.Id(), err) + if err := modifyVPCDnsHostnames(conn, d.Id(), d.Get("enable_dns_hostnames").(bool)); err != nil { + return err } } - _, hasEnableDnsSupportOption := d.GetOk("enable_dns_support") - - if !hasEnableDnsSupportOption || d.HasChange("enable_dns_support") { - input := &ec2.ModifyVpcAttributeInput{ - VpcId: aws.String(d.Id()), - EnableDnsSupport: &ec2.AttributeBooleanValue{ - Value: aws.Bool(d.Get("enable_dns_support").(bool)), - }, - } - - if _, err := conn.ModifyVpcAttribute(input); err != nil { - return fmt.Errorf("error updating EC2 VPC (%s) DNS Support: %w", d.Id(), err) - } - - if _, err := WaitVPCAttributeUpdated(conn, d.Id(), ec2.VpcAttributeNameEnableDnsSupport, d.Get("enable_dns_support").(bool)); err != nil { - return fmt.Errorf("error waiting for EC2 VPC (%s) DNS Support update: %w", d.Id(), err) + if d.HasChange("enable_dns_support") { + if err := modifyVPCDnsSupport(conn, d.Id(), d.Get("enable_dns_support").(bool)); err != nil { + return err } } if d.HasChange("enable_classiclink") { - val := d.Get("enable_classiclink").(bool) - if val { - modifyOpts := &ec2.EnableVpcClassicLinkInput{ - VpcId: &vpcid, - } - log.Printf( - "[INFO] Modifying enable_classiclink vpc attribute for %s: %#v", - d.Id(), modifyOpts) - if _, err := conn.EnableVpcClassicLink(modifyOpts); err != nil { - return err - } - } else { - modifyOpts := &ec2.DisableVpcClassicLinkInput{ - VpcId: &vpcid, - } - log.Printf( - "[INFO] Modifying enable_classiclink vpc attribute for %s: %#v", - d.Id(), modifyOpts) - if _, err := conn.DisableVpcClassicLink(modifyOpts); err != nil { - return err - } + if err := modifyVPCClassicLink(conn, d.Id(), d.Get("enable_classiclink").(bool)); err != nil { + return err } } if d.HasChange("enable_classiclink_dns_support") { - val := d.Get("enable_classiclink_dns_support").(bool) - if val { - modifyOpts := &ec2.EnableVpcClassicLinkDnsSupportInput{ - VpcId: &vpcid, - } - log.Printf( - "[INFO] Modifying enable_classiclink_dns_support vpc attribute for %s: %#v", - d.Id(), modifyOpts) - if _, err := conn.EnableVpcClassicLinkDnsSupport(modifyOpts); err != nil { - return err - } - } else { - modifyOpts := &ec2.DisableVpcClassicLinkDnsSupportInput{ - VpcId: &vpcid, - } - log.Printf( - "[INFO] Modifying enable_classiclink_dns_support vpc attribute for %s: %#v", - d.Id(), modifyOpts) - if _, err := conn.DisableVpcClassicLinkDnsSupport(modifyOpts); err != nil { - return err - } + if err := modifyVPCClassicLinkDnsSupport(conn, d.Id(), d.Get("enable_classiclink_dns_support").(bool)); err != nil { + return err } } - if d.HasChanges("assign_generated_ipv6_cidr_block", "ipv6_cidr_block_network_border_group") { - toAssign := d.Get("assign_generated_ipv6_cidr_block").(bool) - borderNetworkGroup := d.Get("ipv6_cidr_block_network_border_group").(string) - existingCIDR := d.Get("ipv6_cidr_block").(string) - - log.Printf("[INFO] Modifying assign_generated_ipv6_cidr_block to %#v", toAssign) - - if toAssign && borderNetworkGroup != "" { - // if an existing IPv6 CIDR block is assigned, we need to unassign it first - if existingCIDR != "" { - associationID := d.Get("ipv6_association_id").(string) - modifyOpts := &ec2.DisassociateVpcCidrBlockInput{ - AssociationId: aws.String(associationID), - } - log.Printf("[INFO] Disabling assign_generated_ipv6_cidr_block vpc attribute for %s: %#v", - d.Id(), modifyOpts) - if _, err := conn.DisassociateVpcCidrBlock(modifyOpts); err != nil { - return err - } - - log.Printf("[DEBUG] Waiting for EC2 VPC (%s) IPv6 CIDR to become disassociated", d.Id()) - - if err := waitForEc2VpcIpv6CidrBlockAssociationDelete(conn, d.Id(), associationID); err != nil { - return fmt.Errorf("error waiting for EC2 VPC (%s) IPv6 CIDR to become disassociated: %s", d.Id(), err) - } - } - - modifyOpts := &ec2.AssociateVpcCidrBlockInput{ - VpcId: &vpcid, - AmazonProvidedIpv6CidrBlock: aws.Bool(toAssign), - Ipv6CidrBlockNetworkBorderGroup: aws.String(borderNetworkGroup), - } - log.Printf("[INFO] Enabling assign_generated_ipv6_cidr_block vpc attribute for %s: %#v with border network group %s", - d.Id(), modifyOpts, borderNetworkGroup) - - resp, err := conn.AssociateVpcCidrBlock(modifyOpts) - - if err != nil { - return err - } - - log.Printf("[DEBUG] Waiting for EC2 VPC (%s) IPv6 CIDR to become associated", d.Id()) - - if err := waitForEc2VpcIpv6CidrBlockAssociationCreate(conn, d.Id(), aws.StringValue(resp.Ipv6CidrBlockAssociation.AssociationId)); err != nil { - return fmt.Errorf("error waiting for EC2 VPC (%s) IPv6 CIDR to become associated: %s", d.Id(), err) - } - } - // if no IPv6 CIDR block is assigned, we need to unassign the existing one - if !toAssign { - associationID := d.Get("ipv6_association_id").(string) - modifyOpts := &ec2.DisassociateVpcCidrBlockInput{ - AssociationId: aws.String(associationID), - } - log.Printf("[INFO] Disabling assign_generated_ipv6_cidr_block vpc attribute for %s: %#v", - d.Id(), modifyOpts) - if _, err := conn.DisassociateVpcCidrBlock(modifyOpts); err != nil { - return err - } - - log.Printf("[DEBUG] Waiting for EC2 VPC (%s) IPv6 CIDR to become disassociated", d.Id()) - if err := waitForEc2VpcIpv6CidrBlockAssociationDelete(conn, d.Id(), associationID); err != nil { - return fmt.Errorf("error waiting for EC2 VPC (%s) IPv6 CIDR to become disassociated: %s", d.Id(), err) - } - } - // if an IPv6 CIDR blosk is to be assigned and no network border group is specified - // just create the new association and remove the existing one if a border group is configured - if toAssign && borderNetworkGroup == "" { - log.Printf("[INFO] Modifying IPv6 Block Network Border Group") - modifyOpts := &ec2.AssociateVpcCidrBlockInput{ - VpcId: &vpcid, - AmazonProvidedIpv6CidrBlock: aws.Bool(d.Get("assign_generated_ipv6_cidr_block").(bool)), - } - if val := d.Get("ipv6_cidr_block"); val != "" { - log.Printf("[INFO] Disabling assign_generated_ipv6_cidr_block vpc attribute for %s: %#v", - d.Id(), modifyOpts) - disassociationID := d.Get("ipv6_association_id").(string) - disModifyOpts := &ec2.DisassociateVpcCidrBlockInput{ - AssociationId: aws.String(disassociationID), - } - log.Printf("[INFO] Dissaociating IPv6 Block Network Border Group") - if _, err := conn.DisassociateVpcCidrBlock(disModifyOpts); err != nil { - return err - } - } - - if v := d.Get("ipv6_cidr_block_network_border_group"); v != "" { - modifyOpts.Ipv6CidrBlockNetworkBorderGroup = aws.String(v.(string)) - log.Printf("[INFO] Trying to associate IPv6 Block Network Border Group") - if _, err := conn.AssociateVpcCidrBlock(modifyOpts); err != nil { - return err - } - } - if v := d.Get("ipv6_cidr_block_network_border_group"); v == "" { - associationID := d.Get("ipv6_association_id").(string) - modifyOpts := &ec2.DisassociateVpcCidrBlockInput{ - AssociationId: aws.String(associationID), - } - log.Printf("[INFO] Dissaociating IPv6 Block Network Border Group") - if _, err := conn.DisassociateVpcCidrBlock(modifyOpts); err != nil { - return err - } - if d.Get("assign_generated_ipv6_cidr_block").(bool) { - log.Printf("[INFO] Trying to associate IPv6 Block Network Border Group") - modifyOpts := &ec2.AssociateVpcCidrBlockInput{ - VpcId: &vpcid, - AmazonProvidedIpv6CidrBlock: aws.Bool(d.Get("assign_generated_ipv6_cidr_block").(bool)), - } - if _, err := conn.AssociateVpcCidrBlock(modifyOpts); err != nil { - return err - } - } - } + if d.HasChange("instance_tenancy") { + if err := modifyVPCTenancy(conn, d.Id(), d.Get("instance_tenancy").(string)); err != nil { + return err } } - if d.HasChanges("ipv6_cidr_block", "ipv6_ipam_pool_id") { - log.Printf("[INFO] Modifying ipam IPv6 CIDR") + if d.HasChanges("assign_generated_ipv6_cidr_block", "ipv6_cidr_block_network_border_group") { + associationID, err := modifyVPCIPv6CIDRBlockAssociation(conn, d.Id(), + d.Get("ipv6_association_id").(string), + d.Get("assign_generated_ipv6_cidr_block").(bool), + "", + "", + 0, + d.Get("ipv6_cidr_block_network_border_group").(string)) - // if assoc id exists it needs to be disassociated - if v, ok := d.GetOk("ipv6_association_id"); ok { - if err := ipv6DisassociateCidrBlock(conn, d.Id(), v.(string)); err != nil { - return err - } + if err != nil { + return err } - if v := d.Get("ipv6_ipam_pool_id"); v != "" { - modifyOpts := &ec2.AssociateVpcCidrBlockInput{ - VpcId: &vpcid, - Ipv6IpamPoolId: aws.String(v.(string)), - } - - if v := d.Get("ipv6_netmask_length"); v != 0 { - modifyOpts.Ipv6NetmaskLength = aws.Int64(int64(v.(int))) - } - - if v := d.Get("ipv6_cidr_block"); v != "" { - modifyOpts.Ipv6CidrBlock = aws.String(v.(string)) - } - resp, err := conn.AssociateVpcCidrBlock(modifyOpts) - if err != nil { - return err - } - if err := waitForEc2VpcIpv6CidrBlockAssociationCreate(conn, d.Id(), aws.StringValue(resp.Ipv6CidrBlockAssociation.AssociationId)); err != nil { - return fmt.Errorf("error waiting for EC2 VPC (%s) IPv6 CIDR to become associated: %w", d.Id(), err) - } - } + d.Set("ipv6_association_id", associationID) } - if d.HasChange("instance_tenancy") { - modifyOpts := &ec2.ModifyVpcTenancyInput{ - VpcId: aws.String(vpcid), - InstanceTenancy: aws.String(d.Get("instance_tenancy").(string)), - } - log.Printf( - "[INFO] Modifying instance_tenancy vpc attribute for %s: %#v", - d.Id(), modifyOpts) - if _, err := conn.ModifyVpcTenancy(modifyOpts); err != nil { + if d.HasChanges("ipv6_cidr_block", "ipv6_ipam_pool_id") { + associationID, err := modifyVPCIPv6CIDRBlockAssociation(conn, d.Id(), + d.Get("ipv6_association_id").(string), + false, + d.Get("ipv6_cidr_block").(string), + d.Get("ipv6_ipam_pool_id").(string), + d.Get("ipv6_netmask_length").(int), + "") + + if err != nil { return err } + + d.Set("ipv6_association_id", associationID) } if d.HasChange("tags_all") { o, n := d.GetChange("tags_all") if err := UpdateTags(conn, d.Id(), o, n); err != nil { - return fmt.Errorf("error updating tags: %s", err) + return fmt.Errorf("error updating EC2 VPC (%s) tags: %w", d.Id(), err) } } @@ -770,64 +482,33 @@ func resourceVPCUpdate(d *schema.ResourceData, meta interface{}) error { func resourceVPCDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn - vpcID := d.Id() - deleteVpcOpts := &ec2.DeleteVpcInput{ - VpcId: &vpcID, + + input := &ec2.DeleteVpcInput{ + VpcId: aws.String(d.Id()), } - log.Printf("[INFO] Deleting VPC: %s", d.Id()) - err := resource.Retry(5*time.Minute, func() *resource.RetryError { - _, err := conn.DeleteVpc(deleteVpcOpts) - if err == nil { - return nil - } + log.Printf("[INFO] Deleting EC2 VPC: %s", d.Id()) + _, err := tfresource.RetryWhenAWSErrCodeEquals(vpcDeletedTimeout, func() (interface{}, error) { + return conn.DeleteVpc(input) + }, ErrCodeDependencyViolation) - if tfawserr.ErrMessageContains(err, "InvalidVpcID.NotFound", "") { - return nil - } - if tfawserr.ErrMessageContains(err, "DependencyViolation", "") { - return resource.RetryableError(err) - } - return resource.NonRetryableError(fmt.Errorf("Error deleting VPC: %s", err)) - }) - if tfresource.TimedOut(err) { - _, err = conn.DeleteVpc(deleteVpcOpts) - if tfawserr.ErrMessageContains(err, "InvalidVpcID.NotFound", "") { - return nil - } + if tfawserr.ErrCodeEquals(err, ErrCodeInvalidVpcIDNotFound) { + return nil } if err != nil { - return fmt.Errorf("Error deleting VPC: %s", err) + return fmt.Errorf("error deleting EC2 VPC (%s): %w", d.Id(), err) } + return nil } -func ipv6DisassociateCidrBlock(conn *ec2.EC2, id, allocationId string) error { - log.Printf("[INFO] Disassociating IPv6 CIDR association id: %s", allocationId) - modifyOpts := &ec2.DisassociateVpcCidrBlockInput{ - AssociationId: aws.String(allocationId), - } - if _, err := conn.DisassociateVpcCidrBlock(modifyOpts); err != nil { - return err - } - log.Printf("[DEBUG] Waiting for EC2 VPC (%s) IPv6 CIDR to become disassociated", id) - if err := waitForEc2VpcIpv6CidrBlockAssociationDelete(conn, id, allocationId); err != nil { - return fmt.Errorf("error waiting for EC2 VPC (%s) IPv6 CIDR to become disassociated: %w", id, err) - } - - return nil +func resourceVPCImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + d.Set("assign_generated_ipv6_cidr_block", false) + return []*schema.ResourceData{d}, nil } func resourceVPCCustomizeDiff(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { - if diff.HasChange("assign_generated_ipv6_cidr_block") { - if err := diff.SetNewComputed("ipv6_association_id"); err != nil { - return fmt.Errorf("error setting ipv6_association_id to computed: %s", err) - } - if err := diff.SetNewComputed("ipv6_cidr_block"); err != nil { - return fmt.Errorf("error setting ipv6_cidr_block to computed: %s", err) - } - } if diff.HasChange("instance_tenancy") { old, new := diff.GetChange("instance_tenancy") if old.(string) != ec2.TenancyDedicated || new.(string) != ec2.TenancyDefault { @@ -835,235 +516,211 @@ func resourceVPCCustomizeDiff(_ context.Context, diff *schema.ResourceDiff, v in } } + // cidr_block can be set by a value returned from IPAM or explicitly in config. + if diff.Id() != "" && diff.HasChange("cidr_block") { + // If netmask is set then cidr_block is derived from IPAM, ignore changes. + if diff.Get("ipv4_netmask_length") != 0 { + return diff.Clear("cidr_block") + } + return diff.ForceNew("cidr_block") + } + return nil } -// VPCStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch -// a VPC. -func VPCStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc { - return func() (interface{}, string, error) { - describeVpcOpts := &ec2.DescribeVpcsInput{ - VpcIds: []*string{aws.String(id)}, +type vpcInfo struct { + vpc *ec2.Vpc + enableClassicLink bool + enableClassicLinkDNSSupport bool + enableDnsHostnames bool + enableDnsSupport bool +} + +// modifyVPCAttributesOnCreate sets VPC attributes on resource Create. +// Called after new VPC creation or existing default VPC adoption. +func modifyVPCAttributesOnCreate(conn *ec2.EC2, d *schema.ResourceData, vpcInfo *vpcInfo) error { + if new, old := d.Get("enable_dns_hostnames").(bool), vpcInfo.enableDnsHostnames; old != new { + if err := modifyVPCDnsHostnames(conn, d.Id(), new); err != nil { + return err } - resp, err := conn.DescribeVpcs(describeVpcOpts) - if err != nil { - if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidVpcID.NotFound" { - resp = nil - } else { - log.Printf("Error on VPCStateRefresh: %s", err) - return nil, "", err - } + } + + if new, old := d.Get("enable_dns_support").(bool), vpcInfo.enableDnsSupport; old != new { + if err := modifyVPCDnsSupport(conn, d.Id(), new); err != nil { + return err } + } - if resp == nil { - // Sometimes AWS just has consistency issues and doesn't see - // our instance yet. Return an empty state. - return nil, "", nil + if new, old := d.Get("enable_classiclink").(bool), vpcInfo.enableClassicLink; old != new { + if err := modifyVPCClassicLink(conn, d.Id(), new); err != nil { + return err } + } - vpc := resp.Vpcs[0] - return vpc, *vpc.State, nil + if new, old := d.Get("enable_classiclink_dns_support").(bool), vpcInfo.enableClassicLinkDNSSupport; old != new { + if err := modifyVPCClassicLinkDnsSupport(conn, d.Id(), new); err != nil { + return err + } } + + return nil } -func Ipv6CidrStateRefreshFunc(conn *ec2.EC2, id string, associationId string) resource.StateRefreshFunc { - return func() (interface{}, string, error) { - describeVpcOpts := &ec2.DescribeVpcsInput{ - VpcIds: []*string{aws.String(id)}, +func modifyVPCClassicLink(conn *ec2.EC2, vpcID string, v bool) error { + if v { + input := &ec2.EnableVpcClassicLinkInput{ + VpcId: aws.String(vpcID), } - resp, err := conn.DescribeVpcs(describeVpcOpts) - if tfawserr.ErrMessageContains(err, "InvalidVpcID.NotFound", "") { - return nil, "", nil + if _, err := conn.EnableVpcClassicLink(input); err != nil { + return fmt.Errorf("error enabling EC2 VPC (%s) ClassicLink: %w", vpcID, err) + } + } else { + input := &ec2.DisableVpcClassicLinkInput{ + VpcId: aws.String(vpcID), } - if err != nil { - return nil, "", err + if _, err := conn.DisableVpcClassicLink(input); err != nil { + return fmt.Errorf("error disabling EC2 VPC (%s) ClassicLink: %w", vpcID, err) } + } - if resp == nil || len(resp.Vpcs) == 0 || resp.Vpcs[0] == nil || resp.Vpcs[0].Ipv6CidrBlockAssociationSet == nil { - // Sometimes AWS just has consistency issues and doesn't see - // our instance yet. Return an empty state. - return nil, "", nil + return nil +} + +func modifyVPCClassicLinkDnsSupport(conn *ec2.EC2, vpcID string, v bool) error { + if v { + input := &ec2.EnableVpcClassicLinkDnsSupportInput{ + VpcId: aws.String(vpcID), } - for _, association := range resp.Vpcs[0].Ipv6CidrBlockAssociationSet { - if aws.StringValue(association.AssociationId) == associationId { - return association, aws.StringValue(association.Ipv6CidrBlockState.State), nil - } + if _, err := conn.EnableVpcClassicLinkDnsSupport(input); err != nil { + return fmt.Errorf("error enabling EC2 VPC (%s) ClassicLinkDnsSupport: %w", vpcID, err) + } + } else { + input := &ec2.DisableVpcClassicLinkDnsSupportInput{ + VpcId: aws.String(vpcID), } - return nil, "", nil + if _, err := conn.DisableVpcClassicLinkDnsSupport(input); err != nil { + return fmt.Errorf("error disabling EC2 VPC (%s) ClassicLinkDnsSupport: %w", vpcID, err) + } } + + return nil } -func resourceVPCSetDefaultNetworkACL(conn *ec2.EC2, d *schema.ResourceData) error { - filter1 := &ec2.Filter{ - Name: aws.String("default"), - Values: []*string{aws.String("true")}, - } - filter2 := &ec2.Filter{ - Name: aws.String("vpc-id"), - Values: []*string{aws.String(d.Id())}, - } - describeNetworkACLOpts := &ec2.DescribeNetworkAclsInput{ - Filters: []*ec2.Filter{filter1, filter2}, +func modifyVPCDnsHostnames(conn *ec2.EC2, vpcID string, v bool) error { + input := &ec2.ModifyVpcAttributeInput{ + EnableDnsHostnames: &ec2.AttributeBooleanValue{ + Value: aws.Bool(v), + }, + VpcId: aws.String(vpcID), } - networkAclResp, err := conn.DescribeNetworkAcls(describeNetworkACLOpts) - if err != nil { - return err + if _, err := conn.ModifyVpcAttribute(input); err != nil { + return fmt.Errorf("error modifying EC2 VPC (%s) EnableDnsHostnames: %w", vpcID, err) } - if v := networkAclResp.NetworkAcls; len(v) > 0 { - d.Set("default_network_acl_id", v[0].NetworkAclId) + + if _, err := WaitVPCAttributeUpdated(conn, vpcID, ec2.VpcAttributeNameEnableDnsHostnames, v); err != nil { + return fmt.Errorf("error waiting for EC2 VPC (%s) EnableDnsHostnames update: %w", vpcID, err) } return nil } -func resourceVPCSetDefaultSecurityGroup(conn *ec2.EC2, d *schema.ResourceData) error { - filter1 := &ec2.Filter{ - Name: aws.String("group-name"), - Values: []*string{aws.String("default")}, - } - filter2 := &ec2.Filter{ - Name: aws.String("vpc-id"), - Values: []*string{aws.String(d.Id())}, - } - describeSgOpts := &ec2.DescribeSecurityGroupsInput{ - Filters: []*ec2.Filter{filter1, filter2}, +func modifyVPCDnsSupport(conn *ec2.EC2, vpcID string, v bool) error { + input := &ec2.ModifyVpcAttributeInput{ + EnableDnsSupport: &ec2.AttributeBooleanValue{ + Value: aws.Bool(v), + }, + VpcId: aws.String(vpcID), } - securityGroupResp, err := conn.DescribeSecurityGroups(describeSgOpts) - if err != nil { - return err + if _, err := conn.ModifyVpcAttribute(input); err != nil { + return fmt.Errorf("error modifying EC2 VPC (%s) EnableDnsSupport: %w", vpcID, err) } - if v := securityGroupResp.SecurityGroups; len(v) > 0 { - d.Set("default_security_group_id", v[0].GroupId) + + if _, err := WaitVPCAttributeUpdated(conn, vpcID, ec2.VpcAttributeNameEnableDnsSupport, v); err != nil { + return fmt.Errorf("error waiting for EC2 VPC (%s) EnableDnsSupport update: %w", vpcID, err) } return nil } -func resourceVPCSetDefaultRouteTable(conn *ec2.EC2, d *schema.ResourceData) error { - filter1 := &ec2.Filter{ - Name: aws.String("association.main"), - Values: []*string{aws.String("true")}, - } - filter2 := &ec2.Filter{ - Name: aws.String("vpc-id"), - Values: []*string{aws.String(d.Id())}, - } - - findOpts := &ec2.DescribeRouteTablesInput{ - Filters: []*ec2.Filter{filter1, filter2}, - } - - resp, err := conn.DescribeRouteTables(findOpts) - if err != nil { - return err - } +// modifyVPCIPv6CIDRBlockAssociation modify's a VPC's IPv6 CIDR block association. +// Any exiting association is deleted and any new association's ID is returned. +func modifyVPCIPv6CIDRBlockAssociation(conn *ec2.EC2, vpcID, associationID string, amazonProvidedCIDRBlock bool, cidrBlock, ipamPoolID string, netmaskLength int, networkBorderGroup string) (string, error) { + if associationID != "" { + input := &ec2.DisassociateVpcCidrBlockInput{ + AssociationId: aws.String(associationID), + } - if len(resp.RouteTables) < 1 || resp.RouteTables[0] == nil { - return fmt.Errorf("Default Route table not found") - } + _, err := conn.DisassociateVpcCidrBlock(input) - // There Can Be Only 1 ... Default Route Table - d.Set("default_route_table_id", resp.RouteTables[0].RouteTableId) + if err != nil { + return "", fmt.Errorf("error disassociating EC2 VPC (%s) CIDR block (%s): %w", vpcID, associationID, err) + } - return nil -} + _, err = WaitVPCIPv6CIDRBlockAssociationDeleted(conn, associationID, vpcIPv6CIDRBlockAssociationDeletedTimeout) -func resourceVPCSetMainRouteTable(conn *ec2.EC2, vpcid string) (string, error) { - filter1 := &ec2.Filter{ - Name: aws.String("association.main"), - Values: []*string{aws.String("true")}, - } - filter2 := &ec2.Filter{ - Name: aws.String("vpc-id"), - Values: []*string{aws.String(vpcid)}, + if err != nil { + return "", fmt.Errorf("error waiting for EC2 VPC (%s) IPv6 CIDR block (%s) to become disassociated: %w", vpcID, associationID, err) + } } - findOpts := &ec2.DescribeRouteTablesInput{ - Filters: []*ec2.Filter{filter1, filter2}, - } + if amazonProvidedCIDRBlock || cidrBlock != "" || ipamPoolID != "" { + input := &ec2.AssociateVpcCidrBlockInput{ + VpcId: aws.String(vpcID), + } - resp, err := conn.DescribeRouteTables(findOpts) - if err != nil { - return "", err - } + if amazonProvidedCIDRBlock { + input.AmazonProvidedIpv6CidrBlock = aws.Bool(amazonProvidedCIDRBlock) + } - if len(resp.RouteTables) < 1 || resp.RouteTables[0] == nil { - return "", fmt.Errorf("Main Route table not found") - } + if cidrBlock != "" { + input.Ipv6CidrBlock = aws.String(cidrBlock) + } - // There Can Be Only 1 Main Route Table for a VPC - return aws.StringValue(resp.RouteTables[0].RouteTableId), nil -} + if networkBorderGroup != "" { + input.Ipv6CidrBlockNetworkBorderGroup = aws.String(networkBorderGroup) + } -func resourceVPCInstanceImport( - d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - d.Set("assign_generated_ipv6_cidr_block", false) - return []*schema.ResourceData{d}, nil -} + if ipamPoolID != "" { + input.Ipv6IpamPoolId = aws.String(ipamPoolID) + } -// vpcDescribe returns EC2 API information about the specified VPC. -// If the VPC doesn't exist, return nil. -func vpcDescribe(conn *ec2.EC2, vpcId string) (*ec2.Vpc, error) { - resp, err := conn.DescribeVpcs(&ec2.DescribeVpcsInput{ - VpcIds: aws.StringSlice([]string{vpcId}), - }) - if err != nil { - if !tfawserr.ErrMessageContains(err, "InvalidVpcID.NotFound", "") { - return nil, err + if netmaskLength > 0 { + input.Ipv6NetmaskLength = aws.Int64(int64(netmaskLength)) } - resp = nil - } - if resp == nil { - return nil, nil - } + output, err := conn.AssociateVpcCidrBlock(input) - n := len(resp.Vpcs) - switch n { - case 0: - return nil, nil + if err != nil { + return "", fmt.Errorf("error associating EC2 VPC (%s) IPv6 CIDR block: %w", vpcID, err) + } - case 1: - return resp.Vpcs[0], nil + associationID = aws.StringValue(output.Ipv6CidrBlockAssociation.AssociationId) - default: - return nil, fmt.Errorf("Found %d VPCs for %s, expected 1", n, vpcId) - } -} + _, err = WaitVPCIPv6CIDRBlockAssociationCreated(conn, associationID, vpcIPv6CIDRBlockAssociationCreatedTimeout) -func waitForEc2VpcIpv6CidrBlockAssociationCreate(conn *ec2.EC2, vpcID, associationID string) error { - stateConf := &resource.StateChangeConf{ - Pending: []string{ - ec2.VpcCidrBlockStateCodeAssociating, - ec2.VpcCidrBlockStateCodeDisassociated, - }, - Target: []string{ec2.VpcCidrBlockStateCodeAssociated}, - Refresh: Ipv6CidrStateRefreshFunc(conn, vpcID, associationID), - Timeout: 10 * time.Minute, + if err != nil { + return "", fmt.Errorf("error waiting for EC2 VPC (%s) IPv6 CIDR block (%s) to become associated: %w", vpcID, associationID, err) + } } - _, err := stateConf.WaitForState() - return err + return associationID, nil } -func waitForEc2VpcIpv6CidrBlockAssociationDelete(conn *ec2.EC2, vpcID, associationID string) error { - stateConf := &resource.StateChangeConf{ - Pending: []string{ - ec2.VpcCidrBlockStateCodeAssociated, - ec2.VpcCidrBlockStateCodeDisassociating, - }, - Target: []string{ec2.VpcCidrBlockStateCodeDisassociated}, - Refresh: Ipv6CidrStateRefreshFunc(conn, vpcID, associationID), - Timeout: 5 * time.Minute, - NotFoundChecks: 1, +func modifyVPCTenancy(conn *ec2.EC2, vpcID string, v string) error { + input := &ec2.ModifyVpcTenancyInput{ + InstanceTenancy: aws.String(v), + VpcId: aws.String(vpcID), + } + + if _, err := conn.ModifyVpcTenancy(input); err != nil { + return fmt.Errorf("error modifying EC2 VPC (%s) Tenancy: %w", vpcID, err) } - _, err := stateConf.WaitForState() - return err + return nil } diff --git a/internal/service/ec2/vpc_byoip_test.go b/internal/service/ec2/vpc_byoip_test.go index 3b8324efd72..4aa2d95c489 100644 --- a/internal/service/ec2/vpc_byoip_test.go +++ b/internal/service/ec2/vpc_byoip_test.go @@ -5,16 +5,11 @@ import ( "os" "regexp" "strconv" - "strings" "testing" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" "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" ) // Due to the nature of byoip cidrs, we have each possible test represented as a single test with @@ -164,94 +159,6 @@ func TestAccVPCIpam_ByoipIPv6(t *testing.T) { }) } -func testAccCheckVPCIPv6CIDRBlockAssociationDestroy(s *terraform.State) error { - conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn - - for _, rs := range s.RootModule().Resources { - if rs.Type != "aws_vpc_ipv6_cidr_block_association" { - continue - } - - // Try to find the VPC - DescribeVpcOpts := &ec2.DescribeVpcsInput{ - VpcIds: []*string{aws.String(rs.Primary.Attributes["vpc_id"])}, - } - resp, err := conn.DescribeVpcs(DescribeVpcOpts) - if err == nil { - vpc := resp.Vpcs[0] - - for _, ipv6Association := range vpc.Ipv6CidrBlockAssociationSet { - if *ipv6Association.AssociationId == rs.Primary.ID { - return fmt.Errorf("VPC CIDR block association still exists") - } - } - - return nil - } - - // Verify the error is what we want - ec2err, ok := err.(awserr.Error) - if !ok { - return err - } - if ec2err.Code() != "InvalidVpcID.NotFound" { - return err - } - } - - return nil -} - -func testAccCheckVPCIPv6CIDRBlockAssociationExists(n string, association *ec2.VpcIpv6CidrBlockAssociation) 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 VPC ID is set") - } - - conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn - DescribeVpcOpts := &ec2.DescribeVpcsInput{ - VpcIds: []*string{aws.String(rs.Primary.Attributes["vpc_id"])}, - } - resp, err := conn.DescribeVpcs(DescribeVpcOpts) - if err != nil { - return err - } - if len(resp.Vpcs) == 0 { - return fmt.Errorf("VPC not found") - } - - vpc := resp.Vpcs[0] - found := false - for _, ipv6CidrAssociation := range vpc.Ipv6CidrBlockAssociationSet { - if *ipv6CidrAssociation.AssociationId == rs.Primary.ID { - *association = *ipv6CidrAssociation - found = true - } - } - - if !found { - return fmt.Errorf("VPC CIDR block association not found") - } - - return nil - } -} - -func testAccCheckVPCAssociationIPv6CIDRPrefix(association *ec2.VpcIpv6CidrBlockAssociation, expected string) resource.TestCheckFunc { - return func(s *terraform.State) error { - if strings.Split(aws.StringValue(association.Ipv6CidrBlock), "/")[1] != expected { - return fmt.Errorf("Bad cidr prefix: %s", aws.StringValue(association.Ipv6CidrBlock)) - } - - return nil - } -} - func testAccVPCIpamIPv6ByoipSkipExplicitCidr(t *testing.T, ipv6CidrVPC string) func() (bool, error) { return func() (bool, error) { if ipv6CidrVPC != "" { diff --git a/internal/service/ec2/vpc_data_source.go b/internal/service/ec2/vpc_data_source.go index bea303ca02a..59d39a1756c 100644 --- a/internal/service/ec2/vpc_data_source.go +++ b/internal/service/ec2/vpc_data_source.go @@ -2,7 +2,6 @@ package ec2 import ( "fmt" - "log" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/arn" @@ -10,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "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" ) func DataSourceVPC() *schema.Resource { @@ -21,13 +21,11 @@ func DataSourceVPC() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "cidr_block": { Type: schema.TypeString, Optional: true, Computed: true, }, - "cidr_block_associations": { Type: schema.TypeList, Computed: true, @@ -48,69 +46,56 @@ func DataSourceVPC() *schema.Resource { }, }, }, - "default": { Type: schema.TypeBool, Optional: true, Computed: true, }, - "dhcp_options_id": { Type: schema.TypeString, Optional: true, Computed: true, }, - "enable_dns_hostnames": { Type: schema.TypeBool, Computed: true, }, - "enable_dns_support": { Type: schema.TypeBool, Computed: true, }, - "filter": CustomFiltersSchema(), - "id": { Type: schema.TypeString, Optional: true, Computed: true, }, - "instance_tenancy": { Type: schema.TypeString, Computed: true, }, - "ipv6_cidr_block": { Type: schema.TypeString, Computed: true, }, - "ipv6_association_id": { Type: schema.TypeString, Computed: true, }, - "main_route_table_id": { Type: schema.TypeString, Computed: true, }, - - "state": { + "owner_id": { Type: schema.TypeString, - Optional: true, Computed: true, }, - - "tags": tftags.TagsSchemaComputed(), - - "owner_id": { + "state": { Type: schema.TypeString, + Optional: true, Computed: true, }, + "tags": tftags.TagsSchemaComputed(), }, } } @@ -119,17 +104,6 @@ func dataSourceVPCRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig - req := &ec2.DescribeVpcsInput{} - - var id string - if cid, ok := d.GetOk("id"); ok { - id = cid.(string) - } - - if id != "" { - req.VpcIds = []*string{aws.String(id)} - } - // We specify "default" as boolean, but EC2 filters want // it to be serialized as a string. Note that setting it to // "false" here does not actually filter by it *not* being @@ -139,72 +113,84 @@ func dataSourceVPCRead(d *schema.ResourceData, meta interface{}) error { if d.Get("default").(bool) { isDefaultStr = "true" } + input := &ec2.DescribeVpcsInput{ + Filters: BuildAttributeFilterList( + map[string]string{ + "cidr": d.Get("cidr_block").(string), + "dhcp-options-id": d.Get("dhcp_options_id").(string), + "isDefault": isDefaultStr, + "state": d.Get("state").(string), + }, + ), + } - req.Filters = BuildAttributeFilterList( - map[string]string{ - "cidr": d.Get("cidr_block").(string), - "dhcp-options-id": d.Get("dhcp_options_id").(string), - "isDefault": isDefaultStr, - "state": d.Get("state").(string), - }, - ) + if v, ok := d.GetOk("id"); ok { + input.VpcIds = aws.StringSlice([]string{v.(string)}) + } if tags, tagsOk := d.GetOk("tags"); tagsOk { - req.Filters = append(req.Filters, BuildTagFilterList( + input.Filters = append(input.Filters, BuildTagFilterList( Tags(tftags.New(tags.(map[string]interface{}))), )...) } - req.Filters = append(req.Filters, BuildCustomFilterList( + input.Filters = append(input.Filters, BuildCustomFilterList( d.Get("filter").(*schema.Set), )...) - if len(req.Filters) == 0 { + if len(input.Filters) == 0 { // Don't send an empty filters list; the EC2 API won't accept it. - req.Filters = nil + input.Filters = nil } - log.Printf("[DEBUG] Reading AWS VPC: %s", req) - resp, err := conn.DescribeVpcs(req) + vpc, err := FindVPC(conn, input) + if err != nil { - return err - } - if resp == nil || len(resp.Vpcs) == 0 { - return fmt.Errorf("no matching VPC found") + return tfresource.SingularDataSourceFindError("EC2 VPC", err) } - if len(resp.Vpcs) > 1 { - return fmt.Errorf("multiple VPCs matched; use additional constraints to reduce matches to a single VPC") - } - - vpc := resp.Vpcs[0] d.SetId(aws.StringValue(vpc.VpcId)) - d.Set("cidr_block", vpc.CidrBlock) - d.Set("dhcp_options_id", vpc.DhcpOptionsId) - d.Set("instance_tenancy", vpc.InstanceTenancy) - d.Set("default", vpc.IsDefault) - d.Set("state", vpc.State) - - if err := d.Set("tags", KeyValueTags(vpc.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %w", err) - } - - d.Set("owner_id", vpc.OwnerId) + ownerID := aws.StringValue(vpc.OwnerId) arn := arn.ARN{ Partition: meta.(*conns.AWSClient).Partition, Service: ec2.ServiceName, Region: meta.(*conns.AWSClient).Region, - AccountID: aws.StringValue(vpc.OwnerId), + AccountID: ownerID, Resource: fmt.Sprintf("vpc/%s", d.Id()), }.String() d.Set("arn", arn) + d.Set("cidr_block", vpc.CidrBlock) + d.Set("default", vpc.IsDefault) + d.Set("dhcp_options_id", vpc.DhcpOptionsId) + d.Set("instance_tenancy", vpc.InstanceTenancy) + d.Set("owner_id", ownerID) + + if v, err := FindVPCAttribute(conn, d.Id(), ec2.VpcAttributeNameEnableDnsHostnames); err != nil { + return fmt.Errorf("error reading EC2 VPC (%s) Attribute (%s): %w", d.Id(), ec2.VpcAttributeNameEnableDnsHostnames, err) + } else { + d.Set("enable_dns_hostnames", v) + } + + if v, err := FindVPCAttribute(conn, d.Id(), ec2.VpcAttributeNameEnableDnsSupport); err != nil { + return fmt.Errorf("error reading EC2 VPC (%s) Attribute (%s): %w", d.Id(), ec2.VpcAttributeNameEnableDnsSupport, err) + } else { + d.Set("enable_dns_support", v) + } + + routeTable, err := FindVPCMainRouteTable(conn, d.Id()) + + if err != nil { + return fmt.Errorf("error reading EC2 VPC (%s) main Route Table: %w", d.Id(), err) + } + + d.Set("main_route_table_id", routeTable.RouteTableId) cidrAssociations := []interface{}{} - for _, associationSet := range vpc.CidrBlockAssociationSet { + for _, v := range vpc.CidrBlockAssociationSet { association := map[string]interface{}{ - "association_id": aws.StringValue(associationSet.AssociationId), - "cidr_block": aws.StringValue(associationSet.CidrBlock), - "state": aws.StringValue(associationSet.CidrBlockState.State), + "association_id": aws.StringValue(v.AssociationId), + "cidr_block": aws.StringValue(v.CidrBlock), + "state": aws.StringValue(v.CidrBlockState.State), } cidrAssociations = append(cidrAssociations, association) } @@ -212,32 +198,17 @@ func dataSourceVPCRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error setting cidr_block_associations: %w", err) } - if vpc.Ipv6CidrBlockAssociationSet != nil { + if len(vpc.Ipv6CidrBlockAssociationSet) > 0 { d.Set("ipv6_association_id", vpc.Ipv6CidrBlockAssociationSet[0].AssociationId) d.Set("ipv6_cidr_block", vpc.Ipv6CidrBlockAssociationSet[0].Ipv6CidrBlock) + } else { + d.Set("ipv6_association_id", nil) + d.Set("ipv6_cidr_block", nil) } - enableDnsHostnames, err := FindVPCAttribute(conn, aws.StringValue(vpc.VpcId), ec2.VpcAttributeNameEnableDnsHostnames) - - if err != nil { - return fmt.Errorf("error reading EC2 VPC (%s) Attribute (%s): %w", aws.StringValue(vpc.VpcId), ec2.VpcAttributeNameEnableDnsHostnames, err) - } - - d.Set("enable_dns_hostnames", enableDnsHostnames) - - enableDnsSupport, err := FindVPCAttribute(conn, aws.StringValue(vpc.VpcId), ec2.VpcAttributeNameEnableDnsSupport) - - if err != nil { - return fmt.Errorf("error reading EC2 VPC (%s) Attribute (%s): %w", aws.StringValue(vpc.VpcId), ec2.VpcAttributeNameEnableDnsSupport, err) - } - - d.Set("enable_dns_support", enableDnsSupport) - - routeTableId, err := resourceVPCSetMainRouteTable(conn, aws.StringValue(vpc.VpcId)) - if err != nil { - log.Printf("[WARN] Unable to set Main Route Table: %s", err) + if err := d.Set("tags", KeyValueTags(vpc.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) } - d.Set("main_route_table_id", routeTableId) return nil } diff --git a/internal/service/ec2/vpc_data_source_test.go b/internal/service/ec2/vpc_data_source_test.go index e851c4a8ede..810538af3fd 100644 --- a/internal/service/ec2/vpc_data_source_test.go +++ b/internal/service/ec2/vpc_data_source_test.go @@ -2,20 +2,19 @@ package ec2_test import ( "fmt" - "math/rand" "testing" - "time" "github.com/aws/aws-sdk-go/service/ec2" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-provider-aws/internal/acctest" ) func TestAccEC2VPCDataSource_basic(t *testing.T) { - rand.Seed(time.Now().UTC().UnixNano()) - rInt := rand.Intn(254) - cidr := fmt.Sprintf("10.%d.0.0/16", rInt+1) // Prevent common 10.0.0.0/16 cidr_block matches - tag := fmt.Sprintf("terraform-testacc-vpc-data-source-basic-%d", rInt) + rInt1 := sdkacctest.RandIntRange(1, 128) + rInt2 := sdkacctest.RandIntRange(128, 254) + cidr := fmt.Sprintf("10.%d.%d.0/28", rInt1, rInt2) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) vpcResourceName := "aws_vpc.test" ds1ResourceName := "data.aws_vpc.by_id" @@ -29,86 +28,33 @@ func TestAccEC2VPCDataSource_basic(t *testing.T) { Providers: acctest.Providers, Steps: []resource.TestStep{ { - Config: testAccVPCDataSourceConfig(cidr, tag), + Config: testAccVPCDataSourceConfig(rName, cidr), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrPair( - ds1ResourceName, "id", vpcResourceName, "id"), - resource.TestCheckResourceAttrPair( - ds1ResourceName, "owner_id", vpcResourceName, "owner_id"), - resource.TestCheckResourceAttr( - ds1ResourceName, "cidr_block", cidr), - resource.TestCheckResourceAttr( - ds1ResourceName, "tags.Name", tag), - resource.TestCheckResourceAttr( - ds1ResourceName, "enable_dns_support", "true"), - resource.TestCheckResourceAttr( - ds1ResourceName, "enable_dns_hostnames", "false"), - resource.TestCheckResourceAttrSet( - ds1ResourceName, "arn"), - resource.TestCheckResourceAttrPair( - ds1ResourceName, "main_route_table_id", vpcResourceName, "main_route_table_id"), - - resource.TestCheckResourceAttrPair( - ds2ResourceName, "id", vpcResourceName, "id"), - resource.TestCheckResourceAttrPair( - ds2ResourceName, "owner_id", vpcResourceName, "owner_id"), - resource.TestCheckResourceAttr( - ds2ResourceName, "cidr_block", cidr), - resource.TestCheckResourceAttr( - ds2ResourceName, "tags.Name", tag), - - resource.TestCheckResourceAttrPair( - ds3ResourceName, "id", vpcResourceName, "id"), - resource.TestCheckResourceAttrPair( - ds3ResourceName, "owner_id", vpcResourceName, "owner_id"), - resource.TestCheckResourceAttr( - ds3ResourceName, "cidr_block", cidr), - resource.TestCheckResourceAttr( - ds3ResourceName, "tags.Name", tag), - - resource.TestCheckResourceAttrPair( - ds4ResourceName, "id", vpcResourceName, "id"), - resource.TestCheckResourceAttrPair( - ds4ResourceName, "owner_id", vpcResourceName, "owner_id"), - resource.TestCheckResourceAttr( - ds4ResourceName, "cidr_block", cidr), - resource.TestCheckResourceAttr( - ds4ResourceName, "tags.Name", tag), - ), - }, - }, - }) -} - -func TestAccEC2VPCDataSource_ipv6Associated(t *testing.T) { - rand.Seed(time.Now().UTC().UnixNano()) - rInt := rand.Intn(255) - cidr := fmt.Sprintf("10.%d.0.0/16", rInt) - tag := fmt.Sprintf("terraform-testacc-vpc-data-source-ipv6-associated-%d", rInt) - - vpcResourceName := "aws_vpc.test" - ds1ResourceName := "data.aws_vpc.by_id" - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), - Providers: acctest.Providers, - Steps: []resource.TestStep{ - { - Config: testAccVPCIPv6DataSourceConfig(cidr, tag), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrPair( - ds1ResourceName, "id", vpcResourceName, "id"), - resource.TestCheckResourceAttrPair( - ds1ResourceName, "owner_id", vpcResourceName, "owner_id"), - resource.TestCheckResourceAttr( - ds1ResourceName, "cidr_block", cidr), - resource.TestCheckResourceAttr( - ds1ResourceName, "tags.Name", tag), - resource.TestCheckResourceAttrSet( - "data.aws_vpc.by_id", "ipv6_association_id"), - resource.TestCheckResourceAttrSet( - "data.aws_vpc.by_id", "ipv6_cidr_block"), + resource.TestCheckResourceAttrPair(ds1ResourceName, "arn", vpcResourceName, "arn"), + resource.TestCheckResourceAttr(ds1ResourceName, "cidr_block", cidr), + resource.TestCheckResourceAttr(ds1ResourceName, "enable_dns_hostnames", "false"), + resource.TestCheckResourceAttr(ds1ResourceName, "enable_dns_support", "true"), + resource.TestCheckResourceAttrPair(ds1ResourceName, "id", vpcResourceName, "id"), + resource.TestCheckResourceAttrPair(ds1ResourceName, "ipv6_association_id", vpcResourceName, "ipv6_association_id"), + resource.TestCheckResourceAttrPair(ds1ResourceName, "ipv6_cidr_block", vpcResourceName, "ipv6_cidr_block"), + resource.TestCheckResourceAttrPair(ds1ResourceName, "main_route_table_id", vpcResourceName, "main_route_table_id"), + resource.TestCheckResourceAttrPair(ds1ResourceName, "owner_id", vpcResourceName, "owner_id"), + resource.TestCheckResourceAttr(ds1ResourceName, "tags.Name", rName), + + resource.TestCheckResourceAttrPair(ds2ResourceName, "id", vpcResourceName, "id"), + resource.TestCheckResourceAttrPair(ds2ResourceName, "owner_id", vpcResourceName, "owner_id"), + resource.TestCheckResourceAttr(ds2ResourceName, "cidr_block", cidr), + resource.TestCheckResourceAttr(ds2ResourceName, "tags.Name", rName), + + resource.TestCheckResourceAttrPair(ds3ResourceName, "id", vpcResourceName, "id"), + resource.TestCheckResourceAttrPair(ds3ResourceName, "owner_id", vpcResourceName, "owner_id"), + resource.TestCheckResourceAttr(ds3ResourceName, "cidr_block", cidr), + resource.TestCheckResourceAttr(ds3ResourceName, "tags.Name", rName), + + resource.TestCheckResourceAttrPair(ds4ResourceName, "id", vpcResourceName, "id"), + resource.TestCheckResourceAttrPair(ds4ResourceName, "owner_id", vpcResourceName, "owner_id"), + resource.TestCheckResourceAttr(ds4ResourceName, "cidr_block", cidr), + resource.TestCheckResourceAttr(ds4ResourceName, "tags.Name", rName), ), }, }, @@ -117,6 +63,7 @@ func TestAccEC2VPCDataSource_ipv6Associated(t *testing.T) { func TestAccEC2VPCDataSource_CIDRBlockAssociations_multiple(t *testing.T) { dataSourceName := "data.aws_vpc.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -125,7 +72,7 @@ func TestAccEC2VPCDataSource_CIDRBlockAssociations_multiple(t *testing.T) { CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ { - Config: testAccVPCCIDRBlockAssociationsMultipleDataSourceConfig(), + Config: testAccVPCCIDRBlockAssociationsMultipleDataSourceConfig(rName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(dataSourceName, "cidr_block_associations.#", "2"), ), @@ -134,30 +81,15 @@ func TestAccEC2VPCDataSource_CIDRBlockAssociations_multiple(t *testing.T) { }) } -func testAccVPCIPv6DataSourceConfig(cidr, tag string) string { +func testAccVPCDataSourceConfig(rName, cidr string) string { return fmt.Sprintf(` resource "aws_vpc" "test" { - cidr_block = "%s" - assign_generated_ipv6_cidr_block = true - - tags = { - Name = "%s" - } -} + cidr_block = %[2]q -data "aws_vpc" "by_id" { - id = aws_vpc.test.id -} -`, cidr, tag) -} - -func testAccVPCDataSourceConfig(cidr, tag string) string { - return fmt.Sprintf(` -resource "aws_vpc" "test" { - cidr_block = "%s" + assign_generated_ipv6_cidr_block = true tags = { - Name = "%s" + Name = %[1]q } } @@ -181,13 +113,17 @@ data "aws_vpc" "by_filter" { values = [aws_vpc.test.id] } } -`, cidr, tag) +`, rName, cidr) } -func testAccVPCCIDRBlockAssociationsMultipleDataSourceConfig() string { - return ` +func testAccVPCCIDRBlockAssociationsMultipleDataSourceConfig(rName string) string { + return fmt.Sprintf(` resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" + + tags = { + Name = %[1]q + } } resource "aws_vpc_ipv4_cidr_block_association" "test" { @@ -198,5 +134,5 @@ resource "aws_vpc_ipv4_cidr_block_association" "test" { data "aws_vpc" "test" { id = aws_vpc_ipv4_cidr_block_association.test.vpc_id } -` +`, rName) } diff --git a/internal/service/ec2/vpc_dhcp_options.go b/internal/service/ec2/vpc_dhcp_options.go index 27cd0360bf2..77db9c52887 100644 --- a/internal/service/ec2/vpc_dhcp_options.go +++ b/internal/service/ec2/vpc_dhcp_options.go @@ -3,15 +3,11 @@ package ec2 import ( "fmt" "log" - "strings" - "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/arn" - "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/aws-sdk-go-base/tfawserr" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/conns" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" @@ -25,133 +21,89 @@ func ResourceVPCDHCPOptions() *schema.Resource { Read: resourceVPCDHCPOptionsRead, Update: resourceVPCDHCPOptionsUpdate, Delete: resourceVPCDHCPOptionsDelete, + Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, "domain_name": { Type: schema.TypeString, Optional: true, ForceNew: true, }, - "domain_name_servers": { Type: schema.TypeList, Optional: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - - "ntp_servers": { + "netbios_name_servers": { Type: schema.TypeList, Optional: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - "netbios_node_type": { Type: schema.TypeString, Optional: true, ForceNew: true, }, - - "netbios_name_servers": { + "ntp_servers": { Type: schema.TypeList, Optional: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - - "tags": tftags.TagsSchema(), - - "tags_all": tftags.TagsSchemaComputed(), - "owner_id": { Type: schema.TypeString, Computed: true, }, - "arn": { - Type: schema.TypeString, - Computed: true, - }, + "tags": tftags.TagsSchema(), + "tags_all": tftags.TagsSchemaComputed(), }, CustomizeDiff: verify.SetTagsDiff, } } +var ( + optionsMap = newDHCPOptionsMap(map[string]string{ + "domain_name": "domain-name", + "domain_name_servers": "domain-name-servers", + "netbios_name_servers": "netbios-name-servers", + "netbios_node_type": "netbios-node-type", + "ntp_servers": "ntp-servers", + }) +) + func resourceVPCDHCPOptionsCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) - setDHCPOption := func(key string) *ec2.NewDhcpConfiguration { - log.Printf("[DEBUG] Setting DHCP option %s...", key) - tfKey := strings.Replace(key, "-", "_", -1) - - value, ok := d.GetOk(tfKey) - if !ok { - return nil - } - - if v, ok := value.(string); ok { - return &ec2.NewDhcpConfiguration{ - Key: aws.String(key), - Values: []*string{ - aws.String(v), - }, - } - } - - if v, ok := value.([]interface{}); ok { - var s []*string - for _, attr := range v { - s = append(s, aws.String(attr.(string))) - } - - return &ec2.NewDhcpConfiguration{ - Key: aws.String(key), - Values: s, - } - } + dhcpConfigurations, err := optionsMap.resourceDataToDhcpConfigurations(d) - return nil + if err != nil { + return err } - createOpts := &ec2.CreateDhcpOptionsInput{ - DhcpConfigurations: []*ec2.NewDhcpConfiguration{ - setDHCPOption("domain-name"), - setDHCPOption("domain-name-servers"), - setDHCPOption("ntp-servers"), - setDHCPOption("netbios-node-type"), - setDHCPOption("netbios-name-servers"), - }, - TagSpecifications: ec2TagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeDhcpOptions), + input := &ec2.CreateDhcpOptionsInput{ + DhcpConfigurations: dhcpConfigurations, + TagSpecifications: ec2TagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeDhcpOptions), } - resp, err := conn.CreateDhcpOptions(createOpts) + output, err := conn.CreateDhcpOptions(input) + if err != nil { - return fmt.Errorf("Error creating DHCP Options Set: %s", err) + return fmt.Errorf("error creating EC2 DHCP Options Set: %w", err) } - dos := resp.DhcpOptions - d.SetId(aws.StringValue(dos.DhcpOptionsId)) - log.Printf("[INFO] DHCP Options Set ID: %s", d.Id()) - - // Wait for the DHCP Options to become available - log.Printf("[DEBUG] Waiting for DHCP Options (%s) to become available", d.Id()) - stateConf := &resource.StateChangeConf{ - Pending: []string{"pending"}, - Target: []string{"created"}, - Refresh: resourceDHCPOptionsStateRefreshFunc(conn, d.Id()), - Timeout: 5 * time.Minute, - } - if _, err := stateConf.WaitForState(); err != nil { - return fmt.Errorf( - "Error waiting for DHCP Options (%s) to become available: %s", - d.Id(), err) - } + d.SetId(aws.StringValue(output.DhcpOptions.DhcpOptionsId)) return resourceVPCDHCPOptionsRead(d, meta) } @@ -161,27 +113,38 @@ func resourceVPCDHCPOptionsRead(d *schema.ResourceData, meta interface{}) error defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig - req := &ec2.DescribeDhcpOptionsInput{ - DhcpOptionsIds: []*string{ - aws.String(d.Id()), - }, + outputRaw, err := tfresource.RetryWhenNewResourceNotFound(PropagationTimeout, func() (interface{}, error) { + return FindDHCPOptionsByID(conn, d.Id()) + }, d.IsNewResource()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] EC2 DHCP Options Set %s not found, removing from state", d.Id()) + d.SetId("") + return nil } - resp, err := conn.DescribeDhcpOptions(req) if err != nil { - if isNoSuchDhcpOptionIDErr(err) { - log.Printf("[WARN] DHCP Options (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil - } - return fmt.Errorf("Error retrieving DHCP Options: %s", err.Error()) + return fmt.Errorf("error reading EC2 DHCP Options Set (%s): %w", d.Id(), err) } - if len(resp.DhcpOptions) == 0 { - return nil - } + opts := outputRaw.(*ec2.DhcpOptions) - opts := resp.DhcpOptions[0] + ownerID := aws.StringValue(opts.OwnerId) + arn := arn.ARN{ + Partition: meta.(*conns.AWSClient).Partition, + Service: ec2.ServiceName, + Region: meta.(*conns.AWSClient).Region, + AccountID: ownerID, + Resource: fmt.Sprintf("dhcp-options/%s", d.Id()), + }.String() + d.Set("arn", arn) + d.Set("owner_id", ownerID) + + err = optionsMap.dhcpConfigurationsToResourceData(opts.DhcpConfigurations, d) + + if err != nil { + return err + } tags := KeyValueTags(opts.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) @@ -194,33 +157,6 @@ func resourceVPCDHCPOptionsRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("error setting tags_all: %w", err) } - d.Set("owner_id", opts.OwnerId) - - for _, cfg := range opts.DhcpConfigurations { - tfKey := strings.Replace(*cfg.Key, "-", "_", -1) - - if _, ok := d.Get(tfKey).(string); ok { - d.Set(tfKey, cfg.Values[0].Value) - } else { - values := make([]string, 0, len(cfg.Values)) - for _, v := range cfg.Values { - values = append(values, *v.Value) - } - - d.Set(tfKey, values) - } - } - - arn := arn.ARN{ - Partition: meta.(*conns.AWSClient).Partition, - Service: ec2.ServiceName, - Region: meta.(*conns.AWSClient).Region, - AccountID: aws.StringValue(opts.OwnerId), - Resource: fmt.Sprintf("dhcp-options/%s", d.Id()), - }.String() - - d.Set("arn", arn) - return nil } @@ -229,8 +165,9 @@ func resourceVPCDHCPOptionsUpdate(d *schema.ResourceData, meta interface{}) erro if d.HasChange("tags_all") { o, n := d.GetChange("tags_all") + if err := UpdateTags(conn, d.Id(), o, n); err != nil { - return fmt.Errorf("error updating tags: %s", err) + return fmt.Errorf("error updating EC2 DHCP Options Set (%s) tags: %w", d.Id(), err) } } @@ -240,107 +177,129 @@ func resourceVPCDHCPOptionsUpdate(d *schema.ResourceData, meta interface{}) erro func resourceVPCDHCPOptionsDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn - err := resource.Retry(3*time.Minute, func() *resource.RetryError { - _, err := conn.DeleteDhcpOptions(&ec2.DeleteDhcpOptionsInput{ - DhcpOptionsId: aws.String(d.Id()), - }) + vpcs, err := FindVPCs(conn, &ec2.DescribeVpcsInput{ + Filters: BuildAttributeFilterList(map[string]string{ + "dhcp-options-id": d.Id(), + }), + }) - if err == nil { - return nil - } + if err != nil { + return fmt.Errorf("error reading EC2 DHCP Options Set (%s) associated VPCs: %w", d.Id(), err) + } - ec2err, ok := err.(awserr.Error) - if !ok { - return resource.RetryableError(err) - } + for _, v := range vpcs { + vpcID := aws.StringValue(v.VpcId) - switch ec2err.Code() { - case "InvalidDhcpOptionsID.NotFound", "InvalidDhcpOptionID.NotFound": - return nil - case "DependencyViolation": - // If it is a dependency violation, we want to disassociate - // all VPCs using the given DHCP Options ID, and retry deleting. - vpcs, err2 := FindVPCsByDHCPOptionsID(conn, d.Id()) - if err2 != nil { - log.Printf("[ERROR] %s", err2) - return resource.RetryableError(err2) - } + log.Printf("[INFO] Disassociating EC2 DHCP Options Set (%s) from VPC (%s)", d.Id(), vpcID) + _, err := conn.AssociateDhcpOptions(&ec2.AssociateDhcpOptionsInput{ + DhcpOptionsId: aws.String(DefaultDHCPOptionsID), + VpcId: aws.String(vpcID), + }) - for _, vpc := range vpcs { - log.Printf("[INFO] Disassociating DHCP Options Set %s from VPC %s...", d.Id(), *vpc.VpcId) - if _, err := conn.AssociateDhcpOptions(&ec2.AssociateDhcpOptionsInput{ - DhcpOptionsId: aws.String("default"), - VpcId: vpc.VpcId, - }); err != nil { - return resource.RetryableError(err) - } - } - return resource.RetryableError(err) - default: - return resource.NonRetryableError(err) + if tfawserr.ErrCodeEquals(err, ErrCodeInvalidVpcIDNotFound) { + continue } - }) - if tfresource.TimedOut(err) { - _, err = conn.DeleteDhcpOptions(&ec2.DeleteDhcpOptionsInput{ - DhcpOptionsId: aws.String(d.Id()), - }) + if err != nil { + return fmt.Errorf("error disassociating EC2 DHCP Options Set (%s) from VPC (%s): %w", d.Id(), vpcID, err) + } } - return err -} -func FindVPCsByDHCPOptionsID(conn *ec2.EC2, id string) ([]*ec2.Vpc, error) { - req := &ec2.DescribeVpcsInput{ - Filters: []*ec2.Filter{ - { - Name: aws.String("dhcp-options-id"), - Values: []*string{ - aws.String(id), - }, - }, - }, + input := &ec2.DeleteDhcpOptionsInput{ + DhcpOptionsId: aws.String(d.Id()), + } + + log.Printf("[INFO] Deleting EC2 DHCP Options Set: %s", d.Id()) + _, err = tfresource.RetryWhenAWSErrCodeEquals(dhcpOptionSetDeletedTimeout, func() (interface{}, error) { + return conn.DeleteDhcpOptions(input) + }, ErrCodeDependencyViolation) + + if tfawserr.ErrCodeEquals(err, ErrCodeInvalidDhcpOptionIDNotFound) { + return nil } - resp, err := conn.DescribeVpcs(req) if err != nil { - if tfawserr.ErrMessageContains(err, "InvalidVpcID.NotFound", "") { - return nil, nil - } - return nil, err + return fmt.Errorf("error deleting EC2 DHCP Options Set (%s): %w", d.Id(), err) } - return resp.Vpcs, nil + return nil } -func resourceDHCPOptionsStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc { - return func() (interface{}, string, error) { - DescribeDhcpOpts := &ec2.DescribeDhcpOptionsInput{ - DhcpOptionsIds: []*string{ - aws.String(id), - }, - } +// dhcpOptionsMap represents a mapping of Terraform resource attribute name to AWS API DHCP Option name. +type dhcpOptionsMap struct { + tfToApi map[string]string + apiToTf map[string]string +} - resp, err := conn.DescribeDhcpOptions(DescribeDhcpOpts) - if err != nil { - if isNoSuchDhcpOptionIDErr(err) { - resp = nil - } else { - log.Printf("Error on DHCPOptionsStateRefresh: %s", err) - return nil, "", err +func newDHCPOptionsMap(tfToApi map[string]string) *dhcpOptionsMap { + apiToTf := make(map[string]string) + + for k, v := range tfToApi { + apiToTf[v] = k + } + + return &dhcpOptionsMap{ + tfToApi: tfToApi, + apiToTf: apiToTf, + } +} + +// dhcpConfigurationsToResourceData sets Terraform ResourceData from a list of AWS API DHCP configurations. +func (m *dhcpOptionsMap) dhcpConfigurationsToResourceData(dhcpConfigurations []*ec2.DhcpConfiguration, d *schema.ResourceData) error { + for _, dhcpConfiguration := range dhcpConfigurations { + apiName := aws.StringValue(dhcpConfiguration.Key) + if tfName, ok := m.apiToTf[apiName]; ok { + switch v := d.Get(tfName).(type) { + case string: + d.Set(tfName, dhcpConfiguration.Values[0].Value) + case []interface{}: + var values []*string + for _, v := range dhcpConfiguration.Values { + values = append(values, v.Value) + } + d.Set(tfName, aws.StringValueSlice(values)) + default: + return fmt.Errorf("Attribute (%s) is of unsupported type: %T", tfName, v) } + } else { + return fmt.Errorf("Unsupported DHCP option: %s", apiName) } + } - if resp == nil { - // Sometimes AWS just has consistency issues and doesn't see - // our instance yet. Return an empty state. - return nil, "", nil - } + return nil +} - dos := resp.DhcpOptions[0] - return dos, "created", nil +// resourceDataToNewDhcpConfigurations returns a list of AWS API DHCP configurations from Terraform ResourceData. +func (m *dhcpOptionsMap) resourceDataToDhcpConfigurations(d *schema.ResourceData) ([]*ec2.NewDhcpConfiguration, error) { + var output []*ec2.NewDhcpConfiguration + + for tfName, apiName := range m.tfToApi { + switch v := d.Get(tfName).(type) { + case string: + if v != "" { + output = append(output, &ec2.NewDhcpConfiguration{ + Key: aws.String(apiName), + Values: aws.StringSlice([]string{v}), + }) + } + case []interface{}: + var values []string + for _, v := range v { + v := v.(string) + if v != "" { + values = append(values, v) + } + } + if len(values) > 0 { + output = append(output, &ec2.NewDhcpConfiguration{ + Key: aws.String(apiName), + Values: aws.StringSlice(values), + }) + } + default: + return nil, fmt.Errorf("Attribute (%s) is of unsupported type: %T", tfName, v) + } } -} -func isNoSuchDhcpOptionIDErr(err error) bool { - return tfawserr.ErrMessageContains(err, "InvalidDhcpOptionID.NotFound", "") || tfawserr.ErrMessageContains(err, "InvalidDhcpOptionsID.NotFound", "") + return output, nil } diff --git a/internal/service/ec2/vpc_dhcp_options_association.go b/internal/service/ec2/vpc_dhcp_options_association.go index 95150b28ea6..6577b304f7c 100644 --- a/internal/service/ec2/vpc_dhcp_options_association.go +++ b/internal/service/ec2/vpc_dhcp_options_association.go @@ -3,11 +3,11 @@ package ec2 import ( "fmt" "log" + "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/aws-sdk-go-base/tfawserr" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" @@ -15,69 +15,48 @@ import ( func ResourceVPCDHCPOptionsAssociation() *schema.Resource { return &schema.Resource{ - Create: resourceVPCDHCPOptionsAssociationCreate, + Create: resourceVPCDHCPOptionsAssociationPut, Read: resourceVPCDHCPOptionsAssociationRead, - Update: resourceVPCDHCPOptionsAssociationUpdate, + Update: resourceVPCDHCPOptionsAssociationPut, Delete: resourceVPCDHCPOptionsAssociationDelete, + Importer: &schema.ResourceImporter{ State: resourceVPCDHCPOptionsAssociationImport, }, Schema: map[string]*schema.Schema{ - "vpc_id": { + "dhcp_options_id": { Type: schema.TypeString, Required: true, - ForceNew: true, }, - - "dhcp_options_id": { + "vpc_id": { Type: schema.TypeString, Required: true, + ForceNew: true, }, }, } } -func resourceVPCDHCPOptionsAssociationImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - conn := meta.(*conns.AWSClient).EC2Conn - // Provide the vpc_id as the id to import - vpcRaw, _, err := VPCStateRefreshFunc(conn, d.Id())() - if err != nil { - return nil, err - } - if vpcRaw == nil { - return nil, nil - } - vpc := vpcRaw.(*ec2.Vpc) - if err = d.Set("vpc_id", vpc.VpcId); err != nil { - return nil, err - } - if err = d.Set("dhcp_options_id", vpc.DhcpOptionsId); err != nil { - return nil, err - } - d.SetId(fmt.Sprintf("%s-%s", aws.StringValue(vpc.DhcpOptionsId), aws.StringValue(vpc.VpcId))) - return []*schema.ResourceData{d}, nil -} - -func resourceVPCDHCPOptionsAssociationCreate(d *schema.ResourceData, meta interface{}) error { +func resourceVPCDHCPOptionsAssociationPut(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn - vpcId := d.Get("vpc_id").(string) - optsID := d.Get("dhcp_options_id").(string) + dhcpOptionsID := d.Get("dhcp_options_id").(string) + vpcID := d.Get("vpc_id").(string) + id := VPCDHCPOptionsAssociationCreateResourceID(dhcpOptionsID, vpcID) + input := &ec2.AssociateDhcpOptionsInput{ + DhcpOptionsId: aws.String(dhcpOptionsID), + VpcId: aws.String(vpcID), + } - log.Printf("[INFO] Creating DHCP Options association: %s => %s", vpcId, optsID) + log.Printf("[DEBUG] Creating EC2 VPC DHCP Options Set Association: %s", input) + _, err := conn.AssociateDhcpOptions(input) - if _, err := conn.AssociateDhcpOptions(&ec2.AssociateDhcpOptionsInput{ - DhcpOptionsId: aws.String(optsID), - VpcId: aws.String(vpcId), - }); err != nil { - return err + if err != nil { + return fmt.Errorf("error creating EC2 VPC DHCP Options Set Association (%s): %w", id, err) } - // Set the ID and return - d.SetId(fmt.Sprintf("%s-%s", optsID, vpcId)) - - log.Printf("[INFO] VPC DHCP Association ID: %s", d.Id()) + d.SetId(id) return resourceVPCDHCPOptionsAssociationRead(d, meta) } @@ -85,81 +64,100 @@ func resourceVPCDHCPOptionsAssociationCreate(d *schema.ResourceData, meta interf func resourceVPCDHCPOptionsAssociationRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn - var vpc *ec2.Vpc - - err := resource.Retry(PropagationTimeout, func() *resource.RetryError { - var err error - - vpc, err = FindVPCByID(conn, d.Get("vpc_id").(string)) - - if d.IsNewResource() && tfawserr.ErrCodeEquals(err, ErrCodeInvalidVpcIDNotFound) { - return resource.RetryableError(err) - } - - if err != nil { - return resource.NonRetryableError(err) - } + dhcpOptionsID, vpcID, err := VPCDHCPOptionsAssociationParseResourceID(d.Id()) - if d.IsNewResource() && aws.StringValue(vpc.DhcpOptionsId) != d.Get("dhcp_options_id").(string) { - return resource.RetryableError(&resource.NotFoundError{ - LastError: fmt.Errorf("EC2 VPC DHCP Options Association (%s) not found", d.Id()), - }) - } - - return nil - }) - - if tfresource.TimedOut(err) { - vpc, err = FindVPCByID(conn, d.Get("vpc_id").(string)) + if err != nil { + return err } - if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, ErrCodeInvalidVpcIDNotFound) { - log.Printf("[WARN] EC2 VPC DHCP Options Association (%s) not found, removing from state", d.Id()) + _, err = tfresource.RetryWhenNewResourceNotFound(PropagationTimeout, func() (interface{}, error) { + return nil, FindVPCDHCPOptionsAssociation(conn, vpcID, dhcpOptionsID) + }, d.IsNewResource()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] EC2 VPC DHCP Options Set Association %s not found, removing from state", d.Id()) d.SetId("") return nil } if err != nil { - return fmt.Errorf("error reading EC2 VPC DHCP Options Association (%s): %w", d.Id(), err) + return fmt.Errorf("error reading EC2 VPC DHCP Options Set Association (%s): %w", d.Id(), err) } - if vpc == nil { - return fmt.Errorf("error reading EC2 VPC DHCP Options Association (%s): empty response", d.Id()) - } - - d.Set("vpc_id", vpc.VpcId) - d.Set("dhcp_options_id", vpc.DhcpOptionsId) + d.Set("dhcp_options_id", dhcpOptionsID) + d.Set("vpc_id", vpcID) return nil } -// DHCP Options Asociations cannot be updated. -func resourceVPCDHCPOptionsAssociationUpdate(d *schema.ResourceData, meta interface{}) error { - return resourceVPCDHCPOptionsAssociationCreate(d, meta) -} - -const VPCDefaultOptionsID = "default" - -// AWS does not provide an API to disassociate a DHCP Options set from a VPC. -// So, we do this by setting the VPC to the default DHCP Options Set. func resourceVPCDHCPOptionsAssociationDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn - log.Printf("[INFO] Disassociating DHCP Options Set %s from VPC %s...", d.Get("dhcp_options_id"), d.Get("vpc_id")) + dhcpOptionsID, vpcID, err := VPCDHCPOptionsAssociationParseResourceID(d.Id()) - if d.Get("dhcp_options_id").(string) == VPCDefaultOptionsID { - // definition of deleted is DhcpOptionsId being equal to "default", nothing to do + if err != nil { + return err + } + + if dhcpOptionsID == DefaultDHCPOptionsID { return nil } - _, err := conn.AssociateDhcpOptions(&ec2.AssociateDhcpOptionsInput{ - DhcpOptionsId: aws.String(VPCDefaultOptionsID), - VpcId: aws.String(d.Get("vpc_id").(string)), + // AWS does not provide an API to disassociate a DHCP Options set from a VPC. + // So, we do this by setting the VPC to the default DHCP Options Set. + + log.Printf("[DEBUG] Deleting EC2 VPC DHCP Options Set Association: %s", d.Id()) + _, err = conn.AssociateDhcpOptions(&ec2.AssociateDhcpOptionsInput{ + DhcpOptionsId: aws.String(DefaultDHCPOptionsID), + VpcId: aws.String(vpcID), }) if tfawserr.ErrCodeEquals(err, ErrCodeInvalidVpcIDNotFound) { return nil } + if err != nil { + return fmt.Errorf("error disassociating EC2 DHCP Options Set (%s) from VPC (%s): %w", dhcpOptionsID, vpcID, err) + } + return err } + +func resourceVPCDHCPOptionsAssociationImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + conn := meta.(*conns.AWSClient).EC2Conn + + vpc, err := FindVPCByID(conn, d.Id()) + + if err != nil { + return nil, fmt.Errorf("error reading EC2 VPC (%s): %w", d.Id(), err) + } + + dhcpOptionsID := aws.StringValue(vpc.DhcpOptionsId) + vpcID := aws.StringValue(vpc.VpcId) + + d.SetId(VPCDHCPOptionsAssociationCreateResourceID(dhcpOptionsID, vpcID)) + d.Set("dhcp_options_id", dhcpOptionsID) + d.Set("vpc_id", vpcID) + + return []*schema.ResourceData{d}, nil +} + +const vpcDHCPOptionsAssociationResourceIDSeparator = "-" + +func VPCDHCPOptionsAssociationCreateResourceID(dhcpOptionsID, vpcID string) string { + parts := []string{dhcpOptionsID, vpcID} + id := strings.Join(parts, vpcDHCPOptionsAssociationResourceIDSeparator) + + return id +} + +func VPCDHCPOptionsAssociationParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, vpcDHCPOptionsAssociationResourceIDSeparator) + + // DHCP Options ID and VPC ID themselves contain '-'. + if len(parts) == 4 && parts[0] != "" && parts[1] != "" && parts[2] != "" && parts[3] != "" { + return strings.Join([]string{parts[0], parts[1]}, vpcDHCPOptionsAssociationResourceIDSeparator), strings.Join([]string{parts[2], parts[3]}, vpcDHCPOptionsAssociationResourceIDSeparator), nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected DHCPOptionsID%[2]sVPCID", id, vpcDHCPOptionsAssociationResourceIDSeparator) +} diff --git a/internal/service/ec2/vpc_dhcp_options_association_test.go b/internal/service/ec2/vpc_dhcp_options_association_test.go index 38b1cf22490..abe1810889f 100644 --- a/internal/service/ec2/vpc_dhcp_options_association_test.go +++ b/internal/service/ec2/vpc_dhcp_options_association_test.go @@ -4,37 +4,35 @@ import ( "fmt" "testing" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" + 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" tfec2 "github.com/hashicorp/terraform-provider-aws/internal/service/ec2" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) func TestAccEC2VPCDHCPOptionsAssociation_basic(t *testing.T) { - var v ec2.Vpc - var d ec2.DhcpOptions resourceName := "aws_vpc_dhcp_options_association.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), Providers: acctest.Providers, - CheckDestroy: testAccCheckDHCPOptionsAssociationDestroy, + CheckDestroy: testAccCheckVPCDHCPOptionsAssociationDestroy, Steps: []resource.TestStep{ { - Config: testAccDHCPOptionsAssociationConfig, + Config: testAccVPCDHCPOptionsAssociationConfig(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckDHCPOptionsExists("aws_vpc_dhcp_options.test", &d), - acctest.CheckVPCExists("aws_vpc.test", &v), - testAccCheckDHCPOptionsAssociationExist(resourceName, &v), + testAccCheckVPCDHCPOptionsAssociationExist(resourceName), ), }, { ResourceName: resourceName, - ImportStateIdFunc: testAccDHCPOptionsAssociationVPCImportIdFunc(resourceName), + ImportStateIdFunc: testAccVPCDHCPOptionsAssociationVPCImportIdFunc(resourceName), ImportState: true, ImportStateVerify: true, }, @@ -43,22 +41,19 @@ func TestAccEC2VPCDHCPOptionsAssociation_basic(t *testing.T) { } func TestAccEC2VPCDHCPOptionsAssociation_Disappears_vpc(t *testing.T) { - var v ec2.Vpc - var d ec2.DhcpOptions resourceName := "aws_vpc_dhcp_options_association.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), Providers: acctest.Providers, - CheckDestroy: testAccCheckDHCPOptionsAssociationDestroy, + CheckDestroy: testAccCheckVPCDHCPOptionsAssociationDestroy, Steps: []resource.TestStep{ { - Config: testAccDHCPOptionsAssociationConfig, + Config: testAccVPCDHCPOptionsAssociationConfig(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckDHCPOptionsExists("aws_vpc_dhcp_options.test", &d), - acctest.CheckVPCExists("aws_vpc.test", &v), - testAccCheckDHCPOptionsAssociationExist(resourceName, &v), + testAccCheckVPCDHCPOptionsAssociationExist(resourceName), acctest.CheckResourceDisappears(acctest.Provider, tfec2.ResourceVPC(), "aws_vpc.test"), ), ExpectNonEmptyPlan: true, @@ -68,22 +63,19 @@ func TestAccEC2VPCDHCPOptionsAssociation_Disappears_vpc(t *testing.T) { } func TestAccEC2VPCDHCPOptionsAssociation_Disappears_dhcp(t *testing.T) { - var v ec2.Vpc - var d ec2.DhcpOptions resourceName := "aws_vpc_dhcp_options_association.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), Providers: acctest.Providers, - CheckDestroy: testAccCheckDHCPOptionsAssociationDestroy, + CheckDestroy: testAccCheckVPCDHCPOptionsAssociationDestroy, Steps: []resource.TestStep{ { - Config: testAccDHCPOptionsAssociationConfig, + Config: testAccVPCDHCPOptionsAssociationConfig(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckDHCPOptionsExists("aws_vpc_dhcp_options.test", &d), - acctest.CheckVPCExists("aws_vpc.test", &v), - testAccCheckDHCPOptionsAssociationExist(resourceName, &v), + testAccCheckVPCDHCPOptionsAssociationExist(resourceName), acctest.CheckResourceDisappears(acctest.Provider, tfec2.ResourceVPCDHCPOptions(), "aws_vpc_dhcp_options.test"), ), ExpectNonEmptyPlan: true, @@ -93,22 +85,19 @@ func TestAccEC2VPCDHCPOptionsAssociation_Disappears_dhcp(t *testing.T) { } func TestAccEC2VPCDHCPOptionsAssociation_disappears(t *testing.T) { - var v ec2.Vpc - var d ec2.DhcpOptions resourceName := "aws_vpc_dhcp_options_association.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), Providers: acctest.Providers, - CheckDestroy: testAccCheckDHCPOptionsAssociationDestroy, + CheckDestroy: testAccCheckVPCDHCPOptionsAssociationDestroy, Steps: []resource.TestStep{ { - Config: testAccDHCPOptionsAssociationConfig, + Config: testAccVPCDHCPOptionsAssociationConfig(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckDHCPOptionsExists("aws_vpc_dhcp_options.test", &d), - acctest.CheckVPCExists("aws_vpc.test", &v), - testAccCheckDHCPOptionsAssociationExist(resourceName, &v), + testAccCheckVPCDHCPOptionsAssociationExist(resourceName), acctest.CheckResourceDisappears(acctest.Provider, tfec2.ResourceVPCDHCPOptionsAssociation(), resourceName), ), ExpectNonEmptyPlan: true, @@ -117,7 +106,7 @@ func TestAccEC2VPCDHCPOptionsAssociation_disappears(t *testing.T) { }) } -func testAccDHCPOptionsAssociationVPCImportIdFunc(resourceName string) resource.ImportStateIdFunc { +func testAccVPCDHCPOptionsAssociationVPCImportIdFunc(resourceName string) resource.ImportStateIdFunc { return func(s *terraform.State) (string, error) { rs, ok := s.RootModule().Resources[resourceName] if !ok { @@ -128,7 +117,7 @@ func testAccDHCPOptionsAssociationVPCImportIdFunc(resourceName string) resource. } } -func testAccCheckDHCPOptionsAssociationDestroy(s *terraform.State) error { +func testAccCheckVPCDHCPOptionsAssociationDestroy(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn for _, rs := range s.RootModule().Resources { @@ -136,21 +125,29 @@ func testAccCheckDHCPOptionsAssociationDestroy(s *terraform.State) error { continue } - // Try to find the VPC associated to the DHCP Options set - vpcs, err := tfec2.FindVPCsByDHCPOptionsID(conn, rs.Primary.Attributes["dhcp_options_id"]) + dhcpOptionsID, vpcID, err := tfec2.VPCDHCPOptionsAssociationParseResourceID(rs.Primary.ID) + if err != nil { return err } - if rs.Primary.Attributes["dhcp_options_id"] != tfec2.VPCDefaultOptionsID && len(vpcs) > 0 { - return fmt.Errorf("vpc_dhcp_options_association (%s) is still associated to %d VPCs.", rs.Primary.Attributes["dhcp_options_id"], len(vpcs)) + err = tfec2.FindVPCDHCPOptionsAssociation(conn, vpcID, dhcpOptionsID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err } + + return fmt.Errorf("EC2 VPC DHCP Options Set Association %s still exists", rs.Primary.ID) } return nil } -func testAccCheckDHCPOptionsAssociationExist(n string, vpc *ec2.Vpc) resource.TestCheckFunc { +func testAccCheckVPCDHCPOptionsAssociationExist(n string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -158,29 +155,34 @@ func testAccCheckDHCPOptionsAssociationExist(n string, vpc *ec2.Vpc) resource.Te } if rs.Primary.ID == "" { - return fmt.Errorf("No DHCP Options Set association ID is set") + return fmt.Errorf("No EC2 VPC DHCP Options Set Association ID is set") } - if aws.StringValue(vpc.DhcpOptionsId) != rs.Primary.Attributes["dhcp_options_id"] { - return fmt.Errorf("VPC %s does not have DHCP Options Set %s associated", - aws.StringValue(vpc.VpcId), rs.Primary.Attributes["dhcp_options_id"]) + dhcpOptionsID, vpcID, err := tfec2.VPCDHCPOptionsAssociationParseResourceID(rs.Primary.ID) + + if err != nil { + return err } - if aws.StringValue(vpc.VpcId) != rs.Primary.Attributes["vpc_id"] { - return fmt.Errorf("DHCP Options Set %s is not associated with VPC %s", - rs.Primary.Attributes["dhcp_options_id"], aws.StringValue(vpc.VpcId)) + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn + + err = tfec2.FindVPCDHCPOptionsAssociation(conn, vpcID, dhcpOptionsID) + + if err != nil { + return err } return nil } } -const testAccDHCPOptionsAssociationConfig = ` +func testAccVPCDHCPOptionsAssociationConfig(rName string) string { + return fmt.Sprintf(` resource "aws_vpc" "test" { cidr_block = "10.1.0.0/16" tags = { - Name = "terraform-testacc-vpc-dhcp-options-association" + Name = %[1]q } } @@ -192,7 +194,7 @@ resource "aws_vpc_dhcp_options" "test" { netbios_node_type = 2 tags = { - Name = "terraform-testacc-vpc-dhcp-options-association" + Name = %[1]q } } @@ -200,4 +202,5 @@ resource "aws_vpc_dhcp_options_association" "test" { vpc_id = aws_vpc.test.id dhcp_options_id = aws_vpc_dhcp_options.test.id } -` +`, rName) +} diff --git a/internal/service/ec2/vpc_dhcp_options_data_source.go b/internal/service/ec2/vpc_dhcp_options_data_source.go index ad543b77254..23d93715a26 100644 --- a/internal/service/ec2/vpc_dhcp_options_data_source.go +++ b/internal/service/ec2/vpc_dhcp_options_data_source.go @@ -1,10 +1,7 @@ package ec2 import ( - "errors" "fmt" - "log" - "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/arn" @@ -12,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "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" ) func DataSourceVPCDHCPOptions() *schema.Resource { @@ -19,6 +17,10 @@ func DataSourceVPCDHCPOptions() *schema.Resource { Read: dataSourceVPCDHCPOptionsRead, Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, "dhcp_options_id": { Type: schema.TypeString, Optional: true, @@ -48,15 +50,11 @@ func DataSourceVPCDHCPOptions() *schema.Resource { Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - "tags": tftags.TagsSchemaComputed(), "owner_id": { Type: schema.TypeString, Computed: true, }, - "arn": { - Type: schema.TypeString, - Computed: true, - }, + "tags": tftags.TagsSchemaComputed(), }, } } @@ -79,71 +77,35 @@ func dataSourceVPCDHCPOptionsRead(d *schema.ResourceData, meta interface{}) erro input.Filters = nil } - log.Printf("[DEBUG] Reading EC2 DHCP Options: %s", input) - output, err := conn.DescribeDhcpOptions(input) - if err != nil { - if isNoSuchDhcpOptionIDErr(err) { - return errors.New("No matching EC2 DHCP Options found") - } - return fmt.Errorf("error reading EC2 DHCP Options: %w", err) - } - - if len(output.DhcpOptions) == 0 { - return errors.New("No matching EC2 DHCP Options found") - } + opts, err := FindDHCPOptions(conn, input) - if len(output.DhcpOptions) > 1 { - return errors.New("Multiple matching EC2 DHCP Options found") + if err != nil { + return tfresource.SingularDataSourceFindError("EC2 DHCP Options Set", err) } - dhcpOptionID := aws.StringValue(output.DhcpOptions[0].DhcpOptionsId) - d.SetId(dhcpOptionID) - d.Set("dhcp_options_id", dhcpOptionID) - - dhcpConfigurations := output.DhcpOptions[0].DhcpConfigurations - - for _, dhcpConfiguration := range dhcpConfigurations { - key := aws.StringValue(dhcpConfiguration.Key) - tfKey := strings.Replace(key, "-", "_", -1) - - if len(dhcpConfiguration.Values) == 0 { - continue - } - - switch key { - case "domain-name": - d.Set(tfKey, dhcpConfiguration.Values[0].Value) - case "domain-name-servers": - if err := d.Set(tfKey, flattenAttributeValues(dhcpConfiguration.Values)); err != nil { - return fmt.Errorf("error setting %s: %w", tfKey, err) - } - case "netbios-name-servers": - if err := d.Set(tfKey, flattenAttributeValues(dhcpConfiguration.Values)); err != nil { - return fmt.Errorf("error setting %s: %w", tfKey, err) - } - case "netbios-node-type": - d.Set(tfKey, dhcpConfiguration.Values[0].Value) - case "ntp-servers": - if err := d.Set(tfKey, flattenAttributeValues(dhcpConfiguration.Values)); err != nil { - return fmt.Errorf("error setting %s: %w", tfKey, err) - } - } - } - - if err := d.Set("tags", KeyValueTags(output.DhcpOptions[0].Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %w", err) - } - d.Set("owner_id", output.DhcpOptions[0].OwnerId) + d.SetId(aws.StringValue(opts.DhcpOptionsId)) + ownerID := aws.StringValue(opts.OwnerId) arn := arn.ARN{ Partition: meta.(*conns.AWSClient).Partition, Service: ec2.ServiceName, Region: meta.(*conns.AWSClient).Region, - AccountID: aws.StringValue(output.DhcpOptions[0].OwnerId), + AccountID: ownerID, Resource: fmt.Sprintf("dhcp-options/%s", d.Id()), }.String() - d.Set("arn", arn) + d.Set("dhcp_options_id", d.Id()) + d.Set("owner_id", ownerID) + + err = optionsMap.dhcpConfigurationsToResourceData(opts.DhcpConfigurations, d) + + if err != nil { + return err + } + + if err := d.Set("tags", KeyValueTags(opts.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } return nil } diff --git a/internal/service/ec2/vpc_dhcp_options_data_source_test.go b/internal/service/ec2/vpc_dhcp_options_data_source_test.go index 8cf3b6b7d35..5692a6a2322 100644 --- a/internal/service/ec2/vpc_dhcp_options_data_source_test.go +++ b/internal/service/ec2/vpc_dhcp_options_data_source_test.go @@ -22,7 +22,7 @@ func TestAccEC2VPCDHCPOptionsDataSource_basic(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccVPCDHCPOptionsDataSourceConfig_Missing, - ExpectError: regexp.MustCompile(`No matching EC2 DHCP Options found`), + ExpectError: regexp.MustCompile(`no matching EC2 DHCP Options Set found`), }, { Config: testAccVPCDHCPOptionsDataSourceConfig_DhcpOptionsID, @@ -77,7 +77,7 @@ func TestAccEC2VPCDHCPOptionsDataSource_filter(t *testing.T) { }, { Config: testAccVPCDHCPOptionsDataSourceConfig_Filter(rInt, 2), - ExpectError: regexp.MustCompile(`Multiple matching EC2 DHCP Options found`), + ExpectError: regexp.MustCompile(`multiple EC2 DHCP Options Sets matched`), }, { // We have one last empty step here because otherwise we'll leave the diff --git a/internal/service/ec2/vpc_dhcp_options_test.go b/internal/service/ec2/vpc_dhcp_options_test.go index 797cd509bc5..fd94752d8a8 100644 --- a/internal/service/ec2/vpc_dhcp_options_test.go +++ b/internal/service/ec2/vpc_dhcp_options_test.go @@ -5,15 +5,14 @@ import ( "regexp" "testing" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" - "github.com/hashicorp/aws-sdk-go-base/tfawserr" 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" tfec2 "github.com/hashicorp/terraform-provider-aws/internal/service/ec2" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) func TestAccEC2VPCDHCPOptions_basic(t *testing.T) { @@ -51,29 +50,6 @@ func TestAccEC2VPCDHCPOptions_basic(t *testing.T) { }) } -func TestAccEC2VPCDHCPOptions_deleteOptions(t *testing.T) { - var d ec2.DhcpOptions - resourceName := "aws_vpc_dhcp_options.test" - rName := sdkacctest.RandString(5) - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), - Providers: acctest.Providers, - CheckDestroy: testAccCheckDHCPOptionsDestroy, - Steps: []resource.TestStep{ - { - Config: testAccDHCPOptionsConfig(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckDHCPOptionsExists(resourceName, &d), - testAccCheckDHCPOptionsDelete(resourceName), - ), - ExpectNonEmptyPlan: true, - }, - }, - }) -} - func TestAccEC2VPCDHCPOptions_tags(t *testing.T) { var d ec2.DhcpOptions resourceName := "aws_vpc_dhcp_options.test" @@ -150,32 +126,23 @@ func testAccCheckDHCPOptionsDestroy(s *terraform.State) error { continue } - // Try to find the resource - resp, err := conn.DescribeDhcpOptions(&ec2.DescribeDhcpOptionsInput{ - DhcpOptionsIds: []*string{ - aws.String(rs.Primary.ID), - }, - }) - if tfawserr.ErrMessageContains(err, "InvalidDhcpOptionID.NotFound", "") { - continue - } - if err == nil { - if len(resp.DhcpOptions) > 0 { - return fmt.Errorf("still exists") - } + _, err := tfec2.FindDHCPOptionsByID(conn, rs.Primary.ID) - return nil + if tfresource.NotFound(err) { + continue } - if !tfawserr.ErrMessageContains(err, "InvalidDhcpOptionID.NotFound", "") { + if err != nil { return err } + + return fmt.Errorf("EC2 DHCP Options Set %s still exists", rs.Primary.ID) } return nil } -func testAccCheckDHCPOptionsExists(n string, d *ec2.DhcpOptions) resource.TestCheckFunc { +func testAccCheckDHCPOptionsExists(n string, v *ec2.DhcpOptions) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -183,52 +150,27 @@ func testAccCheckDHCPOptionsExists(n string, d *ec2.DhcpOptions) resource.TestCh } if rs.Primary.ID == "" { - return fmt.Errorf("No ID is set") + return fmt.Errorf("No EC2 DHCP Options Set ID is set") } conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn - resp, err := conn.DescribeDhcpOptions(&ec2.DescribeDhcpOptionsInput{ - DhcpOptionsIds: []*string{ - aws.String(rs.Primary.ID), - }, - }) + + output, err := tfec2.FindDHCPOptionsByID(conn, rs.Primary.ID) + if err != nil { return err } - if len(resp.DhcpOptions) == 0 { - return fmt.Errorf("DHCP Options not found") - } - *d = *resp.DhcpOptions[0] + *v = *output return nil } } -func testAccCheckDHCPOptionsDelete(n string) 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 ID is set") - } - - conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn - _, err := conn.DeleteDhcpOptions(&ec2.DeleteDhcpOptionsInput{ - DhcpOptionsId: aws.String(rs.Primary.ID), - }) - - return err - } -} - func testAccDHCPOptionsConfig(rName string) string { return fmt.Sprintf(` resource "aws_vpc_dhcp_options" "test" { - domain_name = "service.%s" + domain_name = "service.%[1]s" domain_name_servers = ["127.0.0.1", "10.0.0.2"] ntp_servers = ["127.0.0.1"] netbios_name_servers = ["127.0.0.1"] diff --git a/internal/service/ec2/vpc_ipv4_cidr_block_association.go b/internal/service/ec2/vpc_ipv4_cidr_block_association.go index 9e17c12243e..22baac4456e 100644 --- a/internal/service/ec2/vpc_ipv4_cidr_block_association.go +++ b/internal/service/ec2/vpc_ipv4_cidr_block_association.go @@ -9,14 +9,10 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/aws-sdk-go-base/tfawserr" - "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" -) - -const ( - VpcCidrBlockStateCodeDeleted = "deleted" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) func ResourceVPCIPv4CIDRBlockAssociation() *schema.Resource { @@ -24,13 +20,15 @@ func ResourceVPCIPv4CIDRBlockAssociation() *schema.Resource { Create: resourceVPCIPv4CIDRBlockAssociationCreate, Read: resourceVPCIPv4CIDRBlockAssociationRead, Delete: resourceVPCIPv4CIDRBlockAssociationDelete, + Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, + CustomizeDiff: func(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { - // cidr_block can be set by a value returned from IPAM or explicitly in config + // cidr_block can be set by a value returned from IPAM or explicitly in config. if diff.Id() != "" && diff.HasChange("cidr_block") { - // if netmask is set then cidr_block is derived from ipam, ignore changes + // If netmask is set then cidr_block is derived from IPAM, ignore changes. if diff.Get("ipv4_netmask_length") != 0 { return diff.Clear("cidr_block") } @@ -38,12 +36,8 @@ func ResourceVPCIPv4CIDRBlockAssociation() *schema.Resource { } return nil }, + Schema: map[string]*schema.Schema{ - "vpc_id": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, "cidr_block": { Type: schema.TypeString, Optional: true, @@ -62,6 +56,11 @@ func ResourceVPCIPv4CIDRBlockAssociation() *schema.Resource { ForceNew: true, ValidateFunc: validation.IntBetween(VPCCIDRMinIPv4, VPCCIDRMaxIPv4), }, + "vpc_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, }, Timeouts: &schema.ResourceTimeout{ @@ -74,41 +73,36 @@ func ResourceVPCIPv4CIDRBlockAssociation() *schema.Resource { func resourceVPCIPv4CIDRBlockAssociationCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn - req := &ec2.AssociateVpcCidrBlockInput{ - VpcId: aws.String(d.Get("vpc_id").(string)), + vpcID := d.Get("vpc_id").(string) + input := &ec2.AssociateVpcCidrBlockInput{ + VpcId: aws.String(vpcID), } if v, ok := d.GetOk("cidr_block"); ok { - req.CidrBlock = aws.String(v.(string)) + input.CidrBlock = aws.String(v.(string)) } if v, ok := d.GetOk("ipv4_ipam_pool_id"); ok { - req.Ipv4IpamPoolId = aws.String(v.(string)) + input.Ipv4IpamPoolId = aws.String(v.(string)) } if v, ok := d.GetOk("ipv4_netmask_length"); ok { - req.Ipv4NetmaskLength = aws.Int64(int64(v.(int))) + input.Ipv4NetmaskLength = aws.Int64(int64(v.(int))) } - log.Printf("[DEBUG] Creating VPC IPv4 CIDR block association: %#v", req) - resp, err := conn.AssociateVpcCidrBlock(req) + log.Printf("[DEBUG] Creating EC2 VPC IPv4 CIDR Block Association: %s", input) + output, err := conn.AssociateVpcCidrBlock(input) + if err != nil { - return fmt.Errorf("Error creating VPC IPv4 CIDR block association: %s", err) + return fmt.Errorf("error creating EC2 VPC (%s) IPv4 CIDR Block Association: %w", vpcID, err) } - d.SetId(aws.StringValue(resp.CidrBlockAssociation.AssociationId)) + d.SetId(aws.StringValue(output.CidrBlockAssociation.AssociationId)) + + _, err = WaitVPCCIDRBlockAssociationCreated(conn, d.Id(), d.Timeout(schema.TimeoutCreate)) - stateConf := &resource.StateChangeConf{ - Pending: []string{ec2.VpcCidrBlockStateCodeAssociating}, - Target: []string{ec2.VpcCidrBlockStateCodeAssociated}, - Refresh: vpcIpv4CidrBlockAssociationStateRefresh(conn, d.Get("vpc_id").(string), d.Id()), - Timeout: d.Timeout(schema.TimeoutCreate), - Delay: 10 * time.Second, - MinTimeout: 5 * time.Second, - } - _, err = stateConf.WaitForState() if err != nil { - return fmt.Errorf("Error waiting for IPv4 CIDR block association (%s) to become available: %s", d.Id(), err) + return fmt.Errorf("error waiting for EC2 VPC (%s) IPv4 CIDR block (%s) to become associated: %w", vpcID, d.Id(), err) } return resourceVPCIPv4CIDRBlockAssociationRead(d, meta) @@ -117,40 +111,16 @@ func resourceVPCIPv4CIDRBlockAssociationCreate(d *schema.ResourceData, meta inte func resourceVPCIPv4CIDRBlockAssociationRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn - input := &ec2.DescribeVpcsInput{ - Filters: BuildAttributeFilterList( - map[string]string{ - "cidr-block-association.association-id": d.Id(), - }, - ), - } - - log.Printf("[DEBUG] Describing VPCs: %s", input) - output, err := conn.DescribeVpcs(input) - if err != nil { - return fmt.Errorf("error describing VPCs: %s", err) - } + vpcCidrBlockAssociation, vpc, err := FindVPCCIDRBlockAssociationByID(conn, d.Id()) - if output == nil || len(output.Vpcs) == 0 || output.Vpcs[0] == nil { - log.Printf("[WARN] IPv4 CIDR block association (%s) not found, removing from state", d.Id()) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] EC2 VPC IPv4 CIDR Block Association %s not found, removing from state", d.Id()) d.SetId("") return nil } - vpc := output.Vpcs[0] - - var vpcCidrBlockAssociation *ec2.VpcCidrBlockAssociation - for _, cidrBlockAssociation := range vpc.CidrBlockAssociationSet { - if aws.StringValue(cidrBlockAssociation.AssociationId) == d.Id() { - vpcCidrBlockAssociation = cidrBlockAssociation - break - } - } - - if vpcCidrBlockAssociation == nil { - log.Printf("[WARN] IPv4 CIDR block association (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil + if err != nil { + return fmt.Errorf("error reading EC2 VPC IPv4 CIDR Block Association (%s): %w", d.Id(), err) } d.Set("cidr_block", vpcCidrBlockAssociation.CidrBlock) @@ -162,48 +132,24 @@ func resourceVPCIPv4CIDRBlockAssociationRead(d *schema.ResourceData, meta interf func resourceVPCIPv4CIDRBlockAssociationDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn - log.Printf("[DEBUG] Deleting VPC IPv4 CIDR block association: %s", d.Id()) + log.Printf("[DEBUG] Deleting EC2 VPC IPv4 CIDR Block Association: %s", d.Id()) _, err := conn.DisassociateVpcCidrBlock(&ec2.DisassociateVpcCidrBlockInput{ AssociationId: aws.String(d.Id()), }) - if err != nil { - if tfawserr.ErrMessageContains(err, "InvalidVpcID.NotFound", "") { - return nil - } - return fmt.Errorf("Error deleting VPC IPv4 CIDR block association: %s", err) - } - stateConf := &resource.StateChangeConf{ - Pending: []string{ec2.VpcCidrBlockStateCodeDisassociating}, - Target: []string{ec2.VpcCidrBlockStateCodeDisassociated, VpcCidrBlockStateCodeDeleted}, - Refresh: vpcIpv4CidrBlockAssociationStateRefresh(conn, d.Get("vpc_id").(string), d.Id()), - Timeout: d.Timeout(schema.TimeoutDelete), - Delay: 10 * time.Second, - MinTimeout: 5 * time.Second, + if tfawserr.ErrCodeEquals(err, ErrCodeInvalidVpcCidrBlockAssociationIDNotFound) { + return nil } - _, err = stateConf.WaitForState() + if err != nil { - return fmt.Errorf("Error waiting for VPC IPv4 CIDR block association (%s) to be deleted: %s", d.Id(), err.Error()) + return fmt.Errorf("error deleting EC2 VPC IPv4 CIDR Block Association (%s): %w", d.Id(), err) } - return nil -} - -func vpcIpv4CidrBlockAssociationStateRefresh(conn *ec2.EC2, vpcId, assocId string) resource.StateRefreshFunc { - return func() (interface{}, string, error) { - vpc, err := vpcDescribe(conn, vpcId) - if err != nil { - return nil, "", err - } - - if vpc != nil { - for _, cidrAssociation := range vpc.CidrBlockAssociationSet { - if aws.StringValue(cidrAssociation.AssociationId) == assocId { - return cidrAssociation, aws.StringValue(cidrAssociation.CidrBlockState.State), nil - } - } - } + _, err = WaitVPCCIDRBlockAssociationDeleted(conn, d.Id(), d.Timeout(schema.TimeoutDelete)) - return "", VpcCidrBlockStateCodeDeleted, nil + if err != nil { + return fmt.Errorf("error waiting for EC2 VPC IPv4 CIDR block (%s) to become disassociated: %w", d.Id(), err) } + + return nil } diff --git a/internal/service/ec2/vpc_ipv4_cidr_block_association_test.go b/internal/service/ec2/vpc_ipv4_cidr_block_association_test.go index 74ab56b6ae7..6ab5874919a 100644 --- a/internal/service/ec2/vpc_ipv4_cidr_block_association_test.go +++ b/internal/service/ec2/vpc_ipv4_cidr_block_association_test.go @@ -6,16 +6,21 @@ import ( "testing" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" + 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" + tfec2 "github.com/hashicorp/terraform-provider-aws/internal/service/ec2" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) func TestAccVPCIPv4CIDRBlockAssociation_basic(t *testing.T) { var associationSecondary, associationTertiary ec2.VpcCidrBlockAssociation + resource1Name := "aws_vpc_ipv4_cidr_block_association.secondary_cidr" + resource2Name := "aws_vpc_ipv4_cidr_block_association.tertiary_cidr" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -24,21 +29,21 @@ func TestAccVPCIPv4CIDRBlockAssociation_basic(t *testing.T) { CheckDestroy: testAccCheckVPCIPv4CIDRBlockAssociationDestroy, Steps: []resource.TestStep{ { - Config: testAccVPCIPv4CIDRBlockAssociationConfig, + Config: testAccVPCIPv4CIDRBlockAssociationConfig(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckVPCIPv4CIDRBlockAssociationExists("aws_vpc_ipv4_cidr_block_association.secondary_cidr", &associationSecondary), + testAccCheckVPCIPv4CIDRBlockAssociationExists(resource1Name, &associationSecondary), testAccCheckAdditionalVPCIPv4CIDRBlock(&associationSecondary, "172.2.0.0/16"), - testAccCheckVPCIPv4CIDRBlockAssociationExists("aws_vpc_ipv4_cidr_block_association.tertiary_cidr", &associationTertiary), + testAccCheckVPCIPv4CIDRBlockAssociationExists(resource2Name, &associationTertiary), testAccCheckAdditionalVPCIPv4CIDRBlock(&associationTertiary, "170.2.0.0/16"), ), }, { - ResourceName: "aws_vpc_ipv4_cidr_block_association.secondary_cidr", + ResourceName: resource1Name, ImportState: true, ImportStateVerify: true, }, { - ResourceName: "aws_vpc_ipv4_cidr_block_association.tertiary_cidr", + ResourceName: resource2Name, ImportState: true, ImportStateVerify: true, }, @@ -46,9 +51,38 @@ func TestAccVPCIPv4CIDRBlockAssociation_basic(t *testing.T) { }) } +func TestAccVPCIPv4CIDRBlockAssociation_disappears(t *testing.T) { + var associationSecondary, associationTertiary ec2.VpcCidrBlockAssociation + resource1Name := "aws_vpc_ipv4_cidr_block_association.secondary_cidr" + resource2Name := "aws_vpc_ipv4_cidr_block_association.tertiary_cidr" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckVPCIPv4CIDRBlockAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVPCIPv4CIDRBlockAssociationConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCIPv4CIDRBlockAssociationExists(resource1Name, &associationSecondary), + testAccCheckVPCIPv4CIDRBlockAssociationExists(resource2Name, &associationTertiary), + acctest.CheckResourceDisappears(acctest.Provider, tfec2.ResourceVPCIPv4CIDRBlockAssociation(), resource1Name), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + func TestAccVPCIPv4CIDRBlockAssociation_IpamBasic(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var associationSecondary ec2.VpcCidrBlockAssociation - netmaskLength := "28" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t); testAccIPAMPreCheck(t) }, @@ -57,10 +91,10 @@ func TestAccVPCIPv4CIDRBlockAssociation_IpamBasic(t *testing.T) { CheckDestroy: testAccCheckVPCIPv4CIDRBlockAssociationDestroy, Steps: []resource.TestStep{ { - Config: testAccVPCIPv4CIDRBlockAssociationIpam(netmaskLength), + Config: testAccVPCIPv4CIDRBlockAssociationIpam(rName, 28), Check: resource.ComposeTestCheckFunc( testAccCheckVPCIPv4CIDRBlockAssociationExists("aws_vpc_ipv4_cidr_block_association.secondary_cidr", &associationSecondary), - testAccCheckVPCAssociationCIDRPrefix(&associationSecondary, netmaskLength), + testAccCheckVPCAssociationCIDRPrefix(&associationSecondary, "28"), ), }, }, @@ -68,8 +102,13 @@ func TestAccVPCIPv4CIDRBlockAssociation_IpamBasic(t *testing.T) { } func TestAccVPCIPv4CIDRBlockAssociation_IpamBasicExplicitCIDR(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var associationSecondary ec2.VpcCidrBlockAssociation cidr := "172.2.0.32/28" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t); testAccIPAMPreCheck(t) }, @@ -78,7 +117,7 @@ func TestAccVPCIPv4CIDRBlockAssociation_IpamBasicExplicitCIDR(t *testing.T) { CheckDestroy: testAccCheckVPCIPv4CIDRBlockAssociationDestroy, Steps: []resource.TestStep{ { - Config: testAccVPCIPv4CIDRBlockAssociationIpamExplicitCIDR(cidr), + Config: testAccVPCIPv4CIDRBlockAssociationIpamExplicitCIDR(rName, cidr), Check: resource.ComposeTestCheckFunc( testAccCheckVPCIPv4CIDRBlockAssociationExists("aws_vpc_ipv4_cidr_block_association.secondary_cidr", &associationSecondary), testAccCheckAdditionalVPCIPv4CIDRBlock(&associationSecondary, cidr)), @@ -116,37 +155,23 @@ func testAccCheckVPCIPv4CIDRBlockAssociationDestroy(s *terraform.State) error { continue } - // Try to find the VPC - DescribeVpcOpts := &ec2.DescribeVpcsInput{ - VpcIds: []*string{aws.String(rs.Primary.Attributes["vpc_id"])}, - } - resp, err := conn.DescribeVpcs(DescribeVpcOpts) - if err == nil { - vpc := resp.Vpcs[0] - - for _, ipv4Association := range vpc.CidrBlockAssociationSet { - if *ipv4Association.AssociationId == rs.Primary.ID { - return fmt.Errorf("VPC CIDR block association still exists") - } - } + _, _, err := tfec2.FindVPCCIDRBlockAssociationByID(conn, rs.Primary.ID) - return nil + if tfresource.NotFound(err) { + continue } - // Verify the error is what we want - ec2err, ok := err.(awserr.Error) - if !ok { - return err - } - if ec2err.Code() != "InvalidVpcID.NotFound" { + if err != nil { return err } + + return fmt.Errorf("EC2 VPC IPv4 CIDR Block Association %s still exists", rs.Primary.ID) } return nil } -func testAccCheckVPCIPv4CIDRBlockAssociationExists(n string, association *ec2.VpcCidrBlockAssociation) resource.TestCheckFunc { +func testAccCheckVPCIPv4CIDRBlockAssociationExists(n string, v *ec2.VpcCidrBlockAssociation) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -154,96 +179,81 @@ func testAccCheckVPCIPv4CIDRBlockAssociationExists(n string, association *ec2.Vp } if rs.Primary.ID == "" { - return fmt.Errorf("No VPC ID is set") + return fmt.Errorf("No EC2 VPC IPv4 CIDR Block Association is set") } conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn - DescribeVpcOpts := &ec2.DescribeVpcsInput{ - VpcIds: []*string{aws.String(rs.Primary.Attributes["vpc_id"])}, - } - resp, err := conn.DescribeVpcs(DescribeVpcOpts) + + output, _, err := tfec2.FindVPCCIDRBlockAssociationByID(conn, rs.Primary.ID) + if err != nil { return err } - if len(resp.Vpcs) == 0 { - return fmt.Errorf("VPC not found") - } - - vpc := resp.Vpcs[0] - found := false - for _, cidrAssociation := range vpc.CidrBlockAssociationSet { - if *cidrAssociation.AssociationId == rs.Primary.ID { - *association = *cidrAssociation - found = true - } - } - if !found { - return fmt.Errorf("VPC CIDR block association not found") - } + *v = *output return nil } } -const testAccVPCIPv4CIDRBlockAssociationConfig = ` -resource "aws_vpc" "foo" { +func testAccVPCIPv4CIDRBlockAssociationConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_vpc" "test" { cidr_block = "10.1.0.0/16" tags = { - Name = "terraform-testacc-vpc-ipv4-cidr-block-association" + Name = %[1]q } } resource "aws_vpc_ipv4_cidr_block_association" "secondary_cidr" { - vpc_id = aws_vpc.foo.id + vpc_id = aws_vpc.test.id cidr_block = "172.2.0.0/16" } resource "aws_vpc_ipv4_cidr_block_association" "tertiary_cidr" { - vpc_id = aws_vpc.foo.id + vpc_id = aws_vpc.test.id cidr_block = "170.2.0.0/16" } -` +`, rName) +} -func testAccVPCIPv4CIDRBlockAssociationIpam(netmaskLength string) string { - return testAccVpcIpamBase + fmt.Sprintf(` +func testAccVPCIPv4CIDRBlockAssociationIpam(rName string, netmaskLength int) string { + return acctest.ConfigCompose(testAccVpcIPv4IPAMConfigBase(rName), fmt.Sprintf(` resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-vpc-ipv4-cidr-block-association" + Name = %[1]q } } resource "aws_vpc_ipv4_cidr_block_association" "secondary_cidr" { ipv4_ipam_pool_id = aws_vpc_ipam_pool.test.id - ipv4_netmask_length = %[1]q + ipv4_netmask_length = %[2]d vpc_id = aws_vpc.test.id - depends_on = [ - aws_vpc_ipam_pool_cidr.test - ] + + depends_on = [aws_vpc_ipam_pool_cidr.test] } -`, netmaskLength) +`, rName, netmaskLength)) } -func testAccVPCIPv4CIDRBlockAssociationIpamExplicitCIDR(cidr string) string { - return testAccVpcIpamBase + fmt.Sprintf(` +func testAccVPCIPv4CIDRBlockAssociationIpamExplicitCIDR(rName, cidr string) string { + return acctest.ConfigCompose(testAccVpcIPv4IPAMConfigBase(rName), fmt.Sprintf(` resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-vpc-ipv4-cidr-block-association" + Name = %[1]q } } resource "aws_vpc_ipv4_cidr_block_association" "secondary_cidr" { ipv4_ipam_pool_id = aws_vpc_ipam_pool.test.id - cidr_block = %[1]q + cidr_block = %[2]q vpc_id = aws_vpc.test.id - depends_on = [ - aws_vpc_ipam_pool_cidr.test - ] + + depends_on = [aws_vpc_ipam_pool_cidr.test] } -`, cidr) +`, rName, cidr)) } diff --git a/internal/service/ec2/vpc_ipv6_cidr_block_association.go b/internal/service/ec2/vpc_ipv6_cidr_block_association.go index 5d9714ce3b9..afd95fbd8d2 100644 --- a/internal/service/ec2/vpc_ipv6_cidr_block_association.go +++ b/internal/service/ec2/vpc_ipv6_cidr_block_association.go @@ -9,26 +9,26 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/aws-sdk-go-base/tfawserr" - "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" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/internal/verify" ) -// acceptance tests for byoip related tests are in vpc_byoip_test.go func ResourceVPCIPv6CIDRBlockAssociation() *schema.Resource { return &schema.Resource{ Create: resourceVPCIPv6CIDRBlockAssociationCreate, Read: resourceVPCIPv6CIDRBlockAssociationRead, Delete: resourceVPCIPv6CIDRBlockAssociationDelete, + Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, CustomizeDiff: func(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { - // ipv6_cidr_block can be set by a value returned from IPAM or explicitly in config + // ipv6_cidr_block can be set by a value returned from IPAM or explicitly in config. if diff.Id() != "" && diff.HasChange("ipv6_cidr_block") { - // if netmask is set then ipv6_cidr_block is derived from ipam, ignore changes + // If netmask is set then ipv6_cidr_block is derived from IPAM, ignore changes. if diff.Get("ipv6_netmask_length") != 0 { return diff.Clear("ipv6_cidr_block") } @@ -37,12 +37,6 @@ func ResourceVPCIPv6CIDRBlockAssociation() *schema.Resource { return nil }, Schema: map[string]*schema.Schema{ - "vpc_id": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - "ipv6_cidr_block": { Type: schema.TypeString, Optional: true, @@ -72,6 +66,11 @@ func ResourceVPCIPv6CIDRBlockAssociation() *schema.Resource { // This RequiredWith setting should be applied once L57 is completed // RequiredWith: []string{"ipv6_ipam_pool_id"}, }, + "vpc_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, }, Timeouts: &schema.ResourceTimeout{ @@ -84,41 +83,36 @@ func ResourceVPCIPv6CIDRBlockAssociation() *schema.Resource { func resourceVPCIPv6CIDRBlockAssociationCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn - req := &ec2.AssociateVpcCidrBlockInput{ - VpcId: aws.String(d.Get("vpc_id").(string)), + vpcID := d.Get("vpc_id").(string) + input := &ec2.AssociateVpcCidrBlockInput{ + VpcId: aws.String(vpcID), } if v, ok := d.GetOk("ipv6_cidr_block"); ok { - req.Ipv6CidrBlock = aws.String(v.(string)) + input.Ipv6CidrBlock = aws.String(v.(string)) } if v, ok := d.GetOk("ipv6_ipam_pool_id"); ok { - req.Ipv6IpamPoolId = aws.String(v.(string)) + input.Ipv6IpamPoolId = aws.String(v.(string)) } if v, ok := d.GetOk("ipv6_netmask_length"); ok { - req.Ipv6NetmaskLength = aws.Int64(int64(v.(int))) + input.Ipv6NetmaskLength = aws.Int64(int64(v.(int))) } - log.Printf("[DEBUG] Creating VPC IPv6 CIDR block association: %#v", req) - resp, err := conn.AssociateVpcCidrBlock(req) + log.Printf("[DEBUG] Creating EC2 VPC IPv6 CIDR Block Association: %s", input) + output, err := conn.AssociateVpcCidrBlock(input) + if err != nil { - return fmt.Errorf("Error creating VPC IPv6 CIDR block association: %s", err) + return fmt.Errorf("error creating EC2 VPC (%s) IPv6 CIDR Block Association: %w", vpcID, err) } - d.SetId(aws.StringValue(resp.Ipv6CidrBlockAssociation.AssociationId)) + d.SetId(aws.StringValue(output.Ipv6CidrBlockAssociation.AssociationId)) + + _, err = WaitVPCIPv6CIDRBlockAssociationCreated(conn, d.Id(), d.Timeout(schema.TimeoutCreate)) - stateConf := &resource.StateChangeConf{ - Pending: []string{ec2.VpcCidrBlockStateCodeAssociating}, - Target: []string{ec2.VpcCidrBlockStateCodeAssociated}, - Refresh: vpcIpv6CidrBlockAssociationStateRefresh(conn, d.Get("vpc_id").(string), d.Id()), - Timeout: d.Timeout(schema.TimeoutCreate), - Delay: 10 * time.Second, - MinTimeout: 5 * time.Second, - } - _, err = stateConf.WaitForState() if err != nil { - return fmt.Errorf("Error waiting for IPv6 CIDR block association (%s) to become available: %s", d.Id(), err) + return fmt.Errorf("error waiting for EC2 VPC (%s) IPv6 CIDR block (%s) to become associated: %w", vpcID, d.Id(), err) } return resourceVPCIPv6CIDRBlockAssociationRead(d, meta) @@ -127,40 +121,16 @@ func resourceVPCIPv6CIDRBlockAssociationCreate(d *schema.ResourceData, meta inte func resourceVPCIPv6CIDRBlockAssociationRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn - input := &ec2.DescribeVpcsInput{ - Filters: BuildAttributeFilterList( - map[string]string{ - "ipv6-cidr-block-association.association-id": d.Id(), - }, - ), - } - - log.Printf("[DEBUG] Describing VPCs: %s", input) - output, err := conn.DescribeVpcs(input) - if err != nil { - return fmt.Errorf("error describing VPCs: %s", err) - } + vpcIpv6CidrBlockAssociation, vpc, err := FindVPCIPv6CIDRBlockAssociationByID(conn, d.Id()) - if output == nil || len(output.Vpcs) == 0 || output.Vpcs[0] == nil { - log.Printf("[WARN] IPv6 CIDR block association (%s) not found, removing from state", d.Id()) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] EC2 VPC IPv6 CIDR Block Association %s not found, removing from state", d.Id()) d.SetId("") return nil } - vpc := output.Vpcs[0] - - var vpcIpv6CidrBlockAssociation *ec2.VpcIpv6CidrBlockAssociation - for _, ipv6CidrBlockAssociation := range vpc.Ipv6CidrBlockAssociationSet { - if aws.StringValue(ipv6CidrBlockAssociation.AssociationId) == d.Id() { - vpcIpv6CidrBlockAssociation = ipv6CidrBlockAssociation - break - } - } - - if vpcIpv6CidrBlockAssociation == nil { - log.Printf("[WARN] IPv6 CIDR block association (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil + if err != nil { + return fmt.Errorf("error reading EC2 VPC IPv6 CIDR Block Association (%s): %w", d.Id(), err) } d.Set("ipv6_cidr_block", vpcIpv6CidrBlockAssociation.Ipv6CidrBlock) @@ -172,48 +142,24 @@ func resourceVPCIPv6CIDRBlockAssociationRead(d *schema.ResourceData, meta interf func resourceVPCIPv6CIDRBlockAssociationDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn - log.Printf("[DEBUG] Deleting VPC IPv6 CIDR block association: %s", d.Id()) + log.Printf("[DEBUG] Deleting VPC IPv6 CIDR Block Association: %s", d.Id()) _, err := conn.DisassociateVpcCidrBlock(&ec2.DisassociateVpcCidrBlockInput{ AssociationId: aws.String(d.Id()), }) - if err != nil { - if tfawserr.ErrMessageContains(err, "InvalidVpcID.NotFound", "") { - return nil - } - return fmt.Errorf("Error deleting VPC IPv6 CIDR block association: %s", err) - } - stateConf := &resource.StateChangeConf{ - Pending: []string{ec2.VpcCidrBlockStateCodeDisassociating}, - Target: []string{ec2.VpcCidrBlockStateCodeDisassociated, VpcCidrBlockStateCodeDeleted}, - Refresh: vpcIpv6CidrBlockAssociationStateRefresh(conn, d.Get("vpc_id").(string), d.Id()), - Timeout: d.Timeout(schema.TimeoutDelete), - Delay: 10 * time.Second, - MinTimeout: 5 * time.Second, + if tfawserr.ErrCodeEquals(err, ErrCodeInvalidVpcCidrBlockAssociationIDNotFound) { + return nil } - _, err = stateConf.WaitForState() + if err != nil { - return fmt.Errorf("Error waiting for VPC IPv6 CIDR block association (%s) to be deleted: %s", d.Id(), err.Error()) + return fmt.Errorf("error deleting EC2 VPC IPv6 CIDR Block Association (%s): %w", d.Id(), err) } - return nil -} + _, err = WaitVPCIPv6CIDRBlockAssociationDeleted(conn, d.Id(), d.Timeout(schema.TimeoutDelete)) -func vpcIpv6CidrBlockAssociationStateRefresh(conn *ec2.EC2, vpcId, assocId string) resource.StateRefreshFunc { - return func() (interface{}, string, error) { - vpc, err := vpcDescribe(conn, vpcId) - if err != nil { - return nil, "", err - } - - if vpc != nil { - for _, ipv6CidrAssociation := range vpc.Ipv6CidrBlockAssociationSet { - if aws.StringValue(ipv6CidrAssociation.AssociationId) == assocId { - return ipv6CidrAssociation, aws.StringValue(ipv6CidrAssociation.Ipv6CidrBlockState.State), nil - } - } - } - - return "", VpcCidrBlockStateCodeDeleted, nil + if err != nil { + return fmt.Errorf("error waiting for EC2 VPC IPv6 CIDR block (%s) to become disassociated: %w", d.Id(), err) } + + return nil } diff --git a/internal/service/ec2/vpc_ipv6_cidr_block_association_test.go b/internal/service/ec2/vpc_ipv6_cidr_block_association_test.go new file mode 100644 index 00000000000..66cbe9e7807 --- /dev/null +++ b/internal/service/ec2/vpc_ipv6_cidr_block_association_test.go @@ -0,0 +1,74 @@ +package ec2_test + +import ( + "fmt" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "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" + tfec2 "github.com/hashicorp/terraform-provider-aws/internal/service/ec2" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func testAccCheckVPCIPv6CIDRBlockAssociationDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_vpc_ipv6_cidr_block_association" { + continue + } + + _, _, err := tfec2.FindVPCIPv6CIDRBlockAssociationByID(conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("EC2 VPC IPv6 CIDR Block Association %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccCheckVPCIPv6CIDRBlockAssociationExists(n string, v *ec2.VpcIpv6CidrBlockAssociation) 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 EC2 VPC IPv6 CIDR Block Association is set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn + + output, _, err := tfec2.FindVPCIPv6CIDRBlockAssociationByID(conn, rs.Primary.ID) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccCheckVPCAssociationIPv6CIDRPrefix(association *ec2.VpcIpv6CidrBlockAssociation, expected string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if strings.Split(aws.StringValue(association.Ipv6CidrBlock), "/")[1] != expected { + return fmt.Errorf("Bad cidr prefix: %s", aws.StringValue(association.Ipv6CidrBlock)) + } + + return nil + } +} diff --git a/internal/service/ec2/vpc_test.go b/internal/service/ec2/vpc_test.go index 3331faed44a..5c79619cb63 100644 --- a/internal/service/ec2/vpc_test.go +++ b/internal/service/ec2/vpc_test.go @@ -7,19 +7,17 @@ import ( "testing" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/service/ec2" + 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/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" "github.com/hashicorp/terraform-provider-aws/internal/conns" tfec2 "github.com/hashicorp/terraform-provider-aws/internal/service/ec2" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) -// add sweeper to delete known test vpcs - func TestAccVPC_basic(t *testing.T) { var vpc ec2.Vpc resourceName := "aws_vpc.test" @@ -32,20 +30,30 @@ func TestAccVPC_basic(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccVpcConfig, - Check: resource.ComposeTestCheckFunc( + Check: resource.ComposeAggregateTestCheckFunc( acctest.CheckVPCExists(resourceName, &vpc), - testAccCheckVpcCidr(&vpc, "10.1.0.0/16"), acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "ec2", regexp.MustCompile(`vpc/vpc-.+`)), - resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), resource.TestCheckResourceAttr(resourceName, "assign_generated_ipv6_cidr_block", "false"), - resource.TestMatchResourceAttr(resourceName, "default_route_table_id", regexp.MustCompile(`^rtb-.+`)), resource.TestCheckResourceAttr(resourceName, "cidr_block", "10.1.0.0/16"), + resource.TestCheckResourceAttrSet(resourceName, "default_network_acl_id"), + resource.TestCheckResourceAttrSet(resourceName, "default_route_table_id"), + resource.TestCheckResourceAttrSet(resourceName, "default_security_group_id"), + resource.TestCheckResourceAttrSet(resourceName, "dhcp_options_id"), + resource.TestCheckResourceAttr(resourceName, "enable_classiclink", "false"), + resource.TestCheckResourceAttr(resourceName, "enable_classiclink_dns_support", "false"), + resource.TestCheckResourceAttr(resourceName, "enable_dns_hostnames", "false"), resource.TestCheckResourceAttr(resourceName, "enable_dns_support", "true"), resource.TestCheckResourceAttr(resourceName, "instance_tenancy", "default"), + resource.TestCheckNoResourceAttr(resourceName, "ipv4_ipam_pool_id"), + resource.TestCheckNoResourceAttr(resourceName, "ipv4_netmask_length"), resource.TestCheckResourceAttr(resourceName, "ipv6_association_id", ""), resource.TestCheckResourceAttr(resourceName, "ipv6_cidr_block", ""), - resource.TestMatchResourceAttr(resourceName, "main_route_table_id", regexp.MustCompile(`^rtb-.+`)), + resource.TestCheckResourceAttr(resourceName, "ipv6_cidr_block_network_border_group", ""), + resource.TestCheckResourceAttr(resourceName, "ipv6_ipam_pool_id", ""), + resource.TestCheckResourceAttr(resourceName, "ipv6_netmask_length", "0"), + resource.TestCheckResourceAttrSet(resourceName, "main_route_table_id"), acctest.CheckResourceAttrAccountID(resourceName, "owner_id"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), ), }, { @@ -71,7 +79,7 @@ func TestAccVPC_disappears(t *testing.T) { Config: testAccVpcConfig, Check: resource.ComposeTestCheckFunc( acctest.CheckVPCExists(resourceName, &vpc), - testAccCheckVpcDisappears(&vpc), + acctest.CheckResourceDisappears(acctest.Provider, tfec2.ResourceVPC(), resourceName), ), ExpectNonEmptyPlan: true, }, @@ -79,6 +87,50 @@ func TestAccVPC_disappears(t *testing.T) { }) } +func TestAccVPC_tags(t *testing.T) { + var vpc ec2.Vpc + resourceName := "aws_vpc.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckVpcDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVPCTags1Config("key1", "value1"), + Check: resource.ComposeTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccVPCTags2Config("key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccVPCTags1Config("key2", "value2"), + Check: resource.ComposeTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + func TestAccVPC_DefaultTags_providerOnly(t *testing.T) { var providers []*schema.Provider var vpc ec2.Vpc @@ -541,121 +593,11 @@ func TestAccVPC_ignoreTags(t *testing.T) { }) } -func TestAccVPC_assignGeneratedIPv6CIDRBlock(t *testing.T) { - var vpc ec2.Vpc - resourceName := "aws_vpc.test" - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), - Providers: acctest.Providers, - CheckDestroy: testAccCheckVpcDestroy, - Steps: []resource.TestStep{ - { - Config: testAccVpcConfigAssignGeneratedIpv6CidrBlock(true), - Check: resource.ComposeAggregateTestCheckFunc( - acctest.CheckVPCExists(resourceName, &vpc), - testAccCheckVpcCidr(&vpc, "10.1.0.0/16"), - resource.TestCheckResourceAttr(resourceName, "assign_generated_ipv6_cidr_block", "true"), - resource.TestCheckResourceAttr(resourceName, "cidr_block", "10.1.0.0/16"), - resource.TestMatchResourceAttr(resourceName, "ipv6_association_id", regexp.MustCompile(`^vpc-cidr-assoc-.+`)), - resource.TestMatchResourceAttr(resourceName, "ipv6_cidr_block", regexp.MustCompile(`/56$`)), - ), - }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"assign_generated_ipv6_cidr_block"}, - }, - { - Config: testAccVpcConfigAssignGeneratedIpv6CidrBlock(false), - Check: resource.ComposeAggregateTestCheckFunc( - acctest.CheckVPCExists(resourceName, &vpc), - testAccCheckVpcCidr(&vpc, "10.1.0.0/16"), - resource.TestCheckResourceAttr(resourceName, "assign_generated_ipv6_cidr_block", "false"), - resource.TestCheckResourceAttr(resourceName, "cidr_block", "10.1.0.0/16"), - resource.TestCheckResourceAttr(resourceName, "ipv6_association_id", ""), - resource.TestCheckResourceAttr(resourceName, "ipv6_cidr_block", ""), - ), - }, - { - Config: testAccVpcConfigAssignGeneratedIpv6CidrBlock(true), - Check: resource.ComposeAggregateTestCheckFunc( - acctest.CheckVPCExists(resourceName, &vpc), - testAccCheckVpcCidr(&vpc, "10.1.0.0/16"), - resource.TestCheckResourceAttr(resourceName, "assign_generated_ipv6_cidr_block", "true"), - resource.TestCheckResourceAttr(resourceName, "cidr_block", "10.1.0.0/16"), - resource.TestMatchResourceAttr(resourceName, "ipv6_association_id", regexp.MustCompile(`^vpc-cidr-assoc-.+`)), - resource.TestMatchResourceAttr(resourceName, "ipv6_cidr_block", regexp.MustCompile(`/56$`)), - ), - }, - }, - }) -} - -func TestAccVPC_assignGeneratedIPv6CIDRBlockWithBorder(t *testing.T) { - var vpc ec2.Vpc - resourceName := "aws_vpc.test" - networkBorderGroup := "us-west-2-lax-1" // lintignore:AWSAT003 // currently the only generally available local zone - - current_region := acctest.Region() - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckRegion(t, endpoints.UsWest2RegionID) }, - ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), - Providers: acctest.Providers, - CheckDestroy: testAccCheckVpcDestroy, - Steps: []resource.TestStep{ - { - Config: testAccVpcConfigAssignGeneratedIpv6CidrBlockWithBorder(true, networkBorderGroup), - Check: resource.ComposeAggregateTestCheckFunc( - acctest.CheckVPCExists(resourceName, &vpc), - testAccCheckVpcCidr(&vpc, "10.1.0.0/16"), - resource.TestCheckResourceAttr(resourceName, "assign_generated_ipv6_cidr_block", "true"), - resource.TestCheckResourceAttr(resourceName, "cidr_block", "10.1.0.0/16"), - resource.TestCheckResourceAttr(resourceName, "ipv6_cidr_block_network_border_group", networkBorderGroup), - resource.TestMatchResourceAttr(resourceName, "ipv6_association_id", regexp.MustCompile(`^vpc-cidr-assoc-.+`)), - resource.TestMatchResourceAttr(resourceName, "ipv6_cidr_block", regexp.MustCompile(`/56$`)), - ), - }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"assign_generated_ipv6_cidr_block"}, - }, - { - Config: testAccVpcConfigAssignGeneratedIpv6CidrBlockWithBorder(false, current_region), - Check: resource.ComposeAggregateTestCheckFunc( - acctest.CheckVPCExists(resourceName, &vpc), - testAccCheckVpcCidr(&vpc, "10.1.0.0/16"), - resource.TestCheckResourceAttr(resourceName, "assign_generated_ipv6_cidr_block", "false"), - resource.TestCheckResourceAttr(resourceName, "cidr_block", "10.1.0.0/16"), - resource.TestCheckResourceAttr(resourceName, "ipv6_cidr_block_network_border_group", current_region), // lintignore:AWSAT003 // currently the only generally available local zone - resource.TestCheckResourceAttr(resourceName, "ipv6_association_id", ""), - resource.TestCheckResourceAttr(resourceName, "ipv6_cidr_block", ""), - ), - }, - { - Config: testAccVpcConfigAssignGeneratedIpv6CidrBlockWithBorder(true, networkBorderGroup), - Check: resource.ComposeAggregateTestCheckFunc( - acctest.CheckVPCExists(resourceName, &vpc), - testAccCheckVpcCidr(&vpc, "10.1.0.0/16"), - resource.TestCheckResourceAttr(resourceName, "assign_generated_ipv6_cidr_block", "true"), - resource.TestCheckResourceAttr(resourceName, "cidr_block", "10.1.0.0/16"), - resource.TestCheckResourceAttr(resourceName, "ipv6_cidr_block_network_border_group", networkBorderGroup), - resource.TestMatchResourceAttr(resourceName, "ipv6_association_id", regexp.MustCompile(`^vpc-cidr-assoc-.+`)), - resource.TestMatchResourceAttr(resourceName, "ipv6_cidr_block", regexp.MustCompile(`/56$`)), - ), - }, - }, - }) -} func TestAccVPC_tenancy(t *testing.T) { var vpcDedicated ec2.Vpc var vpcDefault ec2.Vpc resourceName := "aws_vpc.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -664,7 +606,7 @@ func TestAccVPC_tenancy(t *testing.T) { CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ { - Config: testAccVpcDedicatedConfig, + Config: testAccVpcConfigDedicatedTenancy(rName), Check: resource.ComposeTestCheckFunc( acctest.CheckVPCExists(resourceName, &vpcDedicated), resource.TestCheckResourceAttr(resourceName, "instance_tenancy", "dedicated"), @@ -676,7 +618,7 @@ func TestAccVPC_tenancy(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccVpcConfig, + Config: testAccVpcConfigDefault(rName), Check: resource.ComposeTestCheckFunc( acctest.CheckVPCExists(resourceName, &vpcDefault), resource.TestCheckResourceAttr(resourceName, "instance_tenancy", "default"), @@ -684,7 +626,7 @@ func TestAccVPC_tenancy(t *testing.T) { ), }, { - Config: testAccVpcDedicatedConfig, + Config: testAccVpcConfigDedicatedTenancy(rName), Check: resource.ComposeTestCheckFunc( acctest.CheckVPCExists(resourceName, &vpcDedicated), resource.TestCheckResourceAttr(resourceName, "instance_tenancy", "dedicated"), @@ -695,52 +637,69 @@ func TestAccVPC_tenancy(t *testing.T) { }) } -func TestAccVPC_IpamIpv4BasicNetmask(t *testing.T) { +func TestAccVPC_updateDNSHostnames(t *testing.T) { var vpc ec2.Vpc resourceName := "aws_vpc.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t); testAccIPAMPreCheck(t) }, + PreCheck: func() { acctest.PreCheck(t) }, ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), Providers: acctest.Providers, CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ { - Config: testAccVpcIpamIpv4(28), + Config: testAccVpcConfigDefault(rName), Check: resource.ComposeTestCheckFunc( acctest.CheckVPCExists(resourceName, &vpc), - testAccCheckVpcCidrPrefix(&vpc, "28"), + resource.TestCheckResourceAttr(resourceName, "enable_dns_hostnames", "false"), + ), + }, + { + Config: testAccVpcConfigEnableDNSHostnames(rName), + Check: resource.ComposeTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "enable_dns_hostnames", "true"), ), }, }, }) } -func TestAccVPC_IpamIpv4BasicExplicitCidr(t *testing.T) { +// https://github.com/hashicorp/terraform/issues/1301 +func TestAccVPC_bothDNSOptionsSet(t *testing.T) { var vpc ec2.Vpc resourceName := "aws_vpc.test" - cidr := "172.2.0.32/28" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t); testAccIPAMPreCheck(t) }, + PreCheck: func() { acctest.PreCheck(t) }, ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), Providers: acctest.Providers, CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ { - Config: testAccVpcIpamIpv4ExplicitCidr(cidr), + Config: testAccVpcConfigBothDnsOptions(rName), Check: resource.ComposeTestCheckFunc( acctest.CheckVPCExists(resourceName, &vpc), - testAccCheckVpcCidr(&vpc, cidr), + resource.TestCheckResourceAttr(resourceName, "enable_dns_hostnames", "true"), + resource.TestCheckResourceAttr(resourceName, "enable_dns_support", "true"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } -func TestAccVPC_tags(t *testing.T) { +// https://github.com/hashicorp/terraform/issues/10168 +func TestAccVPC_disabledDNSSupport(t *testing.T) { var vpc ec2.Vpc resourceName := "aws_vpc.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -749,11 +708,10 @@ func TestAccVPC_tags(t *testing.T) { CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ { - Config: testAccVPCTags1Config("key1", "value1"), + Config: testAccVpcConfigDisabledDnsSupport(rName), Check: resource.ComposeTestCheckFunc( acctest.CheckVPCExists(resourceName, &vpc), - resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), - resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + resource.TestCheckResourceAttr(resourceName, "enable_dns_support", "false"), ), }, { @@ -761,30 +719,14 @@ func TestAccVPC_tags(t *testing.T) { ImportState: true, ImportStateVerify: true, }, - { - Config: testAccVPCTags2Config("key1", "value1updated", "key2", "value2"), - Check: resource.ComposeTestCheckFunc( - acctest.CheckVPCExists(resourceName, &vpc), - resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), - resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), - resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), - ), - }, - { - Config: testAccVPCTags1Config("key2", "value2"), - Check: resource.ComposeTestCheckFunc( - acctest.CheckVPCExists(resourceName, &vpc), - resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), - resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), - ), - }, }, }) } -func TestAccVPC_update(t *testing.T) { +func TestAccVPC_classicLinkOptionSet(t *testing.T) { var vpc ec2.Vpc resourceName := "aws_vpc.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -793,28 +735,25 @@ func TestAccVPC_update(t *testing.T) { CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ { - Config: testAccVpcConfig, + Config: testAccVpcConfigClassicLinkOption(rName), Check: resource.ComposeTestCheckFunc( acctest.CheckVPCExists(resourceName, &vpc), - testAccCheckVpcCidr(&vpc, "10.1.0.0/16"), - resource.TestCheckResourceAttr(resourceName, "cidr_block", "10.1.0.0/16"), + resource.TestCheckResourceAttr(resourceName, "enable_classiclink", "true"), ), }, { - Config: testAccVpcConfigUpdate, - Check: resource.ComposeTestCheckFunc( - acctest.CheckVPCExists(resourceName, &vpc), - resource.TestCheckResourceAttr(resourceName, "enable_dns_hostnames", "true"), - ), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, }, }, }) } -// https://github.com/hashicorp/terraform/issues/1301 -func TestAccVPC_bothDNSOptionsSet(t *testing.T) { +func TestAccVPC_classicLinkDNSSupportOptionSet(t *testing.T) { var vpc ec2.Vpc resourceName := "aws_vpc.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -823,11 +762,10 @@ func TestAccVPC_bothDNSOptionsSet(t *testing.T) { CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ { - Config: testAccVpcConfig_BothDnsOptions, + Config: testAccVpcConfigClassicLinkDnsSupportOption(rName), Check: resource.ComposeTestCheckFunc( acctest.CheckVPCExists(resourceName, &vpc), - resource.TestCheckResourceAttr(resourceName, "enable_dns_hostnames", "true"), - resource.TestCheckResourceAttr(resourceName, "enable_dns_support", "true"), + resource.TestCheckResourceAttr(resourceName, "enable_classiclink_dns_support", "true"), ), }, { @@ -839,10 +777,10 @@ func TestAccVPC_bothDNSOptionsSet(t *testing.T) { }) } -// https://github.com/hashicorp/terraform/issues/10168 -func TestAccVPC_disabledDNSSupport(t *testing.T) { +func TestAccVPC_assignGeneratedIPv6CIDRBlock(t *testing.T) { var vpc ec2.Vpc resourceName := "aws_vpc.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -851,10 +789,13 @@ func TestAccVPC_disabledDNSSupport(t *testing.T) { CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ { - Config: testAccVpcConfig_DisabledDnsSupport, - Check: resource.ComposeTestCheckFunc( + Config: testAccVpcConfigAssignGeneratedIpv6CidrBlock(rName, true), + Check: resource.ComposeAggregateTestCheckFunc( acctest.CheckVPCExists(resourceName, &vpc), - resource.TestCheckResourceAttr(resourceName, "enable_dns_support", "false"), + resource.TestCheckResourceAttr(resourceName, "assign_generated_ipv6_cidr_block", "true"), + resource.TestCheckResourceAttr(resourceName, "cidr_block", "10.1.0.0/16"), + resource.TestCheckResourceAttrSet(resourceName, "ipv6_association_id"), + resource.TestMatchResourceAttr(resourceName, "ipv6_cidr_block", regexp.MustCompile(`/56$`)), ), }, { @@ -862,25 +803,52 @@ func TestAccVPC_disabledDNSSupport(t *testing.T) { ImportState: true, ImportStateVerify: true, }, + { + Config: testAccVpcConfigAssignGeneratedIpv6CidrBlock(rName, false), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "assign_generated_ipv6_cidr_block", "false"), + resource.TestCheckResourceAttr(resourceName, "cidr_block", "10.1.0.0/16"), + resource.TestCheckResourceAttr(resourceName, "ipv6_association_id", ""), + resource.TestCheckResourceAttr(resourceName, "ipv6_cidr_block", ""), + ), + }, + { + Config: testAccVpcConfigAssignGeneratedIpv6CidrBlock(rName, true), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "assign_generated_ipv6_cidr_block", "true"), + resource.TestCheckResourceAttr(resourceName, "cidr_block", "10.1.0.0/16"), + resource.TestCheckResourceAttrSet(resourceName, "ipv6_association_id"), + resource.TestMatchResourceAttr(resourceName, "ipv6_cidr_block", regexp.MustCompile(`/56$`)), + ), + }, }, }) } -func TestAccVPC_classicLinkOptionSet(t *testing.T) { +func TestAccVPC_assignGeneratedIPv6CIDRBlockWithNetworkBorderGroup(t *testing.T) { var vpc ec2.Vpc resourceName := "aws_vpc.test" + azDataSourceName := "data.aws_availability_zone.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, + PreCheck: func() { acctest.PreCheck(t); testAccPreCheckLocalZoneAvailable(t) }, ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), Providers: acctest.Providers, CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ { - Config: testAccVpcConfig_ClassiclinkOption, - Check: resource.ComposeTestCheckFunc( + Config: testAccVpcConfigAssignGeneratedIpv6CidrBlockWithOptionalNetworkBorderGroup(rName, true), + Check: resource.ComposeAggregateTestCheckFunc( acctest.CheckVPCExists(resourceName, &vpc), - resource.TestCheckResourceAttr(resourceName, "enable_classiclink", "true"), + resource.TestCheckResourceAttr(resourceName, "assign_generated_ipv6_cidr_block", "true"), + resource.TestCheckResourceAttrSet(resourceName, "ipv6_association_id"), + resource.TestCheckResourceAttrSet(resourceName, "ipv6_cidr_block"), + resource.TestCheckResourceAttrPair(resourceName, "ipv6_cidr_block_network_border_group", azDataSourceName, "network_border_group"), + resource.TestCheckResourceAttr(resourceName, "ipv6_ipam_pool_id", ""), + resource.TestCheckResourceAttr(resourceName, "ipv6_netmask_length", "0"), ), }, { @@ -888,31 +856,70 @@ func TestAccVPC_classicLinkOptionSet(t *testing.T) { ImportState: true, ImportStateVerify: true, }, + { + Config: testAccVpcConfigAssignGeneratedIpv6CidrBlockWithOptionalNetworkBorderGroup(rName, false), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "assign_generated_ipv6_cidr_block", "true"), + resource.TestCheckResourceAttrSet(resourceName, "ipv6_association_id"), + resource.TestCheckResourceAttrSet(resourceName, "ipv6_cidr_block"), + resource.TestCheckResourceAttr(resourceName, "ipv6_cidr_block_network_border_group", acctest.Region()), + resource.TestCheckResourceAttr(resourceName, "ipv6_ipam_pool_id", ""), + resource.TestCheckResourceAttr(resourceName, "ipv6_netmask_length", "0"), + ), + }, }, }) } -func TestAccVPC_classicLinkDNSSupportOptionSet(t *testing.T) { +func TestAccVPC_IpamIpv4BasicNetmask(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var vpc ec2.Vpc resourceName := "aws_vpc.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, + PreCheck: func() { acctest.PreCheck(t); testAccIPAMPreCheck(t) }, ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), Providers: acctest.Providers, CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ { - Config: testAccVpcConfig_ClassiclinkDnsSupportOption, + Config: testAccVpcIpamIpv4(rName, 28), Check: resource.ComposeTestCheckFunc( acctest.CheckVPCExists(resourceName, &vpc), - resource.TestCheckResourceAttr(resourceName, "enable_classiclink_dns_support", "true"), + testAccCheckVpcCidrPrefix(&vpc, "28"), ), }, + }, + }) +} + +func TestAccVPC_IpamIpv4BasicExplicitCidr(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var vpc ec2.Vpc + resourceName := "aws_vpc.test" + cidr := "172.2.0.32/28" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); testAccIPAMPreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckVpcDestroy, + Steps: []resource.TestStep{ { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + Config: testAccVpcIpamIpv4ExplicitCidr(rName, cidr), + Check: resource.ComposeTestCheckFunc( + acctest.CheckVPCExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "cidr_block", cidr), + ), }, }, }) @@ -926,27 +933,17 @@ func testAccCheckVpcDestroy(s *terraform.State) error { continue } - // Try to find the VPC - DescribeVpcOpts := &ec2.DescribeVpcsInput{ - VpcIds: []*string{aws.String(rs.Primary.ID)}, - } - resp, err := conn.DescribeVpcs(DescribeVpcOpts) - if err == nil { - if len(resp.Vpcs) > 0 { - return fmt.Errorf("VPCs still exist.") - } + _, err := tfec2.FindVPCByID(conn, rs.Primary.ID) - return nil + if tfresource.NotFound(err) { + continue } - // Verify the error is what we want - ec2err, ok := err.(awserr.Error) - if !ok { - return err - } - if ec2err.Code() != "InvalidVpcID.NotFound" { + if err != nil { return err } + + return fmt.Errorf("EC2 VPC %s still exists", rs.Primary.ID) } return nil @@ -960,16 +957,6 @@ func testAccCheckVpcUpdateTags(vpc *ec2.Vpc, oldTags, newTags map[string]string) } } -func testAccCheckVpcCidr(vpc *ec2.Vpc, expected string) resource.TestCheckFunc { - return func(s *terraform.State) error { - if aws.StringValue(vpc.CidrBlock) != expected { - return fmt.Errorf("Bad cidr: %s", aws.StringValue(vpc.CidrBlock)) - } - - return nil - } -} - func testAccCheckVpcCidrPrefix(vpc *ec2.Vpc, expected string) resource.TestCheckFunc { return func(s *terraform.State) error { if strings.Split(aws.StringValue(vpc.CidrBlock), "/")[1] != expected { @@ -983,7 +970,7 @@ func testAccCheckVpcCidrPrefix(vpc *ec2.Vpc, expected string) resource.TestCheck func testAccCheckVpcIdsEqual(vpc1, vpc2 *ec2.Vpc) resource.TestCheckFunc { return func(s *terraform.State) error { if aws.StringValue(vpc1.VpcId) != aws.StringValue(vpc2.VpcId) { - return fmt.Errorf("VPC IDs not equal") + return fmt.Errorf("VPC IDs are not equal") } return nil @@ -1000,95 +987,12 @@ func testAccCheckVpcIdsNotEqual(vpc1, vpc2 *ec2.Vpc) resource.TestCheckFunc { } } -func testAccCheckVpcDisappears(vpc *ec2.Vpc) resource.TestCheckFunc { - return func(s *terraform.State) error { - conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn - - input := &ec2.DeleteVpcInput{ - VpcId: vpc.VpcId, - } - - _, err := conn.DeleteVpc(input) - - return err - } -} - const testAccVpcConfig = ` resource "aws_vpc" "test" { cidr_block = "10.1.0.0/16" } ` -func testAccVpcConfigAssignGeneratedIpv6CidrBlock(assignGeneratedIpv6CidrBlock bool) string { - if assignGeneratedIpv6CidrBlock == true { - return fmt.Sprintf(` -data "aws_region" "current" {} - -resource "aws_vpc" "test" { - assign_generated_ipv6_cidr_block = %[1]t - cidr_block = "10.1.0.0/16" - ipv6_cidr_block_network_border_group = data.aws_region.current.name - tags = { - Name = "terraform-testacc-vpc-ipv61" - } -} - `, assignGeneratedIpv6CidrBlock) - } else { - return fmt.Sprintf(` -resource "aws_vpc" "test" { - assign_generated_ipv6_cidr_block = %[1]t - cidr_block = "10.1.0.0/16" - tags = { - Name = "terraform-testacc-vpc-ipv62" - } -} - `, assignGeneratedIpv6CidrBlock) - } -} - -const testAccVpcConfigUpdate = ` -resource "aws_vpc" "test" { - cidr_block = "10.1.0.0/16" - enable_dns_hostnames = true - - tags = { - Name = "terraform-testacc-vpc" - } -} -` - -func testAccVpcConfigAssignGeneratedIpv6CidrBlockWithBorder(assignGeneratedIpv6CidrBlock bool, networkBorderGroup string) string { - // lintignore:AWSAT003 // currently the only generally available local zone - if networkBorderGroup != "us-west-2-lax-1" { - return fmt.Sprintf(` -data "aws_region" "current" {} - -resource "aws_vpc" "test" { - assign_generated_ipv6_cidr_block = %[1]t - cidr_block = "10.1.0.0/16" - ipv6_cidr_block_network_border_group = data.aws_region.current.name - tags = { - Name = "terraform-testacc-vpc-ipv6-with-border-group" - } -} -`, assignGeneratedIpv6CidrBlock, networkBorderGroup) - } else { - return fmt.Sprintf(` -data "aws_region" "current" {} - -resource "aws_vpc" "test" { - assign_generated_ipv6_cidr_block = %[1]t - cidr_block = "10.1.0.0/16" - ipv6_cidr_block_network_border_group = %[2]q - tags = { - Name = "terraform-testacc-vpc-ipv6-with-border-group" - } -} -`, assignGeneratedIpv6CidrBlock, networkBorderGroup) - } -} - func testAccVPCTags1Config(tagKey1, tagValue1 string) string { return fmt.Sprintf(` resource "aws_vpc" "test" { @@ -1156,103 +1060,202 @@ resource "aws_vpc" "test" { } ` -const testAccVpcDedicatedConfig = ` +func testAccVpcConfigAssignGeneratedIpv6CidrBlock(rName string, assignGeneratedIpv6CidrBlock bool) string { + return fmt.Sprintf(` +resource "aws_vpc" "test" { + assign_generated_ipv6_cidr_block = %[2]t + cidr_block = "10.1.0.0/16" + + tags = { + Name = %[1]q + } +} +`, rName, assignGeneratedIpv6CidrBlock) +} + +func testAccVpcConfigAssignGeneratedIpv6CidrBlockWithOptionalNetworkBorderGroup(rName string, localZoneNetworkBorderGroup bool) string { + return fmt.Sprintf(` +data "aws_region" "current" {} + +data "aws_availability_zones" "available" { + state = "available" + + filter { + name = "zone-type" + values = ["local-zone"] + } + + filter { + name = "opt-in-status" + values = ["opted-in"] + } +} + +data "aws_availability_zone" "test" { + zone_id = data.aws_availability_zones.available.zone_ids[0] +} + +resource "aws_vpc" "test" { + assign_generated_ipv6_cidr_block = true + cidr_block = "10.1.0.0/16" + ipv6_cidr_block_network_border_group = %[2]t ? data.aws_availability_zone.test.network_border_group : data.aws_region.current.name + + tags = { + Name = %[1]q + } +} +`, rName, localZoneNetworkBorderGroup) +} + +func testAccVpcConfigDefault(rName string) string { + return fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.1.0.0/16" + + tags = { + Name = %[1]q + } +} +`, rName) +} + +func testAccVpcConfigEnableDNSHostnames(rName string) string { + return fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.1.0.0/16" + enable_dns_hostnames = true + + tags = { + Name = %[1]q + } +} +`, rName) +} + +func testAccVpcConfigDedicatedTenancy(rName string) string { + return fmt.Sprintf(` resource "aws_vpc" "test" { instance_tenancy = "dedicated" cidr_block = "10.1.0.0/16" tags = { - Name = "terraform-testacc-vpc-dedicated" + Name = %[1]q } } -` +`, rName) +} -const testAccVpcConfig_BothDnsOptions = ` +func testAccVpcConfigBothDnsOptions(rName string) string { + return fmt.Sprintf(` resource "aws_vpc" "test" { cidr_block = "10.2.0.0/16" enable_dns_hostnames = true enable_dns_support = true tags = { - Name = "terraform-testacc-vpc-both-dns-opts" + Name = %[1]q } } -` +`, rName) +} -const testAccVpcConfig_DisabledDnsSupport = ` +func testAccVpcConfigDisabledDnsSupport(rName string) string { + return fmt.Sprintf(` resource "aws_vpc" "test" { cidr_block = "10.2.0.0/16" enable_dns_support = false tags = { - Name = "terraform-testacc-vpc-disabled-dns-support" + Name = %[1]q } } -` +`, rName) +} -const testAccVpcConfig_ClassiclinkOption = ` +func testAccVpcConfigClassicLinkOption(rName string) string { + return fmt.Sprintf(` resource "aws_vpc" "test" { cidr_block = "172.2.0.0/16" enable_classiclink = true tags = { - Name = "terraform-testacc-vpc-classic-link" + Name = %[1]q } } -` +`, rName) +} -const testAccVpcConfig_ClassiclinkDnsSupportOption = ` +func testAccVpcConfigClassicLinkDnsSupportOption(rName string) string { + return fmt.Sprintf(` resource "aws_vpc" "test" { cidr_block = "172.2.0.0/16" enable_classiclink = true enable_classiclink_dns_support = true tags = { - Name = "terraform-testacc-vpc-classic-link-support" + Name = %[1]q } } -` -const testAccVpcIpamBase = ` +`, rName) +} + +func testAccVpcIPv4IPAMConfigBase(rName string) string { + return fmt.Sprintf(` data "aws_region" "current" {} resource "aws_vpc_ipam" "test" { operating_regions { region_name = data.aws_region.current.name } + + tags = { + Name = %[1]q + } } resource "aws_vpc_ipam_pool" "test" { address_family = "ipv4" ipam_scope_id = aws_vpc_ipam.test.private_default_scope_id locale = data.aws_region.current.name + + tags = { + Name = %[1]q + } } resource "aws_vpc_ipam_pool_cidr" "test" { ipam_pool_id = aws_vpc_ipam_pool.test.id cidr = "172.2.0.0/16" } -` +`, rName) +} -func testAccVpcIpamIpv4(netmaskLength int) string { - return testAccVpcIpamBase + fmt.Sprintf(` +func testAccVpcIpamIpv4(rName string, netmaskLength int) string { + return acctest.ConfigCompose(testAccVpcIPv4IPAMConfigBase(rName), fmt.Sprintf(` resource "aws_vpc" "test" { ipv4_ipam_pool_id = aws_vpc_ipam_pool.test.id - ipv4_netmask_length = %[1]d - depends_on = [ - aws_vpc_ipam_pool_cidr.test - ] + ipv4_netmask_length = %[2]d + + tags = { + Name = %[1]q + } + + depends_on = [aws_vpc_ipam_pool_cidr.test] } -`, netmaskLength) +`, rName, netmaskLength)) } -func testAccVpcIpamIpv4ExplicitCidr(cidr string) string { - return testAccVpcIpamBase + fmt.Sprintf(` +func testAccVpcIpamIpv4ExplicitCidr(rName, cidr string) string { + return acctest.ConfigCompose(testAccVpcIPv4IPAMConfigBase(rName), fmt.Sprintf(` resource "aws_vpc" "test" { ipv4_ipam_pool_id = aws_vpc_ipam_pool.test.id - cidr_block = %[1]q - depends_on = [ - aws_vpc_ipam_pool_cidr.test - ] + cidr_block = %[2]q + + tags = { + Name = %[1]q + } + + depends_on = [aws_vpc_ipam_pool_cidr.test] } -`, cidr) +`, rName, cidr)) } diff --git a/internal/service/ec2/wait.go b/internal/service/ec2/wait.go index b99f0e171e1..2946b6c3cab 100644 --- a/internal/service/ec2/wait.go +++ b/internal/service/ec2/wait.go @@ -25,6 +25,8 @@ const ( RouteNotFoundChecks = 1000 // Should exceed any reasonable custom timeout value. RouteTableNotFoundChecks = 1000 // Should exceed any reasonable custom timeout value. RouteTableAssociationCreatedNotFoundChecks = 1000 // Should exceed any reasonable custom timeout value. + + SecurityGroupNotFoundChecks = 1000 // Should exceed any reasonable custom timeout value. ) const ( @@ -414,10 +416,12 @@ func WaitRouteTableAssociationUpdated(conn *ec2.EC2, id string) (*ec2.RouteTable func WaitSecurityGroupCreated(conn *ec2.EC2, id string, timeout time.Duration) (*ec2.SecurityGroup, error) { stateConf := &resource.StateChangeConf{ - Pending: []string{SecurityGroupStatusNotFound}, - Target: []string{SecurityGroupStatusCreated}, - Refresh: StatusSecurityGroup(conn, id), - Timeout: timeout, + Pending: []string{}, + Target: []string{SecurityGroupStatusCreated}, + Refresh: StatusSecurityGroup(conn, id), + Timeout: timeout, + NotFoundChecks: SecurityGroupNotFoundChecks, + ContinuousTargetOccurence: 3, } outputRaw, err := stateConf.WaitForState() @@ -705,15 +709,33 @@ func WaitTransitGatewayRouteTablePropagationStateDisabled(conn *ec2.EC2, transit } const ( - VPCPropagationTimeout = 2 * time.Minute - VPCAttributePropagationTimeout = 5 * time.Minute + vpcAttributePropagationTimeout = 5 * time.Minute + vpcCreatedTimeout = 10 * time.Minute + vpcDeletedTimeout = 5 * time.Minute ) +func WaitVPCCreated(conn *ec2.EC2, id string) (*ec2.Vpc, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.VpcStatePending}, + Target: []string{ec2.VpcStateAvailable}, + Refresh: StatusVPCState(conn, id), + Timeout: vpcCreatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.Vpc); ok { + return output, err + } + + return nil, err +} + func WaitVPCAttributeUpdated(conn *ec2.EC2, vpcID string, attribute string, expectedValue bool) (*ec2.Vpc, error) { stateConf := &resource.StateChangeConf{ Target: []string{strconv.FormatBool(expectedValue)}, - Refresh: StatusVPCAttribute(conn, vpcID, attribute), - Timeout: VPCAttributePropagationTimeout, + Refresh: StatusVPCAttributeValue(conn, vpcID, attribute), + Timeout: vpcAttributePropagationTimeout, Delay: 10 * time.Second, MinTimeout: 3 * time.Second, } @@ -727,6 +749,103 @@ func WaitVPCAttributeUpdated(conn *ec2.EC2, vpcID string, attribute string, expe return nil, err } +func WaitVPCCIDRBlockAssociationCreated(conn *ec2.EC2, id string, timeout time.Duration) (*ec2.VpcCidrBlockState, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.VpcCidrBlockStateCodeAssociating, ec2.VpcCidrBlockStateCodeDisassociated, ec2.VpcCidrBlockStateCodeFailing}, + Target: []string{ec2.VpcCidrBlockStateCodeAssociated}, + Refresh: StatusVPCCIDRBlockAssociationState(conn, id), + Timeout: timeout, + Delay: 10 * time.Second, + MinTimeout: 5 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.VpcCidrBlockState); ok { + if state := aws.StringValue(output.State); state == ec2.VpcCidrBlockStateCodeFailed { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StatusMessage))) + } + + return output, err + } + + return nil, err +} + +func WaitVPCCIDRBlockAssociationDeleted(conn *ec2.EC2, id string, timeout time.Duration) (*ec2.VpcCidrBlockState, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.VpcCidrBlockStateCodeAssociated, ec2.VpcCidrBlockStateCodeDisassociating, ec2.VpcCidrBlockStateCodeFailing}, + Target: []string{}, + Refresh: StatusVPCCIDRBlockAssociationState(conn, id), + Timeout: timeout, + Delay: 10 * time.Second, + MinTimeout: 5 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.VpcCidrBlockState); ok { + if state := aws.StringValue(output.State); state == ec2.VpcCidrBlockStateCodeFailed { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StatusMessage))) + } + + return output, err + } + + return nil, err +} + +const ( + vpcIPv6CIDRBlockAssociationCreatedTimeout = 10 * time.Minute + vpcIPv6CIDRBlockAssociationDeletedTimeout = 5 * time.Minute +) + +func WaitVPCIPv6CIDRBlockAssociationCreated(conn *ec2.EC2, id string, timeout time.Duration) (*ec2.VpcCidrBlockState, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.VpcCidrBlockStateCodeAssociating, ec2.VpcCidrBlockStateCodeDisassociated, ec2.VpcCidrBlockStateCodeFailing}, + Target: []string{ec2.VpcCidrBlockStateCodeAssociated}, + Refresh: StatusVPCIPv6CIDRBlockAssociationState(conn, id), + Timeout: timeout, + Delay: 10 * time.Second, + MinTimeout: 5 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.VpcCidrBlockState); ok { + if state := aws.StringValue(output.State); state == ec2.VpcCidrBlockStateCodeFailed { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StatusMessage))) + } + + return output, err + } + + return nil, err +} + +func WaitVPCIPv6CIDRBlockAssociationDeleted(conn *ec2.EC2, id string, timeout time.Duration) (*ec2.VpcCidrBlockState, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.VpcCidrBlockStateCodeAssociated, ec2.VpcCidrBlockStateCodeDisassociating, ec2.VpcCidrBlockStateCodeFailing}, + Target: []string{}, + Refresh: StatusVPCIPv6CIDRBlockAssociationState(conn, id), + Timeout: timeout, + Delay: 10 * time.Second, + MinTimeout: 5 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.VpcCidrBlockState); ok { + if state := aws.StringValue(output.State); state == ec2.VpcCidrBlockStateCodeFailed { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StatusMessage))) + } + + return output, err + } + + return nil, err +} + const ( VPNGatewayDeletedTimeout = 5 * time.Minute @@ -809,6 +928,55 @@ func WaitCustomerGatewayDeleted(conn *ec2.EC2, id string) (*ec2.CustomerGateway, return nil, err } +const ( + natGatewayCreatedTimeout = 10 * time.Minute + natGatewayDeletedTimeout = 30 * time.Minute +) + +func WaitNATGatewayCreated(conn *ec2.EC2, id string) (*ec2.NatGateway, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.NatGatewayStatePending}, + Target: []string{ec2.NatGatewayStateAvailable}, + Refresh: StatusNATGatewayState(conn, id), + Timeout: natGatewayCreatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.NatGateway); ok { + if state := aws.StringValue(output.State); state == ec2.NatGatewayStateFailed { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(output.FailureCode), aws.StringValue(output.FailureMessage))) + } + + return output, err + } + + return nil, err +} + +func WaitNATGatewayDeleted(conn *ec2.EC2, id string) (*ec2.NatGateway, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.NatGatewayStateDeleting}, + Target: []string{}, + Refresh: StatusNATGatewayState(conn, id), + Timeout: natGatewayDeletedTimeout, + Delay: 10 * time.Second, + MinTimeout: 10 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.NatGateway); ok { + if state := aws.StringValue(output.State); state == ec2.NatGatewayStateFailed { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(output.FailureCode), aws.StringValue(output.FailureMessage))) + } + + return output, err + } + + return nil, err +} + const ( vpnConnectionCreatedTimeout = 40 * time.Minute vpnConnectionDeletedTimeout = 30 * time.Minute @@ -968,6 +1136,10 @@ func WaitHostDeleted(conn *ec2.EC2, id string) (*ec2.Host, error) { return nil, err } +const ( + dhcpOptionSetDeletedTimeout = 3 * time.Minute +) + const ( internetGatewayAttachedTimeout = 4 * time.Minute internetGatewayDeletedTimeout = 10 * time.Minute