diff --git a/builtin/providers/aws/network_acl_entry.go b/builtin/providers/aws/network_acl_entry.go index 22b909bce..5a09746d6 100644 --- a/builtin/providers/aws/network_acl_entry.go +++ b/builtin/providers/aws/network_acl_entry.go @@ -69,6 +69,15 @@ func flattenNetworkAclEntries(list []*ec2.NetworkAclEntry) []map[string]interfac } +func protocolStrings(protocolIntegers map[string]int) map[int]string { + protocolStrings := make(map[int]string, len(protocolIntegers)) + for k, v := range protocolIntegers { + protocolStrings[v] = k + } + + return protocolStrings +} + func protocolIntegers() map[string]int { var protocolIntegers = make(map[string]int) protocolIntegers = map[string]int{ diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index c13934c56..6b0c8db2e 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -153,6 +153,7 @@ func Provider() terraform.ResourceProvider { "aws_main_route_table_association": resourceAwsMainRouteTableAssociation(), "aws_nat_gateway": resourceAwsNatGateway(), "aws_network_acl": resourceAwsNetworkAcl(), + "aws_network_acl_rule": resourceAwsNetworkAclRule(), "aws_network_interface": resourceAwsNetworkInterface(), "aws_opsworks_stack": resourceAwsOpsworksStack(), "aws_opsworks_java_app_layer": resourceAwsOpsworksJavaAppLayer(), diff --git a/builtin/providers/aws/resource_aws_network_acl.go b/builtin/providers/aws/resource_aws_network_acl.go index 20144f732..97916f9f0 100644 --- a/builtin/providers/aws/resource_aws_network_acl.go +++ b/builtin/providers/aws/resource_aws_network_acl.go @@ -50,6 +50,7 @@ func resourceAwsNetworkAcl() *schema.Resource { Type: schema.TypeSet, Required: false, Optional: true, + Computed: false, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "from_port": &schema.Schema{ @@ -92,6 +93,7 @@ func resourceAwsNetworkAcl() *schema.Resource { Type: schema.TypeSet, Required: false, Optional: true, + Computed: false, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "from_port": &schema.Schema{ @@ -316,87 +318,89 @@ func resourceAwsNetworkAclUpdate(d *schema.ResourceData, meta interface{}) error func updateNetworkAclEntries(d *schema.ResourceData, entryType string, conn *ec2.EC2) error { - o, n := d.GetChange(entryType) + if d.HasChange(entryType) { + o, n := d.GetChange(entryType) - if o == nil { - o = new(schema.Set) - } - if n == nil { - n = new(schema.Set) - } - - os := o.(*schema.Set) - ns := n.(*schema.Set) - - toBeDeleted, err := expandNetworkAclEntries(os.Difference(ns).List(), entryType) - if err != nil { - return err - } - for _, remove := range toBeDeleted { - - // AWS includes default rules with all network ACLs that can be - // neither modified nor destroyed. They have a custom rule - // number that is out of bounds for any other rule. If we - // encounter it, just continue. There's no work to be done. - if *remove.RuleNumber == 32767 { - continue + if o == nil { + o = new(schema.Set) + } + if n == nil { + n = new(schema.Set) } - // Delete old Acl - _, err := conn.DeleteNetworkAclEntry(&ec2.DeleteNetworkAclEntryInput{ - NetworkAclId: aws.String(d.Id()), - RuleNumber: remove.RuleNumber, - Egress: remove.Egress, - }) + os := o.(*schema.Set) + ns := n.(*schema.Set) + + toBeDeleted, err := expandNetworkAclEntries(os.Difference(ns).List(), entryType) if err != nil { - return fmt.Errorf("Error deleting %s entry: %s", entryType, err) - } - } - - toBeCreated, err := expandNetworkAclEntries(ns.Difference(os).List(), entryType) - if err != nil { - return err - } - for _, add := range toBeCreated { - // Protocol -1 rules don't store ports in AWS. Thus, they'll always - // hash differently when being read out of the API. Force the user - // to set from_port and to_port to 0 for these rules, to keep the - // hashing consistent. - if *add.Protocol == "-1" { - to := *add.PortRange.To - from := *add.PortRange.From - expected := &expectedPortPair{ - to_port: 0, - from_port: 0, - } - if ok := validatePorts(to, from, *expected); !ok { - return fmt.Errorf( - "to_port (%d) and from_port (%d) must both be 0 to use the the 'all' \"-1\" protocol!", - to, from) - } - } - - // AWS mutates the CIDR block into a network implied by the IP and - // mask provided. This results in hashing inconsistencies between - // the local config file and the state returned by the API. Error - // if the user provides a CIDR block with an inappropriate mask - if err := validateCIDRBlock(*add.CidrBlock); err != nil { return err } + for _, remove := range toBeDeleted { - // Add new Acl entry - _, connErr := conn.CreateNetworkAclEntry(&ec2.CreateNetworkAclEntryInput{ - NetworkAclId: aws.String(d.Id()), - CidrBlock: add.CidrBlock, - Egress: add.Egress, - PortRange: add.PortRange, - Protocol: add.Protocol, - RuleAction: add.RuleAction, - RuleNumber: add.RuleNumber, - IcmpTypeCode: add.IcmpTypeCode, - }) - if connErr != nil { - return fmt.Errorf("Error creating %s entry: %s", entryType, connErr) + // AWS includes default rules with all network ACLs that can be + // neither modified nor destroyed. They have a custom rule + // number that is out of bounds for any other rule. If we + // encounter it, just continue. There's no work to be done. + if *remove.RuleNumber == 32767 { + continue + } + + // Delete old Acl + _, err := conn.DeleteNetworkAclEntry(&ec2.DeleteNetworkAclEntryInput{ + NetworkAclId: aws.String(d.Id()), + RuleNumber: remove.RuleNumber, + Egress: remove.Egress, + }) + if err != nil { + return fmt.Errorf("Error deleting %s entry: %s", entryType, err) + } + } + + toBeCreated, err := expandNetworkAclEntries(ns.Difference(os).List(), entryType) + if err != nil { + return err + } + for _, add := range toBeCreated { + // Protocol -1 rules don't store ports in AWS. Thus, they'll always + // hash differently when being read out of the API. Force the user + // to set from_port and to_port to 0 for these rules, to keep the + // hashing consistent. + if *add.Protocol == "-1" { + to := *add.PortRange.To + from := *add.PortRange.From + expected := &expectedPortPair{ + to_port: 0, + from_port: 0, + } + if ok := validatePorts(to, from, *expected); !ok { + return fmt.Errorf( + "to_port (%d) and from_port (%d) must both be 0 to use the the 'all' \"-1\" protocol!", + to, from) + } + } + + // AWS mutates the CIDR block into a network implied by the IP and + // mask provided. This results in hashing inconsistencies between + // the local config file and the state returned by the API. Error + // if the user provides a CIDR block with an inappropriate mask + if err := validateCIDRBlock(*add.CidrBlock); err != nil { + return err + } + + // Add new Acl entry + _, connErr := conn.CreateNetworkAclEntry(&ec2.CreateNetworkAclEntryInput{ + NetworkAclId: aws.String(d.Id()), + CidrBlock: add.CidrBlock, + Egress: add.Egress, + PortRange: add.PortRange, + Protocol: add.Protocol, + RuleAction: add.RuleAction, + RuleNumber: add.RuleNumber, + IcmpTypeCode: add.IcmpTypeCode, + }) + if connErr != nil { + return fmt.Errorf("Error creating %s entry: %s", entryType, connErr) + } } } return nil diff --git a/builtin/providers/aws/resource_aws_network_acl_rule.go b/builtin/providers/aws/resource_aws_network_acl_rule.go new file mode 100644 index 000000000..ec6e15317 --- /dev/null +++ b/builtin/providers/aws/resource_aws_network_acl_rule.go @@ -0,0 +1,229 @@ +package aws + +import ( + "bytes" + "fmt" + "log" + "strconv" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsNetworkAclRule() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsNetworkAclRuleCreate, + Read: resourceAwsNetworkAclRuleRead, + Delete: resourceAwsNetworkAclRuleDelete, + + Schema: map[string]*schema.Schema{ + "network_acl_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "rule_number": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "egress": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + Default: false, + }, + "protocol": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "rule_action": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "cidr_block": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "from_port": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "to_port": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "icmp_type": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "icmp_code": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsNetworkAclRuleCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + protocol := d.Get("protocol").(string) + p, protocolErr := strconv.Atoi(protocol) + if protocolErr != nil { + var ok bool + p, ok = protocolIntegers()[protocol] + if !ok { + return fmt.Errorf("Invalid Protocol %s for rule %#v", protocol, d.Get("rule_number").(int)) + } + } + log.Printf("[INFO] Transformed Protocol %s into %d", protocol, p) + + params := &ec2.CreateNetworkAclEntryInput{ + NetworkAclId: aws.String(d.Get("network_acl_id").(string)), + Egress: aws.Bool(d.Get("egress").(bool)), + RuleNumber: aws.Int64(int64(d.Get("rule_number").(int))), + Protocol: aws.String(strconv.Itoa(p)), + CidrBlock: aws.String(d.Get("cidr_block").(string)), + RuleAction: aws.String(d.Get("rule_action").(string)), + PortRange: &ec2.PortRange{ + From: aws.Int64(int64(d.Get("from_port").(int))), + To: aws.Int64(int64(d.Get("to_port").(int))), + }, + } + + // Specify additional required fields for ICMP + if p == 1 { + params.IcmpTypeCode = &ec2.IcmpTypeCode{} + if v, ok := d.GetOk("icmp_code"); ok { + params.IcmpTypeCode.Code = aws.Int64(int64(v.(int))) + } + if v, ok := d.GetOk("icmp_type"); ok { + params.IcmpTypeCode.Type = aws.Int64(int64(v.(int))) + } + } + + log.Printf("[INFO] Creating Network Acl Rule: %d (%t)", d.Get("rule_number").(int), d.Get("egress").(bool)) + _, err := conn.CreateNetworkAclEntry(params) + if err != nil { + return fmt.Errorf("Error Creating Network Acl Rule: %s", err.Error()) + } + d.SetId(networkAclIdRuleNumberEgressHash(d.Get("network_acl_id").(string), d.Get("rule_number").(int), d.Get("egress").(bool), d.Get("protocol").(string))) + return resourceAwsNetworkAclRuleRead(d, meta) +} + +func resourceAwsNetworkAclRuleRead(d *schema.ResourceData, meta interface{}) error { + resp, err := findNetworkAclRule(d, meta) + if err != nil { + return err + } + + d.Set("rule_number", resp.RuleNumber) + d.Set("cidr_block", resp.CidrBlock) + d.Set("egress", resp.Egress) + if resp.IcmpTypeCode != nil { + d.Set("icmp_code", resp.IcmpTypeCode.Code) + d.Set("icmp_type", resp.IcmpTypeCode.Type) + } + if resp.PortRange != nil { + d.Set("from_port", resp.PortRange.From) + d.Set("to_port", resp.PortRange.To) + } + + d.Set("rule_action", resp.RuleAction) + + p, protocolErr := strconv.Atoi(*resp.Protocol) + log.Printf("[INFO] Converting the protocol %v", p) + if protocolErr == nil { + var ok bool + protocol, ok := protocolStrings(protocolIntegers())[p] + if !ok { + return fmt.Errorf("Invalid Protocol %s for rule %#v", *resp.Protocol, d.Get("rule_number").(int)) + } + log.Printf("[INFO] Transformed Protocol %s back into %s", *resp.Protocol, protocol) + d.Set("protocol", protocol) + } + + return nil +} + +func resourceAwsNetworkAclRuleDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + params := &ec2.DeleteNetworkAclEntryInput{ + NetworkAclId: aws.String(d.Get("network_acl_id").(string)), + RuleNumber: aws.Int64(int64(d.Get("rule_number").(int))), + Egress: aws.Bool(d.Get("egress").(bool)), + } + + log.Printf("[INFO] Deleting Network Acl Rule: %s", d.Id()) + _, err := conn.DeleteNetworkAclEntry(params) + if err != nil { + return fmt.Errorf("Error Deleting Network Acl Rule: %s", err.Error()) + } + + return nil +} + +func findNetworkAclRule(d *schema.ResourceData, meta interface{}) (*ec2.NetworkAclEntry, error) { + conn := meta.(*AWSClient).ec2conn + + filters := make([]*ec2.Filter, 0, 2) + ruleNumberFilter := &ec2.Filter{ + Name: aws.String("entry.rule-number"), + Values: []*string{aws.String(fmt.Sprintf("%v", d.Get("rule_number").(int)))}, + } + filters = append(filters, ruleNumberFilter) + egressFilter := &ec2.Filter{ + Name: aws.String("entry.egress"), + Values: []*string{aws.String(fmt.Sprintf("%v", d.Get("egress").(bool)))}, + } + filters = append(filters, egressFilter) + params := &ec2.DescribeNetworkAclsInput{ + NetworkAclIds: []*string{aws.String(d.Get("network_acl_id").(string))}, + Filters: filters, + } + + log.Printf("[INFO] Describing Network Acl: %s", d.Get("network_acl_id").(string)) + log.Printf("[INFO] Describing Network Acl with the Filters %#v", params) + resp, err := conn.DescribeNetworkAcls(params) + if err != nil { + return nil, fmt.Errorf("Error Finding Network Acl Rule %d: %s", d.Get("rule_number").(int), err.Error()) + } + + if resp == nil || len(resp.NetworkAcls) != 1 || resp.NetworkAcls[0] == nil { + return nil, fmt.Errorf( + "Expected to find one Network ACL, got: %#v", + resp.NetworkAcls) + } + networkAcl := resp.NetworkAcls[0] + if networkAcl.Entries != nil { + for _, i := range networkAcl.Entries { + if *i.RuleNumber == int64(d.Get("rule_number").(int)) && *i.Egress == d.Get("egress").(bool) { + return i, nil + } + } + } + return nil, fmt.Errorf( + "Expected the Network ACL to have Entries, got: %#v", + networkAcl) + +} + +func networkAclIdRuleNumberEgressHash(networkAclId string, ruleNumber int, egress bool, protocol string) string { + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf("%s-", networkAclId)) + buf.WriteString(fmt.Sprintf("%d-", ruleNumber)) + buf.WriteString(fmt.Sprintf("%t-", egress)) + buf.WriteString(fmt.Sprintf("%s-", protocol)) + return fmt.Sprintf("nacl-%d", hashcode.String(buf.String())) +} diff --git a/builtin/providers/aws/resource_aws_network_acl_rule_test.go b/builtin/providers/aws/resource_aws_network_acl_rule_test.go new file mode 100644 index 000000000..98767cb57 --- /dev/null +++ b/builtin/providers/aws/resource_aws_network_acl_rule_test.go @@ -0,0 +1,125 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSNetworkAclRule_basic(t *testing.T) { + var networkAcl ec2.NetworkAcl + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSNetworkAclRuleDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSNetworkAclRuleBasicConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSNetworkAclRuleExists("aws_network_acl_rule.bar", &networkAcl), + ), + }, + }, + }) +} + +func testAccCheckAWSNetworkAclRuleDestroy(s *terraform.State) error { + + for _, rs := range s.RootModule().Resources { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + if rs.Type != "aws_network_acl_rule" { + continue + } + + req := &ec2.DescribeNetworkAclsInput{ + NetworkAclIds: []*string{aws.String(rs.Primary.ID)}, + } + resp, err := conn.DescribeNetworkAcls(req) + if err == nil { + if len(resp.NetworkAcls) > 0 && *resp.NetworkAcls[0].NetworkAclId == rs.Primary.ID { + networkAcl := resp.NetworkAcls[0] + if networkAcl.Entries != nil { + return fmt.Errorf("Network ACL Entries still exist") + } + } + } + + ec2err, ok := err.(awserr.Error) + if !ok { + return err + } + if ec2err.Code() != "InvalidNetworkAclEntry.NotFound" { + return err + } + } + + return nil +} + +func testAccCheckAWSNetworkAclRuleExists(n string, networkAcl *ec2.NetworkAcl) resource.TestCheckFunc { + + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Network ACL Id is set") + } + + req := &ec2.DescribeNetworkAclsInput{ + NetworkAclIds: []*string{aws.String(rs.Primary.ID)}, + } + resp, err := conn.DescribeNetworkAcls(req) + if err == nil { + if len(resp.NetworkAcls) > 0 && *resp.NetworkAcls[0].NetworkAclId == rs.Primary.ID { + networkAcl := resp.NetworkAcls[0] + if networkAcl.Entries == nil { + return fmt.Errorf("No Network ACL Entries exist") + } + } + } + + ec2err, ok := err.(awserr.Error) + if !ok { + return err + } + if ec2err.Code() != "InvalidNetworkAclEntry.NotFound" { + return err + } + + return nil + } + + return nil +} + +const testAccAWSNetworkAclRuleBasicConfig = ` +provider "aws" { + region = "us-east-1" +} +resource "aws_vpc" "foo" { + cidr_block = "10.3.0.0/16" +} +resource "aws_network_acl" "bar" { + vpc_id = "${aws_vpc.foo.id}" +} +resource "aws_network_acl_rule" "bar" { + network_acl_id = "${aws_network_acl.bar.id}" + rule_number = 200 + egress = false + protocol = "tcp" + rule_action = "allow" + cidr_block = "0.0.0.0/0" + from_port = 22 + to_port = 22 +} +` diff --git a/website/source/docs/providers/aws/r/network_acl_rule.html.markdown b/website/source/docs/providers/aws/r/network_acl_rule.html.markdown new file mode 100644 index 000000000..e5766756f --- /dev/null +++ b/website/source/docs/providers/aws/r/network_acl_rule.html.markdown @@ -0,0 +1,53 @@ +--- +layout: "aws" +page_title: "AWS: aws_network_acl_rule" +sidebar_current: "docs-aws-resource-network-acl-rule" +description: |- + Provides an network ACL Rule resource. +--- + +# aws\_network\_acl\_rule + +Creates an entry (a rule) in a network ACL with the specified rule number. + +## Example Usage + +``` +resource "aws_network_acl" "bar" { + vpc_id = "${aws_vpc.foo.id}" +} +resource "aws_network_acl_rule" "bar" { + network_acl_id = "${aws_network_acl.bar.id}" + rule_number = 200 + egress = false + protocol = "tcp" + rule_action = "allow" + cidr_block = "0.0.0.0/0" + from_port = 22 + to_port = 22 +} +``` + +## Argument Reference + +The following arguments are supported: + +* `network_acl_id` - (Required) The ID of the network ACL. +* `rule_number` - (Required) The rule number for the entry (for example, 100). ACL entries are processed in ascending order by rule number. +* `egress` - (Optional, bool) Indicates whether this is an egress rule (rule is applied to traffic leaving the subnet). Default `false`. +* `protocol` - (Required) The protocol. A value of -1 means all protocols. +* `rule_action` - (Required) Indicates whether to allow or deny the traffic that matches the rule. Accepted values: `allow` | `deny` +* `cidr_block` - (Required) The network range to allow or deny, in CIDR notation (for example 172.16.0.0/24 ). +* `from_port` - (Optional) The from port to match. +* `to_port` - (Optional) The to port to match. +* `icmp_type` - (Optional) ICMP protocol: The ICMP type. Required if specifying ICMP for the protocol. e.g. -1 +* `icmp_code` - (Optional) ICMP protocol: The ICMP code. Required if specifying ICMP for the protocol. e.g. -1 + +~> Note: For more information on ICMP types and codes, see here: http://www.nthelp.com/icmp.html + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the network ACL Rule + diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 56feb4970..5a12b432e 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -538,6 +538,10 @@ aws_network_acl +