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