Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cloudflare_list_item resource #2304

Merged
merged 14 commits into from
Mar 21, 2023
Merged
2 changes: 1 addition & 1 deletion .changelog/2296.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ cloudflare_list

```release-note:new-data-source
cloudflare_lists
```
```
3 changes: 3 additions & 0 deletions .changelog/2304.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
cloudflare_list_item
```
5 changes: 4 additions & 1 deletion docs/resources/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -69,13 +71,14 @@ resource "cloudflare_list" "example" {
}
}
```

<!-- schema generated by tfplugindocs -->
## 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
Expand Down
85 changes: 85 additions & 0 deletions docs/resources/list_item.md
Original file line number Diff line number Diff line change
@@ -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 = "f037e56e89293a057740de681ac9abbe"
name = "example_list"
description = "example IPs for a list"
kind = "ip"
}

# IP List Item
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"
}


# Redirect List Item
resource "cloudflare_list_item" "test_two" {
account_id = "f037e56e89293a057740de681ac9abbe"
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"
}
}
```
<!-- schema generated by tfplugindocs -->
## 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) 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

- `id` (String) The ID of this resource.

<a id="nestedblock--redirect"></a>
### 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 <account_id>/<list_id>/<item_id>
```
1 change: 1 addition & 0 deletions examples/resources/cloudflare_list_item/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$ terraform import cloudflare_list.example <account_id>/<list_id>/<item_id>
27 changes: 27 additions & 0 deletions examples/resources/cloudflare_list_item/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
resource "cloudflare_list" "example_ip_list" {
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 = "f037e56e89293a057740de681ac9abbe"
list_id = 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 = "f037e56e89293a057740de681ac9abbe"
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"
}
}
1 change: 1 addition & 0 deletions internal/sdkv2provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
4 changes: 4 additions & 0 deletions internal/sdkv2provider/resource_cloudflare_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
})
Expand Down
210 changes: 210 additions & 0 deletions internal/sdkv2provider/resource_cloudflare_list_item.go
Original file line number Diff line number Diff line change
@@ -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, &notFoundError) {
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]
}
Loading