diff --git a/CHANGELOG.md b/CHANGELOG.md index cb9a05e2..05d42b9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,18 +3,23 @@ All notable changes to this project will be documented in this file. See updating [Changelog example here](https://keepachangelog.com/en/1.0.0/) +# 2.1.6 (31st July 2025) + +### Added + +- Customer Managed Key support for active-active and pro subscriptions. Only supports redis internal GCP cloud subscriptions. CMKs are externally provided by a customer-supplied GCP account and are managed externally by the user. # 2.1.5 (1st July 2025) ### Added -Feature: Support Marketplace as a payment method for Essentials subscription -Feature: Add TLS certificate to databases’ data sources +- Feature: Support Marketplace as a payment method for Essentials subscription +- Feature: Add TLS certificate to databases’ data sources ### Fixed: -Unexpected state `dynamic-endpoints-creation-pending' -Can not disable default user on essentials db +- Unexpected state `dynamic-endpoints-creation-pending' +- Can not disable default user on essentials db # 2.1.4 (22nd May 2025) diff --git a/docs/resources/rediscloud_active_active_subscription.md b/docs/resources/rediscloud_active_active_subscription.md index 0a9f82dd..3e8da02c 100644 --- a/docs/resources/rediscloud_active_active_subscription.md +++ b/docs/resources/rediscloud_active_active_subscription.md @@ -19,6 +19,8 @@ subscription, then the databases defined as separate resources will be attached the subscription. The creation_plan block can ONLY be used for provisioning new subscriptions, the block will be ignored if you make any further changes or try importing the resource (e.g. `terraform import` ...). +~> **Note:** The CMK (customer managed encryption key) fields require a specific flow which involves a multi step apply. Please refer to the relevant documents if using these fields. + ## Example Usage ```hcl @@ -62,6 +64,9 @@ The following arguments are supported: * `redis_version` - (Optional) The Redis version of the databases in the subscription. If omitted, the Redis version will be the default. **Modifying this attribute will force creation of a new resource.** * `creation_plan` - (Required) A creation plan object, documented below. Ignored after creation. * `maintenance_windows` - (Optional) The subscription's maintenance window specification, documented below. +* `customer_managed_key_enabled` - (Optional) Whether to enable the CMK flow. +* `customer_managed_key_deletion_grace_period` - (Optional) The grace period for deleting the subscription. If not set, will default to immediate deletion grace period. +* `customer_managed_key` - (Optional) The customer managed keys (CMK) to use for this subscription. If is active-active subscription, must set a key for each region. The `creation_plan` block supports: @@ -78,6 +83,10 @@ The creation_plan `region` block supports: * `write_operations_per_second` - (Required) Throughput measurement for an active-active subscription * `read_operations_per_second` - (Required) Throughput measurement for an active-active subscription +The `customer_managed_key` block supports: +* `resource_name` - Resource name of the customer managed key as defined by the cloud provider, e.g. projects/PROJECT_ID/locations/LOCATION/keyRings/KEY_RING/cryptoKeys/KEY_NAME +* `region` - Name of the region for the customer managed key as defined by the cloud provider. + The `maintenance_windows` object has these attributes: * `mode` - Either `automatic` (Redis specified) or `manual` (User specified) @@ -93,6 +102,8 @@ The `window` object has these attributes: ## Attribute reference +* `customer_managed_key_redis_service_account` - Outputs the id of the service account associated with the subscription. Useful as part of the CMK flow. + * `pricing` - A list of pricing objects, documented below The `pricing` object has these attributes: diff --git a/docs/resources/rediscloud_subscription.md b/docs/resources/rediscloud_subscription.md index eff8e302..ddf82974 100644 --- a/docs/resources/rediscloud_subscription.md +++ b/docs/resources/rediscloud_subscription.md @@ -20,6 +20,8 @@ subscription, then the databases defined as separate resources will be attached the subscription. The creation_plan block can ONLY be used for provisioning new subscriptions, the block will be ignored if you make any further changes or try importing the resource (e.g. `terraform import` ...). +~> **Note:** The CMK (customer managed encryption key) fields require a specific flow which involves a multi step apply. Please refer to the relevant documents if using these fields. + ## Example Usage ```hcl @@ -80,6 +82,9 @@ The following arguments are supported: * `cloud_provider` - (Required) A cloud provider object, documented below. **Modifying this attribute will force creation of a new resource.** * `creation_plan` - (Required) A creation plan object, documented below. * `maintenance_windows` - (Optional) The subscription's maintenance window specification, documented below. +* `customer_managed_key_enabled` - (Optional) Whether to enable the customer managed encryption key flow. +* `customer_managed_key_deletion_grace_period` - (Optional) The grace period for deleting the subscription. If not set, will default to immediate deletion grace period. +* `customer_managed_key` - (Optional) The customer managed keys (CMK) to use for this subscription. If is active-active subscription, must set a key for each region. The `allowlist` block supports: @@ -128,6 +133,9 @@ The cloud_provider `region` block supports: ~> **Note:** The preferred_availability_zones parameter is required for Terraform, but is optional within the Redis Enterprise Cloud UI. This difference in behaviour is to guarantee that a plan after an apply does not generate differences. In AWS Redis internal cloud account, please set the zone IDs (for example: `["use-az2", "use-az3", "use-az5"]`). +The `customer_managed_key` block supports: +* `resource_name` - The resource name of the customer managed key as defined by the cloud provider, e.g. projects/PROJECT_ID/locations/LOCATION/keyRings/KEY_RING/cryptoKeys/KEY_NAME + The `maintenance_windows` object has these attributes: * `mode` - Either `automatic` (Redis specified) or `manual` (User specified) @@ -149,6 +157,8 @@ The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/d ## Attribute reference +* `customer_managed_key_redis_service_account` - Outputs the id of the service account associated with the subscription. Useful as part of the CMK flow. + The `region` block has these attributes: * `networks` - List of generated network configuration diff --git a/go.mod b/go.mod index e3a6ceee..dda23d2f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/RedisLabs/terraform-provider-rediscloud go 1.22.4 require ( - github.com/RedisLabs/rediscloud-go-api v0.29.0 + github.com/RedisLabs/rediscloud-go-api v0.31.0 github.com/bflad/tfproviderlint v0.31.0 github.com/hashicorp/go-cty v1.5.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 diff --git a/go.sum b/go.sum index a065a2d8..0d5e5a0c 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= -github.com/RedisLabs/rediscloud-go-api v0.29.0 h1:XLVBMSgHwaaHFmf+TXrsU2veQ67J+e5Xrz54FggnwTY= -github.com/RedisLabs/rediscloud-go-api v0.29.0/go.mod h1:3/oVb71rv2OstFRYEc65QCIbfwnJTgZeQhtPCcdHook= +github.com/RedisLabs/rediscloud-go-api v0.31.0 h1:hFdR7nrJcCVQN8h3DeXtP0g4zVQP6X5wtS5FoinG8bo= +github.com/RedisLabs/rediscloud-go-api v0.31.0/go.mod h1:3/oVb71rv2OstFRYEc65QCIbfwnJTgZeQhtPCcdHook= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= diff --git a/provider/datasource_rediscloud_essentials_plan_test.go b/provider/datasource_rediscloud_essentials_plan_test.go index 499feba4..ee1dae1a 100644 --- a/provider/datasource_rediscloud_essentials_plan_test.go +++ b/provider/datasource_rediscloud_essentials_plan_test.go @@ -196,6 +196,7 @@ data "rediscloud_essentials_plan" "impossible" { const testAccResourceRedisCloudPaidEssentialsSubscriptionDataSource = ` data "rediscloud_payment_method" "card" { card_type = "Visa" + last_four_numbers = "5556" } data "rediscloud_essentials_plan" "fixed" { diff --git a/provider/datasource_rediscloud_payment_method_test.go b/provider/datasource_rediscloud_payment_method_test.go index b8249b00..bc0f572e 100644 --- a/provider/datasource_rediscloud_payment_method_test.go +++ b/provider/datasource_rediscloud_payment_method_test.go @@ -31,7 +31,8 @@ func TestAccDataSourceRedisCloudPaymentMethod_basic(t *testing.T) { } const testAccDataSourceRedisCloudPaymentMethod = ` -data "rediscloud_payment_method" "foo" { - card_type = "Visa" +data "rediscloud_payment_method" "card" { + card_type = "Visa" + last_four_numbers = "5556" } ` diff --git a/provider/datasource_rediscloud_pro_database_test.go b/provider/datasource_rediscloud_pro_database_test.go index 9cec9240..23654de7 100644 --- a/provider/datasource_rediscloud_pro_database_test.go +++ b/provider/datasource_rediscloud_pro_database_test.go @@ -69,8 +69,10 @@ func TestAccDataSourceRedisCloudProDatabase_basic(t *testing.T) { const testAccDatasourceRedisCloudProDatabase = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } + data "rediscloud_cloud_account" "account" { exclude_internal_account = true provider_type = "AWS" diff --git a/provider/datasource_rediscloud_pro_subscription_test.go b/provider/datasource_rediscloud_pro_subscription_test.go index 147e6f90..22a83ee1 100644 --- a/provider/datasource_rediscloud_pro_subscription_test.go +++ b/provider/datasource_rediscloud_pro_subscription_test.go @@ -81,8 +81,10 @@ func TestAccDataSourceRedisCloudProSubscription_ignoresAA(t *testing.T) { const testAccDatasourceRedisCloudProSubscription = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } + data "rediscloud_cloud_account" "account" { exclude_internal_account = true provider_type = "AWS" @@ -132,7 +134,9 @@ data "rediscloud_subscription" "example" { const testAccDatasourceRedisCloudAADatabaseWithProDataSource = ` data "rediscloud_payment_method" "card" { card_type = "Visa" + last_four_numbers = "5556" } + resource "rediscloud_active_active_subscription" "example" { name = "%s" payment_method_id = data.rediscloud_payment_method.card.id diff --git a/provider/datasource_rediscloud_subscription_peerings_test.go b/provider/datasource_rediscloud_subscription_peerings_test.go index 99786533..30ef98d2 100644 --- a/provider/datasource_rediscloud_subscription_peerings_test.go +++ b/provider/datasource_rediscloud_subscription_peerings_test.go @@ -60,7 +60,8 @@ func TestAccDataSourceRedisCloudSubscriptionPeerings_basic(t *testing.T) { const testAccDatasourceRedisCloudSubscriptionPeeringsDataSource = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } data "rediscloud_cloud_account" "account" { diff --git a/provider/rediscloud_active_active_database_test.go b/provider/rediscloud_active_active_database_test.go index 40d0f1ec..bc0cc996 100644 --- a/provider/rediscloud_active_active_database_test.go +++ b/provider/rediscloud_active_active_database_test.go @@ -262,6 +262,7 @@ func TestAccResourceRedisCloudActiveActiveDatabase_timeUtcRequiresValidInterval( const activeActiveSubscriptionBoilerplate = ` data "rediscloud_payment_method" "card" { card_type = "Visa" + last_four_numbers = "5556" } resource "rediscloud_active_active_subscription" "example" { @@ -407,6 +408,7 @@ resource "rediscloud_active_active_subscription_database" "example" { const testAccResourceRedisCloudActiveActiveDatabaseOptionalAttributes = ` data "rediscloud_payment_method" "card" { card_type = "Visa" + last_four_numbers = "5556" } resource "rediscloud_active_active_subscription" "example" { diff --git a/provider/rediscloud_active_active_private_service_connect_endpoint_accepter_test.go b/provider/rediscloud_active_active_private_service_connect_endpoint_accepter_test.go index c055c7fa..e9cb0145 100644 --- a/provider/rediscloud_active_active_private_service_connect_endpoint_accepter_test.go +++ b/provider/rediscloud_active_active_private_service_connect_endpoint_accepter_test.go @@ -71,7 +71,8 @@ func TestAccResourceRedisCloudActiveActivePrivateServiceConnectEndpointAccepter_ const testAccResourceRedisCloudActiveActivePrivateServiceConnectEndpointAccepterPro = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } resource "rediscloud_active_active_subscription" "subscription" { diff --git a/provider/rediscloud_active_active_private_service_connect_endpoint_test.go b/provider/rediscloud_active_active_private_service_connect_endpoint_test.go index 8145095d..4938de57 100644 --- a/provider/rediscloud_active_active_private_service_connect_endpoint_test.go +++ b/provider/rediscloud_active_active_private_service_connect_endpoint_test.go @@ -62,9 +62,9 @@ func TestAccResourceRedisCloudActiveActivePrivateServiceConnectEndpoint_CRUDI(t const testAccResourceRedisCloudActiveActivePrivateServiceConnectEndpointProStep1 = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } - resource "rediscloud_active_active_subscription" "subscription_resource" { name = "%s" payment_method_id = data.rediscloud_payment_method.card.id diff --git a/provider/rediscloud_active_active_private_service_connect_test.go b/provider/rediscloud_active_active_private_service_connect_test.go index b19a0994..cdfaa98c 100644 --- a/provider/rediscloud_active_active_private_service_connect_test.go +++ b/provider/rediscloud_active_active_private_service_connect_test.go @@ -54,7 +54,8 @@ func TestAccResourceRedisCloudActiveActivePrivateServiceConnect_CRUDI(t *testing const testAccResourceRedisCloudActiveActivePrivateServiceConnectProStep1 = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } resource "rediscloud_active_active_subscription" "subscription_resource" { diff --git a/provider/rediscloud_active_active_subscription_test.go b/provider/rediscloud_active_active_subscription_test.go index 6c89f4b4..fe825f68 100644 --- a/provider/rediscloud_active_active_subscription_test.go +++ b/provider/rediscloud_active_active_subscription_test.go @@ -346,6 +346,7 @@ func testAccCheckActiveActiveSubscriptionDestroy(s *terraform.State) error { const testAccResourceRedisCloudActiveActiveSubscription = ` data "rediscloud_payment_method" "card" { card_type = "Visa" + last_four_numbers = "5556" } resource "rediscloud_active_active_subscription" "example" { @@ -435,6 +436,7 @@ const testAccResourceRedisCloudActiveActiveSubscriptionNoCreationPlan = ` data "rediscloud_payment_method" "card" { card_type = "Visa" + last_four_numbers = "5556" } resource "rediscloud_active_active_subscription" "example" { @@ -455,6 +457,7 @@ data "rediscloud_active_active_subscription" "example" { const testAccResourceRedisCloudActiveActiveSubscriptionChangedPaymentMethod = ` data "rediscloud_payment_method" "card" { card_type = "Visa" + last_four_numbers = "5556" } resource "rediscloud_active_active_subscription" "example" { diff --git a/provider/rediscloud_essentials_subscription_test.go b/provider/rediscloud_essentials_subscription_test.go index 21d8b638..4b1a3aea 100644 --- a/provider/rediscloud_essentials_subscription_test.go +++ b/provider/rediscloud_essentials_subscription_test.go @@ -310,9 +310,16 @@ data "rediscloud_essentials_plan" "example" { region = "us-east-1" } +data "rediscloud_payment_method" "card" { + card_type = "Visa" + last_four_numbers = "5556" +} + resource "rediscloud_essentials_subscription" "example" { name = "%s" plan_id = data.rediscloud_essentials_plan.example.id + # payment_method = "credit-card" + # payment_method_id = data.rediscloud_payment_method.card.id } data "rediscloud_essentials_subscription" "example" { @@ -341,6 +348,7 @@ data "rediscloud_essentials_subscription" "example" { const testAccResourceRedisCloudPaidCreditCardEssentialsSubscription = ` data "rediscloud_payment_method" "card" { card_type = "Visa" + last_four_numbers = "5556" } data "rediscloud_essentials_plan" "example" { @@ -365,6 +373,7 @@ data "rediscloud_essentials_subscription" "example" { const testAccResourceRedisCloudPaidNoPaymentTypeEssentialsSubscription = ` data "rediscloud_payment_method" "card" { card_type = "Visa" + last_four_numbers = "5556" } data "rediscloud_essentials_plan" "example" { diff --git a/provider/rediscloud_private_service_connect_endpoint_accepter_test.go b/provider/rediscloud_private_service_connect_endpoint_accepter_test.go index a0a1ec86..dfddf854 100644 --- a/provider/rediscloud_private_service_connect_endpoint_accepter_test.go +++ b/provider/rediscloud_private_service_connect_endpoint_accepter_test.go @@ -70,7 +70,8 @@ func TestAccResourceRedisCloudPrivateServiceConnectEndpointAccepter_Create(t *te const testAccResourceRedisCloudPrivateServiceConnectEndpointAccepterPro = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } resource "rediscloud_subscription" "subscription" { diff --git a/provider/rediscloud_private_service_connect_endpoint_test.go b/provider/rediscloud_private_service_connect_endpoint_test.go index 848b3db1..4b67f061 100644 --- a/provider/rediscloud_private_service_connect_endpoint_test.go +++ b/provider/rediscloud_private_service_connect_endpoint_test.go @@ -62,7 +62,8 @@ func TestAccResourceRedisCloudPrivateServiceConnectEndpoint_CRUDI(t *testing.T) const testAccResourceRedisCloudPrivateServiceConnectEndpointProStep1 = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } resource "rediscloud_subscription" "subscription_resource" { diff --git a/provider/rediscloud_private_service_connect_test.go b/provider/rediscloud_private_service_connect_test.go index 1b8ab305..e7b32cee 100644 --- a/provider/rediscloud_private_service_connect_test.go +++ b/provider/rediscloud_private_service_connect_test.go @@ -52,7 +52,8 @@ func TestAccResourceRedisCloudPrivateServiceConnect_CRUDI(t *testing.T) { const testAccResourceRedisCloudPrivateServiceConnectProStep1 = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } resource "rediscloud_subscription" "subscription_resource" { diff --git a/provider/rediscloud_transit_gateway_attachment_test.go b/provider/rediscloud_transit_gateway_attachment_test.go index 777860ff..f882cab1 100644 --- a/provider/rediscloud_transit_gateway_attachment_test.go +++ b/provider/rediscloud_transit_gateway_attachment_test.go @@ -66,7 +66,8 @@ func TestAccResourceRedisCloudTransitGatewayAttachment_Pro(t *testing.T) { const testAccResourceRedisCloudTransitGatewayPro = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } data "rediscloud_cloud_account" "account" { @@ -116,7 +117,8 @@ data "rediscloud_transit_gateway" "test" { const testAccResourceRedisCloudTransitGatewayAttachmentPro = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } data "rediscloud_cloud_account" "account" { @@ -171,7 +173,8 @@ resource "rediscloud_transit_gateway_attachment" "test" { const testAccResourceRedisCloudTransitGatewayAttachmentProWithCidrs = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } data "rediscloud_cloud_account" "account" { diff --git a/provider/resource_rediscloud_active_active_subscription.go b/provider/resource_rediscloud_active_active_subscription.go index 731af1e3..f8cef29d 100644 --- a/provider/resource_rediscloud_active_active_subscription.go +++ b/provider/resource_rediscloud_active_active_subscription.go @@ -287,6 +287,42 @@ func resourceRedisCloudActiveActiveSubscription() *schema.Resource { }, }, }, + "customer_managed_key_enabled": { + Description: "Whether to enable CMK (customer managed key) for the subscription. If this is true, then the subscription will be put in a pending state until you supply the CMEK. See documentation for further details on this process. Defaults to false.", + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "customer_managed_key_deletion_grace_period": { + Description: "The grace period for deleting the subscription. If not set, will default to immediate deletion grace period.", + Type: schema.TypeString, + Optional: true, + Default: "immediate", + }, + "customer_managed_key": { + Description: "CMK resources used to encrypt the databases in this subscription. Ignored if `customer_managed_key_enabled` set to false. See documentation for CMK flow.", + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "resource_name": { + Description: "Resource name of the customer managed key as defined by the cloud provider, e.g. projects/PROJECT_ID/locations/LOCATION/keyRings/KEY_RING/cryptoKeys/KEY_NAME", + Type: schema.TypeString, + Required: true, + }, + "region": { + Description: "Name of region for the customer managed key as defined by the cloud provider.", + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "customer_managed_key_redis_service_account": { + Description: "The principal of the Redis service account that the subscription is created in. This is used by the user to give access to their customer managed key", + Type: schema.TypeString, + Computed: true, + }, }, } } @@ -319,15 +355,13 @@ func resourceRedisCloudActiveActiveSubscriptionCreate(ctx context.Context, d *sc dbs = buildSubscriptionCreatePlanAADatabases(planMap) - createSubscriptionRequest := subscriptions.CreateSubscription{ - DeploymentType: redis.String("active-active"), - Name: redis.String(name), - DryRun: redis.Bool(false), - PaymentMethodID: paymentMethodID, - PaymentMethod: redis.String(paymentMethod), - CloudProviders: providers, - Databases: dbs, - } + cmkEnabled := d.Get("customer_managed_key_enabled").(bool) + createSubscriptionRequest := newCreateSubscription(name, + paymentMethodID, + paymentMethod, + providers, + dbs, + cmkEnabled) redisVersion := d.Get("redis_version").(string) if d.Get("redis_version").(string) != "" { @@ -341,13 +375,22 @@ func resourceRedisCloudActiveActiveSubscriptionCreate(ctx context.Context, d *sc d.SetId(strconv.Itoa(subId)) + // If in a CMK flow, verify the pending state + if cmkEnabled { + err = waitForSubscriptionToBeEncryptionKeyPending(ctx, subId, api) + if err != nil { + return diag.FromErr(err) + } + return resourceRedisCloudActiveActiveSubscriptionRead(ctx, d, meta) + } + // Confirm Subscription Active status err = waitForSubscriptionToBeActive(ctx, subId, api) if err != nil { return diag.FromErr(err) } - // There is a timing issue where the subscription is marked as active before the creation-plan databases are listed . + // There is a timing issue where the subscription is marked as active before the creation-plan databases are listed. // This additional wait ensures that the databases will be listed before calling api.client.Database.List() time.Sleep(30 * time.Second) //lintignore:R018 if err := waitForSubscriptionToBeActive(ctx, subId, api); err != nil { @@ -446,20 +489,30 @@ func resourceRedisCloudActiveActiveSubscriptionRead(ctx context.Context, d *sche return diag.FromErr(err) } - m, err := api.client.Maintenance.Get(ctx, subId) - if err != nil { - return diag.FromErr(err) - } - if err := d.Set("maintenance_windows", flattenMaintenance(m)); err != nil { - return diag.FromErr(err) - } + cmkEnabled := d.Get("customer_managed_key_enabled").(bool) - pricingList, err := api.client.Pricing.List(ctx, subId) - if err != nil { - return diag.FromErr(err) + if !cmkEnabled { + m, err := api.client.Maintenance.Get(ctx, subId) + if err != nil { + return diag.FromErr(err) + } + if err := d.Set("maintenance_windows", flattenMaintenance(m)); err != nil { + return diag.FromErr(err) + } + + pricingList, err := api.client.Pricing.List(ctx, subId) + if err != nil { + return diag.FromErr(err) + } + if err := d.Set("pricing", flattenPricing(pricingList)); err != nil { + return diag.FromErr(err) + } } - if err := d.Set("pricing", flattenPricing(pricingList)); err != nil { - return diag.FromErr(err) + + if subscription.CustomerManagedKeyAccessDetails != nil && subscription.CustomerManagedKeyAccessDetails.RedisServiceAccount != nil { + if err := d.Set("customer_managed_key_redis_service_account", subscription.CustomerManagedKeyAccessDetails.RedisServiceAccount); err != nil { + return diag.FromErr(err) + } } return diags @@ -476,6 +529,22 @@ func resourceRedisCloudActiveActiveSubscriptionUpdate(ctx context.Context, d *sc subscriptionMutex.Lock(subId) defer subscriptionMutex.Unlock(subId) + subscription, err := api.client.Subscription.Get(ctx, subId) + if err != nil { + return diag.FromErr(err) + } + + cmkEnabled := d.Get("customer_managed_key_enabled").(bool) + + // CMK flow + if *subscription.Status == subscriptions.SubscriptionStatusEncryptionKeyPending && cmkEnabled { + diags := resourceRedisCloudActiveActiveSubscriptionUpdateCmk(ctx, d, api, subId) + + if diags != nil { + return diags + } + } + if d.HasChanges("name", "payment_method_id") { updateSubscriptionRequest := subscriptions.UpdateSubscription{} @@ -536,6 +605,37 @@ func resourceRedisCloudActiveActiveSubscriptionUpdate(ctx context.Context, d *sc return resourceRedisCloudActiveActiveSubscriptionRead(ctx, d, meta) } +func resourceRedisCloudActiveActiveSubscriptionUpdateCmk(ctx context.Context, d *schema.ResourceData, api *apiClient, subId int) diag.Diagnostics { + + cmkResourcesRaw, exists := d.GetOk("customer_managed_key") + if !exists { + return diag.Errorf("customer_managed_key must be set when subscription is in encryption key pending state") + } + + cmkList := cmkResourcesRaw.([]interface{}) + if len(cmkList) == 0 || cmkList[0] == nil { + return diag.Errorf("customer_managed_key cannot be empty or null") + } + + customerManagedKeys := buildAACmks(cmkList) + deletionGracePeriod := d.Get("customer_managed_key_deletion_grace_period").(string) + + updateCmkRequest := subscriptions.UpdateSubscriptionCMKs{ + DeletionGracePeriod: redis.String(deletionGracePeriod), + CustomerManagedKeys: &customerManagedKeys, + } + + if err := api.client.Subscription.UpdateCMKs(ctx, subId, updateCmkRequest); err != nil { + return diag.FromErr(err) + } + + if err := waitForSubscriptionToBeActive(ctx, subId, api); err != nil { + return diag.FromErr(err) + } + + return nil +} + func resourceRedisCloudActiveActiveSubscriptionDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { // use the meta value to retrieve your client from the provider configure method api := meta.(*apiClient) @@ -550,18 +650,25 @@ func resourceRedisCloudActiveActiveSubscriptionDelete(ctx context.Context, d *sc subscriptionMutex.Lock(subId) defer subscriptionMutex.Unlock(subId) - // Wait for the subscription to be active before deleting it. - if err := waitForSubscriptionToBeActive(ctx, subId, api); err != nil { + subscription, err := api.client.Subscription.Get(ctx, subId) + if err != nil { return diag.FromErr(err) } - // There is a timing issue where the subscription is marked as active before the creation-plan databases are deleted. - // This additional wait ensures that the databases are deleted before the subscription is deleted. - time.Sleep(30 * time.Second) //lintignore:R018 - if err := waitForSubscriptionToBeActive(ctx, subId, api); err != nil { - return diag.FromErr(err) + if *subscription.Status != subscriptions.SubscriptionStatusEncryptionKeyPending { + // Wait for the subscription to be active before deleting it. + if err := waitForSubscriptionToBeActive(ctx, subId, api); err != nil { + return diag.FromErr(err) + } + + // There is a timing issue where the subscription is marked as active before the creation-plan databases are deleted. + // This additional wait ensures that the databases are deleted before the subscription is deleted. + time.Sleep(30 * time.Second) //lintignore:R018 + if err := waitForSubscriptionToBeActive(ctx, subId, api); err != nil { + return diag.FromErr(err) + } + // Delete subscription once all databases are deleted } - // Delete subscription once all databases are deleted err = api.client.Subscription.Delete(ctx, subId) if err != nil { return diag.FromErr(err) @@ -577,6 +684,24 @@ func resourceRedisCloudActiveActiveSubscriptionDelete(ctx context.Context, d *sc return diags } +func newCreateSubscription(name string, paymentMethodID *int, paymentMethod string, providers []*subscriptions.CreateCloudProvider, dbs []*subscriptions.CreateDatabase, cmkEnabled bool) subscriptions.CreateSubscription { + req := subscriptions.CreateSubscription{ + DeploymentType: redis.String("active-active"), + Name: redis.String(name), + DryRun: redis.Bool(false), + PaymentMethodID: paymentMethodID, + PaymentMethod: redis.String(paymentMethod), + CloudProviders: providers, + Databases: dbs, + } + + if cmkEnabled { + req.PersistentStorageEncryptionType = redis.String(CMK_ENABLED_STRING) + } + + return req +} + func buildCreateActiveActiveCloudProviders(provider string, creationPlan map[string]interface{}) ([]*subscriptions.CreateCloudProvider, error) { createRegions := make([]*subscriptions.CreateRegion, 0) if regions := creationPlan["region"].(*schema.Set).List(); len(regions) != 0 { @@ -675,3 +800,19 @@ func createAADatabase(dbName string, idx *int, localThroughputs []*subscriptions } return dbs } + +func buildAACmks(cmkResources []interface{}) []subscriptions.CustomerManagedKey { + cmks := make([]subscriptions.CustomerManagedKey, 0, len(cmkResources)) + for _, resource := range cmkResources { + cmkMap := resource.(map[string]interface{}) + + cmk := subscriptions.CustomerManagedKey{ + ResourceName: redis.String(cmkMap["resource_name"].(string)), + Region: redis.String(cmkMap["region"].(string)), + } + + cmks = append(cmks, cmk) + } + + return cmks +} diff --git a/provider/resource_rediscloud_active_active_subscription_cmk_test.go b/provider/resource_rediscloud_active_active_subscription_cmk_test.go new file mode 100644 index 00000000..5f2e5984 --- /dev/null +++ b/provider/resource_rediscloud_active_active_subscription_cmk_test.go @@ -0,0 +1,142 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "os" + "testing" +) + +// TestAccResourceRedisCloudActiveActiveSubscription_CMK is a semi-automated test that requires the user to pause midway through +// to give the CMK the necessary permissions. +func TestAccResourceRedisCloudActiveActiveSubscription_CMK(t *testing.T) { + + testAccRequiresEnvVar(t, "EXECUTE_TESTS") + testAccRequiresEnvVar(t, "GCP_CMK_RESOURCE_NAME") + + name := acctest.RandomWithPrefix(testResourcePrefix) + const resourceName = "rediscloud_active_active_subscription.example" + gcpCmkResourceName := os.Getenv("GCP_CMK_RESOURCE_NAME") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckActiveActiveSubscriptionDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(activeActiveCmkStep1Config, name), + ExpectNonEmptyPlan: true, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttrSet(resourceName, "customer_managed_key_redis_service_account"), + resource.TestCheckResourceAttr(resourceName, "payment_method", "credit-card"), + resource.TestCheckResourceAttrSet(resourceName, "payment_method_id"), + resource.TestCheckResourceAttrSet(resourceName, "cloud_provider"), + resource.TestCheckResourceAttrSet(resourceName, "creation_plan.0.dataset_size_in_gb"), + resource.TestCheckResourceAttr(resourceName, "customer_managed_key_enabled", "true"), + ), + }, + { + Config: fmt.Sprintf(activeActiveCmkStep2Config, name, gcpCmkResourceName), + ExpectNonEmptyPlan: true, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttrSet(resourceName, "customer_managed_key_redis_service_account"), + resource.TestCheckResourceAttr(resourceName, "payment_method", "credit-card"), + resource.TestCheckResourceAttrSet(resourceName, "payment_method_id"), + resource.TestCheckResourceAttrSet(resourceName, "cloud_provider"), + resource.TestCheckResourceAttrSet(resourceName, "creation_plan.0.dataset_size_in_gb"), + resource.TestCheckResourceAttr(resourceName, "customer_managed_key_enabled", "true"), + ), + }, + }, + }) +} + +const activeActiveCmkStep1Config = ` + + +locals { +resource_name = "%s" +} + +data "rediscloud_payment_method" "card" { + card_type = "Visa" + last_four_numbers = "5556" +} + +resource "rediscloud_active_active_subscription" "example" { + name = local.resource_name + payment_method = "credit-card" + payment_method_id = data.rediscloud_payment_method.card.id + customer_managed_key_enabled = true + cloud_provider = "GCP" + + creation_plan { + memory_limit_in_gb = 1 + quantity = 1 + region { + region = "europe-west1" + networking_deployment_cidr = "192.168.0.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + region { + region = "europe-west2" + networking_deployment_cidr = "10.0.1.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + } +} +` + +const activeActiveCmkStep2Config = ` + +locals { +resource_name = "%s" +customer_managed_key_resource_name = "%s" +} + +data "rediscloud_payment_method" "card" { + card_type = "Visa" + last_four_numbers = "5556" +} + +resource "rediscloud_active_active_subscription" "example" { + name = local.resource_name + payment_method = "credit-card" + payment_method_id = data.rediscloud_payment_method.card.id + customer_managed_key_enabled = true + cloud_provider = "GCP" + + customer_managed_key { + resource_name = local.customer_managed_key_resource_name + region = "europe-west1" + } + + customer_managed_key { + resource_name = local.customer_managed_key_resource_name + region = "europe-west2" + } + + creation_plan { + memory_limit_in_gb = 1 + quantity = 1 + region { + region = "europe-west1" + networking_deployment_cidr = "192.168.0.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + region { + region = "europe-west2" + networking_deployment_cidr = "10.0.1.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + } +} + +` diff --git a/provider/resource_rediscloud_active_active_subscription_peering_test.go b/provider/resource_rediscloud_active_active_subscription_peering_test.go index 0928d75b..8afda00a 100644 --- a/provider/resource_rediscloud_active_active_subscription_peering_test.go +++ b/provider/resource_rediscloud_active_active_subscription_peering_test.go @@ -112,7 +112,8 @@ func TestAccResourceRedisCloudActiveActiveSubscriptionPeering_gcp(t *testing.T) const testAccResourceRedisCloudActiveActiveSubscriptionPeeringAWS = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } resource "rediscloud_active_active_subscription" "example" { @@ -151,7 +152,8 @@ resource "rediscloud_active_active_subscription_peering" "test" { const testAccResourceRedisCloudActiveActiveSubscriptionPeeringGCP = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } resource "rediscloud_active_active_subscription" "example" { diff --git a/provider/resource_rediscloud_active_active_subscription_regions_test.go b/provider/resource_rediscloud_active_active_subscription_regions_test.go index fb0138eb..10958efa 100644 --- a/provider/resource_rediscloud_active_active_subscription_regions_test.go +++ b/provider/resource_rediscloud_active_active_subscription_regions_test.go @@ -122,6 +122,7 @@ func TestAccResourceRedisCloudActiveActiveSubscriptionRegions_CRUDI(t *testing.T const testAARegionsBoilerplate = ` data "rediscloud_payment_method" "card" { card_type = "Visa" + last_four_numbers = "5556" } resource "rediscloud_active_active_subscription" "example" { diff --git a/provider/resource_rediscloud_essentials_database_test.go b/provider/resource_rediscloud_essentials_database_test.go index a2229dcf..27ea93b1 100644 --- a/provider/resource_rediscloud_essentials_database_test.go +++ b/provider/resource_rediscloud_essentials_database_test.go @@ -34,7 +34,7 @@ func TestAccResourceRedisCloudEssentialsDatabase_CRUDI(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", databaseName), resource.TestCheckResourceAttr(resourceName, "protocol", "stack"), resource.TestCheckResourceAttr(resourceName, "cloud_provider", "AWS"), - resource.TestCheckResourceAttr(resourceName, "region", "eu-west-1"), + resource.TestCheckResourceAttr(resourceName, "region", "us-east-1"), resource.TestCheckResourceAttrSet(resourceName, "redis_version_compliance"), resource.TestCheckResourceAttr(resourceName, "resp_version", "resp3"), resource.TestCheckResourceAttr(resourceName, "data_persistence", "none"), @@ -43,9 +43,6 @@ func TestAccResourceRedisCloudEssentialsDatabase_CRUDI(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, "public_endpoint"), resource.TestCheckResourceAttr(resourceName, "private_endpoint", ""), resource.TestCheckResourceAttr(resourceName, "source_ips.#", "0"), - resource.TestCheckResourceAttr(resourceName, "alert.#", "1"), - resource.TestCheckResourceAttr(resourceName, "alert.0.name", "throughput-higher-than"), - resource.TestCheckResourceAttr(resourceName, "alert.0.value", "80"), resource.TestCheckResourceAttr(resourceName, "enable_default_user", "true"), resource.TestCheckResourceAttr(resourceName, "password", "j43589rhe39f"), @@ -68,7 +65,7 @@ func TestAccResourceRedisCloudEssentialsDatabase_CRUDI(t *testing.T) { resource.TestCheckResourceAttr(datasourceName, "name", databaseName), resource.TestCheckResourceAttr(datasourceName, "protocol", "stack"), resource.TestCheckResourceAttr(datasourceName, "cloud_provider", "AWS"), - resource.TestCheckResourceAttr(datasourceName, "region", "eu-west-1"), + resource.TestCheckResourceAttr(datasourceName, "region", "us-east-1"), resource.TestCheckResourceAttrSet(datasourceName, "redis_version_compliance"), resource.TestCheckResourceAttr(datasourceName, "resp_version", "resp3"), resource.TestCheckResourceAttr(datasourceName, "data_persistence", "none"), @@ -77,9 +74,9 @@ func TestAccResourceRedisCloudEssentialsDatabase_CRUDI(t *testing.T) { resource.TestCheckResourceAttrSet(datasourceName, "public_endpoint"), resource.TestCheckResourceAttr(datasourceName, "private_endpoint", ""), resource.TestCheckResourceAttr(datasourceName, "source_ips.#", "0"), - resource.TestCheckResourceAttr(datasourceName, "alert.#", "1"), - resource.TestCheckResourceAttr(datasourceName, "alert.0.name", "throughput-higher-than"), - resource.TestCheckResourceAttr(datasourceName, "alert.0.value", "80"), + //resource.TestCheckResourceAttr(datasourceName, "alert.#", "1"), + //resource.TestCheckResourceAttr(datasourceName, "alert.0.name", "throughput-higher-than"), + //resource.TestCheckResourceAttr(datasourceName, "alert.0.value", "80"), resource.TestCheckResourceAttr(datasourceName, "enable_default_user", "true"), resource.TestCheckResourceAttr(datasourceName, "password", "j43589rhe39f"), @@ -109,7 +106,7 @@ func TestAccResourceRedisCloudEssentialsDatabase_CRUDI(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", databaseNameUpdated), resource.TestCheckResourceAttr(resourceName, "protocol", "stack"), resource.TestCheckResourceAttr(resourceName, "cloud_provider", "AWS"), - resource.TestCheckResourceAttr(resourceName, "region", "eu-west-1"), + resource.TestCheckResourceAttr(resourceName, "region", "us-east-1"), resource.TestCheckResourceAttrSet(resourceName, "redis_version_compliance"), resource.TestCheckResourceAttr(resourceName, "resp_version", "resp3"), resource.TestCheckResourceAttr(resourceName, "data_persistence", "none"), @@ -119,9 +116,9 @@ func TestAccResourceRedisCloudEssentialsDatabase_CRUDI(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, "public_endpoint"), resource.TestCheckResourceAttr(resourceName, "private_endpoint", ""), resource.TestCheckResourceAttr(resourceName, "source_ips.#", "0"), - resource.TestCheckResourceAttr(resourceName, "alert.#", "1"), - resource.TestCheckResourceAttr(resourceName, "alert.0.name", "throughput-higher-than"), - resource.TestCheckResourceAttr(resourceName, "alert.0.value", "80"), + //resource.TestCheckResourceAttr(resourceName, "alert.#", "1"), + //resource.TestCheckResourceAttr(resourceName, "alert.0.name", "throughput-higher-than"), + //resource.TestCheckResourceAttr(resourceName, "alert.0.value", "80"), resource.TestCheckResourceAttr(resourceName, "enable_default_user", "true"), resource.TestCheckResourceAttr(resourceName, "password", "j43589rhe39f"), @@ -140,7 +137,7 @@ func TestAccResourceRedisCloudEssentialsDatabase_CRUDI(t *testing.T) { resource.TestCheckResourceAttr(datasourceName, "name", databaseNameUpdated), resource.TestCheckResourceAttr(datasourceName, "protocol", "stack"), resource.TestCheckResourceAttr(datasourceName, "cloud_provider", "AWS"), - resource.TestCheckResourceAttr(datasourceName, "region", "eu-west-1"), + resource.TestCheckResourceAttr(datasourceName, "region", "us-east-1"), resource.TestCheckResourceAttrSet(datasourceName, "redis_version_compliance"), resource.TestCheckResourceAttr(datasourceName, "resp_version", "resp3"), resource.TestCheckResourceAttr(datasourceName, "data_persistence", "none"), @@ -149,9 +146,9 @@ func TestAccResourceRedisCloudEssentialsDatabase_CRUDI(t *testing.T) { resource.TestCheckResourceAttrSet(datasourceName, "public_endpoint"), resource.TestCheckResourceAttr(datasourceName, "private_endpoint", ""), resource.TestCheckResourceAttr(datasourceName, "source_ips.#", "0"), - resource.TestCheckResourceAttr(datasourceName, "alert.#", "1"), - resource.TestCheckResourceAttr(datasourceName, "alert.0.name", "throughput-higher-than"), - resource.TestCheckResourceAttr(datasourceName, "alert.0.value", "80"), + //resource.TestCheckResourceAttr(datasourceName, "alert.#", "1"), + //resource.TestCheckResourceAttr(datasourceName, "alert.0.name", "throughput-higher-than"), + //resource.TestCheckResourceAttr(datasourceName, "alert.0.value", "80"), resource.TestCheckResourceAttr(datasourceName, "enable_default_user", "true"), resource.TestCheckResourceAttr(datasourceName, "password", "j43589rhe39f"), @@ -175,19 +172,28 @@ func TestAccResourceRedisCloudEssentialsDatabase_CRUDI(t *testing.T) { } const testAccResourceRedisCloudEssentialsDatabaseBasic = ` -data "rediscloud_payment_method" "card" { - card_type = "Visa" -} data "rediscloud_essentials_plan" "example" { - name = "250MB" + name = "30MB" cloud_provider = "AWS" - region = "eu-west-1" + region = "us-east-1" +} + +data "rediscloud_payment_method" "card" { + card_type = "Visa" + last_four_numbers = "5556" } + resource "rediscloud_essentials_subscription" "example" { name = "%s" plan_id = data.rediscloud_essentials_plan.example.id - payment_method_id = data.rediscloud_payment_method.card.id + # payment_method = "credit-card" + # payment_method_id = data.rediscloud_payment_method.card.id } + +data "rediscloud_essentials_subscription" "example" { + name = rediscloud_essentials_subscription.example.name +} + resource "rediscloud_essentials_database" "example" { subscription_id = rediscloud_essentials_subscription.example.id name = "%s" @@ -197,10 +203,10 @@ resource "rediscloud_essentials_database" "example" { data_persistence = "none" replication = false - alert { - name = "throughput-higher-than" - value = 80 - } + # alert { + # name = "throughput-higher-than" + # value = 80 + # } tags = { "environment" = "production" @@ -218,11 +224,13 @@ data "rediscloud_essentials_database" "example" { const testAccResourceRedisCloudEssentialsDatabaseBasicWithUpperCaseTagKey = ` data "rediscloud_payment_method" "card" { card_type = "Visa" + last_four_numbers = "5556" } + data "rediscloud_essentials_plan" "example" { name = "250MB" cloud_provider = "AWS" - region = "eu-west-1" + region = "us-east-1" } resource "rediscloud_essentials_subscription" "example" { name = "%s" @@ -238,10 +246,10 @@ resource "rediscloud_essentials_database" "example" { data_persistence = "none" replication = false - alert { - name = "throughput-higher-than" - value = 80 - } + # alert { + # name = "throughput-higher-than" + # value = 80 + # '} tags = { "UpperCaseKey" = "invalid" @@ -328,12 +336,13 @@ const testAccResourceRedisCloudEssentialsDatabaseDisableDefaultUserCreate = ` data "rediscloud_payment_method" "card" { card_type = "Visa" + last_four_numbers = "5556" } data "rediscloud_essentials_plan" "example" { name = "Single-Zone_1GB" cloud_provider = "AWS" - region = "eu-west-1" + region = "us-east-1" } data "rediscloud_essentials_database" "example" { @@ -355,11 +364,11 @@ resource "rediscloud_essentials_database" "example" { data_persistence = "none" replication = false - - alert { - name = "throughput-higher-than" - value = 80 - } + # + # alert { + # name = "throughput-higher-than" + # value = 80 + # } tags = { "envaaaa" = "qaaaa" } @@ -369,12 +378,13 @@ resource "rediscloud_essentials_database" "example" { const testAccResourceRedisCloudEssentialsDatabaseDisableDefaultUserUpdate = ` data "rediscloud_payment_method" "card" { card_type = "Visa" + last_four_numbers = "5556" } data "rediscloud_essentials_plan" "example" { name = "Single-Zone_1GB" cloud_provider = "AWS" - region = "eu-west-1" + region = "us-east-1" } data "rediscloud_essentials_database" "example" { @@ -395,10 +405,11 @@ resource "rediscloud_essentials_database" "example" { data_persistence = "none" replication = false - alert { - name = "throughput-higher-than" - value = 80 - } + # alert { + # name = "throughput-higher-than" + # value = 80 + # } + tags = { "envaaaa" = "qaaaa" } diff --git a/provider/resource_rediscloud_pro_database_qpf_test.go b/provider/resource_rediscloud_pro_database_qpf_test.go index 24cb2496..4fab7c9e 100644 --- a/provider/resource_rediscloud_pro_database_qpf_test.go +++ b/provider/resource_rediscloud_pro_database_qpf_test.go @@ -14,7 +14,8 @@ import ( func proSubscriptionQPFBoilerplate(name, cloudAccountName, qpf string) string { return fmt.Sprintf(` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } data "rediscloud_cloud_account" "account" { diff --git a/provider/resource_rediscloud_pro_database_test.go b/provider/resource_rediscloud_pro_database_test.go index c284efa4..b7250b2d 100644 --- a/provider/resource_rediscloud_pro_database_test.go +++ b/provider/resource_rediscloud_pro_database_test.go @@ -273,7 +273,8 @@ func TestAccResourceRedisCloudProDatabase_respversion(t *testing.T) { const proSubscriptionBoilerplate = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } data "rediscloud_cloud_account" "account" { @@ -317,8 +318,10 @@ resource "rediscloud_subscription" "example" { const multiModulesProSubscriptionBoilerplate = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } + data "rediscloud_cloud_account" "account" { exclude_internal_account = true provider_type = "AWS" diff --git a/provider/resource_rediscloud_pro_subscription.go b/provider/resource_rediscloud_pro_subscription.go index d5ced2cd..c528e61d 100644 --- a/provider/resource_rediscloud_pro_subscription.go +++ b/provider/resource_rediscloud_pro_subscription.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "log" + "reflect" "regexp" "strconv" "time" @@ -21,6 +22,8 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) +const CMK_ENABLED_STRING = "customer-managed-key" + func containsModule(modules []interface{}, requiredModule string) bool { for _, m := range modules { if mod, ok := m.(string); ok && mod == requiredModule { @@ -66,6 +69,11 @@ func resourceRedisCloudProSubscription() *schema.Resource { } } + err := cloudRegionsForceNewDiff(ctx, diff, meta) + if err != nil { + return err + } + return nil }, @@ -178,7 +186,7 @@ func resourceRedisCloudProSubscription() *schema.Resource { Description: "Cloud networking details, per region (single region or multiple regions for Active-Active cluster only)", Type: schema.TypeSet, Required: true, - ForceNew: true, + ForceNew: false, // custom force new logic enforced elsewhere MinItems: 1, Set: func(v interface{}) int { var buf bytes.Buffer @@ -197,12 +205,10 @@ func resourceRedisCloudProSubscription() *schema.Resource { Description: "Deployment region as defined by cloud provider", Type: schema.TypeString, Required: true, - ForceNew: true, }, "multiple_availability_zones": { Description: "Support deployment on multiple availability zones within the selected region", Type: schema.TypeBool, - ForceNew: true, Optional: true, Default: false, }, @@ -210,7 +216,6 @@ func resourceRedisCloudProSubscription() *schema.Resource { Description: "List of availability zones used", Type: schema.TypeList, Optional: true, - ForceNew: true, Computed: true, Elem: &schema.Schema{ Type: schema.TypeString, @@ -219,14 +224,12 @@ func resourceRedisCloudProSubscription() *schema.Resource { "networking_deployment_cidr": { Description: "Deployment CIDR mask", Type: schema.TypeString, - ForceNew: true, Required: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IsCIDR), }, "networking_vpc_id": { Description: "Either an existing VPC Id (already exists in the specific region) or create a new VPC (if no VPC is specified)", Type: schema.TypeString, - ForceNew: true, Optional: true, Default: "", }, @@ -476,10 +479,92 @@ func resourceRedisCloudProSubscription() *schema.Resource { }, }, }, + "customer_managed_key_enabled": { + Description: "Whether to enable CMK (customer managed key) for the subscription. If this is true, then the subscription will be put in a pending state until you supply the CMEK. See documentation for further details on this process. Defaults to false.", + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "customer_managed_key_deletion_grace_period": { + Description: "The grace period for deleting the subscription. If not set, will default to immediate deletion grace period.", + Type: schema.TypeString, + Optional: true, + Default: "immediate", + }, + "customer_managed_key": { + Description: "CMK resources used to encrypt the databases in this subscription. Ignored if `customer_managed_key_enabled` set to false. Supply after the database has been put into database pending state. See documentation for CMK flow.", + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "resource_name": { + Description: "Resource name of the customer managed key as defined by the cloud provider.", + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "customer_managed_key_redis_service_account": { + Description: "The principal of the Redis service account that the subscription is created in. This is used by the user to give access to their customer managed key", + Type: schema.TypeString, + Computed: true, + }, }, } } +// cloudRegionsForceNewDiff determines if changes to a cloud region should force +// creation of a new resource based on whether it is a CMK pending state. +func cloudRegionsForceNewDiff(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error { + if diff.Id() == "" { + return handleNewResourceRegionChange(diff) + } + return handleExistingResourceRegionChange(ctx, diff, meta) +} + +func handleNewResourceRegionChange(diff *schema.ResourceDiff) error { + oldRegion, newRegion := diff.GetChange("cloud_provider.0.region") + if shouldForceNewRegion(oldRegion, newRegion) { + diff.ForceNew("cloud_provider.0.region") + } + return nil +} + +func handleExistingResourceRegionChange(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error { + subscription, err := getSubscription(ctx, diff, meta) + if err != nil { + return fmt.Errorf("failed to get subscription: %w", err) + } + + // Only check for force new if not in an encryption key pending state + if redis.StringValue(subscription.Status) != subscriptions.SubscriptionStatusEncryptionKeyPending { + oldRegion, newRegion := diff.GetChange("cloud_provider.0.region") + oldSet := oldRegion.(*schema.Set) + newSet := newRegion.(*schema.Set) + + // Check if any differences between old and new region sets + if !oldSet.Equal(newSet) { + // Force new for the entire region configuration + diff.ForceNew("cloud_provider.0.region") + } + } + return nil +} + +func shouldForceNewRegion(oldRegion, newRegion interface{}) bool { + return oldRegion != nil && newRegion != nil && !reflect.DeepEqual(oldRegion, newRegion) +} + +func getSubscription(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) (*subscriptions.Subscription, error) { + api := meta.(*apiClient) + subscriptionID, err := strconv.Atoi(diff.Id()) + if err != nil { + return nil, fmt.Errorf("invalid subscription ID: %w", err) + } + return api.client.Subscription.Get(ctx, subscriptionID) +} + func resourceRedisCloudProSubscriptionCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { api := meta.(*apiClient) @@ -525,6 +610,12 @@ func resourceRedisCloudProSubscriptionCreate(ctx context.Context, d *schema.Reso createSubscriptionRequest.RedisVersion = redis.String(redisVersion) } + cmkEnabled := d.Get("customer_managed_key_enabled").(bool) + + if cmkEnabled { + createSubscriptionRequest.PersistentStorageEncryptionType = redis.String(CMK_ENABLED_STRING) + } + subId, err := api.client.Subscription.Create(ctx, createSubscriptionRequest) if err != nil { return append(diags, diag.FromErr(err)...) @@ -532,13 +623,23 @@ func resourceRedisCloudProSubscriptionCreate(ctx context.Context, d *schema.Reso d.SetId(strconv.Itoa(subId)) + // If in a CMK flow, verify the pending state + if cmkEnabled { + err = waitForSubscriptionToBeEncryptionKeyPending(ctx, subId, api) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + return resourceRedisCloudProSubscriptionRead(ctx, d, meta) + } + // Confirm Subscription Active status err = waitForSubscriptionToBeActive(ctx, subId, api) + if err != nil { return append(diags, diag.FromErr(err)...) } - // There is a timing issue where the subscription is marked as active before the creation-plan databases are listed . + // There is a timing issue where the subscription is marked as active before the creation-plan databases are listed. // This additional wait ensures that the databases will be listed before calling api.client.Database.List() time.Sleep(30 * time.Second) //lintignore:R018 if err := waitForSubscriptionToBeActive(ctx, subId, api); err != nil { @@ -625,22 +726,30 @@ func resourceRedisCloudProSubscriptionRead(ctx context.Context, d *schema.Resour } } - m, err := api.client.Maintenance.Get(ctx, subId) - if err != nil { - return diag.FromErr(err) - } - if err := d.Set("maintenance_windows", flattenMaintenance(m)); err != nil { - return diag.FromErr(err) - } + cmkEnabled := d.Get("customer_managed_key_enabled").(bool) + if !cmkEnabled { + m, err := api.client.Maintenance.Get(ctx, subId) + if err != nil { + return diag.FromErr(err) + } + if err := d.Set("maintenance_windows", flattenMaintenance(m)); err != nil { + return diag.FromErr(err) + } - pricingList, err := api.client.Pricing.List(ctx, subId) - if err != nil { - return diag.FromErr(err) - } - if err := d.Set("pricing", flattenPricing(pricingList)); err != nil { - return diag.FromErr(err) + pricingList, err := api.client.Pricing.List(ctx, subId) + if err != nil { + return diag.FromErr(err) + } + if err := d.Set("pricing", flattenPricing(pricingList)); err != nil { + return diag.FromErr(err) + } } + if subscription.CustomerManagedKeyAccessDetails != nil && subscription.CustomerManagedKeyAccessDetails.RedisServiceAccount != nil { + if err := d.Set("customer_managed_key_redis_service_account", subscription.CustomerManagedKeyAccessDetails.RedisServiceAccount); err != nil { + return diag.FromErr(err) + } + } return diags } @@ -655,6 +764,22 @@ func resourceRedisCloudProSubscriptionUpdate(ctx context.Context, d *schema.Reso subscriptionMutex.Lock(subId) defer subscriptionMutex.Unlock(subId) + subscription, err := api.client.Subscription.Get(ctx, subId) + if err != nil { + return diag.FromErr(err) + } + + cmkEnabled := d.Get("customer_managed_key_enabled").(bool) + + // CMK flow + if *subscription.Status == subscriptions.SubscriptionStatusEncryptionKeyPending && cmkEnabled { + diags := resourceRedisCloudProSubscriptionUpdateCmk(ctx, d, api, subId) + + if diags != nil { + return diags + } + } + if d.HasChange("allowlist") { cidrs := setToStringSlice(d.Get("allowlist.0.cidrs").(*schema.Set)) sgs := setToStringSlice(d.Get("allowlist.0.security_group_ids").(*schema.Set)) @@ -728,6 +853,52 @@ func resourceRedisCloudProSubscriptionUpdate(ctx context.Context, d *schema.Reso return resourceRedisCloudProSubscriptionRead(ctx, d, meta) } +func resourceRedisCloudProSubscriptionUpdateCmk(ctx context.Context, d *schema.ResourceData, api *apiClient, subId int) diag.Diagnostics { + + cmkResourcesRaw, exists := d.GetOk("customer_managed_key") + if !exists { + return diag.Errorf("customer_managed_key must be set when subscription is in encryption key pending state") + } + + cmkList := cmkResourcesRaw.([]interface{}) + if len(cmkList) == 0 || cmkList[0] == nil { + return diag.Errorf("customer_managed_key cannot be empty or null") + } + + customerManagedKeys := buildProCmks(cmkList) + deletionGracePeriod := d.Get("customer_managed_key_deletion_grace_period").(string) + + updateCmkRequest := subscriptions.UpdateSubscriptionCMKs{ + DeletionGracePeriod: redis.String(deletionGracePeriod), + CustomerManagedKeys: &customerManagedKeys, + } + + if err := api.client.Subscription.UpdateCMKs(ctx, subId, updateCmkRequest); err != nil { + return diag.FromErr(err) + } + + if err := waitForSubscriptionToBeActive(ctx, subId, api); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func buildProCmks(cmkResources []interface{}) []subscriptions.CustomerManagedKey { + cmks := make([]subscriptions.CustomerManagedKey, 0, len(cmkResources)) + for _, resource := range cmkResources { + cmkMap := resource.(map[string]interface{}) + + cmk := subscriptions.CustomerManagedKey{ + ResourceName: redis.String(cmkMap["resource_name"].(string)), + } + + cmks = append(cmks, cmk) + } + + return cmks +} + func resourceRedisCloudProSubscriptionDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { // use the meta value to retrieve your client from the provider configure method api := meta.(*apiClient) @@ -742,17 +913,25 @@ func resourceRedisCloudProSubscriptionDelete(ctx context.Context, d *schema.Reso subscriptionMutex.Lock(subId) defer subscriptionMutex.Unlock(subId) - // Wait for the subscription to be active before deleting it. - if err := waitForSubscriptionToBeActive(ctx, subId, api); err != nil { + subscription, err := api.client.Subscription.Get(ctx, subId) + if err != nil { return diag.FromErr(err) } - // There is a timing issue where the subscription is marked as active before the creation-plan databases are deleted. - // This additional wait ensures that the databases are deleted before the subscription is deleted. - time.Sleep(30 * time.Second) //lintignore:R018 - if err := waitForSubscriptionToBeActive(ctx, subId, api); err != nil { - return diag.FromErr(err) + if *subscription.Status != subscriptions.SubscriptionStatusEncryptionKeyPending { + // Wait for the subscription to be active before deleting it. + if err := waitForSubscriptionToBeActive(ctx, subId, api); err != nil { + return diag.FromErr(err) + } + + // There is a timing issue where the subscription is marked as active before the creation-plan databases are deleted. + // This additional wait ensures that the databases are deleted before the subscription is deleted. + time.Sleep(30 * time.Second) //lintignore:R018 + if err := waitForSubscriptionToBeActive(ctx, subId, api); err != nil { + return diag.FromErr(err) + } } + // Delete subscription once all databases are deleted err = api.client.Subscription.Delete(ctx, subId) if err != nil { @@ -964,7 +1143,33 @@ func waitForSubscriptionToBeActive(ctx context.Context, id int, api *apiClient) PollInterval: 30 * time.Second, Refresh: func() (result interface{}, state string, err error) { - log.Printf("[DEBUG] Waiting for subscription %d to be active", id) + log.Printf("[DEBUG] Waiting for subscription %d to be %s", id, subscriptions.SubscriptionStatusActive) + + subscription, err := api.client.Subscription.Get(ctx, id) + if err != nil { + return nil, "", err + } + + return redis.StringValue(subscription.Status), redis.StringValue(subscription.Status), nil + }, + } + if _, err := wait.WaitForStateContext(ctx); err != nil { + return err + } + + return nil +} + +func waitForSubscriptionToBeEncryptionKeyPending(ctx context.Context, id int, api *apiClient) error { + wait := &retry.StateChangeConf{ + Pending: []string{subscriptions.SubscriptionStatusPending}, + Target: []string{subscriptions.SubscriptionStatusEncryptionKeyPending, subscriptions.SubscriptionStatusActive}, + Timeout: safetyTimeout, + Delay: 10 * time.Second, + PollInterval: 30 * time.Second, + + Refresh: func() (result interface{}, state string, err error) { + log.Printf("[DEBUG] Waiting for subscription %d to be %s", id, subscriptions.SubscriptionStatusEncryptionKeyPending) subscription, err := api.client.Subscription.Get(ctx, id) if err != nil { @@ -984,7 +1189,7 @@ func waitForSubscriptionToBeActive(ctx context.Context, id int, api *apiClient) func waitForSubscriptionToBeDeleted(ctx context.Context, id int, api *apiClient) error { wait := &retry.StateChangeConf{ Pending: []string{subscriptions.SubscriptionStatusDeleting}, - Target: []string{"deleted"}, + Target: []string{"deleted"}, // TODO: update this with deleted field in SDK Timeout: safetyTimeout, Delay: 10 * time.Second, PollInterval: 30 * time.Second, @@ -996,7 +1201,7 @@ func waitForSubscriptionToBeDeleted(ctx context.Context, id int, api *apiClient) if err != nil { if _, ok := err.(*subscriptions.NotFound); ok { return "deleted", "deleted", nil - } + } // TODO: update this with deleted field in SDK return nil, "", err } @@ -1117,9 +1322,9 @@ func flattenCloudDetails(cloudDetails []*subscriptions.CloudDetail, isResource b } if isResource { - regionMapString["networking_deployment_cidr"] = currentRegion.Networking[0].DeploymentCIDR - - if redis.BoolValue(currentRegion.MultipleAvailabilityZones) { + if len(currentRegion.Networking) > 0 && !redis.BoolValue(currentRegion.MultipleAvailabilityZones) { + regionMapString["networking_deployment_cidr"] = currentRegion.Networking[0].DeploymentCIDR + } else { regionMapString["networking_deployment_cidr"] = "" } } diff --git a/provider/resource_rediscloud_pro_subscription_cmk_test.go b/provider/resource_rediscloud_pro_subscription_cmk_test.go new file mode 100644 index 00000000..d20395a8 --- /dev/null +++ b/provider/resource_rediscloud_pro_subscription_cmk_test.go @@ -0,0 +1,128 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "os" + "testing" +) + +// TestAccResourceRedisCloudProSubscription_CMK is a semi-automated test that requires the user to pause midway through +// to give the CMK the necessary permissions. +// TODO: integrate the GCP provider and set up these permissions automatically +func TestAccResourceRedisCloudProSubscription_CMK(t *testing.T) { + + testAccRequiresEnvVar(t, "EXECUTE_TESTS") + testAccRequiresEnvVar(t, "GCP_CMK_RESOURCE_NAME") + + name := acctest.RandomWithPrefix(testResourcePrefix) + const resourceName = "rediscloud_subscription.example" + gcpCmkResourceName := os.Getenv("GCP_CMK_RESOURCE_NAME") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckProSubscriptionDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(proCmkStep1Config, name), + ExpectNonEmptyPlan: true, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttrSet(resourceName, "customer_managed_key_redis_service_account"), + resource.TestCheckResourceAttr(resourceName, "payment_method", "credit-card"), + resource.TestCheckResourceAttrSet(resourceName, "payment_method_id"), + resource.TestCheckResourceAttr(resourceName, "memory_storage", "ram"), + resource.TestCheckResourceAttrSet(resourceName, "cloud_provider.0.provider"), + resource.TestCheckResourceAttrSet(resourceName, "cloud_provider.0.region.#"), // number of regions + resource.TestCheckResourceAttrSet(resourceName, "creation_plan.0.dataset_size_in_gb"), + resource.TestCheckResourceAttr(resourceName, "customer_managed_key_enabled", "true"), + ), + }, + { + Config: fmt.Sprintf(proCmkStep2Config, name, gcpCmkResourceName), + ExpectNonEmptyPlan: true, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttrSet(resourceName, "customer_managed_key_redis_service_account"), + resource.TestCheckResourceAttr(resourceName, "payment_method", "credit-card"), + resource.TestCheckResourceAttrSet(resourceName, "payment_method_id"), + resource.TestCheckResourceAttr(resourceName, "memory_storage", "ram"), + resource.TestCheckResourceAttrSet(resourceName, "cloud_provider.0.provider"), + resource.TestCheckResourceAttrSet(resourceName, "cloud_provider.0.region.#"), + resource.TestCheckResourceAttrSet(resourceName, "creation_plan.0.dataset_size_in_gb"), + resource.TestCheckResourceAttr(resourceName, "customer_managed_key_enabled", "true"), + ), + }, + }, + }) +} + +const proCmkStep1Config = ` +data "rediscloud_payment_method" "card" { + card_type = "Visa" + last_four_numbers = "5556" +} + +resource "rediscloud_subscription" "example" { + name = "%s" + payment_method = "credit-card" + payment_method_id = data.rediscloud_payment_method.card.id + memory_storage = "ram" + customer_managed_key_enabled = true + + cloud_provider { + provider = "GCP" + region { + region = "europe-west2" + networking_deployment_cidr = "10.0.1.0/24" + } + } + + creation_plan { + dataset_size_in_gb = 1 + quantity = 1 + replication = false + support_oss_cluster_api = false + throughput_measurement_by = "operations-per-second" + throughput_measurement_value = 10000 + } +} +` + +const proCmkStep2Config = ` +data "rediscloud_payment_method" "card" { + card_type = "Visa" + last_four_numbers = "5556" +} + +resource "rediscloud_subscription" "example" { + name = "%s" + payment_method = "credit-card" + payment_method_id = data.rediscloud_payment_method.card.id + memory_storage = "ram" + customer_managed_key_enabled = true + + cloud_provider { + provider = "GCP" + region { + region = "europe-west2" + networking_deployment_cidr = "10.0.1.0/24" + } + } + + creation_plan { + dataset_size_in_gb = 1 + quantity = 1 + replication = false + support_oss_cluster_api = false + throughput_measurement_by = "operations-per-second" + throughput_measurement_value = 10000 + } + + customer_managed_key { + resource_name = "%s" + } +} +` diff --git a/provider/resource_rediscloud_pro_subscription_qpf_test.go b/provider/resource_rediscloud_pro_subscription_qpf_test.go index 9c97b8bb..c20319d1 100644 --- a/provider/resource_rediscloud_pro_subscription_qpf_test.go +++ b/provider/resource_rediscloud_pro_subscription_qpf_test.go @@ -14,7 +14,8 @@ import ( func formatSubscriptionConfig(name, cloudAccountName, qpf, extraConfig string) string { return fmt.Sprintf(` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } data "rediscloud_cloud_account" "account" { diff --git a/provider/resource_rediscloud_pro_subscription_test.go b/provider/resource_rediscloud_pro_subscription_test.go index d16e0311..0ea71a11 100644 --- a/provider/resource_rediscloud_pro_subscription_test.go +++ b/provider/resource_rediscloud_pro_subscription_test.go @@ -725,7 +725,8 @@ func testAccCheckProSubscriptionDestroy(s *terraform.State) error { // TF config for provisioning a new subscription. const testAccResourceRedisCloudProSubscription = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } data "rediscloud_cloud_account" "account" { @@ -772,7 +773,8 @@ resource "rediscloud_subscription" "example" { const testAccResourceRedisCloudProSubscriptionWithRedisVersion = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } data "rediscloud_cloud_account" "account" { @@ -818,7 +820,8 @@ resource "rediscloud_subscription" "test" { const testAccResourceRedisCloudProSubscriptionPreferredAZsModulesOptional = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } data "rediscloud_cloud_account" "account" { @@ -861,7 +864,8 @@ resource "rediscloud_subscription" "example" { // TF config for provisioning a subscription without the creation_plan block. const testAccResourceRedisCloudProSubscriptionNoCreationPlan = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } data "rediscloud_cloud_account" "account" { @@ -1016,7 +1020,8 @@ resource "rediscloud_subscription" "example" { const testAccResourceRedisCloudProSubscriptionMaintenanceWindows = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } data "rediscloud_cloud_account" "account" { diff --git a/provider/resource_rediscloud_pro_tls_test.go b/provider/resource_rediscloud_pro_tls_test.go index 2c0b86ac..b68033b4 100644 --- a/provider/resource_rediscloud_pro_tls_test.go +++ b/provider/resource_rediscloud_pro_tls_test.go @@ -383,7 +383,8 @@ func TestAccResourceRedisCloudSubscriptionTls_createWithDatabaseWithEnabledTlsAn const subscriptionBoilerplate = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } data "rediscloud_cloud_account" "account" { diff --git a/provider/resource_rediscloud_subscription_peering_test.go b/provider/resource_rediscloud_subscription_peering_test.go index 28c68db0..4ec71d8c 100644 --- a/provider/resource_rediscloud_subscription_peering_test.go +++ b/provider/resource_rediscloud_subscription_peering_test.go @@ -138,7 +138,8 @@ func cidrRangesOverlap(cidr1 string, cidr2 string) (bool, error) { const testAccResourceRedisCloudSubscriptionPeeringAWS = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } data "rediscloud_cloud_account" "account" { @@ -184,7 +185,8 @@ resource "rediscloud_subscription_peering" "test" { const testAccResourceRedisCloudSubscriptionPeeringGCP = ` data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" + last_four_numbers = "5556" } resource "rediscloud_subscription" "example" {