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