Skip to content

Commit

Permalink
provider/aws: Allow AWS Subnet to change IPv6 CIDR Block without Forc…
Browse files Browse the repository at this point in the history
…eNew

Fixes: #13588

It was pointed out in #13588 that we don't need to ForceNew on a change
of IPv6 CIDR block. The logic I decided to implement here was to
disassociate then associate. We should only be able to be associated to
1 IPv6 CIDR block at once. This feels like a risky move. We can
disassociate and then error on the associate. This would leave us in a
situation where we have no IPv6 CIDR block associated

The alternative here would be that the failure of association, triggers
a reassociation with the old IPv6 CIDR block
  • Loading branch information
stack72 committed Apr 24, 2017
1 parent 891e517 commit d510965
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 25 deletions.
102 changes: 102 additions & 0 deletions builtin/providers/aws/resource_aws_subnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,73 @@ func resourceAwsSubnetUpdate(d *schema.ResourceData, meta interface{}) error {
}
}

// We have to be careful here to not go through a change of association if this is a new resource
// A New resource here would denote that the Update func is called by the Create func
if d.HasChange("ipv6_cidr_block") && !d.IsNewResource() {
// We need to handle that we disassociate the IPv6 CIDR block before we try and associate the new one
// This could be an issue as, we could error out when we try and add the new one
// We may need to roll back the state and reattach the old one if this is the case

_, new := d.GetChange("ipv6_cidr_block")

//Firstly we have to disassociate the old IPv6 CIDR Block
disassociateOps := &ec2.DisassociateSubnetCidrBlockInput{
AssociationId: aws.String(d.Get("ipv6_cidr_block_association_id").(string)),
}

_, err := conn.DisassociateSubnetCidrBlock(disassociateOps)
if err != nil {
return err
}

// Wait for the CIDR to become disassociated
log.Printf(
"[DEBUG] Waiting for IPv6 CIDR (%s) to become disassociated",
d.Id())
stateConf := &resource.StateChangeConf{
Pending: []string{"disassociating", "associated"},
Target: []string{"disassociated"},
Refresh: SubnetIpv6CidrStateRefreshFunc(conn, d.Id(), d.Get("ipv6_cidr_block_association_id").(string)),
Timeout: 1 * time.Minute,
}
if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf(
"Error waiting for IPv6 CIDR (%s) to become disassociated: %s",
d.Id(), err)
}

//Now we need to try and associate the new CIDR block
associatesOpts := &ec2.AssociateSubnetCidrBlockInput{
SubnetId: aws.String(d.Id()),
Ipv6CidrBlock: aws.String(new.(string)),
}

resp, err := conn.AssociateSubnetCidrBlock(associatesOpts)
if err != nil {
//The big question here is, do we want to try and reassociate the old one??
//If we have a failure here, then we may be in a situation that we have nothing associated
return err
}

// Wait for the CIDR to become associated
log.Printf(
"[DEBUG] Waiting for IPv6 CIDR (%s) to become associated",
d.Id())
stateConf = &resource.StateChangeConf{
Pending: []string{"associating", "disassociated"},
Target: []string{"associated"},
Refresh: SubnetIpv6CidrStateRefreshFunc(conn, d.Id(), *resp.Ipv6CidrBlockAssociation.AssociationId),
Timeout: 1 * time.Minute,
}
if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf(
"Error waiting for IPv6 CIDR (%s) to become associated: %s",
d.Id(), err)
}

d.SetPartial("ipv6_cidr_block")
}

d.Partial(false)

return resourceAwsSubnetRead(d, meta)
Expand Down Expand Up @@ -276,3 +343,38 @@ func SubnetStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc
return subnet, *subnet.State, nil
}
}

func SubnetIpv6CidrStateRefreshFunc(conn *ec2.EC2, id string, associationId string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
opts := &ec2.DescribeSubnetsInput{
SubnetIds: []*string{aws.String(id)},
}
resp, err := conn.DescribeSubnets(opts)
if err != nil {
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSubnetID.NotFound" {
resp = nil
} else {
log.Printf("Error on SubnetIpv6CidrStateRefreshFunc: %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
}

if resp.Subnets[0].Ipv6CidrBlockAssociationSet == nil {
return nil, "", nil
}

for _, association := range resp.Subnets[0].Ipv6CidrBlockAssociationSet {
if *association.AssociationId == associationId {
return association, *association.Ipv6CidrBlockState.State, nil
}
}

return nil, "", nil
}
}
65 changes: 40 additions & 25 deletions builtin/providers/aws/resource_aws_subnet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,27 +45,7 @@ func TestAccAWSSubnet_basic(t *testing.T) {
}

func TestAccAWSSubnet_ipv6(t *testing.T) {
var v ec2.Subnet

testCheck := func(*terraform.State) error {
if v.Ipv6CidrBlockAssociationSet == nil {
return fmt.Errorf("Expected IPV6 CIDR Block Association")
}

if *v.AssignIpv6AddressOnCreation != true {
return fmt.Errorf("bad AssignIpv6AddressOnCreation: %t", *v.AssignIpv6AddressOnCreation)
}

return nil
}

testCheckUpdated := func(*terraform.State) error {
if *v.AssignIpv6AddressOnCreation != false {
return fmt.Errorf("bad AssignIpv6AddressOnCreation: %t", *v.AssignIpv6AddressOnCreation)
}

return nil
}
var before, after ec2.Subnet

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Expand All @@ -77,22 +57,57 @@ func TestAccAWSSubnet_ipv6(t *testing.T) {
Config: testAccSubnetConfigIpv6,
Check: resource.ComposeTestCheckFunc(
testAccCheckSubnetExists(
"aws_subnet.foo", &v),
testCheck,
"aws_subnet.foo", &before),
testAccCheckAwsSubnetIpv6BeforeUpdate(t, &before),
),
},
{
Config: testAccSubnetConfigIpv6Updated,
Check: resource.ComposeTestCheckFunc(
testAccCheckSubnetExists(
"aws_subnet.foo", &v),
testCheckUpdated,
"aws_subnet.foo", &after),
testAccCheckAwsSubnetIpv6AfterUpdate(t, &after),
testAccCheckAwsSubnetNotRecreated(t, &before, &after),
),
},
},
})
}

func testAccCheckAwsSubnetIpv6BeforeUpdate(t *testing.T, subnet *ec2.Subnet) resource.TestCheckFunc {
return func(s *terraform.State) error {
if subnet.Ipv6CidrBlockAssociationSet == nil {
return fmt.Errorf("Expected IPV6 CIDR Block Association")
}

if *subnet.AssignIpv6AddressOnCreation != true {
return fmt.Errorf("bad AssignIpv6AddressOnCreation: %t", *subnet.AssignIpv6AddressOnCreation)
}

return nil
}
}

func testAccCheckAwsSubnetIpv6AfterUpdate(t *testing.T, subnet *ec2.Subnet) resource.TestCheckFunc {
return func(s *terraform.State) error {
if *subnet.AssignIpv6AddressOnCreation != false {
return fmt.Errorf("bad AssignIpv6AddressOnCreation: %t", *subnet.AssignIpv6AddressOnCreation)
}

return nil
}
}

func testAccCheckAwsSubnetNotRecreated(t *testing.T,
before, after *ec2.Subnet) resource.TestCheckFunc {
return func(s *terraform.State) error {
if *before.SubnetId != *after.SubnetId {
t.Fatalf("Expected SubnetIDs not to change, but both got before: %s and after: %s", *before.SubnetId, *after.SubnetId)
}
return nil
}
}

func testAccCheckSubnetDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn

Expand Down

0 comments on commit d510965

Please sign in to comment.