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