From 37c49d9124c69ad195436d76568eda13631e8ada Mon Sep 17 00:00:00 2001 From: Roberth Kulbin Date: Thu, 9 Jul 2020 10:58:24 +0100 Subject: [PATCH 1/6] d/aws_prefix_list: fix name filter override and lack of error when matching multiple prefix lists --- aws/data_source_aws_prefix_list.go | 19 ++++++----- aws/data_source_aws_prefix_list_test.go | 45 +++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/aws/data_source_aws_prefix_list.go b/aws/data_source_aws_prefix_list.go index 82c8a2ae37f..7a1c6f57da6 100644 --- a/aws/data_source_aws_prefix_list.go +++ b/aws/data_source_aws_prefix_list.go @@ -45,19 +45,22 @@ func dataSourceAwsPrefixListRead(d *schema.ResourceData, meta interface{}) error if prefixListID := d.Get("prefix_list_id"); prefixListID != "" { req.PrefixListIds = aws.StringSlice([]string{prefixListID.(string)}) } - req.Filters = buildEC2AttributeFilterList( - map[string]string{ - "prefix-list-name": d.Get("name").(string), - }, - ) + if prefixListName := d.Get("name"); prefixListName.(string) != "" { + req.Filters = append(req.Filters, &ec2.Filter{ + Name: aws.String("prefix-list-name"), + Values: aws.StringSlice([]string{prefixListName.(string)}), + }) + } log.Printf("[DEBUG] Reading Prefix List: %s", req) resp, err := conn.DescribePrefixLists(req) - if err != nil { + switch { + case err != nil: return err - } - if resp == nil || len(resp.PrefixLists) == 0 { + case resp == nil || len(resp.PrefixLists) == 0: return fmt.Errorf("no matching prefix list found; the prefix list ID or name may be invalid or not exist in the current region") + case len(resp.PrefixLists) > 1: + return fmt.Errorf("more than one prefix list matched the given set of criteria") } pl := resp.PrefixLists[0] diff --git a/aws/data_source_aws_prefix_list_test.go b/aws/data_source_aws_prefix_list_test.go index e74778a01b3..ae351ae06b0 100644 --- a/aws/data_source_aws_prefix_list_test.go +++ b/aws/data_source_aws_prefix_list_test.go @@ -2,6 +2,7 @@ package aws import ( "fmt" + "regexp" "strconv" "testing" @@ -98,3 +99,47 @@ data "aws_prefix_list" "s3_by_id" { } } ` + +func TestAccDataSourceAwsPrefixList_matchesTooMany(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsPrefixListConfig_matchesTooMany, + ExpectError: regexp.MustCompile(`more than one prefix list matched the given set of criteria`), + }, + }, + }) +} + +const testAccDataSourceAwsPrefixListConfig_matchesTooMany = ` +data "aws_prefix_list" "test" {} +` + +func TestAccDataSourceAwsPrefixList_nameDoesNotOverrideFilter(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + // The vanilla DescribePrefixLists API only supports filtering by + // id and name. In this case, the `name` attribute and `prefix-list-id` + // filter have been set up such that they conflict, thus proving + // that both criteria took effect. + Config: testAccDataSourceAwsPrefixListConfig_nameDoesNotOverrideFilter, + ExpectError: regexp.MustCompile(`no matching prefix list found`), + }, + }, + }) +} + +const testAccDataSourceAwsPrefixListConfig_nameDoesNotOverrideFilter = ` +data "aws_prefix_list" "test" { + name = "com.amazonaws.us-west-2.s3" + filter { + name = "prefix-list-id" + values = ["pl-00a54069"] # com.amazonaws.us-west-2.dynamodb + } +} +` From f259a03f79cd854d26dbfbb5e0c03a81718d4f54 Mon Sep 17 00:00:00 2001 From: Roberth Kulbin Date: Thu, 9 Jul 2020 10:59:07 +0100 Subject: [PATCH 2/6] d/aws_prefix_list: sort cidrs --- aws/data_source_aws_prefix_list.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/aws/data_source_aws_prefix_list.go b/aws/data_source_aws_prefix_list.go index 7a1c6f57da6..54adc054b89 100644 --- a/aws/data_source_aws_prefix_list.go +++ b/aws/data_source_aws_prefix_list.go @@ -3,6 +3,7 @@ package aws import ( "fmt" "log" + "sort" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" @@ -68,10 +69,8 @@ func dataSourceAwsPrefixListRead(d *schema.ResourceData, meta interface{}) error d.SetId(*pl.PrefixListId) d.Set("name", pl.PrefixListName) - cidrs := make([]string, len(pl.Cidrs)) - for i, v := range pl.Cidrs { - cidrs[i] = *v - } + cidrs := aws.StringValueSlice(pl.Cidrs) + sort.Strings(cidrs) d.Set("cidr_blocks", cidrs) return nil From 3d2ca8e9678e9f5b781335385c440a1671a1283f Mon Sep 17 00:00:00 2001 From: Roberth Kulbin Date: Thu, 9 Jul 2020 11:00:11 +0100 Subject: [PATCH 3/6] d/aws_prefix_list: fix doc type and add example about finding a prefix list by name --- website/docs/d/prefix_list.html.markdown | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/website/docs/d/prefix_list.html.markdown b/website/docs/d/prefix_list.html.markdown index 0caeb0809e8..b2b5c8ff356 100644 --- a/website/docs/d/prefix_list.html.markdown +++ b/website/docs/d/prefix_list.html.markdown @@ -1,7 +1,7 @@ --- subcategory: "VPC" layout: "aws" -page_title: "AWS: aws_prefix-list" +page_title: "AWS: aws_prefix_list" description: |- Provides details about a specific prefix list --- @@ -44,6 +44,15 @@ resource "aws_network_acl_rule" "private_s3" { } ``` +### Find the regional DynamoDB prefix list + +```hcl +data "aws_region" "current" {} +data "aws_prefix_list" "dynamo" { + name = "com.amazonaws.${data.aws_region.current.name}.dynamodb" +} +``` + ### Filter ```hcl From a35c88c0fc3b64d97b6f99d22455981a7829d7f7 Mon Sep 17 00:00:00 2001 From: Roberth Kulbin Date: Thu, 9 Jul 2020 11:16:47 +0100 Subject: [PATCH 4/6] d/aws_prefix_list: error check when setting cidr_blocks --- aws/data_source_aws_prefix_list.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aws/data_source_aws_prefix_list.go b/aws/data_source_aws_prefix_list.go index 54adc054b89..fdb0fe24a39 100644 --- a/aws/data_source_aws_prefix_list.go +++ b/aws/data_source_aws_prefix_list.go @@ -71,7 +71,9 @@ func dataSourceAwsPrefixListRead(d *schema.ResourceData, meta interface{}) error cidrs := aws.StringValueSlice(pl.Cidrs) sort.Strings(cidrs) - d.Set("cidr_blocks", cidrs) + if err := d.Set("cidr_blocks", cidrs); err != nil { + return fmt.Errorf("failed to set cidr blocks of prefix list %s: %s", d.Id(), err) + } return nil } From 354559c26950f9b45f9e4497d4ce055c3e226703 Mon Sep 17 00:00:00 2001 From: Roberth Kulbin Date: Wed, 1 Jul 2020 15:19:02 +0100 Subject: [PATCH 5/6] r/aws_prefix_list: new resource --- aws/provider.go | 2 + aws/resource_aws_prefix_list.go | 476 +++++++++++ aws/resource_aws_prefix_list_entry.go | 287 +++++++ aws/resource_aws_prefix_list_entry_test.go | 524 ++++++++++++ aws/resource_aws_prefix_list_test.go | 754 ++++++++++++++++++ website/aws.erb | 6 + website/docs/r/prefix_list.html.markdown | 85 ++ .../docs/r/prefix_list_entry.html.markdown | 66 ++ website/docs/r/security_group.html.markdown | 11 +- .../docs/r/security_group_rule.html.markdown | 10 +- 10 files changed, 2213 insertions(+), 8 deletions(-) create mode 100644 aws/resource_aws_prefix_list.go create mode 100644 aws/resource_aws_prefix_list_entry.go create mode 100644 aws/resource_aws_prefix_list_entry_test.go create mode 100644 aws/resource_aws_prefix_list_test.go create mode 100644 website/docs/r/prefix_list.html.markdown create mode 100644 website/docs/r/prefix_list_entry.html.markdown diff --git a/aws/provider.go b/aws/provider.go index 05d8a757b13..dee0c1ebf60 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -744,6 +744,8 @@ func Provider() terraform.ResourceProvider { "aws_organizations_policy_attachment": resourceAwsOrganizationsPolicyAttachment(), "aws_organizations_organizational_unit": resourceAwsOrganizationsOrganizationalUnit(), "aws_placement_group": resourceAwsPlacementGroup(), + "aws_prefix_list": resourceAwsPrefixList(), + "aws_prefix_list_entry": resourceAwsPrefixListEntry(), "aws_proxy_protocol_policy": resourceAwsProxyProtocolPolicy(), "aws_qldb_ledger": resourceAwsQLDBLedger(), "aws_quicksight_group": resourceAwsQuickSightGroup(), diff --git a/aws/resource_aws_prefix_list.go b/aws/resource_aws_prefix_list.go new file mode 100644 index 00000000000..e62392de67f --- /dev/null +++ b/aws/resource_aws_prefix_list.go @@ -0,0 +1,476 @@ +package aws + +import ( + "errors" + "fmt" + "log" + "sort" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" +) + +var ( + awsPrefixListEntrySetHashFunc = schema.HashResource(prefixListEntrySchema()) +) + +func resourceAwsPrefixList() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsPrefixListCreate, + Read: resourceAwsPrefixListRead, + Update: resourceAwsPrefixListUpdate, + Delete: resourceAwsPrefixListDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "address_family": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice( + []string{"IPv4", "IPv6"}, + false), + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "entry": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + ConfigMode: schema.SchemaConfigModeAttr, + Elem: prefixListEntrySchema(), + Set: awsPrefixListEntrySetHashFunc, + }, + "max_entries": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + ValidateFunc: validation.IntAtLeast(1), + }, + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + "owner_id": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchema(), + }, + } +} + +func prefixListEntrySchema() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cidr_block": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.IsCIDR, + }, + "description": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 255), + }, + }, + } +} + +func resourceAwsPrefixListCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + input := ec2.CreateManagedPrefixListInput{} + + input.AddressFamily = aws.String(d.Get("address_family").(string)) + + if v, ok := d.GetOk("entry"); ok { + input.Entries = expandAddPrefixListEntries(v) + } + + input.MaxEntries = aws.Int64(int64(d.Get("max_entries").(int))) + input.PrefixListName = aws.String(d.Get("name").(string)) + + if v, ok := d.GetOk("tags"); ok { + input.TagSpecifications = ec2TagSpecificationsFromMap( + v.(map[string]interface{}), + "prefix-list") // no ec2.ResourceTypePrefixList as of 01/07/20 + } + + output, err := conn.CreateManagedPrefixList(&input) + if err != nil { + return fmt.Errorf("failed to create managed prefix list: %v", err) + } + + id := aws.StringValue(output.PrefixList.PrefixListId) + + log.Printf("[INFO] Created Managed Prefix List %s (%s)", d.Get("name").(string), id) + + if err := waitUntilAwsManagedPrefixListSettled(id, conn, d.Timeout(schema.TimeoutCreate)); err != nil { + return fmt.Errorf("prefix list %s did not settle after create: %s", id, err) + } + + d.SetId(id) + + return resourceAwsPrefixListRead(d, meta) +} + +func resourceAwsPrefixListRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + id := d.Id() + + pl, ok, err := getManagedPrefixList(id, conn) + switch { + case err != nil: + return err + case !ok: + log.Printf("[WARN] Managed Prefix List %s not found; removing from state.", id) + d.SetId("") + return nil + } + + d.Set("address_family", pl.AddressFamily) + d.Set("arn", pl.PrefixListArn) + + entries, err := getPrefixListEntries(id, conn, 0) + if err != nil { + return err + } + + if err := d.Set("entry", flattenPrefixListEntries(entries)); err != nil { + return fmt.Errorf("error setting attribute entry of managed prefix list %s: %s", id, err) + } + + d.Set("max_entries", pl.MaxEntries) + d.Set("name", pl.PrefixListName) + d.Set("owner_id", pl.OwnerId) + + if err := d.Set("tags", keyvaluetags.Ec2KeyValueTags(pl.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error settings attribute tags of managed prefix list %s: %s", id, err) + } + + return nil +} + +func resourceAwsPrefixListUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + id := d.Id() + modifyPrefixList := false + + input := ec2.ModifyManagedPrefixListInput{} + + input.PrefixListId = aws.String(id) + + if d.HasChange("name") { + input.PrefixListName = aws.String(d.Get("name").(string)) + modifyPrefixList = true + } + + if d.HasChange("entry") { + pl, ok, err := getManagedPrefixList(id, conn) + switch { + case err != nil: + return err + case !ok: + return &resource.NotFoundError{} + } + + currentVersion := aws.Int64Value(pl.Version) + + oldEntries, err := getPrefixListEntries(id, conn, currentVersion) + if err != nil { + return err + } + + newEntries := expandAddPrefixListEntries(d.Get("entry")) + adds, removes := computePrefixListEntriesModification(oldEntries, newEntries) + + if len(adds) > 0 || len(removes) > 0 { + if len(adds) > 0 { + // the Modify API doesn't like empty lists + input.AddEntries = adds + } + + if len(removes) > 0 { + // the Modify API doesn't like empty lists + input.RemoveEntries = removes + } + + input.CurrentVersion = aws.Int64(currentVersion) + modifyPrefixList = true + } + } + + if modifyPrefixList { + log.Printf("[INFO] modifying managed prefix list %s...", id) + + switch _, err := conn.ModifyManagedPrefixList(&input); { + case isAWSErr(err, "PrefixListVersionMismatch", "prefix list has the incorrect version number"): + return fmt.Errorf("failed to modify managed prefix list %s: conflicting change", id) + case err != nil: + return fmt.Errorf("failed to modify managed prefix list %s: %s", id, err) + } + + if err := waitUntilAwsManagedPrefixListSettled(id, conn, d.Timeout(schema.TimeoutUpdate)); err != nil { + return fmt.Errorf("prefix list did not settle after update: %s", err) + } + } + + if d.HasChange("tags") { + before, after := d.GetChange("tags") + if err := keyvaluetags.Ec2UpdateTags(conn, id, before, after); err != nil { + return fmt.Errorf("failed to update tags of managed prefix list %s: %s", id, err) + } + } + + return resourceAwsPrefixListRead(d, meta) +} + +func resourceAwsPrefixListDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + id := d.Id() + + input := ec2.DeleteManagedPrefixListInput{ + PrefixListId: aws.String(id), + } + + err := resource.Retry(d.Timeout(schema.TimeoutDelete), func() *resource.RetryError { + _, err := conn.DeleteManagedPrefixList(&input) + switch { + case isManagedPrefixListModificationConflictErr(err): + return resource.RetryableError(err) + case isAWSErr(err, "InvalidPrefixListID.NotFound", ""): + log.Printf("[WARN] managed prefix list %s has already been deleted", id) + return nil + case err != nil: + return resource.NonRetryableError(err) + } + + return nil + }) + + if isResourceTimeoutError(err) { + _, err = conn.DeleteManagedPrefixList(&input) + } + + if err != nil { + return fmt.Errorf("failed to delete managed prefix list %s: %s", id, err) + } + + if err := waitUntilAwsManagedPrefixListSettled(id, conn, d.Timeout(schema.TimeoutDelete)); err != nil { + return fmt.Errorf("prefix list %s did not settle after delete: %s", id, err) + } + + return nil +} + +func expandAddPrefixListEntries(input interface{}) []*ec2.AddPrefixListEntry { + if input == nil { + return nil + } + + list := input.(*schema.Set).List() + result := make([]*ec2.AddPrefixListEntry, 0, len(list)) + + for _, entry := range list { + m := entry.(map[string]interface{}) + + output := ec2.AddPrefixListEntry{} + + output.Cidr = aws.String(m["cidr_block"].(string)) + + if v, ok := m["description"]; ok { + output.Description = aws.String(v.(string)) + } + + result = append(result, &output) + } + + return result +} + +func flattenPrefixListEntries(entries []*ec2.PrefixListEntry) *schema.Set { + list := make([]interface{}, 0, len(entries)) + + for _, entry := range entries { + m := make(map[string]interface{}, 2) + m["cidr_block"] = aws.StringValue(entry.Cidr) + + if entry.Description != nil { + m["description"] = aws.StringValue(entry.Description) + } + + list = append(list, m) + } + + return schema.NewSet(awsPrefixListEntrySetHashFunc, list) +} + +func getManagedPrefixList( + id string, + conn *ec2.EC2, +) (*ec2.ManagedPrefixList, bool, error) { + input := ec2.DescribeManagedPrefixListsInput{ + PrefixListIds: aws.StringSlice([]string{id}), + } + + output, err := conn.DescribeManagedPrefixLists(&input) + switch { + case isAWSErr(err, "InvalidPrefixListID.NotFound", ""): + return nil, false, nil + case err != nil: + return nil, false, fmt.Errorf("describe managed prefix list %s: %v", id, err) + case len(output.PrefixLists) != 1: + return nil, false, nil + } + + return output.PrefixLists[0], true, nil +} + +func getPrefixListEntries( + id string, + conn *ec2.EC2, + version int64, +) ([]*ec2.PrefixListEntry, error) { + input := ec2.GetManagedPrefixListEntriesInput{ + PrefixListId: aws.String(id), + } + + if version > 0 { + input.TargetVersion = aws.Int64(version) + } + + result := []*ec2.PrefixListEntry(nil) + switch err := conn.GetManagedPrefixListEntriesPages( + &input, + func(output *ec2.GetManagedPrefixListEntriesOutput, last bool) bool { + result = append(result, output.Entries...) + return true + }); { + case err != nil: + return nil, fmt.Errorf("failed to get entries in prefix list %s: %v", id, err) + } + + return result, nil +} + +func computePrefixListEntriesModification( + oldEntries []*ec2.PrefixListEntry, + newEntries []*ec2.AddPrefixListEntry, +) ([]*ec2.AddPrefixListEntry, []*ec2.RemovePrefixListEntry) { + adds := map[string]string{} // CIDR => Description + + removes := map[string]struct{}{} // set of CIDR + for _, oldEntry := range oldEntries { + oldCIDR := aws.StringValue(oldEntry.Cidr) + removes[oldCIDR] = struct{}{} + } + + for _, newEntry := range newEntries { + newCIDR := aws.StringValue(newEntry.Cidr) + newDescription := aws.StringValue(newEntry.Description) + + for _, oldEntry := range oldEntries { + oldCIDR := aws.StringValue(oldEntry.Cidr) + oldDescription := aws.StringValue(oldEntry.Description) + + if oldCIDR == newCIDR { + delete(removes, oldCIDR) + + if oldDescription != newDescription { + adds[oldCIDR] = newDescription + } + + goto nextNewEntry + } + } + + // reach this point when no matching oldEntry found + adds[newCIDR] = newDescription + + nextNewEntry: + } + + addList := make([]*ec2.AddPrefixListEntry, 0, len(adds)) + for cidr, description := range adds { + addList = append(addList, &ec2.AddPrefixListEntry{ + Cidr: aws.String(cidr), + Description: aws.String(description), + }) + } + sort.Slice(addList, func(i, j int) bool { + return aws.StringValue(addList[i].Cidr) < aws.StringValue(addList[j].Cidr) + }) + + removeList := make([]*ec2.RemovePrefixListEntry, 0, len(removes)) + for cidr := range removes { + removeList = append(removeList, &ec2.RemovePrefixListEntry{ + Cidr: aws.String(cidr), + }) + } + sort.Slice(removeList, func(i, j int) bool { + return aws.StringValue(removeList[i].Cidr) < aws.StringValue(removeList[j].Cidr) + }) + + return addList, removeList +} + +func waitUntilAwsManagedPrefixListSettled( + id string, + conn *ec2.EC2, + timeout time.Duration, +) error { + log.Printf("[INFO] Waiting for managed prefix list %s to settle...", id) + + err := resource.Retry(timeout, func() *resource.RetryError { + settled, err := isAwsManagedPrefixListSettled(id, conn) + switch { + case err != nil: + return resource.NonRetryableError(err) + case !settled: + return resource.RetryableError(errors.New("resource not yet settled")) + } + + return nil + }) + + if isResourceTimeoutError(err) { + return fmt.Errorf("timed out: %s", err) + } + + return nil +} + +func isAwsManagedPrefixListSettled(id string, conn *ec2.EC2) (bool, error) { + pl, ok, err := getManagedPrefixList(id, conn) + switch { + case err != nil: + return false, err + case !ok: + return true, nil + } + + switch state := aws.StringValue(pl.State); state { + case ec2.PrefixListStateCreateComplete, ec2.PrefixListStateModifyComplete, ec2.PrefixListStateDeleteComplete: + return true, nil + case ec2.PrefixListStateCreateInProgress, ec2.PrefixListStateModifyInProgress, ec2.PrefixListStateDeleteInProgress: + return false, nil + case ec2.PrefixListStateCreateFailed, ec2.PrefixListStateModifyFailed, ec2.PrefixListStateDeleteFailed: + return false, fmt.Errorf("terminal state %s indicates failure", state) + default: + return false, fmt.Errorf("unexpected state %s", state) + } +} diff --git a/aws/resource_aws_prefix_list_entry.go b/aws/resource_aws_prefix_list_entry.go new file mode 100644 index 00000000000..d756c5318ea --- /dev/null +++ b/aws/resource_aws_prefix_list_entry.go @@ -0,0 +1,287 @@ +package aws + +import ( + "errors" + "fmt" + "log" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" +) + +func resourceAwsPrefixListEntry() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsPrefixListEntryCreate, + Read: resourceAwsPrefixListEntryRead, + Update: resourceAwsPrefixListEntryUpdate, + Delete: resourceAwsPrefixListEntryDelete, + + Importer: &schema.ResourceImporter{ + State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + ss := strings.Split(d.Id(), "_") + if len(ss) != 2 || ss[0] == "" || ss[1] == "" { + return nil, fmt.Errorf("invalid id %s: expected pl-123456_1.0.0.0/8", d.Id()) + } + + d.Set("prefix_list_id", ss[0]) + d.Set("cidr_block", ss[1]) + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "prefix_list_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "cidr_block": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.IsCIDR, + }, + "description": { + Type: schema.TypeString, + Optional: true, + Default: "", + ValidateFunc: validation.StringLenBetween(0, 255), + }, + }, + } +} + +func resourceAwsPrefixListEntryCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + prefixListId := d.Get("prefix_list_id").(string) + cidrBlock := d.Get("cidr_block").(string) + + log.Printf( + "[INFO] adding entry %s to prefix list %s...", + cidrBlock, prefixListId) + + err := modifyAwsManagedPrefixListConcurrently( + prefixListId, conn, d.Timeout(schema.TimeoutUpdate), + ec2.ModifyManagedPrefixListInput{ + PrefixListId: aws.String(prefixListId), + CurrentVersion: nil, // set by modifyAwsManagedPrefixListConcurrently + AddEntries: []*ec2.AddPrefixListEntry{ + { + Cidr: aws.String(cidrBlock), + Description: aws.String(d.Get("description").(string)), + }, + }, + }, + func(pl *ec2.ManagedPrefixList) *resource.RetryError { + currentVersion := int(aws.Int64Value(pl.Version)) + + _, ok, err := getManagedPrefixListEntryByCIDR(prefixListId, conn, currentVersion, cidrBlock) + switch { + case err != nil: + return resource.NonRetryableError(err) + case ok: + return resource.NonRetryableError(errors.New("an entry for this cidr block already exists")) + } + + return nil + }) + + if err != nil { + return fmt.Errorf("failed to add entry %s to prefix list %s: %s", cidrBlock, prefixListId, err) + } + + d.SetId(fmt.Sprintf("%s_%s", prefixListId, cidrBlock)) + + return resourceAwsPrefixListEntryRead(d, meta) +} + +func resourceAwsPrefixListEntryRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + prefixListId := d.Get("prefix_list_id").(string) + cidrBlock := d.Get("cidr_block").(string) + + entry, ok, err := getManagedPrefixListEntryByCIDR(prefixListId, conn, 0, cidrBlock) + switch { + case err != nil: + return err + case !ok: + log.Printf( + "[WARN] entry %s of managed prefix list %s not found; removing from state.", + cidrBlock, prefixListId) + d.SetId("") + return nil + } + + d.Set("description", entry.Description) + + return nil +} + +func resourceAwsPrefixListEntryUpdate(d *schema.ResourceData, meta interface{}) error { + if !d.HasChange("description") { + return fmt.Errorf("all attributes except description should force new resource") + } + + conn := meta.(*AWSClient).ec2conn + prefixListId := d.Get("prefix_list_id").(string) + cidrBlock := d.Get("cidr_block").(string) + + err := modifyAwsManagedPrefixListConcurrently( + prefixListId, conn, d.Timeout(schema.TimeoutUpdate), + ec2.ModifyManagedPrefixListInput{ + PrefixListId: aws.String(prefixListId), + CurrentVersion: nil, // set by modifyAwsManagedPrefixListConcurrently + AddEntries: []*ec2.AddPrefixListEntry{ + { + Cidr: aws.String(cidrBlock), + Description: aws.String(d.Get("description").(string)), + }, + }, + }, + nil) + + if err != nil { + return fmt.Errorf("failed to update entry %s in prefix list %s: %s", cidrBlock, prefixListId, err) + } + + return resourceAwsPrefixListEntryRead(d, meta) +} + +func resourceAwsPrefixListEntryDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + prefixListId := d.Get("prefix_list_id").(string) + cidrBlock := d.Get("cidr_block").(string) + + err := modifyAwsManagedPrefixListConcurrently( + prefixListId, conn, d.Timeout(schema.TimeoutUpdate), + ec2.ModifyManagedPrefixListInput{ + PrefixListId: aws.String(prefixListId), + CurrentVersion: nil, // set by modifyAwsManagedPrefixListConcurrently + RemoveEntries: []*ec2.RemovePrefixListEntry{ + { + Cidr: aws.String(cidrBlock), + }, + }, + }, + nil) + + switch { + case isResourceNotFoundError(err): + log.Printf("[WARN] managed prefix list %s not found; removing from state", prefixListId) + return nil + case err != nil: + return fmt.Errorf("failed to remove entry %s from prefix list %s: %s", cidrBlock, prefixListId, err) + } + + return nil +} + +func getManagedPrefixListEntryByCIDR( + id string, + conn *ec2.EC2, + version int, + cidr string, +) (*ec2.PrefixListEntry, bool, error) { + input := ec2.GetManagedPrefixListEntriesInput{ + PrefixListId: aws.String(id), + } + + if version > 0 { + input.TargetVersion = aws.Int64(int64(version)) + } + + result := (*ec2.PrefixListEntry)(nil) + + err := conn.GetManagedPrefixListEntriesPages( + &input, + func(output *ec2.GetManagedPrefixListEntriesOutput, last bool) bool { + for _, entry := range output.Entries { + entryCidr := aws.StringValue(entry.Cidr) + if entryCidr == cidr { + result = entry + return false + } + } + + return true + }) + + switch { + case isAWSErr(err, "InvalidPrefixListID.NotFound", ""): + return nil, false, nil + case err != nil: + return nil, false, fmt.Errorf("failed to get entries in prefix list %s: %v", id, err) + case result == nil: + return nil, false, nil + } + + return result, true, nil +} + +func modifyAwsManagedPrefixListConcurrently( + id string, + conn *ec2.EC2, + timeout time.Duration, + input ec2.ModifyManagedPrefixListInput, + check func(pl *ec2.ManagedPrefixList) *resource.RetryError, +) error { + isModified := false + err := resource.Retry(timeout, func() *resource.RetryError { + if !isModified { + pl, ok, err := getManagedPrefixList(id, conn) + switch { + case err != nil: + return resource.NonRetryableError(err) + case !ok: + return resource.NonRetryableError(&resource.NotFoundError{}) + } + + input.CurrentVersion = pl.Version + + if check != nil { + if err := check(pl); err != nil { + return err + } + } + + switch _, err := conn.ModifyManagedPrefixList(&input); { + case isManagedPrefixListModificationConflictErr(err): + return resource.RetryableError(err) + case err != nil: + return resource.NonRetryableError(fmt.Errorf("modify failed: %s", err)) + } + + isModified = true + } + + switch settled, err := isAwsManagedPrefixListSettled(id, conn); { + case err != nil: + return resource.NonRetryableError(fmt.Errorf("resource failed to settle: %s", err)) + case !settled: + return resource.RetryableError(errors.New("resource not yet settled")) + } + + return nil + }) + + switch { + case isResourceTimeoutError(err): + return fmt.Errorf("timed out: %s", err) + case err != nil: + return err + } + + return nil +} + +func isManagedPrefixListModificationConflictErr(err error) bool { + return isAWSErr(err, "IncorrectState", "in the current state (modify-in-progress)") || + isAWSErr(err, "IncorrectState", "in the current state (create-in-progress)") || + isAWSErr(err, "PrefixListVersionMismatch", "") || + isAWSErr(err, "ConcurrentMutationLimitExceeded", "") +} diff --git a/aws/resource_aws_prefix_list_entry_test.go b/aws/resource_aws_prefix_list_entry_test.go new file mode 100644 index 00000000000..2f86995d96a --- /dev/null +++ b/aws/resource_aws_prefix_list_entry_test.go @@ -0,0 +1,524 @@ +package aws + +import ( + "fmt" + "reflect" + "regexp" + "sort" + "strings" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func TestAccAwsPrefixListEntry_basic(t *testing.T) { + resourceName := "aws_prefix_list_entry.test" + entry := ec2.PrefixListEntry{} + + checkAttributes := func(*terraform.State) error { + if actual := aws.StringValue(entry.Cidr); actual != "1.0.0.0/8" { + return fmt.Errorf("bad cidr: %s", actual) + } + + if actual := aws.StringValue(entry.Description); actual != "Create" { + return fmt.Errorf("bad description: %s", actual) + } + + return nil + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSPrefixListDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsPrefixListEntryConfig_basic_create, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListEntryExists(resourceName, &entry), + checkAttributes, + resource.TestCheckResourceAttr(resourceName, "cidr_block", "1.0.0.0/8"), + resource.TestCheckResourceAttr(resourceName, "description", "Create"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsPrefixListEntryConfig_basic_update, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListEntryExists(resourceName, &entry), + resource.TestCheckResourceAttr(resourceName, "cidr_block", "1.0.0.0/8"), + resource.TestCheckResourceAttr(resourceName, "description", "Update"), + ), + }, + }, + }) +} + +const testAccAwsPrefixListEntryConfig_basic_create = ` +resource "aws_prefix_list" "test" { + name = "tf-test-acc" + address_family = "IPv4" + max_entries = 5 +} + +resource "aws_prefix_list_entry" "test" { + prefix_list_id = aws_prefix_list.test.id + cidr_block = "1.0.0.0/8" + description = "Create" +} +` + +const testAccAwsPrefixListEntryConfig_basic_update = ` +resource "aws_prefix_list" "test" { + name = "tf-test-acc" + address_family = "IPv4" + max_entries = 5 +} + +resource "aws_prefix_list_entry" "test" { + prefix_list_id = aws_prefix_list.test.id + cidr_block = "1.0.0.0/8" + description = "Update" +} +` + +func testAccAwsPrefixListEntryExists( + name string, + out *ec2.PrefixListEntry, +) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + switch { + case !ok: + return fmt.Errorf("resource %s not found", name) + case rs.Primary.ID == "": + return fmt.Errorf("resource %s has not set its id", name) + } + + conn := testAccProvider.Meta().(*AWSClient).ec2conn + ss := strings.Split(rs.Primary.ID, "_") + prefixListId, cidrBlock := ss[0], ss[1] + + entry, ok, err := getManagedPrefixListEntryByCIDR(prefixListId, conn, 0, cidrBlock) + switch { + case err != nil: + return err + case !ok: + return fmt.Errorf("resource %s (%s) has not been created", name, prefixListId) + } + + if out != nil { + *out = *entry + } + + return nil + } +} + +func TestAccAwsPrefixListEntry_disappears(t *testing.T) { + prefixListResourceName := "aws_prefix_list.test" + resourceName := "aws_prefix_list_entry.test" + pl := ec2.ManagedPrefixList{} + entry := ec2.PrefixListEntry{} + + checkDisappears := func(*terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + input := ec2.ModifyManagedPrefixListInput{ + PrefixListId: pl.PrefixListId, + CurrentVersion: pl.Version, + RemoveEntries: []*ec2.RemovePrefixListEntry{ + { + Cidr: entry.Cidr, + }, + }, + } + + _, err := conn.ModifyManagedPrefixList(&input) + return err + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSPrefixListDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsPrefixListEntryConfig_disappears, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListEntryExists(resourceName, &entry), + testAccAwsPrefixListExists(prefixListResourceName, &pl, nil), + checkDisappears, + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +const testAccAwsPrefixListEntryConfig_disappears = ` +resource "aws_prefix_list" "test" { + name = "tf-test-acc" + address_family = "IPv4" + max_entries = 5 +} + +resource "aws_prefix_list_entry" "test" { + prefix_list_id = aws_prefix_list.test.id + cidr_block = "1.0.0.0/8" +} +` + +func TestAccAwsPrefixListEntry_prefixListDisappears(t *testing.T) { + prefixListResourceName := "aws_prefix_list.test" + resourceName := "aws_prefix_list_entry.test" + pl := ec2.ManagedPrefixList{} + entry := ec2.PrefixListEntry{} + + checkDisappears := func(*terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + input := ec2.DeleteManagedPrefixListInput{ + PrefixListId: pl.PrefixListId, + } + + _, err := conn.DeleteManagedPrefixList(&input) + return err + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSPrefixListDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsPrefixListEntryConfig_disappears, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListEntryExists(resourceName, &entry), + testAccAwsPrefixListExists(prefixListResourceName, &pl, nil), + checkDisappears, + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAwsPrefixListEntry_alreadyExists(t *testing.T) { + resourceName := "aws_prefix_list_entry.test" + entry := ec2.PrefixListEntry{} + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSPrefixListDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsPrefixListEntryConfig_alreadyExists, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListEntryExists(resourceName, &entry), + ), + ExpectError: regexp.MustCompile(`an entry for this cidr block already exists`), + }, + }, + }) +} + +const testAccAwsPrefixListEntryConfig_alreadyExists = ` +resource "aws_prefix_list" "test" { + name = "tf-test-acc" + address_family = "IPv4" + max_entries = 5 + + entry { + cidr_block = "1.0.0.0/8" + } +} + +resource "aws_prefix_list_entry" "test" { + prefix_list_id = aws_prefix_list.test.id + cidr_block = "1.0.0.0/8" + description = "Test" +} +` + +func TestAccAwsPrefixListEntry_description(t *testing.T) { + resourceName := "aws_prefix_list_entry.test" + entry := ec2.PrefixListEntry{} + + checkDescription := func(expect string) resource.TestCheckFunc { + return func(*terraform.State) error { + if actual := aws.StringValue(entry.Description); actual != expect { + return fmt.Errorf("bad description: %s", actual) + } + + return nil + } + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSPrefixListDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsPrefixListEntryConfig_description_none, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListEntryExists(resourceName, &entry), + checkDescription("Test1"), + resource.TestCheckResourceAttr(resourceName, "cidr_block", "1.0.0.0/8"), + resource.TestCheckResourceAttr(resourceName, "description", "Test1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsPrefixListEntryConfig_description_some, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListEntryExists(resourceName, &entry), + checkDescription("Test2"), + resource.TestCheckResourceAttr(resourceName, "cidr_block", "1.0.0.0/8"), + resource.TestCheckResourceAttr(resourceName, "description", "Test2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsPrefixListEntryConfig_description_empty, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListEntryExists(resourceName, &entry), + checkDescription(""), + resource.TestCheckResourceAttr(resourceName, "cidr_block", "1.0.0.0/8"), + resource.TestCheckResourceAttr(resourceName, "description", ""), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsPrefixListEntryConfig_description_null, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListEntryExists(resourceName, &entry), + checkDescription(""), + resource.TestCheckResourceAttr(resourceName, "cidr_block", "1.0.0.0/8"), + resource.TestCheckResourceAttr(resourceName, "description", ""), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +const testAccAwsPrefixListEntryConfig_description_none = ` +resource "aws_prefix_list" "test" { + name = "tf-test-acc" + address_family = "IPv4" + max_entries = 5 +} + +resource "aws_prefix_list_entry" "test" { + prefix_list_id = aws_prefix_list.test.id + cidr_block = "1.0.0.0/8" + description = "Test1" +} +` + +const testAccAwsPrefixListEntryConfig_description_some = ` +resource "aws_prefix_list" "test" { + name = "tf-test-acc" + address_family = "IPv4" + max_entries = 5 +} + +resource "aws_prefix_list_entry" "test" { + prefix_list_id = aws_prefix_list.test.id + cidr_block = "1.0.0.0/8" + description = "Test2" +} +` + +const testAccAwsPrefixListEntryConfig_description_empty = ` +resource "aws_prefix_list" "test" { + name = "tf-test-acc" + address_family = "IPv4" + max_entries = 5 +} + +resource "aws_prefix_list_entry" "test" { + prefix_list_id = aws_prefix_list.test.id + cidr_block = "1.0.0.0/8" + description = "" +} +` + +const testAccAwsPrefixListEntryConfig_description_null = ` +resource "aws_prefix_list" "test" { + name = "tf-test-acc" + address_family = "IPv4" + max_entries = 5 +} + +resource "aws_prefix_list_entry" "test" { + prefix_list_id = aws_prefix_list.test.id + cidr_block = "1.0.0.0/8" +} +` + +func TestAccAwsPrefixListEntry_exceedLimit(t *testing.T) { + resourceName := "aws_prefix_list_entry.test_1" + entry := ec2.PrefixListEntry{} + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSPrefixListDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsPrefixListEntryConfig_exceedLimit(2), + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListEntryExists(resourceName, &entry)), + }, + { + Config: testAccAwsPrefixListEntryConfig_exceedLimit(3), + ResourceName: resourceName, + ExpectError: regexp.MustCompile(`You've reached the maximum number of entries for the prefix list.`), + }, + }, + }) +} + +func testAccAwsPrefixListEntryConfig_exceedLimit(count int) string { + entries := `` + for i := 0; i < count; i++ { + entries += fmt.Sprintf(` +resource "aws_prefix_list_entry" "test_%[1]d" { + prefix_list_id = aws_prefix_list.test.id + cidr_block = "%[1]d.0.0.0/8" + description = "Test_%[1]d" +} +`, + i+1) + } + + return fmt.Sprintf(` +resource "aws_prefix_list" "test" { + name = "tf-test-acc" + address_family = "IPv4" + max_entries = 2 +} + +%[1]s +`, + entries) +} + +func testAccAwsPrefixListSortEntries(list []*ec2.PrefixListEntry) { + sort.Slice(list, func(i, j int) bool { + return aws.StringValue(list[i].Cidr) < aws.StringValue(list[j].Cidr) + }) +} + +func TestAccAwsPrefixListEntry_concurrentModification(t *testing.T) { + prefixListResourceName := "aws_prefix_list.test" + pl, entries := ec2.ManagedPrefixList{}, []*ec2.PrefixListEntry(nil) + + checkAllEntriesExist := func(prefix string, count int) resource.TestCheckFunc { + return func(state *terraform.State) error { + if len(entries) != count { + return fmt.Errorf("expected %d entries", count) + } + + expectEntries := make([]*ec2.PrefixListEntry, 0, count) + for i := 0; i < count; i++ { + expectEntries = append(expectEntries, &ec2.PrefixListEntry{ + Cidr: aws.String(fmt.Sprintf("%d.0.0.0/8", i+1)), + Description: aws.String(fmt.Sprintf("%s%d", prefix, i+1))}) + } + testAccAwsPrefixListSortEntries(expectEntries) + + testAccAwsPrefixListSortEntries(entries) + + if !reflect.DeepEqual(expectEntries, entries) { + return fmt.Errorf("expected entries %#v, got %#v", expectEntries, entries) + } + + return nil + } + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSPrefixListDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsPrefixListEntryConfig_concurrentModification("Step0_", 20), + ResourceName: prefixListResourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListExists(prefixListResourceName, &pl, &entries), + checkAllEntriesExist("Step0_", 20)), + }, + { + // update the first 10 and drop the last 10 + Config: testAccAwsPrefixListEntryConfig_concurrentModification("Step1_", 10), + ResourceName: prefixListResourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListExists(prefixListResourceName, &pl, &entries), + checkAllEntriesExist("Step1_", 10)), + }, + }, + }) +} + +func testAccAwsPrefixListEntryConfig_concurrentModification(prefix string, count int) string { + entries := `` + for i := 0; i < count; i++ { + entries += fmt.Sprintf(` +resource "aws_prefix_list_entry" "test_%[1]d" { + prefix_list_id = aws_prefix_list.test.id + cidr_block = "%[1]d.0.0.0/8" + description = "%[2]s%[1]d" +} +`, + i+1, + prefix) + } + + return fmt.Sprintf(` +resource "aws_prefix_list" "test" { + name = "tf-test-acc" + address_family = "IPv4" + max_entries = 20 +} + +%[1]s +`, + entries) +} diff --git a/aws/resource_aws_prefix_list_test.go b/aws/resource_aws_prefix_list_test.go new file mode 100644 index 00000000000..2a3e66078a5 --- /dev/null +++ b/aws/resource_aws_prefix_list_test.go @@ -0,0 +1,754 @@ +package aws + +import ( + "fmt" + "reflect" + "regexp" + "sort" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func TestAccAwsPrefixList_computePrefixListEntriesModification(t *testing.T) { + type testEntry struct { + CIDR string + Description string + } + + tests := []struct { + name string + oldEntries []testEntry + newEntries []testEntry + expectedAdds []testEntry + expectedRemoves []testEntry + }{ + { + name: "add two", + oldEntries: []testEntry{}, + newEntries: []testEntry{{"1.2.3.4/32", "test1"}, {"2.3.4.5/32", "test2"}}, + expectedAdds: []testEntry{{"1.2.3.4/32", "test1"}, {"2.3.4.5/32", "test2"}}, + expectedRemoves: []testEntry{}, + }, + { + name: "remove one", + oldEntries: []testEntry{{"1.2.3.4/32", "test1"}, {"2.3.4.5/32", "test2"}}, + newEntries: []testEntry{{"1.2.3.4/32", "test1"}}, + expectedAdds: []testEntry{}, + expectedRemoves: []testEntry{{"2.3.4.5/32", "test2"}}, + }, + { + name: "modify description of one", + oldEntries: []testEntry{{"1.2.3.4/32", "test1"}, {"2.3.4.5/32", "test2"}}, + newEntries: []testEntry{{"1.2.3.4/32", "test1"}, {"2.3.4.5/32", "test2-1"}}, + expectedAdds: []testEntry{{"2.3.4.5/32", "test2-1"}}, + expectedRemoves: []testEntry{}, + }, + { + name: "add third", + oldEntries: []testEntry{{"1.2.3.4/32", "test1"}, {"2.3.4.5/32", "test2"}}, + newEntries: []testEntry{{"1.2.3.4/32", "test1"}, {"2.3.4.5/32", "test2"}, {"3.4.5.6/32", "test3"}}, + expectedAdds: []testEntry{{"3.4.5.6/32", "test3"}}, + expectedRemoves: []testEntry{}, + }, + { + name: "add and remove one", + oldEntries: []testEntry{{"1.2.3.4/32", "test1"}, {"2.3.4.5/32", "test2"}}, + newEntries: []testEntry{{"1.2.3.4/32", "test1"}, {"3.4.5.6/32", "test3"}}, + expectedAdds: []testEntry{{"3.4.5.6/32", "test3"}}, + expectedRemoves: []testEntry{{"2.3.4.5/32", "test2"}}, + }, + { + name: "add and remove one with description change", + oldEntries: []testEntry{{"1.2.3.4/32", "test1"}, {"2.3.4.5/32", "test2"}}, + newEntries: []testEntry{{"1.2.3.4/32", "test1-1"}, {"3.4.5.6/32", "test3"}}, + expectedAdds: []testEntry{{"1.2.3.4/32", "test1-1"}, {"3.4.5.6/32", "test3"}}, + expectedRemoves: []testEntry{{"2.3.4.5/32", "test2"}}, + }, + { + name: "basic test update", + oldEntries: []testEntry{{"1.0.0.0/8", "Test1"}}, + newEntries: []testEntry{{"1.0.0.0/8", "Test1-1"}, {"2.2.0.0/16", "Test2"}}, + expectedAdds: []testEntry{{"1.0.0.0/8", "Test1-1"}, {"2.2.0.0/16", "Test2"}}, + expectedRemoves: []testEntry{}, + }, + } + + for _, test := range tests { + oldEntryList := []*ec2.PrefixListEntry(nil) + for _, entry := range test.oldEntries { + oldEntryList = append(oldEntryList, &ec2.PrefixListEntry{ + Cidr: aws.String(entry.CIDR), + Description: aws.String(entry.Description), + }) + } + + newEntryList := []*ec2.AddPrefixListEntry(nil) + for _, entry := range test.newEntries { + newEntryList = append(newEntryList, &ec2.AddPrefixListEntry{ + Cidr: aws.String(entry.CIDR), + Description: aws.String(entry.Description), + }) + } + + addList, removeList := computePrefixListEntriesModification(oldEntryList, newEntryList) + + if len(addList) != len(test.expectedAdds) { + t.Errorf("expected %d adds, got %d", len(test.expectedAdds), len(addList)) + } + + for i, added := range addList { + expected := test.expectedAdds[i] + + actualCidr := aws.StringValue(added.Cidr) + expectedCidr := expected.CIDR + if actualCidr != expectedCidr { + t.Errorf("add[%d]: expected cidr %s, got %s", i, expectedCidr, actualCidr) + } + + actualDesc := aws.StringValue(added.Description) + expectedDesc := expected.Description + if actualDesc != expectedDesc { + t.Errorf("add[%d]: expected description '%s', got '%s'", i, expectedDesc, actualDesc) + } + } + + if len(removeList) != len(test.expectedRemoves) { + t.Errorf("expected %d removes, got %d", len(test.expectedRemoves), len(removeList)) + } + + for i, removed := range removeList { + expected := test.expectedRemoves[i] + + actualCidr := aws.StringValue(removed.Cidr) + expectedCidr := expected.CIDR + if actualCidr != expectedCidr { + t.Errorf("add[%d]: expected cidr %s, got %s", i, expectedCidr, actualCidr) + } + } + } +} + +func testAccCheckAWSPrefixListDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_prefix_list" { + continue + } + + id := rs.Primary.ID + + switch _, ok, err := getManagedPrefixList(id, conn); { + case err != nil: + return err + case ok: + return fmt.Errorf("managed prefix list %s still exists", id) + } + } + + return nil +} + +func testAccCheckAwsPrefixListVersion( + prefixList *ec2.ManagedPrefixList, + version int64, +) resource.TestCheckFunc { + return func(state *terraform.State) error { + if actual := aws.Int64Value(prefixList.Version); actual != version { + return fmt.Errorf("expected prefix list version %d, got %d", version, actual) + } + + return nil + } +} + +func TestAccAwsPrefixList_basic(t *testing.T) { + resourceName := "aws_prefix_list.test" + pl, entries := ec2.ManagedPrefixList{}, []*ec2.PrefixListEntry(nil) + + checkAttributes := func(*terraform.State) error { + if actual := aws.StringValue(pl.AddressFamily); actual != "IPv4" { + return fmt.Errorf("bad address family: %s", actual) + } + + if actual := aws.Int64Value(pl.MaxEntries); actual != 5 { + return fmt.Errorf("bad max entries: %d", actual) + } + + if actual := aws.StringValue(pl.OwnerId); actual != testAccGetAccountID() { + return fmt.Errorf("bad owner id: %s", actual) + } + + if actual := aws.StringValue(pl.PrefixListName); actual != "tf-test-basic-create" { + return fmt.Errorf("bad name: %s", actual) + } + + sort.Slice(pl.Tags, func(i, j int) bool { + return aws.StringValue(pl.Tags[i].Key) < aws.StringValue(pl.Tags[j].Key) + }) + + expectTags := []*ec2.Tag{ + {Key: aws.String("Key1"), Value: aws.String("Value1")}, + {Key: aws.String("Key2"), Value: aws.String("Value2")}, + } + + if !reflect.DeepEqual(expectTags, pl.Tags) { + return fmt.Errorf("expected tags %#v, got %#v", expectTags, pl.Tags) + } + + sort.Slice(entries, func(i, j int) bool { + return aws.StringValue(entries[i].Cidr) < aws.StringValue(entries[j].Cidr) + }) + + expectEntries := []*ec2.PrefixListEntry{ + {Cidr: aws.String("1.0.0.0/8"), Description: aws.String("Test1")}, + {Cidr: aws.String("2.0.0.0/8"), Description: aws.String("Test2")}, + } + + if !reflect.DeepEqual(expectEntries, entries) { + return fmt.Errorf("expected entries %#v, got %#v", expectEntries, entries) + } + + return nil + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSPrefixListDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsPrefixListConfig_basic_create, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListExists(resourceName, &pl, &entries), + checkAttributes, + resource.TestCheckResourceAttr(resourceName, "name", "tf-test-basic-create"), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "ec2", regexp.MustCompile(`prefix-list/pl-[[:xdigit:]]+`)), + resource.TestCheckResourceAttr(resourceName, "address_family", "IPv4"), + resource.TestCheckResourceAttr(resourceName, "max_entries", "5"), + resource.TestCheckResourceAttr(resourceName, "entry.#", "2"), + resource.TestCheckResourceAttr(resourceName, "entry.3370291439.cidr_block", "1.0.0.0/8"), + resource.TestCheckResourceAttr(resourceName, "entry.3370291439.description", "Test1"), + resource.TestCheckResourceAttr(resourceName, "entry.3776037899.cidr_block", "2.0.0.0/8"), + resource.TestCheckResourceAttr(resourceName, "entry.3776037899.description", "Test2"), + testAccCheckResourceAttrAccountID(resourceName, "owner_id"), + testAccCheckAwsPrefixListVersion(&pl, 1), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.Key1", "Value1"), + resource.TestCheckResourceAttr(resourceName, "tags.Key2", "Value2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsPrefixListConfig_basic_update, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListExists(resourceName, &pl, &entries), + resource.TestCheckResourceAttr(resourceName, "name", "tf-test-basic-update"), + resource.TestCheckResourceAttr(resourceName, "entry.#", "2"), + resource.TestCheckResourceAttr(resourceName, "entry.3370291439.cidr_block", "1.0.0.0/8"), + resource.TestCheckResourceAttr(resourceName, "entry.3370291439.description", "Test1"), + resource.TestCheckResourceAttr(resourceName, "entry.4190046295.cidr_block", "3.0.0.0/8"), + resource.TestCheckResourceAttr(resourceName, "entry.4190046295.description", "Test3"), + testAccCheckAwsPrefixListVersion(&pl, 2), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.Key1", "Value1"), + resource.TestCheckResourceAttr(resourceName, "tags.Key3", "Value3"), + ), + }, + }, + }) +} + +const testAccAwsPrefixListConfig_basic_create = ` +resource "aws_prefix_list" "test" { + name = "tf-test-basic-create" + address_family = "IPv4" + max_entries = 5 + + entry { + cidr_block = "1.0.0.0/8" + description = "Test1" + } + + entry { + cidr_block = "2.0.0.0/8" + description = "Test2" + } + + tags = { + Key1 = "Value1" + Key2 = "Value2" + } +} +` + +const testAccAwsPrefixListConfig_basic_update = ` +resource "aws_prefix_list" "test" { + name = "tf-test-basic-update" + address_family = "IPv4" + max_entries = 5 + + entry { + cidr_block = "1.0.0.0/8" + description = "Test1" + } + + entry { + cidr_block = "3.0.0.0/8" + description = "Test3" + } + + tags = { + Key1 = "Value1" + Key3 = "Value3" + } +} +` + +func testAccAwsPrefixListExists( + name string, + out *ec2.ManagedPrefixList, + entries *[]*ec2.PrefixListEntry, +) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + switch { + case !ok: + return fmt.Errorf("resource %s not found", name) + case rs.Primary.ID == "": + return fmt.Errorf("resource %s has not set its id", name) + } + + conn := testAccProvider.Meta().(*AWSClient).ec2conn + id := rs.Primary.ID + + pl, ok, err := getManagedPrefixList(id, conn) + switch { + case err != nil: + return err + case !ok: + return fmt.Errorf("resource %s (%s) has not been created", name, id) + } + + if out != nil { + *out = *pl + } + + if entries != nil { + entries1, err := getPrefixListEntries(id, conn, *pl.Version) + if err != nil { + return err + } + + *entries = entries1 + } + + return nil + } +} + +func TestAccAwsPrefixList_disappears(t *testing.T) { + resourceName := "aws_prefix_list.test" + pl := ec2.ManagedPrefixList{} + + checkDisappears := func(*terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + input := ec2.DeleteManagedPrefixListInput{ + PrefixListId: pl.PrefixListId, + } + + _, err := conn.DeleteManagedPrefixList(&input) + return err + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSPrefixListDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsPrefixListConfig_disappears, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListExists(resourceName, &pl, nil), + checkDisappears, + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +const testAccAwsPrefixListConfig_disappears = ` +resource "aws_prefix_list" "test" { + name = "tf-test-disappears" + address_family = "IPv4" + max_entries = 2 + + entry { + cidr_block = "1.0.0.0/8" + } +} +` + +func TestAccAwsPrefixList_name(t *testing.T) { + resourceName := "aws_prefix_list.test" + pl := ec2.ManagedPrefixList{} + + checkName := func(name string) resource.TestCheckFunc { + return func(*terraform.State) error { + if actual := aws.StringValue(pl.PrefixListName); actual != name { + return fmt.Errorf("expected name %s, got %s", name, actual) + } + + return nil + } + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSPrefixListDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsPrefixListConfig_name_create, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListExists(resourceName, &pl, nil), + resource.TestCheckResourceAttr(resourceName, "name", "tf-test-name-create"), + checkName("tf-test-name-create"), + testAccCheckAwsPrefixListVersion(&pl, 1), + ), + }, + { + Config: testAccAwsPrefixListConfig_name_update, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListExists(resourceName, &pl, nil), + resource.TestCheckResourceAttr(resourceName, "name", "tf-test-name-update"), + checkName("tf-test-name-update"), + testAccCheckAwsPrefixListVersion(&pl, 1), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +const testAccAwsPrefixListConfig_name_create = ` +resource "aws_prefix_list" "test" { + name = "tf-test-name-create" + address_family = "IPv4" + max_entries = 5 +} +` + +const testAccAwsPrefixListConfig_name_update = ` +resource "aws_prefix_list" "test" { + name = "tf-test-name-update" + address_family = "IPv4" + max_entries = 5 +} +` + +func TestAccAwsPrefixList_tags(t *testing.T) { + resourceName := "aws_prefix_list.test" + pl := ec2.ManagedPrefixList{} + + checkTags := func(m map[string]string) resource.TestCheckFunc { + return func(*terraform.State) error { + sort.Slice(pl.Tags, func(i, j int) bool { + return aws.StringValue(pl.Tags[i].Key) < aws.StringValue(pl.Tags[j].Key) + }) + + expectTags := []*ec2.Tag(nil) + + if m != nil { + for k, v := range m { + expectTags = append(expectTags, &ec2.Tag{ + Key: aws.String(k), + Value: aws.String(v), + }) + } + + sort.Slice(expectTags, func(i, j int) bool { + return aws.StringValue(expectTags[i].Key) < aws.StringValue(expectTags[j].Key) + }) + } + + if !reflect.DeepEqual(expectTags, pl.Tags) { + return fmt.Errorf("expected tags %#v, got %#v", expectTags, pl.Tags) + } + + return nil + } + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSPrefixListDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsPrefixListConfig_tags_none, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListExists(resourceName, &pl, nil), + checkTags(nil), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsPrefixListConfig_tags_addSome, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListExists(resourceName, &pl, nil), + checkTags(map[string]string{"Key1": "Value1", "Key2": "Value2", "Key3": "Value3"}), + resource.TestCheckResourceAttr(resourceName, "tags.%", "3"), + resource.TestCheckResourceAttr(resourceName, "tags.Key1", "Value1"), + resource.TestCheckResourceAttr(resourceName, "tags.Key2", "Value2"), + resource.TestCheckResourceAttr(resourceName, "tags.Key3", "Value3"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsPrefixListConfig_tags_dropOrModifySome, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListExists(resourceName, &pl, nil), + checkTags(map[string]string{"Key2": "Value2-1", "Key3": "Value3"}), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.Key2", "Value2-1"), + resource.TestCheckResourceAttr(resourceName, "tags.Key3", "Value3"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsPrefixListConfig_tags_empty, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListExists(resourceName, &pl, nil), + checkTags(nil), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsPrefixListConfig_tags_none, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListExists(resourceName, &pl, nil), + checkTags(nil), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +const testAccAwsPrefixListConfig_tags_none = ` +resource "aws_prefix_list" "test" { + name = "tf-test-acc" + address_family = "IPv4" + max_entries = 5 +} +` + +const testAccAwsPrefixListConfig_tags_addSome = ` +resource "aws_prefix_list" "test" { + name = "tf-test-acc" + address_family = "IPv4" + max_entries = 5 + + tags = { + Key1 = "Value1" + Key2 = "Value2" + Key3 = "Value3" + } +} +` + +const testAccAwsPrefixListConfig_tags_dropOrModifySome = ` +resource "aws_prefix_list" "test" { + name = "tf-test-acc" + address_family = "IPv4" + max_entries = 5 + + tags = { + Key2 = "Value2-1" + Key3 = "Value3" + } +} +` + +const testAccAwsPrefixListConfig_tags_empty = ` +resource "aws_prefix_list" "test" { + name = "tf-test-acc" + address_family = "IPv4" + max_entries = 5 + + tags = {} +} +` + +func TestAccAwsPrefixList_entryConfigMode(t *testing.T) { + resourceName := "aws_prefix_list.test" + prefixList := ec2.ManagedPrefixList{} + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSPrefixListDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsPrefixListConfig_entryConfigMode_blocks, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListExists(resourceName, &prefixList, nil), + resource.TestCheckResourceAttr(resourceName, "entry.#", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsPrefixListConfig_entryConfigMode_noBlocks, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListExists(resourceName, &prefixList, nil), + resource.TestCheckResourceAttr(resourceName, "entry.#", "2"), + ), + }, + { + Config: testAccAwsPrefixListConfig_entryConfigMode_zeroed, + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListExists(resourceName, &prefixList, nil), + resource.TestCheckResourceAttr(resourceName, "entry.#", "0"), + ), + }, + }, + }) +} + +const testAccAwsPrefixListConfig_entryConfigMode_blocks = ` +resource "aws_prefix_list" "test" { + name = "tf-test-acc" + max_entries = 5 + address_family = "IPv4" + + entry { + cidr_block = "1.0.0.0/8" + description = "Entry1" + } + + entry { + cidr_block = "2.0.0.0/8" + description = "Entry2" + } +} +` + +const testAccAwsPrefixListConfig_entryConfigMode_noBlocks = ` +resource "aws_prefix_list" "test" { + name = "tf-test-acc" + max_entries = 5 + address_family = "IPv4" +} +` + +const testAccAwsPrefixListConfig_entryConfigMode_zeroed = ` +resource "aws_prefix_list" "test" { + name = "tf-test-acc" + max_entries = 5 + address_family = "IPv4" + entry = [] +} +` + +func TestAccAwsPrefixList_exceedLimit(t *testing.T) { + resourceName := "aws_prefix_list.test" + prefixList := ec2.ManagedPrefixList{} + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSPrefixListDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsPrefixListConfig_exceedLimit(2), + ResourceName: resourceName, + Check: resource.ComposeAggregateTestCheckFunc( + testAccAwsPrefixListExists(resourceName, &prefixList, nil), + resource.TestCheckResourceAttr(resourceName, "entry.#", "2"), + ), + }, + { + Config: testAccAwsPrefixListConfig_exceedLimit(3), + ResourceName: resourceName, + ExpectError: regexp.MustCompile(`You've reached the maximum number of entries for the prefix list.`), + }, + }, + }) +} + +func testAccAwsPrefixListConfig_exceedLimit(count int) string { + entries := `` + for i := 0; i < count; i++ { + entries += fmt.Sprintf(` + entry { + cidr_block = "%[1]d.0.0.0/8" + description = "Test_%[1]d" + } +`, i+1) + } + + return fmt.Sprintf(` +resource "aws_prefix_list" "test" { + name = "tf-test-acc" + address_family = "IPv4" + max_entries = 2 +%[1]s +} +`, + entries) +} diff --git a/website/aws.erb b/website/aws.erb index a9fff231091..fc16cb9846e 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -3419,6 +3419,12 @@
  • aws_network_interface_attachment
  • +
  • + aws_prefix_list +
  • +
  • + aws_prefix_list_entry +
  • aws_route
  • diff --git a/website/docs/r/prefix_list.html.markdown b/website/docs/r/prefix_list.html.markdown new file mode 100644 index 00000000000..041fbd1d3c7 --- /dev/null +++ b/website/docs/r/prefix_list.html.markdown @@ -0,0 +1,85 @@ +--- +subcategory: "VPC" +layout: "aws" +page_title: "AWS: aws_prefix_list" +description: |- + Provides a managed prefix list resource. +--- + +# Resource: aws_prefix_list + +Provides a managed prefix list resource. + +~> **NOTE on Prefix Lists and Prefix List Entries:** Terraform currently +provides both a standalone [Prefix List Entry resource](prefix_list_entry.html), +and a Prefix List resource with an `entry` set defined in-line. At this time you +cannot use a Prefix List with in-line rules in conjunction with any Prefix List Entry +resources. Doing so will cause a conflict of rule settings and will unpredictably +fail or overwrite rules. + +~> **NOTE on `max_entries`:** When you reference a Prefix List in a resource, +the maximum number of entries for the prefix lists counts as the same number of rules +or entries for the resource. For example, if you create a prefix list with a maximum +of 20 entries and you reference that prefix list in a security group rule, this counts +as 20 rules for the security group. + +## Example Usage + +Basic usage + +```hcl +resource "aws_prefix_list" "example" { + name = "All VPC CIDR-s" + address_family = "IPv4" + max_entries = 5 + + entry { + cidr_block = aws_vpc.example.cidr_block + description = "Primary" + } + + entry { + cidr_block = aws_vpc_ipv4_cidr_block_association.example.cidr_block + description = "Secondary" + } + + tags = { + Env = "live" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of this resource. The name must not start with `com.amazonaws`. +* `address_family` - (Required, Forces new resource) The address family (`IPv4` or `IPv6`) of + this prefix list. +* `entry` - (Optional) Can be specified multiple times for each prefix list entry. + Each entry block supports fields documented below. Different entries may have + overlapping CIDR blocks, but a particular CIDR should not be duplicated. +* `max_entries` - (Required, Forces new resource) The maximum number of entries that + this prefix list can contain. +* `tags` - (Optional) A map of tags to assign to this resource. + +The `entry` block supports: + +* `cidr_block` - (Required) The CIDR block of this entry. +* `description` - (Optional) Description of this entry. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of the prefix list. +* `arn` - The ARN of the prefix list. +* `owner_id` - The ID of the AWS account that owns this prefix list. + +## Import + +Prefix Lists can be imported using the `id`, e.g. + +``` +$ terraform import aws_prefix_list.default pl-0570a1d2d725c16be +``` diff --git a/website/docs/r/prefix_list_entry.html.markdown b/website/docs/r/prefix_list_entry.html.markdown new file mode 100644 index 00000000000..feb5b758a8b --- /dev/null +++ b/website/docs/r/prefix_list_entry.html.markdown @@ -0,0 +1,66 @@ +--- +subcategory: "VPC" +layout: "aws" +page_title: "AWS: aws_prefix_list_entry" +description: |- + Provides a managed prefix list entry resource. +--- + +# Resource: aws_prefix_list_entry + +Provides a managed prefix list entry resource. Represents a single `entry`, which +can be added to external Prefix Lists. + +~> **NOTE on Prefix Lists and Prefix List Entries:** Terraform currently +provides both a standalone Prefix List Entry, and a [Prefix List resource](prefix_list.html) +with an `entry` set defined in-line. At this time you +cannot use a Prefix List with in-line rules in conjunction with any Prefix List Entry +resources. Doing so will cause a conflict of rule settings and will unpredictably +fail or overwrite rules. + +~> **NOTE:** A Prefix List will have an upper bound on the number of rules +that it can support. + +~> **NOTE:** Resource creation will fail if the target Prefix List already has a +rule against the given CIDR block. + +## Example Usage + +Basic usage + +```hcl +resource "aws_prefix_list" "example" { + name = "All VPC CIDR-s" + address_family = "IPv4" + max_entries = 5 +} + +resource "aws_prefix_list_entry" "example" { + prefix_list_id = aws_prefix_list.example.id + cidr_block = aws_vpc.example.cidr_block + description = "Primary" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `prefix_list_id` - (Required, Forces new resource) ID of the Prefix List to add this entry to. +* `cidr_block` - (Required, Forces new resource) The CIDR block to add an entry for. Different entries may have + overlapping CIDR blocks, but duplicating a particular block is not allowed. +* `description` - (Optional, Up to 255 characters) The description of this entry. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of the prefix list entry. + +## Import + +Prefix List Entries can be imported using a concatenation of the `prefix_list_id` and `cidr_block` by an underscore (`_`). For example: + +```console +$ terraform import aws_prefix_list_entry.example pl-0570a1d2d725c16be_10.30.0.0/16 +``` diff --git a/website/docs/r/security_group.html.markdown b/website/docs/r/security_group.html.markdown index 0b4227d481a..17c4a388a1f 100644 --- a/website/docs/r/security_group.html.markdown +++ b/website/docs/r/security_group.html.markdown @@ -84,7 +84,7 @@ The `ingress` block supports: * `cidr_blocks` - (Optional) List of CIDR blocks. * `ipv6_cidr_blocks` - (Optional) List of IPv6 CIDR blocks. -* `prefix_list_ids` - (Optional) List of prefix list IDs. +* `prefix_list_ids` - (Optional) List of Prefix List IDs. * `from_port` - (Required) The start port (or ICMP type number if protocol is "icmp" or "icmpv6") * `protocol` - (Required) The protocol. If you select a protocol of "-1" (semantically equivalent to `"all"`, which is not a valid value here), you must specify a "from_port" and "to_port" equal to 0. If not icmp, icmpv6, tcp, udp, or "-1" use the [protocol number](https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml) @@ -99,7 +99,7 @@ The `egress` block supports: * `cidr_blocks` - (Optional) List of CIDR blocks. * `ipv6_cidr_blocks` - (Optional) List of IPv6 CIDR blocks. -* `prefix_list_ids` - (Optional) List of prefix list IDs (for allowing access to VPC endpoints) +* `prefix_list_ids` - (Optional) List of Prefix List IDs. * `from_port` - (Required) The start port (or ICMP type number if protocol is "icmp") * `protocol` - (Required) The protocol. If you select a protocol of "-1" (semantically equivalent to `"all"`, which is not a valid value here), you must specify a "from_port" and "to_port" equal to 0. If not icmp, tcp, udp, or "-1" use the [protocol number](https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml) @@ -128,8 +128,9 @@ egress { ## Usage with prefix list IDs -Prefix list IDs are managed by AWS internally. Prefix list IDs -are associated with a prefix list name, or service name, that is linked to a specific region. +Prefix Lists are either managed by AWS internally, or created by the customer using a +[Prefix List resource](prefix_list.html). Prefix Lists provided by +AWS are associated with a prefix list name, or service name, that is linked to a specific region. Prefix list IDs are exported on VPC Endpoints, so you can use this format: ```hcl @@ -147,6 +148,8 @@ resource "aws_vpc_endpoint" "my_endpoint" { } ``` +You can also find a specific Prefix List using the `aws_prefix_list` data source. + ## Attributes Reference In addition to all arguments above, the following attributes are exported: diff --git a/website/docs/r/security_group_rule.html.markdown b/website/docs/r/security_group_rule.html.markdown index 2439da4deff..b121f8e36a7 100644 --- a/website/docs/r/security_group_rule.html.markdown +++ b/website/docs/r/security_group_rule.html.markdown @@ -45,8 +45,7 @@ The following arguments are supported: or `egress` (outbound). * `cidr_blocks` - (Optional) List of CIDR blocks. Cannot be specified with `source_security_group_id`. * `ipv6_cidr_blocks` - (Optional) List of IPv6 CIDR blocks. -* `prefix_list_ids` - (Optional) List of prefix list IDs (for allowing access to VPC endpoints). -Only valid with `egress`. +* `prefix_list_ids` - (Optional) List of Prefix List IDs. * `from_port` - (Required) The start port (or ICMP type number if protocol is "icmp" or "icmpv6"). * `protocol` - (Required) The protocol. If not icmp, icmpv6, tcp, udp, or all use the [protocol number](https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml) * `security_group_id` - (Required) The security group to apply this rule to. @@ -59,8 +58,9 @@ Only valid with `egress`. ## Usage with prefix list IDs -Prefix list IDs are manged by AWS internally. Prefix list IDs -are associated with a prefix list name, or service name, that is linked to a specific region. +Prefix Lists are either managed by AWS internally, or created by the customer using a +[Prefix List resource](prefix_list.html). Prefix Lists provided by +AWS are associated with a prefix list name, or service name, that is linked to a specific region. Prefix list IDs are exported on VPC Endpoints, so you can use this format: ```hcl @@ -79,6 +79,8 @@ resource "aws_vpc_endpoint" "my_endpoint" { } ``` +You can also find a specific Prefix List using the `aws_prefix_list` data source. + ## Attributes Reference In addition to all arguments above, the following attributes are exported: From ccd00bf1ec35e7815beabc0ee4c2f3ba99f9274f Mon Sep 17 00:00:00 2001 From: Roberth Kulbin Date: Tue, 21 Jul 2020 22:49:00 +0100 Subject: [PATCH 6/6] d/aws_prefix_list: add managed prefix list support --- aws/data_source_aws_prefix_list.go | 55 +++++++++++++++++- aws/data_source_aws_prefix_list_test.go | 74 ++++++++++++++++++++++++ website/docs/d/prefix_list.html.markdown | 35 ++++++++++- 3 files changed, 158 insertions(+), 6 deletions(-) diff --git a/aws/data_source_aws_prefix_list.go b/aws/data_source_aws_prefix_list.go index fdb0fe24a39..eea2f8af8c2 100644 --- a/aws/data_source_aws_prefix_list.go +++ b/aws/data_source_aws_prefix_list.go @@ -8,6 +8,8 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" ) func dataSourceAwsPrefixList() *schema.Resource { @@ -30,16 +32,34 @@ func dataSourceAwsPrefixList() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, }, "filter": dataSourceFiltersSchema(), + "owner_id": { + Type: schema.TypeString, + Computed: true, + }, + "address_family": { + Type: schema.TypeString, + Computed: true, + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "max_entries": { + Type: schema.TypeInt, + Computed: true, + }, + "tags": tagsSchemaComputed(), }, } } func dataSourceAwsPrefixListRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).ec2conn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig filters, filtersOk := d.GetOk("filter") - req := &ec2.DescribePrefixListsInput{} + req := &ec2.DescribeManagedPrefixListsInput{} if filtersOk { req.Filters = buildAwsDataSourceFilters(filters.(*schema.Set)) } @@ -54,7 +74,7 @@ func dataSourceAwsPrefixListRead(d *schema.ResourceData, meta interface{}) error } log.Printf("[DEBUG] Reading Prefix List: %s", req) - resp, err := conn.DescribePrefixLists(req) + resp, err := conn.DescribeManagedPrefixLists(req) switch { case err != nil: return err @@ -69,11 +89,40 @@ func dataSourceAwsPrefixListRead(d *schema.ResourceData, meta interface{}) error d.SetId(*pl.PrefixListId) d.Set("name", pl.PrefixListName) - cidrs := aws.StringValueSlice(pl.Cidrs) + getEntriesInput := ec2.GetManagedPrefixListEntriesInput{ + PrefixListId: pl.PrefixListId, + } + + cidrs := []string(nil) + + err = conn.GetManagedPrefixListEntriesPages( + &getEntriesInput, func(output *ec2.GetManagedPrefixListEntriesOutput, last bool) bool { + for _, entry := range output.Entries { + cidrs = append(cidrs, aws.StringValue(entry.Cidr)) + } + return true + }) + if err != nil { + return fmt.Errorf("failed to get entries of prefix list %s: %s", *pl.PrefixListId, err) + } + sort.Strings(cidrs) + if err := d.Set("cidr_blocks", cidrs); err != nil { return fmt.Errorf("failed to set cidr blocks of prefix list %s: %s", d.Id(), err) } + d.Set("owner_id", pl.OwnerId) + d.Set("address_family", pl.AddressFamily) + d.Set("arn", pl.PrefixListArn) + + if actual := aws.Int64Value(pl.MaxEntries); actual > 0 { + d.Set("max_entries", actual) + } + + if err := d.Set("tags", keyvaluetags.Ec2KeyValueTags(pl.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("failed to set tags of prefix list %s: %s", d.Id(), err) + } + return nil } diff --git a/aws/data_source_aws_prefix_list_test.go b/aws/data_source_aws_prefix_list_test.go index ae351ae06b0..501b1ad5dd4 100644 --- a/aws/data_source_aws_prefix_list_test.go +++ b/aws/data_source_aws_prefix_list_test.go @@ -70,6 +70,26 @@ func testAccDataSourceAwsPrefixListCheck(name string) resource.TestCheckFunc { return fmt.Errorf("cidr_blocks seem suspiciously low: %d", cidrBlockSize) } + if actual := attr["owner_id"]; actual != "AWS" { + return fmt.Errorf("bad owner_id %s", actual) + } + + if actual := attr["address_family"]; actual != "IPv4" { + return fmt.Errorf("bad address_family %s", actual) + } + + if actual := attr["arn"]; actual != "arn:aws:ec2:us-west-2:aws:prefix-list/pl-68a54001" { + return fmt.Errorf("bad arn %s", actual) + } + + if actual := attr["max_entries"]; actual != "" { + return fmt.Errorf("unexpected max_entries %s", actual) + } + + if attr["tags.%"] != "0" { + return fmt.Errorf("expected 0 tags") + } + return nil } } @@ -143,3 +163,57 @@ data "aws_prefix_list" "test" { } } ` + +func TestAccDataSourceAwsPrefixList_managedPrefixList(t *testing.T) { + resourceName := "aws_prefix_list.test" + dataSourceName := "data.aws_prefix_list.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSPrefixListDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsPrefixListConfig_managedPrefixList, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "id", dataSourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "name", dataSourceName, "name"), + resource.TestCheckResourceAttrPair(resourceName, "arn", dataSourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "owner_id", dataSourceName, "owner_id"), + testAccCheckResourceAttrAccountID(dataSourceName, "owner_id"), + resource.TestCheckResourceAttrPair(resourceName, "name", dataSourceName, "name"), + resource.TestCheckResourceAttrPair(resourceName, "address_family", dataSourceName, "address_family"), + resource.TestCheckResourceAttrPair(resourceName, "max_entries", dataSourceName, "max_entries"), + resource.TestCheckResourceAttr(dataSourceName, "cidr_blocks.#", "2"), + resource.TestCheckResourceAttr(dataSourceName, "cidr_blocks.0", "1.0.0.0/8"), + resource.TestCheckResourceAttr(dataSourceName, "cidr_blocks.1", "2.0.0.0/8"), + resource.TestCheckResourceAttr(dataSourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(dataSourceName, "tags.Key1", "Value1"), + resource.TestCheckResourceAttr(dataSourceName, "tags.Key2", "Value2"), + ), + }, + }, + }) +} + +const testAccDataSourceAwsPrefixListConfig_managedPrefixList = ` +resource "aws_prefix_list" "test" { + name = "tf-test-acc" + max_entries = 5 + address_family = "IPv4" + entry { + cidr_block = "1.0.0.0/8" + } + entry { + cidr_block = "2.0.0.0/8" + } + tags = { + Key1 = "Value1" + Key2 = "Value2" + } +} + +data "aws_prefix_list" "test" { + prefix_list_id = aws_prefix_list.test.id +} +` diff --git a/website/docs/d/prefix_list.html.markdown b/website/docs/d/prefix_list.html.markdown index b2b5c8ff356..20372f4c78a 100644 --- a/website/docs/d/prefix_list.html.markdown +++ b/website/docs/d/prefix_list.html.markdown @@ -8,8 +8,8 @@ description: |- # Data Source: aws_prefix_list -`aws_prefix_list` provides details about a specific prefix list (PL) -in the current region. +`aws_prefix_list` provides details about a specific AWS prefix list (PL) +or a customer-managed prefix list in the current region. This can be used both to validate a prefix list given in a variable and to obtain the CIDR blocks (IP address ranges) for the associated @@ -64,6 +64,30 @@ data "aws_prefix_list" "test" { } ``` +### Find a managed prefix list + +```hcl +resource "aws_prefix_list" "example" { + name = "example" + max_entries = 5 + address_family = "IPv4" + entry { + cidr_block = "1.0.0.0/8" + } + entry { + cidr_block = "2.0.0.0/8" + } + tags = { + Key1 = "Value1" + Key2 = "Value2" + } +} + +data "aws_prefix_list" "example" { + prefix_list_id = aws_prefix_list.example.id +} +``` + ## Argument Reference The arguments of this data source act as filters for querying the available @@ -78,7 +102,7 @@ whose data will be exported as attributes. The following arguments are supported by the `filter` configuration block: -* `name` - (Required) The name of the filter field. Valid values can be found in the [EC2 DescribePrefixLists API Reference](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribePrefixLists.html). +* `name` - (Required) The name of the filter field. Valid values can be found in the EC2 [DescribeManagedPrefixLists](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeManagedPrefixLists.html) API Reference. * `values` - (Required) Set of values that are accepted for the given filter field. Results will be selected if any given value matches. ## Attributes Reference @@ -86,5 +110,10 @@ The following arguments are supported by the `filter` configuration block: In addition to all arguments above, the following attributes are exported: * `id` - The ID of the selected prefix list. +* `arn` - The ARN of the selected prefix list. * `name` - The name of the selected prefix list. * `cidr_blocks` - The list of CIDR blocks for the AWS service associated with the prefix list. +* `owner_id` - The Account ID of the owner of a customer-managed prefix list, or `AWS` otherwise. +* `address_family` - The address family of the prefix list. Valid values are `IPv4` and `IPv6`. +* `max_entries` - When then prefix list is managed, the maximum number of entries it supports, or null otherwise. +* `tags` - A map of tags assigned to the resource.