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