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