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