diff --git a/.github/actions/run-testacc/action.yml b/.github/actions/run-testacc/action.yml new file mode 100644 index 00000000..e09ef5cb --- /dev/null +++ b/.github/actions/run-testacc/action.yml @@ -0,0 +1,20 @@ +name: 'Run Acceptance Tests' +description: 'Run acceptance tests with validation that tests actually ran' +inputs: + test_pattern: + description: 'Test pattern to pass to -run flag' + required: true +runs: + using: 'composite' + steps: + - name: Run tests + shell: bash + env: + TEST_PATTERN: ${{ inputs.test_pattern }} + run: | + set -o pipefail + EXECUTE_TESTS=true make testacc TESTARGS="-run=\"$TEST_PATTERN\"" 2>&1 | tee test_output.txt + if ! grep -q "=== RUN" test_output.txt; then + echo "ERROR: No tests matched the pattern. Please check the -run argument." + exit 1 + fi diff --git a/.github/workflows/terraform_provider_pr.yml b/.github/workflows/terraform_provider_pr.yml index 3b085435..6167ce0a 100644 --- a/.github/workflows/terraform_provider_pr.yml +++ b/.github/workflows/terraform_provider_pr.yml @@ -125,6 +125,9 @@ jobs: go-version-file: go.mod - run: EXECUTE_TESTS=true make testacc TESTARGS='-run="TestAccResourceRedisCloudActiveActiveTransitGatewayAttachment_CRUDI"' + # ===== WAVE 1: Critical smoke tests for PR changes ===== + # These test our direct code changes and must pass first + go_test_smoke_aa_db: name: go test smoke aa db needs: [go_unit_test, tfproviderlint, terraform_providers_schema] @@ -134,10 +137,12 @@ jobs: - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version-file: go.mod - - run: EXECUTE_TESTS=true make testacc TESTARGS='-run="TestAccResourceRedisCloudActiveActiveDatabase_CRUDI"' + - uses: ./.github/actions/run-testacc + with: + test_pattern: TestAccResourceRedisCloudActiveActiveDatabase_CRUDI - go_test_smoke_essentials_sub: - name: go test smoke essentials sub + go_test_smoke_aa_db_enable_default_user: + name: go test smoke aa db enable default user needs: [go_unit_test, tfproviderlint, terraform_providers_schema] runs-on: ubuntu-latest steps: @@ -145,19 +150,22 @@ jobs: - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version-file: go.mod - - run: EXECUTE_TESTS=true make testacc TESTARGS='-run="TestAccResourceRedisCloudEssentialsSubscription"' - + - uses: ./.github/actions/run-testacc + with: + test_pattern: TestAccResourceRedisCloudActiveActiveDatabase_enableDefaultUser - go_test_smoke_essentials_db: - name: go test smoke essentials db - needs: go_test_smoke_essentials_sub + go_test_smoke_aa_sub: + name: go test smoke aa sub + needs: [go_unit_test, tfproviderlint, terraform_providers_schema] runs-on: ubuntu-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version-file: go.mod - - run: EXECUTE_TESTS=true make testacc TESTARGS='-run="TestAccResourceRedisCloudEssentialsDatabase_CRUDI"' + - uses: ./.github/actions/run-testacc + with: + test_pattern: TestAccResourceRedisCloudActiveActiveSubscription_CRUDI go_test_smoke_pro_db: name: go test smoke pro db @@ -168,64 +176,104 @@ jobs: - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version-file: go.mod - - run: EXECUTE_TESTS=true make testacc TESTARGS='-run="TestAccResourceRedisCloudProDatabase_CRUDI"' + - uses: ./.github/actions/run-testacc + with: + test_pattern: TestAccResourceRedisCloudProDatabase_CRUDI + go_test_smoke_pro_sub: + name: go test smoke pro sub + needs: [go_unit_test, tfproviderlint, terraform_providers_schema] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version-file: go.mod + - uses: ./.github/actions/run-testacc + with: + test_pattern: TestAccResourceRedisCloudProSubscription_CRUDI + + # ===== WAVE 2: Additional smoke tests (run after Wave 1 passes) ===== + + go_test_smoke_essentials_sub: + name: go test smoke essentials sub + needs: [go_test_smoke_aa_db, go_test_smoke_aa_db_enable_default_user, go_test_smoke_aa_sub, go_test_smoke_pro_db, go_test_smoke_pro_sub] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version-file: go.mod + - uses: ./.github/actions/run-testacc + with: + test_pattern: TestAccResourceRedisCloudEssentialsSubscription + + + go_test_smoke_essentials_db: + name: go test smoke essentials db + needs: go_test_smoke_essentials_sub + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version-file: go.mod + - uses: ./.github/actions/run-testacc + with: + test_pattern: TestAccResourceRedisCloudEssentialsDatabase_CRUDI go_test_smoke_misc: name: go test smoke misc - needs: [go_unit_test, tfproviderlint, terraform_providers_schema] + needs: [go_test_smoke_aa_db, go_test_smoke_aa_db_enable_default_user, go_test_smoke_aa_sub, go_test_smoke_pro_db, go_test_smoke_pro_sub] runs-on: ubuntu-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version-file: go.mod - - run: EXECUTE_TESTS=true make testacc TESTARGS='-run="TestAccResourceRedisCloud(PrivateServiceConnect_CRUDI|AclRule_CRUDI)"' + - uses: ./.github/actions/run-testacc + with: + test_pattern: TestAccResourceRedisCloud(PrivateServiceConnect_CRUDI|AclRule_CRUDI) go_test_pro_db_upgrade: name: go test smoke pro db upgrade - needs: [go_unit_test, tfproviderlint, terraform_providers_schema] + needs: [go_test_smoke_aa_db, go_test_smoke_aa_db_enable_default_user, go_test_smoke_aa_sub, go_test_smoke_pro_db, go_test_smoke_pro_sub] runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version-file: go.mod - - run: EXECUTE_TESTS=true make testacc TESTARGS='-run="TestAccResourceRedisCloudProDatabase_Upgrade"' + - uses: ./.github/actions/run-testacc + with: + test_pattern: TestAccResourceRedisCloudProDatabase_Redis8_Upgrade go_test_privatelink: name: go test smoke privatelink - needs: [go_unit_test, tfproviderlint, terraform_providers_schema] + needs: [go_test_smoke_aa_db, go_test_smoke_aa_db_enable_default_user, go_test_smoke_aa_sub, go_test_smoke_pro_db, go_test_smoke_pro_sub] runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version-file: go.mod - - run: EXECUTE_TESTS=true make testacc TESTARGS='-run="TestAccResourceRedisCloudPrivateLink_CRUDI"' + - uses: ./.github/actions/run-testacc + with: + test_pattern: TestAccResourceRedisCloudPrivateLink_CRUDI go_test_block_public_endpoints: name: go test smoke public endpoints - needs: [go_unit_test, tfproviderlint, terraform_providers_schema] + needs: [go_test_smoke_aa_db, go_test_smoke_aa_db_enable_default_user, go_test_smoke_aa_sub, go_test_smoke_pro_db, go_test_smoke_pro_sub] runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version-file: go.mod - - run: EXECUTE_TESTS=true make testacc TESTARGS='-run="TestAcc(RedisCloudProDatabaseBlockPublicEndpoints|ActiveActiveSubscriptionDatabaseBlockPublicEndpoints)"' - - go_test_smoke_qpf: - name: go test smoke query performance factor - needs: [go_unit_test, tfproviderlint, terraform_providers_schema] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + - uses: ./.github/actions/run-testacc with: - go-version-file: go.mod - - run: EXECUTE_TESTS=true make testacc TESTARGS='-run="TestAccResourceRedisCloudProDatabase_qpf"' + test_pattern: TestAcc(RedisCloudProDatabase_BlockPublicEndpoints|ActiveActiveSubscriptionDatabase_BlockPublicEndpoints) tfproviderlint: name: tfproviderlint diff --git a/CHANGELOG.md b/CHANGELOG.md index 45d9f249..7923bc7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See updating [Changelog example here](https://keepachangelog.com/en/1.0.0/) + +# Unreleased + +## Fixed +- `rediscloud_active_active_subscription_database`: Fixed drift detection for `enable_default_user` in region overrides. Regions now correctly inherit from `global_enable_default_user` when not explicitly set, eliminating spurious diffs. +- `rediscloud_active_active_subscription_database`: Fixed drift detection for `global_data_persistence`. This field now correctly tracks API-returned values without causing unexpected diffs. +- `rediscloud_subscription` and `rediscloud_active_active_subscription`: `customer_managed_key_enabled` and `customer_managed_key_deletion_grace_period` fields now support computed values from the API. + # 2.8.0 (10th November 2025) ## Added diff --git a/docs/data-sources/rediscloud_subscription.md b/docs/data-sources/rediscloud_subscription.md index f396d21c..4a91ae12 100644 --- a/docs/data-sources/rediscloud_subscription.md +++ b/docs/data-sources/rediscloud_subscription.md @@ -32,7 +32,6 @@ output "rediscloud_subscription" { `id` is set to the ID of the found subscription. -* `aws_account_id` - AWS account ID that the subscription is deployed in (AWS subscriptions only). * `payment_method_id` - A valid payment method pre-defined in the current account * `memory_storage` - Memory storage preference: either 'ram' or a combination of 'ram-and-flash' * `cloud_provider` - A cloud provider object, documented below @@ -45,6 +44,7 @@ The `cloud_provider` block supports: * `provider` - The cloud provider to use with the subscription, (either `AWS` or `GCP`) * `cloud_account_id` - Cloud account identifier, (A Cloud Account Id = 1 implies using Redis Labs internal cloud account) +* `aws_account_id` - AWS account ID that the subscription is deployed in (AWS subscriptions only). * `region` - Cloud networking details, per region (single region or multiple regions for Active-Active cluster only), documented below The cloud_provider `region` block supports: diff --git a/docs/resources/rediscloud_subscription.md b/docs/resources/rediscloud_subscription.md index d1b1e403..f8e7198a 100644 --- a/docs/resources/rediscloud_subscription.md +++ b/docs/resources/rediscloud_subscription.md @@ -151,9 +151,12 @@ The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/d ## Attribute reference -* `aws_account_id` - AWS account ID that the subscription is deployed in (AWS subscriptions only). * `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 `cloud_provider` block has these attributes: + +* `aws_account_id` - AWS account ID that the subscription is deployed in (AWS subscriptions only). + The `region` block has these attributes: * `networks` - List of generated network configuration diff --git a/go.mod b/go.mod index 5b668819..9cc473fa 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,10 @@ go 1.24.0 toolchain go1.24.1 require ( - github.com/RedisLabs/rediscloud-go-api v0.42.0 + github.com/RedisLabs/rediscloud-go-api v0.43.0 github.com/bflad/tfproviderlint v0.31.0 github.com/hashicorp/go-cty v1.5.0 + github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 github.com/stretchr/testify v1.11.1 ) @@ -38,7 +39,6 @@ require ( github.com/hashicorp/terraform-exec v0.23.1 // indirect github.com/hashicorp/terraform-json v0.27.1 // indirect github.com/hashicorp/terraform-plugin-go v0.29.0 // indirect - github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect github.com/hashicorp/terraform-registry-address v0.4.0 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.2 // indirect @@ -71,4 +71,4 @@ require ( ) // for local development, uncomment this -//replace github.com/RedisLabs/rediscloud-go-api => ../rediscloud-go-api +// replace github.com/RedisLabs/rediscloud-go-api => ../rediscloud-go-api diff --git a/go.sum b/go.sum index 388b7d72..540a8732 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= -github.com/RedisLabs/rediscloud-go-api v0.42.0 h1:nsEgDF9IlPSGVTpMtDMAKR3mzk1oqATAxpO/hAqrp80= -github.com/RedisLabs/rediscloud-go-api v0.42.0/go.mod h1:ZsOzeXCzczue7vOiAF0be0sl1FTEgltAGmh+4s0BFq0= +github.com/RedisLabs/rediscloud-go-api v0.43.0 h1:fMODeDNQoD/o2afeSkMl8zezxQ/scOW17Z54p3GrhyI= +github.com/RedisLabs/rediscloud-go-api v0.43.0/go.mod h1:ZsOzeXCzczue7vOiAF0be0sl1FTEgltAGmh+4s0BFq0= 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/activeactive/testdata/enable_default_user_all_explicit.tf b/provider/activeactive/testdata/enable_default_user_all_explicit.tf new file mode 100644 index 00000000..6f18f2bb --- /dev/null +++ b/provider/activeactive/testdata/enable_default_user_all_explicit.tf @@ -0,0 +1,59 @@ +# Template signature: fmt.Sprintf(template, subscription_name, database_name, password) +locals { + subscription_name = "%s" + database_name = "%s" + password = "%s" +} + +data "rediscloud_payment_method" "card" { + card_type = "Visa" + last_four_numbers = "5556" +} + +resource "rediscloud_active_active_subscription" "example" { + name = local.subscription_name + payment_method_id = data.rediscloud_payment_method.card.id + cloud_provider = "AWS" + + creation_plan { + dataset_size_in_gb = 1 + quantity = 1 + + region { + region = "us-east-1" + networking_deployment_cidr = "10.0.0.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + + region { + region = "us-east-2" + networking_deployment_cidr = "10.0.1.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + } +} + +resource "rediscloud_active_active_subscription_database" "example" { + subscription_id = rediscloud_active_active_subscription.example.id + name = local.database_name + memory_limit_in_gb = 1 + + # Global enable_default_user is true + global_enable_default_user = true + global_password = local.password + + # us-east-1 explicitly set to true (matches global but is EXPLICIT) + # This tests that explicit values are preserved even when matching global + override_region { + name = "us-east-1" + enable_default_user = true + } + + # us-east-2 explicitly set to false (differs from global) + override_region { + name = "us-east-2" + enable_default_user = false + } +} diff --git a/provider/activeactive/testdata/enable_default_user_debug_import_step1.tf b/provider/activeactive/testdata/enable_default_user_debug_import_step1.tf new file mode 100644 index 00000000..d9b279d0 --- /dev/null +++ b/provider/activeactive/testdata/enable_default_user_debug_import_step1.tf @@ -0,0 +1,39 @@ +# DEBUG VERSION - Imports existing database +# Template signature: fmt.Sprintf(template, subscription_id, password) +locals { + subscription_id = "%s" + password = "%s" +} + +# Step 1: global=true, both regions inherit (NO enable_default_user in override_region) +resource "rediscloud_active_active_subscription_database" "example" { + subscription_id = local.subscription_id + name = "matt-test-debugging" + memory_limit_in_gb = 1 + + # Global enable_default_user is true + global_enable_default_user = true + global_password = local.password + + # Both regions inherit from global - NO enable_default_user specified + override_region { + name = "eu-west-1" + } + + override_region { + name = "us-east-1" + } + + lifecycle { + ignore_changes = [ + # Ignore changes to fields we're not testing + name, + memory_limit_in_gb, + global_data_persistence, + global_source_ips, + global_alert, + global_modules, + port, + ] + } +} diff --git a/provider/activeactive/testdata/enable_default_user_debug_import_step2.tf b/provider/activeactive/testdata/enable_default_user_debug_import_step2.tf new file mode 100644 index 00000000..9ee0166a --- /dev/null +++ b/provider/activeactive/testdata/enable_default_user_debug_import_step2.tf @@ -0,0 +1,41 @@ +# DEBUG VERSION - Imports existing database +# Template signature: fmt.Sprintf(template, subscription_id, password) +locals { + subscription_id = "%s" + password = "%s" +} + +# Step 2: global=true, us-east-1 explicit false +resource "rediscloud_active_active_subscription_database" "example" { + subscription_id = local.subscription_id + name = "matt-test-debugging" + memory_limit_in_gb = 1 + + # Global enable_default_user is true + global_enable_default_user = true + global_password = local.password + + # eu-west-1 inherits from global + override_region { + name = "eu-west-1" + } + + # us-east-1 explicitly set to false (differs from global) + override_region { + name = "us-east-1" + enable_default_user = false + } + + lifecycle { + ignore_changes = [ + # Ignore changes to fields we're not testing + name, + memory_limit_in_gb, + global_data_persistence, + global_source_ips, + global_alert, + global_modules, + port, + ] + } +} diff --git a/provider/activeactive/testdata/enable_default_user_debug_import_step3.tf b/provider/activeactive/testdata/enable_default_user_debug_import_step3.tf new file mode 100644 index 00000000..a5bf642f --- /dev/null +++ b/provider/activeactive/testdata/enable_default_user_debug_import_step3.tf @@ -0,0 +1,41 @@ +# DEBUG VERSION - Imports existing database +# Template signature: fmt.Sprintf(template, subscription_id, password) +locals { + subscription_id = "%s" + password = "%s" +} + +# Step 3: global=false, us-east-1 explicit true +resource "rediscloud_active_active_subscription_database" "example" { + subscription_id = local.subscription_id + name = "matt-test-debugging" + memory_limit_in_gb = 1 + + # Global enable_default_user is false + global_enable_default_user = false + global_password = local.password + + # eu-west-1 inherits from global + override_region { + name = "eu-west-1" + } + + # us-east-1 explicitly set to true (differs from global) + override_region { + name = "us-east-1" + enable_default_user = true + } + + lifecycle { + ignore_changes = [ + # Ignore changes to fields we're not testing + name, + memory_limit_in_gb, + global_data_persistence, + global_source_ips, + global_alert, + global_modules, + port, + ] + } +} diff --git a/provider/activeactive/testdata/enable_default_user_debug_step1.tf b/provider/activeactive/testdata/enable_default_user_debug_step1.tf new file mode 100644 index 00000000..7c308ae2 --- /dev/null +++ b/provider/activeactive/testdata/enable_default_user_debug_step1.tf @@ -0,0 +1,27 @@ +# DEBUG VERSION - Reuses existing subscription +# Template signature: fmt.Sprintf(template, subscription_id, database_name, password) +locals { + subscription_id = "%s" + database_name = "%s" + password = "%s" +} + +# Step 1: global=true, both regions inherit (NO enable_default_user in override_region) +resource "rediscloud_active_active_subscription_database" "example" { + subscription_id = local.subscription_id + name = local.database_name + memory_limit_in_gb = 1 + + # Global enable_default_user is true + global_enable_default_user = true + global_password = local.password + + # Both regions inherit from global - NO enable_default_user specified + override_region { + name = "us-east-1" + } + + override_region { + name = "us-east-2" + } +} diff --git a/provider/activeactive/testdata/enable_default_user_debug_step2.tf b/provider/activeactive/testdata/enable_default_user_debug_step2.tf new file mode 100644 index 00000000..f6bdae04 --- /dev/null +++ b/provider/activeactive/testdata/enable_default_user_debug_step2.tf @@ -0,0 +1,29 @@ +# DEBUG VERSION - Reuses existing subscription +# Template signature: fmt.Sprintf(template, subscription_id, database_name, password) +locals { + subscription_id = "%s" + database_name = "%s" + password = "%s" +} + +# Step 2: global=true, us-east-1 explicit false +resource "rediscloud_active_active_subscription_database" "example" { + subscription_id = local.subscription_id + name = local.database_name + memory_limit_in_gb = 1 + + # Global enable_default_user is true + global_enable_default_user = true + global_password = local.password + + # us-east-1 explicitly set to false (differs from global) + override_region { + name = "us-east-1" + enable_default_user = false + } + + # us-east-2 inherits from global + override_region { + name = "us-east-2" + } +} diff --git a/provider/activeactive/testdata/enable_default_user_global_false_inherit.tf b/provider/activeactive/testdata/enable_default_user_global_false_inherit.tf new file mode 100644 index 00000000..a100410f --- /dev/null +++ b/provider/activeactive/testdata/enable_default_user_global_false_inherit.tf @@ -0,0 +1,55 @@ +# Template signature: fmt.Sprintf(template, subscription_name, database_name, password) +locals { + subscription_name = "%s" + database_name = "%s" + password = "%s" +} + +data "rediscloud_payment_method" "card" { + card_type = "Visa" + last_four_numbers = "5556" +} + +resource "rediscloud_active_active_subscription" "example" { + name = local.subscription_name + payment_method_id = data.rediscloud_payment_method.card.id + cloud_provider = "AWS" + + creation_plan { + dataset_size_in_gb = 1 + quantity = 1 + + region { + region = "us-east-1" + networking_deployment_cidr = "10.0.0.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + + region { + region = "us-east-2" + networking_deployment_cidr = "10.0.1.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + } +} + +resource "rediscloud_active_active_subscription_database" "example" { + subscription_id = rediscloud_active_active_subscription.example.id + name = local.database_name + memory_limit_in_gb = 1 + + # Global enable_default_user is false + global_enable_default_user = false + global_password = local.password + + # Both regions inherit from global - NO enable_default_user specified + override_region { + name = "us-east-1" + } + + override_region { + name = "us-east-2" + } +} diff --git a/provider/activeactive/testdata/enable_default_user_global_false_region_false.tf b/provider/activeactive/testdata/enable_default_user_global_false_region_false.tf new file mode 100644 index 00000000..0a6a3a10 --- /dev/null +++ b/provider/activeactive/testdata/enable_default_user_global_false_region_false.tf @@ -0,0 +1,57 @@ +# Template signature: fmt.Sprintf(template, subscription_name, database_name, password) +locals { + subscription_name = "%s" + database_name = "%s" + password = "%s" +} + +data "rediscloud_payment_method" "card" { + card_type = "Visa" + last_four_numbers = "5556" +} + +resource "rediscloud_active_active_subscription" "example" { + name = local.subscription_name + payment_method_id = data.rediscloud_payment_method.card.id + cloud_provider = "AWS" + + creation_plan { + dataset_size_in_gb = 1 + quantity = 1 + + region { + region = "us-east-1" + networking_deployment_cidr = "10.0.0.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + + region { + region = "us-east-2" + networking_deployment_cidr = "10.0.1.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + } +} + +resource "rediscloud_active_active_subscription_database" "example" { + subscription_id = rediscloud_active_active_subscription.example.id + name = local.database_name + memory_limit_in_gb = 1 + + # Global enable_default_user is false + global_enable_default_user = false + global_password = local.password + + # us-east-1 explicitly set to false (matches global but is EXPLICIT) + override_region { + name = "us-east-1" + enable_default_user = false + } + + # us-east-2 inherits from global (false) - NO enable_default_user specified + override_region { + name = "us-east-2" + } +} diff --git a/provider/activeactive/testdata/enable_default_user_global_false_region_true.tf b/provider/activeactive/testdata/enable_default_user_global_false_region_true.tf new file mode 100644 index 00000000..f0c26549 --- /dev/null +++ b/provider/activeactive/testdata/enable_default_user_global_false_region_true.tf @@ -0,0 +1,57 @@ +# Template signature: fmt.Sprintf(template, subscription_name, database_name, password) +locals { + subscription_name = "%s" + database_name = "%s" + password = "%s" +} + +data "rediscloud_payment_method" "card" { + card_type = "Visa" + last_four_numbers = "5556" +} + +resource "rediscloud_active_active_subscription" "example" { + name = local.subscription_name + payment_method_id = data.rediscloud_payment_method.card.id + cloud_provider = "AWS" + + creation_plan { + dataset_size_in_gb = 1 + quantity = 1 + + region { + region = "us-east-1" + networking_deployment_cidr = "10.0.0.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + + region { + region = "us-east-2" + networking_deployment_cidr = "10.0.1.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + } +} + +resource "rediscloud_active_active_subscription_database" "example" { + subscription_id = rediscloud_active_active_subscription.example.id + name = local.database_name + memory_limit_in_gb = 1 + + # Global enable_default_user is false + global_enable_default_user = false + global_password = local.password + + # us-east-1 explicitly enables default user (differs from global) + override_region { + name = "us-east-1" + enable_default_user = true + } + + # us-east-2 inherits from global (false) - NO enable_default_user specified + override_region { + name = "us-east-2" + } +} diff --git a/provider/activeactive/testdata/enable_default_user_global_true_inherit.tf b/provider/activeactive/testdata/enable_default_user_global_true_inherit.tf new file mode 100644 index 00000000..6579926c --- /dev/null +++ b/provider/activeactive/testdata/enable_default_user_global_true_inherit.tf @@ -0,0 +1,55 @@ +# Template signature: fmt.Sprintf(template, subscription_name, database_name, password) +locals { + subscription_name = "%s" + database_name = "%s" + password = "%s" +} + +data "rediscloud_payment_method" "card" { + card_type = "Visa" + last_four_numbers = "5556" +} + +resource "rediscloud_active_active_subscription" "example" { + name = local.subscription_name + payment_method_id = data.rediscloud_payment_method.card.id + cloud_provider = "AWS" + + creation_plan { + dataset_size_in_gb = 1 + quantity = 1 + + region { + region = "us-east-1" + networking_deployment_cidr = "10.0.0.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + + region { + region = "us-east-2" + networking_deployment_cidr = "10.0.1.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + } +} + +resource "rediscloud_active_active_subscription_database" "example" { + subscription_id = rediscloud_active_active_subscription.example.id + name = local.database_name + memory_limit_in_gb = 1 + + # Global enable_default_user is true (default behavior) + global_enable_default_user = true + global_password = local.password + + # Both regions inherit from global - NO enable_default_user specified + override_region { + name = "us-east-1" + } + + override_region { + name = "us-east-2" + } +} diff --git a/provider/activeactive/testdata/enable_default_user_global_true_region_false.tf b/provider/activeactive/testdata/enable_default_user_global_true_region_false.tf new file mode 100644 index 00000000..32adbc3e --- /dev/null +++ b/provider/activeactive/testdata/enable_default_user_global_true_region_false.tf @@ -0,0 +1,57 @@ +# Template signature: fmt.Sprintf(template, subscription_name, database_name, password) +locals { + subscription_name = "%s" + database_name = "%s" + password = "%s" +} + +data "rediscloud_payment_method" "card" { + card_type = "Visa" + last_four_numbers = "5556" +} + +resource "rediscloud_active_active_subscription" "example" { + name = local.subscription_name + payment_method_id = data.rediscloud_payment_method.card.id + cloud_provider = "AWS" + + creation_plan { + dataset_size_in_gb = 1 + quantity = 1 + + region { + region = "us-east-1" + networking_deployment_cidr = "10.0.0.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + + region { + region = "us-east-2" + networking_deployment_cidr = "10.0.1.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + } +} + +resource "rediscloud_active_active_subscription_database" "example" { + subscription_id = rediscloud_active_active_subscription.example.id + name = local.database_name + memory_limit_in_gb = 1 + + # Global enable_default_user is true + global_enable_default_user = true + global_password = local.password + + # us-east-1 explicitly disables default user (differs from global) + override_region { + name = "us-east-1" + enable_default_user = false + } + + # us-east-2 inherits from global - NO enable_default_user specified + override_region { + name = "us-east-2" + } +} diff --git a/provider/activeactive/testdata/enable_default_user_import_stub.tf b/provider/activeactive/testdata/enable_default_user_import_stub.tf new file mode 100644 index 00000000..cb176547 --- /dev/null +++ b/provider/activeactive/testdata/enable_default_user_import_stub.tf @@ -0,0 +1,24 @@ +# Minimal stub config for import - just defines the resource shell +# Template signature: fmt.Sprintf(template, subscription_id, password) +locals { + subscription_id = "%s" + password = "%s" +} + +# Minimal resource definition for import +resource "rediscloud_active_active_subscription_database" "example" { + subscription_id = local.subscription_id + name = "matt-test-debugging" + memory_limit_in_gb = 1 + + global_enable_default_user = true + global_password = local.password + + override_region { + name = "eu-west-1" + } + + override_region { + name = "us-east-1" + } +} diff --git a/provider/pro/resource_rediscloud_pro_database.go b/provider/pro/resource_rediscloud_pro_database.go index 317120e6..4cac607b 100644 --- a/provider/pro/resource_rediscloud_pro_database.go +++ b/provider/pro/resource_rediscloud_pro_database.go @@ -623,11 +623,18 @@ func resourceRedisCloudProDatabaseRead(ctx context.Context, d *schema.ResourceDa // Check if returned source_ips matches default public access ["0.0.0.0/0"] isDefaultPublicAccess := len(db.Security.SourceIPs) == 1 && redis.StringValue(db.Security.SourceIPs[0]) == "0.0.0.0/0" - // Check if returned source_ips matches default RFC1918 private ranges - isDefaultPrivateRanges := len(db.Security.SourceIPs) == len(defaultPrivateIPRanges) - if isDefaultPrivateRanges { - for i, ip := range db.Security.SourceIPs { - if redis.StringValue(ip) != defaultPrivateIPRanges[i] { + // Check if returned source_ips matches default RFC1918 private ranges (order-independent) + isDefaultPrivateRanges := false + if len(db.Security.SourceIPs) == len(defaultPrivateIPRanges) { + // Create a map for O(1) lookup + defaultRangesMap := make(map[string]bool) + for _, cidr := range defaultPrivateIPRanges { + defaultRangesMap[cidr] = true + } + // Check if all API-returned IPs are in the defaults map + isDefaultPrivateRanges = true + for _, ip := range db.Security.SourceIPs { + if !defaultRangesMap[redis.StringValue(ip)] { isDefaultPrivateRanges = false break } diff --git a/provider/pro/resource_rediscloud_pro_subscription.go b/provider/pro/resource_rediscloud_pro_subscription.go index c0293f3f..6f5dcdc7 100644 --- a/provider/pro/resource_rediscloud_pro_subscription.go +++ b/provider/pro/resource_rediscloud_pro_subscription.go @@ -493,28 +493,16 @@ 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.", + 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. When not specified, defaults to false.", Type: schema.TypeBool, Optional: true, - Default: false, + Computed: true, }, "customer_managed_key_deletion_grace_period": { - Description: "The grace period for deleting the subscription. If not set, will default to immediate deletion grace period.", + Description: "The grace period for deleting the subscription. When not specified, defaults to immediate deletion.", Type: schema.TypeString, Optional: true, - Default: "immediate", - // TODO: remove this when customer_managed_key_deletion_grace_period is supported on api side - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - // Only suppress diff when: - // 1. Old is empty (upgrading from provider version without this field) - // 2. New is the default value "immediate" - // 3. CMK is NOT being enabled (customer_managed_key_enabled is false) - if old == "" && new == "immediate" { - cmkEnabled := d.Get("customer_managed_key_enabled").(bool) - return !cmkEnabled - } - return false - }, + Computed: true, }, "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.", @@ -760,7 +748,14 @@ func resourceRedisCloudProSubscriptionRead(ctx context.Context, d *schema.Resour } } - cmkEnabled := d.Get("customer_managed_key_enabled").(bool) + // Determine CMK status from API response + // CMK is enabled if persistentStorageEncryptionType is "customer-managed-key" + cmkEnabled := subscription.PersistentStorageEncryptionType != nil && + *subscription.PersistentStorageEncryptionType == CMK_ENABLED_STRING + if err := d.Set("customer_managed_key_enabled", cmkEnabled); err != nil { + return diag.FromErr(err) + } + if !cmkEnabled { m, err := api.Client.Maintenance.Get(ctx, subId) if err != nil { @@ -785,6 +780,13 @@ func resourceRedisCloudProSubscriptionRead(ctx context.Context, d *schema.Resour } } + // Set customer_managed_key_deletion_grace_period from API response if available + if subscription.DeletionGracePeriod != nil { + if err := d.Set("customer_managed_key_deletion_grace_period", redis.StringValue(subscription.DeletionGracePeriod)); err != nil { + return diag.FromErr(err) + } + } + // Set public_endpoint_access, default to true if not set by API publicEndpointAccess := true if subscription.PublicEndpointAccess != nil { diff --git a/provider/pro/testdata/pro_database_redis_8.tf b/provider/pro/testdata/pro_database_redis_8.tf index 9f967e5c..cc767296 100644 --- a/provider/pro/testdata/pro_database_redis_8.tf +++ b/provider/pro/testdata/pro_database_redis_8.tf @@ -54,7 +54,7 @@ resource "rediscloud_subscription_database" "example" { client_ssl_certificate = "" periodic_backup_path = "" enable_default_user = true - redis_version = "8.0" + redis_version = "8.2" alert { name = "dataset-size" diff --git a/provider/pro/testdata/pro_database_redis_8_with_modules.tf b/provider/pro/testdata/pro_database_redis_8_with_modules.tf index 8b9ed6a7..dc04979c 100644 --- a/provider/pro/testdata/pro_database_redis_8_with_modules.tf +++ b/provider/pro/testdata/pro_database_redis_8_with_modules.tf @@ -46,7 +46,7 @@ resource "rediscloud_subscription_database" "example" { throughput_measurement_by = "operations-per-second" throughput_measurement_value = 1000 password = local.rediscloud_database_password - redis_version = "8.0" + redis_version = "8.2" modules = [ { diff --git a/provider/rediscloud_active_active_database_enable_default_user_debug_test.go b/provider/rediscloud_active_active_database_enable_default_user_debug_test.go new file mode 100644 index 00000000..851378c3 --- /dev/null +++ b/provider/rediscloud_active_active_database_enable_default_user_debug_test.go @@ -0,0 +1,220 @@ +package provider + +import ( + "context" + "fmt" + "os" + "strconv" + "testing" + + "github.com/RedisLabs/rediscloud-go-api/redis" + "github.com/RedisLabs/rediscloud-go-api/service/databases" + "github.com/RedisLabs/terraform-provider-rediscloud/provider/client" + "github.com/RedisLabs/terraform-provider-rediscloud/provider/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +// TestAccResourceRedisCloudActiveActiveDatabase_enableDefaultUserDebug is a DEBUG version +// that reuses an existing subscription to speed up testing during development. +// +// SETUP: +// 1. Set DEBUG_SUBSCRIPTION_ID environment variable to an existing AA subscription ID +// 2. The subscription must have us-east-1 and us-east-2 regions +// 3. Run with: DEBUG_SUBSCRIPTION_ID=12345 EXECUTE_TESTS=true make testacc TESTARGS='-run=TestAccResourceRedisCloudActiveActiveDatabase_enableDefaultUserDebug' +// +// This test will: +// - Create a new database in the existing subscription +// - Update it through 2 test steps (global=true variants) +// - Delete the database at the end +func TestAccResourceRedisCloudActiveActiveDatabase_enableDefaultUserDebug(t *testing.T) { + utils.AccRequiresEnvVar(t, "EXECUTE_TESTS") + + subscriptionID := os.Getenv("DEBUG_SUBSCRIPTION_ID") + if subscriptionID == "" { + t.Skip("DEBUG_SUBSCRIPTION_ID not set - skipping debug test") + } + + databaseName := acctest.RandomWithPrefix("debug-enable-default-user") + databasePassword := acctest.RandString(20) + + const databaseResourceName = "rediscloud_active_active_subscription_database.example" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckActiveActiveDatabaseDestroy, + Steps: []resource.TestStep{ + // Step 1: global=true, both regions inherit + { + PreConfig: func() { + t.Logf("DEBUG Step 1: global=true, both inherit (subscription: %s, database: %s)", subscriptionID, databaseName) + }, + Config: fmt.Sprintf( + utils.GetTestConfig(t, "./activeactive/testdata/enable_default_user_debug_step1.tf"), + subscriptionID, + databaseName, + databasePassword, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Global setting + resource.TestCheckResourceAttr(databaseResourceName, "global_enable_default_user", "true"), + resource.TestCheckResourceAttr(databaseResourceName, "subscription_id", subscriptionID), + + // Both regions should exist + resource.TestCheckResourceAttr(databaseResourceName, "override_region.#", "2"), + + // Neither region should have enable_default_user in state + resource.TestCheckNoResourceAttr(databaseResourceName, "override_region.0.enable_default_user"), + resource.TestCheckNoResourceAttr(databaseResourceName, "override_region.1.enable_default_user"), + + // API check + testCheckEnableDefaultUserInAPIDebug(databaseResourceName, true, map[string]*bool{ + "us-east-1": nil, + "us-east-2": nil, + }), + ), + }, + // Step 2: global=true, us-east-1 explicit false + { + PreConfig: func() { + t.Logf("DEBUG Step 2: global=true, us-east-1 explicit false") + }, + Config: fmt.Sprintf( + utils.GetTestConfig(t, "./activeactive/testdata/enable_default_user_debug_step2.tf"), + subscriptionID, + databaseName, + databasePassword, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Global setting + resource.TestCheckResourceAttr(databaseResourceName, "global_enable_default_user", "true"), + + // Two regions + resource.TestCheckResourceAttr(databaseResourceName, "override_region.#", "2"), + + // us-east-1 has explicit false + resource.TestCheckTypeSetElemNestedAttrs(databaseResourceName, "override_region.*", map[string]string{ + "name": "us-east-1", + "enable_default_user": "false", + }), + + // API check + testCheckEnableDefaultUserInAPIDebug(databaseResourceName, true, map[string]*bool{ + "us-east-1": redis.Bool(false), + "us-east-2": nil, + }), + ), + }, + }, + }) +} + +// testCheckEnableDefaultUserInAPIDebug is identical to the regular version but for the debug test +func testCheckEnableDefaultUserInAPIDebug(resourceName string, expectedGlobal bool, expectedRegions map[string]*bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("resource not found: %s", resourceName) + } + + subIdStr := rs.Primary.Attributes["subscription_id"] + dbIdStr := rs.Primary.Attributes["db_id"] + + subId, err := strconv.Atoi(subIdStr) + if err != nil { + return fmt.Errorf("failed to parse subscription_id: %v", err) + } + + dbId, err := strconv.Atoi(dbIdStr) + if err != nil { + return fmt.Errorf("failed to parse db_id: %v", err) + } + + apiClient, err := client.NewClient() + if err != nil { + return fmt.Errorf("failed to get API client: %v", err) + } + + ctx := context.Background() + db, err := apiClient.Client.Database.GetActiveActive(ctx, subId, dbId) + if err != nil { + return fmt.Errorf("failed to get database from API: %v", err) + } + + if db.GlobalEnableDefaultUser == nil { + return fmt.Errorf("API returned nil for GlobalEnableDefaultUser") + } + actualGlobal := redis.BoolValue(db.GlobalEnableDefaultUser) + if actualGlobal != expectedGlobal { + return fmt.Errorf("API global_enable_default_user: expected %v, got %v", expectedGlobal, actualGlobal) + } + + for _, regionDb := range db.CrdbDatabases { + regionName := redis.StringValue(regionDb.Region) + + if regionDb.Security == nil || regionDb.Security.EnableDefaultUser == nil { + return fmt.Errorf("API returned nil for region %s EnableDefaultUser", regionName) + } + + actualRegionValue := redis.BoolValue(regionDb.Security.EnableDefaultUser) + + expectedValue, hasExplicitOverride := expectedRegions[regionName] + + var expectedRegionValue bool + if hasExplicitOverride && expectedValue != nil { + expectedRegionValue = *expectedValue + } else { + expectedRegionValue = expectedGlobal + } + + if actualRegionValue != expectedRegionValue { + return fmt.Errorf("API region %s enable_default_user: expected %v, got %v", + regionName, expectedRegionValue, actualRegionValue) + } + } + + return nil + } +} + +// testAccCheckActiveActiveDatabaseDestroy verifies the database was destroyed (subscription remains) +func testAccCheckActiveActiveDatabaseDestroy(s *terraform.State) error { + apiClient, err := client.NewClient() + if err != nil { + return err + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "rediscloud_active_active_subscription_database" { + continue + } + + subId, err := strconv.Atoi(rs.Primary.Attributes["subscription_id"]) + if err != nil { + continue + } + + dbId, err := strconv.Atoi(rs.Primary.Attributes["db_id"]) + if err != nil { + continue + } + + ctx := context.Background() + db, err := apiClient.Client.Database.GetActiveActive(ctx, subId, dbId) + if err != nil { + // Database not found is expected + if _, ok := err.(*databases.NotFound); ok { + continue + } + return err + } + + if db != nil { + return fmt.Errorf("database %d still exists in subscription %d", dbId, subId) + } + } + + return nil +} diff --git a/provider/rediscloud_active_active_database_enable_default_user_import_test.go b/provider/rediscloud_active_active_database_enable_default_user_import_test.go new file mode 100644 index 00000000..78fb7b76 --- /dev/null +++ b/provider/rediscloud_active_active_database_enable_default_user_import_test.go @@ -0,0 +1,284 @@ +package provider + +import ( + "context" + "fmt" + "log" + "os" + "strconv" + "testing" + + "github.com/RedisLabs/rediscloud-go-api/redis" + "github.com/RedisLabs/terraform-provider-rediscloud/provider/client" + "github.com/RedisLabs/terraform-provider-rediscloud/provider/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +// TestAccResourceRedisCloudActiveActiveDatabase_enableDefaultUserImport is a DEBUG version +// that imports and modifies an existing database to speed up testing during development. +// +// Unfortunately this test does not currently work as many fields are NOT supported in active active databases. + +// This test will: +// - Import the existing database with minimal stub config (Step 1) +// - Apply initial baseline config with both regions inheriting from global (Step 2) +// - Update to set us-east-1 explicit false while global remains true (Step 3) +// - Update to set global false and us-east-1 explicit true (Step 4) +// - Leave the database in place (no destroy) +func TestAccResourceRedisCloudActiveActiveDatabase_enableDefaultUserImport(t *testing.T) { + // Enable detailed logging + log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) + + utils.AccRequiresEnvVar(t, "EXECUTE_TESTS") + + subscriptionID := os.Getenv("DEBUG_SUBSCRIPTION_ID") + databaseID := os.Getenv("DEBUG_DATABASE_ID") + + if subscriptionID == "" || databaseID == "" { + t.Skip("DEBUG_SUBSCRIPTION_ID and DEBUG_DATABASE_ID must be set - skipping import debug test") + } + + t.Logf("=== TEST SETUP: subscription=%s, database=%s ===", subscriptionID, databaseID) + databasePassword := acctest.RandString(20) // Use new password for testing + + const databaseResourceName = "rediscloud_active_active_subscription_database.example" + importID := fmt.Sprintf("%s/%s", subscriptionID, databaseID) + t.Logf("=== IMPORT ID: %s ===", importID) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: nil, // No destroy - this test imports and modifies an existing database + Steps: []resource.TestStep{ + // Step 1: Import existing database with full config + { + PreConfig: func() { + t.Logf("DEBUG Step 1 PreConfig: Import existing database %s/%s", subscriptionID, databaseID) + }, + // Full config - must match what Read returns for import to stick + Config: fmt.Sprintf( + utils.GetTestConfig(t, "./activeactive/testdata/enable_default_user_debug_import_step1.tf"), + subscriptionID, + databasePassword, + ), + ResourceName: databaseResourceName, + ImportState: true, + ImportStateId: importID, + ImportStateVerify: false, + Check: resource.ComposeAggregateTestCheckFunc( + // Debug: Print state + func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[databaseResourceName] + if !ok { + return fmt.Errorf("resource not found in state: %s", databaseResourceName) + } + t.Logf("DEBUG Step 1 State - ID: %s", rs.Primary.ID) + t.Logf("DEBUG Step 1 State - subscription_id: %s", rs.Primary.Attributes["subscription_id"]) + t.Logf("DEBUG Step 1 State - db_id: %s", rs.Primary.Attributes["db_id"]) + t.Logf("DEBUG Step 1 State - name: %s", rs.Primary.Attributes["name"]) + return nil + }, + // Basic import checks - just verify the resource was imported + resource.TestCheckResourceAttr(databaseResourceName, "subscription_id", subscriptionID), + resource.TestCheckResourceAttr(databaseResourceName, "db_id", databaseID), + ), + }, + // Step 2: Apply initial baseline config - global=true, both regions inherit + { + PreConfig: func() { + t.Logf("DEBUG Step 2 PreConfig: Apply initial config - global=true, both inherit") + }, + Config: fmt.Sprintf( + utils.GetTestConfig(t, "./activeactive/testdata/enable_default_user_debug_import_step1.tf"), + subscriptionID, + databasePassword, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Debug: Print state + func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[databaseResourceName] + if !ok { + return fmt.Errorf("resource not found in state: %s", databaseResourceName) + } + t.Logf("DEBUG Step 2 State - ID: %s", rs.Primary.ID) + t.Logf("DEBUG Step 2 State - subscription_id: %s", rs.Primary.Attributes["subscription_id"]) + t.Logf("DEBUG Step 2 State - db_id: %s", rs.Primary.Attributes["db_id"]) + t.Logf("DEBUG Step 2 State - name: %s", rs.Primary.Attributes["name"]) + return nil + }, + // Global setting + resource.TestCheckResourceAttr(databaseResourceName, "global_enable_default_user", "true"), + resource.TestCheckResourceAttr(databaseResourceName, "subscription_id", subscriptionID), + resource.TestCheckResourceAttr(databaseResourceName, "db_id", databaseID), + + // Both regions should exist + resource.TestCheckResourceAttr(databaseResourceName, "override_region.#", "2"), + + // Neither region should have enable_default_user in state + resource.TestCheckNoResourceAttr(databaseResourceName, "override_region.0.enable_default_user"), + resource.TestCheckNoResourceAttr(databaseResourceName, "override_region.1.enable_default_user"), + + // API check + testCheckEnableDefaultUserInAPIImport(databaseResourceName, true, map[string]*bool{ + "eu-west-1": nil, + "us-east-1": nil, + }), + ), + }, + // Step 3: global=true, us-east-1 explicit false + { + PreConfig: func() { + t.Logf("DEBUG Step 3 PreConfig: global=true, us-east-1 explicit false") + }, + Config: fmt.Sprintf( + utils.GetTestConfig(t, "./activeactive/testdata/enable_default_user_debug_import_step2.tf"), + subscriptionID, + databasePassword, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Debug: Print state + func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[databaseResourceName] + if !ok { + return fmt.Errorf("resource not found in state: %s", databaseResourceName) + } + t.Logf("DEBUG Step 3 State - ID: %s", rs.Primary.ID) + t.Logf("DEBUG Step 3 State - subscription_id: %s", rs.Primary.Attributes["subscription_id"]) + t.Logf("DEBUG Step 3 State - db_id: %s", rs.Primary.Attributes["db_id"]) + t.Logf("DEBUG Step 3 State - name: %s", rs.Primary.Attributes["name"]) + return nil + }, + // Global setting + resource.TestCheckResourceAttr(databaseResourceName, "global_enable_default_user", "true"), + + // Two regions + resource.TestCheckResourceAttr(databaseResourceName, "override_region.#", "2"), + + // us-east-1 has explicit false + resource.TestCheckTypeSetElemNestedAttrs(databaseResourceName, "override_region.*", map[string]string{ + "name": "us-east-1", + "enable_default_user": "false", + }), + + // API check + testCheckEnableDefaultUserInAPIImport(databaseResourceName, true, map[string]*bool{ + "eu-west-1": nil, + "us-east-1": redis.Bool(false), + }), + ), + }, + // Step 4: global=false, us-east-1 explicit true + { + PreConfig: func() { + t.Logf("DEBUG Step 4 PreConfig: global=false, us-east-1 explicit true") + }, + Config: fmt.Sprintf( + utils.GetTestConfig(t, "./activeactive/testdata/enable_default_user_debug_import_step3.tf"), + subscriptionID, + databasePassword, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Debug: Print state + func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[databaseResourceName] + if !ok { + return fmt.Errorf("resource not found in state: %s", databaseResourceName) + } + t.Logf("DEBUG Step 4 State - ID: %s", rs.Primary.ID) + t.Logf("DEBUG Step 4 State - subscription_id: %s", rs.Primary.Attributes["subscription_id"]) + t.Logf("DEBUG Step 4 State - db_id: %s", rs.Primary.Attributes["db_id"]) + t.Logf("DEBUG Step 4 State - name: %s", rs.Primary.Attributes["name"]) + return nil + }, + // Global setting + resource.TestCheckResourceAttr(databaseResourceName, "global_enable_default_user", "false"), + + // Two regions + resource.TestCheckResourceAttr(databaseResourceName, "override_region.#", "2"), + + // us-east-1 has explicit true + resource.TestCheckTypeSetElemNestedAttrs(databaseResourceName, "override_region.*", map[string]string{ + "name": "us-east-1", + "enable_default_user": "true", + }), + + // API check + testCheckEnableDefaultUserInAPIImport(databaseResourceName, false, map[string]*bool{ + "eu-west-1": nil, + "us-east-1": redis.Bool(true), + }), + ), + }, + }, + }) +} + +// testCheckEnableDefaultUserInAPIImport is identical to the regular version but for the import test +func testCheckEnableDefaultUserInAPIImport(resourceName string, expectedGlobal bool, expectedRegions map[string]*bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("resource not found: %s", resourceName) + } + + subIdStr := rs.Primary.Attributes["subscription_id"] + dbIdStr := rs.Primary.Attributes["db_id"] + + subId, err := strconv.Atoi(subIdStr) + if err != nil { + return fmt.Errorf("failed to parse subscription_id: %v", err) + } + + dbId, err := strconv.Atoi(dbIdStr) + if err != nil { + return fmt.Errorf("failed to parse db_id: %v", err) + } + + apiClient, err := client.NewClient() + if err != nil { + return fmt.Errorf("failed to get API client: %v", err) + } + + ctx := context.Background() + db, err := apiClient.Client.Database.GetActiveActive(ctx, subId, dbId) + if err != nil { + return fmt.Errorf("failed to get database from API: %v", err) + } + + if db.GlobalEnableDefaultUser == nil { + return fmt.Errorf("API returned nil for GlobalEnableDefaultUser") + } + actualGlobal := redis.BoolValue(db.GlobalEnableDefaultUser) + if actualGlobal != expectedGlobal { + return fmt.Errorf("API global_enable_default_user: expected %v, got %v", expectedGlobal, actualGlobal) + } + + for _, regionDb := range db.CrdbDatabases { + regionName := redis.StringValue(regionDb.Region) + + if regionDb.Security == nil || regionDb.Security.EnableDefaultUser == nil { + return fmt.Errorf("API returned nil for region %s EnableDefaultUser", regionName) + } + + actualRegionValue := redis.BoolValue(regionDb.Security.EnableDefaultUser) + + expectedValue, hasExplicitOverride := expectedRegions[regionName] + + var expectedRegionValue bool + if hasExplicitOverride && expectedValue != nil { + expectedRegionValue = *expectedValue + } else { + expectedRegionValue = expectedGlobal + } + + if actualRegionValue != expectedRegionValue { + return fmt.Errorf("API region %s enable_default_user: expected %v, got %v", + regionName, expectedRegionValue, actualRegionValue) + } + } + + return nil + } +} diff --git a/provider/rediscloud_active_active_database_enable_default_user_test.go b/provider/rediscloud_active_active_database_enable_default_user_test.go new file mode 100644 index 00000000..14386ad7 --- /dev/null +++ b/provider/rediscloud_active_active_database_enable_default_user_test.go @@ -0,0 +1,317 @@ +package provider + +import ( + "context" + "fmt" + "strconv" + "testing" + + "github.com/RedisLabs/rediscloud-go-api/redis" + "github.com/RedisLabs/terraform-provider-rediscloud/provider/client" + "github.com/RedisLabs/terraform-provider-rediscloud/provider/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +// TestAccResourceRedisCloudActiveActiveDatabase_enableDefaultUser tests the enable_default_user field +// for global and regional override behavior, specifically testing for drift issues when: +// - Regions inherit from global (field NOT in override_region) - Steps 1, 5 +// - Regions explicitly override global (field IS in override_region) - Steps 2, 3 +// - User explicitly sets same value as global (field IS in override_region, tests explicit vs inherited) - Steps 4, 6 +// +// Tests all 6 combinations: +// Step 1: global=true, both inherit +// Step 2: global=true, one region=false, one inherit +// Step 3: global=false, one region=true, one inherit +// Step 4: global=true, region1=true (matches), region2=false +// Step 5: global=false, both inherit +// Step 6: global=false, region1=false (matches), one inherit +func TestAccResourceRedisCloudActiveActiveDatabase_enableDefaultUser(t *testing.T) { + subscriptionName := acctest.RandomWithPrefix(testResourcePrefix) + "-subscription" + databaseName := acctest.RandomWithPrefix(testResourcePrefix) + "-database" + databasePassword := acctest.RandString(20) + + const databaseResourceName = "rediscloud_active_active_subscription_database.example" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckActiveActiveSubscriptionDestroy, + Steps: []resource.TestStep{ + // Step 1: global=true, both regions inherit (NO enable_default_user in override_region) + { + PreConfig: func() { + t.Logf("Starting Step 1: global=true, both regions inherit (subscription: %s, database: %s)", subscriptionName, databaseName) + }, + Config: fmt.Sprintf( + utils.GetTestConfig(t, "./activeactive/testdata/enable_default_user_global_true_inherit.tf"), + subscriptionName, + databaseName, + databasePassword, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Global setting + resource.TestCheckResourceAttr(databaseResourceName, "global_enable_default_user", "true"), + + // Both regions should exist + resource.TestCheckResourceAttr(databaseResourceName, "override_region.#", "2"), + + // Neither region should have enable_default_user in state (inheriting from global) + resource.TestCheckNoResourceAttr(databaseResourceName, "override_region.0.enable_default_user"), + resource.TestCheckNoResourceAttr(databaseResourceName, "override_region.1.enable_default_user"), + + // API check: Both regions should have true (inherited from global) + testCheckEnableDefaultUserInAPI(databaseResourceName, true, map[string]*bool{ + "us-east-1": nil, // nil means inherits from global + "us-east-2": nil, + }), + ), + }, + // Step 2: global=true, us-east-1 explicit false (field SHOULD appear in override_region) + { + PreConfig: func() { + t.Logf("Starting Step 2: global=true, us-east-1 explicit false") + }, + Config: fmt.Sprintf( + utils.GetTestConfig(t, "./activeactive/testdata/enable_default_user_global_true_region_false.tf"), + subscriptionName, + databaseName, + databasePassword, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Global setting + resource.TestCheckResourceAttr(databaseResourceName, "global_enable_default_user", "true"), + + // Two regions + resource.TestCheckResourceAttr(databaseResourceName, "override_region.#", "2"), + + // us-east-1 has explicit false (differs from global=true) + // us-east-2 inherits (no explicit field) + // Use TestCheckTypeSet to verify specific TypeSet element fields + resource.TestCheckTypeSetElemNestedAttrs(databaseResourceName, "override_region.*", map[string]string{ + "name": "us-east-1", + "enable_default_user": "false", + }), + // us-east-2 should NOT have enable_default_user field in this step + + // API check: us-east-1=false (explicit), us-east-2=true (inherited) + testCheckEnableDefaultUserInAPI(databaseResourceName, true, map[string]*bool{ + "us-east-1": redis.Bool(false), // Explicit override + "us-east-2": nil, // Inherits from global + }), + ), + }, + // Step 3: global=false, us-east-1 explicit true (field SHOULD appear in override_region) + { + PreConfig: func() { + t.Logf("Starting Step 3: global=false, us-east-1 explicit true") + }, + Config: fmt.Sprintf( + utils.GetTestConfig(t, "./activeactive/testdata/enable_default_user_global_false_region_true.tf"), + subscriptionName, + databaseName, + databasePassword, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Global setting + resource.TestCheckResourceAttr(databaseResourceName, "global_enable_default_user", "false"), + + // Two regions + resource.TestCheckResourceAttr(databaseResourceName, "override_region.#", "2"), + + // us-east-1 has explicit true (differs from global=false) + resource.TestCheckTypeSetElemNestedAttrs(databaseResourceName, "override_region.*", map[string]string{ + "name": "us-east-1", + "enable_default_user": "true", + }), + // us-east-2 inherits (no explicit field) + + // API check: us-east-1=true (explicit), us-east-2=false (inherited) + testCheckEnableDefaultUserInAPI(databaseResourceName, false, map[string]*bool{ + "us-east-1": redis.Bool(true), // Explicit override + "us-east-2": nil, // Inherits from global + }), + ), + }, + // Step 4: global=true, both regions explicit (us-east-1=true, us-east-2=false) + // This tests that explicit values matching global are still preserved + { + PreConfig: func() { + t.Logf("Starting Step 4: global=true, both regions explicit (us-east-1=true, us-east-2=false)") + }, + Config: fmt.Sprintf( + utils.GetTestConfig(t, "./activeactive/testdata/enable_default_user_all_explicit.tf"), + subscriptionName, + databaseName, + databasePassword, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Global setting + resource.TestCheckResourceAttr(databaseResourceName, "global_enable_default_user", "true"), + + // Two regions + resource.TestCheckResourceAttr(databaseResourceName, "override_region.#", "2"), + + // Both regions have explicit enable_default_user - both should be in state + // us-east-1 has true (matches global but explicit) + resource.TestCheckTypeSetElemNestedAttrs(databaseResourceName, "override_region.*", map[string]string{ + "name": "us-east-1", + "enable_default_user": "true", + }), + // us-east-2 has false (differs from global) + resource.TestCheckTypeSetElemNestedAttrs(databaseResourceName, "override_region.*", map[string]string{ + "name": "us-east-2", + "enable_default_user": "false", + }), + + // API check: us-east-1=true (explicit), us-east-2=false (explicit) + testCheckEnableDefaultUserInAPI(databaseResourceName, true, map[string]*bool{ + "us-east-1": redis.Bool(true), // Explicit (matches global) + "us-east-2": redis.Bool(false), // Explicit (differs from global) + }), + ), + }, + // Step 5: global=false, both regions inherit (NO enable_default_user in override_region) + // Mirror of Step 1 but with global=false + { + PreConfig: func() { + t.Logf("Starting Step 5: global=false, both regions inherit") + }, + Config: fmt.Sprintf( + utils.GetTestConfig(t, "./activeactive/testdata/enable_default_user_global_false_inherit.tf"), + subscriptionName, + databaseName, + databasePassword, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Global setting + resource.TestCheckResourceAttr(databaseResourceName, "global_enable_default_user", "false"), + + // Both regions should exist + resource.TestCheckResourceAttr(databaseResourceName, "override_region.#", "2"), + + // Neither region should have enable_default_user in state (inheriting from global) + resource.TestCheckNoResourceAttr(databaseResourceName, "override_region.0.enable_default_user"), + resource.TestCheckNoResourceAttr(databaseResourceName, "override_region.1.enable_default_user"), + + // API check: Both regions should have false (inherited from global) + testCheckEnableDefaultUserInAPI(databaseResourceName, false, map[string]*bool{ + "us-east-1": nil, // Inherits from global + "us-east-2": nil, + }), + ), + }, + // Step 6: global=false, us-east-1 explicit false (field SHOULD appear in override_region) + // Tests explicit false matching global false (vs inheriting) + { + PreConfig: func() { + t.Logf("Starting Step 6: global=false, us-east-1 explicit false (matches global)") + }, + Config: fmt.Sprintf( + utils.GetTestConfig(t, "./activeactive/testdata/enable_default_user_global_false_region_false.tf"), + subscriptionName, + databaseName, + databasePassword, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Global setting + resource.TestCheckResourceAttr(databaseResourceName, "global_enable_default_user", "false"), + + // Two regions + resource.TestCheckResourceAttr(databaseResourceName, "override_region.#", "2"), + + // us-east-1 has explicit false (matches global but is EXPLICIT) + resource.TestCheckTypeSetElemNestedAttrs(databaseResourceName, "override_region.*", map[string]string{ + "name": "us-east-1", + "enable_default_user": "false", + }), + // us-east-2 inherits (no explicit field) + + // API check: us-east-1=false (explicit, matches global), us-east-2=false (inherited) + testCheckEnableDefaultUserInAPI(databaseResourceName, false, map[string]*bool{ + "us-east-1": redis.Bool(false), // Explicit (matches global) + "us-east-2": nil, // Inherits from global + }), + ), + }, + }, + }) +} + +// testCheckEnableDefaultUserInAPI verifies the enable_default_user values in the actual Redis Cloud API +// expected: map[regionName]expectedValue (nil means should match global) +func testCheckEnableDefaultUserInAPI(resourceName string, expectedGlobal bool, expectedRegions map[string]*bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("resource not found: %s", resourceName) + } + + // Parse subscription_id and db_id from resource + subIdStr := rs.Primary.Attributes["subscription_id"] + dbIdStr := rs.Primary.Attributes["db_id"] + + subId, err := strconv.Atoi(subIdStr) + if err != nil { + return fmt.Errorf("failed to parse subscription_id: %v", err) + } + + dbId, err := strconv.Atoi(dbIdStr) + if err != nil { + return fmt.Errorf("failed to parse db_id: %v", err) + } + + // Get API client + apiClient, err := client.NewClient() + if err != nil { + return fmt.Errorf("failed to get API client: %v", err) + } + + // Fetch database from API + ctx := context.Background() + db, err := apiClient.Client.Database.GetActiveActive(ctx, subId, dbId) + if err != nil { + return fmt.Errorf("failed to get database from API: %v", err) + } + + // Check global enable_default_user + if db.GlobalEnableDefaultUser == nil { + return fmt.Errorf("API returned nil for GlobalEnableDefaultUser") + } + actualGlobal := redis.BoolValue(db.GlobalEnableDefaultUser) + if actualGlobal != expectedGlobal { + return fmt.Errorf("API global_enable_default_user: expected %v, got %v", expectedGlobal, actualGlobal) + } + + // Check regional enable_default_user values + for _, regionDb := range db.CrdbDatabases { + regionName := redis.StringValue(regionDb.Region) + + if regionDb.Security == nil || regionDb.Security.EnableDefaultUser == nil { + return fmt.Errorf("API returned nil for region %s EnableDefaultUser", regionName) + } + + actualRegionValue := redis.BoolValue(regionDb.Security.EnableDefaultUser) + + // Get expected value for this region + expectedValue, hasExplicitOverride := expectedRegions[regionName] + + var expectedRegionValue bool + if hasExplicitOverride && expectedValue != nil { + // Region has explicit override + expectedRegionValue = *expectedValue + } else { + // Region inherits from global + expectedRegionValue = expectedGlobal + } + + if actualRegionValue != expectedRegionValue { + return fmt.Errorf("API region %s enable_default_user: expected %v, got %v", + regionName, expectedRegionValue, actualRegionValue) + } + } + + return nil + } +} diff --git a/provider/resource_rediscloud_active_active_database.go b/provider/resource_rediscloud_active_active_database.go index 8d6ed45f..bd45b9cd 100644 --- a/provider/resource_rediscloud_active_active_database.go +++ b/provider/resource_rediscloud_active_active_database.go @@ -12,6 +12,7 @@ import ( "github.com/RedisLabs/terraform-provider-rediscloud/provider/client" "github.com/RedisLabs/terraform-provider-rediscloud/provider/pro" "github.com/RedisLabs/terraform-provider-rediscloud/provider/utils" + "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -28,17 +29,30 @@ func resourceRedisCloudActiveActiveDatabase() *schema.Resource { Importer: &schema.ResourceImporter{ StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + log.Printf("[DEBUG] IMPORT StateContext CALLED: import_id=%s", d.Id()) + log.Printf("[INFO] Starting Active-Active database import: import_id=%s", d.Id()) + subId, dbId, err := pro.ToDatabaseId(d.Id()) if err != nil { + log.Printf("[ERROR] Failed to parse import ID: import_id=%s, error=%v", d.Id(), err) return nil, err } + + log.Printf("[DEBUG] IMPORT: Parsed subscription_id=%d, db_id=%d", subId, dbId) + if err := d.Set("subscription_id", subId); err != nil { + log.Printf("[ERROR] Failed to set subscription_id: subscription_id=%d, error=%v", subId, err) return nil, err } if err := d.Set("db_id", dbId); err != nil { + log.Printf("[ERROR] Failed to set db_id: db_id=%d, error=%v", dbId, err) return nil, err } d.SetId(utils.BuildResourceId(subId, dbId)) + + log.Printf("[DEBUG] IMPORT: Set resource ID to: %s", d.Id()) + log.Printf("[INFO] Import initialization complete - Read will be called next: resource_id=%s", d.Id()) + return []*schema.ResourceData{d}, nil }, }, @@ -148,6 +162,7 @@ func resourceRedisCloudActiveActiveDatabase() *schema.Resource { Description: "Rate of database data persistence (in persistent storage)", Type: schema.TypeString, Optional: true, + Computed: true, }, "global_password": { Description: "Password used to access the database. If left empty, the password will be generated automatically", @@ -260,10 +275,9 @@ func resourceRedisCloudActiveActiveDatabase() *schema.Resource { Optional: true, }, "enable_default_user": { - Description: "When 'true', enables connecting to the database with the 'default' user. Default: 'true'", + Description: "When 'true', enables connecting to the database with the 'default' user. If not specified, the region inherits the value from global_enable_default_user.", Type: schema.TypeBool, Optional: true, - Default: true, }, "remote_backup": { Description: "An object that specifies the backup options for the database in this region", @@ -356,9 +370,11 @@ func resourceRedisCloudActiveActiveDatabaseCreate(ctx context.Context, d *schema api := meta.(*client.ApiClient) subId := d.Get("subscription_id").(int) - utils.SubscriptionMutex.Lock(subId) - name := d.Get("name").(string) + log.Printf("[DEBUG] CREATE CALLED: subscription_id=%d, name=%s, state_id=%s, has_id=%v", + subId, name, d.Id(), d.Id() != "") + + utils.SubscriptionMutex.Lock(subId) supportOSSClusterAPI := d.Get("support_oss_cluster_api").(bool) useExternalEndpointForOSSClusterAPI := d.Get("external_endpoint_for_oss_cluster_api").(bool) globalSourceIp := utils.SetToStringSlice(d.Get("global_source_ips").(*schema.Set)) @@ -449,7 +465,6 @@ func resourceRedisCloudActiveActiveDatabaseCreate(ctx context.Context, d *schema createDatabase.RedisVersion = s }) - // Confirm Subscription Active status before creating database err = utils.WaitForSubscriptionToBeActive(ctx, subId, api) if err != nil { @@ -483,6 +498,49 @@ func resourceRedisCloudActiveActiveDatabaseCreate(ctx context.Context, d *schema return resourceRedisCloudActiveActiveDatabaseUpdate(ctx, d, meta) } +// readOperationMode identifies the context in which Read is being called +type readOperationMode int + +const ( + readModeImport readOperationMode = iota // Import: no config/state exists yet + readModeApply // Apply/Update: config is available + readModeRefresh // Refresh: only state is available +) + +// String returns a human-readable name for the mode +func (m readOperationMode) String() string { + switch m { + case readModeImport: + return "import" + case readModeApply: + return "apply/update" + case readModeRefresh: + return "refresh" + default: + return "unknown" + } +} + +// detectReadOperationMode determines which operation mode we're in based on availability of config and state +func detectReadOperationMode(d *schema.ResourceData) readOperationMode { + rawConfig := d.GetRawConfig() + rawState := d.GetRawState() + + configAvailable := !rawConfig.IsNull() && rawConfig.IsKnown() + stateExists := !rawState.IsNull() && rawState.IsKnown() && len(d.Get("override_region").(*schema.Set).List()) > 0 + + if !configAvailable && !stateExists { + // Import: Neither config nor state available yet + return readModeImport + } else if configAvailable { + // Apply/Update: Config is available (Create/Update operation) + return readModeApply + } else { + // Refresh: Only state available (standalone refresh operation) + return readModeRefresh + } +} + func resourceRedisCloudActiveActiveDatabaseRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { api := meta.(*client.ApiClient) @@ -498,15 +556,27 @@ func resourceRedisCloudActiveActiveDatabaseRead(ctx context.Context, d *schema.R subId = d.Get("subscription_id").(int) } + log.Printf("[DEBUG] READ CALLED: state_id=%s, parsed_sub_id=%d, parsed_db_id=%d, schema_sub_id=%v", + d.Id(), subId, dbId, d.Get("subscription_id")) + log.Printf("[INFO] Starting Active-Active database Read: resource_id=%s, subscription_id=%d, db_id=%d", + d.Id(), subId, dbId) + db, err := api.Client.Database.GetActiveActive(ctx, subId, dbId) if err != nil { if _, ok := err.(*databases.NotFound); ok { + log.Printf("[DEBUG] READ: Database not found, clearing state: sub_id=%d, db_id=%d", subId, dbId) + log.Printf("[INFO] Database not found, clearing state: subscription_id=%d, db_id=%d", subId, dbId) d.SetId("") return diags } return diag.FromErr(err) } + log.Printf("[DEBUG] READ: Fetched from API - name=%s, id=%d, sub_id=%d", + redis.StringValue(db.Name), redis.IntValue(db.ID), subId) + log.Printf("[DEBUG] Fetched database from API: name=%s, id=%d, global_enable_default_user=%v, num_regions=%d", + redis.StringValue(db.Name), redis.IntValue(db.ID), redis.BoolValue(db.GlobalEnableDefaultUser), len(db.CrdbDatabases)) + if err := d.Set("db_id", redis.IntValue(db.ID)); err != nil { return diag.FromErr(err) } @@ -563,16 +633,54 @@ func resourceRedisCloudActiveActiveDatabaseRead(ctx context.Context, d *schema.R var regionDbConfigs []map[string]interface{} publicEndpointConfig := make(map[string]interface{}) privateEndpointConfig := make(map[string]interface{}) + + // Determine if override_region should be tracked based on operation mode + mode := detectReadOperationMode(d) + shouldTrackOverrideRegion := false + + switch mode { + case readModeImport: + // Import mode: Don't populate override_region (preserves historical behavior since resource creation) + shouldTrackOverrideRegion = false + log.Printf("[DEBUG] Import mode detected") + + case readModeApply: + // Apply/Update mode: Check if config has override_region blocks + // Config is source of truth - only track if user declared it + rawConfig := d.GetRawConfig() + overrideRegionValue := rawConfig.GetAttr("override_region") + if !overrideRegionValue.IsNull() && overrideRegionValue.LengthInt() > 0 { + shouldTrackOverrideRegion = true + } + log.Printf("[DEBUG] Apply/Update mode detected - shouldTrackOverrideRegion=%v (from config)", shouldTrackOverrideRegion) + + case readModeRefresh: + // Refresh mode: Check if state has override_region blocks + // Preserve what was previously in state + shouldTrackOverrideRegion = len(d.Get("override_region").(*schema.Set).List()) > 0 + log.Printf("[DEBUG] Refresh mode detected - shouldTrackOverrideRegion=%v (from state)", shouldTrackOverrideRegion) + } + + log.Printf("[DEBUG] Processing regions from API response: num_regions=%d, mode=%v, shouldTrackOverrideRegion=%v", + len(db.CrdbDatabases), mode, shouldTrackOverrideRegion) + for _, regionDb := range db.CrdbDatabases { region := redis.StringValue(regionDb.Region) // Set the endpoints for the region publicEndpointConfig[region] = redis.StringValue(regionDb.PublicEndpoint) privateEndpointConfig[region] = redis.StringValue(regionDb.PrivateEndpoint) - // Check if the region is in the state as an override - stateOverrideRegion := getStateOverrideRegion(d, region) - if stateOverrideRegion == nil { + + // If config doesn't have override_region blocks, skip all region processing + if !shouldTrackOverrideRegion { + log.Printf("[DEBUG] Skipping region - no override_region blocks in config: region=%s", region) continue } + + // Get state for this region (may be nil during import, that's OK) + stateOverrideRegion := getStateOverrideRegion(d, region) + + log.Printf("[DEBUG] Processing region: region=%s, has_override_in_state=%v, shouldTrackOverrideRegion=%v, region_enable_default_user=%v", + region, stateOverrideRegion != nil, shouldTrackOverrideRegion, redis.BoolValue(regionDb.Security.EnableDefaultUser)) regionDbConfig := map[string]interface{}{ "name": region, } @@ -631,19 +739,81 @@ func resourceRedisCloudActiveActiveDatabaseRead(ctx context.Context, d *schema.R regionDbConfig["remote_backup"] = pro.FlattenBackupPlan(regionDb.Backup, getStateRemoteBackup(d, region), "") - // Only set enable_default_user if it was explicitly configured in the override_region - if stateEnableDefaultUser := getStateOverrideRegion(d, region)["enable_default_user"]; stateEnableDefaultUser != nil { - regionDbConfig["enable_default_user"] = redis.BoolValue(regionDb.Security.EnableDefaultUser) + // Handle enable_default_user with hybrid GetRawConfig/GetRawState approach + // to avoid drift issues with TypeSet materialization + if regionDb.Security.EnableDefaultUser != nil { + // Use API value for global, not state value, to ensure correct comparison during import + globalEnableDefaultUser := redis.BoolValue(db.GlobalEnableDefaultUser) + regionEnableDefaultUser := redis.BoolValue(regionDb.Security.EnableDefaultUser) + + log.Printf("[DEBUG] Read enable_default_user for region - starting evaluation: region=%s, region_value_from_api=%v, global_value=%v, values_match=%v", + region, regionEnableDefaultUser, globalEnableDefaultUser, regionEnableDefaultUser == globalEnableDefaultUser) + + // Check if GetRawConfig is available (during Apply/Update) + rawConfig := d.GetRawConfig() + getRawConfigAvailable := !rawConfig.IsNull() && rawConfig.IsKnown() + + shouldInclude := false + var reason string + + if getRawConfigAvailable { + // Config-based mode: Check if explicitly set in config + wasExplicitlySet := isEnableDefaultUserExplicitlySetInConfig(d, region) + log.Printf("[DEBUG] Config-based detection for region: region=%s, wasExplicitlySet=%v", region, wasExplicitlySet) + + if wasExplicitlySet { + shouldInclude = true + reason = "explicitly set in config" + } else if regionEnableDefaultUser != globalEnableDefaultUser { + shouldInclude = true + reason = "differs from global (API override)" + } else { + shouldInclude = false + reason = "not in config and matches global (inherited)" + } + } else { + // State-based mode: Check if was in actual persisted state + fieldWasInActualState := isEnableDefaultUserInActualPersistedState(d, region) + log.Printf("[DEBUG] State-based detection for region: region=%s, fieldWasInActualState=%v", region, fieldWasInActualState) + + if fieldWasInActualState { + shouldInclude = true + reason = "was in state, preserving (user explicit)" + } else if regionEnableDefaultUser != globalEnableDefaultUser { + shouldInclude = true + reason = "not in state but differs from global (API override)" + } else { + shouldInclude = false + reason = "not in state and matches global (inherited)" + } + } + + log.Printf("[INFO] enable_default_user decision for region: region=%s, shouldInclude=%v, reason=%s, will_set_in_state=%v, value_if_set=%v", + region, shouldInclude, reason, shouldInclude, regionEnableDefaultUser) + + if shouldInclude { + regionDbConfig["enable_default_user"] = regionEnableDefaultUser + log.Printf("[DEBUG] Set enable_default_user in regionDbConfig: region=%s, value=%v", region, regionEnableDefaultUser) + } else { + log.Printf("[DEBUG] NOT setting enable_default_user in regionDbConfig (will inherit from global): region=%s", region) + } } + log.Printf("[DEBUG] Completed processing region, appending to regionDbConfigs: region=%s, has_enable_default_user_key=%v", + region, regionDbConfig["enable_default_user"] != nil) + regionDbConfigs = append(regionDbConfigs, regionDbConfig) } - // Only set override_region if it is defined in the config - if len(d.Get("override_region").(*schema.Set).List()) > 0 { + // Only set override_region if it should be tracked (based on config check above) + if shouldTrackOverrideRegion { + log.Printf("[DEBUG] Setting override_region in state: num_regions=%d", len(regionDbConfigs)) if err := d.Set("override_region", regionDbConfigs); err != nil { return diag.FromErr(err) } + log.Printf("[INFO] Successfully set override_region in state: num_regions=%d", len(regionDbConfigs)) + } else { + log.Printf("[DEBUG] NOT setting override_region - no override_region blocks in config") } if err := d.Set("public_endpoint", publicEndpointConfig); err != nil { @@ -660,10 +830,11 @@ func resourceRedisCloudActiveActiveDatabaseRead(ctx context.Context, d *schema.R return diag.FromErr(err) } - // Read global_enable_default_user from API response if db.GlobalEnableDefaultUser != nil { - if err := d.Set("global_enable_default_user", redis.BoolValue(db.GlobalEnableDefaultUser)); err != nil { + globalValue := redis.BoolValue(db.GlobalEnableDefaultUser) + log.Printf("[DEBUG] Setting global_enable_default_user in state: value=%v", globalValue) + if err := d.Set("global_enable_default_user", globalValue); err != nil { return diag.FromErr(err) } } @@ -677,6 +848,9 @@ func resourceRedisCloudActiveActiveDatabaseRead(ctx context.Context, d *schema.R return diag.FromErr(err) } + log.Printf("[INFO] Completed Active-Active database Read: resource_id=%s, global_enable_default_user=%v, num_override_regions=%d", + d.Id(), d.Get("global_enable_default_user"), len(d.Get("override_region").(*schema.Set).List())) + return diags } @@ -720,6 +894,11 @@ func resourceRedisCloudActiveActiveDatabaseUpdate(ctx context.Context, d *schema } subId := d.Get("subscription_id").(int) + name := d.Get("name").(string) + log.Printf("[DEBUG] UPDATE CALLED: subscription_id=%d, db_id=%d, name=%s, state_id=%s", subId, dbId, name, d.Id()) + log.Printf("[INFO] Starting Active-Active database Update: subscription_id=%d, db_id=%d, name=%s, global_enable_default_user=%v", + subId, dbId, name, d.Get("global_enable_default_user")) + utils.SubscriptionMutex.Lock(subId) defer utils.SubscriptionMutex.Unlock(subId) @@ -740,10 +919,17 @@ func resourceRedisCloudActiveActiveDatabaseUpdate(ctx context.Context, d *schema // Make a list of region-specific configurations var regions []*databases.LocalRegionProperties + + log.Printf("[DEBUG] Building region-specific configurations for Update: num_regions=%d", len(d.Get("override_region").(*schema.Set).List())) + for _, region := range d.Get("override_region").(*schema.Set).List() { dbRegion := region.(map[string]interface{}) + regionName := dbRegion["name"].(string) + + log.Printf("[DEBUG] Processing region for Update: region=%s, has_enable_default_user_key=%v", + regionName, dbRegion["enable_default_user"] != nil) - overrideAlerts := getStateAlertsFromDbRegion(getStateOverrideRegion(d, dbRegion["name"].(string))) + overrideAlerts := getStateAlertsFromDbRegion(getStateOverrideRegion(d, regionName)) // Make a list of region-specific source IPs for use in the regions list below var overrideSourceIps []*string @@ -752,12 +938,30 @@ func resourceRedisCloudActiveActiveDatabaseUpdate(ctx context.Context, d *schema } regionProps := &databases.LocalRegionProperties{ - Region: redis.String(dbRegion["name"].(string)), + Region: redis.String(regionName), } - // Only set EnableDefaultUser if it was explicitly configured in the override_region - if val, exists := dbRegion["enable_default_user"]; exists && val != nil { - regionProps.EnableDefaultUser = redis.Bool(val.(bool)) + // Handle enable_default_user: Only send if explicitly set in config + // With Default removed from schema, we use GetRawConfig to detect explicit setting + explicitlySet := isEnableDefaultUserExplicitlySetInConfig(d, regionName) + + log.Printf("[DEBUG] Update: Checking enable_default_user for region: region=%s, explicitly_set_in_config=%v, value_in_dbRegion=%v", + regionName, explicitlySet, dbRegion["enable_default_user"]) + + if explicitlySet { + // User explicitly set it in config - send the value + if val, exists := dbRegion["enable_default_user"]; exists && val != nil { + boolVal := val.(bool) + regionProps.EnableDefaultUser = redis.Bool(boolVal) + log.Printf("[INFO] Update: SENDING enable_default_user for region (explicitly set): region=%s, value=%v, will_override_global=%v", + regionName, boolVal, boolVal != d.Get("global_enable_default_user").(bool)) + } else { + log.Printf("[WARN] Update: Field marked as explicitly set but value missing: region=%s", regionName) + } + } else { + // Not explicitly set - don't send field, API will use global + log.Printf("[INFO] Update: NOT sending enable_default_user for region (inherits from global): region=%s, global_value=%v", + regionName, d.Get("global_enable_default_user")) } if len(overrideAlerts) > 0 { @@ -820,10 +1024,12 @@ func resourceRedisCloudActiveActiveDatabaseUpdate(ctx context.Context, d *schema update.GlobalDataPersistence = redis.String(d.Get("global_data_persistence").(string)) } - if v, ok := d.GetOkExists("global_enable_default_user"); ok { - update.GlobalEnableDefaultUser = redis.Bool(v.(bool)) - } + // global_enable_default_user has Default: true, so field always has a value + // No need for GetOkExists - just use d.Get() directly + globalEnableDefaultUserValue := d.Get("global_enable_default_user").(bool) + update.GlobalEnableDefaultUser = redis.Bool(globalEnableDefaultUserValue) + log.Printf("[INFO] Update: Setting global_enable_default_user in API request: value=%v", globalEnableDefaultUserValue) if v, ok := d.GetOk("support_oss_cluster_api"); ok { update.SupportOSSClusterAPI = redis.Bool(v.(bool)) @@ -862,11 +1068,17 @@ func resourceRedisCloudActiveActiveDatabaseUpdate(ctx context.Context, d *schema } } + log.Printf("[INFO] Sending ActiveActiveUpdate API request: subscription_id=%d, db_id=%d, global_enable_default_user=%v, num_regions=%d", + subId, dbId, globalEnableDefaultUserValue, len(regions)) + err = api.Client.Database.ActiveActiveUpdate(ctx, subId, dbId, update) if err != nil { + log.Printf("[ERROR] ActiveActiveUpdate API request failed: subscription_id=%d, db_id=%d, error=%v", subId, dbId, err) return diag.FromErr(err) } + log.Printf("[INFO] ActiveActiveUpdate API request successful, waiting for database to be active: subscription_id=%d, db_id=%d", subId, dbId) + if err := utils.WaitForDatabaseToBeActive(ctx, subId, dbId, api); err != nil { return diag.FromErr(err) } @@ -880,6 +1092,8 @@ func resourceRedisCloudActiveActiveDatabaseUpdate(ctx context.Context, d *schema return diag.FromErr(err) } + log.Printf("[INFO] Update complete, calling Read to refresh state: subscription_id=%d, db_id=%d", subId, dbId) + return resourceRedisCloudActiveActiveDatabaseRead(ctx, d, meta) } @@ -959,3 +1173,131 @@ func flattenModulesToNames(modules []*databases.Module) []string { } return moduleNames } + +// findRegionFieldInCtyValue navigates through a cty.Value representing override_region Set +// and finds a specific field within a region identified by regionName. +// Returns the field's cty.Value and true if found, or cty.NilVal and false if not found. +// This helper is used by both config and state detection functions. +func findRegionFieldInCtyValue(ctyVal cty.Value, regionName string, fieldName string) (cty.Value, bool) { + // Check if ctyVal is null or unknown + if ctyVal.IsNull() || !ctyVal.IsKnown() { + log.Printf("[DEBUG] findRegionFieldInCtyValue: cty.Value is null or unknown for region=%s field=%s", regionName, fieldName) + return cty.NilVal, false + } + + // Get the override_region attribute + if !ctyVal.Type().HasAttribute("override_region") { + log.Printf("[DEBUG] findRegionFieldInCtyValue: No override_region attribute found") + return cty.NilVal, false + } + + overrideRegions := ctyVal.GetAttr("override_region") + if overrideRegions.IsNull() || !overrideRegions.IsKnown() { + log.Printf("[DEBUG] findRegionFieldInCtyValue: override_region is null or unknown") + return cty.NilVal, false + } + + // override_region is a Set, so we need to iterate through it + if !overrideRegions.Type().IsSetType() && !overrideRegions.Type().IsListType() { + log.Printf("[DEBUG] findRegionFieldInCtyValue: override_region is not a Set or List type: %s", overrideRegions.Type().FriendlyName()) + return cty.NilVal, false + } + + // Iterate through each region in the Set + iter := overrideRegions.ElementIterator() + for iter.Next() { + _, regionVal := iter.Element() + + if regionVal.IsNull() || !regionVal.IsKnown() { + continue + } + + // Check if this region has a "name" attribute matching our search + if !regionVal.Type().HasAttribute("name") { + continue + } + + nameAttr := regionVal.GetAttr("name") + if nameAttr.IsNull() || !nameAttr.IsKnown() { + continue + } + + // Check if the name matches + if nameAttr.AsString() != regionName { + continue + } + + // Found the matching region! Now check for the field + log.Printf("[DEBUG] findRegionFieldInCtyValue: Found matching region %s", regionName) + + if !regionVal.Type().HasAttribute(fieldName) { + log.Printf("[DEBUG] findRegionFieldInCtyValue: Region %s does not have attribute %s", regionName, fieldName) + return cty.NilVal, false + } + + fieldAttr := regionVal.GetAttr(fieldName) + if fieldAttr.IsNull() { + log.Printf("[DEBUG] findRegionFieldInCtyValue: Field %s is null for region %s", fieldName, regionName) + return cty.NilVal, false + } + + // For Set/List fields, check if they have elements + // Empty sets mean the field was not explicitly set + if fieldAttr.Type().IsSetType() || fieldAttr.Type().IsListType() { + if fieldAttr.LengthInt() == 0 { + log.Printf("[DEBUG] findRegionFieldInCtyValue: Field %s is empty Set/List for region %s", fieldName, regionName) + return cty.NilVal, false + } + } + + log.Printf("[DEBUG] findRegionFieldInCtyValue: Found field %s for region %s", fieldName, regionName) + return fieldAttr, true + } + + log.Printf("[DEBUG] findRegionFieldInCtyValue: Region %s not found in override_region Set", regionName) + return cty.NilVal, false +} + +// isEnableDefaultUserExplicitlySetInConfig checks if enable_default_user is explicitly +// set in the user's HCL config for a given region using GetRawConfig. +// Returns true only if the field exists and is not null in the actual config. +func isEnableDefaultUserExplicitlySetInConfig(d *schema.ResourceData, regionName string) bool { + // Note: ctx is not available in this function, so we use log.Printf + // The calling functions already have tflog statements showing the result + rawConfig := d.GetRawConfig() + if rawConfig.IsNull() || !rawConfig.IsKnown() { + log.Printf("[DEBUG] isEnableDefaultUserExplicitlySetInConfig: GetRawConfig is null/unknown for region %s", regionName) + return false + } + + log.Printf("[DEBUG] isEnableDefaultUserExplicitlySetInConfig: Checking region %s in config", regionName) + + // Use the helper to navigate and find the field + fieldVal, found := findRegionFieldInCtyValue(rawConfig, regionName, "enable_default_user") + log.Printf("[DEBUG] isEnableDefaultUserExplicitlySetInConfig: Field found=%v for region %s, fieldVal.IsKnown=%v", + found, regionName, !fieldVal.IsNull() && fieldVal.IsKnown()) + + return found +} + +// isEnableDefaultUserInActualPersistedState checks if enable_default_user exists in the +// actual persisted state file (not the materialized state) for a given region using GetRawState. +// Returns true only if the field exists and is not null in the state file. +func isEnableDefaultUserInActualPersistedState(d *schema.ResourceData, regionName string) bool { + // Note: ctx is not available in this function, so we use log.Printf + // The calling functions already have tflog statements showing the result + rawState := d.GetRawState() + if rawState.IsNull() || !rawState.IsKnown() { + log.Printf("[DEBUG] isEnableDefaultUserInActualPersistedState: GetRawState is null/unknown for region %s", regionName) + return false + } + + log.Printf("[DEBUG] isEnableDefaultUserInActualPersistedState: Checking region %s in state", regionName) + + // Use the helper to navigate and find the field + fieldVal, found := findRegionFieldInCtyValue(rawState, regionName, "enable_default_user") + log.Printf("[DEBUG] isEnableDefaultUserInActualPersistedState: Field found=%v for region %s, fieldVal.IsKnown=%v", + found, regionName, !fieldVal.IsNull() && fieldVal.IsKnown()) + + return found +} diff --git a/provider/resource_rediscloud_active_active_subscription.go b/provider/resource_rediscloud_active_active_subscription.go index 57dd0c8b..e5ad0d75 100644 --- a/provider/resource_rediscloud_active_active_subscription.go +++ b/provider/resource_rediscloud_active_active_subscription.go @@ -297,27 +297,16 @@ 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.", + 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. When not specified, defaults to false.", Type: schema.TypeBool, Optional: true, - Default: false, + Computed: true, }, "customer_managed_key_deletion_grace_period": { - Description: "The grace period for deleting the subscription. If not set, will default to immediate deletion grace period.", + Description: "The grace period for deleting the subscription. When not specified, defaults to immediate deletion.", Type: schema.TypeString, Optional: true, - Default: "immediate", - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - // Only suppress diff when: - // 1. Old is empty (upgrading from provider version without this field) - // 2. New is the default value "immediate" - // 3. CMK is NOT being enabled (customer_managed_key_enabled is false) - if old == "" && new == "immediate" { - cmkEnabled := d.Get("customer_managed_key_enabled").(bool) - return !cmkEnabled - } - return false - }, + Computed: true, }, "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.", @@ -523,7 +512,13 @@ func resourceRedisCloudActiveActiveSubscriptionRead(ctx context.Context, d *sche } } - cmkEnabled := d.Get("customer_managed_key_enabled").(bool) + // Determine CMK status from API response + // CMK is enabled if persistentStorageEncryptionType is "customer-managed-key" + cmkEnabled := subscription.PersistentStorageEncryptionType != nil && + *subscription.PersistentStorageEncryptionType == pro.CMK_ENABLED_STRING + if err := d.Set("customer_managed_key_enabled", cmkEnabled); err != nil { + return diag.FromErr(err) + } if !cmkEnabled { m, err := api.Client.Maintenance.Get(ctx, subId) @@ -549,6 +544,13 @@ func resourceRedisCloudActiveActiveSubscriptionRead(ctx context.Context, d *sche } } + // Set customer_managed_key_deletion_grace_period from API response if available + if subscription.DeletionGracePeriod != nil { + if err := d.Set("customer_managed_key_deletion_grace_period", redis.StringValue(subscription.DeletionGracePeriod)); err != nil { + return diag.FromErr(err) + } + } + // Set public_endpoint_access, default to true if not set by API publicEndpointAccess := true if subscription.PublicEndpointAccess != nil { diff --git a/provider/sweeper_test.go b/provider/sweeper_test.go index 01c5f450..bac41907 100644 --- a/provider/sweeper_test.go +++ b/provider/sweeper_test.go @@ -152,12 +152,13 @@ func testSweepProSubscriptions(region string) error { func testSweepReadDatabases(client *rediscloudApi.Client, subId int) (bool, []int, error) { var dbIds []int list := client.Database.List(context.TODO(), subId) + forceSweep := os.Getenv("FORCE_SWEEP") != "" for list.Next() { db := list.Value() - if !redis.TimeValue(db.ActivatedOn).Add(24 * -1 * time.Hour).Before(time.Now()) { - // Subscription _probably_ created within the last day, so assume someone might be + if !forceSweep && !redis.TimeValue(db.ActivatedOn).Add(2 * -1 * time.Hour).Before(time.Now()) { + // Subscription _probably_ created within the last 2 hours, so assume someone might be // currently running the tests return false, nil, nil } @@ -185,12 +186,13 @@ func testSweepReadDatabases(client *rediscloudApi.Client, subId int) (bool, []in func testSweepReadEssentialsDatabases(client *rediscloudApi.Client, subId int) (bool, []int, error) { var dbIds []int list := client.FixedDatabases.List(context.TODO(), subId) + forceSweep := os.Getenv("FORCE_SWEEP") != "" for list.Next() { db := list.Value() - if !redis.TimeValue(db.ActivatedOn).Add(24 * -1 * time.Hour).Before(time.Now()) { - // Subscription _probably_ created within the last day, so assume someone might be + if !forceSweep && !redis.TimeValue(db.ActivatedOn).Add(2 * -1 * time.Hour).Before(time.Now()) { + // Subscription _probably_ created within the last 2 hours, so assume someone might be // currently running the tests return false, nil, nil } @@ -329,8 +331,8 @@ func testSweepAcl(region string) error { continue } - if client.Users.Delete(ctx, redis.IntValue(user.ID)) != nil { - return err + if delErr := client.Users.Delete(ctx, redis.IntValue(user.ID)); delErr != nil { + return delErr } } @@ -344,8 +346,8 @@ func testSweepAcl(region string) error { continue } - if client.Roles.Delete(ctx, redis.IntValue(role.ID)) != nil { - return err + if delErr := client.Roles.Delete(ctx, redis.IntValue(role.ID)); delErr != nil { + return delErr } }