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