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