github.com/ottenhoff/terraform@v0.7.0-rc1.0.20160607213102-ac2d195cc560/builtin/providers/aws/resource_aws_security_group_rule.go (about)

     1  package aws
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"log"
     7  	"sort"
     8  	"strings"
     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 resourceAwsSecurityGroupRule() *schema.Resource {
    20  	return &schema.Resource{
    21  		Create: resourceAwsSecurityGroupRuleCreate,
    22  		Read:   resourceAwsSecurityGroupRuleRead,
    23  		Delete: resourceAwsSecurityGroupRuleDelete,
    24  
    25  		SchemaVersion: 2,
    26  		MigrateState:  resourceAwsSecurityGroupRuleMigrateState,
    27  
    28  		Schema: map[string]*schema.Schema{
    29  			"type": &schema.Schema{
    30  				Type:        schema.TypeString,
    31  				Required:    true,
    32  				ForceNew:    true,
    33  				Description: "Type of rule, ingress (inbound) or egress (outbound).",
    34  			},
    35  
    36  			"from_port": &schema.Schema{
    37  				Type:     schema.TypeInt,
    38  				Required: true,
    39  				ForceNew: true,
    40  			},
    41  
    42  			"to_port": &schema.Schema{
    43  				Type:     schema.TypeInt,
    44  				Required: true,
    45  				ForceNew: true,
    46  			},
    47  
    48  			"protocol": &schema.Schema{
    49  				Type:      schema.TypeString,
    50  				Required:  true,
    51  				ForceNew:  true,
    52  				StateFunc: protocolStateFunc,
    53  			},
    54  
    55  			"cidr_blocks": &schema.Schema{
    56  				Type:     schema.TypeList,
    57  				Optional: true,
    58  				ForceNew: true,
    59  				Elem:     &schema.Schema{Type: schema.TypeString},
    60  			},
    61  
    62  			"security_group_id": &schema.Schema{
    63  				Type:     schema.TypeString,
    64  				Required: true,
    65  				ForceNew: true,
    66  			},
    67  
    68  			"source_security_group_id": &schema.Schema{
    69  				Type:          schema.TypeString,
    70  				Optional:      true,
    71  				ForceNew:      true,
    72  				Computed:      true,
    73  				ConflictsWith: []string{"cidr_blocks", "self"},
    74  			},
    75  
    76  			"self": &schema.Schema{
    77  				Type:          schema.TypeBool,
    78  				Optional:      true,
    79  				Default:       false,
    80  				ForceNew:      true,
    81  				ConflictsWith: []string{"cidr_blocks"},
    82  			},
    83  		},
    84  	}
    85  }
    86  
    87  func resourceAwsSecurityGroupRuleCreate(d *schema.ResourceData, meta interface{}) error {
    88  	conn := meta.(*AWSClient).ec2conn
    89  	sg_id := d.Get("security_group_id").(string)
    90  
    91  	awsMutexKV.Lock(sg_id)
    92  	defer awsMutexKV.Unlock(sg_id)
    93  
    94  	sg, err := findResourceSecurityGroup(conn, sg_id)
    95  	if err != nil {
    96  		return err
    97  	}
    98  
    99  	perm, err := expandIPPerm(d, sg)
   100  	if err != nil {
   101  		return err
   102  	}
   103  
   104  	ruleType := d.Get("type").(string)
   105  	isVPC := sg.VpcId != nil && *sg.VpcId != ""
   106  
   107  	var autherr error
   108  	switch ruleType {
   109  	case "ingress":
   110  		log.Printf("[DEBUG] Authorizing security group %s %s rule: %s",
   111  			sg_id, "Ingress", perm)
   112  
   113  		req := &ec2.AuthorizeSecurityGroupIngressInput{
   114  			GroupId:       sg.GroupId,
   115  			IpPermissions: []*ec2.IpPermission{perm},
   116  		}
   117  
   118  		if !isVPC {
   119  			req.GroupId = nil
   120  			req.GroupName = sg.GroupName
   121  		}
   122  
   123  		_, autherr = conn.AuthorizeSecurityGroupIngress(req)
   124  
   125  	case "egress":
   126  		log.Printf("[DEBUG] Authorizing security group %s %s rule: %#v",
   127  			sg_id, "Egress", perm)
   128  
   129  		req := &ec2.AuthorizeSecurityGroupEgressInput{
   130  			GroupId:       sg.GroupId,
   131  			IpPermissions: []*ec2.IpPermission{perm},
   132  		}
   133  
   134  		_, autherr = conn.AuthorizeSecurityGroupEgress(req)
   135  
   136  	default:
   137  		return fmt.Errorf("Security Group Rule must be type 'ingress' or type 'egress'")
   138  	}
   139  
   140  	if autherr != nil {
   141  		if awsErr, ok := autherr.(awserr.Error); ok {
   142  			if awsErr.Code() == "InvalidPermission.Duplicate" {
   143  				return fmt.Errorf(`[WARN] A duplicate Security Group rule was found on (%s). This may be
   144  a side effect of a now-fixed Terraform issue causing two security groups with
   145  identical attributes but different source_security_group_ids to overwrite each
   146  other in the state. See https://github.com/hashicorp/terraform/pull/2376 for more
   147  information and instructions for recovery. Error message: %s`, sg_id, awsErr.Message())
   148  			}
   149  		}
   150  
   151  		return fmt.Errorf(
   152  			"Error authorizing security group rule type %s: %s",
   153  			ruleType, autherr)
   154  	}
   155  
   156  	id := ipPermissionIDHash(sg_id, ruleType, perm)
   157  	log.Printf("[DEBUG] Computed group rule ID %s", id)
   158  
   159  	retErr := resource.Retry(5*time.Minute, func() *resource.RetryError {
   160  		sg, err := findResourceSecurityGroup(conn, sg_id)
   161  
   162  		if err != nil {
   163  			log.Printf("[DEBUG] Error finding Secuirty Group (%s) for Rule (%s): %s", sg_id, id, err)
   164  			return resource.NonRetryableError(err)
   165  		}
   166  
   167  		var rules []*ec2.IpPermission
   168  		switch ruleType {
   169  		case "ingress":
   170  			rules = sg.IpPermissions
   171  		default:
   172  			rules = sg.IpPermissionsEgress
   173  		}
   174  
   175  		rule := findRuleMatch(perm, rules, isVPC)
   176  
   177  		if rule == nil {
   178  			log.Printf("[DEBUG] Unable to find matching %s Security Group Rule (%s) for Group %s",
   179  				ruleType, id, sg_id)
   180  			return resource.RetryableError(fmt.Errorf("No match found"))
   181  		}
   182  
   183  		log.Printf("[DEBUG] Found rule for Security Group Rule (%s): %s", id, rule)
   184  		return nil
   185  	})
   186  
   187  	if retErr != nil {
   188  		log.Printf("[DEBUG] Error finding matching %s Security Group Rule (%s) for Group %s -- NO STATE WILL BE SAVED",
   189  			ruleType, id, sg_id)
   190  		return nil
   191  	}
   192  
   193  	d.SetId(id)
   194  	return nil
   195  }
   196  
   197  func resourceAwsSecurityGroupRuleRead(d *schema.ResourceData, meta interface{}) error {
   198  	conn := meta.(*AWSClient).ec2conn
   199  	sg_id := d.Get("security_group_id").(string)
   200  	sg, err := findResourceSecurityGroup(conn, sg_id)
   201  	if err != nil {
   202  		log.Printf("[DEBUG] Error finding Secuirty Group (%s) for Rule (%s): %s", sg_id, d.Id(), err)
   203  		d.SetId("")
   204  		return nil
   205  	}
   206  
   207  	isVPC := sg.VpcId != nil && *sg.VpcId != ""
   208  
   209  	var rule *ec2.IpPermission
   210  	var rules []*ec2.IpPermission
   211  	ruleType := d.Get("type").(string)
   212  	switch ruleType {
   213  	case "ingress":
   214  		rules = sg.IpPermissions
   215  	default:
   216  		rules = sg.IpPermissionsEgress
   217  	}
   218  
   219  	p, err := expandIPPerm(d, sg)
   220  	if err != nil {
   221  		return err
   222  	}
   223  
   224  	if len(rules) == 0 {
   225  		log.Printf("[WARN] No %s rules were found for Security Group (%s) looking for Security Group Rule (%s)",
   226  			ruleType, *sg.GroupName, d.Id())
   227  		d.SetId("")
   228  		return nil
   229  	}
   230  
   231  	rule = findRuleMatch(p, rules, isVPC)
   232  
   233  	if rule == nil {
   234  		log.Printf("[DEBUG] Unable to find matching %s Security Group Rule (%s) for Group %s",
   235  			ruleType, d.Id(), sg_id)
   236  		d.SetId("")
   237  		return nil
   238  	}
   239  
   240  	log.Printf("[DEBUG] Found rule for Security Group Rule (%s): %s", d.Id(), rule)
   241  
   242  	d.Set("type", ruleType)
   243  	setFromIPPerm(d, sg, p)
   244  	return nil
   245  }
   246  
   247  func resourceAwsSecurityGroupRuleDelete(d *schema.ResourceData, meta interface{}) error {
   248  	conn := meta.(*AWSClient).ec2conn
   249  	sg_id := d.Get("security_group_id").(string)
   250  
   251  	awsMutexKV.Lock(sg_id)
   252  	defer awsMutexKV.Unlock(sg_id)
   253  
   254  	sg, err := findResourceSecurityGroup(conn, sg_id)
   255  	if err != nil {
   256  		return err
   257  	}
   258  
   259  	perm, err := expandIPPerm(d, sg)
   260  	if err != nil {
   261  		return err
   262  	}
   263  	ruleType := d.Get("type").(string)
   264  	switch ruleType {
   265  	case "ingress":
   266  		log.Printf("[DEBUG] Revoking rule (%s) from security group %s:\n%s",
   267  			"ingress", sg_id, perm)
   268  		req := &ec2.RevokeSecurityGroupIngressInput{
   269  			GroupId:       sg.GroupId,
   270  			IpPermissions: []*ec2.IpPermission{perm},
   271  		}
   272  
   273  		_, err = conn.RevokeSecurityGroupIngress(req)
   274  
   275  		if err != nil {
   276  			return fmt.Errorf(
   277  				"Error revoking security group %s rules: %s",
   278  				sg_id, err)
   279  		}
   280  	case "egress":
   281  
   282  		log.Printf("[DEBUG] Revoking security group %#v %s rule: %#v",
   283  			sg_id, "egress", perm)
   284  		req := &ec2.RevokeSecurityGroupEgressInput{
   285  			GroupId:       sg.GroupId,
   286  			IpPermissions: []*ec2.IpPermission{perm},
   287  		}
   288  
   289  		_, err = conn.RevokeSecurityGroupEgress(req)
   290  
   291  		if err != nil {
   292  			return fmt.Errorf(
   293  				"Error revoking security group %s rules: %s",
   294  				sg_id, err)
   295  		}
   296  	}
   297  
   298  	d.SetId("")
   299  
   300  	return nil
   301  }
   302  
   303  func findResourceSecurityGroup(conn *ec2.EC2, id string) (*ec2.SecurityGroup, error) {
   304  	req := &ec2.DescribeSecurityGroupsInput{
   305  		GroupIds: []*string{aws.String(id)},
   306  	}
   307  	resp, err := conn.DescribeSecurityGroups(req)
   308  	if err != nil {
   309  		return nil, err
   310  	}
   311  
   312  	if resp == nil || len(resp.SecurityGroups) != 1 || resp.SecurityGroups[0] == nil {
   313  		return nil, fmt.Errorf(
   314  			"Expected to find one security group with ID %q, got: %#v",
   315  			id, resp.SecurityGroups)
   316  	}
   317  
   318  	return resp.SecurityGroups[0], nil
   319  }
   320  
   321  // ByGroupPair implements sort.Interface for []*ec2.UserIDGroupPairs based on
   322  // GroupID or GroupName field (only one should be set).
   323  type ByGroupPair []*ec2.UserIdGroupPair
   324  
   325  func (b ByGroupPair) Len() int      { return len(b) }
   326  func (b ByGroupPair) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
   327  func (b ByGroupPair) Less(i, j int) bool {
   328  	if b[i].GroupId != nil && b[j].GroupId != nil {
   329  		return *b[i].GroupId < *b[j].GroupId
   330  	}
   331  	if b[i].GroupName != nil && b[j].GroupName != nil {
   332  		return *b[i].GroupName < *b[j].GroupName
   333  	}
   334  
   335  	panic("mismatched security group rules, may be a terraform bug")
   336  }
   337  
   338  func findRuleMatch(p *ec2.IpPermission, rules []*ec2.IpPermission, isVPC bool) *ec2.IpPermission {
   339  	var rule *ec2.IpPermission
   340  	for _, r := range rules {
   341  		if r.ToPort != nil && *p.ToPort != *r.ToPort {
   342  			continue
   343  		}
   344  
   345  		if r.FromPort != nil && *p.FromPort != *r.FromPort {
   346  			continue
   347  		}
   348  
   349  		if r.IpProtocol != nil && *p.IpProtocol != *r.IpProtocol {
   350  			continue
   351  		}
   352  
   353  		remaining := len(p.IpRanges)
   354  		for _, ip := range p.IpRanges {
   355  			for _, rip := range r.IpRanges {
   356  				if *ip.CidrIp == *rip.CidrIp {
   357  					remaining--
   358  				}
   359  			}
   360  		}
   361  
   362  		if remaining > 0 {
   363  			continue
   364  		}
   365  
   366  		remaining = len(p.UserIdGroupPairs)
   367  		for _, ip := range p.UserIdGroupPairs {
   368  			for _, rip := range r.UserIdGroupPairs {
   369  				if isVPC {
   370  					if *ip.GroupId == *rip.GroupId {
   371  						remaining--
   372  					}
   373  				} else {
   374  					if *ip.GroupName == *rip.GroupName {
   375  						remaining--
   376  					}
   377  				}
   378  			}
   379  		}
   380  
   381  		if remaining > 0 {
   382  			continue
   383  		}
   384  
   385  		rule = r
   386  	}
   387  	return rule
   388  }
   389  
   390  func ipPermissionIDHash(sg_id, ruleType string, ip *ec2.IpPermission) string {
   391  	var buf bytes.Buffer
   392  	buf.WriteString(fmt.Sprintf("%s-", sg_id))
   393  	if ip.FromPort != nil && *ip.FromPort > 0 {
   394  		buf.WriteString(fmt.Sprintf("%d-", *ip.FromPort))
   395  	}
   396  	if ip.ToPort != nil && *ip.ToPort > 0 {
   397  		buf.WriteString(fmt.Sprintf("%d-", *ip.ToPort))
   398  	}
   399  	buf.WriteString(fmt.Sprintf("%s-", *ip.IpProtocol))
   400  	buf.WriteString(fmt.Sprintf("%s-", ruleType))
   401  
   402  	// We need to make sure to sort the strings below so that we always
   403  	// generate the same hash code no matter what is in the set.
   404  	if len(ip.IpRanges) > 0 {
   405  		s := make([]string, len(ip.IpRanges))
   406  		for i, r := range ip.IpRanges {
   407  			s[i] = *r.CidrIp
   408  		}
   409  		sort.Strings(s)
   410  
   411  		for _, v := range s {
   412  			buf.WriteString(fmt.Sprintf("%s-", v))
   413  		}
   414  	}
   415  
   416  	if len(ip.UserIdGroupPairs) > 0 {
   417  		sort.Sort(ByGroupPair(ip.UserIdGroupPairs))
   418  		for _, pair := range ip.UserIdGroupPairs {
   419  			if pair.GroupId != nil {
   420  				buf.WriteString(fmt.Sprintf("%s-", *pair.GroupId))
   421  			} else {
   422  				buf.WriteString("-")
   423  			}
   424  			if pair.GroupName != nil {
   425  				buf.WriteString(fmt.Sprintf("%s-", *pair.GroupName))
   426  			} else {
   427  				buf.WriteString("-")
   428  			}
   429  		}
   430  	}
   431  
   432  	return fmt.Sprintf("sgrule-%d", hashcode.String(buf.String()))
   433  }
   434  
   435  func expandIPPerm(d *schema.ResourceData, sg *ec2.SecurityGroup) (*ec2.IpPermission, error) {
   436  	var perm ec2.IpPermission
   437  
   438  	perm.FromPort = aws.Int64(int64(d.Get("from_port").(int)))
   439  	perm.ToPort = aws.Int64(int64(d.Get("to_port").(int)))
   440  	protocol := protocolForValue(d.Get("protocol").(string))
   441  	perm.IpProtocol = aws.String(protocol)
   442  
   443  	// build a group map that behaves like a set
   444  	groups := make(map[string]bool)
   445  	if raw, ok := d.GetOk("source_security_group_id"); ok {
   446  		groups[raw.(string)] = true
   447  	}
   448  
   449  	if v, ok := d.GetOk("self"); ok && v.(bool) {
   450  		// if sg.GroupId != nil {
   451  		if sg.VpcId != nil && *sg.VpcId != "" {
   452  			groups[*sg.GroupId] = true
   453  		} else {
   454  			groups[*sg.GroupName] = true
   455  		}
   456  	}
   457  
   458  	if len(groups) > 0 {
   459  		perm.UserIdGroupPairs = make([]*ec2.UserIdGroupPair, len(groups))
   460  		// build string list of group name/ids
   461  		var gl []string
   462  		for k, _ := range groups {
   463  			gl = append(gl, k)
   464  		}
   465  
   466  		for i, name := range gl {
   467  			ownerId, id := "", name
   468  			if items := strings.Split(id, "/"); len(items) > 1 {
   469  				ownerId, id = items[0], items[1]
   470  			}
   471  
   472  			perm.UserIdGroupPairs[i] = &ec2.UserIdGroupPair{
   473  				GroupId: aws.String(id),
   474  				UserId:  aws.String(ownerId),
   475  			}
   476  
   477  			if sg.VpcId == nil || *sg.VpcId == "" {
   478  				perm.UserIdGroupPairs[i].GroupId = nil
   479  				perm.UserIdGroupPairs[i].GroupName = aws.String(id)
   480  				perm.UserIdGroupPairs[i].UserId = nil
   481  			}
   482  		}
   483  	}
   484  
   485  	if raw, ok := d.GetOk("cidr_blocks"); ok {
   486  		list := raw.([]interface{})
   487  		perm.IpRanges = make([]*ec2.IpRange, len(list))
   488  		for i, v := range list {
   489  			cidrIP, ok := v.(string)
   490  			if !ok {
   491  				return nil, fmt.Errorf("empty element found in cidr_blocks - consider using the compact function")
   492  			}
   493  			perm.IpRanges[i] = &ec2.IpRange{CidrIp: aws.String(cidrIP)}
   494  		}
   495  	}
   496  
   497  	return &perm, nil
   498  }
   499  
   500  func setFromIPPerm(d *schema.ResourceData, sg *ec2.SecurityGroup, rule *ec2.IpPermission) error {
   501  	isVPC := sg.VpcId != nil && *sg.VpcId != ""
   502  
   503  	d.Set("from_port", rule.FromPort)
   504  	d.Set("to_port", rule.ToPort)
   505  	d.Set("protocol", rule.IpProtocol)
   506  
   507  	var cb []string
   508  	for _, c := range rule.IpRanges {
   509  		cb = append(cb, *c.CidrIp)
   510  	}
   511  
   512  	d.Set("cidr_blocks", cb)
   513  
   514  	if len(rule.UserIdGroupPairs) > 0 {
   515  		s := rule.UserIdGroupPairs[0]
   516  		if isVPC {
   517  			d.Set("source_security_group_id", *s.GroupId)
   518  		} else {
   519  			d.Set("source_security_group_id", *s.GroupName)
   520  		}
   521  	}
   522  
   523  	return nil
   524  }