github.com/andresvia/terraform@v0.6.15-0.20160412045437-d51c75946785/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 == 32767 { 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 _, err = conn.ReplaceNetworkAclAssociation(&ec2.ReplaceNetworkAclAssociationInput{ 289 AssociationId: association.NetworkAclAssociationId, 290 NetworkAclId: defaultAcl.NetworkAclId, 291 }) 292 if err != nil { 293 return err 294 } 295 } 296 } 297 298 if len(add) > 0 { 299 for _, a := range add { 300 association, err := findNetworkAclAssociation(a.(string), conn) 301 if err != nil { 302 return fmt.Errorf("Failed to find acl association: acl %s with subnet %s: %s", d.Id(), a, err) 303 } 304 _, err = conn.ReplaceNetworkAclAssociation(&ec2.ReplaceNetworkAclAssociationInput{ 305 AssociationId: association.NetworkAclAssociationId, 306 NetworkAclId: aws.String(d.Id()), 307 }) 308 if err != nil { 309 return err 310 } 311 } 312 } 313 314 } 315 316 if err := setTags(conn, d); err != nil { 317 return err 318 } else { 319 d.SetPartial("tags") 320 } 321 322 d.Partial(false) 323 return resourceAwsNetworkAclRead(d, meta) 324 } 325 326 func updateNetworkAclEntries(d *schema.ResourceData, entryType string, conn *ec2.EC2) error { 327 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 347 // AWS includes default rules with all network ACLs that can be 348 // neither modified nor destroyed. They have a custom rule 349 // number that is out of bounds for any other rule. If we 350 // encounter it, just continue. There's no work to be done. 351 if *remove.RuleNumber == 32767 { 352 continue 353 } 354 355 // Delete old Acl 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 _, replaceErr := conn.ReplaceNetworkAclAssociation(&ec2.ReplaceNetworkAclAssociationInput{ 459 AssociationId: a.NetworkAclAssociationId, 460 NetworkAclId: defaultAcl.NetworkAclId, 461 }) 462 if replaceErr != nil { 463 log.Printf("[ERR] Non retryable error in replacing associtions for Network ACL (%s): %s", d.Id(), replaceErr) 464 return resource.NonRetryableError(replaceErr) 465 } 466 } 467 return resource.RetryableError(fmt.Errorf("Dependencies found and cleaned up, retrying")) 468 default: 469 // Any other error, we want to quit the retry loop immediately 470 return resource.NonRetryableError(err) 471 } 472 } 473 log.Printf("[Info] Deleted network ACL %s successfully", d.Id()) 474 return nil 475 }) 476 477 if retryErr != nil { 478 return fmt.Errorf("[ERR] Error destroying Network ACL (%s): %s", d.Id(), retryErr) 479 } 480 return nil 481 } 482 483 func resourceAwsNetworkAclEntryHash(v interface{}) int { 484 var buf bytes.Buffer 485 m := v.(map[string]interface{}) 486 buf.WriteString(fmt.Sprintf("%d-", m["from_port"].(int))) 487 buf.WriteString(fmt.Sprintf("%d-", m["to_port"].(int))) 488 buf.WriteString(fmt.Sprintf("%d-", m["rule_no"].(int))) 489 buf.WriteString(fmt.Sprintf("%s-", m["action"].(string))) 490 491 // The AWS network ACL API only speaks protocol numbers, and that's 492 // all we store. Never hash a protocol name. 493 protocol := m["protocol"].(string) 494 if _, err := strconv.Atoi(m["protocol"].(string)); err != nil { 495 // We're a protocol name. Look up the number. 496 buf.WriteString(fmt.Sprintf("%d-", protocolIntegers()[protocol])) 497 } else { 498 // We're a protocol number. Pass the value through. 499 buf.WriteString(fmt.Sprintf("%s-", protocol)) 500 } 501 502 buf.WriteString(fmt.Sprintf("%s-", m["cidr_block"].(string))) 503 504 if v, ok := m["ssl_certificate_id"]; ok { 505 buf.WriteString(fmt.Sprintf("%s-", v.(string))) 506 } 507 508 if v, ok := m["icmp_type"]; ok { 509 buf.WriteString(fmt.Sprintf("%d-", v.(int))) 510 } 511 if v, ok := m["icmp_code"]; ok { 512 buf.WriteString(fmt.Sprintf("%d-", v.(int))) 513 } 514 515 return hashcode.String(buf.String()) 516 } 517 518 func getDefaultNetworkAcl(vpc_id string, conn *ec2.EC2) (defaultAcl *ec2.NetworkAcl, err error) { 519 resp, err := conn.DescribeNetworkAcls(&ec2.DescribeNetworkAclsInput{ 520 Filters: []*ec2.Filter{ 521 &ec2.Filter{ 522 Name: aws.String("default"), 523 Values: []*string{aws.String("true")}, 524 }, 525 &ec2.Filter{ 526 Name: aws.String("vpc-id"), 527 Values: []*string{aws.String(vpc_id)}, 528 }, 529 }, 530 }) 531 532 if err != nil { 533 return nil, err 534 } 535 return resp.NetworkAcls[0], nil 536 } 537 538 func findNetworkAclAssociation(subnetId string, conn *ec2.EC2) (networkAclAssociation *ec2.NetworkAclAssociation, err error) { 539 resp, err := conn.DescribeNetworkAcls(&ec2.DescribeNetworkAclsInput{ 540 Filters: []*ec2.Filter{ 541 &ec2.Filter{ 542 Name: aws.String("association.subnet-id"), 543 Values: []*string{aws.String(subnetId)}, 544 }, 545 }, 546 }) 547 548 if err != nil { 549 return nil, err 550 } 551 if resp.NetworkAcls != nil && len(resp.NetworkAcls) > 0 { 552 for _, association := range resp.NetworkAcls[0].Associations { 553 if *association.SubnetId == subnetId { 554 return association, nil 555 } 556 } 557 } 558 return nil, fmt.Errorf("could not find association for subnet: %s ", subnetId) 559 } 560 561 // networkAclEntriesToMapList turns ingress/egress rules read from AWS into a list 562 // of maps. 563 func networkAclEntriesToMapList(networkAcls []*ec2.NetworkAclEntry) []map[string]interface{} { 564 result := make([]map[string]interface{}, 0, len(networkAcls)) 565 for _, entry := range networkAcls { 566 acl := make(map[string]interface{}) 567 acl["rule_no"] = *entry.RuleNumber 568 acl["action"] = *entry.RuleAction 569 acl["cidr_block"] = *entry.CidrBlock 570 571 // The AWS network ACL API only speaks protocol numbers, and 572 // that's all we record. 573 if _, err := strconv.Atoi(*entry.Protocol); err != nil { 574 // We're a protocol name. Look up the number. 575 acl["protocol"] = protocolIntegers()[*entry.Protocol] 576 } else { 577 // We're a protocol number. Pass through. 578 acl["protocol"] = *entry.Protocol 579 } 580 581 acl["protocol"] = *entry.Protocol 582 if entry.PortRange != nil { 583 acl["from_port"] = *entry.PortRange.From 584 acl["to_port"] = *entry.PortRange.To 585 } 586 587 if entry.IcmpTypeCode != nil { 588 acl["icmp_type"] = *entry.IcmpTypeCode.Type 589 acl["icmp_code"] = *entry.IcmpTypeCode.Code 590 } 591 592 result = append(result, acl) 593 } 594 595 return result 596 }