github.com/leeprovoost/terraform@v0.6.10-0.20160119085442-96f3f76118e7/builtin/providers/aws/resource_aws_network_acl.go (about) 1 package aws 2 3 import ( 4 "bytes" 5 "fmt" 6 "log" 7 "sort" 8 "strconv" 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 resourceAwsNetworkAcl() *schema.Resource { 20 21 return &schema.Resource{ 22 Create: resourceAwsNetworkAclCreate, 23 Read: resourceAwsNetworkAclRead, 24 Delete: resourceAwsNetworkAclDelete, 25 Update: resourceAwsNetworkAclUpdate, 26 27 Schema: map[string]*schema.Schema{ 28 "vpc_id": &schema.Schema{ 29 Type: schema.TypeString, 30 Required: true, 31 ForceNew: true, 32 Computed: false, 33 }, 34 "subnet_id": &schema.Schema{ 35 Type: schema.TypeString, 36 Optional: true, 37 ForceNew: true, 38 Computed: false, 39 Deprecated: "Attribute subnet_id is deprecated on network_acl resources. Use subnet_ids instead", 40 }, 41 "subnet_ids": &schema.Schema{ 42 Type: schema.TypeSet, 43 Optional: true, 44 Computed: true, 45 ConflictsWith: []string{"subnet_id"}, 46 Elem: &schema.Schema{Type: schema.TypeString}, 47 Set: schema.HashString, 48 }, 49 "ingress": &schema.Schema{ 50 Type: schema.TypeSet, 51 Required: false, 52 Optional: true, 53 Computed: true, 54 Elem: &schema.Resource{ 55 Schema: map[string]*schema.Schema{ 56 "from_port": &schema.Schema{ 57 Type: schema.TypeInt, 58 Required: true, 59 }, 60 "to_port": &schema.Schema{ 61 Type: schema.TypeInt, 62 Required: true, 63 }, 64 "rule_no": &schema.Schema{ 65 Type: schema.TypeInt, 66 Required: true, 67 }, 68 "action": &schema.Schema{ 69 Type: schema.TypeString, 70 Required: true, 71 }, 72 "protocol": &schema.Schema{ 73 Type: schema.TypeString, 74 Required: true, 75 }, 76 "cidr_block": &schema.Schema{ 77 Type: schema.TypeString, 78 Optional: true, 79 }, 80 "icmp_type": &schema.Schema{ 81 Type: schema.TypeInt, 82 Optional: true, 83 }, 84 "icmp_code": &schema.Schema{ 85 Type: schema.TypeInt, 86 Optional: true, 87 }, 88 }, 89 }, 90 Set: resourceAwsNetworkAclEntryHash, 91 }, 92 "egress": &schema.Schema{ 93 Type: schema.TypeSet, 94 Required: false, 95 Optional: true, 96 Computed: true, 97 Elem: &schema.Resource{ 98 Schema: map[string]*schema.Schema{ 99 "from_port": &schema.Schema{ 100 Type: schema.TypeInt, 101 Required: true, 102 }, 103 "to_port": &schema.Schema{ 104 Type: schema.TypeInt, 105 Required: true, 106 }, 107 "rule_no": &schema.Schema{ 108 Type: schema.TypeInt, 109 Required: true, 110 }, 111 "action": &schema.Schema{ 112 Type: schema.TypeString, 113 Required: true, 114 }, 115 "protocol": &schema.Schema{ 116 Type: schema.TypeString, 117 Required: true, 118 }, 119 "cidr_block": &schema.Schema{ 120 Type: schema.TypeString, 121 Optional: true, 122 }, 123 "icmp_type": &schema.Schema{ 124 Type: schema.TypeInt, 125 Optional: true, 126 }, 127 "icmp_code": &schema.Schema{ 128 Type: schema.TypeInt, 129 Optional: true, 130 }, 131 }, 132 }, 133 Set: resourceAwsNetworkAclEntryHash, 134 }, 135 "tags": tagsSchema(), 136 }, 137 } 138 } 139 140 func resourceAwsNetworkAclCreate(d *schema.ResourceData, meta interface{}) error { 141 142 conn := meta.(*AWSClient).ec2conn 143 144 // Create the Network Acl 145 createOpts := &ec2.CreateNetworkAclInput{ 146 VpcId: aws.String(d.Get("vpc_id").(string)), 147 } 148 149 log.Printf("[DEBUG] Network Acl create config: %#v", createOpts) 150 resp, err := conn.CreateNetworkAcl(createOpts) 151 if err != nil { 152 return fmt.Errorf("Error creating network acl: %s", err) 153 } 154 155 // Get the ID and store it 156 networkAcl := resp.NetworkAcl 157 d.SetId(*networkAcl.NetworkAclId) 158 log.Printf("[INFO] Network Acl ID: %s", *networkAcl.NetworkAclId) 159 160 // Update rules and subnet association once acl is created 161 return resourceAwsNetworkAclUpdate(d, meta) 162 } 163 164 func resourceAwsNetworkAclRead(d *schema.ResourceData, meta interface{}) error { 165 conn := meta.(*AWSClient).ec2conn 166 167 resp, err := conn.DescribeNetworkAcls(&ec2.DescribeNetworkAclsInput{ 168 NetworkAclIds: []*string{aws.String(d.Id())}, 169 }) 170 171 if err != nil { 172 return err 173 } 174 if resp == nil { 175 return nil 176 } 177 178 networkAcl := resp.NetworkAcls[0] 179 var ingressEntries []*ec2.NetworkAclEntry 180 var egressEntries []*ec2.NetworkAclEntry 181 182 // separate the ingress and egress rules 183 for _, e := range networkAcl.Entries { 184 // Skip the default rules added by AWS. They can be neither 185 // configured or deleted by users. 186 if *e.RuleNumber == 32767 { 187 continue 188 } 189 190 if *e.Egress == true { 191 egressEntries = append(egressEntries, e) 192 } else { 193 ingressEntries = append(ingressEntries, e) 194 } 195 } 196 197 d.Set("vpc_id", networkAcl.VpcId) 198 d.Set("tags", tagsToMap(networkAcl.Tags)) 199 200 var s []string 201 for _, a := range networkAcl.Associations { 202 s = append(s, *a.SubnetId) 203 } 204 sort.Strings(s) 205 if err := d.Set("subnet_ids", s); err != nil { 206 return err 207 } 208 209 if err := d.Set("ingress", networkAclEntriesToMapList(ingressEntries)); err != nil { 210 return err 211 } 212 if err := d.Set("egress", networkAclEntriesToMapList(egressEntries)); err != nil { 213 return err 214 } 215 216 return nil 217 } 218 219 func resourceAwsNetworkAclUpdate(d *schema.ResourceData, meta interface{}) error { 220 conn := meta.(*AWSClient).ec2conn 221 d.Partial(true) 222 223 if d.HasChange("ingress") { 224 err := updateNetworkAclEntries(d, "ingress", conn) 225 if err != nil { 226 return err 227 } 228 } 229 230 if d.HasChange("egress") { 231 err := updateNetworkAclEntries(d, "egress", conn) 232 if err != nil { 233 return err 234 } 235 } 236 237 if d.HasChange("subnet_id") { 238 //associate new subnet with the acl. 239 _, n := d.GetChange("subnet_id") 240 newSubnet := n.(string) 241 association, err := findNetworkAclAssociation(newSubnet, conn) 242 if err != nil { 243 return fmt.Errorf("Failed to update acl %s with subnet %s: %s", d.Id(), newSubnet, err) 244 } 245 _, err = conn.ReplaceNetworkAclAssociation(&ec2.ReplaceNetworkAclAssociationInput{ 246 AssociationId: association.NetworkAclAssociationId, 247 NetworkAclId: aws.String(d.Id()), 248 }) 249 if err != nil { 250 return err 251 } 252 } 253 254 if d.HasChange("subnet_ids") { 255 o, n := d.GetChange("subnet_ids") 256 if o == nil { 257 o = new(schema.Set) 258 } 259 if n == nil { 260 n = new(schema.Set) 261 } 262 263 os := o.(*schema.Set) 264 ns := n.(*schema.Set) 265 266 remove := os.Difference(ns).List() 267 add := ns.Difference(os).List() 268 269 if len(remove) > 0 { 270 // A Network ACL is required for each subnet. In order to disassociate a 271 // subnet from this ACL, we must associate it with the default ACL. 272 defaultAcl, err := getDefaultNetworkAcl(d.Get("vpc_id").(string), conn) 273 if err != nil { 274 return fmt.Errorf("Failed to find Default ACL for VPC %s", d.Get("vpc_id").(string)) 275 } 276 for _, r := range remove { 277 association, err := findNetworkAclAssociation(r.(string), conn) 278 if err != nil { 279 return fmt.Errorf("Failed to find acl association: acl %s with subnet %s: %s", d.Id(), r, err) 280 } 281 _, err = conn.ReplaceNetworkAclAssociation(&ec2.ReplaceNetworkAclAssociationInput{ 282 AssociationId: association.NetworkAclAssociationId, 283 NetworkAclId: defaultAcl.NetworkAclId, 284 }) 285 if err != nil { 286 return err 287 } 288 } 289 } 290 291 if len(add) > 0 { 292 for _, a := range add { 293 association, err := findNetworkAclAssociation(a.(string), conn) 294 if err != nil { 295 return fmt.Errorf("Failed to find acl association: acl %s with subnet %s: %s", d.Id(), a, err) 296 } 297 _, err = conn.ReplaceNetworkAclAssociation(&ec2.ReplaceNetworkAclAssociationInput{ 298 AssociationId: association.NetworkAclAssociationId, 299 NetworkAclId: aws.String(d.Id()), 300 }) 301 if err != nil { 302 return err 303 } 304 } 305 } 306 307 } 308 309 if err := setTags(conn, d); err != nil { 310 return err 311 } else { 312 d.SetPartial("tags") 313 } 314 315 d.Partial(false) 316 return resourceAwsNetworkAclRead(d, meta) 317 } 318 319 func updateNetworkAclEntries(d *schema.ResourceData, entryType string, conn *ec2.EC2) error { 320 321 if d.HasChange(entryType) { 322 o, n := d.GetChange(entryType) 323 324 if o == nil { 325 o = new(schema.Set) 326 } 327 if n == nil { 328 n = new(schema.Set) 329 } 330 331 os := o.(*schema.Set) 332 ns := n.(*schema.Set) 333 334 toBeDeleted, err := expandNetworkAclEntries(os.Difference(ns).List(), entryType) 335 if err != nil { 336 return err 337 } 338 for _, remove := range toBeDeleted { 339 340 // AWS includes default rules with all network ACLs that can be 341 // neither modified nor destroyed. They have a custom rule 342 // number that is out of bounds for any other rule. If we 343 // encounter it, just continue. There's no work to be done. 344 if *remove.RuleNumber == 32767 { 345 continue 346 } 347 348 // Delete old Acl 349 _, err := conn.DeleteNetworkAclEntry(&ec2.DeleteNetworkAclEntryInput{ 350 NetworkAclId: aws.String(d.Id()), 351 RuleNumber: remove.RuleNumber, 352 Egress: remove.Egress, 353 }) 354 if err != nil { 355 return fmt.Errorf("Error deleting %s entry: %s", entryType, err) 356 } 357 } 358 359 toBeCreated, err := expandNetworkAclEntries(ns.Difference(os).List(), entryType) 360 if err != nil { 361 return err 362 } 363 for _, add := range toBeCreated { 364 // Protocol -1 rules don't store ports in AWS. Thus, they'll always 365 // hash differently when being read out of the API. Force the user 366 // to set from_port and to_port to 0 for these rules, to keep the 367 // hashing consistent. 368 if *add.Protocol == "-1" { 369 to := *add.PortRange.To 370 from := *add.PortRange.From 371 expected := &expectedPortPair{ 372 to_port: 0, 373 from_port: 0, 374 } 375 if ok := validatePorts(to, from, *expected); !ok { 376 return fmt.Errorf( 377 "to_port (%d) and from_port (%d) must both be 0 to use the the 'all' \"-1\" protocol!", 378 to, from) 379 } 380 } 381 382 // AWS mutates the CIDR block into a network implied by the IP and 383 // mask provided. This results in hashing inconsistencies between 384 // the local config file and the state returned by the API. Error 385 // if the user provides a CIDR block with an inappropriate mask 386 if err := validateCIDRBlock(*add.CidrBlock); err != nil { 387 return err 388 } 389 390 // Add new Acl entry 391 _, connErr := conn.CreateNetworkAclEntry(&ec2.CreateNetworkAclEntryInput{ 392 NetworkAclId: aws.String(d.Id()), 393 CidrBlock: add.CidrBlock, 394 Egress: add.Egress, 395 PortRange: add.PortRange, 396 Protocol: add.Protocol, 397 RuleAction: add.RuleAction, 398 RuleNumber: add.RuleNumber, 399 IcmpTypeCode: add.IcmpTypeCode, 400 }) 401 if connErr != nil { 402 return fmt.Errorf("Error creating %s entry: %s", entryType, connErr) 403 } 404 } 405 } 406 return nil 407 } 408 409 func resourceAwsNetworkAclDelete(d *schema.ResourceData, meta interface{}) error { 410 conn := meta.(*AWSClient).ec2conn 411 412 log.Printf("[INFO] Deleting Network Acl: %s", d.Id()) 413 return resource.Retry(5*time.Minute, func() error { 414 _, err := conn.DeleteNetworkAcl(&ec2.DeleteNetworkAclInput{ 415 NetworkAclId: aws.String(d.Id()), 416 }) 417 if err != nil { 418 ec2err := err.(awserr.Error) 419 switch ec2err.Code() { 420 case "InvalidNetworkAclID.NotFound": 421 return nil 422 case "DependencyViolation": 423 // In case of dependency violation, we remove the association between subnet and network acl. 424 // This means the subnet is attached to default acl of vpc. 425 var associations []*ec2.NetworkAclAssociation 426 if v, ok := d.GetOk("subnet_id"); ok { 427 428 a, err := findNetworkAclAssociation(v.(string), conn) 429 if err != nil { 430 return resource.RetryError{Err: fmt.Errorf("Dependency violation: Cannot find ACL %s: %s", d.Id(), err)} 431 } 432 associations = append(associations, a) 433 } else if v, ok := d.GetOk("subnet_ids"); ok { 434 ids := v.(*schema.Set).List() 435 for _, i := range ids { 436 a, err := findNetworkAclAssociation(i.(string), conn) 437 if err != nil { 438 return resource.RetryError{Err: fmt.Errorf("Dependency violation: Cannot delete acl %s: %s", d.Id(), err)} 439 } 440 associations = append(associations, a) 441 } 442 } 443 defaultAcl, err := getDefaultNetworkAcl(d.Get("vpc_id").(string), conn) 444 if err != nil { 445 return resource.RetryError{Err: fmt.Errorf("Dependency violation: Cannot delete acl %s: %s", d.Id(), err)} 446 } 447 448 for _, a := range associations { 449 _, err = conn.ReplaceNetworkAclAssociation(&ec2.ReplaceNetworkAclAssociationInput{ 450 AssociationId: a.NetworkAclAssociationId, 451 NetworkAclId: defaultAcl.NetworkAclId, 452 }) 453 } 454 return resource.RetryError{Err: err} 455 default: 456 // Any other error, we want to quit the retry loop immediately 457 return resource.RetryError{Err: err} 458 } 459 } 460 log.Printf("[Info] Deleted network ACL %s successfully", d.Id()) 461 return nil 462 }) 463 } 464 465 func resourceAwsNetworkAclEntryHash(v interface{}) int { 466 var buf bytes.Buffer 467 m := v.(map[string]interface{}) 468 buf.WriteString(fmt.Sprintf("%d-", m["from_port"].(int))) 469 buf.WriteString(fmt.Sprintf("%d-", m["to_port"].(int))) 470 buf.WriteString(fmt.Sprintf("%d-", m["rule_no"].(int))) 471 buf.WriteString(fmt.Sprintf("%s-", m["action"].(string))) 472 473 // The AWS network ACL API only speaks protocol numbers, and that's 474 // all we store. Never hash a protocol name. 475 protocol := m["protocol"].(string) 476 if _, err := strconv.Atoi(m["protocol"].(string)); err != nil { 477 // We're a protocol name. Look up the number. 478 buf.WriteString(fmt.Sprintf("%d-", protocolIntegers()[protocol])) 479 } else { 480 // We're a protocol number. Pass the value through. 481 buf.WriteString(fmt.Sprintf("%s-", protocol)) 482 } 483 484 buf.WriteString(fmt.Sprintf("%s-", m["cidr_block"].(string))) 485 486 if v, ok := m["ssl_certificate_id"]; ok { 487 buf.WriteString(fmt.Sprintf("%s-", v.(string))) 488 } 489 490 if v, ok := m["icmp_type"]; ok { 491 buf.WriteString(fmt.Sprintf("%d-", v.(int))) 492 } 493 if v, ok := m["icmp_code"]; ok { 494 buf.WriteString(fmt.Sprintf("%d-", v.(int))) 495 } 496 497 return hashcode.String(buf.String()) 498 } 499 500 func getDefaultNetworkAcl(vpc_id string, conn *ec2.EC2) (defaultAcl *ec2.NetworkAcl, err error) { 501 resp, err := conn.DescribeNetworkAcls(&ec2.DescribeNetworkAclsInput{ 502 Filters: []*ec2.Filter{ 503 &ec2.Filter{ 504 Name: aws.String("default"), 505 Values: []*string{aws.String("true")}, 506 }, 507 &ec2.Filter{ 508 Name: aws.String("vpc-id"), 509 Values: []*string{aws.String(vpc_id)}, 510 }, 511 }, 512 }) 513 514 if err != nil { 515 return nil, err 516 } 517 return resp.NetworkAcls[0], nil 518 } 519 520 func findNetworkAclAssociation(subnetId string, conn *ec2.EC2) (networkAclAssociation *ec2.NetworkAclAssociation, err error) { 521 resp, err := conn.DescribeNetworkAcls(&ec2.DescribeNetworkAclsInput{ 522 Filters: []*ec2.Filter{ 523 &ec2.Filter{ 524 Name: aws.String("association.subnet-id"), 525 Values: []*string{aws.String(subnetId)}, 526 }, 527 }, 528 }) 529 530 if err != nil { 531 return nil, err 532 } 533 if resp.NetworkAcls != nil && len(resp.NetworkAcls) > 0 { 534 for _, association := range resp.NetworkAcls[0].Associations { 535 if *association.SubnetId == subnetId { 536 return association, nil 537 } 538 } 539 } 540 return nil, fmt.Errorf("could not find association for subnet: %s ", subnetId) 541 } 542 543 // networkAclEntriesToMapList turns ingress/egress rules read from AWS into a list 544 // of maps. 545 func networkAclEntriesToMapList(networkAcls []*ec2.NetworkAclEntry) []map[string]interface{} { 546 result := make([]map[string]interface{}, 0, len(networkAcls)) 547 for _, entry := range networkAcls { 548 acl := make(map[string]interface{}) 549 acl["rule_no"] = *entry.RuleNumber 550 acl["action"] = *entry.RuleAction 551 acl["cidr_block"] = *entry.CidrBlock 552 553 // The AWS network ACL API only speaks protocol numbers, and 554 // that's all we record. 555 if _, err := strconv.Atoi(*entry.Protocol); err != nil { 556 // We're a protocol name. Look up the number. 557 acl["protocol"] = protocolIntegers()[*entry.Protocol] 558 } else { 559 // We're a protocol number. Pass through. 560 acl["protocol"] = *entry.Protocol 561 } 562 563 acl["protocol"] = *entry.Protocol 564 if entry.PortRange != nil { 565 acl["from_port"] = *entry.PortRange.From 566 acl["to_port"] = *entry.PortRange.To 567 } 568 569 if entry.IcmpTypeCode != nil { 570 acl["icmp_type"] = *entry.IcmpTypeCode.Type 571 acl["icmp_code"] = *entry.IcmpTypeCode.Code 572 } 573 574 result = append(result, acl) 575 } 576 577 return result 578 }