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