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 }