opentofu/builtin/providers/aws/resource_aws_network_acl_rule.go
James Nugent 7620381a4f provider/aws: Wait for NACL rule to be visible
Fixes #4721. It seems there may be some eventual consistency in the API
for network ACLs. This fix doesn't use resource.WaitForState() as there
the NACL is not something that can be looked up by ID and has a
property which determines if it is present.

Instead we reuse the findNetworkAclRule function which the Read function
exhibiting the problem uses, and retry over a 3 minute period, returning
an error message informing the user that running `terraform apply` again
will likely allow them to continue.
2016-01-19 08:12:20 -05:00

248 lines
7.3 KiB
Go

package aws
import (
"bytes"
"fmt"
"log"
"strconv"
"time"
"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/resource"
"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)))
// It appears it might be a while until the newly created rule is visible via the
// API (see issue GH-4721). Retry the `findNetworkAclRule` function until it is
// visible (which in most cases is likely immediately).
err = resource.Retry(3*time.Minute, func() error {
_, findErr := findNetworkAclRule(d, meta)
if findErr != nil {
return findErr
}
return nil
})
if err != nil {
return fmt.Errorf("Created Network ACL Rule was not visible in API within 3 minute period. Running 'terraform apply' again will resume infrastructure creation.")
}
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()))
}