github.com/jdextraze/terraform@v0.6.17-0.20160511153921-e33847c8a8af/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 if ec2err, ok := err.(awserr.Error); ok { 173 if ec2err.Code() == "InvalidNetworkAclID.NotFound" { 174 log.Printf("[DEBUG] Network ACL (%s) not found", d.Id()) 175 d.SetId("") 176 return nil 177 } 178 } 179 return err 180 } 181 if resp == nil { 182 return nil 183 } 184 185 networkAcl := resp.NetworkAcls[0] 186 var ingressEntries []*ec2.NetworkAclEntry 187 var egressEntries []*ec2.NetworkAclEntry 188 189 // separate the ingress and egress rules 190 for _, e := range networkAcl.Entries { 191 // Skip the default rules added by AWS. They can be neither 192 // configured or deleted by users. 193 if *e.RuleNumber == awsDefaultAclRuleNumber { 194 continue 195 } 196 197 if *e.Egress == true { 198 egressEntries = append(egressEntries, e) 199 } else { 200 ingressEntries = append(ingressEntries, e) 201 } 202 } 203 204 d.Set("vpc_id", networkAcl.VpcId) 205 d.Set("tags", tagsToMap(networkAcl.Tags)) 206 207 var s []string 208 for _, a := range networkAcl.Associations { 209 s = append(s, *a.SubnetId) 210 } 211 sort.Strings(s) 212 if err := d.Set("subnet_ids", s); err != nil { 213 return err 214 } 215 216 if err := d.Set("ingress", networkAclEntriesToMapList(ingressEntries)); err != nil { 217 return err 218 } 219 if err := d.Set("egress", networkAclEntriesToMapList(egressEntries)); err != nil { 220 return err 221 } 222 223 return nil 224 } 225 226 func resourceAwsNetworkAclUpdate(d *schema.ResourceData, meta interface{}) error { 227 conn := meta.(*AWSClient).ec2conn 228 d.Partial(true) 229 230 if d.HasChange("ingress") { 231 err := updateNetworkAclEntries(d, "ingress", conn) 232 if err != nil { 233 return err 234 } 235 } 236 237 if d.HasChange("egress") { 238 err := updateNetworkAclEntries(d, "egress", conn) 239 if err != nil { 240 return err 241 } 242 } 243 244 if d.HasChange("subnet_id") { 245 //associate new subnet with the acl. 246 _, n := d.GetChange("subnet_id") 247 newSubnet := n.(string) 248 association, err := findNetworkAclAssociation(newSubnet, conn) 249 if err != nil { 250 return fmt.Errorf("Failed to update acl %s with subnet %s: %s", d.Id(), newSubnet, err) 251 } 252 _, err = conn.ReplaceNetworkAclAssociation(&ec2.ReplaceNetworkAclAssociationInput{ 253 AssociationId: association.NetworkAclAssociationId, 254 NetworkAclId: aws.String(d.Id()), 255 }) 256 if err != nil { 257 return err 258 } 259 } 260 261 if d.HasChange("subnet_ids") { 262 o, n := d.GetChange("subnet_ids") 263 if o == nil { 264 o = new(schema.Set) 265 } 266 if n == nil { 267 n = new(schema.Set) 268 } 269 270 os := o.(*schema.Set) 271 ns := n.(*schema.Set) 272 273 remove := os.Difference(ns).List() 274 add := ns.Difference(os).List() 275 276 if len(remove) > 0 { 277 // A Network ACL is required for each subnet. In order to disassociate a 278 // subnet from this ACL, we must associate it with the default ACL. 279 defaultAcl, err := getDefaultNetworkAcl(d.Get("vpc_id").(string), conn) 280 if err != nil { 281 return fmt.Errorf("Failed to find Default ACL for VPC %s", d.Get("vpc_id").(string)) 282 } 283 for _, r := range remove { 284 association, err := findNetworkAclAssociation(r.(string), conn) 285 if err != nil { 286 return fmt.Errorf("Failed to find acl association: acl %s with subnet %s: %s", d.Id(), r, err) 287 } 288 log.Printf("DEBUG] Replacing Network Acl Association (%s) with Default Network ACL ID (%s)", *association.NetworkAclAssociationId, *defaultAcl.NetworkAclId) 289 _, err = conn.ReplaceNetworkAclAssociation(&ec2.ReplaceNetworkAclAssociationInput{ 290 AssociationId: association.NetworkAclAssociationId, 291 NetworkAclId: defaultAcl.NetworkAclId, 292 }) 293 if err != nil { 294 return err 295 } 296 } 297 } 298 299 if len(add) > 0 { 300 for _, a := range add { 301 association, err := findNetworkAclAssociation(a.(string), conn) 302 if err != nil { 303 return fmt.Errorf("Failed to find acl association: acl %s with subnet %s: %s", d.Id(), a, err) 304 } 305 _, err = conn.ReplaceNetworkAclAssociation(&ec2.ReplaceNetworkAclAssociationInput{ 306 AssociationId: association.NetworkAclAssociationId, 307 NetworkAclId: aws.String(d.Id()), 308 }) 309 if err != nil { 310 return err 311 } 312 } 313 } 314 315 } 316 317 if err := setTags(conn, d); err != nil { 318 return err 319 } else { 320 d.SetPartial("tags") 321 } 322 323 d.Partial(false) 324 return resourceAwsNetworkAclRead(d, meta) 325 } 326 327 func updateNetworkAclEntries(d *schema.ResourceData, entryType string, conn *ec2.EC2) error { 328 if d.HasChange(entryType) { 329 o, n := d.GetChange(entryType) 330 331 if o == nil { 332 o = new(schema.Set) 333 } 334 if n == nil { 335 n = new(schema.Set) 336 } 337 338 os := o.(*schema.Set) 339 ns := n.(*schema.Set) 340 341 toBeDeleted, err := expandNetworkAclEntries(os.Difference(ns).List(), entryType) 342 if err != nil { 343 return err 344 } 345 for _, remove := range toBeDeleted { 346 // AWS includes default rules with all network ACLs that can be 347 // neither modified nor destroyed. They have a custom rule 348 // number that is out of bounds for any other rule. If we 349 // encounter it, just continue. There's no work to be done. 350 if *remove.RuleNumber == awsDefaultAclRuleNumber { 351 continue 352 } 353 354 // Delete old Acl 355 log.Printf("[DEBUG] Destroying Network ACL Entry number (%d)", int(*remove.RuleNumber)) 356 _, err := conn.DeleteNetworkAclEntry(&ec2.DeleteNetworkAclEntryInput{ 357 NetworkAclId: aws.String(d.Id()), 358 RuleNumber: remove.RuleNumber, 359 Egress: remove.Egress, 360 }) 361 if err != nil { 362 return fmt.Errorf("Error deleting %s entry: %s", entryType, err) 363 } 364 } 365 366 toBeCreated, err := expandNetworkAclEntries(ns.Difference(os).List(), entryType) 367 if err != nil { 368 return err 369 } 370 for _, add := range toBeCreated { 371 // Protocol -1 rules don't store ports in AWS. Thus, they'll always 372 // hash differently when being read out of the API. Force the user 373 // to set from_port and to_port to 0 for these rules, to keep the 374 // hashing consistent. 375 if *add.Protocol == "-1" { 376 to := *add.PortRange.To 377 from := *add.PortRange.From 378 expected := &expectedPortPair{ 379 to_port: 0, 380 from_port: 0, 381 } 382 if ok := validatePorts(to, from, *expected); !ok { 383 return fmt.Errorf( 384 "to_port (%d) and from_port (%d) must both be 0 to use the the 'all' \"-1\" protocol!", 385 to, from) 386 } 387 } 388 389 // AWS mutates the CIDR block into a network implied by the IP and 390 // mask provided. This results in hashing inconsistencies between 391 // the local config file and the state returned by the API. Error 392 // if the user provides a CIDR block with an inappropriate mask 393 if err := validateCIDRBlock(*add.CidrBlock); err != nil { 394 return err 395 } 396 397 // Add new Acl entry 398 _, connErr := conn.CreateNetworkAclEntry(&ec2.CreateNetworkAclEntryInput{ 399 NetworkAclId: aws.String(d.Id()), 400 CidrBlock: add.CidrBlock, 401 Egress: add.Egress, 402 PortRange: add.PortRange, 403 Protocol: add.Protocol, 404 RuleAction: add.RuleAction, 405 RuleNumber: add.RuleNumber, 406 IcmpTypeCode: add.IcmpTypeCode, 407 }) 408 if connErr != nil { 409 return fmt.Errorf("Error creating %s entry: %s", entryType, connErr) 410 } 411 } 412 } 413 return nil 414 } 415 416 func resourceAwsNetworkAclDelete(d *schema.ResourceData, meta interface{}) error { 417 conn := meta.(*AWSClient).ec2conn 418 419 log.Printf("[INFO] Deleting Network Acl: %s", d.Id()) 420 retryErr := resource.Retry(5*time.Minute, func() *resource.RetryError { 421 _, err := conn.DeleteNetworkAcl(&ec2.DeleteNetworkAclInput{ 422 NetworkAclId: aws.String(d.Id()), 423 }) 424 if err != nil { 425 ec2err := err.(awserr.Error) 426 switch ec2err.Code() { 427 case "InvalidNetworkAclID.NotFound": 428 return nil 429 case "DependencyViolation": 430 // In case of dependency violation, we remove the association between subnet and network acl. 431 // This means the subnet is attached to default acl of vpc. 432 var associations []*ec2.NetworkAclAssociation 433 if v, ok := d.GetOk("subnet_id"); ok { 434 435 a, err := findNetworkAclAssociation(v.(string), conn) 436 if err != nil { 437 return resource.NonRetryableError(err) 438 } 439 associations = append(associations, a) 440 } else if v, ok := d.GetOk("subnet_ids"); ok { 441 ids := v.(*schema.Set).List() 442 for _, i := range ids { 443 a, err := findNetworkAclAssociation(i.(string), conn) 444 if err != nil { 445 return resource.NonRetryableError(err) 446 } 447 associations = append(associations, a) 448 } 449 } 450 451 log.Printf("[DEBUG] Replacing network associations for Network ACL (%s): %s", d.Id(), associations) 452 defaultAcl, err := getDefaultNetworkAcl(d.Get("vpc_id").(string), conn) 453 if err != nil { 454 return resource.NonRetryableError(err) 455 } 456 457 for _, a := range associations { 458 log.Printf("DEBUG] Replacing Network Acl Association (%s) with Default Network ACL ID (%s)", *a.NetworkAclAssociationId, *defaultAcl.NetworkAclId) 459 _, replaceErr := conn.ReplaceNetworkAclAssociation(&ec2.ReplaceNetworkAclAssociationInput{ 460 AssociationId: a.NetworkAclAssociationId, 461 NetworkAclId: defaultAcl.NetworkAclId, 462 }) 463 if replaceErr != nil { 464 if replaceEc2err, ok := replaceErr.(awserr.Error); ok { 465 // It's possible that during an attempt to replace this 466 // association, the Subnet in question has already been moved to 467 // another ACL. This can happen if you're destroying a network acl 468 // and simultaneously re-associating it's subnet(s) with another 469 // ACL; Terraform may have already re-associated the subnet(s) by 470 // the time we attempt to destroy them, even between the time we 471 // list them and then try to destroy them. In this case, the 472 // association we're trying to replace will no longer exist and 473 // this call will fail. Here we trap that error and fail 474 // gracefully; the association we tried to replace gone, we trust 475 // someone else has taken ownership. 476 if replaceEc2err.Code() == "InvalidAssociationID.NotFound" { 477 log.Printf("[WARN] Network Association (%s) no longer found; Network Association likely updated or removed externally, removing from state", *a.NetworkAclAssociationId) 478 continue 479 } 480 } 481 log.Printf("[ERR] Non retry-able error in replacing associations for Network ACL (%s): %s", d.Id(), replaceErr) 482 return resource.NonRetryableError(replaceErr) 483 } 484 } 485 return resource.RetryableError(fmt.Errorf("Dependencies found and cleaned up, retrying")) 486 default: 487 // Any other error, we want to quit the retry loop immediately 488 return resource.NonRetryableError(err) 489 } 490 } 491 log.Printf("[Info] Deleted network ACL %s successfully", d.Id()) 492 return nil 493 }) 494 495 if retryErr != nil { 496 return fmt.Errorf("[ERR] Error destroying Network ACL (%s): %s", d.Id(), retryErr) 497 } 498 return nil 499 } 500 501 func resourceAwsNetworkAclEntryHash(v interface{}) int { 502 var buf bytes.Buffer 503 m := v.(map[string]interface{}) 504 buf.WriteString(fmt.Sprintf("%d-", m["from_port"].(int))) 505 buf.WriteString(fmt.Sprintf("%d-", m["to_port"].(int))) 506 buf.WriteString(fmt.Sprintf("%d-", m["rule_no"].(int))) 507 buf.WriteString(fmt.Sprintf("%s-", m["action"].(string))) 508 509 // The AWS network ACL API only speaks protocol numbers, and that's 510 // all we store. Never hash a protocol name. 511 protocol := m["protocol"].(string) 512 if _, err := strconv.Atoi(m["protocol"].(string)); err != nil { 513 // We're a protocol name. Look up the number. 514 buf.WriteString(fmt.Sprintf("%d-", protocolIntegers()[protocol])) 515 } else { 516 // We're a protocol number. Pass the value through. 517 buf.WriteString(fmt.Sprintf("%s-", protocol)) 518 } 519 520 buf.WriteString(fmt.Sprintf("%s-", m["cidr_block"].(string))) 521 522 if v, ok := m["ssl_certificate_id"]; ok { 523 buf.WriteString(fmt.Sprintf("%s-", v.(string))) 524 } 525 526 if v, ok := m["icmp_type"]; ok { 527 buf.WriteString(fmt.Sprintf("%d-", v.(int))) 528 } 529 if v, ok := m["icmp_code"]; ok { 530 buf.WriteString(fmt.Sprintf("%d-", v.(int))) 531 } 532 533 return hashcode.String(buf.String()) 534 } 535 536 func getDefaultNetworkAcl(vpc_id string, conn *ec2.EC2) (defaultAcl *ec2.NetworkAcl, err error) { 537 resp, err := conn.DescribeNetworkAcls(&ec2.DescribeNetworkAclsInput{ 538 Filters: []*ec2.Filter{ 539 &ec2.Filter{ 540 Name: aws.String("default"), 541 Values: []*string{aws.String("true")}, 542 }, 543 &ec2.Filter{ 544 Name: aws.String("vpc-id"), 545 Values: []*string{aws.String(vpc_id)}, 546 }, 547 }, 548 }) 549 550 if err != nil { 551 return nil, err 552 } 553 return resp.NetworkAcls[0], nil 554 } 555 556 func findNetworkAclAssociation(subnetId string, conn *ec2.EC2) (networkAclAssociation *ec2.NetworkAclAssociation, err error) { 557 resp, err := conn.DescribeNetworkAcls(&ec2.DescribeNetworkAclsInput{ 558 Filters: []*ec2.Filter{ 559 &ec2.Filter{ 560 Name: aws.String("association.subnet-id"), 561 Values: []*string{aws.String(subnetId)}, 562 }, 563 }, 564 }) 565 566 if err != nil { 567 return nil, err 568 } 569 if resp.NetworkAcls != nil && len(resp.NetworkAcls) > 0 { 570 for _, association := range resp.NetworkAcls[0].Associations { 571 if *association.SubnetId == subnetId { 572 return association, nil 573 } 574 } 575 } 576 return nil, fmt.Errorf("could not find association for subnet: %s ", subnetId) 577 } 578 579 // networkAclEntriesToMapList turns ingress/egress rules read from AWS into a list 580 // of maps. 581 func networkAclEntriesToMapList(networkAcls []*ec2.NetworkAclEntry) []map[string]interface{} { 582 result := make([]map[string]interface{}, 0, len(networkAcls)) 583 for _, entry := range networkAcls { 584 acl := make(map[string]interface{}) 585 acl["rule_no"] = *entry.RuleNumber 586 acl["action"] = *entry.RuleAction 587 acl["cidr_block"] = *entry.CidrBlock 588 589 // The AWS network ACL API only speaks protocol numbers, and 590 // that's all we record. 591 if _, err := strconv.Atoi(*entry.Protocol); err != nil { 592 // We're a protocol name. Look up the number. 593 acl["protocol"] = protocolIntegers()[*entry.Protocol] 594 } else { 595 // We're a protocol number. Pass through. 596 acl["protocol"] = *entry.Protocol 597 } 598 599 acl["protocol"] = *entry.Protocol 600 if entry.PortRange != nil { 601 acl["from_port"] = *entry.PortRange.From 602 acl["to_port"] = *entry.PortRange.To 603 } 604 605 if entry.IcmpTypeCode != nil { 606 acl["icmp_type"] = *entry.IcmpTypeCode.Type 607 acl["icmp_code"] = *entry.IcmpTypeCode.Code 608 } 609 610 result = append(result, acl) 611 } 612 613 return result 614 }