Skip to content

Commit

Permalink
Merge pull request #22253 from dirk39/resource/aws_default_vpc
Browse files Browse the repository at this point in the history
`aws_default_vpc` and `aws_default_subnet`: Full lifecycle
  • Loading branch information
ewbankkit committed Jan 21, 2022
2 parents 6623846 + d79cf7e commit ffeacc8
Show file tree
Hide file tree
Showing 26 changed files with 2,010 additions and 822 deletions.
11 changes: 11 additions & 0 deletions .changelog/22253.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
```release-note:note
resource/aws_default_subnet: If no default subnet exists in the specified Availability Zone one is now created. The `force_destroy` destroy argument has been added (defaults to `false`). Setting this argument to `true` deletes the default subnet on `terraform destroy`
```

```release-note:note
resource/aws_default_vpc: If no default VPC exists in the current AWS Region one is now created. The `force_destroy` destroy argument has been added (defaults to `false`). Setting this argument to `true` deletes the default VPC on `terraform destroy`
```

```release-note:note
data-source/aws_vpcs: The type of the `ids` attributes has changed from Set to List. If no VPCs match the specified criteria an empty list is returned (previously an error was raised)
```
29 changes: 29 additions & 0 deletions internal/acctest/acctest.go
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,15 @@ func PreCheckRegion(t *testing.T, region string) {
}
}

// PreCheckRegionNot checks that the test region is not one of the specified regions.
func PreCheckRegionNot(t *testing.T, regions ...string) {
for _, region := range regions {
if Region() == region {
t.Skipf("skipping tests; %s (%s) not supported", conns.EnvVarDefaultRegion, region)
}
}
}

// PreCheckPartition checks that the test partition is the specified partition.
func PreCheckPartition(partition string, t *testing.T) {
if Partition() != partition {
Expand Down Expand Up @@ -1844,3 +1853,23 @@ func CheckCallerIdentityAccountID(n string) resource.TestCheckFunc {
return nil
}
}

func CheckResourceAttrGreaterThanValue(n, key, value 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 v, ok := rs.Primary.Attributes[key]; !ok || !(v > value) {
if !ok {
return fmt.Errorf("%s: Attribute %q not found", n, key)
}

return fmt.Errorf("%s: Attribute %q is not greater than %q, got %q", n, key, value, v)
}

return nil

}
}
4 changes: 2 additions & 2 deletions internal/service/ec2/coip_pool_data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func TestAccEC2CoIPPoolDataSource_filter(t *testing.T) {
Check: resource.ComposeTestCheckFunc(
resource.TestMatchResourceAttr(dataSourceName, "local_gateway_route_table_id", regexp.MustCompile(`^lgw-rtb-`)),
resource.TestMatchResourceAttr(dataSourceName, "pool_id", regexp.MustCompile(`^ipv4pool-coip-`)),
testCheckResourceAttrGreaterThanValue(dataSourceName, "pool_cidrs.#", "0"),
acctest.CheckResourceAttrGreaterThanValue(dataSourceName, "pool_cidrs.#", "0"),
),
},
},
Expand All @@ -43,7 +43,7 @@ func TestAccEC2CoIPPoolDataSource_id(t *testing.T) {
resource.TestMatchResourceAttr(dataSourceName, "local_gateway_route_table_id", regexp.MustCompile(`^lgw-rtb-`)),
resource.TestMatchResourceAttr(dataSourceName, "pool_id", regexp.MustCompile(`^ipv4pool-coip-`)),
acctest.MatchResourceAttrRegionalARN(dataSourceName, "arn", "ec2", regexp.MustCompile(`coip-pool/ipv4pool-coip-.+$`)),
testCheckResourceAttrGreaterThanValue(dataSourceName, "pool_cidrs.#", "0"),
acctest.CheckResourceAttrGreaterThanValue(dataSourceName, "pool_cidrs.#", "0"),
),
},
},
Expand Down
2 changes: 1 addition & 1 deletion internal/service/ec2/coip_pools_data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TestAccEC2CoIPPoolsDataSource_basic(t *testing.T) {
{
Config: testAccCoIPPoolsDataSourceConfig(),
Check: resource.ComposeTestCheckFunc(
testCheckResourceAttrGreaterThanValue(dataSourceName, "pool_ids.#", "0"),
acctest.CheckResourceAttrGreaterThanValue(dataSourceName, "pool_ids.#", "0"),
),
},
},
Expand Down
277 changes: 220 additions & 57 deletions internal/service/ec2/default_subnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,84 +3,247 @@ 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/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 ResourceDefaultSubnet() *schema.Resource {
// reuse aws_subnet schema, and methods for READ, UPDATE
dsubnet := ResourceSubnet()
dsubnet.Create = resourceDefaultSubnetCreate
dsubnet.Delete = resourceDefaultSubnetDelete

// availability_zone is a required value for Default Subnets
dsubnet.Schema["availability_zone"] = &schema.Schema{
Type: schema.TypeString,
Required: true,
}
// availability_zone_id is a computed value for Default Subnets
dsubnet.Schema["availability_zone_id"] = &schema.Schema{
Type: schema.TypeString,
Computed: true,
}
// vpc_id is a computed value for Default Subnets
dsubnet.Schema["vpc_id"] = &schema.Schema{
Type: schema.TypeString,
Computed: true,
}
// cidr_block is a computed value for Default Subnets
dsubnet.Schema["cidr_block"] = &schema.Schema{
Type: schema.TypeString,
Computed: true,
}
// ipv6_cidr_block is a computed value for Default Subnets
dsubnet.Schema["ipv6_cidr_block"] = &schema.Schema{
Type: schema.TypeString,
Computed: true,
}
// map_public_ip_on_launch is a computed value for Default Subnets
dsubnet.Schema["map_public_ip_on_launch"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Computed: true,
}
// assign_ipv6_address_on_creation is a computed value for Default Subnets
dsubnet.Schema["assign_ipv6_address_on_creation"] = &schema.Schema{
Type: schema.TypeBool,
Computed: true,
}
//lintignore:R011
return &schema.Resource{
Create: resourceDefaultSubnetCreate,
Read: resourceSubnetRead,
Update: resourceSubnetUpdate,
Delete: resourceDefaultSubnetDelete,

Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

return dsubnet
CustomizeDiff: verify.SetTagsDiff,

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(10 * time.Minute),
Delete: schema.DefaultTimeout(20 * time.Minute),
},

SchemaVersion: 1,
MigrateState: SubnetMigrateState,

// Keep in sync with aws_subnet's schema with the following changes:
// - availability_zone is Required/ForceNew
// - availability_zone_id is Computed-only
// - cidr_block is Computed-only
// - ipv6_cidr_block is Optional/Computed as it's automatically assigned if ipv6_native = true
// - map_public_ip_on_launch has a Default of true
// - outpost_arn is Computed-only
// - vpc_id is Computed-only
// and additions:
// - existing_default_subnet Computed-only, set in resourceDefaultSubnetCreate
// - force_destroy Optional
Schema: map[string]*schema.Schema{
"arn": {
Type: schema.TypeString,
Computed: true,
},
"assign_ipv6_address_on_creation": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"availability_zone": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"availability_zone_id": {
Type: schema.TypeString,
Computed: true,
},
"cidr_block": {
Type: schema.TypeString,
Computed: true,
},
"customer_owned_ipv4_pool": {
Type: schema.TypeString,
Optional: true,
RequiredWith: []string{"map_customer_owned_ip_on_launch"},
},
"enable_dns64": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"enable_resource_name_dns_aaaa_record_on_launch": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"enable_resource_name_dns_a_record_on_launch": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"existing_default_subnet": {
Type: schema.TypeBool,
Computed: true,
},
"force_destroy": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"ipv6_cidr_block": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateFunc: verify.ValidIPv6CIDRNetworkAddress,
},
"ipv6_cidr_block_association_id": {
Type: schema.TypeString,
Computed: true,
},
"ipv6_native": {
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
Default: false,
},
"map_customer_owned_ip_on_launch": {
Type: schema.TypeBool,
Optional: true,
RequiredWith: []string{"customer_owned_ipv4_pool", "outpost_arn"},
},
"map_public_ip_on_launch": {
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"outpost_arn": {
Type: schema.TypeString,
Computed: true,
},
"owner_id": {
Type: schema.TypeString,
Computed: true,
},
"private_dns_hostname_type_on_launch": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateFunc: validation.StringInSlice(ec2.HostnameType_Values(), false),
},
"tags": tftags.TagsSchema(),
"tags_all": tftags.TagsSchemaComputed(),
"vpc_id": {
Type: schema.TypeString,
Computed: true,
},
},
}
}

func resourceDefaultSubnetCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*conns.AWSClient).EC2Conn

req := &ec2.DescribeSubnetsInput{}
req.Filters = BuildAttributeFilterList(
map[string]string{
"availabilityZone": d.Get("availability_zone").(string),
"defaultForAz": "true",
},
)
availabilityZone := d.Get("availability_zone").(string)
input := &ec2.DescribeSubnetsInput{
Filters: BuildAttributeFilterList(
map[string]string{
"availabilityZone": availabilityZone,
"defaultForAz": "true",
},
),
}

var computedIPv6CIDRBlock bool
subnet, err := FindSubnet(conn, input)

log.Printf("[DEBUG] Reading Default Subnet: %s", req)
resp, err := conn.DescribeSubnets(req)
if err != nil {
if err == nil {
log.Printf("[INFO] Found existing EC2 Default Subnet (%s)", availabilityZone)
d.SetId(aws.StringValue(subnet.SubnetId))
d.Set("existing_default_subnet", true)
} else if tfresource.NotFound(err) {
input := &ec2.CreateDefaultSubnetInput{
AvailabilityZone: aws.String(availabilityZone),
}

var ipv6Native bool
if v, ok := d.GetOk("ipv6_native"); ok {
ipv6Native = v.(bool)
input.Ipv6Native = aws.Bool(ipv6Native)
}

log.Printf("[DEBUG] Creating EC2 Default Subnet: %s", input)
output, err := conn.CreateDefaultSubnet(input)

if err != nil {
return fmt.Errorf("error creating EC2 Default Subnet (%s): %w", availabilityZone, err)
}

subnet = output.Subnet

d.SetId(aws.StringValue(subnet.SubnetId))
d.Set("existing_default_subnet", false)

subnet, err = WaitSubnetAvailable(conn, d.Id(), d.Timeout(schema.TimeoutCreate))

if err != nil {
return fmt.Errorf("error waiting for EC2 Default Subnet (%s) create: %w", d.Id(), err)
}

// Creating an IPv6-native default subnets associates an IPv6 CIDR block.
for _, v := range subnet.Ipv6CidrBlockAssociationSet {
if aws.StringValue(v.Ipv6CidrBlockState.State) == ec2.SubnetCidrBlockStateCodeAssociating { //we can only ever have 1 IPv6 block associated at once
associationID := aws.StringValue(v.AssociationId)

_, err = WaitSubnetIPv6CIDRBlockAssociationCreated(conn, associationID)

if err != nil {
return fmt.Errorf("error waiting for EC2 Default Subnet (%s) IPv6 CIDR block (%s) to become associated: %w", d.Id(), associationID, err)
}
}
}

if ipv6Native {
computedIPv6CIDRBlock = true
}
} else {
return fmt.Errorf("error reading EC2 Default Subnet (%s): %w", d.Id(), err)
}

if err := modifySubnetAttributesOnCreate(conn, d, subnet, computedIPv6CIDRBlock); err != nil {
return err
}
if len(resp.Subnets) != 1 || resp.Subnets[0] == nil {
return fmt.Errorf("Default subnet not found")

// Configure tags.
defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig
ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig
newTags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))).IgnoreConfig(ignoreTagsConfig)
oldTags := KeyValueTags(subnet.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig)

if !oldTags.Equal(newTags) {
if err := UpdateTags(conn, d.Id(), oldTags, newTags); err != nil {
return fmt.Errorf("error updating EC2 Default Subnet (%s) tags: %w", d.Id(), err)
}
}

d.SetId(aws.StringValue(resp.Subnets[0].SubnetId))
return resourceSubnetUpdate(d, meta)
return resourceSubnetRead(d, meta)
}

func resourceDefaultSubnetDelete(d *schema.ResourceData, meta interface{}) error {
log.Printf("[WARN] Cannot destroy Default Subnet. Terraform will remove this resource from the state file, however resources may remain.")
if d.Get("force_destroy").(bool) {
return resourceSubnetDelete(d, meta)
}

log.Printf("[WARN] EC2 Default Subnet (%s) not deleted, removing from state", d.Id())

return nil
}
Loading

0 comments on commit ffeacc8

Please sign in to comment.