Skip to content

Commit

Permalink
service/ec2: Add COIP support to aws_subnet resource and data source (#…
Browse files Browse the repository at this point in the history
…16676)

* service/ec2: Add COIP support to aws_subnet resource and data source

Reference: #13170
Reference: #13171

Output from acceptance testing in OTL account:

```
--- PASS: TestAccDataSourceAwsSubnet_basic (25.67s)
--- PASS: TestAccDataSourceAwsSubnet_ipv6ByIpv6CidrBlock (46.37s)
--- PASS: TestAccDataSourceAwsSubnet_ipv6ByIpv6Filter (51.83s)

--- PASS: TestAccAWSSubnet_availabilityZoneId (29.12s)
--- PASS: TestAccAWSSubnet_basic (31.04s)
--- PASS: TestAccAWSSubnet_CustomerOwnedIpv4Pool (63.80s)
--- PASS: TestAccAWSSubnet_disappears (21.79s)
--- PASS: TestAccAWSSubnet_enableIpv6 (79.03s)
--- PASS: TestAccAWSSubnet_ignoreTags (45.48s)
--- PASS: TestAccAWSSubnet_ipv6 (90.26s)
--- PASS: TestAccAWSSubnet_MapCustomerOwnedIpOnLaunch (49.07s)
--- PASS: TestAccAWSSubnet_outpost (47.45s)
--- PASS: TestAccAWSSubnet_tags (68.74s)
```

* Update CHANGELOG for #16676

* Fix typo in CHANGELOG
  • Loading branch information
bflad authored Jan 29, 2021
1 parent 0739656 commit afa5e87
Show file tree
Hide file tree
Showing 11 changed files with 323 additions and 1 deletion.
7 changes: 7 additions & 0 deletions .changelog/16676.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:enhancement
data-source/aws_subnet: Add `customer_owned_ipv4_pool` and `map_customer_owned_ip_on_launch` attributes
```

```release-note:enhancement
resource/aws_subnet: Add `customer_owned_ipv4_pool` and `map_customer_owned_ip_on_launch` attributes
```
12 changes: 12 additions & 0 deletions aws/data_source_aws_subnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ func dataSourceAwsSubnet() *schema.Resource {
Computed: true,
},

"customer_owned_ipv4_pool": {
Type: schema.TypeString,
Computed: true,
},

"map_customer_owned_ip_on_launch": {
Type: schema.TypeBool,
Computed: true,
},

"map_public_ip_on_launch": {
Type: schema.TypeBool,
Computed: true,
Expand Down Expand Up @@ -180,6 +190,8 @@ func dataSourceAwsSubnetRead(d *schema.ResourceData, meta interface{}) error {
}

d.Set("assign_ipv6_address_on_creation", subnet.AssignIpv6AddressOnCreation)
d.Set("customer_owned_ipv4_pool", subnet.CustomerOwnedIpv4Pool)
d.Set("map_customer_owned_ip_on_launch", subnet.MapCustomerOwnedIpOnLaunch)
d.Set("map_public_ip_on_launch", subnet.MapPublicIpOnLaunch)

for _, a := range subnet.Ipv6CidrBlockAssociationSet {
Expand Down
12 changes: 12 additions & 0 deletions aws/data_source_aws_subnet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ func TestAccDataSourceAwsSubnet_basic(t *testing.T) {
resource.TestCheckResourceAttr(ds1ResourceName, "cidr_block", cidr),
resource.TestCheckResourceAttr(ds1ResourceName, "tags.Name", tag),
resource.TestCheckResourceAttrPair(ds1ResourceName, "arn", snResourceName, "arn"),
resource.TestCheckResourceAttrPair(ds1ResourceName, "customer_owned_ipv4_pool", snResourceName, "customer_owned_ipv4_pool"),
resource.TestCheckResourceAttrPair(ds1ResourceName, "map_customer_owned_ip_on_launch", snResourceName, "map_customer_owned_ip_on_launch"),
resource.TestCheckResourceAttrPair(ds1ResourceName, "outpost_arn", snResourceName, "outpost_arn"),

resource.TestCheckResourceAttrPair(ds2ResourceName, "id", snResourceName, "id"),
Expand All @@ -48,6 +50,8 @@ func TestAccDataSourceAwsSubnet_basic(t *testing.T) {
resource.TestCheckResourceAttr(ds2ResourceName, "cidr_block", cidr),
resource.TestCheckResourceAttr(ds2ResourceName, "tags.Name", tag),
resource.TestCheckResourceAttrPair(ds2ResourceName, "arn", snResourceName, "arn"),
resource.TestCheckResourceAttrPair(ds2ResourceName, "customer_owned_ipv4_pool", snResourceName, "customer_owned_ipv4_pool"),
resource.TestCheckResourceAttrPair(ds2ResourceName, "map_customer_owned_ip_on_launch", snResourceName, "map_customer_owned_ip_on_launch"),
resource.TestCheckResourceAttrPair(ds2ResourceName, "outpost_arn", snResourceName, "outpost_arn"),

resource.TestCheckResourceAttrPair(ds3ResourceName, "id", snResourceName, "id"),
Expand All @@ -58,6 +62,8 @@ func TestAccDataSourceAwsSubnet_basic(t *testing.T) {
resource.TestCheckResourceAttr(ds3ResourceName, "cidr_block", cidr),
resource.TestCheckResourceAttr(ds3ResourceName, "tags.Name", tag),
resource.TestCheckResourceAttrPair(ds3ResourceName, "arn", snResourceName, "arn"),
resource.TestCheckResourceAttrPair(ds3ResourceName, "customer_owned_ipv4_pool", snResourceName, "customer_owned_ipv4_pool"),
resource.TestCheckResourceAttrPair(ds3ResourceName, "map_customer_owned_ip_on_launch", snResourceName, "map_customer_owned_ip_on_launch"),
resource.TestCheckResourceAttrPair(ds3ResourceName, "outpost_arn", snResourceName, "outpost_arn"),

resource.TestCheckResourceAttrPair(ds4ResourceName, "id", snResourceName, "id"),
Expand All @@ -68,6 +74,8 @@ func TestAccDataSourceAwsSubnet_basic(t *testing.T) {
resource.TestCheckResourceAttr(ds4ResourceName, "cidr_block", cidr),
resource.TestCheckResourceAttr(ds4ResourceName, "tags.Name", tag),
resource.TestCheckResourceAttrPair(ds4ResourceName, "arn", snResourceName, "arn"),
resource.TestCheckResourceAttrPair(ds4ResourceName, "customer_owned_ipv4_pool", snResourceName, "customer_owned_ipv4_pool"),
resource.TestCheckResourceAttrPair(ds4ResourceName, "map_customer_owned_ip_on_launch", snResourceName, "map_customer_owned_ip_on_launch"),
resource.TestCheckResourceAttrPair(ds4ResourceName, "outpost_arn", snResourceName, "outpost_arn"),

resource.TestCheckResourceAttrPair(ds5ResourceName, "id", snResourceName, "id"),
Expand All @@ -78,6 +86,8 @@ func TestAccDataSourceAwsSubnet_basic(t *testing.T) {
resource.TestCheckResourceAttr(ds5ResourceName, "cidr_block", cidr),
resource.TestCheckResourceAttr(ds5ResourceName, "tags.Name", tag),
resource.TestCheckResourceAttrPair(ds5ResourceName, "arn", snResourceName, "arn"),
resource.TestCheckResourceAttrPair(ds5ResourceName, "customer_owned_ipv4_pool", snResourceName, "customer_owned_ipv4_pool"),
resource.TestCheckResourceAttrPair(ds5ResourceName, "map_customer_owned_ip_on_launch", snResourceName, "map_customer_owned_ip_on_launch"),
resource.TestCheckResourceAttrPair(ds5ResourceName, "outpost_arn", snResourceName, "outpost_arn"),

resource.TestCheckResourceAttrPair(ds6ResourceName, "id", snResourceName, "id"),
Expand All @@ -88,6 +98,8 @@ func TestAccDataSourceAwsSubnet_basic(t *testing.T) {
resource.TestCheckResourceAttr(ds6ResourceName, "cidr_block", cidr),
resource.TestCheckResourceAttr(ds6ResourceName, "tags.Name", tag),
resource.TestCheckResourceAttrPair(ds6ResourceName, "arn", snResourceName, "arn"),
resource.TestCheckResourceAttrPair(ds6ResourceName, "customer_owned_ipv4_pool", snResourceName, "customer_owned_ipv4_pool"),
resource.TestCheckResourceAttrPair(ds6ResourceName, "map_customer_owned_ip_on_launch", snResourceName, "map_customer_owned_ip_on_launch"),
resource.TestCheckResourceAttrPair(ds6ResourceName, "outpost_arn", snResourceName, "outpost_arn"),
),
},
Expand Down
4 changes: 4 additions & 0 deletions aws/internal/service/ec2/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ const (
InvalidGroupNotFound = "InvalidGroup.NotFound"
)

const (
ErrCodeInvalidSubnetIDNotFound = "InvalidSubnetID.NotFound"
)

const (
ErrCodeInvalidVpcPeeringConnectionIDNotFound = "InvalidVpcPeeringConnectionID.NotFound"
)
Expand Down
19 changes: 19 additions & 0 deletions aws/internal/service/ec2/finder/finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,25 @@ func SecurityGroupByID(conn *ec2.EC2, id string) (*ec2.SecurityGroup, error) {
return result.SecurityGroups[0], nil
}

// SubnetByID looks up a Subnet by ID. When not found, returns nil and potentially an API error.
func SubnetByID(conn *ec2.EC2, id string) (*ec2.Subnet, error) {
input := &ec2.DescribeSubnetsInput{
SubnetIds: aws.StringSlice([]string{id}),
}

output, err := conn.DescribeSubnets(input)

if err != nil {
return nil, err
}

if output == nil || len(output.Subnets) == 0 || output.Subnets[0] == nil {
return nil, nil
}

return output.Subnets[0], nil
}

// VpcPeeringConnectionByID returns the VPC peering connection corresponding to the specified identifier.
// Returns nil and potentially an error if no VPC peering connection is found.
func VpcPeeringConnectionByID(conn *ec2.EC2, id string) (*ec2.VpcPeeringConnection, error) {
Expand Down
22 changes: 22 additions & 0 deletions aws/internal/service/ec2/waiter/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package waiter
import (
"fmt"
"log"
"strconv"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
Expand Down Expand Up @@ -237,6 +238,27 @@ func SecurityGroupStatus(conn *ec2.EC2, id string) resource.StateRefreshFunc {
}
}

// SubnetMapCustomerOwnedIpOnLaunch fetches the Subnet and its MapCustomerOwnedIpOnLaunch
func SubnetMapCustomerOwnedIpOnLaunch(conn *ec2.EC2, id string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
subnet, err := finder.SubnetByID(conn, id)

if tfawserr.ErrCodeEquals(err, tfec2.ErrCodeInvalidSubnetIDNotFound) {
return nil, "false", nil
}

if err != nil {
return nil, "false", err
}

if subnet == nil {
return nil, "false", nil
}

return subnet, strconv.FormatBool(aws.BoolValue(subnet.MapCustomerOwnedIpOnLaunch)), nil
}
}

const (
vpcPeeringConnectionStatusNotFound = "NotFound"
vpcPeeringConnectionStatusUnknown = "Unknown"
Expand Down
23 changes: 23 additions & 0 deletions aws/internal/service/ec2/waiter/waiter.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package waiter

import (
"strconv"
"time"

"github.com/aws/aws-sdk-go/service/ec2"
Expand Down Expand Up @@ -249,6 +250,28 @@ func SecurityGroupCreated(conn *ec2.EC2, id string, timeout time.Duration) (*ec2
return nil, err
}

const (
SubnetAttributePropagationTimeout = 5 * time.Minute
)

func SubnetMapCustomerOwnedIpOnLaunchUpdated(conn *ec2.EC2, subnetID string, expectedValue bool) (*ec2.Subnet, error) {
stateConf := &resource.StateChangeConf{
Target: []string{strconv.FormatBool(expectedValue)},
Refresh: SubnetMapCustomerOwnedIpOnLaunch(conn, subnetID),
Timeout: SubnetAttributePropagationTimeout,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}

outputRaw, err := stateConf.WaitForState()

if output, ok := outputRaw.(*ec2.Subnet); ok {
return output, err
}

return nil, err
}

const (
VpnGatewayVpcAttachmentAttachedTimeout = 15 * time.Minute

Expand Down
61 changes: 60 additions & 1 deletion aws/resource_aws_subnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2/waiter"
)

func resourceAwsSubnet() *schema.Resource {
Expand Down Expand Up @@ -67,6 +68,18 @@ func resourceAwsSubnet() *schema.Resource {
ConflictsWith: []string{"availability_zone"},
},

"customer_owned_ipv4_pool": {
Type: schema.TypeString,
Optional: true,
RequiredWith: []string{"map_customer_owned_ip_on_launch", "outpost_arn"},
},

"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,
Expand Down Expand Up @@ -153,7 +166,8 @@ func resourceAwsSubnetCreate(d *schema.ResourceData, meta interface{}) error {
return fmt.Errorf("error waiting for subnet (%s) to become ready: %w", d.Id(), err)
}

// You cannot modify multiple subnet attributes in the same request.
// You cannot modify multiple subnet attributes in the same request,
// except CustomerOwnedIpv4Pool and MapCustomerOwnedIpOnLaunch.
// Reference: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_ModifySubnetAttribute.html

if d.Get("assign_ipv6_address_on_creation").(bool) {
Expand All @@ -169,6 +183,24 @@ func resourceAwsSubnetCreate(d *schema.ResourceData, meta interface{}) error {
}
}

if v, ok := d.GetOk("customer_owned_ipv4_pool"); ok {
input := &ec2.ModifySubnetAttributeInput{
CustomerOwnedIpv4Pool: aws.String(v.(string)),
MapCustomerOwnedIpOnLaunch: &ec2.AttributeBooleanValue{
Value: aws.Bool(d.Get("map_customer_owned_ip_on_launch").(bool)),
},
SubnetId: aws.String(d.Id()),
}

if _, err := conn.ModifySubnetAttribute(input); err != nil {
return fmt.Errorf("error setting EC2 Subnet (%s) customer owned IPv4 pool and map customer owned IP on launch: %w", d.Id(), err)
}

if _, err := waiter.SubnetMapCustomerOwnedIpOnLaunchUpdated(conn, d.Id(), d.Get("map_customer_owned_ip_on_launch").(bool)); err != nil {
return fmt.Errorf("error waiting for EC2 Subnet (%s) map customer owned IP on launch update: %w", d.Id(), err)
}
}

if d.Get("map_public_ip_on_launch").(bool) {
input := &ec2.ModifySubnetAttributeInput{
MapPublicIpOnLaunch: &ec2.AttributeBooleanValue{
Expand Down Expand Up @@ -211,6 +243,8 @@ func resourceAwsSubnetRead(d *schema.ResourceData, meta interface{}) error {
d.Set("availability_zone", subnet.AvailabilityZone)
d.Set("availability_zone_id", subnet.AvailabilityZoneId)
d.Set("cidr_block", subnet.CidrBlock)
d.Set("customer_owned_ipv4_pool", subnet.CustomerOwnedIpv4Pool)
d.Set("map_customer_owned_ip_on_launch", subnet.MapCustomerOwnedIpOnLaunch)
d.Set("map_public_ip_on_launch", subnet.MapPublicIpOnLaunch)
d.Set("assign_ipv6_address_on_creation", subnet.AssignIpv6AddressOnCreation)
d.Set("outpost_arn", subnet.OutpostArn)
Expand Down Expand Up @@ -249,6 +283,31 @@ func resourceAwsSubnetUpdate(d *schema.ResourceData, meta interface{}) error {
}
}

// You cannot modify multiple subnet attributes in the same request,
// except CustomerOwnedIpv4Pool and MapCustomerOwnedIpOnLaunch.
// Reference: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_ModifySubnetAttribute.html

if d.HasChanges("customer_owned_ipv4_pool", "map_customer_owned_ip_on_launch") {
input := &ec2.ModifySubnetAttributeInput{
MapCustomerOwnedIpOnLaunch: &ec2.AttributeBooleanValue{
Value: aws.Bool(d.Get("map_customer_owned_ip_on_launch").(bool)),
},
SubnetId: aws.String(d.Id()),
}

if v, ok := d.GetOk("customer_owned_ipv4_pool"); ok {
input.CustomerOwnedIpv4Pool = aws.String(v.(string))
}

if _, err := conn.ModifySubnetAttribute(input); err != nil {
return fmt.Errorf("error updating EC2 Subnet (%s) customer owned IPv4 pool and map customer owned IP on launch: %w", d.Id(), err)
}

if _, err := waiter.SubnetMapCustomerOwnedIpOnLaunchUpdated(conn, d.Id(), d.Get("map_customer_owned_ip_on_launch").(bool)); err != nil {
return fmt.Errorf("error waiting for EC2 Subnet (%s) map customer owned IP on launch update: %w", d.Id(), err)
}
}

if d.HasChange("map_public_ip_on_launch") {
modifyOpts := &ec2.ModifySubnetAttributeInput{
SubnetId: aws.String(d.Id()),
Expand Down
Loading

0 comments on commit afa5e87

Please sign in to comment.