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