From 1f0589ff543046d5633850963ab280405747a5a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wilczy=C5=84ski?= Date: Sun, 30 Jul 2023 05:34:28 +0900 Subject: [PATCH] resource/cloudflare_zone_cache_reserve: add a new resource type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new resource that can be used to manage a Cache Reserve feature of a given zone. While at it, add a new data source to complete the newly added resource so that the current status of the Cache Reserve feature can be read for a given zone. Signed-off-by: Krzysztof WilczyƄski --- .changelog/2642.txt | 7 + CHANGELOG.md | 5 + docs/data-sources/zone_cache_reserve.md | 37 +++++ docs/resources/zone_cache_reserve.md | 45 ++++++ .../data-source.tf | 3 + .../cloudflare_zone_cache_reserve/import.sh | 1 + .../cloudflare_zone_cache_reserve/resource.tf | 5 + .../data_source_zone_cache_reserve.go | 71 +++++++++ .../data_source_zone_cache_reserve_test.go | 59 ++++++++ ...urce_cloudflare_zone_cache_reserve_test.go | 44 ++++++ internal/sdkv2provider/provider.go | 2 + .../resource_cloudflare_zone_cache_reserve.go | 138 ++++++++++++++++++ ...urce_cloudflare_zone_cache_reserve_test.go | 131 +++++++++++++++++ .../schema_cloudflare_zone_cache_reserve.go | 29 ++++ internal/sdkv2provider/validators.go | 20 ++- internal/sdkv2provider/validators_test.go | 55 +++++++ .../data-sources/zone_cache_reserve.md.tmpl | 23 +++ .../resources/zone_cache_reserve.md.tmpl | 27 ++++ 18 files changed, 700 insertions(+), 2 deletions(-) create mode 100644 .changelog/2642.txt create mode 100644 docs/data-sources/zone_cache_reserve.md create mode 100644 docs/resources/zone_cache_reserve.md create mode 100644 examples/data-sources/cloudflare_zone_cache_reserve/data-source.tf create mode 100644 examples/resources/cloudflare_zone_cache_reserve/import.sh create mode 100644 examples/resources/cloudflare_zone_cache_reserve/resource.tf create mode 100644 internal/sdkv2provider/data_source_zone_cache_reserve.go create mode 100644 internal/sdkv2provider/data_source_zone_cache_reserve_test.go create mode 100644 internal/sdkv2provider/import_resource_cloudflare_zone_cache_reserve_test.go create mode 100644 internal/sdkv2provider/resource_cloudflare_zone_cache_reserve.go create mode 100644 internal/sdkv2provider/resource_cloudflare_zone_cache_reserve_test.go create mode 100644 internal/sdkv2provider/schema_cloudflare_zone_cache_reserve.go create mode 100644 templates/data-sources/zone_cache_reserve.md.tmpl create mode 100644 templates/resources/zone_cache_reserve.md.tmpl diff --git a/.changelog/2642.txt b/.changelog/2642.txt new file mode 100644 index 00000000000..17d983047f2 --- /dev/null +++ b/.changelog/2642.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +cloudflare_zone_cache_reserve +``` + +```release-note:new-data-source +cloudflare_zone_cache_reserve +``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 5889b096cf7..e8d6e3a0cd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## 4.12.0 (Unreleased) +FEATURES: + +* **New Data Source:** `cloudflare_zone_cache_reserve` ([#2642](https://github.com/cloudflare/terraform-provider-cloudflare/issues/2642)) +* **New Resource:** `cloudflare_zone_cache_reserve` ([#2642](https://github.com/cloudflare/terraform-provider-cloudflare/issues/2642)) + ENHANCEMENTS: * resource/cloudflare_user_agent_blocking_rules: add support for importing resources ([#2640](https://github.com/cloudflare/terraform-provider-cloudflare/issues/2640)) diff --git a/docs/data-sources/zone_cache_reserve.md b/docs/data-sources/zone_cache_reserve.md new file mode 100644 index 00000000000..fd625d243b7 --- /dev/null +++ b/docs/data-sources/zone_cache_reserve.md @@ -0,0 +1,37 @@ +--- +page_title: "cloudflare_zone_cache_reserve Data Source - Cloudflare" +subcategory: "" +description: |- + Provides a Cloudflare data source to look up Cache Reserve + status for a given zone. + Requires Cache Reserve subscription. +--- + +# cloudflare_zone_cache_reserve (Data Source) + +~> Requires Cache Reserve subscription. + +Provides a Cloudflare data source to look up [Cache Reserve][cache-reserve] +status for a given zone. + +[cache-reserve]: https://developers.cloudflare.com/cache/advanced-configuration/cache-reserve + +## Example Usage + +```terraform +data "cloudflare_zone_cache_reserve" "example" { + zone_id = "0da42c8d2132a9ddaf714f9e7c920711" +} +``` + + +## Schema + +### Required + +- `zone_id` (String) The zone identifier to target for the resource. + +### Read-Only + +- `enabled` (Boolean) The status of Cache Reserve support. +- `id` (String) The ID of this resource. diff --git a/docs/resources/zone_cache_reserve.md b/docs/resources/zone_cache_reserve.md new file mode 100644 index 00000000000..a7630fefa7d --- /dev/null +++ b/docs/resources/zone_cache_reserve.md @@ -0,0 +1,45 @@ +--- +page_title: "cloudflare_zone_cache_reserve Resource - Cloudflare" +subcategory: "" +description: |- + Provides a Cloudflare Cache Reserve resource. Cache Reserve can + increase cache lifetimes by automatically storing all cacheable + files in Cloudflare's persistent object storage buckets. + Note: Using Cache Reserve without Tiered Cache is not recommended. + Requires Cache Reserve subscription. +--- + +# cloudflare_zone_cache_reserve (Resource) + +~> Requires Cache Reserve subscription. + +Provides a Cloudflare [Cache Reserve][cache-reserve] resource. Cache +Reserve can increase cache lifetimes by automatically storing all +cacheable files in Cloudflare's persistent object storage buckets. + +-> Using Cache Reserve without [Tiered Cache][tiered-cache] is not recommended. + +[cache-reserve]: https://developers.cloudflare.com/cache/advanced-configuration/cache-reserve +[tiered-cache]: https://developers.cloudflare.com/cache/how-to/tiered-cache + +## Example Usage + +```terraform +# Enable the Cache Reserve support for a given zone. +resource "cloudflare_zone_cache_variants" "example" { + zone_id = "0da42c8d2132a9ddaf714f9e7c920711" + enabled = true +} +``` + + +## Schema + +### Required + +- `enabled` (Boolean) Whether to enable or disable Cache Reserve support for a given zone. +- `zone_id` (String) The zone identifier to target for the resource. **Modifying this attribute will force creation of a new resource.** + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/examples/data-sources/cloudflare_zone_cache_reserve/data-source.tf b/examples/data-sources/cloudflare_zone_cache_reserve/data-source.tf new file mode 100644 index 00000000000..8e38b236eb9 --- /dev/null +++ b/examples/data-sources/cloudflare_zone_cache_reserve/data-source.tf @@ -0,0 +1,3 @@ +data "cloudflare_zone_cache_reserve" "example" { + zone_id = "0da42c8d2132a9ddaf714f9e7c920711" +} diff --git a/examples/resources/cloudflare_zone_cache_reserve/import.sh b/examples/resources/cloudflare_zone_cache_reserve/import.sh new file mode 100644 index 00000000000..2aae119ec4c --- /dev/null +++ b/examples/resources/cloudflare_zone_cache_reserve/import.sh @@ -0,0 +1 @@ +$ terraform import cloudflare_zone_cache_reserve.example diff --git a/examples/resources/cloudflare_zone_cache_reserve/resource.tf b/examples/resources/cloudflare_zone_cache_reserve/resource.tf new file mode 100644 index 00000000000..b5bbffcf88d --- /dev/null +++ b/examples/resources/cloudflare_zone_cache_reserve/resource.tf @@ -0,0 +1,5 @@ +# Enable the Cache Reserve support for a given zone. +resource "cloudflare_zone_cache_variants" "example" { + zone_id = "0da42c8d2132a9ddaf714f9e7c920711" + enabled = true +} diff --git a/internal/sdkv2provider/data_source_zone_cache_reserve.go b/internal/sdkv2provider/data_source_zone_cache_reserve.go new file mode 100644 index 00000000000..c1bf210ead5 --- /dev/null +++ b/internal/sdkv2provider/data_source_zone_cache_reserve.go @@ -0,0 +1,71 @@ +package sdkv2provider + +import ( + "context" + "errors" + + "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 dataSourceCloudflareZoneCacheReserve() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceCloudflareZoneCacheReserveRead, + + Schema: map[string]*schema.Schema{ + consts.ZoneIDSchemaKey: { + Type: schema.TypeString, + Required: true, + Description: consts.ZoneIDSchemaDescription, + ValidateFunc: func(value any, key string) (_ []string, errs []error) { + // Ensure that a valid Zone ID was passed. + if err := validateZoneID(value.(string)); err != nil { + errs = append(errs, err) + } + return + }, + }, + "enabled": { + Type: schema.TypeBool, + Computed: true, + Description: "The status of Cache Reserve support.", + }, + }, + Description: heredoc.Doc(` + Provides a Cloudflare data source to look up Cache Reserve + status for a given zone. + + Requires Cache Reserve subscription. + `), + } +} + +func dataSourceCloudflareZoneCacheReserveRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + zoneID := d.Get(consts.ZoneIDSchemaKey).(string) + + tflog.Info(ctx, "reading Cache Reserve", map[string]interface{}{ + "zone_id": zoneID, + }) + + params := cloudflare.GetCacheReserveParams{} + output, err := client.GetCacheReserve(ctx, cloudflare.ZoneIdentifier(zoneID), params) + if err != nil { + var notFoundError *cloudflare.NotFoundError + if errors.As(err, ¬FoundError) { + return diag.Errorf("unable to find zone: %s", zoneID) + } + return diag.Errorf("unable to read Cache Reserve for zone %q: %s", zoneID, err) + } + + d.Set(consts.ZoneIDSchemaKey, zoneID) + d.Set("enabled", output.Value == cacheReserveEnabled) + + d.SetId(stringChecksum(output.ModifiedOn.String())) + + return nil +} diff --git a/internal/sdkv2provider/data_source_zone_cache_reserve_test.go b/internal/sdkv2provider/data_source_zone_cache_reserve_test.go new file mode 100644 index 00000000000..5a94be960ba --- /dev/null +++ b/internal/sdkv2provider/data_source_zone_cache_reserve_test.go @@ -0,0 +1,59 @@ +package sdkv2provider + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/cloudflare/terraform-provider-cloudflare/internal/consts" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDataCloudflareZoneCacheReserve_Simple(t *testing.T) { + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + rnd := generateRandomResourceName() + name := fmt.Sprintf("data.cloudflare_zone_cache_reserve.%s", rnd) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccCloudflareZoneCacheReserveUpdate(t, zoneID, true) + }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccDataCloudflareZoneCacheReserveConfig(zoneID, rnd), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareZoneCacheReserveValuesUpdated(zoneID, true), + resource.TestCheckResourceAttrSet(name, consts.ZoneIDSchemaKey), + resource.TestCheckResourceAttr(name, "enabled", "true"), + ), + }, + }, + }) +} + +func TestAccDataCloudflareZoneCacheReserve_Error(t *testing.T) { + rnd := generateRandomResourceName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccDataCloudflareZoneCacheReserveConfig("this is a test", rnd), + ExpectError: regexp.MustCompile(regexp.QuoteMeta("must be a valid Zone ID, got: this is a test")), + }, + }, + }) +} + +func testAccDataCloudflareZoneCacheReserveConfig(zoneID, name string) string { + return fmt.Sprintf(` + data "cloudflare_zone_cache_reserve" "%[2]s" { + zone_id = "%[1]s" + }`, zoneID, name) +} diff --git a/internal/sdkv2provider/import_resource_cloudflare_zone_cache_reserve_test.go b/internal/sdkv2provider/import_resource_cloudflare_zone_cache_reserve_test.go new file mode 100644 index 00000000000..7c6ea92b765 --- /dev/null +++ b/internal/sdkv2provider/import_resource_cloudflare_zone_cache_reserve_test.go @@ -0,0 +1,44 @@ +package sdkv2provider + +import ( + "fmt" + "os" + "testing" + + "github.com/cloudflare/terraform-provider-cloudflare/internal/consts" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccCloudflareZoneCacheReserve_Import(t *testing.T) { + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + rnd := generateRandomResourceName() + name := fmt.Sprintf("cloudflare_zone_cache_reserve.%s", rnd) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccCloudflareZoneCacheReserveUpdate(t, zoneID, true) + }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccCloudflareZoneCacheReserveConfig(zoneID, rnd, false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(name, consts.ZoneIDSchemaKey), + resource.TestCheckResourceAttr(name, "enabled", "false"), + ), + }, + { + ImportState: true, + ImportStateId: zoneID, // Ensure that a zone ID, not resource ID, is passed. + ImportStateVerify: true, + ResourceName: name, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(name, consts.ZoneIDSchemaKey), + resource.TestCheckResourceAttr(name, "enabled", "true"), + ), + }, + }, + CheckDestroy: testAccCheckCloudflareZoneCacheReserveDestroy(zoneID), + }) +} diff --git a/internal/sdkv2provider/provider.go b/internal/sdkv2provider/provider.go index d4f37521d18..b25fc7cdd6f 100644 --- a/internal/sdkv2provider/provider.go +++ b/internal/sdkv2provider/provider.go @@ -167,6 +167,7 @@ func New(version string) func() *schema.Provider { "cloudflare_origin_ca_root_certificate": dataSourceCloudflareOriginCARootCertificate(), "cloudflare_record": dataSourceCloudflareRecord(), "cloudflare_rulesets": dataSourceCloudflareRulesets(), + "cloudflare_zone_cache_reserve": dataSourceCloudflareZoneCacheReserve(), "cloudflare_zone_dnssec": dataSourceCloudflareZoneDNSSEC(), "cloudflare_zone": dataSourceCloudflareZone(), "cloudflare_zones": dataSourceCloudflareZones(), @@ -263,6 +264,7 @@ func New(version string) func() *schema.Provider { "cloudflare_worker_script": resourceCloudflareWorkerScript(), "cloudflare_workers_kv_namespace": resourceCloudflareWorkersKVNamespace(), "cloudflare_workers_kv": resourceCloudflareWorkerKV(), + "cloudflare_zone_cache_reserve": resourceCloudflareZoneCacheReserve(), "cloudflare_zone_cache_variants": resourceCloudflareZoneCacheVariants(), "cloudflare_zone_dnssec": resourceCloudflareZoneDNSSEC(), "cloudflare_zone_lockdown": resourceCloudflareZoneLockdown(), diff --git a/internal/sdkv2provider/resource_cloudflare_zone_cache_reserve.go b/internal/sdkv2provider/resource_cloudflare_zone_cache_reserve.go new file mode 100644 index 00000000000..a5505e384c1 --- /dev/null +++ b/internal/sdkv2provider/resource_cloudflare_zone_cache_reserve.go @@ -0,0 +1,138 @@ +package sdkv2provider + +import ( + "context" + "errors" + "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" +) + +const ( + cacheReserveEnabled = "on" + cacheReserveDisabled = "off" +) + +func resourceCloudflareZoneCacheReserve() *schema.Resource { + return &schema.Resource{ + Schema: resourceCloudflareZoneCacheReserveSchema(), + CreateContext: resourceCloudflareZoneCacheReserveCreate, + ReadContext: resourceCloudflareZoneCacheReserveRead, + UpdateContext: resourceCloudflareZoneCacheReserveUpdate, + DeleteContext: resourceCloudflareZoneCacheReserveDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceCloudflareZoneCacheReserveImport, + }, + Description: heredoc.Doc(` + Provides a Cloudflare Cache Reserve resource. Cache Reserve can + increase cache lifetimes by automatically storing all cacheable + files in Cloudflare's persistent object storage buckets. + + Note: Using Cache Reserve without Tiered Cache is not recommended. + + Requires Cache Reserve subscription. + `), + } +} + +func resourceCloudflareZoneCacheReserveCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + zoneID := d.Get(consts.ZoneIDSchemaKey).(string) + + // Ensure an unique resource ID to differentiate it from a Zone ID. + d.SetId(stringChecksum(fmt.Sprintf("%s/cache-reserve", zoneID))) + + return resourceCloudflareZoneCacheReserveUpdate(ctx, d, meta) +} + +func resourceCloudflareZoneCacheReserveRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + zoneID := d.Get(consts.ZoneIDSchemaKey).(string) + + tflog.Info(ctx, "reading Cache Reserve", map[string]interface{}{ + "zone_id": zoneID, + }) + + params := cloudflare.GetCacheReserveParams{} + output, err := client.GetCacheReserve(ctx, cloudflare.ZoneIdentifier(zoneID), params) + if err != nil { + var notFoundError *cloudflare.NotFoundError + if errors.As(err, ¬FoundError) { + tflog.Warn(ctx, "zone could not be found", map[string]interface{}{ + "zone_id": zoneID, + }) + d.SetId("") + return nil + } + return diag.Errorf("unable to read Cache Reserve for zone %q: %s", zoneID, err) + } + + d.Set("enabled", output.Value == cacheReserveEnabled) + + return nil +} + +func resourceCloudflareZoneCacheReserveUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + zoneID := d.Get(consts.ZoneIDSchemaKey).(string) + + params := cloudflare.UpdateCacheReserveParams{ + Value: cacheReserveDisabled, + } + if value, ok := d.GetOk("enabled"); ok && value.(bool) { + params.Value = cacheReserveEnabled + } + + tflog.Info(ctx, "setting Cache Reserve", map[string]interface{}{ + "zone_id": zoneID, + "cache_reserve": params.Value == cacheReserveEnabled, + }) + + _, err := client.UpdateCacheReserve(ctx, cloudflare.ZoneIdentifier(zoneID), params) + if err != nil { + return diag.Errorf("unable to set Cache Reserve for zone %q: %s", zoneID, err) + } + + return resourceCloudflareZoneCacheReserveRead(ctx, d, meta) +} + +func resourceCloudflareZoneCacheReserveDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + zoneID := d.Get(consts.ZoneIDSchemaKey).(string) + + tflog.Info(ctx, "deleting Cache Reserve", map[string]interface{}{ + "zone_id": zoneID, + }) + + // The Cache Reserve does not have a concept of being added or removed, + // it's either turned enabled or disabled, and as such, deleting a Cache + // Reserve simply means disabling it, which is also the default. + params := cloudflare.UpdateCacheReserveParams{ + Value: cacheReserveDisabled, + } + _, err := client.UpdateCacheReserve(ctx, cloudflare.ZoneIdentifier(zoneID), params) + if err != nil { + return diag.Errorf("unable to delete Cache Reserve for zone %q: %s", zoneID, err) + } + + return nil +} + +func resourceCloudflareZoneCacheReserveImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + zoneID := d.Id() + + // Ensure that only a valid Zone ID will be used. + if err := validateZoneID(zoneID); err != nil { + return nil, err + } + d.SetId(stringChecksum(fmt.Sprintf("%s/cache-reserve", zoneID))) + d.Set(consts.ZoneIDSchemaKey, zoneID) + + resourceCloudflareZoneCacheReserveRead(ctx, d, meta) + + return []*schema.ResourceData{d}, nil +} diff --git a/internal/sdkv2provider/resource_cloudflare_zone_cache_reserve_test.go b/internal/sdkv2provider/resource_cloudflare_zone_cache_reserve_test.go new file mode 100644 index 00000000000..b16d0f3c4db --- /dev/null +++ b/internal/sdkv2provider/resource_cloudflare_zone_cache_reserve_test.go @@ -0,0 +1,131 @@ +package sdkv2provider + +import ( + "context" + "fmt" + "os" + "regexp" + "testing" + + "github.com/cloudflare/cloudflare-go" + "github.com/cloudflare/terraform-provider-cloudflare/internal/consts" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccCloudflareZoneCacheReserve_Simple(t *testing.T) { + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + rnd := generateRandomResourceName() + name := fmt.Sprintf("cloudflare_zone_cache_reserve.%s", rnd) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccCloudflareZoneCacheReserveConfig(zoneID, rnd, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareZoneCacheReserveValuesUpdated(zoneID, true), + resource.TestCheckResourceAttrSet(name, consts.ZoneIDSchemaKey), + resource.TestCheckResourceAttr(name, "enabled", "true"), + ), + }, + { + Config: testAccCloudflareZoneCacheReserveConfig(zoneID, rnd, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareZoneCacheReserveValuesUpdated(zoneID, false), + resource.TestCheckResourceAttrSet(name, consts.ZoneIDSchemaKey), + resource.TestCheckResourceAttr(name, "enabled", "false"), + ), + }, + }, + CheckDestroy: testAccCheckCloudflareZoneCacheReserveDestroy(zoneID), + }) +} + +func TestAccCloudflareZoneCacheReserve_Error(t *testing.T) { + rnd := generateRandomResourceName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccCloudflareZoneCacheReserveConfig("this is a test", rnd, false), + ExpectError: regexp.MustCompile(regexp.QuoteMeta("must be a valid Zone ID, got: this is a test")), + }, + }, + }) +} + +func testAccCheckCloudflareZoneCacheReserveValuesUpdated(zoneID string, enable bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*cloudflare.API) + + params := cloudflare.GetCacheReserveParams{} + output, err := client.GetCacheReserve(context.Background(), cloudflare.ZoneIdentifier(zoneID), params) + if err != nil { + return fmt.Errorf("unable to read Cache Reserve for zone %q: %w", zoneID, err) + } + + // Default state for the Cache Reserve for any zone. + var status, value = "disabled", cacheReserveDisabled + + if enable { + status, value = "enabled", cacheReserveEnabled + } + if output.Value != value { + return fmt.Errorf("expected Cache Reserve to be %q for zone: %s", status, zoneID) + } + + return nil + } +} + +func testAccCheckCloudflareZoneCacheReserveDestroy(zoneID string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*cloudflare.API) + + params := cloudflare.GetCacheReserveParams{} + output, err := client.GetCacheReserve(context.Background(), cloudflare.ZoneIdentifier(zoneID), params) + if err != nil { + return fmt.Errorf("unable to read Cache Reserve for zone %q: %w", zoneID, err) + } + + // Ensure that the Cache Reserve support has been correctly + // disabled for a given zone, which is also the default. + if output.Value == cacheReserveEnabled { + return fmt.Errorf("unable to disable Cache Reserve for zone: %s", zoneID) + } + + return nil + } +} + +func testAccCloudflareZoneCacheReserveUpdate(t *testing.T, zoneID string, enable bool) { + client := testAccProvider.Meta().(*cloudflare.API) + + params := cloudflare.UpdateCacheReserveParams{ + Value: cacheReserveDisabled, + } + if enable { + params.Value = cacheReserveEnabled + } + + _, err := client.UpdateCacheReserve(context.Background(), cloudflare.ZoneIdentifier(zoneID), params) + if err != nil { + t.Errorf("unable to set Cache Reserve for zone %q: %s", zoneID, err) + } +} + +func testAccCloudflareZoneCacheReserveConfig(zoneID, name string, enable bool) string { + return fmt.Sprintf(` + resource "cloudflare_zone_cache_reserve" "%[2]s" { + zone_id = "%[1]s" + enabled = "%[3]t" + }`, zoneID, name, enable) +} diff --git a/internal/sdkv2provider/schema_cloudflare_zone_cache_reserve.go b/internal/sdkv2provider/schema_cloudflare_zone_cache_reserve.go new file mode 100644 index 00000000000..5028283b583 --- /dev/null +++ b/internal/sdkv2provider/schema_cloudflare_zone_cache_reserve.go @@ -0,0 +1,29 @@ +package sdkv2provider + +import ( + "github.com/cloudflare/terraform-provider-cloudflare/internal/consts" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceCloudflareZoneCacheReserveSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + consts.ZoneIDSchemaKey: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: consts.ZoneIDSchemaDescription, + ValidateFunc: func(value any, key string) (_ []string, errs []error) { + // Ensure that a valid Zone ID was passed. + if err := validateZoneID(value.(string)); err != nil { + errs = append(errs, err) + } + return + }, + }, + "enabled": { + Type: schema.TypeBool, + Required: true, + Description: "Whether to enable or disable Cache Reserve support for a given zone.", + }, + } +} diff --git a/internal/sdkv2provider/validators.go b/internal/sdkv2provider/validators.go index 5fbbdc0fe5d..eb76df932e5 100644 --- a/internal/sdkv2provider/validators.go +++ b/internal/sdkv2provider/validators.go @@ -4,11 +4,18 @@ import ( "fmt" "net" "net/url" + "regexp" "strings" ) -var allowedHTTPMethods = []string{"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "_ALL_"} -var allowedSchemes = []string{"HTTP", "HTTPS", "_ALL_"} +var ( + allowedHTTPMethods = []string{"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "_ALL_"} + allowedSchemes = []string{"HTTP", "HTTPS", "_ALL_"} + + // A typical Zone ID is a 32 characters long alpha-numeric + // string that closely resembles an MD5 checksum. + zoneIDRegexp = regexp.MustCompile(`^([0-9a-f]{32}|[0-9A-F]{32})$`) +) // validateRecordType ensures that the cloudflare record type is valid. func validateRecordType(t string, proxied bool) error { @@ -74,3 +81,12 @@ func validateURL(v interface{}, k string) (s []string, errors []error) { } return } + +// validateZoneID ensures that the given Zone ID is valid. +func validateZoneID(value string) error { + if matched := zoneIDRegexp.MatchString(value); !matched { + return fmt.Errorf("must be a valid Zone ID, got: %s", value) + } + + return nil +} diff --git a/internal/sdkv2provider/validators_test.go b/internal/sdkv2provider/validators_test.go index dcbcc00b4d0..6d883ce638b 100644 --- a/internal/sdkv2provider/validators_test.go +++ b/internal/sdkv2provider/validators_test.go @@ -7,6 +7,8 @@ import ( ) func TestValidateRecordType(t *testing.T) { + t.Parallel() + validTypes := map[string]*bool{ "A": cloudflare.BoolPtr(true), "AAAA": cloudflare.BoolPtr(true), @@ -44,6 +46,8 @@ func TestValidateRecordType(t *testing.T) { } func TestValidateRecordName(t *testing.T) { + t.Parallel() + validNames := map[string]string{ "A": "192.168.0.1", "AAAA": "2001:0db8:0000:0000:0000:0000:0000:0000", @@ -67,3 +71,54 @@ func TestValidateRecordName(t *testing.T) { } } } + +func TestValidateZoneID(t *testing.T) { + t.Parallel() + + cases := []struct { + description string + given string + error bool + }{ + { + "invalid zone ID with empty value", + "", + true, + }, + { + "invalid zone ID with text only value", + "this is a test", + true, + }, + { + "invalid zone ID with mixed case value", + "0DA42C8D2132A9DDaf714f9e7c920711", + true, + }, + { + "valid zone ID with lower case value", + "0da42c8d2132a9ddaf714f9e7c920711", + false, + }, + { + "valid zone ID with upper case value", + "0DA42C8D2132A9DDAF714F9E7C920711", + false, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.description, func(t *testing.T) { + t.Parallel() + + err := validateZoneID(tc.given) + if err != nil && !tc.error { + t.Fatalf("expected %q to be a valid zone ID", tc.given) + } + if err == nil && tc.error { + t.Fatalf("expected %q to be an invalid zone ID", tc.given) + } + }) + } +} diff --git a/templates/data-sources/zone_cache_reserve.md.tmpl b/templates/data-sources/zone_cache_reserve.md.tmpl new file mode 100644 index 00000000000..fb90c386d9a --- /dev/null +++ b/templates/data-sources/zone_cache_reserve.md.tmpl @@ -0,0 +1,23 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.RenderedProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +~> Requires Cache Reserve subscription. + +Provides a Cloudflare data source to look up [Cache Reserve][cache-reserve] +status for a given zone. + +[cache-reserve]: https://developers.cloudflare.com/cache/advanced-configuration/cache-reserve + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "%s%s%s" "examples/data-sources/" .Name "/data-source.tf") }} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} diff --git a/templates/resources/zone_cache_reserve.md.tmpl b/templates/resources/zone_cache_reserve.md.tmpl new file mode 100644 index 00000000000..a21a5560542 --- /dev/null +++ b/templates/resources/zone_cache_reserve.md.tmpl @@ -0,0 +1,27 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.RenderedProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +~> Requires Cache Reserve subscription. + +Provides a Cloudflare [Cache Reserve][cache-reserve] resource. Cache +Reserve can increase cache lifetimes by automatically storing all +cacheable files in Cloudflare's persistent object storage buckets. + +-> Using Cache Reserve without [Tiered Cache][tiered-cache] is not recommended. + +[cache-reserve]: https://developers.cloudflare.com/cache/advanced-configuration/cache-reserve +[tiered-cache]: https://developers.cloudflare.com/cache/how-to/tiered-cache + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "%s%s%s" "examples/resources/" .Name "/resource.tf") }} +{{- end }} + +{{ .SchemaMarkdown | trimspace }}