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