From 398b9cb5d1d31ac558d596579eaa818e22622e10 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Mon, 13 Jun 2022 16:29:21 +0100 Subject: [PATCH 1/6] Use new list api for ip list resource. --- .../provider/resource_cloudflare_ip_list.go | 48 ++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/internal/provider/resource_cloudflare_ip_list.go b/internal/provider/resource_cloudflare_ip_list.go index 5f40806891..8df678fbf7 100644 --- a/internal/provider/resource_cloudflare_ip_list.go +++ b/internal/provider/resource_cloudflare_ip_list.go @@ -29,7 +29,12 @@ func resourceCloudflareIPListCreate(ctx context.Context, d *schema.ResourceData, client := meta.(*cloudflare.API) accountID := d.Get("account_id").(string) - list, err := client.CreateIPList(ctx, accountID, d.Get("name").(string), d.Get("description").(string), d.Get("kind").(string)) + list, err := client.CreateList(ctx, cloudflare.ListCreateParams{ + AccountID: accountID, + Name: d.Get("name").(string), + Description: d.Get("description").(string), + Kind: d.Get("kind").(string), + }) if err != nil { return diag.FromErr(errors.Wrap(err, fmt.Sprintf("error creating IP List %s", d.Get("name").(string)))) } @@ -38,7 +43,11 @@ func resourceCloudflareIPListCreate(ctx context.Context, d *schema.ResourceData, if items, ok := d.GetOk("item"); ok { IPListItems := buildIPListItemsCreateRequest(items.(*schema.Set).List()) - _, err = client.CreateIPListItems(ctx, accountID, d.Id(), IPListItems) + _, err = client.CreateListItems(ctx, cloudflare.ListCreateItemsParams{ + AccountID: accountID, + ID: d.Id(), + Items: IPListItems, + }) if err != nil { return diag.FromErr(errors.Wrap(err, fmt.Sprintf("error creating IP List Items"))) } @@ -67,7 +76,10 @@ func resourceCloudflareIPListRead(ctx context.Context, d *schema.ResourceData, m client := meta.(*cloudflare.API) accountID := d.Get("account_id").(string) - list, err := client.GetIPList(ctx, accountID, d.Id()) + list, err := client.GetList(ctx, cloudflare.ListGetParams{ + AccountID: accountID, + ID: d.Id(), + }) if err != nil { if strings.Contains(err.Error(), "could not find list") { tflog.Info(ctx, fmt.Sprintf("IP List %s no longer exists", d.Id())) @@ -81,7 +93,10 @@ func resourceCloudflareIPListRead(ctx context.Context, d *schema.ResourceData, m d.Set("description", list.Description) d.Set("kind", list.Kind) - items, err := client.ListIPListItems(ctx, accountID, d.Id()) + items, err := client.ListListItems(ctx, cloudflare.ListListItemsParams{ + AccountID: accountID, + ID: d.Id(), + }) if err != nil { return diag.FromErr(errors.Wrap(err, fmt.Sprintf("error reading IP List Items"))) } @@ -106,14 +121,22 @@ func resourceCloudflareIPListUpdate(ctx context.Context, d *schema.ResourceData, client := meta.(*cloudflare.API) accountID := d.Get("account_id").(string) - _, err := client.UpdateIPList(ctx, accountID, d.Id(), d.Get("description").(string)) + _, err := client.UpdateList(ctx, cloudflare.ListUpdateParams{ + AccountID: accountID, + ID: d.Id(), + Description: d.Get("description").(string), + }) if err != nil { return diag.FromErr(errors.Wrap(err, fmt.Sprintf("error updating IP List description"))) } if items, ok := d.GetOk("item"); ok { IPListItems := buildIPListItemsCreateRequest(items.(*schema.Set).List()) - _, err = client.ReplaceIPListItems(ctx, accountID, d.Id(), IPListItems) + _, err = client.ReplaceListItems(ctx, cloudflare.ListReplaceItemsParams{ + AccountID: accountID, + ID: d.Id(), + Items: IPListItems, + }) if err != nil { return diag.FromErr(errors.Wrap(err, fmt.Sprintf("error creating IP List Items"))) } @@ -126,7 +149,10 @@ func resourceCloudflareIPListDelete(ctx context.Context, d *schema.ResourceData, client := meta.(*cloudflare.API) accountID := d.Get("account_id").(string) - _, err := client.DeleteIPList(ctx, accountID, d.Id()) + _, err := client.DeleteList(ctx, cloudflare.ListDeleteParams{ + AccountID: accountID, + ID: d.Id(), + }) if err != nil { return diag.FromErr(errors.Wrap(err, fmt.Sprintf("error deleting IP List with ID %q", d.Id()))) } @@ -134,12 +160,12 @@ func resourceCloudflareIPListDelete(ctx context.Context, d *schema.ResourceData, return nil } -func buildIPListItemsCreateRequest(items []interface{}) []cloudflare.IPListItemCreateRequest { - var IPListItems []cloudflare.IPListItemCreateRequest +func buildIPListItemsCreateRequest(items []interface{}) []cloudflare.ListItemCreateRequest { + var IPListItems []cloudflare.ListItemCreateRequest for _, item := range items { - IPListItems = append(IPListItems, cloudflare.IPListItemCreateRequest{ - IP: item.(map[string]interface{})["value"].(string), + IPListItems = append(IPListItems, cloudflare.ListItemCreateRequest{ + IP: cloudflare.StringPtr(item.(map[string]interface{})["value"].(string)), Comment: item.(map[string]interface{})["comment"].(string), }) } From 0c574cba7ac03d9c6ed1a64eeea980b46b42e0ba Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Mon, 13 Jun 2022 16:53:49 +0100 Subject: [PATCH 2/6] Created new resource for lists. --- internal/provider/provider.go | 1 + internal/provider/resource_cloudflare_list.go | 178 +++++++++++++++ .../provider/resource_cloudflare_list_test.go | 203 ++++++++++++++++++ .../provider/schema_cloudflare_ip_list.go | 4 +- internal/provider/schema_cloudflare_list.go | 61 ++++++ 5 files changed, 445 insertions(+), 2 deletions(-) create mode 100644 internal/provider/resource_cloudflare_list.go create mode 100644 internal/provider/resource_cloudflare_list_test.go create mode 100644 internal/provider/schema_cloudflare_list.go diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 031fce9b3c..dab0baa62c 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -190,6 +190,7 @@ func New(version string) func() *schema.Provider { "cloudflare_healthcheck": resourceCloudflareHealthcheck(), "cloudflare_ip_list": resourceCloudflareIPList(), "cloudflare_ipsec_tunnel": resourceCloudflareIPsecTunnel(), + "cloudflare_list": resourceCloudflareList(), "cloudflare_load_balancer_monitor": resourceCloudflareLoadBalancerMonitor(), "cloudflare_load_balancer_pool": resourceCloudflareLoadBalancerPool(), "cloudflare_load_balancer": resourceCloudflareLoadBalancer(), diff --git a/internal/provider/resource_cloudflare_list.go b/internal/provider/resource_cloudflare_list.go new file mode 100644 index 0000000000..f04557e234 --- /dev/null +++ b/internal/provider/resource_cloudflare_list.go @@ -0,0 +1,178 @@ +package provider + +import ( + "context" + "fmt" + "strings" + + "github.com/cloudflare/cloudflare-go" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pkg/errors" +) + +func resourceCloudflareList() *schema.Resource { + return &schema.Resource{ + Schema: resourceCloudflareListSchema(), + CreateContext: resourceCloudflareListCreate, + ReadContext: resourceCloudflareListRead, + UpdateContext: resourceCloudflareListUpdate, + DeleteContext: resourceCloudflareListDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceCloudflareListImport, + }, + } +} + +func resourceCloudflareListCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + accountID := d.Get("account_id").(string) + + list, err := client.CreateList(ctx, cloudflare.ListCreateParams{ + AccountID: accountID, + Name: d.Get("name").(string), + Description: d.Get("description").(string), + Kind: d.Get("kind").(string), + }) + if err != nil { + return diag.FromErr(errors.Wrap(err, fmt.Sprintf("error creating List %s", d.Get("name").(string)))) + } + + d.SetId(list.ID) + + if items, ok := d.GetOk("item"); ok { + items := buildListItemsCreateRequest(items.(*schema.Set).List()) + _, err = client.CreateListItems(ctx, cloudflare.ListCreateItemsParams{ + AccountID: accountID, + ID: d.Id(), + Items: items, + }) + if err != nil { + return diag.FromErr(errors.Wrap(err, fmt.Sprintf("error creating List Items"))) + } + } + + return resourceCloudflareListRead(ctx, d, meta) +} + +func resourceCloudflareListImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + attributes := strings.SplitN(d.Id(), "/", 2) + + if len(attributes) != 2 { + return nil, fmt.Errorf("invalid id (\"%s\") specified, should be in format \"accountID/listID\"", d.Id()) + } + + accountID, listID := attributes[0], attributes[1] + d.SetId(listID) + d.Set("account_id", accountID) + + resourceCloudflareListRead(ctx, d, meta) + + return []*schema.ResourceData{d}, nil +} + +func resourceCloudflareListRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + accountID := d.Get("account_id").(string) + + list, err := client.GetList(ctx, cloudflare.ListGetParams{ + AccountID: accountID, + ID: d.Id(), + }) + if err != nil { + if strings.Contains(err.Error(), "could not find list") { + tflog.Info(ctx, fmt.Sprintf("List %s no longer exists", d.Id())) + d.SetId("") + return nil + } + return diag.FromErr(errors.Wrap(err, fmt.Sprintf("error reading List with ID %q", d.Id()))) + } + + d.Set("name", list.Name) + d.Set("description", list.Description) + d.Set("kind", list.Kind) + + items, err := client.ListListItems(ctx, cloudflare.ListListItemsParams{ + AccountID: accountID, + ID: d.Id(), + }) + if err != nil { + return diag.FromErr(errors.Wrap(err, fmt.Sprintf("error reading List Items"))) + } + + var itemData []map[string]interface{} + var item map[string]interface{} + + for _, i := range items { + item = make(map[string]interface{}) + item["value"] = []map[string]interface{}{ + {"ip": i.IP}, + } + item["comment"] = i.Comment + + itemData = append(itemData, item) + } + + d.Set("item", itemData) + + return nil +} + +func resourceCloudflareListUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + accountID := d.Get("account_id").(string) + + _, err := client.UpdateList(ctx, cloudflare.ListUpdateParams{ + AccountID: accountID, + ID: d.Id(), + Description: d.Get("description").(string), + }) + if err != nil { + return diag.FromErr(errors.Wrap(err, fmt.Sprintf("error updating List description"))) + } + + if items, ok := d.GetOk("item"); ok { + items := buildListItemsCreateRequest(items.(*schema.Set).List()) + _, err = client.ReplaceListItems(ctx, cloudflare.ListReplaceItemsParams{ + AccountID: accountID, + ID: d.Id(), + Items: items, + }) + if err != nil { + return diag.FromErr(errors.Wrap(err, fmt.Sprintf("error creating List Items"))) + } + } + + return resourceCloudflareListRead(ctx, d, meta) +} + +func resourceCloudflareListDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + accountID := d.Get("account_id").(string) + + _, err := client.DeleteList(ctx, cloudflare.ListDeleteParams{ + AccountID: accountID, + ID: d.Id(), + }) + if err != nil { + return diag.FromErr(errors.Wrap(err, fmt.Sprintf("error deleting List with ID %q", d.Id()))) + } + + return nil +} + +func buildListItemsCreateRequest(items []interface{}) []cloudflare.ListItemCreateRequest { + var listItems []cloudflare.ListItemCreateRequest + + for _, item := range items { + value := item.(map[string]interface{})["value"].([]interface{})[0] + + listItems = append(listItems, cloudflare.ListItemCreateRequest{ + IP: cloudflare.StringPtr(value.(map[string]interface{})["ip"].(string)), + Comment: item.(map[string]interface{})["comment"].(string), + }) + } + + return listItems +} diff --git a/internal/provider/resource_cloudflare_list_test.go b/internal/provider/resource_cloudflare_list_test.go new file mode 100644 index 0000000000..c40e57a029 --- /dev/null +++ b/internal/provider/resource_cloudflare_list_test.go @@ -0,0 +1,203 @@ +package provider + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/cloudflare/cloudflare-go" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccCloudflareList_Exists(t *testing.T) { + // Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the IP List + // endpoint does not yet support the API tokens. + if os.Getenv("CLOUDFLARE_API_TOKEN") != "" { + defer func(apiToken string) { + os.Setenv("CLOUDFLARE_API_TOKEN", apiToken) + }(os.Getenv("CLOUDFLARE_API_TOKEN")) + os.Setenv("CLOUDFLARE_API_TOKEN", "") + } + + rnd := generateRandomResourceName() + name := fmt.Sprintf("cloudflare_list.%s", rnd) + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + + var list cloudflare.List + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckAccount(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareList(rnd, rnd, rnd, accountID), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareListExists(name, &list), + resource.TestCheckResourceAttr( + name, "name", rnd), + ), + }, + }, + }) +} + +func TestAccCloudflareList_UpdateDescription(t *testing.T) { + // Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the IP List + // endpoint does not yet support the API tokens. + if os.Getenv("CLOUDFLARE_API_TOKEN") != "" { + defer func(apiToken string) { + os.Setenv("CLOUDFLARE_API_TOKEN", apiToken) + }(os.Getenv("CLOUDFLARE_API_TOKEN")) + os.Setenv("CLOUDFLARE_API_TOKEN", "") + } + + rnd := generateRandomResourceName() + name := fmt.Sprintf("cloudflare_list.%s", rnd) + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + + var list cloudflare.List + var initialID string + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckAccount(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareList(rnd, rnd, rnd, accountID), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareListExists(name, &list), + resource.TestCheckResourceAttr( + name, "description", rnd), + ), + }, + { + PreConfig: func() { + initialID = list.ID + }, + Config: testAccCheckCloudflareList(rnd, rnd, rnd+"-updated", accountID), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareListExists(name, &list), + func(state *terraform.State) error { + if initialID != list.ID { + return fmt.Errorf("wanted update but List got recreated (id changed %q -> %q)", initialID, list.ID) + } + return nil + }, + resource.TestCheckResourceAttr(name, "description", rnd+"-updated"), + ), + }, + }, + }) +} + +func TestAccCloudflareList_Update(t *testing.T) { + // Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the IP List + // endpoint does not yet support the API tokens. + if os.Getenv("CLOUDFLARE_API_TOKEN") != "" { + defer func(apiToken string) { + os.Setenv("CLOUDFLARE_API_TOKEN", apiToken) + }(os.Getenv("CLOUDFLARE_API_TOKEN")) + os.Setenv("CLOUDFLARE_API_TOKEN", "") + } + + rnd := generateRandomResourceName() + name := fmt.Sprintf("cloudflare_list.%s", rnd) + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + + var list cloudflare.List + var initialID string + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckAccount(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareList(rnd, rnd, rnd, accountID), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareListExists(name, &list), + resource.TestCheckResourceAttr( + name, "name", rnd), + ), + }, + { + PreConfig: func() { + initialID = list.ID + }, + Config: testAccCheckCloudflareListUpdate(rnd, rnd, rnd, accountID), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareListExists(name, &list), + func(state *terraform.State) error { + if initialID != list.ID { + return fmt.Errorf("wanted update but List got recreated (id changed %q -> %q)", initialID, list.ID) + } + return nil + }, + resource.TestCheckResourceAttr(name, "item.#", "2"), + ), + }, + }, + }) +} + +func testAccCheckCloudflareListExists(n string, list *cloudflare.List) resource.TestCheckFunc { + return func(s *terraform.State) error { + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No List ID is set") + } + + client := testAccProvider.Meta().(*cloudflare.API) + foundList, err := client.GetList(context.Background(), cloudflare.ListGetParams{ + AccountID: accountID, + ID: rs.Primary.ID, + }) + if err != nil { + return err + } + + *list = foundList + + return nil + } +} + +func testAccCheckCloudflareList(ID, name, description, accountID string) string { + return fmt.Sprintf(` + resource "cloudflare_list" "%[1]s" { + account_id = "%[4]s" + name = "%[2]s" + description = "%[3]s" + kind = "ip" + }`, ID, name, description, accountID) +} + +func testAccCheckCloudflareListUpdate(ID, name, description, accountID string) string { + return fmt.Sprintf(` + resource "cloudflare_list" "%[1]s" { + account_id = "%[4]s" + name = "%[2]s" + description = "%[3]s" + kind = "ip" + + item { + value { + ip = "192.0.2.0" + } + comment = "one" + } + + item { + value { + ip = "192.0.2.1" + } + comment = "two" + } + }`, ID, name, description, accountID) +} diff --git a/internal/provider/schema_cloudflare_ip_list.go b/internal/provider/schema_cloudflare_ip_list.go index ee4748e003..12962b34d0 100644 --- a/internal/provider/schema_cloudflare_ip_list.go +++ b/internal/provider/schema_cloudflare_ip_list.go @@ -32,12 +32,12 @@ func resourceCloudflareIPListSchema() map[string]*schema.Schema { "item": { Type: schema.TypeSet, Optional: true, - Elem: listItemElem, + Elem: ipListItemElem, }, } } -var listItemElem = &schema.Resource{ +var ipListItemElem = &schema.Resource{ Schema: map[string]*schema.Schema{ "value": { Type: schema.TypeString, diff --git a/internal/provider/schema_cloudflare_list.go b/internal/provider/schema_cloudflare_list.go new file mode 100644 index 0000000000..5669fdff7a --- /dev/null +++ b/internal/provider/schema_cloudflare_list.go @@ -0,0 +1,61 @@ +package provider + +import ( + "regexp" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceCloudflareListSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "account_id": { + Description: "The account identifier to target for the resource.", + Type: schema.TypeString, + Required: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile("^[0-9a-z_]+$"), "List name must only contain lowercase letters, numbers and underscores"), + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "kind": { + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"ip"}, false), + Required: true, + }, + "item": { + Type: schema.TypeSet, + Optional: true, + Elem: listItemElem, + }, + } +} + +var listItemElem = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "value": { + Type: schema.TypeList, + MinItems: 1, + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "comment": { + Type: schema.TypeString, + Optional: true, + }, + }, +} From 7f9902fca1d1b11c9a7454d1bd02fa653b72cbad Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 14 Jun 2022 13:34:32 +0100 Subject: [PATCH 3/6] Deprecate resource type `cloudflare_ip_list` in favor of `cloudflare_list`. --- internal/provider/resource_cloudflare_ip_list.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/provider/resource_cloudflare_ip_list.go b/internal/provider/resource_cloudflare_ip_list.go index 8df678fbf7..e047d9b160 100644 --- a/internal/provider/resource_cloudflare_ip_list.go +++ b/internal/provider/resource_cloudflare_ip_list.go @@ -12,6 +12,9 @@ import ( "github.com/pkg/errors" ) +// The resource in this file is deprecated and should be removed on the next major release. +// Use the more general `list` resource instead. + func resourceCloudflareIPList() *schema.Resource { return &schema.Resource{ Schema: resourceCloudflareIPListSchema(), @@ -22,6 +25,7 @@ func resourceCloudflareIPList() *schema.Resource { Importer: &schema.ResourceImporter{ StateContext: resourceCloudflareIPListImport, }, + DeprecationMessage: "This resource is deprecated, use the `cloudflare_list` instead.", } } From cd7c11c26ce4d54d0eed12c8ffb539347ba08e57 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 15 Jun 2022 13:45:24 +0100 Subject: [PATCH 4/6] Added support for redirect lists. --- .changelog/1700.txt | 7 ++ internal/provider/resource_cloudflare_list.go | 90 ++++++++++++++++-- .../provider/resource_cloudflare_list_test.go | 95 +++++++++++++++---- internal/provider/schema_cloudflare_list.go | 61 ++++++++++-- 4 files changed, 221 insertions(+), 32 deletions(-) create mode 100644 .changelog/1700.txt diff --git a/.changelog/1700.txt b/.changelog/1700.txt new file mode 100644 index 0000000000..574fd2856c --- /dev/null +++ b/.changelog/1700.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +resource/cloudflare_list: Added support for generic list types, including redirect lists. +``` + +```release-note:note +resource/cloudflare_ip_list: Deprecated cloudflare_ip_list in favor of cloudflare_list. +``` diff --git a/internal/provider/resource_cloudflare_list.go b/internal/provider/resource_cloudflare_list.go index f04557e234..55d510a7c7 100644 --- a/internal/provider/resource_cloudflare_list.go +++ b/internal/provider/resource_cloudflare_list.go @@ -42,7 +42,7 @@ func resourceCloudflareListCreate(ctx context.Context, d *schema.ResourceData, m d.SetId(list.ID) if items, ok := d.GetOk("item"); ok { - items := buildListItemsCreateRequest(items.(*schema.Set).List()) + items := buildListItemsCreateRequest(d, items.([]interface{})) _, err = client.CreateListItems(ctx, cloudflare.ListCreateItemsParams{ AccountID: accountID, ID: d.Id(), @@ -106,9 +106,25 @@ func resourceCloudflareListRead(ctx context.Context, d *schema.ResourceData, met for _, i := range items { item = make(map[string]interface{}) - item["value"] = []map[string]interface{}{ - {"ip": i.IP}, + + value := make(map[string]interface{}) + + if i.IP != nil { + value["ip"] = *i.IP + } + if i.Redirect != nil { + value["redirect"] = []map[string]interface{}{{ + "source_url": i.Redirect.SourceUrl, + "include_subdomains": i.Redirect.IncludeSubdomains, + "target_url": i.Redirect.TargetUrl, + "status_code": i.Redirect.StatusCode, + "preserve_query_string": i.Redirect.PreserveQueryString, + "subpath_matching": i.Redirect.SubpathMatching, + "preserve_path_suffix": i.Redirect.PreservePathSuffix, + }} } + + item["value"] = []map[string]interface{}{value} item["comment"] = i.Comment itemData = append(itemData, item) @@ -133,7 +149,7 @@ func resourceCloudflareListUpdate(ctx context.Context, d *schema.ResourceData, m } if items, ok := d.GetOk("item"); ok { - items := buildListItemsCreateRequest(items.(*schema.Set).List()) + items := buildListItemsCreateRequest(d, items.([]interface{})) _, err = client.ReplaceListItems(ctx, cloudflare.ListReplaceItemsParams{ AccountID: accountID, ID: d.Id(), @@ -162,15 +178,71 @@ func resourceCloudflareListDelete(ctx context.Context, d *schema.ResourceData, m return nil } -func buildListItemsCreateRequest(items []interface{}) []cloudflare.ListItemCreateRequest { +func buildListItemsCreateRequest(resource *schema.ResourceData, items []interface{}) []cloudflare.ListItemCreateRequest { var listItems []cloudflare.ListItemCreateRequest - for _, item := range items { - value := item.(map[string]interface{})["value"].([]interface{})[0] + for i, item := range items { + value := item.(map[string]interface{})["value"].([]interface{})[0].(map[string]interface{}) + + _, hasIP := resource.GetOkExists(fmt.Sprintf("item.%d.value.0.ip", i)) + + var ip *string = nil + if hasIP { + maybeIP := value["ip"].(string) + ip = &maybeIP + } + + _, hasRedirect := resource.GetOkExists(fmt.Sprintf("item.%d.value.0.redirect", i)) + + var redirect *cloudflare.Redirect = nil + if hasRedirect { + r := value["redirect"].([]interface{})[0].(map[string]interface{}) + + sourceUrl := r["source_url"].(string) + targetUrl := r["target_url"].(string) + + var includeSubdomains *bool = nil + var subpathMatching *bool = nil + var statusCode *int = nil + var preserveQueryString *bool = nil + var preservePathSuffix *bool = nil + + hasField := func(field string) bool { + _, has := resource.GetOkExists(fmt.Sprintf("item.%d.value.0.redirect.0.%s", i, field)) + return has + } + + if hasField("include_subdomains") { + includeSubdomains = cloudflare.BoolPtr(r["include_subdomains"].(bool)) + } + if hasField("subpath_matching") { + subpathMatching = cloudflare.BoolPtr(r["subpath_matching"].(bool)) + } + if hasField("status_code") { + statusCode = cloudflare.IntPtr(r["status_code"].(int)) + } + if hasField("preserve_query_string") { + preserveQueryString = cloudflare.BoolPtr(r["preserve_query_string"].(bool)) + } + if hasField("preserve_path_suffix") { + preservePathSuffix = cloudflare.BoolPtr(r["preserve_path_suffix"].(bool)) + } + + redirect = &cloudflare.Redirect{ + SourceUrl: sourceUrl, + IncludeSubdomains: includeSubdomains, + TargetUrl: targetUrl, + StatusCode: statusCode, + PreserveQueryString: preserveQueryString, + SubpathMatching: subpathMatching, + PreservePathSuffix: preservePathSuffix, + } + } listItems = append(listItems, cloudflare.ListItemCreateRequest{ - IP: cloudflare.StringPtr(value.(map[string]interface{})["ip"].(string)), - Comment: item.(map[string]interface{})["comment"].(string), + IP: ip, + Redirect: redirect, + Comment: item.(map[string]interface{})["comment"].(string), }) } diff --git a/internal/provider/resource_cloudflare_list_test.go b/internal/provider/resource_cloudflare_list_test.go index c40e57a029..77302e55fc 100644 --- a/internal/provider/resource_cloudflare_list_test.go +++ b/internal/provider/resource_cloudflare_list_test.go @@ -32,7 +32,7 @@ func TestAccCloudflareList_Exists(t *testing.T) { ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: testAccCheckCloudflareList(rnd, rnd, rnd, accountID), + Config: testAccCheckCloudflareList(rnd, rnd, rnd, accountID, "ip"), Check: resource.ComposeTestCheckFunc( testAccCheckCloudflareListExists(name, &list), resource.TestCheckResourceAttr( @@ -65,7 +65,7 @@ func TestAccCloudflareList_UpdateDescription(t *testing.T) { ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: testAccCheckCloudflareList(rnd, rnd, rnd, accountID), + Config: testAccCheckCloudflareList(rnd, rnd, rnd, accountID, "ip"), Check: resource.ComposeTestCheckFunc( testAccCheckCloudflareListExists(name, &list), resource.TestCheckResourceAttr( @@ -76,7 +76,7 @@ func TestAccCloudflareList_UpdateDescription(t *testing.T) { PreConfig: func() { initialID = list.ID }, - Config: testAccCheckCloudflareList(rnd, rnd, rnd+"-updated", accountID), + Config: testAccCheckCloudflareList(rnd, rnd, rnd+"-updated", accountID, "ip"), Check: resource.ComposeTestCheckFunc( testAccCheckCloudflareListExists(name, &list), func(state *terraform.State) error { @@ -102,8 +102,12 @@ func TestAccCloudflareList_Update(t *testing.T) { os.Setenv("CLOUDFLARE_API_TOKEN", "") } - rnd := generateRandomResourceName() - name := fmt.Sprintf("cloudflare_list.%s", rnd) + rndIP := generateRandomResourceName() + rndRedirect := generateRandomResourceName() + + nameIP := fmt.Sprintf("cloudflare_list.%s", rndIP) + nameRedirect := fmt.Sprintf("cloudflare_list.%s", rndRedirect) + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") var list cloudflare.List @@ -114,27 +118,51 @@ func TestAccCloudflareList_Update(t *testing.T) { ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: testAccCheckCloudflareList(rnd, rnd, rnd, accountID), + Config: testAccCheckCloudflareList(rndIP, rndIP, rndIP, accountID, "ip"), Check: resource.ComposeTestCheckFunc( - testAccCheckCloudflareListExists(name, &list), + testAccCheckCloudflareListExists(nameIP, &list), resource.TestCheckResourceAttr( - name, "name", rnd), + nameIP, "name", rndIP), ), }, { PreConfig: func() { initialID = list.ID }, - Config: testAccCheckCloudflareListUpdate(rnd, rnd, rnd, accountID), + Config: testAccCheckCloudflareListIPUpdate(rndIP, rndIP, rndIP, accountID), Check: resource.ComposeTestCheckFunc( - testAccCheckCloudflareListExists(name, &list), + testAccCheckCloudflareListExists(nameIP, &list), + func(state *terraform.State) error { + if initialID != list.ID { + return fmt.Errorf("wanted update but List got recreated (id changed %q -> %q)", initialID, list.ID) + } + return nil + }, + resource.TestCheckResourceAttr(nameIP, "item.#", "2"), + ), + }, + { + Config: testAccCheckCloudflareList(rndRedirect, rndRedirect, rndRedirect, accountID, "redirect"), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareListExists(nameRedirect, &list), + resource.TestCheckResourceAttr( + nameRedirect, "name", rndRedirect), + ), + }, + { + PreConfig: func() { + initialID = list.ID + }, + Config: testAccCheckCloudflareListRedirectUpdate(rndRedirect, rndRedirect, rndRedirect, accountID), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareListExists(nameRedirect, &list), func(state *terraform.State) error { if initialID != list.ID { return fmt.Errorf("wanted update but List got recreated (id changed %q -> %q)", initialID, list.ID) } return nil }, - resource.TestCheckResourceAttr(name, "item.#", "2"), + resource.TestCheckResourceAttr(nameRedirect, "item.#", "2"), ), }, }, @@ -168,17 +196,17 @@ func testAccCheckCloudflareListExists(n string, list *cloudflare.List) resource. } } -func testAccCheckCloudflareList(ID, name, description, accountID string) string { +func testAccCheckCloudflareList(ID, name, description, accountID, kind string) string { return fmt.Sprintf(` resource "cloudflare_list" "%[1]s" { account_id = "%[4]s" name = "%[2]s" description = "%[3]s" - kind = "ip" - }`, ID, name, description, accountID) + kind = "%[5]s" + }`, ID, name, description, accountID, kind) } -func testAccCheckCloudflareListUpdate(ID, name, description, accountID string) string { +func testAccCheckCloudflareListIPUpdate(ID, name, description, accountID string) string { return fmt.Sprintf(` resource "cloudflare_list" "%[1]s" { account_id = "%[4]s" @@ -189,7 +217,7 @@ func testAccCheckCloudflareListUpdate(ID, name, description, accountID string) s item { value { ip = "192.0.2.0" - } + } comment = "one" } @@ -201,3 +229,38 @@ func testAccCheckCloudflareListUpdate(ID, name, description, accountID string) s } }`, ID, name, description, accountID) } + +func testAccCheckCloudflareListRedirectUpdate(ID, name, description, accountID string) string { + return fmt.Sprintf(` + resource "cloudflare_list" "%[1]s" { + account_id = "%[4]s" + name = "%[2]s" + description = "%[3]s" + kind = "redirect" + + item { + value { + redirect { + source_url = "cloudflare.com/blog" + target_url = "https://blog.cloudflare.com" + } + } + comment = "one" + } + + item { + value { + redirect { + source_url = "cloudflare.com/foo" + target_url = "https://foo.cloudflare.com" + include_subdomains = true + subpath_matching = true + status_code = 301 + preserve_query_string = true + preserve_path_suffix = false + } + } + comment = "two" + } + }`, ID, name, description, accountID) +} diff --git a/internal/provider/schema_cloudflare_list.go b/internal/provider/schema_cloudflare_list.go index 5669fdff7a..41a454d775 100644 --- a/internal/provider/schema_cloudflare_list.go +++ b/internal/provider/schema_cloudflare_list.go @@ -15,22 +15,25 @@ func resourceCloudflareListSchema() map[string]*schema.Schema { Required: true, }, "name": { + Description: "The name of the list.", Type: schema.TypeString, Required: true, ForceNew: true, ValidateFunc: validation.StringMatch(regexp.MustCompile("^[0-9a-z_]+$"), "List name must only contain lowercase letters, numbers and underscores"), }, "description": { - Type: schema.TypeString, - Optional: true, + Description: "An optional description of the list.", + Type: schema.TypeString, + Optional: true, }, "kind": { + Description: "The type of items the list will contain.", Type: schema.TypeString, - ValidateFunc: validation.StringInSlice([]string{"ip"}, false), + ValidateFunc: validation.StringInSlice([]string{"ip", "redirect"}, false), Required: true, }, "item": { - Type: schema.TypeSet, + Type: schema.TypeList, Optional: true, Elem: listItemElem, }, @@ -48,14 +51,58 @@ var listItemElem = &schema.Resource{ Schema: map[string]*schema.Schema{ "ip": { Type: schema.TypeString, - Required: true, + Optional: true, + }, + "redirect": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "source_url": { + Description: "The source url of the redirect.", + Type: schema.TypeString, + Required: true, + }, + "target_url": { + Description: "The target url of the redirect.", + Type: schema.TypeString, + Required: true, + }, + "include_subdomains": { + Description: "Whether the redirect also matches subdomains of the source url.", + Type: schema.TypeBool, + Optional: true, + }, + "subpath_matching": { + Description: "Whether the redirect also matches subpaths of the source url.", + Type: schema.TypeBool, + Optional: true, + }, + "status_code": { + Description: "The status code to be used when redirecting a request.", + Type: schema.TypeInt, + Optional: true, + }, + "preserve_query_string": { + Description: "Whether the redirect target url should keep the query string of the request's url.", + Type: schema.TypeBool, + Optional: true, + }, + "preserve_path_suffix": { + Description: "Whether to preserve the path suffix when doing subpath matching.", + Type: schema.TypeBool, + Optional: true, + }, + }, + }, }, }, }, }, "comment": { - Type: schema.TypeString, - Optional: true, + Description: "An optional comment for the item.", + Type: schema.TypeString, + Optional: true, }, }, } From fdc1f829e96955582607f4ac96167a1f2072013f Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Thu, 23 Jun 2022 14:34:30 +1000 Subject: [PATCH 5/6] add generated docs --- docs/resources/list.md | 128 ++++++++++++++++++ examples/resources/cloudflare_list/import.sh | 1 + .../resources/cloudflare_list/resource.tf | 54 ++++++++ internal/provider/resource_cloudflare_list.go | 1 + 4 files changed, 184 insertions(+) create mode 100644 docs/resources/list.md create mode 100644 examples/resources/cloudflare_list/import.sh create mode 100644 examples/resources/cloudflare_list/resource.tf diff --git a/docs/resources/list.md b/docs/resources/list.md new file mode 100644 index 0000000000..d19fc0c90d --- /dev/null +++ b/docs/resources/list.md @@ -0,0 +1,128 @@ +--- +page_title: "cloudflare_list Resource - Cloudflare" +subcategory: "" +description: |- + Provides Lists (IPs, Redirects) to be used in Edge Rules Engine across all zones within the same account. +--- + +# cloudflare_list (Resource) + +Provides Lists (IPs, Redirects) to be used in Edge Rules Engine across all zones within the same account. + +## Example Usage + +```terraform +# IP list +resource "cloudflare_list" "example" { + account_id = "919f297a62fdfb28844177128ed4d331" + name = "example list" + description = "example IPs for a list" + kind = "ip" + + item { + value { + ip = "192.0.2.0" + } + comment = "one" + } + + item { + value { + ip = "192.0.2.1" + } + comment = "two" + } +} + +# Redirect list +resource "cloudflare_list" "example" { + account_id = "919f297a62fdfb28844177128ed4d331" + name = "example list" + description = "example redirects for a list" + kind = "redirect" + + item { + value { + redirect { + source_url = "example.com/blog" + target_url = "https://blog.example.com" + } + } + comment = "one" + } + + item { + value { + redirect { + source_url = "example.com/foo" + target_url = "https://foo.example.com" + include_subdomains = true + subpath_matching = true + status_code = 301 + preserve_query_string = true + preserve_path_suffix = false + } + } + comment = "two" + } +} +``` + +## Schema + +### Required + +- `account_id` (String) The account identifier to target for the resource. +- `kind` (String) The type of items the list will contain. +- `name` (String) The name of the list. + +### Optional + +- `description` (String) An optional description of the list. +- `item` (Block List) (see [below for nested schema](#nestedblock--item)) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `item` + +Required: + +- `value` (Block List, Min: 1, Max: 1) (see [below for nested schema](#nestedblock--item--value)) + +Optional: + +- `comment` (String) An optional comment for the item. + + +### Nested Schema for `item.value` + +Optional: + +- `ip` (String) +- `redirect` (Block List) (see [below for nested schema](#nestedblock--item--value--redirect)) + + +### Nested Schema for `item.value.redirect` + +Required: + +- `source_url` (String) The source url of the redirect. +- `target_url` (String) The target url of the redirect. + +Optional: + +- `include_subdomains` (Boolean) Whether the redirect also matches subdomains of the source url. +- `preserve_path_suffix` (Boolean) Whether to preserve the path suffix when doing subpath matching. +- `preserve_query_string` (Boolean) Whether the redirect target url should keep the query string of the request's url. +- `status_code` (Number) The status code to be used when redirecting a request. +- `subpath_matching` (Boolean) Whether the redirect also matches subpaths of the source url. + +## Import + +Import is supported using the following syntax: +```shell +$ terraform import cloudflare_list.example / +``` diff --git a/examples/resources/cloudflare_list/import.sh b/examples/resources/cloudflare_list/import.sh new file mode 100644 index 0000000000..665ec4b7dc --- /dev/null +++ b/examples/resources/cloudflare_list/import.sh @@ -0,0 +1 @@ +$ terraform import cloudflare_list.example / diff --git a/examples/resources/cloudflare_list/resource.tf b/examples/resources/cloudflare_list/resource.tf new file mode 100644 index 0000000000..d637d4d080 --- /dev/null +++ b/examples/resources/cloudflare_list/resource.tf @@ -0,0 +1,54 @@ +# IP list +resource "cloudflare_list" "example" { + account_id = "919f297a62fdfb28844177128ed4d331" + name = "example list" + description = "example IPs for a list" + kind = "ip" + + item { + value { + ip = "192.0.2.0" + } + comment = "one" + } + + item { + value { + ip = "192.0.2.1" + } + comment = "two" + } +} + +# Redirect list +resource "cloudflare_list" "example" { + account_id = "919f297a62fdfb28844177128ed4d331" + name = "example list" + description = "example redirects for a list" + kind = "redirect" + + item { + value { + redirect { + source_url = "example.com/blog" + target_url = "https://blog.example.com" + } + } + comment = "one" + } + + item { + value { + redirect { + source_url = "example.com/foo" + target_url = "https://foo.example.com" + include_subdomains = true + subpath_matching = true + status_code = 301 + preserve_query_string = true + preserve_path_suffix = false + } + } + comment = "two" + } +} diff --git a/internal/provider/resource_cloudflare_list.go b/internal/provider/resource_cloudflare_list.go index 55d510a7c7..6c591956c1 100644 --- a/internal/provider/resource_cloudflare_list.go +++ b/internal/provider/resource_cloudflare_list.go @@ -22,6 +22,7 @@ func resourceCloudflareList() *schema.Resource { Importer: &schema.ResourceImporter{ StateContext: resourceCloudflareListImport, }, + Description: "Provides Lists (IPs, Redirects) to be used in Edge Rules Engine across all zones within the same account.", } } From f508cbef3517fa769c863b6136cbc3aeccfb8382 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Thu, 23 Jun 2022 14:50:04 +1000 Subject: [PATCH 6/6] fix stub client setup in tests --- .../provider/resource_cloudflare_ip_list_test.go | 10 ++++++++-- .../provider/resource_cloudflare_list_test.go | 15 ++++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/internal/provider/resource_cloudflare_ip_list_test.go b/internal/provider/resource_cloudflare_ip_list_test.go index b742c3439c..6e5f211237 100644 --- a/internal/provider/resource_cloudflare_ip_list_test.go +++ b/internal/provider/resource_cloudflare_ip_list_test.go @@ -28,7 +28,10 @@ func TestAccCloudflareIPList_Exists(t *testing.T) { var IPList cloudflare.IPList resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheckAccount(t) }, + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckAccount(t) + }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { @@ -61,7 +64,10 @@ func TestAccCloudflareIPList_UpdateDescription(t *testing.T) { var initialID string resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheckAccount(t) }, + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckAccount(t) + }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { diff --git a/internal/provider/resource_cloudflare_list_test.go b/internal/provider/resource_cloudflare_list_test.go index 77302e55fc..484bf8efee 100644 --- a/internal/provider/resource_cloudflare_list_test.go +++ b/internal/provider/resource_cloudflare_list_test.go @@ -28,7 +28,10 @@ func TestAccCloudflareList_Exists(t *testing.T) { var list cloudflare.List resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheckAccount(t) }, + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckAccount(t) + }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { @@ -61,7 +64,10 @@ func TestAccCloudflareList_UpdateDescription(t *testing.T) { var initialID string resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheckAccount(t) }, + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckAccount(t) + }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { @@ -114,7 +120,10 @@ func TestAccCloudflareList_Update(t *testing.T) { var initialID string resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheckAccount(t) }, + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckAccount(t) + }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ {