github.com/markdia/terraform@v0.5.1-0.20150508012022-f1ae920aa970/builtin/providers/aws/resource_aws_network_acl.go (about)

     1  package aws
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"log"
     7  	"strconv"
     8  	"time"
     9  
    10  	"github.com/awslabs/aws-sdk-go/aws"
    11  	"github.com/awslabs/aws-sdk-go/service/ec2"
    12  	"github.com/hashicorp/terraform/helper/hashcode"
    13  	"github.com/hashicorp/terraform/helper/resource"
    14  	"github.com/hashicorp/terraform/helper/schema"
    15  )
    16  
    17  func resourceAwsNetworkAcl() *schema.Resource {
    18  
    19  	return &schema.Resource{
    20  		Create: resourceAwsNetworkAclCreate,
    21  		Read:   resourceAwsNetworkAclRead,
    22  		Delete: resourceAwsNetworkAclDelete,
    23  		Update: resourceAwsNetworkAclUpdate,
    24  
    25  		Schema: map[string]*schema.Schema{
    26  			"vpc_id": &schema.Schema{
    27  				Type:     schema.TypeString,
    28  				Required: true,
    29  				ForceNew: true,
    30  				Computed: false,
    31  			},
    32  			"subnet_id": &schema.Schema{
    33  				Type:     schema.TypeString,
    34  				Optional: true,
    35  				ForceNew: true,
    36  				Computed: false,
    37  			},
    38  			"ingress": &schema.Schema{
    39  				Type:     schema.TypeSet,
    40  				Required: false,
    41  				Optional: true,
    42  				Elem: &schema.Resource{
    43  					Schema: map[string]*schema.Schema{
    44  						"from_port": &schema.Schema{
    45  							Type:     schema.TypeInt,
    46  							Required: true,
    47  						},
    48  						"to_port": &schema.Schema{
    49  							Type:     schema.TypeInt,
    50  							Required: true,
    51  						},
    52  						"rule_no": &schema.Schema{
    53  							Type:     schema.TypeInt,
    54  							Required: true,
    55  						},
    56  						"action": &schema.Schema{
    57  							Type:     schema.TypeString,
    58  							Required: true,
    59  						},
    60  						"protocol": &schema.Schema{
    61  							Type:     schema.TypeString,
    62  							Required: true,
    63  						},
    64  						"cidr_block": &schema.Schema{
    65  							Type:     schema.TypeString,
    66  							Optional: true,
    67  						},
    68  					},
    69  				},
    70  				Set: resourceAwsNetworkAclEntryHash,
    71  			},
    72  			"egress": &schema.Schema{
    73  				Type:     schema.TypeSet,
    74  				Required: false,
    75  				Optional: true,
    76  				Elem: &schema.Resource{
    77  					Schema: map[string]*schema.Schema{
    78  						"from_port": &schema.Schema{
    79  							Type:     schema.TypeInt,
    80  							Required: true,
    81  						},
    82  						"to_port": &schema.Schema{
    83  							Type:     schema.TypeInt,
    84  							Required: true,
    85  						},
    86  						"rule_no": &schema.Schema{
    87  							Type:     schema.TypeInt,
    88  							Required: true,
    89  						},
    90  						"action": &schema.Schema{
    91  							Type:     schema.TypeString,
    92  							Required: true,
    93  						},
    94  						"protocol": &schema.Schema{
    95  							Type:     schema.TypeString,
    96  							Required: true,
    97  						},
    98  						"cidr_block": &schema.Schema{
    99  							Type:     schema.TypeString,
   100  							Optional: true,
   101  						},
   102  					},
   103  				},
   104  				Set: resourceAwsNetworkAclEntryHash,
   105  			},
   106  			"tags": tagsSchema(),
   107  		},
   108  	}
   109  }
   110  
   111  func resourceAwsNetworkAclCreate(d *schema.ResourceData, meta interface{}) error {
   112  
   113  	conn := meta.(*AWSClient).ec2conn
   114  
   115  	// Create the Network Acl
   116  	createOpts := &ec2.CreateNetworkACLInput{
   117  		VPCID: aws.String(d.Get("vpc_id").(string)),
   118  	}
   119  
   120  	log.Printf("[DEBUG] Network Acl create config: %#v", createOpts)
   121  	resp, err := conn.CreateNetworkACL(createOpts)
   122  	if err != nil {
   123  		return fmt.Errorf("Error creating network acl: %s", err)
   124  	}
   125  
   126  	// Get the ID and store it
   127  	networkAcl := resp.NetworkACL
   128  	d.SetId(*networkAcl.NetworkACLID)
   129  	log.Printf("[INFO] Network Acl ID: %s", *networkAcl.NetworkACLID)
   130  
   131  	// Update rules and subnet association once acl is created
   132  	return resourceAwsNetworkAclUpdate(d, meta)
   133  }
   134  
   135  func resourceAwsNetworkAclRead(d *schema.ResourceData, meta interface{}) error {
   136  	conn := meta.(*AWSClient).ec2conn
   137  
   138  	resp, err := conn.DescribeNetworkACLs(&ec2.DescribeNetworkACLsInput{
   139  		NetworkACLIDs: []*string{aws.String(d.Id())},
   140  	})
   141  
   142  	if err != nil {
   143  		return err
   144  	}
   145  	if resp == nil {
   146  		return nil
   147  	}
   148  
   149  	networkAcl := resp.NetworkACLs[0]
   150  	var ingressEntries []*ec2.NetworkACLEntry
   151  	var egressEntries []*ec2.NetworkACLEntry
   152  
   153  	// separate the ingress and egress rules
   154  	for _, e := range networkAcl.Entries {
   155  		// Skip the default rules added by AWS. They can be neither
   156  		// configured or deleted by users.
   157  		if *e.RuleNumber == 32767 {
   158  			continue
   159  		}
   160  
   161  		if *e.Egress == true {
   162  			egressEntries = append(egressEntries, e)
   163  		} else {
   164  			ingressEntries = append(ingressEntries, e)
   165  		}
   166  	}
   167  
   168  	d.Set("vpc_id", networkAcl.VPCID)
   169  	d.Set("tags", tagsToMapSDK(networkAcl.Tags))
   170  
   171  	if err := d.Set("ingress", networkAclEntriesToMapList(ingressEntries)); err != nil {
   172  		return err
   173  	}
   174  	if err := d.Set("egress", networkAclEntriesToMapList(egressEntries)); err != nil {
   175  		return err
   176  	}
   177  
   178  	return nil
   179  }
   180  
   181  func resourceAwsNetworkAclUpdate(d *schema.ResourceData, meta interface{}) error {
   182  	conn := meta.(*AWSClient).ec2conn
   183  	d.Partial(true)
   184  
   185  	if d.HasChange("ingress") {
   186  		err := updateNetworkAclEntries(d, "ingress", conn)
   187  		if err != nil {
   188  			return err
   189  		}
   190  	}
   191  
   192  	if d.HasChange("egress") {
   193  		err := updateNetworkAclEntries(d, "egress", conn)
   194  		if err != nil {
   195  			return err
   196  		}
   197  	}
   198  
   199  	if d.HasChange("subnet_id") {
   200  		//associate new subnet with the acl.
   201  		_, n := d.GetChange("subnet_id")
   202  		newSubnet := n.(string)
   203  		association, err := findNetworkAclAssociation(newSubnet, conn)
   204  		if err != nil {
   205  			return fmt.Errorf("Failed to update acl %s with subnet %s: %s", d.Id(), newSubnet, err)
   206  		}
   207  		_, err = conn.ReplaceNetworkACLAssociation(&ec2.ReplaceNetworkACLAssociationInput{
   208  			AssociationID: association.NetworkACLAssociationID,
   209  			NetworkACLID:  aws.String(d.Id()),
   210  		})
   211  		if err != nil {
   212  			return err
   213  		}
   214  	}
   215  
   216  	if err := setTagsSDK(conn, d); err != nil {
   217  		return err
   218  	} else {
   219  		d.SetPartial("tags")
   220  	}
   221  
   222  	d.Partial(false)
   223  	return resourceAwsNetworkAclRead(d, meta)
   224  }
   225  
   226  func updateNetworkAclEntries(d *schema.ResourceData, entryType string, conn *ec2.EC2) error {
   227  
   228  	o, n := d.GetChange(entryType)
   229  
   230  	if o == nil {
   231  		o = new(schema.Set)
   232  	}
   233  	if n == nil {
   234  		n = new(schema.Set)
   235  	}
   236  
   237  	os := o.(*schema.Set)
   238  	ns := n.(*schema.Set)
   239  
   240  	toBeDeleted, err := expandNetworkAclEntries(os.Difference(ns).List(), entryType)
   241  	if err != nil {
   242  		return err
   243  	}
   244  	for _, remove := range toBeDeleted {
   245  
   246  		// AWS includes default rules with all network ACLs that can be
   247  		// neither modified nor destroyed. They have a custom rule
   248  		// number that is out of bounds for any other rule. If we
   249  		// encounter it, just continue. There's no work to be done.
   250  		if *remove.RuleNumber == 32767 {
   251  			continue
   252  		}
   253  
   254  		// Delete old Acl
   255  		_, err := conn.DeleteNetworkACLEntry(&ec2.DeleteNetworkACLEntryInput{
   256  			NetworkACLID: aws.String(d.Id()),
   257  			RuleNumber:   remove.RuleNumber,
   258  			Egress:       remove.Egress,
   259  		})
   260  		if err != nil {
   261  			return fmt.Errorf("Error deleting %s entry: %s", entryType, err)
   262  		}
   263  	}
   264  
   265  	toBeCreated, err := expandNetworkAclEntries(ns.Difference(os).List(), entryType)
   266  	if err != nil {
   267  		return err
   268  	}
   269  	for _, add := range toBeCreated {
   270  		// Protocol -1 rules don't store ports in AWS. Thus, they'll always
   271  		// hash differently when being read out of the API. Force the user
   272  		// to set from_port and to_port to 0 for these rules, to keep the
   273  		// hashing consistent.
   274  		if *add.Protocol == "-1" {
   275  			to := *add.PortRange.To
   276  			from := *add.PortRange.From
   277  			expected := &expectedPortPair{
   278  				to_port:   0,
   279  				from_port: 0,
   280  			}
   281  			if ok := validatePorts(to, from, *expected); !ok {
   282  				return fmt.Errorf(
   283  					"to_port (%d) and from_port (%d) must both be 0 to use the the 'all' \"-1\" protocol!",
   284  					to, from)
   285  			}
   286  		}
   287  
   288  		// AWS mutates the CIDR block into a network implied by the IP and
   289  		// mask provided. This results in hashing inconsistencies between
   290  		// the local config file and the state returned by the API. Error
   291  		// if the user provides a CIDR block with an inappropriate mask
   292  		if err := validateCIDRBlock(*add.CIDRBlock); err != nil {
   293  			return err
   294  		}
   295  
   296  		// Add new Acl entry
   297  		_, connErr := conn.CreateNetworkACLEntry(&ec2.CreateNetworkACLEntryInput{
   298  			NetworkACLID: aws.String(d.Id()),
   299  			CIDRBlock:    add.CIDRBlock,
   300  			Egress:       add.Egress,
   301  			PortRange:    add.PortRange,
   302  			Protocol:     add.Protocol,
   303  			RuleAction:   add.RuleAction,
   304  			RuleNumber:   add.RuleNumber,
   305  		})
   306  		if connErr != nil {
   307  			return fmt.Errorf("Error creating %s entry: %s", entryType, err)
   308  		}
   309  	}
   310  	return nil
   311  }
   312  
   313  func resourceAwsNetworkAclDelete(d *schema.ResourceData, meta interface{}) error {
   314  	conn := meta.(*AWSClient).ec2conn
   315  
   316  	log.Printf("[INFO] Deleting Network Acl: %s", d.Id())
   317  	return resource.Retry(5*time.Minute, func() error {
   318  		_, err := conn.DeleteNetworkACL(&ec2.DeleteNetworkACLInput{
   319  			NetworkACLID: aws.String(d.Id()),
   320  		})
   321  		if err != nil {
   322  			ec2err := err.(aws.APIError)
   323  			switch ec2err.Code {
   324  			case "InvalidNetworkAclID.NotFound":
   325  				return nil
   326  			case "DependencyViolation":
   327  				// In case of dependency violation, we remove the association between subnet and network acl.
   328  				// This means the subnet is attached to default acl of vpc.
   329  				association, err := findNetworkAclAssociation(d.Get("subnet_id").(string), conn)
   330  				if err != nil {
   331  					return resource.RetryError{Err: fmt.Errorf("Dependency violation: Cannot delete acl %s: %s", d.Id(), err)}
   332  				}
   333  				defaultAcl, err := getDefaultNetworkAcl(d.Get("vpc_id").(string), conn)
   334  				if err != nil {
   335  					return resource.RetryError{Err: fmt.Errorf("Dependency violation: Cannot delete acl %s: %s", d.Id(), err)}
   336  				}
   337  				_, err = conn.ReplaceNetworkACLAssociation(&ec2.ReplaceNetworkACLAssociationInput{
   338  					AssociationID: association.NetworkACLAssociationID,
   339  					NetworkACLID:  defaultAcl.NetworkACLID,
   340  				})
   341  				return resource.RetryError{Err: err}
   342  			default:
   343  				// Any other error, we want to quit the retry loop immediately
   344  				return resource.RetryError{Err: err}
   345  			}
   346  		}
   347  		log.Printf("[Info] Deleted network ACL %s successfully", d.Id())
   348  		return nil
   349  	})
   350  }
   351  
   352  func resourceAwsNetworkAclEntryHash(v interface{}) int {
   353  	var buf bytes.Buffer
   354  	m := v.(map[string]interface{})
   355  	buf.WriteString(fmt.Sprintf("%d-", m["from_port"].(int)))
   356  	buf.WriteString(fmt.Sprintf("%d-", m["to_port"].(int)))
   357  	buf.WriteString(fmt.Sprintf("%d-", m["rule_no"].(int)))
   358  	buf.WriteString(fmt.Sprintf("%s-", m["action"].(string)))
   359  
   360  	// The AWS network ACL API only speaks protocol numbers, and that's
   361  	// all we store. Never hash a protocol name.
   362  	protocol := m["protocol"].(string)
   363  	if _, err := strconv.Atoi(m["protocol"].(string)); err != nil {
   364  		// We're a protocol name. Look up the number.
   365  		buf.WriteString(fmt.Sprintf("%d-", protocolIntegers()[protocol]))
   366  	} else {
   367  		// We're a protocol number. Pass the value through.
   368  		buf.WriteString(fmt.Sprintf("%s-", protocol))
   369  	}
   370  
   371  	buf.WriteString(fmt.Sprintf("%s-", m["cidr_block"].(string)))
   372  
   373  	if v, ok := m["ssl_certificate_id"]; ok {
   374  		buf.WriteString(fmt.Sprintf("%s-", v.(string)))
   375  	}
   376  
   377  	return hashcode.String(buf.String())
   378  }
   379  
   380  func getDefaultNetworkAcl(vpc_id string, conn *ec2.EC2) (defaultAcl *ec2.NetworkACL, err error) {
   381  	resp, err := conn.DescribeNetworkACLs(&ec2.DescribeNetworkACLsInput{
   382  		Filters: []*ec2.Filter{
   383  			&ec2.Filter{
   384  				Name:   aws.String("default"),
   385  				Values: []*string{aws.String("true")},
   386  			},
   387  			&ec2.Filter{
   388  				Name:   aws.String("vpc-id"),
   389  				Values: []*string{aws.String(vpc_id)},
   390  			},
   391  		},
   392  	})
   393  
   394  	if err != nil {
   395  		return nil, err
   396  	}
   397  	return resp.NetworkACLs[0], nil
   398  }
   399  
   400  func findNetworkAclAssociation(subnetId string, conn *ec2.EC2) (networkAclAssociation *ec2.NetworkACLAssociation, err error) {
   401  	resp, err := conn.DescribeNetworkACLs(&ec2.DescribeNetworkACLsInput{
   402  		Filters: []*ec2.Filter{
   403  			&ec2.Filter{
   404  				Name:   aws.String("association.subnet-id"),
   405  				Values: []*string{aws.String(subnetId)},
   406  			},
   407  		},
   408  	})
   409  
   410  	if err != nil {
   411  		return nil, err
   412  	}
   413  	if resp.NetworkACLs != nil && len(resp.NetworkACLs) > 0 {
   414  		for _, association := range resp.NetworkACLs[0].Associations {
   415  			if *association.SubnetID == subnetId {
   416  				return association, nil
   417  			}
   418  		}
   419  	}
   420  	return nil, fmt.Errorf("could not find association for subnet %s ", subnetId)
   421  }
   422  
   423  // networkAclEntriesToMapList turns ingress/egress rules read from AWS into a list
   424  // of maps.
   425  func networkAclEntriesToMapList(networkAcls []*ec2.NetworkACLEntry) []map[string]interface{} {
   426  	result := make([]map[string]interface{}, 0, len(networkAcls))
   427  	for _, entry := range networkAcls {
   428  		acl := make(map[string]interface{})
   429  		acl["rule_no"] = *entry.RuleNumber
   430  		acl["action"] = *entry.RuleAction
   431  		acl["cidr_block"] = *entry.CIDRBlock
   432  
   433  		// The AWS network ACL API only speaks protocol numbers, and
   434  		// that's all we record.
   435  		if _, err := strconv.Atoi(*entry.Protocol); err != nil {
   436  			// We're a protocol name. Look up the number.
   437  			acl["protocol"] = protocolIntegers()[*entry.Protocol]
   438  		} else {
   439  			// We're a protocol number. Pass through.
   440  			acl["protocol"] = *entry.Protocol
   441  		}
   442  
   443  		acl["protocol"] = *entry.Protocol
   444  		if entry.PortRange != nil {
   445  			acl["from_port"] = *entry.PortRange.From
   446  			acl["to_port"] = *entry.PortRange.To
   447  		}
   448  
   449  		result = append(result, acl)
   450  	}
   451  
   452  	return result
   453  }