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

Add support for Address Map resource #2290

Merged
merged 7 commits into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/2290.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
cloudflare_address_map
```
83 changes: 83 additions & 0 deletions docs/resources/address_map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
hunts marked this conversation as resolved.
Show resolved Hide resolved
page_title: "cloudflare_address_map Resource - Cloudflare"
subcategory: ""
description: |-
Provides the ability to manage IP addresses that can be used by DNS records when
they are proxied through Cloudflare.
---

# cloudflare_address_map (Resource)

Provides the ability to manage IP addresses that can be used by DNS records when
they are proxied through Cloudflare.

## Example Usage

```terraform
resource "cloudflare_address_map" "example" {
account_id = "f037e56e89293a057740de681ac9abbe"
description = "My address map"
default_sni = "*.example.com"
enabled = true

ips { ip = "192.0.2.1" }
ips { ip = "203.0.113.1" }

memberships {
identifier = "92f17202ed8bd63d69a66b86a49a8f6b"
kind = "account"
}
memberships {
identifier = "023e105f4ecef8ad9ca31a8372d0c353"
kind = "zone"
}
}
```
<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `account_id` (String) The account identifier to target for the resource.
- `enabled` (Boolean) Whether the Address Map is enabled or not.

### Optional

- `default_sni` (String) If you have legacy TLS clients which do not send the TLS server name indicator, then you can specify one default SNI on the map.
- `description` (String) Description of the address map.
- `ips` (Block Set) The set of IPs on the Address Map. (see [below for nested schema](#nestedblock--ips))
- `memberships` (Block Set) Zones and Accounts which will be assigned IPs on this Address Map. (see [below for nested schema](#nestedblock--memberships))

### Read-Only

- `can_delete` (Boolean) If set to false, then the Address Map cannot be deleted via API. This is true for Cloudflare-managed maps.
- `can_modify_ips` (Boolean) If set to false, then the IPs on the Address Map cannot be modified via the API. This is true for Cloudflare-managed maps.
- `id` (String) The ID of this resource.

<a id="nestedblock--ips"></a>
### Nested Schema for `ips`

Required:

- `ip` (String) An IPv4 or IPv6 address.


<a id="nestedblock--memberships"></a>
### Nested Schema for `memberships`

Required:

- `identifier` (String) Identifier of the account or zone.
- `kind` (String) The type of the membership.

Read-Only:

- `can_delete` (Boolean) Controls whether the membership can be deleted via the API or not.

## Import

Import is supported using the following syntax:

```shell
$ terraform import cloudflare_address_map.example <account_id>/<address_map_id>
```
1 change: 1 addition & 0 deletions examples/resources/cloudflare_address_map/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$ terraform import cloudflare_address_map.example <account_id>/<address_map_id>
18 changes: 18 additions & 0 deletions examples/resources/cloudflare_address_map/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
resource "cloudflare_address_map" "example" {
account_id = "f037e56e89293a057740de681ac9abbe"
description = "My address map"
default_sni = "*.example.com"
enabled = true

ips { ip = "192.0.2.1" }
ips { ip = "203.0.113.1" }

memberships {
identifier = "92f17202ed8bd63d69a66b86a49a8f6b"
kind = "account"
}
memberships {
identifier = "023e105f4ecef8ad9ca31a8372d0c353"
kind = "zone"
}
}
1 change: 1 addition & 0 deletions internal/sdkv2provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ func New(version string) func() *schema.Provider {
"cloudflare_access_service_token": resourceCloudflareAccessServiceToken(),
"cloudflare_account_member": resourceCloudflareAccountMember(),
"cloudflare_account": resourceCloudflareAccount(),
"cloudflare_address_map": resourceCloudflareAddressMap(),
"cloudflare_api_shield": resourceCloudflareAPIShield(),
"cloudflare_api_token": resourceCloudflareApiToken(),
"cloudflare_argo": resourceCloudflareArgo(),
Expand Down
282 changes: 282 additions & 0 deletions internal/sdkv2provider/resource_cloudflare_address_map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
package sdkv2provider

import (
"context"
"fmt"
"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"
)

func resourceCloudflareAddressMap() *schema.Resource {
return &schema.Resource{
Schema: resourceCloudflareAddressMapSchema(),
CreateContext: resourceCloudflareAddressMapCreate,
ReadContext: resourceCloudflareAddressMapRead,
UpdateContext: resourceCloudflareAddressMapUpdate,
DeleteContext: resourceCloudflareAddressMapDelete,
Importer: &schema.ResourceImporter{
StateContext: resourceCloudflareAddressMapImport,
},
Description: heredoc.Doc(`
Provides the ability to manage IP addresses that can be used by DNS records when
they are proxied through Cloudflare.
`),
}
}

func resourceCloudflareAddressMapCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
accountID := d.Get(consts.AccountIDSchemaKey).(string)

payload := cloudflare.CreateAddressMapParams{}

if v, ok := d.GetOk("description"); ok {
desc := v.(string)
payload.Description = &desc
}

if v, ok := d.GetOk("enabled"); ok {
enabled := v.(bool)
payload.Enabled = &enabled
}

if v, ok := d.GetOk("ips"); ok {
payload.IPs = getIPsFromSchema(v.(*schema.Set).List())
}

if v, ok := d.GetOk("memberships"); ok {
payload.Memberships = getMembershipContainersFromSchema(v.(*schema.Set).List())
}

tflog.Debug(ctx, fmt.Sprintf("Creating AddressMap from struct: %+v", payload))

addressMap, err := client.CreateAddressMap(ctx, cloudflare.AccountIdentifier(accountID), payload)
if err != nil {
return diag.FromErr(fmt.Errorf("error creating AddressMap: %w", err))
}

d.SetId(addressMap.ID)
return resourceCloudflareAddressMapRead(ctx, d, meta)
}

func resourceCloudflareAddressMapRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
accountID := d.Get(consts.AccountIDSchemaKey).(string)

addressMap, err := client.GetAddressMap(ctx, cloudflare.AccountIdentifier(accountID), d.Id())
if err != nil {
return diag.FromErr(fmt.Errorf("error reading address map %q: %w", d.Id(), err))
}

d.Set("description", cloudflare.String(addressMap.Description))
d.Set("default_sni", cloudflare.String(addressMap.DefaultSNI))
d.Set("enabled", cloudflare.Bool(addressMap.Enabled))
d.Set("can_delete", cloudflare.Bool(addressMap.Deletable))
d.Set("can_modify_ips", cloudflare.Bool(addressMap.CanModifyIPs))
d.Set("ips", convertIPsToSchema(addressMap.IPs))
d.Set("memberships", convertMembershipsToSchema(addressMap.Memberships))

return nil
}

func resourceCloudflareAddressMapUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
accountID := d.Get(consts.AccountIDSchemaKey).(string)

hasChanges := false
payload := cloudflare.UpdateAddressMapParams{ID: d.Id()}

if d.HasChange("enabled") {
hasChanges = true
payload.Enabled = cloudflare.BoolPtr(d.Get("enabled").(bool))
}

if d.HasChange("description") {
hasChanges = true
payload.Description = cloudflare.StringPtr(d.Get("description").(string))
}

if d.HasChange("default_sni") {
hasChanges = true
payload.DefaultSNI = cloudflare.StringPtr(d.Get("default_sni").(string))
}

if hasChanges {
tflog.Debug(ctx, fmt.Sprintf("Updating AddressMap from struct: %+v", payload))

if _, err := client.UpdateAddressMap(ctx, cloudflare.AccountIdentifier(accountID), payload); err != nil {
return diag.FromErr(fmt.Errorf("error updating address map %q: %w", d.Id(), err))
}
}

membershipDiff := make(map[cloudflare.AddressMapMembershipContainer]int)
if d.HasChange("memberships") {
old, new := d.GetChange("memberships")
oldMembers, newMembers := getMembershipsFromSchema(old.(*schema.Set).List()), getMembershipsFromSchema(new.(*schema.Set).List())

for _, member := range newMembers {
membershipDiff[cloudflare.AddressMapMembershipContainer{Identifier: member.Identifier, Kind: member.Kind}] += 1
}
for _, member := range oldMembers {
membershipDiff[cloudflare.AddressMapMembershipContainer{Identifier: member.Identifier, Kind: member.Kind}] -= 1
}
}

ipsDiff := make(map[string]int)
if d.HasChange("ips") {
old, new := d.GetChange("ips")
oldIPs, newIPs := getIPsFromSchema(old.(*schema.Set).List()), getIPsFromSchema(new.(*schema.Set).List())
for _, ip := range newIPs {
ipsDiff[ip] += 1
}
for _, ip := range oldIPs {
ipsDiff[ip] -= 1
}
}

// Add ip addresses before adding any memberships
for ip, flag := range ipsDiff {
if flag > 0 {
tflog.Debug(ctx, fmt.Sprintf("Adding ip %q to AddressMap %q", ip, d.Id()))

if err := client.CreateIPAddressToAddressMap(ctx, cloudflare.AccountIdentifier(accountID), cloudflare.CreateIPAddressToAddressMapParams{ID: d.Id(), IP: ip}); err != nil {
return diag.FromErr(fmt.Errorf("error adding ip %q from address map %q: %w", ip, d.Id(), err))
}
}
}

// Add memberships
for member, flag := range membershipDiff {
if flag > 0 {
tflog.Debug(ctx, fmt.Sprintf("Adding membership %v to AddressMap %q", member, d.Id()))

params := cloudflare.CreateMembershipToAddressMapParams{ID: d.Id(), Membership: member}
if err := client.CreateMembershipToAddressMap(ctx, cloudflare.AccountIdentifier(accountID), params); err != nil {
return diag.FromErr(fmt.Errorf("error adding %v from address map %q: %w", member, d.Id(), err))
}
}
}

// Remove memberships before removing any ip address
for member, flag := range membershipDiff {
if flag < 0 {
tflog.Debug(ctx, fmt.Sprintf("Removing membership %v from AddressMap %q", member, d.Id()))

params := cloudflare.DeleteMembershipFromAddressMapParams{ID: d.Id(), Membership: member}
if err := client.DeleteMembershipFromAddressMap(ctx, cloudflare.AccountIdentifier(accountID), params); err != nil {
return diag.FromErr(fmt.Errorf("error removing %v from address map %q: %w", member, d.Id(), err))
}
}
}

// Remove ip addresses
for ip, flag := range ipsDiff {
if flag < 0 {
tflog.Debug(ctx, fmt.Sprintf("Removing ip %q from AddressMap %q", ip, d.Id()))

if err := client.DeleteIPAddressFromAddressMap(ctx, cloudflare.AccountIdentifier(accountID), cloudflare.DeleteIPAddressFromAddressMapParams{ID: d.Id(), IP: ip}); err != nil {
return diag.FromErr(fmt.Errorf("error removing ip %q from address map %q: %w", ip, d.Id(), err))
}
}
}

return resourceCloudflareAddressMapRead(ctx, d, meta)
}

func resourceCloudflareAddressMapDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
accountID := d.Get(consts.AccountIDSchemaKey).(string)

if err := client.DeleteAddressMap(ctx, cloudflare.AccountIdentifier(accountID), d.Id()); err != nil {
return diag.FromErr(fmt.Errorf("error deleting address map %q: %w", d.Id(), err))
}

return nil
}

func resourceCloudflareAddressMapImport(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 (%q) specified, should be in format "<account_id>/<address_map_id>"`, d.Id())
}

accountID, addressMapID := attributes[0], attributes[1]
tflog.Debug(ctx, fmt.Sprintf("Importing Cloudflare Address Map: id %s for account %s", addressMapID, accountID))

d.Set(consts.AccountIDSchemaKey, accountID)
d.SetId(addressMapID)

if readErr := resourceCloudflareAddressMapRead(ctx, d, meta); readErr != nil {
return nil, fmt.Errorf("failed to read Address Map state")
}

return []*schema.ResourceData{d}, nil
}

func convertIPsToSchema(ips []cloudflare.AddressMapIP) []interface{} {
data := []interface{}{}
for _, ip := range ips {
data = append(data, map[string]string{
"ip": ip.IP,
})
}
return data
}

func getIPsFromSchema(values []interface{}) []string {
ips := []string{}

for _, value := range values {
m := value.(map[string]interface{})
ips = append(ips, m["ip"].(string))
}

return ips
}

func convertMembershipsToSchema(members []cloudflare.AddressMapMembership) []interface{} {
data := []interface{}{}
for _, member := range members {
data = append(data, map[string]interface{}{
"identifier": member.Identifier,
"kind": string(member.Kind),
"can_delete": member.Deletable,
})
}
return data
}

func getMembershipsFromSchema(values []interface{}) []cloudflare.AddressMapMembership {
memberships := []cloudflare.AddressMapMembership{}

for _, value := range values {
m := value.(map[string]interface{})
memberships = append(memberships, cloudflare.AddressMapMembership{
Identifier: m["identifier"].(string),
Kind: cloudflare.AddressMapMembershipKind(m["kind"].(string)),
Deletable: cloudflare.BoolPtr(m["can_delete"].(bool)),
})
}

return memberships
}

func getMembershipContainersFromSchema(values []interface{}) []cloudflare.AddressMapMembershipContainer {
memberships := []cloudflare.AddressMapMembershipContainer{}

for _, value := range values {
m := value.(map[string]interface{})
memberships = append(memberships, cloudflare.AddressMapMembershipContainer{
Identifier: m["identifier"].(string),
Kind: cloudflare.AddressMapMembershipKind(m["kind"].(string)),
})
}

return memberships
}
Loading