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