github.com/armen/terraform@v0.5.2-0.20150529052519-caa8117a08f1/builtin/providers/aws/resource_aws_network_acl.go (about)

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