mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-14 01:13:59 -06:00
fcdcb4b916
* provider/aws: Default Network ACL resource Provides a resource to manage the default AWS Network ACL. VPC Only. * Remove subnet_id update, mark as computed value. Remove extra tag update * refactor default rule number to be a constant * refactor revokeRulesForType to be revokeAllNetworkACLEntries Refactor method to delete all network ACL entries, regardless of type. The previous implementation was under the assumption that we may only eliminate some rule types and possibly not others, so the split was necessary. We're now removing them all, so the logic isn't necessary Several doc and test cleanups are here as well * smite subnet_id, improve docs
596 lines
16 KiB
Go
596 lines
16 KiB
Go
package aws
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"log"
|
|
"sort"
|
|
"strconv"
|
|
"time"
|
|
|
|
"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/hashcode"
|
|
"github.com/hashicorp/terraform/helper/resource"
|
|
"github.com/hashicorp/terraform/helper/schema"
|
|
)
|
|
|
|
func resourceAwsNetworkAcl() *schema.Resource {
|
|
|
|
return &schema.Resource{
|
|
Create: resourceAwsNetworkAclCreate,
|
|
Read: resourceAwsNetworkAclRead,
|
|
Delete: resourceAwsNetworkAclDelete,
|
|
Update: resourceAwsNetworkAclUpdate,
|
|
|
|
Schema: map[string]*schema.Schema{
|
|
"vpc_id": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ForceNew: true,
|
|
Computed: false,
|
|
},
|
|
"subnet_id": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Computed: false,
|
|
Deprecated: "Attribute subnet_id is deprecated on network_acl resources. Use subnet_ids instead",
|
|
},
|
|
"subnet_ids": &schema.Schema{
|
|
Type: schema.TypeSet,
|
|
Optional: true,
|
|
Computed: true,
|
|
ConflictsWith: []string{"subnet_id"},
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
Set: schema.HashString,
|
|
},
|
|
"ingress": &schema.Schema{
|
|
Type: schema.TypeSet,
|
|
Required: false,
|
|
Optional: true,
|
|
Computed: 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,
|
|
},
|
|
"rule_no": &schema.Schema{
|
|
Type: schema.TypeInt,
|
|
Required: true,
|
|
},
|
|
"action": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
},
|
|
"protocol": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
},
|
|
"cidr_block": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"icmp_type": &schema.Schema{
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
},
|
|
"icmp_code": &schema.Schema{
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
Set: resourceAwsNetworkAclEntryHash,
|
|
},
|
|
"egress": &schema.Schema{
|
|
Type: schema.TypeSet,
|
|
Required: false,
|
|
Optional: true,
|
|
Computed: 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,
|
|
},
|
|
"rule_no": &schema.Schema{
|
|
Type: schema.TypeInt,
|
|
Required: true,
|
|
},
|
|
"action": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
},
|
|
"protocol": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
},
|
|
"cidr_block": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"icmp_type": &schema.Schema{
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
},
|
|
"icmp_code": &schema.Schema{
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
Set: resourceAwsNetworkAclEntryHash,
|
|
},
|
|
"tags": tagsSchema(),
|
|
},
|
|
}
|
|
}
|
|
|
|
func resourceAwsNetworkAclCreate(d *schema.ResourceData, meta interface{}) error {
|
|
|
|
conn := meta.(*AWSClient).ec2conn
|
|
|
|
// Create the Network Acl
|
|
createOpts := &ec2.CreateNetworkAclInput{
|
|
VpcId: aws.String(d.Get("vpc_id").(string)),
|
|
}
|
|
|
|
log.Printf("[DEBUG] Network Acl create config: %#v", createOpts)
|
|
resp, err := conn.CreateNetworkAcl(createOpts)
|
|
if err != nil {
|
|
return fmt.Errorf("Error creating network acl: %s", err)
|
|
}
|
|
|
|
// Get the ID and store it
|
|
networkAcl := resp.NetworkAcl
|
|
d.SetId(*networkAcl.NetworkAclId)
|
|
log.Printf("[INFO] Network Acl ID: %s", *networkAcl.NetworkAclId)
|
|
|
|
// Update rules and subnet association once acl is created
|
|
return resourceAwsNetworkAclUpdate(d, meta)
|
|
}
|
|
|
|
func resourceAwsNetworkAclRead(d *schema.ResourceData, meta interface{}) error {
|
|
conn := meta.(*AWSClient).ec2conn
|
|
|
|
resp, err := conn.DescribeNetworkAcls(&ec2.DescribeNetworkAclsInput{
|
|
NetworkAclIds: []*string{aws.String(d.Id())},
|
|
})
|
|
|
|
if err != nil {
|
|
if ec2err, ok := err.(awserr.Error); ok {
|
|
if ec2err.Code() == "InvalidNetworkAclID.NotFound" {
|
|
log.Printf("[DEBUG] Network ACL (%s) not found", d.Id())
|
|
d.SetId("")
|
|
return nil
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
if resp == nil {
|
|
return nil
|
|
}
|
|
|
|
networkAcl := resp.NetworkAcls[0]
|
|
var ingressEntries []*ec2.NetworkAclEntry
|
|
var egressEntries []*ec2.NetworkAclEntry
|
|
|
|
// separate the ingress and egress rules
|
|
for _, e := range networkAcl.Entries {
|
|
// Skip the default rules added by AWS. They can be neither
|
|
// configured or deleted by users.
|
|
if *e.RuleNumber == awsDefaultAclRuleNumber {
|
|
continue
|
|
}
|
|
|
|
if *e.Egress == true {
|
|
egressEntries = append(egressEntries, e)
|
|
} else {
|
|
ingressEntries = append(ingressEntries, e)
|
|
}
|
|
}
|
|
|
|
d.Set("vpc_id", networkAcl.VpcId)
|
|
d.Set("tags", tagsToMap(networkAcl.Tags))
|
|
|
|
var s []string
|
|
for _, a := range networkAcl.Associations {
|
|
s = append(s, *a.SubnetId)
|
|
}
|
|
sort.Strings(s)
|
|
if err := d.Set("subnet_ids", s); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := d.Set("ingress", networkAclEntriesToMapList(ingressEntries)); err != nil {
|
|
return err
|
|
}
|
|
if err := d.Set("egress", networkAclEntriesToMapList(egressEntries)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func resourceAwsNetworkAclUpdate(d *schema.ResourceData, meta interface{}) error {
|
|
conn := meta.(*AWSClient).ec2conn
|
|
d.Partial(true)
|
|
|
|
if d.HasChange("ingress") {
|
|
err := updateNetworkAclEntries(d, "ingress", conn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if d.HasChange("egress") {
|
|
err := updateNetworkAclEntries(d, "egress", conn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if d.HasChange("subnet_id") {
|
|
//associate new subnet with the acl.
|
|
_, n := d.GetChange("subnet_id")
|
|
newSubnet := n.(string)
|
|
association, err := findNetworkAclAssociation(newSubnet, conn)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to update acl %s with subnet %s: %s", d.Id(), newSubnet, err)
|
|
}
|
|
_, err = conn.ReplaceNetworkAclAssociation(&ec2.ReplaceNetworkAclAssociationInput{
|
|
AssociationId: association.NetworkAclAssociationId,
|
|
NetworkAclId: aws.String(d.Id()),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if d.HasChange("subnet_ids") {
|
|
o, n := d.GetChange("subnet_ids")
|
|
if o == nil {
|
|
o = new(schema.Set)
|
|
}
|
|
if n == nil {
|
|
n = new(schema.Set)
|
|
}
|
|
|
|
os := o.(*schema.Set)
|
|
ns := n.(*schema.Set)
|
|
|
|
remove := os.Difference(ns).List()
|
|
add := ns.Difference(os).List()
|
|
|
|
if len(remove) > 0 {
|
|
// A Network ACL is required for each subnet. In order to disassociate a
|
|
// subnet from this ACL, we must associate it with the default ACL.
|
|
defaultAcl, err := getDefaultNetworkAcl(d.Get("vpc_id").(string), conn)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to find Default ACL for VPC %s", d.Get("vpc_id").(string))
|
|
}
|
|
for _, r := range remove {
|
|
association, err := findNetworkAclAssociation(r.(string), conn)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to find acl association: acl %s with subnet %s: %s", d.Id(), r, err)
|
|
}
|
|
_, err = conn.ReplaceNetworkAclAssociation(&ec2.ReplaceNetworkAclAssociationInput{
|
|
AssociationId: association.NetworkAclAssociationId,
|
|
NetworkAclId: defaultAcl.NetworkAclId,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(add) > 0 {
|
|
for _, a := range add {
|
|
association, err := findNetworkAclAssociation(a.(string), conn)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to find acl association: acl %s with subnet %s: %s", d.Id(), a, err)
|
|
}
|
|
_, err = conn.ReplaceNetworkAclAssociation(&ec2.ReplaceNetworkAclAssociationInput{
|
|
AssociationId: association.NetworkAclAssociationId,
|
|
NetworkAclId: aws.String(d.Id()),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if err := setTags(conn, d); err != nil {
|
|
return err
|
|
} else {
|
|
d.SetPartial("tags")
|
|
}
|
|
|
|
d.Partial(false)
|
|
return resourceAwsNetworkAclRead(d, meta)
|
|
}
|
|
|
|
func updateNetworkAclEntries(d *schema.ResourceData, entryType string, conn *ec2.EC2) error {
|
|
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 == awsDefaultAclRuleNumber {
|
|
continue
|
|
}
|
|
|
|
// Delete old Acl
|
|
log.Printf("[DEBUG] Destroying Network ACL Entry number (%d)", int(*remove.RuleNumber))
|
|
_, 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
|
|
}
|
|
|
|
func resourceAwsNetworkAclDelete(d *schema.ResourceData, meta interface{}) error {
|
|
conn := meta.(*AWSClient).ec2conn
|
|
|
|
log.Printf("[INFO] Deleting Network Acl: %s", d.Id())
|
|
retryErr := resource.Retry(5*time.Minute, func() *resource.RetryError {
|
|
_, err := conn.DeleteNetworkAcl(&ec2.DeleteNetworkAclInput{
|
|
NetworkAclId: aws.String(d.Id()),
|
|
})
|
|
if err != nil {
|
|
ec2err := err.(awserr.Error)
|
|
switch ec2err.Code() {
|
|
case "InvalidNetworkAclID.NotFound":
|
|
return nil
|
|
case "DependencyViolation":
|
|
// In case of dependency violation, we remove the association between subnet and network acl.
|
|
// This means the subnet is attached to default acl of vpc.
|
|
var associations []*ec2.NetworkAclAssociation
|
|
if v, ok := d.GetOk("subnet_id"); ok {
|
|
|
|
a, err := findNetworkAclAssociation(v.(string), conn)
|
|
if err != nil {
|
|
return resource.NonRetryableError(err)
|
|
}
|
|
associations = append(associations, a)
|
|
} else if v, ok := d.GetOk("subnet_ids"); ok {
|
|
ids := v.(*schema.Set).List()
|
|
for _, i := range ids {
|
|
a, err := findNetworkAclAssociation(i.(string), conn)
|
|
if err != nil {
|
|
return resource.NonRetryableError(err)
|
|
}
|
|
associations = append(associations, a)
|
|
}
|
|
}
|
|
|
|
log.Printf("[DEBUG] Replacing network associations for Network ACL (%s): %s", d.Id(), associations)
|
|
defaultAcl, err := getDefaultNetworkAcl(d.Get("vpc_id").(string), conn)
|
|
if err != nil {
|
|
return resource.NonRetryableError(err)
|
|
}
|
|
|
|
for _, a := range associations {
|
|
_, replaceErr := conn.ReplaceNetworkAclAssociation(&ec2.ReplaceNetworkAclAssociationInput{
|
|
AssociationId: a.NetworkAclAssociationId,
|
|
NetworkAclId: defaultAcl.NetworkAclId,
|
|
})
|
|
if replaceErr != nil {
|
|
log.Printf("[ERR] Non retryable error in replacing associtions for Network ACL (%s): %s", d.Id(), replaceErr)
|
|
return resource.NonRetryableError(replaceErr)
|
|
}
|
|
}
|
|
return resource.RetryableError(fmt.Errorf("Dependencies found and cleaned up, retrying"))
|
|
default:
|
|
// Any other error, we want to quit the retry loop immediately
|
|
return resource.NonRetryableError(err)
|
|
}
|
|
}
|
|
log.Printf("[Info] Deleted network ACL %s successfully", d.Id())
|
|
return nil
|
|
})
|
|
|
|
if retryErr != nil {
|
|
return fmt.Errorf("[ERR] Error destroying Network ACL (%s): %s", d.Id(), retryErr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func resourceAwsNetworkAclEntryHash(v interface{}) int {
|
|
var buf bytes.Buffer
|
|
m := v.(map[string]interface{})
|
|
buf.WriteString(fmt.Sprintf("%d-", m["from_port"].(int)))
|
|
buf.WriteString(fmt.Sprintf("%d-", m["to_port"].(int)))
|
|
buf.WriteString(fmt.Sprintf("%d-", m["rule_no"].(int)))
|
|
buf.WriteString(fmt.Sprintf("%s-", m["action"].(string)))
|
|
|
|
// The AWS network ACL API only speaks protocol numbers, and that's
|
|
// all we store. Never hash a protocol name.
|
|
protocol := m["protocol"].(string)
|
|
if _, err := strconv.Atoi(m["protocol"].(string)); err != nil {
|
|
// We're a protocol name. Look up the number.
|
|
buf.WriteString(fmt.Sprintf("%d-", protocolIntegers()[protocol]))
|
|
} else {
|
|
// We're a protocol number. Pass the value through.
|
|
buf.WriteString(fmt.Sprintf("%s-", protocol))
|
|
}
|
|
|
|
buf.WriteString(fmt.Sprintf("%s-", m["cidr_block"].(string)))
|
|
|
|
if v, ok := m["ssl_certificate_id"]; ok {
|
|
buf.WriteString(fmt.Sprintf("%s-", v.(string)))
|
|
}
|
|
|
|
if v, ok := m["icmp_type"]; ok {
|
|
buf.WriteString(fmt.Sprintf("%d-", v.(int)))
|
|
}
|
|
if v, ok := m["icmp_code"]; ok {
|
|
buf.WriteString(fmt.Sprintf("%d-", v.(int)))
|
|
}
|
|
|
|
return hashcode.String(buf.String())
|
|
}
|
|
|
|
func getDefaultNetworkAcl(vpc_id string, conn *ec2.EC2) (defaultAcl *ec2.NetworkAcl, err error) {
|
|
resp, err := conn.DescribeNetworkAcls(&ec2.DescribeNetworkAclsInput{
|
|
Filters: []*ec2.Filter{
|
|
&ec2.Filter{
|
|
Name: aws.String("default"),
|
|
Values: []*string{aws.String("true")},
|
|
},
|
|
&ec2.Filter{
|
|
Name: aws.String("vpc-id"),
|
|
Values: []*string{aws.String(vpc_id)},
|
|
},
|
|
},
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.NetworkAcls[0], nil
|
|
}
|
|
|
|
func findNetworkAclAssociation(subnetId string, conn *ec2.EC2) (networkAclAssociation *ec2.NetworkAclAssociation, err error) {
|
|
resp, err := conn.DescribeNetworkAcls(&ec2.DescribeNetworkAclsInput{
|
|
Filters: []*ec2.Filter{
|
|
&ec2.Filter{
|
|
Name: aws.String("association.subnet-id"),
|
|
Values: []*string{aws.String(subnetId)},
|
|
},
|
|
},
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.NetworkAcls != nil && len(resp.NetworkAcls) > 0 {
|
|
for _, association := range resp.NetworkAcls[0].Associations {
|
|
if *association.SubnetId == subnetId {
|
|
return association, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("could not find association for subnet: %s ", subnetId)
|
|
}
|
|
|
|
// networkAclEntriesToMapList turns ingress/egress rules read from AWS into a list
|
|
// of maps.
|
|
func networkAclEntriesToMapList(networkAcls []*ec2.NetworkAclEntry) []map[string]interface{} {
|
|
result := make([]map[string]interface{}, 0, len(networkAcls))
|
|
for _, entry := range networkAcls {
|
|
acl := make(map[string]interface{})
|
|
acl["rule_no"] = *entry.RuleNumber
|
|
acl["action"] = *entry.RuleAction
|
|
acl["cidr_block"] = *entry.CidrBlock
|
|
|
|
// The AWS network ACL API only speaks protocol numbers, and
|
|
// that's all we record.
|
|
if _, err := strconv.Atoi(*entry.Protocol); err != nil {
|
|
// We're a protocol name. Look up the number.
|
|
acl["protocol"] = protocolIntegers()[*entry.Protocol]
|
|
} else {
|
|
// We're a protocol number. Pass through.
|
|
acl["protocol"] = *entry.Protocol
|
|
}
|
|
|
|
acl["protocol"] = *entry.Protocol
|
|
if entry.PortRange != nil {
|
|
acl["from_port"] = *entry.PortRange.From
|
|
acl["to_port"] = *entry.PortRange.To
|
|
}
|
|
|
|
if entry.IcmpTypeCode != nil {
|
|
acl["icmp_type"] = *entry.IcmpTypeCode.Type
|
|
acl["icmp_code"] = *entry.IcmpTypeCode.Code
|
|
}
|
|
|
|
result = append(result, acl)
|
|
}
|
|
|
|
return result
|
|
}
|