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