Skip to content

Commit

Permalink
Merge pull request #2304 from rwhelan/resource_list_item
Browse files Browse the repository at this point in the history
`cloudflare_list_item` resource
  • Loading branch information
jacobbednarz authored Mar 21, 2023
2 parents e895743 + d94b897 commit 91de77a
Show file tree
Hide file tree
Showing 14 changed files with 666 additions and 3 deletions.
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 @@ -212,6 +212,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

0 comments on commit 91de77a

Please sign in to comment.