Merge pull request #4286 from stack72/aws-network-acl-rule

provider/aws: aws_network_acl_rule
This commit is contained in:
James Nugent 2015-12-18 18:58:21 -05:00
commit 7c1811df36
7 changed files with 499 additions and 74 deletions

View File

@ -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{

View File

@ -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(),

View File

@ -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

View File

@ -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()))
}

View File

@ -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
}
`

View File

@ -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

View File

@ -538,6 +538,10 @@
<a href="/docs/providers/aws/r/network_acl.html">aws_network_acl</a>
</li>
<li<%= sidebar_current("docs-aws-resource-network-acl-rule") %>>
<a href="/docs/providers/aws/r/network_acl_rule.html">aws_network_acl_rule</a>
</li>
<li<%= sidebar_current("docs-aws-resource-network-interface") %>>
<a href="/docs/providers/aws/r/network_interface.html">aws_network_interface</a>
</li>