github.com/anfernee/terraform@v0.6.16-0.20160430000239-06e5085a92f2/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 _, 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 if d.HasChange(entryType) { 328 o, n := d.GetChange(entryType) 329 330 if o == nil { 331 o = new(schema.Set) 332 } 333 if n == nil { 334 n = new(schema.Set) 335 } 336 337 os := o.(*schema.Set) 338 ns := n.(*schema.Set) 339 340 toBeDeleted, err := expandNetworkAclEntries(os.Difference(ns).List(), entryType) 341 if err != nil { 342 return err 343 } 344 for _, remove := range toBeDeleted { 345 // AWS includes default rules with all network ACLs that can be 346 // neither modified nor destroyed. They have a custom rule 347 // number that is out of bounds for any other rule. If we 348 // encounter it, just continue. There's no work to be done. 349 if *remove.RuleNumber == awsDefaultAclRuleNumber { 350 continue 351 } 352 353 // Delete old Acl 354 log.Printf("[DEBUG] Destroying Network ACL Entry number (%d)", int(*remove.RuleNumber)) 355 _, err := conn.DeleteNetworkAclEntry(&ec2.DeleteNetworkAclEntryInput{ 356 NetworkAclId: aws.String(d.Id()), 357 RuleNumber: remove.RuleNumber, 358 Egress: remove.Egress, 359 }) 360 if err != nil { 361 return fmt.Errorf("Error deleting %s entry: %s", entryType, err) 362 } 363 } 364 365 toBeCreated, err := expandNetworkAclEntries(ns.Difference(os).List(), entryType) 366 if err != nil { 367 return err 368 } 369 for _, add := range toBeCreated { 370 // Protocol -1 rules don't store ports in AWS. Thus, they'll always 371 // hash differently when being read out of the API. Force the user 372 // to set from_port and to_port to 0 for these rules, to keep the 373 // hashing consistent. 374 if *add.Protocol == "-1" { 375 to := *add.PortRange.To 376 from := *add.PortRange.From 377 expected := &expectedPortPair{ 378 to_port: 0, 379 from_port: 0, 380 } 381 if ok := validatePorts(to, from, *expected); !ok { 382 return fmt.Errorf( 383 "to_port (%d) and from_port (%d) must both be 0 to use the the 'all' \"-1\" protocol!", 384 to, from) 385 } 386 } 387 388 // AWS mutates the CIDR block into a network implied by the IP and 389 // mask provided. This results in hashing inconsistencies between 390 // the local config file and the state returned by the API. Error 391 // if the user provides a CIDR block with an inappropriate mask 392 if err := validateCIDRBlock(*add.CidrBlock); err != nil { 393 return err 394 } 395 396 // Add new Acl entry 397 _, connErr := conn.CreateNetworkAclEntry(&ec2.CreateNetworkAclEntryInput{ 398 NetworkAclId: aws.String(d.Id()), 399 CidrBlock: add.CidrBlock, 400 Egress: add.Egress, 401 PortRange: add.PortRange, 402 Protocol: add.Protocol, 403 RuleAction: add.RuleAction, 404 RuleNumber: add.RuleNumber, 405 IcmpTypeCode: add.IcmpTypeCode, 406 }) 407 if connErr != nil { 408 return fmt.Errorf("Error creating %s entry: %s", entryType, connErr) 409 } 410 } 411 } 412 return nil 413 } 414 415 func resourceAwsNetworkAclDelete(d *schema.ResourceData, meta interface{}) error { 416 conn := meta.(*AWSClient).ec2conn 417 418 log.Printf("[INFO] Deleting Network Acl: %s", d.Id()) 419 retryErr := resource.Retry(5*time.Minute, func() *resource.RetryError { 420 _, err := conn.DeleteNetworkAcl(&ec2.DeleteNetworkAclInput{ 421 NetworkAclId: aws.String(d.Id()), 422 }) 423 if err != nil { 424 ec2err := err.(awserr.Error) 425 switch ec2err.Code() { 426 case "InvalidNetworkAclID.NotFound": 427 return nil 428 case "DependencyViolation": 429 // In case of dependency violation, we remove the association between subnet and network acl. 430 // This means the subnet is attached to default acl of vpc. 431 var associations []*ec2.NetworkAclAssociation 432 if v, ok := d.GetOk("subnet_id"); ok { 433 434 a, err := findNetworkAclAssociation(v.(string), conn) 435 if err != nil { 436 return resource.NonRetryableError(err) 437 } 438 associations = append(associations, a) 439 } else if v, ok := d.GetOk("subnet_ids"); ok { 440 ids := v.(*schema.Set).List() 441 for _, i := range ids { 442 a, err := findNetworkAclAssociation(i.(string), conn) 443 if err != nil { 444 return resource.NonRetryableError(err) 445 } 446 associations = append(associations, a) 447 } 448 } 449 450 log.Printf("[DEBUG] Replacing network associations for Network ACL (%s): %s", d.Id(), associations) 451 defaultAcl, err := getDefaultNetworkAcl(d.Get("vpc_id").(string), conn) 452 if err != nil { 453 return resource.NonRetryableError(err) 454 } 455 456 for _, a := range associations { 457 _, replaceErr := conn.ReplaceNetworkAclAssociation(&ec2.ReplaceNetworkAclAssociationInput{ 458 AssociationId: a.NetworkAclAssociationId, 459 NetworkAclId: defaultAcl.NetworkAclId, 460 }) 461 if replaceErr != nil { 462 log.Printf("[ERR] Non retryable error in replacing associtions for Network ACL (%s): %s", d.Id(), replaceErr) 463 return resource.NonRetryableError(replaceErr) 464 } 465 } 466 return resource.RetryableError(fmt.Errorf("Dependencies found and cleaned up, retrying")) 467 default: 468 // Any other error, we want to quit the retry loop immediately 469 return resource.NonRetryableError(err) 470 } 471 } 472 log.Printf("[Info] Deleted network ACL %s successfully", d.Id()) 473 return nil 474 }) 475 476 if retryErr != nil { 477 return fmt.Errorf("[ERR] Error destroying Network ACL (%s): %s", d.Id(), retryErr) 478 } 479 return nil 480 } 481 482 func resourceAwsNetworkAclEntryHash(v interface{}) int { 483 var buf bytes.Buffer 484 m := v.(map[string]interface{}) 485 buf.WriteString(fmt.Sprintf("%d-", m["from_port"].(int))) 486 buf.WriteString(fmt.Sprintf("%d-", m["to_port"].(int))) 487 buf.WriteString(fmt.Sprintf("%d-", m["rule_no"].(int))) 488 buf.WriteString(fmt.Sprintf("%s-", m["action"].(string))) 489 490 // The AWS network ACL API only speaks protocol numbers, and that's 491 // all we store. Never hash a protocol name. 492 protocol := m["protocol"].(string) 493 if _, err := strconv.Atoi(m["protocol"].(string)); err != nil { 494 // We're a protocol name. Look up the number. 495 buf.WriteString(fmt.Sprintf("%d-", protocolIntegers()[protocol])) 496 } else { 497 // We're a protocol number. Pass the value through. 498 buf.WriteString(fmt.Sprintf("%s-", protocol)) 499 } 500 501 buf.WriteString(fmt.Sprintf("%s-", m["cidr_block"].(string))) 502 503 if v, ok := m["ssl_certificate_id"]; ok { 504 buf.WriteString(fmt.Sprintf("%s-", v.(string))) 505 } 506 507 if v, ok := m["icmp_type"]; ok { 508 buf.WriteString(fmt.Sprintf("%d-", v.(int))) 509 } 510 if v, ok := m["icmp_code"]; ok { 511 buf.WriteString(fmt.Sprintf("%d-", v.(int))) 512 } 513 514 return hashcode.String(buf.String()) 515 } 516 517 func getDefaultNetworkAcl(vpc_id string, conn *ec2.EC2) (defaultAcl *ec2.NetworkAcl, err error) { 518 resp, err := conn.DescribeNetworkAcls(&ec2.DescribeNetworkAclsInput{ 519 Filters: []*ec2.Filter{ 520 &ec2.Filter{ 521 Name: aws.String("default"), 522 Values: []*string{aws.String("true")}, 523 }, 524 &ec2.Filter{ 525 Name: aws.String("vpc-id"), 526 Values: []*string{aws.String(vpc_id)}, 527 }, 528 }, 529 }) 530 531 if err != nil { 532 return nil, err 533 } 534 return resp.NetworkAcls[0], nil 535 } 536 537 func findNetworkAclAssociation(subnetId string, conn *ec2.EC2) (networkAclAssociation *ec2.NetworkAclAssociation, err error) { 538 resp, err := conn.DescribeNetworkAcls(&ec2.DescribeNetworkAclsInput{ 539 Filters: []*ec2.Filter{ 540 &ec2.Filter{ 541 Name: aws.String("association.subnet-id"), 542 Values: []*string{aws.String(subnetId)}, 543 }, 544 }, 545 }) 546 547 if err != nil { 548 return nil, err 549 } 550 if resp.NetworkAcls != nil && len(resp.NetworkAcls) > 0 { 551 for _, association := range resp.NetworkAcls[0].Associations { 552 if *association.SubnetId == subnetId { 553 return association, nil 554 } 555 } 556 } 557 return nil, fmt.Errorf("could not find association for subnet: %s ", subnetId) 558 } 559 560 // networkAclEntriesToMapList turns ingress/egress rules read from AWS into a list 561 // of maps. 562 func networkAclEntriesToMapList(networkAcls []*ec2.NetworkAclEntry) []map[string]interface{} { 563 result := make([]map[string]interface{}, 0, len(networkAcls)) 564 for _, entry := range networkAcls { 565 acl := make(map[string]interface{}) 566 acl["rule_no"] = *entry.RuleNumber 567 acl["action"] = *entry.RuleAction 568 acl["cidr_block"] = *entry.CidrBlock 569 570 // The AWS network ACL API only speaks protocol numbers, and 571 // that's all we record. 572 if _, err := strconv.Atoi(*entry.Protocol); err != nil { 573 // We're a protocol name. Look up the number. 574 acl["protocol"] = protocolIntegers()[*entry.Protocol] 575 } else { 576 // We're a protocol number. Pass through. 577 acl["protocol"] = *entry.Protocol 578 } 579 580 acl["protocol"] = *entry.Protocol 581 if entry.PortRange != nil { 582 acl["from_port"] = *entry.PortRange.From 583 acl["to_port"] = *entry.PortRange.To 584 } 585 586 if entry.IcmpTypeCode != nil { 587 acl["icmp_type"] = *entry.IcmpTypeCode.Type 588 acl["icmp_code"] = *entry.IcmpTypeCode.Code 589 } 590 591 result = append(result, acl) 592 } 593 594 return result 595 }