Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

r/security_group: Add option to forcefully revoke rules before deletion #2074

Merged
merged 4 commits into from
Oct 30, 2017
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions aws/import_aws_security_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (

func TestAccAWSSecurityGroup_importBasic(t *testing.T) {
checkFn := func(s []*terraform.InstanceState) error {
// Expect 3: group, 2 rules
if len(s) != 3 {
return fmt.Errorf("expected 3 states: %#v", s)
// Expect 2: group, 2 rules
if len(s) != 2 {
return fmt.Errorf("expected 2 states: %#v", s)
}

return nil
Expand All @@ -28,9 +28,10 @@ func TestAccAWSSecurityGroup_importBasic(t *testing.T) {
},

{
ResourceName: "aws_security_group.web",
ImportState: true,
ImportStateCheck: checkFn,
ResourceName: "aws_security_group.web",
ImportState: true,
ImportStateCheck: checkFn,
ImportStateVerifyIgnore: []string{"revoke_rules_on_delete"},
},
},
})
Expand Down Expand Up @@ -75,9 +76,10 @@ func TestAccAWSSecurityGroup_importSelf(t *testing.T) {
},

{
ResourceName: "aws_security_group.allow_all",
ImportState: true,
ImportStateVerify: true,
ResourceName: "aws_security_group.allow_all",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"revoke_rules_on_delete"},
},
},
})
Expand All @@ -94,9 +96,10 @@ func TestAccAWSSecurityGroup_importSourceSecurityGroup(t *testing.T) {
},

{
ResourceName: "aws_security_group.test_group_1",
ImportState: true,
ImportStateVerify: true,
ResourceName: "aws_security_group.test_group_1",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"revoke_rules_on_delete"},
},
},
})
Expand Down
70 changes: 69 additions & 1 deletion aws/resource_aws_security_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ func resourceAwsSecurityGroup() *schema.Resource {
State: resourceAwsSecurityGroupImportState,
},

SchemaVersion: 1,
MigrateState: resourceAwsSecurityGroupMigrateState,

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

Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Expand Down Expand Up @@ -218,6 +225,12 @@ func resourceAwsSecurityGroup() *schema.Resource {
},

"tags": tagsSchema(),

"revoke_rules_on_delete": {
Type: schema.TypeBool,
Default: false,
Optional: true,
},
},
}
}
Expand Down Expand Up @@ -427,7 +440,16 @@ func resourceAwsSecurityGroupDelete(d *schema.ResourceData, meta interface{}) er
return fmt.Errorf("Failed to delete Lambda ENIs: %s", err)
}

return resource.Retry(5*time.Minute, func() *resource.RetryError {
// conditionally revoke rules first before attempting to delete the group
if v := d.Get("revoke_rules_on_delete").(bool); v {
if err := forceRevokeSecurityGroupRules(conn, d); err != nil {
return err
}
}

dTimeout := d.Timeout(schema.TimeoutDelete)

return resource.Retry(dTimeout, func() *resource.RetryError {
_, err := conn.DeleteSecurityGroup(&ec2.DeleteSecurityGroupInput{
GroupId: aws.String(d.Id()),
})
Expand All @@ -453,6 +475,52 @@ func resourceAwsSecurityGroupDelete(d *schema.ResourceData, meta interface{}) er
})
}

// Revoke all ingress/egress rules that a Security Group has
func forceRevokeSecurityGroupRules(conn *ec2.EC2, d *schema.ResourceData) error {
sgRaw, _, err := SGStateRefreshFunc(conn, d.Id())()
if err != nil {
return err
}
if sgRaw == nil {
return nil
}

group := sgRaw.(*ec2.SecurityGroup)
if len(group.IpPermissions) > 0 {
req := &ec2.RevokeSecurityGroupIngressInput{
GroupId: group.GroupId,
IpPermissions: group.IpPermissions,
}
if group.VpcId == nil || *group.VpcId == "" {
req.GroupId = nil
req.GroupName = group.GroupName
}
_, err = conn.RevokeSecurityGroupIngress(req)

if err != nil {
return fmt.Errorf(
"Error revoking security group %s rules: %s",
*group.GroupId, err)
}
}

if len(group.IpPermissionsEgress) > 0 {
req := &ec2.RevokeSecurityGroupEgressInput{
GroupId: group.GroupId,
IpPermissions: group.IpPermissionsEgress,
}
_, err = conn.RevokeSecurityGroupEgress(req)

if err != nil {
return fmt.Errorf(
"Error revoking security group %s rules: %s",
*group.GroupId, err)
}
}

return nil
}

func resourceAwsSecurityGroupRuleHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
Expand Down
34 changes: 34 additions & 0 deletions aws/resource_aws_security_group_migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package aws

import (
"fmt"
"log"

"github.com/hashicorp/terraform/terraform"
)

func resourceAwsSecurityGroupMigrateState(
v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) {
switch v {
case 0:
log.Println("[INFO] Found AWS SecurityGroup State v0; migrating to v1")
return migrateAwsSecurityGroupStateV0toV1(is)
default:
return is, fmt.Errorf("Unexpected schema version: %d", v)
}
}

func migrateAwsSecurityGroupStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) {
if is.Empty() || is.Attributes == nil {
log.Println("[DEBUG] Empty InstanceState; nothing to migrate.")
return is, nil
}

log.Printf("[DEBUG] Attributes before migration: %#v", is.Attributes)

// set default for revoke_rules_on_delete
is.Attributes["revoke_rules_on_delete"] = "false"

log.Printf("[DEBUG] Attributes after migration: %#v", is.Attributes)
return is, nil
}
71 changes: 71 additions & 0 deletions aws/resource_aws_security_group_migrate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package aws

import (
"testing"

"github.com/hashicorp/terraform/terraform"
)

func TestAWSSecurityGroupMigrateState(t *testing.T) {
cases := map[string]struct {
StateVersion int
Attributes map[string]string
Expected map[string]string
Meta interface{}
}{
"v0": {
StateVersion: 0,
Attributes: map[string]string{
"name": "test",
},
Expected: map[string]string{
"name": "test",
"revoke_rules_on_delete": "false",
},
},
}

for tn, tc := range cases {
is := &terraform.InstanceState{
ID: "i-abc123",
Attributes: tc.Attributes,
}
is, err := resourceAwsSecurityGroupMigrateState(
tc.StateVersion, is, tc.Meta)

if err != nil {
t.Fatalf("bad: %s, err: %#v", tn, err)
}

for k, v := range tc.Expected {
if is.Attributes[k] != v {
t.Fatalf(
"bad: %s\n\n expected: %#v -> %#v\n got: %#v -> %#v\n in: %#v",
tn, k, v, k, is.Attributes[k], is.Attributes)
}
}
}
}

func TestAWSSecurityGroupMigrateState_empty(t *testing.T) {
var is *terraform.InstanceState
var meta interface{}

// should handle nil
is, err := resourceAwsSecurityGroupMigrateState(0, is, meta)

if err != nil {
t.Fatalf("err: %#v", err)
}
if is != nil {
t.Fatalf("expected nil instancestate, got: %#v", is)
}

// should handle non-nil but empty
is = &terraform.InstanceState{}
is, err = resourceAwsSecurityGroupMigrateState(0, is, meta)

if err != nil {
t.Fatalf("err: %#v", err)
}
}
Loading