diff --git a/builtin/providers/aws/resource_aws_security_group.go b/builtin/providers/aws/resource_aws_security_group.go index a5196c890..67439cf2e 100644 --- a/builtin/providers/aws/resource_aws_security_group.go +++ b/builtin/providers/aws/resource_aws_security_group.go @@ -81,7 +81,52 @@ func resourceAwsSecurityGroup() *schema.Resource { }, }, }, - Set: resourceAwsSecurityGroupIngressHash, + Set: resourceAwsSecurityGroupRuleHash, + }, + + "egress": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "from_port": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + + "to_port": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + + "protocol": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "cidr_blocks": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "security_groups": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + + "self": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + }, + }, + Set: resourceAwsSecurityGroupRuleHash, }, "owner_id": &schema.Schema{ @@ -139,28 +184,14 @@ func resourceAwsSecurityGroupCreate(d *schema.ResourceData, meta interface{}) er return resourceAwsSecurityGroupUpdate(d, meta) } -func resourceAwsSecurityGroupRead(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn - - sgRaw, _, err := SGStateRefreshFunc(ec2conn, d.Id())() - if err != nil { - return err - } - if sgRaw == nil { - d.SetId("") - return nil - } - - sg := sgRaw.(*ec2.SecurityGroupInfo) - - // Gather our ingress rules - ingressMap := make(map[string]map[string]interface{}) - for _, perm := range sg.IPPerms { +func resourceAwsSecurityGroupIPPermGather(d *schema.ResourceData, permissions []ec2.IPPerm) []map[string]interface{} { + ruleMap := make(map[string]map[string]interface{}) + for _, perm := range permissions { k := fmt.Sprintf("%s-%d-%d", perm.Protocol, perm.FromPort, perm.ToPort) - m, ok := ingressMap[k] + m, ok := ruleMap[k] if !ok { m = make(map[string]interface{}) - ingressMap[k] = m + ruleMap[k] = m } m["from_port"] = perm.FromPort @@ -200,22 +231,15 @@ func resourceAwsSecurityGroupRead(d *schema.ResourceData, meta interface{}) erro m["security_groups"] = list } } - ingressRules := make([]map[string]interface{}, 0, len(ingressMap)) - for _, m := range ingressMap { - ingressRules = append(ingressRules, m) + rules := make([]map[string]interface{}, 0, len(ruleMap)) + for _, m := range ruleMap { + rules = append(rules, m) } - d.Set("description", sg.Description) - d.Set("name", sg.Name) - d.Set("vpc_id", sg.VpcId) - d.Set("owner_id", sg.OwnerId) - d.Set("ingress", ingressRules) - d.Set("tags", tagsToMap(sg.Tags)) - - return nil + return rules } -func resourceAwsSecurityGroupUpdate(d *schema.ResourceData, meta interface{}) error { +func resourceAwsSecurityGroupRead(d *schema.ResourceData, meta interface{}) error { ec2conn := meta.(*AWSClient).ec2conn sgRaw, _, err := SGStateRefreshFunc(ec2conn, d.Id())() @@ -226,10 +250,26 @@ func resourceAwsSecurityGroupUpdate(d *schema.ResourceData, meta interface{}) er d.SetId("") return nil } - group := sgRaw.(*ec2.SecurityGroupInfo).SecurityGroup - if d.HasChange("ingress") { - o, n := d.GetChange("ingress") + sg := sgRaw.(*ec2.SecurityGroupInfo) + + ingressRules := resourceAwsSecurityGroupIPPermGather(d, sg.IPPerms) + egressRules := resourceAwsSecurityGroupIPPermGather(d, sg.IPPermsEgress) + + d.Set("description", sg.Description) + d.Set("name", sg.Name) + d.Set("vpc_id", sg.VpcId) + d.Set("owner_id", sg.OwnerId) + d.Set("ingress", ingressRules) + d.Set("egress", egressRules) + d.Set("tags", tagsToMap(sg.Tags)) + + return nil +} + +func resourceAwsSecurityGroupUpdateRules(d *schema.ResourceData, ruleset string, meta interface{}, group ec2.SecurityGroup) error { + if d.HasChange(ruleset) { + o, n := d.GetChange(ruleset) if o == nil { o = new(schema.Set) } @@ -252,29 +292,70 @@ func resourceAwsSecurityGroupUpdate(d *schema.ResourceData, meta interface{}) er // adding is easier here, and Terraform should be fast enough to // not have service issues. - if len(remove) > 0 { - // Revoke the old rules - _, err = ec2conn.RevokeSecurityGroup(group, remove) - if err != nil { - return fmt.Errorf("Error authorizing security group ingress rules: %s", err) + if len(remove) > 0 || len(add) > 0 { + + ec2conn := meta.(*AWSClient).ec2conn + + if len(remove) > 0 { + // Revoke the old rules + revoke := ec2conn.RevokeSecurityGroup + if ruleset == "egress" { + revoke = ec2conn.RevokeSecurityGroupEgress + } + _, err := revoke(group, remove) + if err != nil { + return fmt.Errorf("Error revoking security group %s rules: %s", ruleset, err) + } + } + + if len(add) > 0 { + // Authorize the new rules + authorize := ec2conn.AuthorizeSecurityGroup + if ruleset == "egress" { + authorize = ec2conn.AuthorizeSecurityGroupEgress + } + _, err := authorize(group, add) + if err != nil { + return fmt.Errorf("Error authorizing security group %s rules: %s", ruleset, err) + } } } + } - if len(add) > 0 { - // Authorize the new rules - _, err := ec2conn.AuthorizeSecurityGroup(group, add) - if err != nil { - return fmt.Errorf("Error authorizing security group ingress rules: %s", err) - } + return nil +} + +func resourceAwsSecurityGroupUpdate(d *schema.ResourceData, meta interface{}) error { + ec2conn := meta.(*AWSClient).ec2conn + + sgRaw, _, err := SGStateRefreshFunc(ec2conn, d.Id())() + if err != nil { + return err + } + if sgRaw == nil { + d.SetId("") + return nil + } + group := sgRaw.(*ec2.SecurityGroupInfo).SecurityGroup + + err = resourceAwsSecurityGroupUpdateRules(d, "ingress", ec2conn, group) + if err != nil { + return err + } + + if d.Get("vpc_id") != nil { + err = resourceAwsSecurityGroupUpdateRules(d, "egress", ec2conn, group) + if err != nil { + return err } } if err := setTags(ec2conn, d); err != nil { return err - } else { - d.SetPartial("tags") } + d.SetPartial("tags") + return resourceAwsSecurityGroupRead(d, meta) } @@ -307,7 +388,7 @@ func resourceAwsSecurityGroupDelete(d *schema.ResourceData, meta interface{}) er }) } -func resourceAwsSecurityGroupIngressHash(v interface{}) int { +func resourceAwsSecurityGroupRuleHash(v interface{}) int { var buf bytes.Buffer m := v.(map[string]interface{}) buf.WriteString(fmt.Sprintf("%d-", m["from_port"].(int))) diff --git a/builtin/providers/aws/resource_aws_security_group_test.go b/builtin/providers/aws/resource_aws_security_group_test.go index 1b8773b51..d31f9754b 100644 --- a/builtin/providers/aws/resource_aws_security_group_test.go +++ b/builtin/providers/aws/resource_aws_security_group_test.go @@ -123,6 +123,16 @@ func TestAccAWSSecurityGroup_vpc(t *testing.T) { "aws_security_group.web", "ingress.332851786.cidr_blocks.#", "1"), resource.TestCheckResourceAttr( "aws_security_group.web", "ingress.332851786.cidr_blocks.0", "10.0.0.0/8"), + resource.TestCheckResourceAttr( + "aws_security_group.web", "egress.332851786.protocol", "tcp"), + resource.TestCheckResourceAttr( + "aws_security_group.web", "egress.332851786.from_port", "80"), + resource.TestCheckResourceAttr( + "aws_security_group.web", "egress.332851786.to_port", "8000"), + resource.TestCheckResourceAttr( + "aws_security_group.web", "egress.332851786.cidr_blocks.#", "1"), + resource.TestCheckResourceAttr( + "aws_security_group.web", "egress.332851786.cidr_blocks.0", "10.0.0.0/8"), testCheck, ), }, @@ -418,6 +428,13 @@ resource "aws_security_group" "web" { to_port = 8000 cidr_blocks = ["10.0.0.0/8"] } + + egress { + protocol = "tcp" + from_port = 80 + to_port = 8000 + cidr_blocks = ["10.0.0.0/8"] + } } ` diff --git a/website/source/docs/providers/aws/r/security_group.html.markdown b/website/source/docs/providers/aws/r/security_group.html.markdown index e408026e5..c8f76a121 100644 --- a/website/source/docs/providers/aws/r/security_group.html.markdown +++ b/website/source/docs/providers/aws/r/security_group.html.markdown @@ -25,6 +25,13 @@ resource "aws_security_group" "allow_all" { protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } + + egress { + from_port = 0 + to_port = 65535 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } } ``` @@ -56,6 +63,9 @@ The following arguments are supported: * `description` - (Required) The security group description. * `ingress` - (Optional) Can be specified multiple times for each ingress rule. Each ingress block supports fields documented below. +* `egress` - (Optional) Can be specified multiple times for each + egress rule. Each egress block supports fields documented below. + VPC only. * `vpc_id` - (Optional) The VPC ID. * `owner_id` - (Optional) The AWS Owner ID. @@ -70,6 +80,17 @@ The `ingress` block supports: * `to_port` - (Required) The end range port. * `tags` - (Optional) A mapping of tags to assign to the resource. +The `egress` block supports: + +* `cidr_blocks` - (Optional) List of CIDR blocks. Cannot be used with `security_groups`. +* `from_port` - (Required) The start port. +* `protocol` - (Required) The protocol. +* `security_groups` - (Optional) List of security group IDs. Cannot be used with `cidr_blocks`. +* `self` - (Optional) If true, the security group itself will be added as + a source to this egress rule. +* `to_port` - (Required) The end range port. +* `tags` - (Optional) A mapping of tags to assign to the resource. + ## Attributes Reference The following attributes are exported: @@ -80,3 +101,4 @@ The following attributes are exported: * `name` - The name of the security group * `description` - The description of the security group * `ingress` - The ingress rules. See above for more. +* `egress` - The egress rules. See above for more.