github.com/i0n/terraform@v0.4.3-0.20150506151324-010a39a58ec1/builtin/providers/aws/resource_aws_security_group.go (about)

     1  package aws
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"log"
     7  	"sort"
     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 resourceAwsSecurityGroup() *schema.Resource {
    18  	return &schema.Resource{
    19  		Create: resourceAwsSecurityGroupCreate,
    20  		Read:   resourceAwsSecurityGroupRead,
    21  		Update: resourceAwsSecurityGroupUpdate,
    22  		Delete: resourceAwsSecurityGroupDelete,
    23  
    24  		Schema: map[string]*schema.Schema{
    25  			"name": &schema.Schema{
    26  				Type:     schema.TypeString,
    27  				Optional: true,
    28  				Computed: true,
    29  				ForceNew: true,
    30  			},
    31  
    32  			"description": &schema.Schema{
    33  				Type:     schema.TypeString,
    34  				Optional: true,
    35  				Default:  "Managed by Terraform",
    36  			},
    37  
    38  			"vpc_id": &schema.Schema{
    39  				Type:     schema.TypeString,
    40  				Optional: true,
    41  				ForceNew: true,
    42  				Computed: true,
    43  			},
    44  
    45  			"ingress": &schema.Schema{
    46  				Type:     schema.TypeSet,
    47  				Optional: true,
    48  				Computed: true,
    49  				Elem: &schema.Resource{
    50  					Schema: map[string]*schema.Schema{
    51  						"from_port": &schema.Schema{
    52  							Type:     schema.TypeInt,
    53  							Required: true,
    54  						},
    55  
    56  						"to_port": &schema.Schema{
    57  							Type:     schema.TypeInt,
    58  							Required: true,
    59  						},
    60  
    61  						"protocol": &schema.Schema{
    62  							Type:     schema.TypeString,
    63  							Required: true,
    64  						},
    65  
    66  						"cidr_blocks": &schema.Schema{
    67  							Type:     schema.TypeList,
    68  							Optional: true,
    69  							Elem:     &schema.Schema{Type: schema.TypeString},
    70  						},
    71  
    72  						"security_groups": &schema.Schema{
    73  							Type:     schema.TypeSet,
    74  							Optional: true,
    75  							Elem:     &schema.Schema{Type: schema.TypeString},
    76  							Set: func(v interface{}) int {
    77  								return hashcode.String(v.(string))
    78  							},
    79  						},
    80  
    81  						"self": &schema.Schema{
    82  							Type:     schema.TypeBool,
    83  							Optional: true,
    84  							Default:  false,
    85  						},
    86  					},
    87  				},
    88  				Set: resourceAwsSecurityGroupRuleHash,
    89  			},
    90  
    91  			"egress": &schema.Schema{
    92  				Type:     schema.TypeSet,
    93  				Optional: true,
    94  				Computed: 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  
   102  						"to_port": &schema.Schema{
   103  							Type:     schema.TypeInt,
   104  							Required: true,
   105  						},
   106  
   107  						"protocol": &schema.Schema{
   108  							Type:     schema.TypeString,
   109  							Required: true,
   110  						},
   111  
   112  						"cidr_blocks": &schema.Schema{
   113  							Type:     schema.TypeList,
   114  							Optional: true,
   115  							Elem:     &schema.Schema{Type: schema.TypeString},
   116  						},
   117  
   118  						"security_groups": &schema.Schema{
   119  							Type:     schema.TypeSet,
   120  							Optional: true,
   121  							Elem:     &schema.Schema{Type: schema.TypeString},
   122  							Set: func(v interface{}) int {
   123  								return hashcode.String(v.(string))
   124  							},
   125  						},
   126  
   127  						"self": &schema.Schema{
   128  							Type:     schema.TypeBool,
   129  							Optional: true,
   130  							Default:  false,
   131  						},
   132  					},
   133  				},
   134  				Set: resourceAwsSecurityGroupRuleHash,
   135  			},
   136  
   137  			"owner_id": &schema.Schema{
   138  				Type:     schema.TypeString,
   139  				Computed: true,
   140  			},
   141  
   142  			"tags": tagsSchema(),
   143  		},
   144  	}
   145  }
   146  
   147  func resourceAwsSecurityGroupCreate(d *schema.ResourceData, meta interface{}) error {
   148  	conn := meta.(*AWSClient).ec2conn
   149  
   150  	securityGroupOpts := &ec2.CreateSecurityGroupInput{}
   151  
   152  	if v := d.Get("vpc_id"); v != nil {
   153  		securityGroupOpts.VPCID = aws.String(v.(string))
   154  	}
   155  
   156  	if v := d.Get("description"); v != nil {
   157  		securityGroupOpts.Description = aws.String(v.(string))
   158  	}
   159  
   160  	var groupName string
   161  	if v, ok := d.GetOk("name"); ok {
   162  		groupName = v.(string)
   163  	} else {
   164  		groupName = resource.UniqueId()
   165  	}
   166  	securityGroupOpts.GroupName = aws.String(groupName)
   167  
   168  	log.Printf(
   169  		"[DEBUG] Security Group create configuration: %#v", securityGroupOpts)
   170  	createResp, err := conn.CreateSecurityGroup(securityGroupOpts)
   171  	if err != nil {
   172  		return fmt.Errorf("Error creating Security Group: %s", err)
   173  	}
   174  
   175  	d.SetId(*createResp.GroupID)
   176  
   177  	log.Printf("[INFO] Security Group ID: %s", d.Id())
   178  
   179  	// Wait for the security group to truly exist
   180  	log.Printf(
   181  		"[DEBUG] Waiting for Security Group (%s) to exist",
   182  		d.Id())
   183  	stateConf := &resource.StateChangeConf{
   184  		Pending: []string{""},
   185  		Target:  "exists",
   186  		Refresh: SGStateRefreshFunc(conn, d.Id()),
   187  		Timeout: 1 * time.Minute,
   188  	}
   189  
   190  	resp, err := stateConf.WaitForState()
   191  	if err != nil {
   192  		return fmt.Errorf(
   193  			"Error waiting for Security Group (%s) to become available: %s",
   194  			d.Id(), err)
   195  	}
   196  
   197  	// AWS defaults all Security Groups to have an ALLOW ALL egress rule. Here we
   198  	// revoke that rule, so users don't unknowningly have/use it.
   199  	group := resp.(*ec2.SecurityGroup)
   200  	if group.VPCID != nil && *group.VPCID != "" {
   201  		log.Printf("[DEBUG] Revoking default egress rule for Security Group for %s", d.Id())
   202  
   203  		req := &ec2.RevokeSecurityGroupEgressInput{
   204  			GroupID: createResp.GroupID,
   205  			IPPermissions: []*ec2.IPPermission{
   206  				&ec2.IPPermission{
   207  					FromPort: aws.Long(int64(0)),
   208  					ToPort:   aws.Long(int64(0)),
   209  					IPRanges: []*ec2.IPRange{
   210  						&ec2.IPRange{
   211  							CIDRIP: aws.String("0.0.0.0/0"),
   212  						},
   213  					},
   214  					IPProtocol: aws.String("-1"),
   215  				},
   216  			},
   217  		}
   218  
   219  		if _, err = conn.RevokeSecurityGroupEgress(req); err != nil {
   220  			return fmt.Errorf(
   221  				"Error revoking default egress rule for Security Group (%s): %s",
   222  				d.Id(), err)
   223  		}
   224  
   225  	}
   226  
   227  	return resourceAwsSecurityGroupUpdate(d, meta)
   228  }
   229  
   230  func resourceAwsSecurityGroupRead(d *schema.ResourceData, meta interface{}) error {
   231  	conn := meta.(*AWSClient).ec2conn
   232  
   233  	sgRaw, _, err := SGStateRefreshFunc(conn, d.Id())()
   234  	if err != nil {
   235  		return err
   236  	}
   237  	if sgRaw == nil {
   238  		d.SetId("")
   239  		return nil
   240  	}
   241  
   242  	sg := sgRaw.(*ec2.SecurityGroup)
   243  
   244  	ingressRules := resourceAwsSecurityGroupIPPermGather(d, sg.IPPermissions)
   245  	egressRules := resourceAwsSecurityGroupIPPermGather(d, sg.IPPermissionsEgress)
   246  
   247  	d.Set("description", sg.Description)
   248  	d.Set("name", sg.GroupName)
   249  	d.Set("vpc_id", sg.VPCID)
   250  	d.Set("owner_id", sg.OwnerID)
   251  	d.Set("ingress", ingressRules)
   252  	d.Set("egress", egressRules)
   253  	d.Set("tags", tagsToMapSDK(sg.Tags))
   254  	return nil
   255  }
   256  
   257  func resourceAwsSecurityGroupUpdate(d *schema.ResourceData, meta interface{}) error {
   258  	conn := meta.(*AWSClient).ec2conn
   259  
   260  	sgRaw, _, err := SGStateRefreshFunc(conn, d.Id())()
   261  	if err != nil {
   262  		return err
   263  	}
   264  	if sgRaw == nil {
   265  		d.SetId("")
   266  		return nil
   267  	}
   268  
   269  	group := sgRaw.(*ec2.SecurityGroup)
   270  
   271  	err = resourceAwsSecurityGroupUpdateRules(d, "ingress", meta, group)
   272  	if err != nil {
   273  		return err
   274  	}
   275  
   276  	if d.Get("vpc_id") != nil {
   277  		err = resourceAwsSecurityGroupUpdateRules(d, "egress", meta, group)
   278  		if err != nil {
   279  			return err
   280  		}
   281  	}
   282  
   283  	if err := setTagsSDK(conn, d); err != nil {
   284  		return err
   285  	}
   286  
   287  	d.SetPartial("tags")
   288  
   289  	return resourceAwsSecurityGroupRead(d, meta)
   290  }
   291  
   292  func resourceAwsSecurityGroupDelete(d *schema.ResourceData, meta interface{}) error {
   293  	conn := meta.(*AWSClient).ec2conn
   294  
   295  	log.Printf("[DEBUG] Security Group destroy: %v", d.Id())
   296  
   297  	return resource.Retry(2*time.Minute, func() error {
   298  		_, err := conn.DeleteSecurityGroup(&ec2.DeleteSecurityGroupInput{
   299  			GroupID: aws.String(d.Id()),
   300  		})
   301  		if err != nil {
   302  			ec2err, ok := err.(aws.APIError)
   303  			if !ok {
   304  				return err
   305  			}
   306  
   307  			switch ec2err.Code {
   308  			case "InvalidGroup.NotFound":
   309  				return nil
   310  			case "DependencyViolation":
   311  				// If it is a dependency violation, we want to retry
   312  				return err
   313  			default:
   314  				// Any other error, we want to quit the retry loop immediately
   315  				return resource.RetryError{Err: err}
   316  			}
   317  		}
   318  
   319  		return nil
   320  	})
   321  }
   322  
   323  func resourceAwsSecurityGroupRuleHash(v interface{}) int {
   324  	var buf bytes.Buffer
   325  	m := v.(map[string]interface{})
   326  	buf.WriteString(fmt.Sprintf("%d-", m["from_port"].(int)))
   327  	buf.WriteString(fmt.Sprintf("%d-", m["to_port"].(int)))
   328  	buf.WriteString(fmt.Sprintf("%s-", m["protocol"].(string)))
   329  	buf.WriteString(fmt.Sprintf("%t-", m["self"].(bool)))
   330  
   331  	// We need to make sure to sort the strings below so that we always
   332  	// generate the same hash code no matter what is in the set.
   333  	if v, ok := m["cidr_blocks"]; ok {
   334  		vs := v.([]interface{})
   335  		s := make([]string, len(vs))
   336  		for i, raw := range vs {
   337  			s[i] = raw.(string)
   338  		}
   339  		sort.Strings(s)
   340  
   341  		for _, v := range s {
   342  			buf.WriteString(fmt.Sprintf("%s-", v))
   343  		}
   344  	}
   345  	if v, ok := m["security_groups"]; ok {
   346  		vs := v.(*schema.Set).List()
   347  		s := make([]string, len(vs))
   348  		for i, raw := range vs {
   349  			s[i] = raw.(string)
   350  		}
   351  		sort.Strings(s)
   352  
   353  		for _, v := range s {
   354  			buf.WriteString(fmt.Sprintf("%s-", v))
   355  		}
   356  	}
   357  
   358  	return hashcode.String(buf.String())
   359  }
   360  
   361  func resourceAwsSecurityGroupIPPermGather(d *schema.ResourceData, permissions []*ec2.IPPermission) []map[string]interface{} {
   362  	ruleMap := make(map[string]map[string]interface{})
   363  	for _, perm := range permissions {
   364  		var fromPort, toPort int64
   365  		if v := perm.FromPort; v != nil {
   366  			fromPort = *v
   367  		}
   368  		if v := perm.ToPort; v != nil {
   369  			toPort = *v
   370  		}
   371  
   372  		k := fmt.Sprintf("%s-%d-%d", *perm.IPProtocol, fromPort, toPort)
   373  		m, ok := ruleMap[k]
   374  		if !ok {
   375  			m = make(map[string]interface{})
   376  			ruleMap[k] = m
   377  		}
   378  
   379  		m["from_port"] = fromPort
   380  		m["to_port"] = toPort
   381  		m["protocol"] = *perm.IPProtocol
   382  
   383  		if len(perm.IPRanges) > 0 {
   384  			raw, ok := m["cidr_blocks"]
   385  			if !ok {
   386  				raw = make([]string, 0, len(perm.IPRanges))
   387  			}
   388  			list := raw.([]string)
   389  
   390  			for _, ip := range perm.IPRanges {
   391  				list = append(list, *ip.CIDRIP)
   392  			}
   393  
   394  			m["cidr_blocks"] = list
   395  		}
   396  
   397  		var groups []string
   398  		if len(perm.UserIDGroupPairs) > 0 {
   399  			groups = flattenSecurityGroups(perm.UserIDGroupPairs)
   400  		}
   401  		for i, id := range groups {
   402  			if id == d.Id() {
   403  				groups[i], groups = groups[len(groups)-1], groups[:len(groups)-1]
   404  				m["self"] = true
   405  			}
   406  		}
   407  
   408  		if len(groups) > 0 {
   409  			raw, ok := m["security_groups"]
   410  			if !ok {
   411  				raw = make([]string, 0, len(groups))
   412  			}
   413  			list := raw.([]string)
   414  
   415  			list = append(list, groups...)
   416  			m["security_groups"] = list
   417  		}
   418  	}
   419  	rules := make([]map[string]interface{}, 0, len(ruleMap))
   420  	for _, m := range ruleMap {
   421  		rules = append(rules, m)
   422  	}
   423  	return rules
   424  }
   425  
   426  func resourceAwsSecurityGroupUpdateRules(
   427  	d *schema.ResourceData, ruleset string,
   428  	meta interface{}, group *ec2.SecurityGroup) error {
   429  
   430  	if d.HasChange(ruleset) {
   431  		o, n := d.GetChange(ruleset)
   432  		if o == nil {
   433  			o = new(schema.Set)
   434  		}
   435  		if n == nil {
   436  			n = new(schema.Set)
   437  		}
   438  
   439  		os := o.(*schema.Set)
   440  		ns := n.(*schema.Set)
   441  
   442  		remove := expandIPPerms(group, os.Difference(ns).List())
   443  		add := expandIPPerms(group, ns.Difference(os).List())
   444  
   445  		// TODO: We need to handle partial state better in the in-between
   446  		// in this update.
   447  
   448  		// TODO: It'd be nicer to authorize before removing, but then we have
   449  		// to deal with complicated unrolling to get individual CIDR blocks
   450  		// to avoid authorizing already authorized sources. Removing before
   451  		// adding is easier here, and Terraform should be fast enough to
   452  		// not have service issues.
   453  
   454  		if len(remove) > 0 || len(add) > 0 {
   455  			conn := meta.(*AWSClient).ec2conn
   456  
   457  			var err error
   458  			if len(remove) > 0 {
   459  				log.Printf("[DEBUG] Revoking security group %#v %s rule: %#v",
   460  					group, ruleset, remove)
   461  
   462  				if ruleset == "egress" {
   463  					req := &ec2.RevokeSecurityGroupEgressInput{
   464  						GroupID:       group.GroupID,
   465  						IPPermissions: remove,
   466  					}
   467  					_, err = conn.RevokeSecurityGroupEgress(req)
   468  				} else {
   469  					req := &ec2.RevokeSecurityGroupIngressInput{
   470  						GroupID:       group.GroupID,
   471  						IPPermissions: remove,
   472  					}
   473  					_, err = conn.RevokeSecurityGroupIngress(req)
   474  				}
   475  
   476  				if err != nil {
   477  					return fmt.Errorf(
   478  						"Error authorizing security group %s rules: %s",
   479  						ruleset, err)
   480  				}
   481  			}
   482  
   483  			if len(add) > 0 {
   484  				log.Printf("[DEBUG] Authorizing security group %#v %s rule: %#v",
   485  					group, ruleset, add)
   486  				// Authorize the new rules
   487  				if ruleset == "egress" {
   488  					req := &ec2.AuthorizeSecurityGroupEgressInput{
   489  						GroupID:       group.GroupID,
   490  						IPPermissions: add,
   491  					}
   492  					_, err = conn.AuthorizeSecurityGroupEgress(req)
   493  				} else {
   494  					req := &ec2.AuthorizeSecurityGroupIngressInput{
   495  						GroupID:       group.GroupID,
   496  						IPPermissions: add,
   497  					}
   498  					if group.VPCID == nil || *group.VPCID == "" {
   499  						req.GroupID = nil
   500  						req.GroupName = group.GroupName
   501  					}
   502  
   503  					_, err = conn.AuthorizeSecurityGroupIngress(req)
   504  				}
   505  
   506  				if err != nil {
   507  					return fmt.Errorf(
   508  						"Error authorizing security group %s rules: %s",
   509  						ruleset, err)
   510  				}
   511  			}
   512  		}
   513  	}
   514  	return nil
   515  }
   516  
   517  // SGStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
   518  // a security group.
   519  func SGStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc {
   520  	return func() (interface{}, string, error) {
   521  		req := &ec2.DescribeSecurityGroupsInput{
   522  			GroupIDs: []*string{aws.String(id)},
   523  		}
   524  		resp, err := conn.DescribeSecurityGroups(req)
   525  		if err != nil {
   526  			if ec2err, ok := err.(aws.APIError); ok {
   527  				if ec2err.Code == "InvalidSecurityGroupID.NotFound" ||
   528  					ec2err.Code == "InvalidGroup.NotFound" {
   529  					resp = nil
   530  					err = nil
   531  				}
   532  			}
   533  
   534  			if err != nil {
   535  				log.Printf("Error on SGStateRefresh: %s", err)
   536  				return nil, "", err
   537  			}
   538  		}
   539  
   540  		if resp == nil {
   541  			return nil, "", nil
   542  		}
   543  
   544  		group := resp.SecurityGroups[0]
   545  		return group, "exists", nil
   546  	}
   547  }