From 3c24e4284c27743904546626a2f6f5746bbb713d Mon Sep 17 00:00:00 2001 From: Ryan Whelan <4249368+rwhelan@users.noreply.github.com> Date: Mon, 20 Mar 2023 11:30:10 -0400 Subject: [PATCH 01/12] feat: cloudflare_list_item resource --- .changelog/2295.txt | 3 + docs/resources/access_organization.md | 1 + docs/resources/list.md | 5 +- docs/resources/list_item.md | 85 +++++++ docs/resources/worker_script.md | 1 + .../resources/cloudflare_list_item/import.sh | 1 + .../cloudflare_list_item/resource.tf | 27 +++ internal/sdkv2provider/provider.go | 1 + .../sdkv2provider/resource_cloudflare_list.go | 4 + .../resource_cloudflare_list_item.go | 210 ++++++++++++++++++ .../resource_cloudflare_list_item_test.go | 151 +++++++++++++ .../sdkv2provider/schema_cloudflare_list.go | 1 + .../schema_cloudflare_list_item.go | 84 +++++++ templates/resources/list.md.tmpl | 28 +++ 14 files changed, 601 insertions(+), 1 deletion(-) create mode 100644 .changelog/2295.txt create mode 100644 docs/resources/list_item.md create mode 100644 examples/resources/cloudflare_list_item/import.sh create mode 100644 examples/resources/cloudflare_list_item/resource.tf create mode 100644 internal/sdkv2provider/resource_cloudflare_list_item.go create mode 100644 internal/sdkv2provider/resource_cloudflare_list_item_test.go create mode 100644 internal/sdkv2provider/schema_cloudflare_list_item.go create mode 100644 templates/resources/list.md.tmpl diff --git a/.changelog/2295.txt b/.changelog/2295.txt new file mode 100644 index 0000000000..54bf5029d9 --- /dev/null +++ b/.changelog/2295.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +cloudflare_list_item +``` diff --git a/docs/resources/access_organization.md b/docs/resources/access_organization.md index c551d4d296..30484f3d5f 100644 --- a/docs/resources/access_organization.md +++ b/docs/resources/access_organization.md @@ -41,6 +41,7 @@ resource "cloudflare_access_organization" "example" { - `is_ui_read_only` (Boolean) When set to true, this will disable all editing of Access resources via the Zero Trust Dashboard. - `login_design` (Block List) (see [below for nested schema](#nestedblock--login_design)) - `name` (String) The name of your Zero Trust organization. +- `ui_read_only_toggle_reason` (String) A description of the reason why the UI read only field is being toggled. - `user_seat_expiration_inactive_time` (String) The amount of time a user seat is inactive before it expires. When the user seat exceeds the set time of inactivity, the user is removed as an active seat and no longer counts against your Teams seat count. Must be in the format `300ms` or `2h45m`. - `zone_id` (String) The zone identifier to target for the resource. Conflicts with `account_id`. diff --git a/docs/resources/list.md b/docs/resources/list.md index 976d086b8c..8c450928cc 100644 --- a/docs/resources/list.md +++ b/docs/resources/list.md @@ -11,6 +11,8 @@ description: |- Provides Lists (IPs, Redirects) to be used in Edge Rules Engine across all zones within the same account. +~> The `cloudflare_list` resource supports defining list items in line with the `item` attribute. The provider also has a `cloudflare_list_item` resource for managing items as independent resources. Using both in line `item` definitions _and_ `cloudflare_list_items` on the same list is not supported and will cause Terraform into an irreconcilable state + ## Example Usage ```terraform @@ -69,13 +71,14 @@ resource "cloudflare_list" "example" { } } ``` + ## Schema ### Required - `account_id` (String) The account identifier to target for the resource. -- `kind` (String) The type of items the list will contain. +- `kind` (String) The type of items the list will contain. **Modifying this attribute will force creation of a new resource.** - `name` (String) The name of the list. **Modifying this attribute will force creation of a new resource.** ### Optional diff --git a/docs/resources/list_item.md b/docs/resources/list_item.md new file mode 100644 index 0000000000..03525a7331 --- /dev/null +++ b/docs/resources/list_item.md @@ -0,0 +1,85 @@ +--- +page_title: "cloudflare_list_item Resource - Cloudflare" +subcategory: "" +description: |- + Provides individual list items (IPs, Redirects) to be used in Edge Rules Engine + across all zones within the same account. +--- + +# cloudflare_list_item (Resource) + +Provides individual list items (IPs, Redirects) to be used in Edge Rules Engine +across all zones within the same account. + +## Example Usage + +```terraform +resource "cloudflare_list" "example_ip_list" { + account_id = "01234567890123456789012345678901" + name = "example_list" + description = "example IPs for a list" + kind = "ip" +} + +# IP List Item +resource "cloudflare_list_item" example_ip_item" { + account_id = "01234567890123456789012345678901" + list_id = data.cloudflare_list.example_ip_list.id + comment = "List Item Comment" + ip = "192.0.2.0" +} + + +# Redirect List Item +resource "cloudflare_list_item" "test_two" { + account_id = "01234567890123456789012345678901" + list_id = data.cloudflare_list.example_ip_list.id + redirect { + source_url = "https://source.tld" + target_url = "https://target.tld" + status_code = 302 + subpath_matching = "enabled" + } +} +``` + +## Schema + +### Required + +- `account_id` (String) The account identifier to target for the resource. +- `list_id` (String) The list identifier to target for the resource. + +### Optional + +- `comment` (String) An optional comment for the item. +- `ip` (String) Must provide only one of `ip`, `redirect`. **Modifying this attribute will force creation of a new resource.** +- `redirect` (Block List, Max: 1) **Modifying this attribute will force creation of a new resource.** (see [below for nested schema](#nestedblock--redirect)) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `redirect` + +Required: + +- `source_url` (String) The source url of the redirect. +- `target_url` (String) The target url of the redirect. + +Optional: + +- `include_subdomains` (String) Whether the redirect also matches subdomains of the source url. Available values: `disabled`, `enabled`. +- `preserve_path_suffix` (String) Whether to preserve the path suffix when doing subpath matching. Available values: `disabled`, `enabled`. +- `preserve_query_string` (String) Whether the redirect target url should keep the query string of the request's url. Available values: `disabled`, `enabled`. +- `status_code` (Number) The status code to be used when redirecting a request. +- `subpath_matching` (String) Whether the redirect also matches subpaths of the source url. Available values: `disabled`, `enabled`. + +## Import + +Import is supported using the following syntax: + +```shell +$ terraform import cloudflare_list.example // +``` diff --git a/docs/resources/worker_script.md b/docs/resources/worker_script.md index 76b0e5dc54..fd70892c38 100644 --- a/docs/resources/worker_script.md +++ b/docs/resources/worker_script.md @@ -76,6 +76,7 @@ resource "cloudflare_worker_script" "my_script" { ### Optional - `analytics_engine_binding` (Block Set) (see [below for nested schema](#nestedblock--analytics_engine_binding)) +- `compatibility_date` (String) The date to use for the compatibility flag. - `kv_namespace_binding` (Block Set) (see [below for nested schema](#nestedblock--kv_namespace_binding)) - `module` (Boolean) Whether to upload Worker as a module. - `compatibility_date` (String) The date to use for the compatibility flag. This is used to determine which version of the Workers runtime to use. The date must be in the format `YYYY-MM-DD`. diff --git a/examples/resources/cloudflare_list_item/import.sh b/examples/resources/cloudflare_list_item/import.sh new file mode 100644 index 0000000000..419da0678d --- /dev/null +++ b/examples/resources/cloudflare_list_item/import.sh @@ -0,0 +1 @@ +$ terraform import cloudflare_list.example // diff --git a/examples/resources/cloudflare_list_item/resource.tf b/examples/resources/cloudflare_list_item/resource.tf new file mode 100644 index 0000000000..b0f3038a6b --- /dev/null +++ b/examples/resources/cloudflare_list_item/resource.tf @@ -0,0 +1,27 @@ +resource "cloudflare_list" "example_ip_list" { + account_id = "01234567890123456789012345678901" + name = "example_list" + description = "example IPs for a list" + kind = "ip" +} + +# IP List Item +resource "cloudflare_list_item" example_ip_item" { + account_id = "01234567890123456789012345678901" + list_id = data.cloudflare_list.example_ip_list.id + comment = "List Item Comment" + ip = "192.0.2.0" +} + + +# Redirect List Item +resource "cloudflare_list_item" "test_two" { + account_id = "01234567890123456789012345678901" + list_id = data.cloudflare_list.example_ip_list.id + redirect { + source_url = "https://source.tld" + target_url = "https://target.tld" + status_code = 302 + subpath_matching = "enabled" + } +} \ No newline at end of file diff --git a/internal/sdkv2provider/provider.go b/internal/sdkv2provider/provider.go index 1f43951f13..d2c05059b8 100644 --- a/internal/sdkv2provider/provider.go +++ b/internal/sdkv2provider/provider.go @@ -211,6 +211,7 @@ func New(version string) func() *schema.Provider { "cloudflare_gre_tunnel": resourceCloudflareGRETunnel(), "cloudflare_healthcheck": resourceCloudflareHealthcheck(), "cloudflare_ipsec_tunnel": resourceCloudflareIPsecTunnel(), + "cloudflare_list_item": resourceCloudflareListItem(), "cloudflare_list": resourceCloudflareList(), "cloudflare_load_balancer_monitor": resourceCloudflareLoadBalancerMonitor(), "cloudflare_load_balancer_pool": resourceCloudflareLoadBalancerPool(), diff --git a/internal/sdkv2provider/resource_cloudflare_list.go b/internal/sdkv2provider/resource_cloudflare_list.go index 48fb9154df..d2dd771e4e 100644 --- a/internal/sdkv2provider/resource_cloudflare_list.go +++ b/internal/sdkv2provider/resource_cloudflare_list.go @@ -94,6 +94,10 @@ func resourceCloudflareListRead(ctx context.Context, d *schema.ResourceData, met d.Set("description", list.Description) d.Set("kind", list.Kind) + if !d.HasChange("items") && len(d.Get("item").(*schema.Set).List()) == 0 { + return nil + } + items, err := client.ListListItems(ctx, cloudflare.AccountIdentifier(accountID), cloudflare.ListListItemsParams{ ID: d.Id(), }) diff --git a/internal/sdkv2provider/resource_cloudflare_list_item.go b/internal/sdkv2provider/resource_cloudflare_list_item.go new file mode 100644 index 0000000000..00a3e4e47a --- /dev/null +++ b/internal/sdkv2provider/resource_cloudflare_list_item.go @@ -0,0 +1,210 @@ +package sdkv2provider + +import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/MakeNowJust/heredoc/v2" + "github.com/cloudflare/cloudflare-go" + "github.com/cloudflare/terraform-provider-cloudflare/internal/consts" + "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 resourceCloudflareListItem() *schema.Resource { + return &schema.Resource{ + Schema: resourceCloudflareListItemSchema(), + CreateContext: resourceCloudflareListItemCreate, + ReadContext: resourceCloudflareListItemRead, + UpdateContext: resourceCloudflareListItemUpdate, + DeleteContext: resourceCloudflareListItemDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceCloudflareListItemImport, + }, + Description: heredoc.Doc(` + Provides individual list items (IPs, Redirects) to be used in Edge Rules Engine + across all zones within the same account. + `), + } +} + +func resourceCloudflareListItemCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + accountID := d.Get(consts.AccountIDSchemaKey).(string) + listID := d.Get("list_id").(string) + listItemType := listItemType(d) + + list, err := client.GetList(ctx, cloudflare.AccountIdentifier(accountID), listID) + if err != nil { + return diag.FromErr(fmt.Errorf("unable to find list with id %s: %w", listID, err)) + } + + if list.Kind != listItemType { + return diag.FromErr(fmt.Errorf("items of type %s can not be added to lists of type %s", listItemType, list.Kind)) + } + + createListItemResponse, err := client.CreateListItem(ctx, cloudflare.AccountIdentifier(accountID), cloudflare.ListCreateItemParams{ + ID: listID, + Item: buildListItemCreateRequest(d), + }) + if err != nil { + return diag.FromErr(fmt.Errorf("unable to create list item on list id %s: %w", listID, err)) + } + + newestItem := mostRecentlyCreatedItem(createListItemResponse) + d.SetId(newestItem.ID) + + return nil +} + +func resourceCloudflareListItemImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + attributes := strings.SplitN(d.Id(), "/", 3) + + if len(attributes) != 3 { + return nil, fmt.Errorf("invalid id (\"%s\") specified, should be in format \"accountID/listID/itemID\"", d.Id()) + } + + accountID, listID, itemID := attributes[0], attributes[1], attributes[2] + d.SetId(itemID) + d.Set(consts.AccountIDSchemaKey, accountID) + d.Set("list_id", listID) + + resourceCloudflareListItemRead(ctx, d, meta) + + return []*schema.ResourceData{d}, nil +} + +func resourceCloudflareListItemRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + accountID := d.Get(consts.AccountIDSchemaKey).(string) + listID := d.Get("list_id").(string) + + listItem, err := client.GetListItem(ctx, cloudflare.AccountIdentifier(accountID), listID, d.Id()) + if err != nil { + var notFoundError *cloudflare.NotFoundError + if errors.As(err, ¬FoundError) { + tflog.Info(ctx, fmt.Sprintf("List item %s no longer exists", d.Id())) + d.SetId("") + return nil + } + return diag.FromErr(errors.Wrap(err, fmt.Sprintf("error reading List Item with ID %q", d.Id()))) + } + + d.Set("comment", listItem.Comment) + + if listItem.IP != nil { + d.Set("ip", *listItem.IP) + } + + if listItem.Redirect != nil { + optBoolToString := func(b *bool) string { + if b != nil { + switch *b { + case true: + return "enabled" + case false: + return "disabled" + } + } + return "" + } + + d.Set("source_url", listItem.Redirect.SourceUrl) + d.Set("include_subdomains", optBoolToString(listItem.Redirect.IncludeSubdomains)) + d.Set("target_url", listItem.Redirect.TargetUrl) + d.Set("status_code", listItem.Redirect.StatusCode) + d.Set("preserve_query_string", optBoolToString(listItem.Redirect.PreserveQueryString)) + d.Set("subpath_matching", optBoolToString(listItem.Redirect.SubpathMatching)) + d.Set("preserve_path_suffix", optBoolToString(listItem.Redirect.PreservePathSuffix)) + } + + return nil +} + +func resourceCloudflareListItemUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + cr := resourceCloudflareListItemCreate(ctx, d, meta) + if cr != nil { + return cr + } + + return resourceCloudflareListItemRead(ctx, d, meta) +} + +func resourceCloudflareListItemDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + accountID := d.Get(consts.AccountIDSchemaKey).(string) + listID := d.Get("list_id").(string) + + _, err := client.DeleteListItems(ctx, cloudflare.AccountIdentifier(accountID), cloudflare.ListDeleteItemsParams{ + ID: listID, + Items: cloudflare.ListItemDeleteRequest{ + Items: []cloudflare.ListItemDeleteItemRequest{{ID: d.Id()}}, + }, + }) + if err != nil { + return diag.FromErr(errors.Wrap(err, fmt.Sprintf("error removing List Item %s from list %s", listID, d.Id()))) + } + + return nil +} + +func listItemType(d *schema.ResourceData) string { + if _, ok := d.GetOk("ip"); ok { + return "ip" + } + + return "redirect" +} + +func buildListItemCreateRequest(d *schema.ResourceData) cloudflare.ListItemCreateRequest { + itemType := listItemType(d) + + request := cloudflare.ListItemCreateRequest{ + Comment: d.Get("comment").(string), + } + + if itemType == "ip" { + request.IP = cloudflare.StringPtr(d.Get("ip").(string)) + return request + } + + stringToOptBool := func(r map[string]interface{}, s string) *bool { + switch r[s] { + case "enabled": + return cloudflare.BoolPtr(true) + case "disabled": + return cloudflare.BoolPtr(false) + default: + return nil + } + } + + redirect := d.Get("redirect").([]interface{})[0].(map[string]interface{}) + request.Redirect = &cloudflare.Redirect{ + SourceUrl: redirect["source_url"].(string), + TargetUrl: redirect["target_url"].(string), + } + + if value, ok := redirect["status_code"]; ok && value != 0 { + request.Redirect.StatusCode = cloudflare.IntPtr(value.(int)) + } + + request.Redirect.IncludeSubdomains = stringToOptBool(redirect, "include_subdomains") + request.Redirect.PreserveQueryString = stringToOptBool(redirect, "preserve_query_string") + request.Redirect.SubpathMatching = stringToOptBool(redirect, "subpath_matching") + request.Redirect.PreservePathSuffix = stringToOptBool(redirect, "preserve_path_suffix") + + return request +} + +func mostRecentlyCreatedItem(createListItemResponse []cloudflare.ListItem) cloudflare.ListItem { + sort.Slice(createListItemResponse, func(i, j int) bool { + return createListItemResponse[i].CreatedOn.After(*createListItemResponse[j].CreatedOn) + }) + + return createListItemResponse[0] +} diff --git a/internal/sdkv2provider/resource_cloudflare_list_item_test.go b/internal/sdkv2provider/resource_cloudflare_list_item_test.go new file mode 100644 index 0000000000..ae24f7eac3 --- /dev/null +++ b/internal/sdkv2provider/resource_cloudflare_list_item_test.go @@ -0,0 +1,151 @@ +package sdkv2provider + +import ( + "context" + "fmt" + "os" + "regexp" + "testing" + + "github.com/cloudflare/cloudflare-go" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccCloudflareListItem_Exists(t *testing.T) { + rnd := generateRandomResourceName() + name := fmt.Sprintf("cloudflare_list_item.%s", rnd) + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + + var ListItem cloudflare.ListItem + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckAccount(t) + }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareIPListItem(rnd, rnd, rnd, accountID), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareListItemExists(name, rnd, &ListItem), + resource.TestCheckResourceAttr( + name, "ip", "192.0.2.0"), + ), + }, + }, + }) +} + +func TestAccCloudflareListItem_Update(t *testing.T) { + rnd := generateRandomResourceName() + name := fmt.Sprintf("cloudflare_list_item.%s", rnd) + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + + var listItem cloudflare.ListItem + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckAccount(t) + }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareIPListItem(rnd, rnd, rnd, accountID), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareListItemExists(name, rnd, &listItem), + resource.TestCheckResourceAttr( + name, "ip", "192.0.2.0"), + ), + }, + { + Config: testAccCheckCloudflareIPListItem(rnd, rnd, rnd+"-updated", accountID), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareListItemExists(name, rnd, &listItem), + resource.TestCheckResourceAttr(name, "comment", rnd+"-updated"), + ), + }, + }, + }) +} + +func TestAccCloudflareListItem_BadListItemType(t *testing.T) { + rnd := generateRandomResourceName() + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckAccount(t) + }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareBadListItemType(rnd, rnd, rnd, accountID), + ExpectError: regexp.MustCompile(" can not be added to lists of type "), + }, + }, + }) +} + +func testAccCheckCloudflareIPListItem(ID, name, comment, accountID string) string { + return fmt.Sprintf(` + resource "cloudflare_list" "%[2]s" { + account_id = "%[4]s" + name = "%[2]s" + description = "list named %[2]s" + kind = "ip" + } + + resource "cloudflare_list_item" "%[1]s" { + account_id = "%[4]s" + list_id = cloudflare_list.%[2]s.id + ip = "192.0.2.0" + comment = "%[3]s" + } `, ID, name, comment, accountID) +} + +func testAccCheckCloudflareListItemExists(n string, name string, listItem *cloudflare.ListItem) resource.TestCheckFunc { + return func(s *terraform.State) error { + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + listRS := s.RootModule().Resources["cloudflare_list."+name] + + 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.GetListItem(context.Background(), cloudflare.AccountIdentifier(accountID), listRS.Primary.ID, rs.Primary.ID) + if err != nil { + return err + } + + *listItem = foundList + + return nil + } +} + +func testAccCheckCloudflareBadListItemType(ID, name, comment, accountID string) string { + return fmt.Sprintf(` + resource "cloudflare_list" "%[2]s" { + account_id = "%[4]s" + name = "%[2]s" + description = "list named %[2]s" + kind = "redirect" + } + + resource "cloudflare_list_item" "%[2]s" { + account_id = "%[4]s" + list_id = cloudflare_list.%[2]s.id + ip = "192.0.2.0" + comment = "%[3]s" + } `, ID, name, comment, accountID) +} diff --git a/internal/sdkv2provider/schema_cloudflare_list.go b/internal/sdkv2provider/schema_cloudflare_list.go index 9dd7156d37..e94dd3c884 100644 --- a/internal/sdkv2provider/schema_cloudflare_list.go +++ b/internal/sdkv2provider/schema_cloudflare_list.go @@ -33,6 +33,7 @@ func resourceCloudflareListSchema() map[string]*schema.Schema { Type: schema.TypeString, ValidateFunc: validation.StringInSlice([]string{"ip", "redirect"}, false), Required: true, + ForceNew: true, }, "item": { Type: schema.TypeSet, diff --git a/internal/sdkv2provider/schema_cloudflare_list_item.go b/internal/sdkv2provider/schema_cloudflare_list_item.go new file mode 100644 index 0000000000..55d7522a2a --- /dev/null +++ b/internal/sdkv2provider/schema_cloudflare_list_item.go @@ -0,0 +1,84 @@ +package sdkv2provider + +import ( + "fmt" + + "github.com/cloudflare/terraform-provider-cloudflare/internal/consts" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceCloudflareListItemSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + consts.AccountIDSchemaKey: { + Description: "The account identifier to target for the resource.", + Type: schema.TypeString, + Required: true, + }, + "list_id": { + Description: "The list identifier to target for the resource.", + Type: schema.TypeString, + Required: true, + }, + "ip": { + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{"ip", "redirect"}, + ForceNew: true, + }, + "redirect": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ForceNew: 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: fmt.Sprintf("Whether the redirect also matches subdomains of the source url. %s", renderAvailableDocumentationValuesStringSlice([]string{"disabled", "enabled"})), + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"disabled", "enabled"}, false), + }, + "subpath_matching": { + Description: fmt.Sprintf("Whether the redirect also matches subpaths of the source url. %s", renderAvailableDocumentationValuesStringSlice([]string{"disabled", "enabled"})), + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"disabled", "enabled"}, false), + }, + "status_code": { + Description: "The status code to be used when redirecting a request.", + Type: schema.TypeInt, + Optional: true, + }, + "preserve_query_string": { + Description: fmt.Sprintf("Whether the redirect target url should keep the query string of the request's url. %s", renderAvailableDocumentationValuesStringSlice([]string{"disabled", "enabled"})), + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"disabled", "enabled"}, false), + }, + "preserve_path_suffix": { + Description: fmt.Sprintf("Whether to preserve the path suffix when doing subpath matching. %s", renderAvailableDocumentationValuesStringSlice([]string{"disabled", "enabled"})), + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"disabled", "enabled"}, false), + }, + }, + }, + }, + "comment": { + Description: "An optional comment for the item.", + Type: schema.TypeString, + Optional: true, + }, + } +} diff --git a/templates/resources/list.md.tmpl b/templates/resources/list.md.tmpl new file mode 100644 index 0000000000..8330d78a4d --- /dev/null +++ b/templates/resources/list.md.tmpl @@ -0,0 +1,28 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.RenderedProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +~> The `cloudflare_list` resource supports defining list items in line with the `item` attribute. The provider also has a `cloudflare_list_item` resource for managing items as independent resources. Using both in line `item` definitions _and_ `cloudflare_list_items` on the same list is not supported and will cause Terraform into an irreconcilable state + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "%s%s%s" "examples/resources/" .Name "/resource.tf") }} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} + +{{ if .HasImport -}} +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" (printf "%s%s%s" "examples/resources/" .Name "/import.sh") }} +{{- end }} From 97cd85f0a64f005d13e0c61672e383297ca44c3d Mon Sep 17 00:00:00 2001 From: Ryan Whelan <4249368+rwhelan@users.noreply.github.com> Date: Mon, 20 Mar 2023 11:36:39 -0400 Subject: [PATCH 02/12] chore: new PR number --- .changelog/2296.txt | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .changelog/2296.txt diff --git a/.changelog/2296.txt b/.changelog/2296.txt deleted file mode 100644 index 2ec71af6e0..0000000000 --- a/.changelog/2296.txt +++ /dev/null @@ -1,7 +0,0 @@ -```release-note:new-data-source -cloudflare_list -``` - -```release-note:new-data-source -cloudflare_lists -``` \ No newline at end of file From 50596eac8d3bc0d9fc6f706bd53c7aa187a40f3f Mon Sep 17 00:00:00 2001 From: Ryan Whelan <4249368+rwhelan@users.noreply.github.com> Date: Mon, 20 Mar 2023 12:08:24 -0400 Subject: [PATCH 03/12] doc: changelog file --- .changelog/2304.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/2304.txt diff --git a/.changelog/2304.txt b/.changelog/2304.txt new file mode 100644 index 0000000000..54bf5029d9 --- /dev/null +++ b/.changelog/2304.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +cloudflare_list_item +``` From adc97627cc32da7f9c59ab41b9c99c1fce7c1753 Mon Sep 17 00:00:00 2001 From: Ryan Whelan <4249368+rwhelan@users.noreply.github.com> Date: Mon, 20 Mar 2023 12:10:15 -0400 Subject: [PATCH 04/12] doc: remove old changelog --- .changelog/2295.txt | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .changelog/2295.txt diff --git a/.changelog/2295.txt b/.changelog/2295.txt deleted file mode 100644 index 54bf5029d9..0000000000 --- a/.changelog/2295.txt +++ /dev/null @@ -1,3 +0,0 @@ -```release-note:new-resource -cloudflare_list_item -``` From 37563dcac67c9378ade5123ee50f03b24ef17d18 Mon Sep 17 00:00:00 2001 From: Ryan Whelan <4249368+rwhelan@users.noreply.github.com> Date: Mon, 20 Mar 2023 12:35:35 -0400 Subject: [PATCH 05/12] fix: testSweepCloudflareList() client.DeleteList() --- internal/sdkv2provider/resource_cloudflare_list_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/sdkv2provider/resource_cloudflare_list_test.go b/internal/sdkv2provider/resource_cloudflare_list_test.go index 211d5ea42d..81ad6a7ded 100644 --- a/internal/sdkv2provider/resource_cloudflare_list_test.go +++ b/internal/sdkv2provider/resource_cloudflare_list_test.go @@ -46,7 +46,7 @@ func testSweepCloudflareList(r string) error { for _, list := range lists { tflog.Info(ctx, fmt.Sprintf("Deleting Cloudflare List ID: %s", list.ID)) //nolint:errcheck - client.DeleteLoadBalancerPool(ctx, cloudflare.AccountIdentifier(accountID), list.ID) + client.DeleteList(ctx, cloudflare.AccountIdentifier(accountID), list.ID) } return nil From e783f63a1bf47f422292541965335e637b04456b Mon Sep 17 00:00:00 2001 From: Ryan Whelan <4249368+rwhelan@users.noreply.github.com> Date: Mon, 20 Mar 2023 12:53:01 -0400 Subject: [PATCH 06/12] test: fix acctests --- internal/sdkv2provider/resource_cloudflare_list_item_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/sdkv2provider/resource_cloudflare_list_item_test.go b/internal/sdkv2provider/resource_cloudflare_list_item_test.go index ae24f7eac3..03d5c40cc6 100644 --- a/internal/sdkv2provider/resource_cloudflare_list_item_test.go +++ b/internal/sdkv2provider/resource_cloudflare_list_item_test.go @@ -8,8 +8,8 @@ import ( "testing" "github.com/cloudflare/cloudflare-go" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" ) func TestAccCloudflareListItem_Exists(t *testing.T) { From 44e7673bfce591471be35731a469bf603943fa05 Mon Sep 17 00:00:00 2001 From: Ryan Whelan <4249368+rwhelan@users.noreply.github.com> Date: Mon, 20 Mar 2023 12:55:55 -0400 Subject: [PATCH 07/12] fix: use account f037e56e89293a057740de681ac9abbe in example docs --- examples/resources/cloudflare_list_item/resource.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/resources/cloudflare_list_item/resource.tf b/examples/resources/cloudflare_list_item/resource.tf index b0f3038a6b..592af50a3d 100644 --- a/examples/resources/cloudflare_list_item/resource.tf +++ b/examples/resources/cloudflare_list_item/resource.tf @@ -1,5 +1,5 @@ resource "cloudflare_list" "example_ip_list" { - account_id = "01234567890123456789012345678901" + account_id = "f037e56e89293a057740de681ac9abbe" name = "example_list" description = "example IPs for a list" kind = "ip" @@ -7,7 +7,7 @@ resource "cloudflare_list" "example_ip_list" { # IP List Item resource "cloudflare_list_item" example_ip_item" { - account_id = "01234567890123456789012345678901" + account_id = "f037e56e89293a057740de681ac9abbe" list_id = data.cloudflare_list.example_ip_list.id comment = "List Item Comment" ip = "192.0.2.0" @@ -16,7 +16,7 @@ resource "cloudflare_list_item" example_ip_item" { # Redirect List Item resource "cloudflare_list_item" "test_two" { - account_id = "01234567890123456789012345678901" + account_id = "f037e56e89293a057740de681ac9abbe" list_id = data.cloudflare_list.example_ip_list.id redirect { source_url = "https://source.tld" From b0e94388ee7e4e4d1fd3559b6fdacb6d519a8c11 Mon Sep 17 00:00:00 2001 From: Ryan Whelan <4249368+rwhelan@users.noreply.github.com> Date: Mon, 20 Mar 2023 13:11:37 -0400 Subject: [PATCH 08/12] fix: R002: ResourceData.Set() pointer value dereference is extraneous --- internal/sdkv2provider/resource_cloudflare_list_item.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/sdkv2provider/resource_cloudflare_list_item.go b/internal/sdkv2provider/resource_cloudflare_list_item.go index 00a3e4e47a..0b017f2bc0 100644 --- a/internal/sdkv2provider/resource_cloudflare_list_item.go +++ b/internal/sdkv2provider/resource_cloudflare_list_item.go @@ -97,7 +97,7 @@ func resourceCloudflareListItemRead(ctx context.Context, d *schema.ResourceData, d.Set("comment", listItem.Comment) if listItem.IP != nil { - d.Set("ip", *listItem.IP) + d.Set("ip", listItem.IP) } if listItem.Redirect != nil { From 33d6ecd430475083f737b6d45dd4fb50d8ee7913 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Wed, 22 Mar 2023 10:06:56 +1100 Subject: [PATCH 09/12] fix example --- docs/resources/list_item.md | 18 +++++++++--------- .../resources/cloudflare_list_item/resource.tf | 16 ++++++++-------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/resources/list_item.md b/docs/resources/list_item.md index 03525a7331..784408bb8b 100644 --- a/docs/resources/list_item.md +++ b/docs/resources/list_item.md @@ -15,16 +15,16 @@ across all zones within the same account. ```terraform resource "cloudflare_list" "example_ip_list" { - account_id = "01234567890123456789012345678901" - name = "example_list" - description = "example IPs for a list" - kind = "ip" + account_id = "f037e56e89293a057740de681ac9abbe" + name = "example_list" + description = "example IPs for a list" + kind = "ip" } # IP List Item -resource "cloudflare_list_item" example_ip_item" { - account_id = "01234567890123456789012345678901" - list_id = data.cloudflare_list.example_ip_list.id +resource "cloudflare_list_item" "example_ip_item" { + account_id = "f037e56e89293a057740de681ac9abbe" + list_id = cloudflare_list.example_ip_list.id comment = "List Item Comment" ip = "192.0.2.0" } @@ -32,8 +32,8 @@ resource "cloudflare_list_item" example_ip_item" { # Redirect List Item resource "cloudflare_list_item" "test_two" { - account_id = "01234567890123456789012345678901" - list_id = data.cloudflare_list.example_ip_list.id + account_id = "f037e56e89293a057740de681ac9abbe" + list_id = cloudflare_list.example_ip_list.id redirect { source_url = "https://source.tld" target_url = "https://target.tld" diff --git a/examples/resources/cloudflare_list_item/resource.tf b/examples/resources/cloudflare_list_item/resource.tf index 592af50a3d..5bfcc3de2c 100644 --- a/examples/resources/cloudflare_list_item/resource.tf +++ b/examples/resources/cloudflare_list_item/resource.tf @@ -1,14 +1,14 @@ resource "cloudflare_list" "example_ip_list" { - account_id = "f037e56e89293a057740de681ac9abbe" - name = "example_list" - description = "example IPs for a list" - kind = "ip" + account_id = "f037e56e89293a057740de681ac9abbe" + name = "example_list" + description = "example IPs for a list" + kind = "ip" } # IP List Item -resource "cloudflare_list_item" example_ip_item" { +resource "cloudflare_list_item" "example_ip_item" { account_id = "f037e56e89293a057740de681ac9abbe" - list_id = data.cloudflare_list.example_ip_list.id + list_id = cloudflare_list.example_ip_list.id comment = "List Item Comment" ip = "192.0.2.0" } @@ -17,11 +17,11 @@ resource "cloudflare_list_item" example_ip_item" { # Redirect List Item resource "cloudflare_list_item" "test_two" { account_id = "f037e56e89293a057740de681ac9abbe" - list_id = data.cloudflare_list.example_ip_list.id + list_id = cloudflare_list.example_ip_list.id redirect { source_url = "https://source.tld" target_url = "https://target.tld" status_code = 302 subpath_matching = "enabled" } -} \ No newline at end of file +} From 516d0852ca34c1b1929fb1674aa261b898f90e72 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Wed, 22 Mar 2023 10:08:08 +1100 Subject: [PATCH 10/12] reinstate 2296 changelog --- .changelog/2296.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changelog/2296.txt diff --git a/.changelog/2296.txt b/.changelog/2296.txt new file mode 100644 index 0000000000..a5ce9669b3 --- /dev/null +++ b/.changelog/2296.txt @@ -0,0 +1,7 @@ +```release-note:new-data-source +cloudflare_list +``` + +```release-note:new-data-source +cloudflare_lists +``` From 2cea756b5b54d3a0588e81250f523cc1a5d4a71d Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Wed, 22 Mar 2023 10:39:35 +1100 Subject: [PATCH 11/12] add test coverage for new usage --- docs/resources/list.md | 2 +- .../resource_cloudflare_list_test.go | 62 +++++++++++++++++++ templates/resources/list.md.tmpl | 2 +- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/docs/resources/list.md b/docs/resources/list.md index 8c450928cc..5218c8dbb2 100644 --- a/docs/resources/list.md +++ b/docs/resources/list.md @@ -11,7 +11,7 @@ description: |- Provides Lists (IPs, Redirects) to be used in Edge Rules Engine across all zones within the same account. -~> The `cloudflare_list` resource supports defining list items in line with the `item` attribute. The provider also has a `cloudflare_list_item` resource for managing items as independent resources. Using both in line `item` definitions _and_ `cloudflare_list_items` on the same list is not supported and will cause Terraform into an irreconcilable state +~> The `cloudflare_list` resource supports defining list items in line with the `item` attribute. The provider also has a `cloudflare_list_item` resource for managing items as independent resources. Using both in line `item` definitions _and_ `cloudflare_list_items` on the same list is not supported and will cause Terraform into an irreconcilable state. ## Example Usage diff --git a/internal/sdkv2provider/resource_cloudflare_list_test.go b/internal/sdkv2provider/resource_cloudflare_list_test.go index 81ad6a7ded..c4f56df71c 100644 --- a/internal/sdkv2provider/resource_cloudflare_list_test.go +++ b/internal/sdkv2provider/resource_cloudflare_list_test.go @@ -253,6 +253,51 @@ func TestAccCloudflareList_UpdateIgnoreIPOrdering(t *testing.T) { }) } +func TestAccCloudflareList_RemoveInlineConfig(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") != "" { + t.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() { + testAccPreCheck(t) + testAccPreCheckAccount(t) + }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareList(rnd, rnd, rnd, accountID, "ip"), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareListExists(name, &list), + resource.TestCheckResourceAttr(name, "item.#", "0"), + ), + }, + { + Config: testAccCheckCloudflareListBasicIP(rnd, rnd, rnd, accountID), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareListExists(name, &list), + resource.TestCheckResourceAttr(name, "item.#", "1"), + ), + }, + { + Config: testAccCheckCloudflareList(rnd, rnd, rnd, accountID, "ip"), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareListExists(name, &list), + resource.TestCheckResourceAttr(name, "item.#", "0"), + ), + }, + }, + }) +} + func testAccCheckCloudflareListIPListOrdered(ID, name, description, accountID string) string { return fmt.Sprintf(` resource "cloudflare_list" "%[1]s" { @@ -440,3 +485,20 @@ func testAccCheckCloudflareListRedirectUpdateTargetUrl(ID, name, description, ac } }`, ID, name, description, accountID) } + +func testAccCheckCloudflareListBasicIP(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" + } + }`, ID, name, description, accountID) +} diff --git a/templates/resources/list.md.tmpl b/templates/resources/list.md.tmpl index 8330d78a4d..ac8c17bebd 100644 --- a/templates/resources/list.md.tmpl +++ b/templates/resources/list.md.tmpl @@ -9,7 +9,7 @@ description: |- {{ .Description | trimspace }} -~> The `cloudflare_list` resource supports defining list items in line with the `item` attribute. The provider also has a `cloudflare_list_item` resource for managing items as independent resources. Using both in line `item` definitions _and_ `cloudflare_list_items` on the same list is not supported and will cause Terraform into an irreconcilable state +~> The `cloudflare_list` resource supports defining list items in line with the `item` attribute. The provider also has a `cloudflare_list_item` resource for managing items as independent resources. Using both in line `item` definitions _and_ `cloudflare_list_items` on the same list is not supported and will cause Terraform into an irreconcilable state. {{ if .HasExample -}} ## Example Usage From d94b897d8d5cd84f8755e1a82ecdbc9c6bed5c50 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Wed, 22 Mar 2023 10:53:42 +1100 Subject: [PATCH 12/12] update schema descriptions --- docs/resources/list_item.md | 4 ++-- internal/sdkv2provider/schema_cloudflare_list_item.go | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/resources/list_item.md b/docs/resources/list_item.md index 784408bb8b..eea90f4647 100644 --- a/docs/resources/list_item.md +++ b/docs/resources/list_item.md @@ -53,8 +53,8 @@ resource "cloudflare_list_item" "test_two" { ### Optional - `comment` (String) An optional comment for the item. -- `ip` (String) Must provide only one of `ip`, `redirect`. **Modifying this attribute will force creation of a new resource.** -- `redirect` (Block List, Max: 1) **Modifying this attribute will force creation of a new resource.** (see [below for nested schema](#nestedblock--redirect)) +- `ip` (String) IP address to include in the list. Must provide only one of `ip`, `redirect`. **Modifying this attribute will force creation of a new resource.** +- `redirect` (Block List, Max: 1) Redirect configuration to store in the list. Must provide only one of `ip`, `redirect`. **Modifying this attribute will force creation of a new resource.** (see [below for nested schema](#nestedblock--redirect)) ### Read-Only diff --git a/internal/sdkv2provider/schema_cloudflare_list_item.go b/internal/sdkv2provider/schema_cloudflare_list_item.go index 55d7522a2a..0cf01d632e 100644 --- a/internal/sdkv2provider/schema_cloudflare_list_item.go +++ b/internal/sdkv2provider/schema_cloudflare_list_item.go @@ -23,14 +23,17 @@ func resourceCloudflareListItemSchema() map[string]*schema.Schema { "ip": { Type: schema.TypeString, Optional: true, + Description: "IP address to include in the list.", ExactlyOneOf: []string{"ip", "redirect"}, ForceNew: true, }, "redirect": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - ForceNew: true, + Type: schema.TypeList, + ExactlyOneOf: []string{"ip", "redirect"}, + Description: "Redirect configuration to store in the list.", + Optional: true, + MaxItems: 1, + ForceNew: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "source_url": {