From b27becb69618ab8cbd6fde717477fe75094e6b2d Mon Sep 17 00:00:00 2001 From: Minghang Chen Date: Tue, 14 Mar 2023 18:27:40 -0700 Subject: [PATCH 1/6] Add support for Address Map resource --- .../cloudflare_address_map/import.sh | 1 + .../cloudflare_address_map/resource.tf | 18 ++ internal/sdkv2provider/provider.go | 1 + .../resource_cloudflare_address_map.go | 259 ++++++++++++++++++ .../resource_cloudflare_address_map_test.go | 105 +++++++ .../schema_cloudflare_address_map.go | 81 ++++++ tools/cmd/changelog-check/main.go | 2 +- 7 files changed, 466 insertions(+), 1 deletion(-) create mode 100644 examples/resources/cloudflare_address_map/import.sh create mode 100644 examples/resources/cloudflare_address_map/resource.tf create mode 100644 internal/sdkv2provider/resource_cloudflare_address_map.go create mode 100644 internal/sdkv2provider/resource_cloudflare_address_map_test.go create mode 100644 internal/sdkv2provider/schema_cloudflare_address_map.go diff --git a/examples/resources/cloudflare_address_map/import.sh b/examples/resources/cloudflare_address_map/import.sh new file mode 100644 index 0000000000..2ca6cde02e --- /dev/null +++ b/examples/resources/cloudflare_address_map/import.sh @@ -0,0 +1 @@ +$ terraform import cloudflare_address_map.example / diff --git a/examples/resources/cloudflare_address_map/resource.tf b/examples/resources/cloudflare_address_map/resource.tf new file mode 100644 index 0000000000..44408b86bb --- /dev/null +++ b/examples/resources/cloudflare_address_map/resource.tf @@ -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" + } +} diff --git a/internal/sdkv2provider/provider.go b/internal/sdkv2provider/provider.go index a7bd3d575d..a50d0923e8 100644 --- a/internal/sdkv2provider/provider.go +++ b/internal/sdkv2provider/provider.go @@ -182,6 +182,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(), diff --git a/internal/sdkv2provider/resource_cloudflare_address_map.go b/internal/sdkv2provider/resource_cloudflare_address_map.go new file mode 100644 index 0000000000..aa6fe94b9e --- /dev/null +++ b/internal/sdkv2provider/resource_cloudflare_address_map.go @@ -0,0 +1,259 @@ +package sdkv2provider + +import ( + "context" + "fmt" + + "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, + 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 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 +} diff --git a/internal/sdkv2provider/resource_cloudflare_address_map_test.go b/internal/sdkv2provider/resource_cloudflare_address_map_test.go new file mode 100644 index 0000000000..77ae8e7f8f --- /dev/null +++ b/internal/sdkv2provider/resource_cloudflare_address_map_test.go @@ -0,0 +1,105 @@ +package sdkv2provider + +import ( + "fmt" + "testing" + + "github.com/cloudflare/cloudflare-go" + "github.com/cloudflare/terraform-provider-cloudflare/internal/consts" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccCloudflareAddressMap(t *testing.T) { + rnd := generateRandomResourceName() + name := fmt.Sprintf("cloudflare_address_map.%s", rnd) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheckAccount(t) + }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: generateCloudflareAddressMapConfig(rnd, accountID, nil, nil, false, nil, nil), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID), + resource.TestCheckResourceAttr(name, "description", ""), + resource.TestCheckResourceAttr(name, "default_sni", ""), + resource.TestCheckResourceAttr(name, "enabled", "false"), + resource.TestCheckResourceAttr(name, "ips.#", "0"), + resource.TestCheckResourceAttr(name, "memberships.#", "0"), + ), + }, + { + Config: generateCloudflareAddressMapConfig( + rnd, + accountID, + cloudflare.StringPtr("Terraform provider test"), + cloudflare.StringPtr("*.ipam.rocks"), + true, + []string{"1.0.0.2", "1.0.0.3"}, + []cloudflare.AddressMapMembershipContainer{ + {Identifier: "a4de22ad6ae5f5ab736ec887dc14660d", Kind: cloudflare.AddressMapMembershipZone}, + {Identifier: "c6737c4cd61718ad3c3953680e638959", Kind: cloudflare.AddressMapMembershipZone}, + }), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID), + resource.TestCheckResourceAttr(name, "description", "Terraform provider test"), + resource.TestCheckResourceAttr(name, "default_sni", "*.ipam.rocks"), + resource.TestCheckResourceAttr(name, "enabled", "true"), + resource.TestCheckResourceAttr(name, "ips.0.ip", "1.0.0.2"), + resource.TestCheckResourceAttr(name, "ips.1.ip", "1.0.0.3"), + resource.TestCheckResourceAttr(name, "memberships.0.identifier", "a4de22ad6ae5f5ab736ec887dc14660d"), + resource.TestCheckResourceAttr(name, "memberships.0.kind", "zone"), + resource.TestCheckResourceAttr(name, "memberships.1.identifier", "c6737c4cd61718ad3c3953680e638959"), + resource.TestCheckResourceAttr(name, "memberships.1.kind", "zone"), + ), + }, + { + Config: generateCloudflareAddressMapConfig(rnd, accountID, cloudflare.StringPtr(""), cloudflare.StringPtr("*.ipam.rocks"), false, nil, nil), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID), + resource.TestCheckResourceAttr(name, "description", ""), + resource.TestCheckResourceAttr(name, "default_sni", "*.ipam.rocks"), + resource.TestCheckResourceAttr(name, "enabled", "false"), + resource.TestCheckResourceAttr(name, "ips.#", "0"), + resource.TestCheckResourceAttr(name, "memberships.#", "0"), + ), + }, + }, + }) +} + +func generateCloudflareAddressMapConfig(rnd, accountId string, desc, sni *string, enabled bool, ips []string, memberships []cloudflare.AddressMapMembershipContainer) string { + descFragment := "# Description" + if desc != nil { + descFragment = fmt.Sprintf("description = %q", *desc) + } + sniFragment := "# SNI" + if sni != nil { + sniFragment = fmt.Sprintf("default_sni = %q", *sni) + } + ipsFragment := "# IPs" + if len(ips) > 0 { + for _, ip := range ips { + ipsFragment += fmt.Sprintf("\n ips { ip = %q }", ip) + } + } + membershipsFragment := "# Memberships" + if len(memberships) > 0 { + for _, membership := range memberships { + membershipsFragment += fmt.Sprintf("\n memberships {\n identifier = %q\n kind = %q\n}", membership.Identifier, membership.Kind) + } + } + + return fmt.Sprintf(` +resource "cloudflare_address_map" "%[1]s" { + account_id = "%[2]s" + enabled = %t + %[4]s + %[5]s + %[6]s + %[7]s +} +`, rnd, accountId, enabled, descFragment, sniFragment, ipsFragment, membershipsFragment) +} diff --git a/internal/sdkv2provider/schema_cloudflare_address_map.go b/internal/sdkv2provider/schema_cloudflare_address_map.go new file mode 100644 index 0000000000..b7605ffc94 --- /dev/null +++ b/internal/sdkv2provider/schema_cloudflare_address_map.go @@ -0,0 +1,81 @@ +package sdkv2provider + +import ( + "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 resourceCloudflareAddressMapSchema() 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, + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "Description of the address map.", + }, + "default_sni": { + Type: schema.TypeString, + Optional: true, + Description: "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.", + }, + "enabled": { + Type: schema.TypeBool, + Required: true, + Description: "Whether the Address Map is enabled or not.", + }, + "can_delete": { + Type: schema.TypeBool, + Computed: true, + Description: "If set to false, then the Address Map cannot be deleted via API. This is true for Cloudflare-managed maps.", + }, + "can_modify_ips": { + Type: schema.TypeBool, + Computed: true, + Description: "If set to false, then the IPs on the Address Map cannot be modified via the API. This is true for Cloudflare-managed maps.", + }, + "ips": { + Type: schema.TypeSet, + Optional: true, + Description: "The set of IPs on the Address Map.", + Elem: &schema.Resource{ + SchemaVersion: 1, + Schema: map[string]*schema.Schema{ + "ip": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.IsIPAddress, + }, + }, + }, + }, + "memberships": { + Type: schema.TypeSet, + Optional: true, + Description: "Zones and Accounts which will be assigned IPs on this Address Map.", + Elem: &schema.Resource{ + SchemaVersion: 1, + Schema: map[string]*schema.Schema{ + "identifier": { + Type: schema.TypeString, + Required: true, + }, + "kind": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"account", "zone"}, false), + }, + "can_delete": { + Type: schema.TypeBool, + Computed: true, + Description: "Controls whether the membership can be deleted via the API or not.", + }, + }, + }, + }, + } +} diff --git a/tools/cmd/changelog-check/main.go b/tools/cmd/changelog-check/main.go index 9e8221d44d..4e09b31425 100644 --- a/tools/cmd/changelog-check/main.go +++ b/tools/cmd/changelog-check/main.go @@ -128,7 +128,7 @@ func main() { if err != nil { log.Fatalf("failed to comment on pull request %s/%s#%d: %s", owner, repo, prNo, err) - } + } os.Exit(1) } From 5897071a16c5dd6ace6dcde60fb333ede431c2d1 Mon Sep 17 00:00:00 2001 From: Minghang Chen Date: Tue, 14 Mar 2023 19:07:26 -0700 Subject: [PATCH 2/6] make docs for Address Map --- docs/resources/address_map.md | 83 +++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 docs/resources/address_map.md diff --git a/docs/resources/address_map.md b/docs/resources/address_map.md new file mode 100644 index 0000000000..20fd138729 --- /dev/null +++ b/docs/resources/address_map.md @@ -0,0 +1,83 @@ +--- +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 + +### 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. + + +### Nested Schema for `ips` + +Required: + +- `ip` (String) + + + +### Nested Schema for `memberships` + +Required: + +- `identifier` (String) +- `kind` (String) + +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 / +``` From 3381d611fe7932e7c8deb7aff06ee5c513bc0ed9 Mon Sep 17 00:00:00 2001 From: Minghang Chen Date: Tue, 14 Mar 2023 21:09:01 -0700 Subject: [PATCH 3/6] add missing field description in AddressMap schema --- docs/resources/address_map.md | 6 +++--- internal/sdkv2provider/schema_cloudflare_address_map.go | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/resources/address_map.md b/docs/resources/address_map.md index 20fd138729..655e7f66a4 100644 --- a/docs/resources/address_map.md +++ b/docs/resources/address_map.md @@ -59,7 +59,7 @@ resource "cloudflare_address_map" "example" { Required: -- `ip` (String) +- `ip` (String) An IPv4 or IPv6 address. @@ -67,8 +67,8 @@ Required: Required: -- `identifier` (String) -- `kind` (String) +- `identifier` (String) Identifier of the account or zone. +- `kind` (String) The type of the membership. Read-Only: diff --git a/internal/sdkv2provider/schema_cloudflare_address_map.go b/internal/sdkv2provider/schema_cloudflare_address_map.go index b7605ffc94..4b41f054f4 100644 --- a/internal/sdkv2provider/schema_cloudflare_address_map.go +++ b/internal/sdkv2provider/schema_cloudflare_address_map.go @@ -9,9 +9,9 @@ import ( func resourceCloudflareAddressMapSchema() 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, + Description: "The account identifier to target for the resource.", }, "description": { Type: schema.TypeString, @@ -48,6 +48,7 @@ func resourceCloudflareAddressMapSchema() map[string]*schema.Schema { "ip": { Type: schema.TypeString, Required: true, + Description: "An IPv4 or IPv6 address.", ValidateFunc: validation.IsIPAddress, }, }, @@ -61,12 +62,14 @@ func resourceCloudflareAddressMapSchema() map[string]*schema.Schema { SchemaVersion: 1, Schema: map[string]*schema.Schema{ "identifier": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + Description: "Identifier of the account or zone", }, "kind": { Type: schema.TypeString, Required: true, + Description: "The type of the membership.", ValidateFunc: validation.StringInSlice([]string{"account", "zone"}, false), }, "can_delete": { From a2cbf784246a4f85de19557bcc1b5b5eb7f07e30 Mon Sep 17 00:00:00 2001 From: Minghang Chen Date: Tue, 14 Mar 2023 21:26:14 -0700 Subject: [PATCH 4/6] Add address map importer implementation --- .../resource_cloudflare_address_map.go | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/internal/sdkv2provider/resource_cloudflare_address_map.go b/internal/sdkv2provider/resource_cloudflare_address_map.go index aa6fe94b9e..7efb7be844 100644 --- a/internal/sdkv2provider/resource_cloudflare_address_map.go +++ b/internal/sdkv2provider/resource_cloudflare_address_map.go @@ -3,6 +3,7 @@ package sdkv2provider import ( "context" "fmt" + "strings" "github.com/MakeNowJust/heredoc/v2" "github.com/cloudflare/cloudflare-go" @@ -19,6 +20,9 @@ func resourceCloudflareAddressMap() *schema.Resource { 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. @@ -196,6 +200,25 @@ func resourceCloudflareAddressMapDelete(ctx context.Context, d *schema.ResourceD 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 "/"`, 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 { From 63d6f249ab1ffaf364cea4d3d343ef6098fdb023 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Wed, 22 Mar 2023 08:14:11 +1100 Subject: [PATCH 5/6] generalise test setup --- .../resource_cloudflare_address_map_test.go | 36 +++++++++++-------- tools/cmd/sync-github-issue-to-jira/main.go | 4 +-- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/internal/sdkv2provider/resource_cloudflare_address_map_test.go b/internal/sdkv2provider/resource_cloudflare_address_map_test.go index 77ae8e7f8f..8216104a3c 100644 --- a/internal/sdkv2provider/resource_cloudflare_address_map_test.go +++ b/internal/sdkv2provider/resource_cloudflare_address_map_test.go @@ -2,17 +2,21 @@ package sdkv2provider import ( "fmt" + "os" "testing" "github.com/cloudflare/cloudflare-go" "github.com/cloudflare/terraform-provider-cloudflare/internal/consts" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) func TestAccCloudflareAddressMap(t *testing.T) { rnd := generateRandomResourceName() name := fmt.Sprintf("cloudflare_address_map.%s", rnd) - + domain := os.Getenv("CLOUDFLARE_DOMAIN") + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + altZoneID := os.Getenv("CLOUDFLARE_ALT_ZONE_ID") + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheckAccount(t) @@ -34,33 +38,35 @@ func TestAccCloudflareAddressMap(t *testing.T) { Config: generateCloudflareAddressMapConfig( rnd, accountID, - cloudflare.StringPtr("Terraform provider test"), - cloudflare.StringPtr("*.ipam.rocks"), + cloudflare.StringPtr(rnd), + cloudflare.StringPtr("*."+domain), true, - []string{"1.0.0.2", "1.0.0.3"}, + []string{"199.212.90.4", "199.212.90.5"}, []cloudflare.AddressMapMembershipContainer{ - {Identifier: "a4de22ad6ae5f5ab736ec887dc14660d", Kind: cloudflare.AddressMapMembershipZone}, - {Identifier: "c6737c4cd61718ad3c3953680e638959", Kind: cloudflare.AddressMapMembershipZone}, + {Identifier: zoneID, Kind: cloudflare.AddressMapMembershipZone}, + {Identifier: altZoneID, Kind: cloudflare.AddressMapMembershipZone}, }), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID), - resource.TestCheckResourceAttr(name, "description", "Terraform provider test"), - resource.TestCheckResourceAttr(name, "default_sni", "*.ipam.rocks"), + resource.TestCheckResourceAttr(name, "description", rnd), + resource.TestCheckResourceAttr(name, "default_sni", "*."+domain), resource.TestCheckResourceAttr(name, "enabled", "true"), - resource.TestCheckResourceAttr(name, "ips.0.ip", "1.0.0.2"), - resource.TestCheckResourceAttr(name, "ips.1.ip", "1.0.0.3"), - resource.TestCheckResourceAttr(name, "memberships.0.identifier", "a4de22ad6ae5f5ab736ec887dc14660d"), + resource.TestCheckResourceAttr(name, "ips.#", "2"), + resource.TestCheckResourceAttr(name, "ips.0.ip", "199.212.90.4"), + resource.TestCheckResourceAttr(name, "ips.1.ip", "199.212.90.5"), + resource.TestCheckResourceAttr(name, "memberships.#", "2"), + resource.TestCheckResourceAttr(name, "memberships.0.identifier", zoneID), resource.TestCheckResourceAttr(name, "memberships.0.kind", "zone"), - resource.TestCheckResourceAttr(name, "memberships.1.identifier", "c6737c4cd61718ad3c3953680e638959"), + resource.TestCheckResourceAttr(name, "memberships.1.identifier", altZoneID), resource.TestCheckResourceAttr(name, "memberships.1.kind", "zone"), ), }, { - Config: generateCloudflareAddressMapConfig(rnd, accountID, cloudflare.StringPtr(""), cloudflare.StringPtr("*.ipam.rocks"), false, nil, nil), + Config: generateCloudflareAddressMapConfig(rnd, accountID, cloudflare.StringPtr(""), cloudflare.StringPtr("*."+domain), false, nil, nil), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID), resource.TestCheckResourceAttr(name, "description", ""), - resource.TestCheckResourceAttr(name, "default_sni", "*.ipam.rocks"), + resource.TestCheckResourceAttr(name, "default_sni", "*."+domain), resource.TestCheckResourceAttr(name, "enabled", "false"), resource.TestCheckResourceAttr(name, "ips.#", "0"), resource.TestCheckResourceAttr(name, "memberships.#", "0"), diff --git a/tools/cmd/sync-github-issue-to-jira/main.go b/tools/cmd/sync-github-issue-to-jira/main.go index ca7d4612f6..860d19906e 100644 --- a/tools/cmd/sync-github-issue-to-jira/main.go +++ b/tools/cmd/sync-github-issue-to-jira/main.go @@ -64,11 +64,11 @@ var ( // Mapping of service label to owning internal team. serviceOwnership = map[string]serviceOwner{ - "provider/internals": serviceOwner{ + "provider/internals": { teamName: "API & Zones", manager: "rupalim", }, - "service/zones": serviceOwner{ + "service/zones": { teamName: "API & Zones", manager: "rupalim", }, From ee7bdb2d85604a719e6d992c1038f6eb0eae25a8 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Wed, 22 Mar 2023 09:23:32 +1100 Subject: [PATCH 6/6] add changelog entry --- .changelog/2290.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/2290.txt diff --git a/.changelog/2290.txt b/.changelog/2290.txt new file mode 100644 index 0000000000..310f04b7ac --- /dev/null +++ b/.changelog/2290.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +cloudflare_address_map +```