diff --git a/aws/import_aws_security_group_test.go b/aws/import_aws_security_group_test.go index a57313ae52d..88720ec1bbd 100644 --- a/aws/import_aws_security_group_test.go +++ b/aws/import_aws_security_group_test.go @@ -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 @@ -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"}, }, }, }) @@ -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"}, }, }, }) @@ -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"}, }, }, }) diff --git a/aws/resource_aws_security_group.go b/aws/resource_aws_security_group.go index 4752634816e..9c8426e39ca 100644 --- a/aws/resource_aws_security_group.go +++ b/aws/resource_aws_security_group.go @@ -27,6 +27,9 @@ func resourceAwsSecurityGroup() *schema.Resource { State: resourceAwsSecurityGroupImportState, }, + SchemaVersion: 1, + MigrateState: resourceAwsSecurityGroupMigrateState, + Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, @@ -218,6 +221,12 @@ func resourceAwsSecurityGroup() *schema.Resource { }, "tags": tagsSchema(), + + "revoke_rules_on_delete": { + Type: schema.TypeBool, + Default: false, + Optional: true, + }, }, } } @@ -427,6 +436,13 @@ func resourceAwsSecurityGroupDelete(d *schema.ResourceData, meta interface{}) er return fmt.Errorf("Failed to delete Lambda ENIs: %s", err) } + // 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 + } + } + return resource.Retry(5*time.Minute, func() *resource.RetryError { _, err := conn.DeleteSecurityGroup(&ec2.DeleteSecurityGroupInput{ GroupId: aws.String(d.Id()), @@ -453,6 +469,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{}) diff --git a/aws/resource_aws_security_group_migrate.go b/aws/resource_aws_security_group_migrate.go new file mode 100644 index 00000000000..88357447f3c --- /dev/null +++ b/aws/resource_aws_security_group_migrate.go @@ -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 +} diff --git a/aws/resource_aws_security_group_migrate_test.go b/aws/resource_aws_security_group_migrate_test.go new file mode 100644 index 00000000000..3b14edb0e5e --- /dev/null +++ b/aws/resource_aws_security_group_migrate_test.go @@ -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) + } +} diff --git a/aws/resource_aws_security_group_test.go b/aws/resource_aws_security_group_test.go index e251f7ddc4c..5de843f5812 100644 --- a/aws/resource_aws_security_group_test.go +++ b/aws/resource_aws_security_group_test.go @@ -2,6 +2,7 @@ package aws import ( "fmt" + "log" "reflect" "regexp" "strings" @@ -16,6 +17,80 @@ import ( "github.com/hashicorp/terraform/terraform" ) +// add sweeper to delete known test sgs +func init() { + resource.AddTestSweepers("aws_security_group", &resource.Sweeper{ + Name: "aws_security_group", + F: testSweepSecurityGroups, + }) +} + +func testSweepSecurityGroups(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + conn := client.(*AWSClient).ec2conn + + req := &ec2.DescribeSecurityGroupsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("tag-value"), + Values: []*string{aws.String("tf-acc-revoke*")}, + }, + }, + } + resp, err := conn.DescribeSecurityGroups(req) + + if len(resp.SecurityGroups) == 0 { + log.Print("[DEBUG] No aws security groups to sweep") + return nil + } + + for _, sg := range resp.SecurityGroups { + // revoke the rules + if sg.IpPermissions != nil { + req := &ec2.RevokeSecurityGroupIngressInput{ + GroupId: sg.GroupId, + IpPermissions: sg.IpPermissions, + } + + if _, err = conn.RevokeSecurityGroupIngress(req); err != nil { + return fmt.Errorf( + "Error revoking default egress rule for Security Group (%s): %s", + *sg.GroupId, err) + } + } + + if sg.IpPermissionsEgress != nil { + req := &ec2.RevokeSecurityGroupEgressInput{ + GroupId: sg.GroupId, + IpPermissions: sg.IpPermissionsEgress, + } + + if _, err = conn.RevokeSecurityGroupEgress(req); err != nil { + return fmt.Errorf( + "Error revoking default egress rule for Security Group (%s): %s", + *sg.GroupId, err) + } + } + } + + for _, sg := range resp.SecurityGroups { + // delete the group + _, err := conn.DeleteSecurityGroup(&ec2.DeleteSecurityGroupInput{ + GroupId: sg.GroupId, + }) + if err != nil { + return fmt.Errorf( + "Error deleting Security Group (%s): %s", + *sg.GroupId, err) + } + } + + return nil +} + func TestProtocolStateFunc(t *testing.T) { cases := []struct { input interface{} @@ -296,6 +371,240 @@ func TestAccAWSSecurityGroup_basic(t *testing.T) { }) } +// cycleIpPermForGroup returns an IpPermission struct with a configured +// UserIdGroupPair for the groupid given. Used in +// TestAccAWSSecurityGroup_forceRevokeRules_should_fail to create a cyclic rule +// between 2 security groups +func cycleIpPermForGroup(groupId string) *ec2.IpPermission { + var perm ec2.IpPermission + perm.FromPort = aws.Int64(0) + perm.ToPort = aws.Int64(0) + perm.IpProtocol = aws.String("icmp") + perm.UserIdGroupPairs = make([]*ec2.UserIdGroupPair, 1) + perm.UserIdGroupPairs[0] = &ec2.UserIdGroupPair{ + GroupId: aws.String(groupId), + } + return &perm +} + +// testAddRuleCycle returns a TestCheckFunc to use at the end of a test, such +// that a Security Group Rule cyclic dependency will be created between the two +// Security Groups. A companion function, testRemoveRuleCycle, will undo this. +func testAddRuleCycle(primary, secondary *ec2.SecurityGroup) resource.TestCheckFunc { + return func(s *terraform.State) error { + if primary.GroupId == nil { + return fmt.Errorf("Primary SG not set for TestAccAWSSecurityGroup_forceRevokeRules_should_fail") + } + if secondary.GroupId == nil { + return fmt.Errorf("Secondary SG not set for TestAccAWSSecurityGroup_forceRevokeRules_should_fail") + } + + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + // cycle from primary to secondary + perm1 := cycleIpPermForGroup(*secondary.GroupId) + // cycle from secondary to primary + perm2 := cycleIpPermForGroup(*primary.GroupId) + + req1 := &ec2.AuthorizeSecurityGroupEgressInput{ + GroupId: primary.GroupId, + IpPermissions: []*ec2.IpPermission{perm1}, + } + req2 := &ec2.AuthorizeSecurityGroupEgressInput{ + GroupId: secondary.GroupId, + IpPermissions: []*ec2.IpPermission{perm2}, + } + + var err error + _, err = conn.AuthorizeSecurityGroupEgress(req1) + if err != nil { + return fmt.Errorf( + "Error authorizing primary security group %s rules: %s", *primary.GroupId, + err) + } + _, err = conn.AuthorizeSecurityGroupEgress(req2) + if err != nil { + return fmt.Errorf( + "Error authorizing secondary security group %s rules: %s", *secondary.GroupId, + err) + } + return nil + } +} + +// testRemoveRuleCycle removes the cyclic dependency between two security groups +// that was added in testAddRuleCycle +func testRemoveRuleCycle(primary, secondary *ec2.SecurityGroup) resource.TestCheckFunc { + return func(s *terraform.State) error { + if primary.GroupId == nil { + return fmt.Errorf("Primary SG not set for TestAccAWSSecurityGroup_forceRevokeRules_should_fail") + } + if secondary.GroupId == nil { + return fmt.Errorf("Secondary SG not set for TestAccAWSSecurityGroup_forceRevokeRules_should_fail") + } + + conn := testAccProvider.Meta().(*AWSClient).ec2conn + for _, sg := range []*ec2.SecurityGroup{primary, secondary} { + var err error + if sg.IpPermissions != nil { + req := &ec2.RevokeSecurityGroupIngressInput{ + GroupId: sg.GroupId, + IpPermissions: sg.IpPermissions, + } + + if _, err = conn.RevokeSecurityGroupIngress(req); err != nil { + return fmt.Errorf( + "Error revoking default ingress rule for Security Group in testRemoveCycle (%s): %s", + *primary.GroupId, err) + } + } + + if sg.IpPermissionsEgress != nil { + req := &ec2.RevokeSecurityGroupEgressInput{ + GroupId: sg.GroupId, + IpPermissions: sg.IpPermissionsEgress, + } + + if _, err = conn.RevokeSecurityGroupEgress(req); err != nil { + return fmt.Errorf( + "Error revoking default egress rule for Security Group in testRemoveCycle (%s): %s", + *sg.GroupId, err) + } + } + } + return nil + } +} + +// This test should fail to destroy the Security Groups and VPC, due to a +// dependency cycle added outside of terraform's managment. There is a sweeper +// 'aws_vpc' and 'aws_security_group' that cleans these up, however, the test is +// written to allow Terraform to clean it up because we do go and revoke the +// cyclic rules that were added. +func TestAccAWSSecurityGroup_forceRevokeRules_true(t *testing.T) { + var primary ec2.SecurityGroup + var secondary ec2.SecurityGroup + + // Add rules to create a cycle between primary and secondary. This prevents + // Terraform/AWS from being able to destroy the groups + testAddCycle := testAddRuleCycle(&primary, &secondary) + // Remove the rules that created the cycle; Terraform/AWS can now destroy them + testRemoveCycle := testRemoveRuleCycle(&primary, &secondary) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSecurityGroupDestroy, + Steps: []resource.TestStep{ + // create the configuration with 2 security groups, then create a + // dependency cycle such that they cannot be deleted + { + Config: testAccAWSSecurityGroupConfig_revoke_base, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSecurityGroupExists("aws_security_group.primary", &primary), + testAccCheckAWSSecurityGroupExists("aws_security_group.secondary", &secondary), + testAddCycle, + ), + }, + // Verify the DependencyViolation error by using a configration with the + // groups removed. Terraform tries to destroy them but cannot. Expect a + // DependencyViolation error + { + Config: testAccAWSSecurityGroupConfig_revoke_base_removed, + ExpectError: regexp.MustCompile("DependencyViolation"), + }, + // Restore the config (a no-op plan) but also remove the dependencies + // between the groups with testRemoveCycle + { + Config: testAccAWSSecurityGroupConfig_revoke_base, + // ExpectError: regexp.MustCompile("DependencyViolation"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSecurityGroupExists("aws_security_group.primary", &primary), + testAccCheckAWSSecurityGroupExists("aws_security_group.secondary", &secondary), + testRemoveCycle, + ), + }, + // Again try to apply the config with the sgs removed; it should work + { + Config: testAccAWSSecurityGroupConfig_revoke_base_removed, + }, + //// + // now test with revoke_rules_on_delete + //// + // create the configuration with 2 security groups, then create a + // dependency cycle such that they cannot be deleted. In this + // configuration, each Security Group has `revoke_rules_on_delete` + // specified, and should delete with no issue + { + Config: testAccAWSSecurityGroupConfig_revoke_true, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSecurityGroupExists("aws_security_group.primary", &primary), + testAccCheckAWSSecurityGroupExists("aws_security_group.secondary", &secondary), + testAddCycle, + ), + }, + // Again try to apply the config with the sgs removed; it should work, + // because we've told the SGs to forecfully revoke their rules first + { + Config: testAccAWSSecurityGroupConfig_revoke_base_removed, + }, + }, + }) +} + +func TestAccAWSSecurityGroup_forceRevokeRules_false(t *testing.T) { + var primary ec2.SecurityGroup + var secondary ec2.SecurityGroup + + // Add rules to create a cycle between primary and secondary. This prevents + // Terraform/AWS from being able to destroy the groups + testAddCycle := testAddRuleCycle(&primary, &secondary) + // Remove the rules that created the cycle; Terraform/AWS can now destroy them + testRemoveCycle := testRemoveRuleCycle(&primary, &secondary) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSecurityGroupDestroy, + Steps: []resource.TestStep{ + // create the configuration with 2 security groups, then create a + // dependency cycle such that they cannot be deleted. These Security + // Groups are configured to explicitly not revoke rules on delete, + // `revoke_rules_on_delete = false` + { + Config: testAccAWSSecurityGroupConfig_revoke_false, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSecurityGroupExists("aws_security_group.primary", &primary), + testAccCheckAWSSecurityGroupExists("aws_security_group.secondary", &secondary), + testAddCycle, + ), + }, + // Verify the DependencyViolation error by using a configration with the + // groups removed, and the Groups not configured to revoke their ruls. + // Terraform tries to destroy them but cannot. Expect a + // DependencyViolation error + { + Config: testAccAWSSecurityGroupConfig_revoke_base_removed, + ExpectError: regexp.MustCompile("DependencyViolation"), + }, + // Restore the config (a no-op plan) but also remove the dependencies + // between the groups with testRemoveCycle + { + Config: testAccAWSSecurityGroupConfig_revoke_false, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSecurityGroupExists("aws_security_group.primary", &primary), + testAccCheckAWSSecurityGroupExists("aws_security_group.secondary", &secondary), + testRemoveCycle, + ), + }, + // Again try to apply the config with the sgs removed; it should work + { + Config: testAccAWSSecurityGroupConfig_revoke_base_removed, + }, + }, + }) +} + func TestAccAWSSecurityGroup_basicRuleDescription(t *testing.T) { var group ec2.SecurityGroup @@ -1361,6 +1670,9 @@ resource "aws_security_group" "web" { const testAccAWSSecurityGroupConfig = ` resource "aws_vpc" "foo" { cidr_block = "10.1.0.0/16" + tags { + Name = "tf-acc-revoke-test" + } } resource "aws_security_group" "web" { @@ -1375,17 +1687,113 @@ resource "aws_security_group" "web" { cidr_blocks = ["10.0.0.0/8"] } - egress { - protocol = "tcp" - from_port = 80 - to_port = 8000 - cidr_blocks = ["10.0.0.0/8"] - } + tags { + Name = "tf-acc-revoke-test" + } +} +` +const testAccAWSSecurityGroupConfig_revoke_base_removed = ` +resource "aws_vpc" "sg-race-revoke" { + cidr_block = "10.1.0.0/16" tags { - Name = "tf-acc-test" + Name = "tf-acc-revoke-test" + } +} +` +const testAccAWSSecurityGroupConfig_revoke_base = ` +resource "aws_vpc" "sg-race-revoke" { + cidr_block = "10.1.0.0/16" + tags { + Name = "tf-acc-revoke-test" + } +} + +resource "aws_security_group" "primary" { + name = "tf-acc-sg-race-revoke-primary" + description = "Used in the terraform acceptance tests" + vpc_id = "${aws_vpc.sg-race-revoke.id}" + + tags { + Name = "tf-acc-revoke-test-primary" } } + +resource "aws_security_group" "secondary" { + name = "tf-acc-sg-race-revoke-secondary" + description = "Used in the terraform acceptance tests" + vpc_id = "${aws_vpc.sg-race-revoke.id}" + + tags { + Name = "tf-acc-revoke-test-secondary" + } +} +` + +const testAccAWSSecurityGroupConfig_revoke_false = ` +resource "aws_vpc" "sg-race-revoke" { + cidr_block = "10.1.0.0/16" + tags { + Name = "tf-acc-revoke-test" + } +} + +resource "aws_security_group" "primary" { + name = "tf-acc-sg-race-revoke-primary" + description = "Used in the terraform acceptance tests" + vpc_id = "${aws_vpc.sg-race-revoke.id}" + + tags { + Name = "tf-acc-revoke-test-primary" + } + + revoke_rules_on_delete = false +} + +resource "aws_security_group" "secondary" { + name = "tf-acc-sg-race-revoke-secondary" + description = "Used in the terraform acceptance tests" + vpc_id = "${aws_vpc.sg-race-revoke.id}" + + tags { + Name = "tf-acc-revoke-test-secondary" + } + + revoke_rules_on_delete = false +} +` + +const testAccAWSSecurityGroupConfig_revoke_true = ` +resource "aws_vpc" "sg-race-revoke" { + cidr_block = "10.1.0.0/16" + tags { + Name = "tf-acc-revoke-test" + } +} + +resource "aws_security_group" "primary" { + name = "tf-acc-sg-race-revoke-primary" + description = "Used in the terraform acceptance tests" + vpc_id = "${aws_vpc.sg-race-revoke.id}" + + tags { + Name = "tf-acc-revoke-test-primary" + } + + revoke_rules_on_delete = true +} + +resource "aws_security_group" "secondary" { + name = "tf-acc-sg-race-revoke-secondary" + description = "Used in the terraform acceptance tests" + vpc_id = "${aws_vpc.sg-race-revoke.id}" + + tags { + Name = "tf-acc-revoke-test-secondary" + } + + revoke_rules_on_delete = true +} ` const testAccAWSSecurityGroupConfigChange = ` diff --git a/aws/resource_aws_vpc_test.go b/aws/resource_aws_vpc_test.go index 2aeafe512c4..bda4da3e820 100644 --- a/aws/resource_aws_vpc_test.go +++ b/aws/resource_aws_vpc_test.go @@ -2,6 +2,7 @@ package aws import ( "fmt" + "log" "testing" "github.com/aws/aws-sdk-go/aws" @@ -11,6 +12,55 @@ import ( "github.com/hashicorp/terraform/terraform" ) +// add sweeper to delete known test vpcs +func init() { + resource.AddTestSweepers("aws_vpc", &resource.Sweeper{ + Name: "aws_vpc", + Dependencies: []string{"aws_security_group"}, + F: testSweepVPCs, + }) +} + +func testSweepVPCs(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + conn := client.(*AWSClient).ec2conn + + req := &ec2.DescribeVpcsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("tag-value"), + Values: []*string{aws.String("tf-acc-revoke*")}, + }, + }, + } + resp, err := conn.DescribeVpcs(req) + if err != nil { + return fmt.Errorf("Error describing vpcs: %s", err) + } + + if len(resp.Vpcs) == 0 { + log.Print("[DEBUG] No aws vpcs to sweep") + return nil + } + + for _, vpc := range resp.Vpcs { + // delete the vpc + _, err := conn.DeleteVpc(&ec2.DeleteVpcInput{ + VpcId: vpc.VpcId, + }) + if err != nil { + return fmt.Errorf( + "Error deleting VPC (%s): %s", + *vpc.VpcId, err) + } + } + + return nil +} + func TestAccAWSVpc_basic(t *testing.T) { var vpc ec2.Vpc diff --git a/website/docs/r/emr_cluster.html.md b/website/docs/r/emr_cluster.html.md index 24073b0537d..7ecce71c052 100644 --- a/website/docs/r/emr_cluster.html.md +++ b/website/docs/r/emr_cluster.html.md @@ -104,7 +104,20 @@ Cannot specify the `cc1.4xlarge` instance type for nodes of a job flow launched * `service_access_security_group` - (Optional) Identifier of the Amazon EC2 service-access security group - required when the cluster runs on a private subnet * `instance_profile` - (Required) Instance Profile for EC2 instances of the cluster assume this role -~> **NOTE on EMR-Managed security groups:** These security groups will have any missing inbound or outbound access rules added and maintained by AWS, to ensure proper communication between instances in a cluster. Maintenance of the security group rules by AWS may lead Terraform to fail because of a race condition between Terraform and AWS in managing the rules. To avoid this, make the `emr_cluster` dependent on all the required security group rules. See [Amazon EMR-Managed Security Groups](http://docs.aws.amazon.com/emr/latest/ManagementGuide/emr-man-sec-groups.html) for more information about the EMR-managed security group rules. +~> **NOTE on EMR-Managed security groups:** These security groups will have any +missing inbound or outbound access rules added and maintained by AWS, to ensure +proper communication between instances in a cluster. The EMR service will +maintain these rules for groups provided in `emr_managed_master_security_group` +and `emr_managed_slave_security_group`; attempts to remove the required rules +may succeed, only for the EMR service to re-add them in a matter of minutes. +This may cause Terraform to fail to destroy an environment that contains an EMR +cluster, because the EMR service does not revoke rules added on deletion, +leaving a cyclic dependency between the security groups that prevents their +deletion. To avoid this, use the `revoke_rules_on_delete` optional attribute for +any Security Group used in `emr_managed_master_security_group` and +`emr_managed_slave_security_group`. See [Amazon EMR-Managed Security +Groups](http://docs.aws.amazon.com/emr/latest/ManagementGuide/emr-man-sec-groups.html) +for more information about the EMR-managed security group rules. ## instance\_group diff --git a/website/docs/r/security_group.html.markdown b/website/docs/r/security_group.html.markdown index 1a771f19f54..cfe7c21fd50 100644 --- a/website/docs/r/security_group.html.markdown +++ b/website/docs/r/security_group.html.markdown @@ -80,6 +80,13 @@ assign a random, unique name ingress rule. Each ingress block supports fields documented below. * `egress` - (Optional, VPC only) Can be specified multiple times for each egress rule. Each egress block supports fields documented below. +* `revoke_rules_on_delete` - (Optional) Instruct Terraform to revoke all of the +Security Groups attached ingress and egress rules before deleting the rule +itself. This is normally not needed, however certain AWS services such as +Elastic Map Reduce may automatically add required rules to security groups used +with the service, and those rules may contain a cyclic dependency that prevent +the security groups from being destroyed without removing the dependency first. +Default `false` * `vpc_id` - (Optional, Forces new resource) The VPC ID. * `tags` - (Optional) A mapping of tags to assign to the resource.