From b04314cf4c5099957ba4266805cf5c032849c027 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Fri, 21 Mar 2025 08:52:12 +0000 Subject: [PATCH 01/39] test: Support testing PlanModifier --- .github/workflows/code-health.yml | 2 + Makefile | 4 +- .../resource_advanced_cluster_test.go | 9 +- ...dvancedCluster_removeBlocksFromConfig.yaml | 312 ++++++++++++++++++ ...Id}_clusters_{clusterName}_2024-10-23.json | 4 + ...d}_clusters_{clusterName}_2023-02-01.json} | 0 .../advancedclustertpf/plan_modifier_test.go | 58 ++++ .../advancedclustertpf/testdata/.gitignore | 1 + ...oRepSpecsWithAutoScalingAndSpecs.tmpl.yaml | 117 +++++++ ...Id}_clusters_{clusterName}_2023-02-01.json | 165 +++++++++ ...Id}_clusters_{clusterName}_2024-08-05.json | 170 ++++++++++ ..._{clusterName}_processArgs_2023-01-01.json | 19 ++ ..._{clusterName}_processArgs_2024-08-05.json | 17 + ...ontainers?providerName=AWS_2023-01-01.json | 27 ++ .../main.tf | 70 ++++ ..._blocks_from_config_and_instance_change.tf | 30 ++ ...oved_blocks_from_config_no_plan_changes.tf | 30 ++ internal/testutil/unit/http_mock_configs.go | 19 ++ internal/testutil/unit/http_mocker.go | 19 +- .../testutil/unit/http_mocker_api_paths.go | 10 +- .../unit/http_mocker_config_capture.go | 4 +- internal/testutil/unit/http_mocker_data.go | 26 +- .../testutil/unit/http_mocker_plan_checks.go | 186 +++++++++++ .../unit/http_mocker_plan_checks_test.go | 136 ++++++++ .../unit/http_mocker_round_tripper.go | 68 ++-- 25 files changed, 1443 insertions(+), 60 deletions(-) create mode 100644 internal/service/advancedcluster/testdata/TestAccMockableAdvancedCluster_removeBlocksFromConfig.yaml create mode 100644 internal/service/advancedcluster/testdata/TestAccMockableAdvancedCluster_replicasetAdvConfigUpdate/03_01_PATCH__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2024-10-23.json rename internal/service/advancedcluster/testdata/TestAccMockableAdvancedCluster_replicasetAdvConfigUpdate/{03_01_DELETE__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2023-02-01.json => 04_01_DELETE__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2023-02-01.json} (100%) create mode 100644 internal/service/advancedclustertpf/plan_modifier_test.go create mode 100644 internal/service/advancedclustertpf/testdata/.gitignore create mode 100644 internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs.tmpl.yaml create mode 100644 internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2023-02-01.json create mode 100644 internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2024-08-05.json create mode 100644 internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2023-01-01.json create mode 100644 internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2024-08-05.json create mode 100644 internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_containers?providerName=AWS_2023-01-01.json create mode 100644 internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/main.tf create mode 100644 internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/main_removed_blocks_from_config_and_instance_change.tf create mode 100644 internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/main_removed_blocks_from_config_no_plan_changes.tf create mode 100644 internal/testutil/unit/http_mock_configs.go create mode 100644 internal/testutil/unit/http_mocker_plan_checks.go create mode 100644 internal/testutil/unit/http_mocker_plan_checks_test.go diff --git a/.github/workflows/code-health.yml b/.github/workflows/code-health.yml index f352a5e039..b82a859213 100644 --- a/.github/workflows/code-health.yml +++ b/.github/workflows/code-health.yml @@ -35,6 +35,8 @@ jobs: go-version-file: 'go.mod' - name: Unit Test run: make test + env: + MONGODB_ATLAS_PREVIEW_PROVIDER_V2_ADVANCED_CLUSTER: "true" lint: runs-on: ubuntu-latest permissions: {} diff --git a/Makefile b/Makefile index 268395d034..a9f1caa37a 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ clean-atlas-org: ## Run a test to clean all projects and pending resources in an .PHONY: test test: fmtcheck ## Run unit tests - go test ./... -timeout=30s -parallel=4 -race + go test ./... -timeout=60s -parallel=16 -race .PHONY: testmact testmact: ## Run MacT tests (mocked acc tests) @@ -119,7 +119,7 @@ docs: ## Give URL to test Terraform documentation .PHONY: tflint tflint: fmtcheck ## Linter for Terraform files - tflint -f compact --recursive --minimum-failure-severity=warning + tflint --chdir=examples/ -f compact --recursive --minimum-failure-severity=warning .PHONY: tf-validate tf-validate: fmtcheck ## Validate Terraform files diff --git a/internal/service/advancedcluster/resource_advanced_cluster_test.go b/internal/service/advancedcluster/resource_advanced_cluster_test.go index 70879e31e7..eb99788785 100644 --- a/internal/service/advancedcluster/resource_advanced_cluster_test.go +++ b/internal/service/advancedcluster/resource_advanced_cluster_test.go @@ -61,16 +61,9 @@ const ( var ( configServerManagementModeFixedToDedicated = "FIXED_TO_DEDICATED" configServerManagementModeAtlasManaged = "ATLAS_MANAGED" - mockConfig = unit.MockHTTPDataConfig{AllowMissingRequests: true, SideEffect: shortenRetries, IsDiffMustSubstrings: []string{"/clusters"}, QueryVars: []string{"providerName"}} + mockConfig = unit.MockConfigAdvancedClusterTPF ) -func shortenRetries() error { - advancedclustertpf.RetryMinTimeout = 100 * time.Millisecond - advancedclustertpf.RetryDelay = 100 * time.Millisecond - advancedclustertpf.RetryPollInterval = 100 * time.Millisecond - return nil -} - func TestGetReplicationSpecAttributesFromOldAPI(t *testing.T) { var ( projectID = "11111" diff --git a/internal/service/advancedcluster/testdata/TestAccMockableAdvancedCluster_removeBlocksFromConfig.yaml b/internal/service/advancedcluster/testdata/TestAccMockableAdvancedCluster_removeBlocksFromConfig.yaml new file mode 100644 index 0000000000..a74dc6279e --- /dev/null +++ b/internal/service/advancedcluster/testdata/TestAccMockableAdvancedCluster_removeBlocksFromConfig.yaml @@ -0,0 +1,312 @@ +variables: + clusterName: test-acc-tf-c-7398840803408065070 + groupId: 67d01a24f610961835455eb1 +steps: + - config: |- + resource "mongodbatlas_advanced_cluster" "test" { + project_id = "67d01a24f610961835455eb1" + name = "test-acc-tf-c-7398840803408065070" + cluster_type = "GEOSHARDED" + + + replication_specs = [{ + region_configs = [{ + analytics_auto_scaling = { + compute_enabled = true + compute_max_instance_size = "M30" + compute_min_instance_size = "M10" + compute_scale_down_enabled = true + disk_gb_enabled = true + } + auto_scaling = { + compute_enabled = true + compute_max_instance_size = "M30" + compute_min_instance_size = "M10" + compute_scale_down_enabled = true + disk_gb_enabled = true + } + electable_specs = { + instance_size = "M10" + node_count = 5 + } + priority = 7 + provider_name = "AWS" + read_only_specs = { + instance_size = "M10" + node_count = 2 + } + region_name = "US_EAST_1" + }] + zone_name = "Zone 1" + }, { + region_configs = [{ + analytics_auto_scaling = { + compute_enabled = true + compute_max_instance_size = "M30" + compute_min_instance_size = "M10" + compute_scale_down_enabled = true + disk_gb_enabled = true + } + analytics_specs = { + instance_size = "M10" + node_count = 4 + } + auto_scaling = { + compute_enabled = true + compute_max_instance_size = "M30" + compute_min_instance_size = "M10" + compute_scale_down_enabled = true + disk_gb_enabled = true + } + electable_specs = { + instance_size = "M10" + node_count = 3 + } + priority = 7 + provider_name = "AWS" + read_only_specs = { + instance_size = "M10" + node_count = 1 + } + region_name = "US_WEST_2" + }] + zone_name = "Zone 2" + }] + } + diff_requests: + - path: /api/atlas/v2/groups/{groupId}/clusters + method: POST + version: '2024-10-23' + text: "{\n \"clusterType\": \"GEOSHARDED\",\n \"labels\": [],\n \"name\": \"{clusterName}\",\n \"replicationSpecs\": [\n {\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"instanceSize\": \"M10\",\n \"nodeCount\": 5\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"instanceSize\": \"M10\",\n \"nodeCount\": 2\n },\n \"regionName\": \"US_EAST_1\"\n }\n ],\n \"zoneName\": \"Zone 1\"\n },\n {\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"instanceSize\": \"M10\",\n \"nodeCount\": 4\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"instanceSize\": \"M10\",\n \"nodeCount\": 3\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"instanceSize\": \"M10\",\n \"nodeCount\": 1\n },\n \"regionName\": \"US_WEST_2\"\n }\n ],\n \"zoneName\": \"Zone 2\"\n }\n ],\n \"tags\": []\n}" + responses: + - response_index: 1 + status: 201 + text: "{\n \"advancedConfiguration\": {\n \"customOpensslCipherConfigTls12\": [],\n \"minimumEnabledTlsProtocol\": \"TLS1_2\",\n \"tlsCipherConfigMode\": \"DEFAULT\"\n },\n \"backupEnabled\": false,\n \"biConnector\": {\n \"enabled\": false,\n \"readPreference\": \"secondary\"\n },\n \"clusterType\": \"GEOSHARDED\",\n \"configServerManagementMode\": \"ATLAS_MANAGED\",\n \"configServerType\": \"DEDICATED\",\n \"connectionStrings\": {\n \"awsPrivateLinkSrv\": {},\n \"privateEndpoint\": []\n },\n \"createDate\": \"2025-03-11T11:10:37Z\",\n \"diskWarmingMode\": \"FULLY_WARMED\",\n \"encryptionAtRestProvider\": \"NONE\",\n \"featureCompatibilityVersion\": \"8.0\",\n \"globalClusterSelfManagedSharding\": false,\n \"groupId\": \"{groupId}\",\n \"id\": \"67d01a2d01d3561b07caf76e\",\n \"labels\": [],\n \"links\": [\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/restoreJobs\",\n \"rel\": \"https://cloud.mongodb.com/restoreJobs\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/snapshots\",\n \"rel\": \"https://cloud.mongodb.com/snapshots\"\n }\n ],\n \"mongoDBMajorVersion\": \"8.0\",\n \"mongoDBVersion\": \"8.0.5\",\n \"name\": \"{clusterName}\",\n \"paused\": false,\n \"pitEnabled\": false,\n \"redactClientLogData\": false,\n \"replicationSpecs\": [\n {\n \"id\": \"67d01a2c01d3561b07caf756\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 0\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 5\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 2\n },\n \"regionName\": \"US_EAST_1\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf753\",\n \"zoneName\": \"Zone 1\"\n },\n {\n \"id\": \"67d01a2c01d3561b07caf758\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 4\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 3\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 1\n },\n \"regionName\": \"US_WEST_2\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf754\",\n \"zoneName\": \"Zone 2\"\n }\n ],\n \"rootCertType\": \"ISRGROOTX1\",\n \"stateName\": \"CREATING\",\n \"tags\": [],\n \"terminationProtectionEnabled\": false,\n \"versionReleaseSystem\": \"LTS\"\n}" + request_responses: + - path: /api/atlas/v2/groups/{groupId}/clusters + method: POST + version: '2024-10-23' + text: "{\n \"clusterType\": \"GEOSHARDED\",\n \"labels\": [],\n \"name\": \"{clusterName}\",\n \"replicationSpecs\": [\n {\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"instanceSize\": \"M10\",\n \"nodeCount\": 5\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"instanceSize\": \"M10\",\n \"nodeCount\": 2\n },\n \"regionName\": \"US_EAST_1\"\n }\n ],\n \"zoneName\": \"Zone 1\"\n },\n {\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"instanceSize\": \"M10\",\n \"nodeCount\": 4\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"instanceSize\": \"M10\",\n \"nodeCount\": 3\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"instanceSize\": \"M10\",\n \"nodeCount\": 1\n },\n \"regionName\": \"US_WEST_2\"\n }\n ],\n \"zoneName\": \"Zone 2\"\n }\n ],\n \"tags\": []\n}" + responses: + - response_index: 1 + status: 201 + text: "{\n \"advancedConfiguration\": {\n \"customOpensslCipherConfigTls12\": [],\n \"minimumEnabledTlsProtocol\": \"TLS1_2\",\n \"tlsCipherConfigMode\": \"DEFAULT\"\n },\n \"backupEnabled\": false,\n \"biConnector\": {\n \"enabled\": false,\n \"readPreference\": \"secondary\"\n },\n \"clusterType\": \"GEOSHARDED\",\n \"configServerManagementMode\": \"ATLAS_MANAGED\",\n \"configServerType\": \"DEDICATED\",\n \"connectionStrings\": {\n \"awsPrivateLinkSrv\": {},\n \"privateEndpoint\": []\n },\n \"createDate\": \"2025-03-11T11:10:37Z\",\n \"diskWarmingMode\": \"FULLY_WARMED\",\n \"encryptionAtRestProvider\": \"NONE\",\n \"featureCompatibilityVersion\": \"8.0\",\n \"globalClusterSelfManagedSharding\": false,\n \"groupId\": \"{groupId}\",\n \"id\": \"67d01a2d01d3561b07caf76e\",\n \"labels\": [],\n \"links\": [\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/restoreJobs\",\n \"rel\": \"https://cloud.mongodb.com/restoreJobs\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/snapshots\",\n \"rel\": \"https://cloud.mongodb.com/snapshots\"\n }\n ],\n \"mongoDBMajorVersion\": \"8.0\",\n \"mongoDBVersion\": \"8.0.5\",\n \"name\": \"{clusterName}\",\n \"paused\": false,\n \"pitEnabled\": false,\n \"redactClientLogData\": false,\n \"replicationSpecs\": [\n {\n \"id\": \"67d01a2c01d3561b07caf756\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 0\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 5\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 2\n },\n \"regionName\": \"US_EAST_1\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf753\",\n \"zoneName\": \"Zone 1\"\n },\n {\n \"id\": \"67d01a2c01d3561b07caf758\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 4\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 3\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 1\n },\n \"regionName\": \"US_WEST_2\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf754\",\n \"zoneName\": \"Zone 2\"\n }\n ],\n \"rootCertType\": \"ISRGROOTX1\",\n \"stateName\": \"CREATING\",\n \"tags\": [],\n \"terminationProtectionEnabled\": false,\n \"versionReleaseSystem\": \"LTS\"\n}" + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName} + method: GET + version: '2024-08-05' + text: "" + responses: + - response_index: 2 + status: 200 + duplicate_responses: 24 + text: "{\n \"advancedConfiguration\": {\n \"customOpensslCipherConfigTls12\": [],\n \"minimumEnabledTlsProtocol\": \"TLS1_2\",\n \"tlsCipherConfigMode\": \"DEFAULT\"\n },\n \"backupEnabled\": false,\n \"biConnector\": {\n \"enabled\": false,\n \"readPreference\": \"secondary\"\n },\n \"clusterType\": \"GEOSHARDED\",\n \"configServerManagementMode\": \"ATLAS_MANAGED\",\n \"configServerType\": \"DEDICATED\",\n \"connectionStrings\": {\n \"awsPrivateLinkSrv\": {},\n \"privateEndpoint\": []\n },\n \"createDate\": \"2025-03-11T11:10:37Z\",\n \"diskWarmingMode\": \"FULLY_WARMED\",\n \"encryptionAtRestProvider\": \"NONE\",\n \"featureCompatibilityVersion\": \"8.0\",\n \"globalClusterSelfManagedSharding\": false,\n \"groupId\": \"{groupId}\",\n \"id\": \"67d01a2d01d3561b07caf76e\",\n \"labels\": [],\n \"links\": [\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/restoreJobs\",\n \"rel\": \"https://cloud.mongodb.com/restoreJobs\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/snapshots\",\n \"rel\": \"https://cloud.mongodb.com/snapshots\"\n }\n ],\n \"mongoDBMajorVersion\": \"8.0\",\n \"mongoDBVersion\": \"8.0.5\",\n \"name\": \"{clusterName}\",\n \"paused\": false,\n \"pitEnabled\": false,\n \"redactClientLogData\": false,\n \"replicationSpecs\": [\n {\n \"id\": \"67d01a2c01d3561b07caf756\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 0\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 5\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 2\n },\n \"regionName\": \"US_EAST_1\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf753\",\n \"zoneName\": \"Zone 1\"\n },\n {\n \"id\": \"67d01a2c01d3561b07caf758\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 4\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 3\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 1\n },\n \"regionName\": \"US_WEST_2\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf754\",\n \"zoneName\": \"Zone 2\"\n }\n ],\n \"rootCertType\": \"ISRGROOTX1\",\n \"stateName\": \"CREATING\",\n \"tags\": [],\n \"terminationProtectionEnabled\": false,\n \"versionReleaseSystem\": \"LTS\"\n}" + - response_index: 27 + status: 200 + duplicate_responses: 1 + text: "{\n \"advancedConfiguration\": {\n \"customOpensslCipherConfigTls12\": [],\n \"minimumEnabledTlsProtocol\": \"TLS1_2\",\n \"tlsCipherConfigMode\": \"DEFAULT\"\n },\n \"backupEnabled\": false,\n \"biConnector\": {\n \"enabled\": false,\n \"readPreference\": \"secondary\"\n },\n \"clusterType\": \"GEOSHARDED\",\n \"configServerManagementMode\": \"ATLAS_MANAGED\",\n \"configServerType\": \"DEDICATED\",\n \"connectionStrings\": {\n \"awsPrivateLinkSrv\": {},\n \"privateEndpoint\": [],\n \"standard\": \"mongodb://test-acc-tf-c-739884080-shard-00-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-07.gwbdm.mongodb-dev.net:27016/?ssl=true\\u0026authSource=admin\",\n \"standardSrv\": \"mongodb+srv://test-acc-tf-c-739884080.gwbdm.mongodb-dev.net\"\n },\n \"createDate\": \"2025-03-11T11:10:37Z\",\n \"diskWarmingMode\": \"FULLY_WARMED\",\n \"encryptionAtRestProvider\": \"NONE\",\n \"featureCompatibilityVersion\": \"8.0\",\n \"globalClusterSelfManagedSharding\": false,\n \"groupId\": \"{groupId}\",\n \"id\": \"67d01a2d01d3561b07caf76e\",\n \"labels\": [],\n \"links\": [\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/restoreJobs\",\n \"rel\": \"https://cloud.mongodb.com/restoreJobs\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/snapshots\",\n \"rel\": \"https://cloud.mongodb.com/snapshots\"\n }\n ],\n \"mongoDBMajorVersion\": \"8.0\",\n \"mongoDBVersion\": \"8.0.5\",\n \"name\": \"{clusterName}\",\n \"paused\": false,\n \"pitEnabled\": false,\n \"redactClientLogData\": false,\n \"replicationSpecs\": [\n {\n \"id\": \"67d01a2c01d3561b07caf756\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 0\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 5\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 2\n },\n \"regionName\": \"US_EAST_1\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf753\",\n \"zoneName\": \"Zone 1\"\n },\n {\n \"id\": \"67d01a2c01d3561b07caf758\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 4\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 3\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 1\n },\n \"regionName\": \"US_WEST_2\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf754\",\n \"zoneName\": \"Zone 2\"\n }\n ],\n \"rootCertType\": \"ISRGROOTX1\",\n \"stateName\": \"IDLE\",\n \"tags\": [],\n \"terminationProtectionEnabled\": false,\n \"versionReleaseSystem\": \"LTS\"\n}" + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName} + method: GET + version: '2023-02-01' + text: "" + responses: + - response_index: 28 + status: 200 + duplicate_responses: 1 + text: "{\n \"advancedConfiguration\": {\n \"customOpensslCipherConfigTls12\": [],\n \"minimumEnabledTlsProtocol\": \"TLS1_2\",\n \"tlsCipherConfigMode\": \"DEFAULT\"\n },\n \"backupEnabled\": false,\n \"biConnector\": {\n \"enabled\": false,\n \"readPreference\": \"secondary\"\n },\n \"clusterType\": \"GEOSHARDED\",\n \"configServerManagementMode\": \"ATLAS_MANAGED\",\n \"configServerType\": \"DEDICATED\",\n \"connectionStrings\": {\n \"awsPrivateLinkSrv\": {},\n \"privateEndpoint\": [],\n \"standard\": \"mongodb://test-acc-tf-c-739884080-shard-00-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-07.gwbdm.mongodb-dev.net:27016/?ssl=true\\u0026authSource=admin\",\n \"standardSrv\": \"mongodb+srv://test-acc-tf-c-739884080.gwbdm.mongodb-dev.net\"\n },\n \"createDate\": \"2025-03-11T11:10:37Z\",\n \"diskSizeGB\": 10,\n \"diskWarmingMode\": \"FULLY_WARMED\",\n \"encryptionAtRestProvider\": \"NONE\",\n \"globalClusterSelfManagedSharding\": false,\n \"groupId\": \"{groupId}\",\n \"id\": \"67d01a2d01d3561b07caf76e\",\n \"labels\": [],\n \"links\": [\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/restoreJobs\",\n \"rel\": \"https://cloud.mongodb.com/restoreJobs\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/snapshots\",\n \"rel\": \"https://cloud.mongodb.com/snapshots\"\n }\n ],\n \"mongoDBMajorVersion\": \"8.0\",\n \"mongoDBVersion\": \"8.0.5\",\n \"name\": \"{clusterName}\",\n \"paused\": false,\n \"pitEnabled\": false,\n \"replicationSpecs\": [\n {\n \"id\": \"67d01a2c01d3561b07caf755\",\n \"numShards\": 1,\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 0\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 5\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 2\n },\n \"regionName\": \"US_EAST_1\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf753\",\n \"zoneName\": \"Zone 1\"\n },\n {\n \"id\": \"67d01a2c01d3561b07caf757\",\n \"numShards\": 1,\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 4\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 3\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 1\n },\n \"regionName\": \"US_WEST_2\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf754\",\n \"zoneName\": \"Zone 2\"\n }\n ],\n \"rootCertType\": \"ISRGROOTX1\",\n \"stateName\": \"IDLE\",\n \"tags\": [],\n \"terminationProtectionEnabled\": false,\n \"versionReleaseSystem\": \"LTS\"\n}" + - path: /api/atlas/v2/groups/{groupId}/containers?providerName=AWS + method: GET + version: '2023-01-01' + text: "" + responses: + - response_index: 29 + status: 200 + duplicate_responses: 1 + text: "{\n \"links\": [\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/containers?includeCount=true\\u0026providerName=AWS\\u0026pageNum=1\\u0026itemsPerPage=100\",\n \"rel\": \"self\"\n }\n ],\n \"results\": [\n {\n \"atlasCidrBlock\": \"192.168.248.0/21\",\n \"id\": \"67d01a2d01d3561b07caf76c\",\n \"providerName\": \"AWS\",\n \"provisioned\": true,\n \"regionName\": \"US_EAST_1\",\n \"vpcId\": \"vpc-0e0706bcd69b8855a\"\n },\n {\n \"atlasCidrBlock\": \"192.168.240.0/21\",\n \"id\": \"67d01a2d01d3561b07caf76d\",\n \"providerName\": \"AWS\",\n \"provisioned\": true,\n \"regionName\": \"US_WEST_2\",\n \"vpcId\": \"vpc-04a84758be9599707\"\n }\n ],\n \"totalCount\": 2\n}" + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName}/processArgs + method: GET + version: '2023-01-01' + text: "" + responses: + - response_index: 30 + status: 200 + duplicate_responses: 1 + text: "{\n \"changeStreamOptionsPreAndPostImagesExpireAfterSeconds\": null,\n \"chunkMigrationConcurrency\": null,\n \"customOpensslCipherConfigTls12\": [],\n \"defaultMaxTimeMS\": null,\n \"defaultReadConcern\": null,\n \"defaultWriteConcern\": null,\n \"failIndexKeyTooLong\": null,\n \"javascriptEnabled\": true,\n \"minimumEnabledTlsProtocol\": \"TLS1_2\",\n \"noTableScan\": false,\n \"oplogMinRetentionHours\": null,\n \"oplogSizeMB\": null,\n \"queryStatsLogVerbosity\": 1,\n \"sampleRefreshIntervalBIConnector\": null,\n \"sampleSizeBIConnector\": null,\n \"tlsCipherConfigMode\": \"DEFAULT\",\n \"transactionLifetimeLimitSeconds\": null\n}" + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName}/processArgs + method: GET + version: '2024-08-05' + text: "" + responses: + - response_index: 31 + status: 200 + duplicate_responses: 1 + text: "{\n \"changeStreamOptionsPreAndPostImagesExpireAfterSeconds\": null,\n \"chunkMigrationConcurrency\": null,\n \"customOpensslCipherConfigTls12\": [],\n \"defaultMaxTimeMS\": null,\n \"defaultWriteConcern\": null,\n \"javascriptEnabled\": true,\n \"minimumEnabledTlsProtocol\": \"TLS1_2\",\n \"noTableScan\": false,\n \"oplogMinRetentionHours\": null,\n \"oplogSizeMB\": null,\n \"queryStatsLogVerbosity\": 1,\n \"sampleRefreshIntervalBIConnector\": null,\n \"sampleSizeBIConnector\": null,\n \"tlsCipherConfigMode\": \"DEFAULT\",\n \"transactionLifetimeLimitSeconds\": null\n}" + - config: |- + resource "mongodbatlas_advanced_cluster" "test" { + project_id = "67d01a24f610961835455eb1" + name = "test-acc-tf-c-7398840803408065070" + cluster_type = "GEOSHARDED" + + + replication_specs = [{ + region_configs = [{ + electable_specs = { + instance_size = "M10" + node_count = 5 + } + priority = 7 + provider_name = "AWS" + region_name = "US_EAST_1" + }] + zone_name = "Zone 1" + }, { + region_configs = [{ + electable_specs = { + instance_size = "M20" + node_count = 3 + } + priority = 7 + provider_name = "AWS" + region_name = "US_WEST_2" + }] + zone_name = "Zone 2" + }] + } + diff_requests: + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName} + method: PATCH + version: '2024-10-23' + text: "{\n \"replicationSpecs\": [\n {\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskSizeGB\": 10,\n \"instanceSize\": \"M10\",\n \"nodeCount\": 5\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskSizeGB\": 10,\n \"instanceSize\": \"M10\",\n \"nodeCount\": 2\n },\n \"regionName\": \"US_EAST_1\"\n }\n ],\n \"zoneName\": \"Zone 1\"\n },\n {\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 4\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskSizeGB\": 10,\n \"instanceSize\": \"M20\",\n \"nodeCount\": 3\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskSizeGB\": 10,\n \"instanceSize\": \"M20\",\n \"nodeCount\": 1\n },\n \"regionName\": \"US_WEST_2\"\n }\n ],\n \"zoneName\": \"Zone 2\"\n }\n ]\n}" + responses: + - response_index: 42 + status: 200 + text: "{\n \"advancedConfiguration\": {\n \"customOpensslCipherConfigTls12\": [],\n \"minimumEnabledTlsProtocol\": \"TLS1_2\",\n \"tlsCipherConfigMode\": \"DEFAULT\"\n },\n \"backupEnabled\": false,\n \"biConnector\": {\n \"enabled\": false,\n \"readPreference\": \"secondary\"\n },\n \"clusterType\": \"GEOSHARDED\",\n \"configServerManagementMode\": \"ATLAS_MANAGED\",\n \"configServerType\": \"DEDICATED\",\n \"connectionStrings\": {\n \"awsPrivateLinkSrv\": {},\n \"privateEndpoint\": [],\n \"standard\": \"mongodb://test-acc-tf-c-739884080-shard-00-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-07.gwbdm.mongodb-dev.net:27016/?ssl=true\\u0026authSource=admin\",\n \"standardSrv\": \"mongodb+srv://test-acc-tf-c-739884080.gwbdm.mongodb-dev.net\"\n },\n \"createDate\": \"2025-03-11T11:10:37Z\",\n \"diskWarmingMode\": \"FULLY_WARMED\",\n \"encryptionAtRestProvider\": \"NONE\",\n \"featureCompatibilityVersion\": \"8.0\",\n \"globalClusterSelfManagedSharding\": false,\n \"groupId\": \"{groupId}\",\n \"id\": \"67d01a2d01d3561b07caf76e\",\n \"labels\": [],\n \"links\": [\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/restoreJobs\",\n \"rel\": \"https://cloud.mongodb.com/restoreJobs\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/snapshots\",\n \"rel\": \"https://cloud.mongodb.com/snapshots\"\n }\n ],\n \"mongoDBMajorVersion\": \"8.0\",\n \"mongoDBVersion\": \"8.0.5\",\n \"name\": \"{clusterName}\",\n \"paused\": false,\n \"pitEnabled\": false,\n \"redactClientLogData\": false,\n \"replicationSpecs\": [\n {\n \"id\": \"67d01a2c01d3561b07caf756\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 0\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 5\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 2\n },\n \"regionName\": \"US_EAST_1\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf753\",\n \"zoneName\": \"Zone 1\"\n },\n {\n \"id\": \"67d01a2c01d3561b07caf758\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 4\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M20\",\n \"nodeCount\": 3\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M20\",\n \"nodeCount\": 1\n },\n \"regionName\": \"US_WEST_2\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf754\",\n \"zoneName\": \"Zone 2\"\n }\n ],\n \"rootCertType\": \"ISRGROOTX1\",\n \"stateName\": \"UPDATING\",\n \"tags\": [],\n \"terminationProtectionEnabled\": false,\n \"versionReleaseSystem\": \"LTS\"\n}" + request_responses: + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName} + method: GET + version: '2024-08-05' + text: "" + responses: + - response_index: 37 + status: 200 + text: "{\n \"advancedConfiguration\": {\n \"customOpensslCipherConfigTls12\": [],\n \"minimumEnabledTlsProtocol\": \"TLS1_2\",\n \"tlsCipherConfigMode\": \"DEFAULT\"\n },\n \"backupEnabled\": false,\n \"biConnector\": {\n \"enabled\": false,\n \"readPreference\": \"secondary\"\n },\n \"clusterType\": \"GEOSHARDED\",\n \"configServerManagementMode\": \"ATLAS_MANAGED\",\n \"configServerType\": \"DEDICATED\",\n \"connectionStrings\": {\n \"awsPrivateLinkSrv\": {},\n \"privateEndpoint\": [],\n \"standard\": \"mongodb://test-acc-tf-c-739884080-shard-00-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-07.gwbdm.mongodb-dev.net:27016/?ssl=true\\u0026authSource=admin\",\n \"standardSrv\": \"mongodb+srv://test-acc-tf-c-739884080.gwbdm.mongodb-dev.net\"\n },\n \"createDate\": \"2025-03-11T11:10:37Z\",\n \"diskWarmingMode\": \"FULLY_WARMED\",\n \"encryptionAtRestProvider\": \"NONE\",\n \"featureCompatibilityVersion\": \"8.0\",\n \"globalClusterSelfManagedSharding\": false,\n \"groupId\": \"{groupId}\",\n \"id\": \"67d01a2d01d3561b07caf76e\",\n \"labels\": [],\n \"links\": [\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/restoreJobs\",\n \"rel\": \"https://cloud.mongodb.com/restoreJobs\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/snapshots\",\n \"rel\": \"https://cloud.mongodb.com/snapshots\"\n }\n ],\n \"mongoDBMajorVersion\": \"8.0\",\n \"mongoDBVersion\": \"8.0.5\",\n \"name\": \"{clusterName}\",\n \"paused\": false,\n \"pitEnabled\": false,\n \"redactClientLogData\": false,\n \"replicationSpecs\": [\n {\n \"id\": \"67d01a2c01d3561b07caf756\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 0\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 5\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 2\n },\n \"regionName\": \"US_EAST_1\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf753\",\n \"zoneName\": \"Zone 1\"\n },\n {\n \"id\": \"67d01a2c01d3561b07caf758\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 4\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 3\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 1\n },\n \"regionName\": \"US_WEST_2\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf754\",\n \"zoneName\": \"Zone 2\"\n }\n ],\n \"rootCertType\": \"ISRGROOTX1\",\n \"stateName\": \"IDLE\",\n \"tags\": [],\n \"terminationProtectionEnabled\": false,\n \"versionReleaseSystem\": \"LTS\"\n}" + - response_index: 43 + status: 200 + duplicate_responses: 37 + text: "{\n \"advancedConfiguration\": {\n \"customOpensslCipherConfigTls12\": [],\n \"minimumEnabledTlsProtocol\": \"TLS1_2\",\n \"tlsCipherConfigMode\": \"DEFAULT\"\n },\n \"backupEnabled\": false,\n \"biConnector\": {\n \"enabled\": false,\n \"readPreference\": \"secondary\"\n },\n \"clusterType\": \"GEOSHARDED\",\n \"configServerManagementMode\": \"ATLAS_MANAGED\",\n \"configServerType\": \"DEDICATED\",\n \"connectionStrings\": {\n \"awsPrivateLinkSrv\": {},\n \"privateEndpoint\": [],\n \"standard\": \"mongodb://test-acc-tf-c-739884080-shard-00-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-07.gwbdm.mongodb-dev.net:27016/?ssl=true\\u0026authSource=admin\",\n \"standardSrv\": \"mongodb+srv://test-acc-tf-c-739884080.gwbdm.mongodb-dev.net\"\n },\n \"createDate\": \"2025-03-11T11:10:37Z\",\n \"diskWarmingMode\": \"FULLY_WARMED\",\n \"encryptionAtRestProvider\": \"NONE\",\n \"featureCompatibilityVersion\": \"8.0\",\n \"globalClusterSelfManagedSharding\": false,\n \"groupId\": \"{groupId}\",\n \"id\": \"67d01a2d01d3561b07caf76e\",\n \"labels\": [],\n \"links\": [\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/restoreJobs\",\n \"rel\": \"https://cloud.mongodb.com/restoreJobs\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/snapshots\",\n \"rel\": \"https://cloud.mongodb.com/snapshots\"\n }\n ],\n \"mongoDBMajorVersion\": \"8.0\",\n \"mongoDBVersion\": \"8.0.5\",\n \"name\": \"{clusterName}\",\n \"paused\": false,\n \"pitEnabled\": false,\n \"redactClientLogData\": false,\n \"replicationSpecs\": [\n {\n \"id\": \"67d01a2c01d3561b07caf756\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 0\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 5\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 2\n },\n \"regionName\": \"US_EAST_1\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf753\",\n \"zoneName\": \"Zone 1\"\n },\n {\n \"id\": \"67d01a2c01d3561b07caf758\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 4\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M20\",\n \"nodeCount\": 3\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M20\",\n \"nodeCount\": 1\n },\n \"regionName\": \"US_WEST_2\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf754\",\n \"zoneName\": \"Zone 2\"\n }\n ],\n \"rootCertType\": \"ISRGROOTX1\",\n \"stateName\": \"UPDATING\",\n \"tags\": [],\n \"terminationProtectionEnabled\": false,\n \"versionReleaseSystem\": \"LTS\"\n}" + - response_index: 81 + status: 200 + duplicate_responses: 1 + text: "{\n \"advancedConfiguration\": {\n \"customOpensslCipherConfigTls12\": [],\n \"minimumEnabledTlsProtocol\": \"TLS1_2\",\n \"tlsCipherConfigMode\": \"DEFAULT\"\n },\n \"backupEnabled\": false,\n \"biConnector\": {\n \"enabled\": false,\n \"readPreference\": \"secondary\"\n },\n \"clusterType\": \"GEOSHARDED\",\n \"configServerManagementMode\": \"ATLAS_MANAGED\",\n \"configServerType\": \"DEDICATED\",\n \"connectionStrings\": {\n \"awsPrivateLinkSrv\": {},\n \"privateEndpoint\": [],\n \"standard\": \"mongodb://test-acc-tf-c-739884080-shard-00-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-07.gwbdm.mongodb-dev.net:27016/?ssl=true\\u0026authSource=admin\",\n \"standardSrv\": \"mongodb+srv://test-acc-tf-c-739884080.gwbdm.mongodb-dev.net\"\n },\n \"createDate\": \"2025-03-11T11:10:37Z\",\n \"diskWarmingMode\": \"FULLY_WARMED\",\n \"encryptionAtRestProvider\": \"NONE\",\n \"featureCompatibilityVersion\": \"8.0\",\n \"globalClusterSelfManagedSharding\": false,\n \"groupId\": \"{groupId}\",\n \"id\": \"67d01a2d01d3561b07caf76e\",\n \"labels\": [],\n \"links\": [\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/restoreJobs\",\n \"rel\": \"https://cloud.mongodb.com/restoreJobs\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/snapshots\",\n \"rel\": \"https://cloud.mongodb.com/snapshots\"\n }\n ],\n \"mongoDBMajorVersion\": \"8.0\",\n \"mongoDBVersion\": \"8.0.5\",\n \"name\": \"{clusterName}\",\n \"paused\": false,\n \"pitEnabled\": false,\n \"redactClientLogData\": false,\n \"replicationSpecs\": [\n {\n \"id\": \"67d01a2c01d3561b07caf756\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 0\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 5\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 2\n },\n \"regionName\": \"US_EAST_1\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf753\",\n \"zoneName\": \"Zone 1\"\n },\n {\n \"id\": \"67d01a2c01d3561b07caf758\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 4\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M20\",\n \"nodeCount\": 3\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M20\",\n \"nodeCount\": 1\n },\n \"regionName\": \"US_WEST_2\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf754\",\n \"zoneName\": \"Zone 2\"\n }\n ],\n \"rootCertType\": \"ISRGROOTX1\",\n \"stateName\": \"IDLE\",\n \"tags\": [],\n \"terminationProtectionEnabled\": false,\n \"versionReleaseSystem\": \"LTS\"\n}" + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName} + method: GET + version: '2023-02-01' + text: "" + responses: + - response_index: 38 + status: 200 + text: "{\n \"advancedConfiguration\": {\n \"customOpensslCipherConfigTls12\": [],\n \"minimumEnabledTlsProtocol\": \"TLS1_2\",\n \"tlsCipherConfigMode\": \"DEFAULT\"\n },\n \"backupEnabled\": false,\n \"biConnector\": {\n \"enabled\": false,\n \"readPreference\": \"secondary\"\n },\n \"clusterType\": \"GEOSHARDED\",\n \"configServerManagementMode\": \"ATLAS_MANAGED\",\n \"configServerType\": \"DEDICATED\",\n \"connectionStrings\": {\n \"awsPrivateLinkSrv\": {},\n \"privateEndpoint\": [],\n \"standard\": \"mongodb://test-acc-tf-c-739884080-shard-00-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-07.gwbdm.mongodb-dev.net:27016/?ssl=true\\u0026authSource=admin\",\n \"standardSrv\": \"mongodb+srv://test-acc-tf-c-739884080.gwbdm.mongodb-dev.net\"\n },\n \"createDate\": \"2025-03-11T11:10:37Z\",\n \"diskSizeGB\": 10,\n \"diskWarmingMode\": \"FULLY_WARMED\",\n \"encryptionAtRestProvider\": \"NONE\",\n \"globalClusterSelfManagedSharding\": false,\n \"groupId\": \"{groupId}\",\n \"id\": \"67d01a2d01d3561b07caf76e\",\n \"labels\": [],\n \"links\": [\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/restoreJobs\",\n \"rel\": \"https://cloud.mongodb.com/restoreJobs\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/snapshots\",\n \"rel\": \"https://cloud.mongodb.com/snapshots\"\n }\n ],\n \"mongoDBMajorVersion\": \"8.0\",\n \"mongoDBVersion\": \"8.0.5\",\n \"name\": \"{clusterName}\",\n \"paused\": false,\n \"pitEnabled\": false,\n \"replicationSpecs\": [\n {\n \"id\": \"67d01a2c01d3561b07caf755\",\n \"numShards\": 1,\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 0\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 5\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 2\n },\n \"regionName\": \"US_EAST_1\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf753\",\n \"zoneName\": \"Zone 1\"\n },\n {\n \"id\": \"67d01a2c01d3561b07caf757\",\n \"numShards\": 1,\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 4\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 3\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 1\n },\n \"regionName\": \"US_WEST_2\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf754\",\n \"zoneName\": \"Zone 2\"\n }\n ],\n \"rootCertType\": \"ISRGROOTX1\",\n \"stateName\": \"IDLE\",\n \"tags\": [],\n \"terminationProtectionEnabled\": false,\n \"versionReleaseSystem\": \"LTS\"\n}" + - response_index: 82 + status: 400 + duplicate_responses: 1 + text: "{\n \"detail\": \"Asymmetric sharded cluster is not supported by the current API version. Please use the latest API instead. Documentation for the latest API is available at https://docs.atlas.mongodb.com/reference/api/clusters-advanced/.\",\n \"error\": 400,\n \"errorCode\": \"ASYMMETRIC_SHARD_UNSUPPORTED\",\n \"parameters\": [],\n \"reason\": \"Bad Request\"\n}" + - path: /api/atlas/v2/groups/{groupId}/containers?providerName=AWS + method: GET + version: '2023-01-01' + text: "" + responses: + - response_index: 39 + status: 200 + duplicate_responses: 2 + text: "{\n \"links\": [\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/containers?includeCount=true\\u0026providerName=AWS\\u0026pageNum=1\\u0026itemsPerPage=100\",\n \"rel\": \"self\"\n }\n ],\n \"results\": [\n {\n \"atlasCidrBlock\": \"192.168.248.0/21\",\n \"id\": \"67d01a2d01d3561b07caf76c\",\n \"providerName\": \"AWS\",\n \"provisioned\": true,\n \"regionName\": \"US_EAST_1\",\n \"vpcId\": \"vpc-0e0706bcd69b8855a\"\n },\n {\n \"atlasCidrBlock\": \"192.168.240.0/21\",\n \"id\": \"67d01a2d01d3561b07caf76d\",\n \"providerName\": \"AWS\",\n \"provisioned\": true,\n \"regionName\": \"US_WEST_2\",\n \"vpcId\": \"vpc-04a84758be9599707\"\n }\n ],\n \"totalCount\": 2\n}" + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName}/processArgs + method: GET + version: '2023-01-01' + text: "" + responses: + - response_index: 40 + status: 200 + duplicate_responses: 1 + text: "{\n \"changeStreamOptionsPreAndPostImagesExpireAfterSeconds\": null,\n \"chunkMigrationConcurrency\": null,\n \"customOpensslCipherConfigTls12\": [],\n \"defaultMaxTimeMS\": null,\n \"defaultReadConcern\": null,\n \"defaultWriteConcern\": null,\n \"failIndexKeyTooLong\": null,\n \"javascriptEnabled\": true,\n \"minimumEnabledTlsProtocol\": \"TLS1_2\",\n \"noTableScan\": false,\n \"oplogMinRetentionHours\": null,\n \"oplogSizeMB\": null,\n \"queryStatsLogVerbosity\": 1,\n \"sampleRefreshIntervalBIConnector\": null,\n \"sampleSizeBIConnector\": null,\n \"tlsCipherConfigMode\": \"DEFAULT\",\n \"transactionLifetimeLimitSeconds\": null\n}" + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName}/processArgs + method: GET + version: '2024-08-05' + text: "" + responses: + - response_index: 41 + status: 200 + duplicate_responses: 1 + text: "{\n \"changeStreamOptionsPreAndPostImagesExpireAfterSeconds\": null,\n \"chunkMigrationConcurrency\": null,\n \"customOpensslCipherConfigTls12\": [],\n \"defaultMaxTimeMS\": null,\n \"defaultWriteConcern\": null,\n \"javascriptEnabled\": true,\n \"minimumEnabledTlsProtocol\": \"TLS1_2\",\n \"noTableScan\": false,\n \"oplogMinRetentionHours\": null,\n \"oplogSizeMB\": null,\n \"queryStatsLogVerbosity\": 1,\n \"sampleRefreshIntervalBIConnector\": null,\n \"sampleSizeBIConnector\": null,\n \"tlsCipherConfigMode\": \"DEFAULT\",\n \"transactionLifetimeLimitSeconds\": null\n}" + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName} + method: PATCH + version: '2024-10-23' + text: "{\n \"replicationSpecs\": [\n {\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskSizeGB\": 10,\n \"instanceSize\": \"M10\",\n \"nodeCount\": 5\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskSizeGB\": 10,\n \"instanceSize\": \"M10\",\n \"nodeCount\": 2\n },\n \"regionName\": \"US_EAST_1\"\n }\n ],\n \"zoneName\": \"Zone 1\"\n },\n {\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 4\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskSizeGB\": 10,\n \"instanceSize\": \"M20\",\n \"nodeCount\": 3\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskSizeGB\": 10,\n \"instanceSize\": \"M20\",\n \"nodeCount\": 1\n },\n \"regionName\": \"US_WEST_2\"\n }\n ],\n \"zoneName\": \"Zone 2\"\n }\n ]\n}" + responses: + - response_index: 42 + status: 200 + text: "{\n \"advancedConfiguration\": {\n \"customOpensslCipherConfigTls12\": [],\n \"minimumEnabledTlsProtocol\": \"TLS1_2\",\n \"tlsCipherConfigMode\": \"DEFAULT\"\n },\n \"backupEnabled\": false,\n \"biConnector\": {\n \"enabled\": false,\n \"readPreference\": \"secondary\"\n },\n \"clusterType\": \"GEOSHARDED\",\n \"configServerManagementMode\": \"ATLAS_MANAGED\",\n \"configServerType\": \"DEDICATED\",\n \"connectionStrings\": {\n \"awsPrivateLinkSrv\": {},\n \"privateEndpoint\": [],\n \"standard\": \"mongodb://test-acc-tf-c-739884080-shard-00-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-07.gwbdm.mongodb-dev.net:27016/?ssl=true\\u0026authSource=admin\",\n \"standardSrv\": \"mongodb+srv://test-acc-tf-c-739884080.gwbdm.mongodb-dev.net\"\n },\n \"createDate\": \"2025-03-11T11:10:37Z\",\n \"diskWarmingMode\": \"FULLY_WARMED\",\n \"encryptionAtRestProvider\": \"NONE\",\n \"featureCompatibilityVersion\": \"8.0\",\n \"globalClusterSelfManagedSharding\": false,\n \"groupId\": \"{groupId}\",\n \"id\": \"67d01a2d01d3561b07caf76e\",\n \"labels\": [],\n \"links\": [\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/restoreJobs\",\n \"rel\": \"https://cloud.mongodb.com/restoreJobs\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/snapshots\",\n \"rel\": \"https://cloud.mongodb.com/snapshots\"\n }\n ],\n \"mongoDBMajorVersion\": \"8.0\",\n \"mongoDBVersion\": \"8.0.5\",\n \"name\": \"{clusterName}\",\n \"paused\": false,\n \"pitEnabled\": false,\n \"redactClientLogData\": false,\n \"replicationSpecs\": [\n {\n \"id\": \"67d01a2c01d3561b07caf756\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 0\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 5\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 2\n },\n \"regionName\": \"US_EAST_1\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf753\",\n \"zoneName\": \"Zone 1\"\n },\n {\n \"id\": \"67d01a2c01d3561b07caf758\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 4\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M20\",\n \"nodeCount\": 3\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M20\",\n \"nodeCount\": 1\n },\n \"regionName\": \"US_WEST_2\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf754\",\n \"zoneName\": \"Zone 2\"\n }\n ],\n \"rootCertType\": \"ISRGROOTX1\",\n \"stateName\": \"UPDATING\",\n \"tags\": [],\n \"terminationProtectionEnabled\": false,\n \"versionReleaseSystem\": \"LTS\"\n}" + - config: "" + diff_requests: + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName} + method: DELETE + version: '2023-02-01' + text: "" + responses: + - response_index: 94 + status: 202 + text: "{}" + request_responses: + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName} + method: GET + version: '2024-08-05' + text: "" + responses: + - response_index: 89 + status: 200 + text: "{\n \"advancedConfiguration\": {\n \"customOpensslCipherConfigTls12\": [],\n \"minimumEnabledTlsProtocol\": \"TLS1_2\",\n \"tlsCipherConfigMode\": \"DEFAULT\"\n },\n \"backupEnabled\": false,\n \"biConnector\": {\n \"enabled\": false,\n \"readPreference\": \"secondary\"\n },\n \"clusterType\": \"GEOSHARDED\",\n \"configServerManagementMode\": \"ATLAS_MANAGED\",\n \"configServerType\": \"DEDICATED\",\n \"connectionStrings\": {\n \"awsPrivateLinkSrv\": {},\n \"privateEndpoint\": [],\n \"standard\": \"mongodb://test-acc-tf-c-739884080-shard-00-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-07.gwbdm.mongodb-dev.net:27016/?ssl=true\\u0026authSource=admin\",\n \"standardSrv\": \"mongodb+srv://test-acc-tf-c-739884080.gwbdm.mongodb-dev.net\"\n },\n \"createDate\": \"2025-03-11T11:10:37Z\",\n \"diskWarmingMode\": \"FULLY_WARMED\",\n \"encryptionAtRestProvider\": \"NONE\",\n \"featureCompatibilityVersion\": \"8.0\",\n \"globalClusterSelfManagedSharding\": false,\n \"groupId\": \"{groupId}\",\n \"id\": \"67d01a2d01d3561b07caf76e\",\n \"labels\": [],\n \"links\": [\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/restoreJobs\",\n \"rel\": \"https://cloud.mongodb.com/restoreJobs\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/snapshots\",\n \"rel\": \"https://cloud.mongodb.com/snapshots\"\n }\n ],\n \"mongoDBMajorVersion\": \"8.0\",\n \"mongoDBVersion\": \"8.0.5\",\n \"name\": \"{clusterName}\",\n \"paused\": false,\n \"pitEnabled\": false,\n \"redactClientLogData\": false,\n \"replicationSpecs\": [\n {\n \"id\": \"67d01a2c01d3561b07caf756\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 0\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 5\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 2\n },\n \"regionName\": \"US_EAST_1\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf753\",\n \"zoneName\": \"Zone 1\"\n },\n {\n \"id\": \"67d01a2c01d3561b07caf758\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 4\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M20\",\n \"nodeCount\": 3\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M20\",\n \"nodeCount\": 1\n },\n \"regionName\": \"US_WEST_2\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf754\",\n \"zoneName\": \"Zone 2\"\n }\n ],\n \"rootCertType\": \"ISRGROOTX1\",\n \"stateName\": \"IDLE\",\n \"tags\": [],\n \"terminationProtectionEnabled\": false,\n \"versionReleaseSystem\": \"LTS\"\n}" + - response_index: 95 + status: 200 + duplicate_responses: 6 + text: "{\n \"advancedConfiguration\": {\n \"customOpensslCipherConfigTls12\": [],\n \"minimumEnabledTlsProtocol\": \"TLS1_2\",\n \"tlsCipherConfigMode\": \"DEFAULT\"\n },\n \"backupEnabled\": false,\n \"biConnector\": {\n \"enabled\": false,\n \"readPreference\": \"secondary\"\n },\n \"clusterType\": \"GEOSHARDED\",\n \"configServerManagementMode\": \"ATLAS_MANAGED\",\n \"configServerType\": \"DEDICATED\",\n \"connectionStrings\": {\n \"awsPrivateLinkSrv\": {},\n \"privateEndpoint\": [],\n \"standard\": \"mongodb://test-acc-tf-c-739884080-shard-00-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-07.gwbdm.mongodb-dev.net:27016/?ssl=true\\u0026authSource=admin\",\n \"standardSrv\": \"mongodb+srv://test-acc-tf-c-739884080.gwbdm.mongodb-dev.net\"\n },\n \"createDate\": \"2025-03-11T11:10:37Z\",\n \"diskWarmingMode\": \"FULLY_WARMED\",\n \"encryptionAtRestProvider\": \"NONE\",\n \"featureCompatibilityVersion\": \"8.0\",\n \"globalClusterSelfManagedSharding\": false,\n \"groupId\": \"{groupId}\",\n \"id\": \"67d01a2d01d3561b07caf76e\",\n \"labels\": [],\n \"links\": [\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/restoreJobs\",\n \"rel\": \"https://cloud.mongodb.com/restoreJobs\"\n },\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/snapshots\",\n \"rel\": \"https://cloud.mongodb.com/snapshots\"\n }\n ],\n \"mongoDBMajorVersion\": \"8.0\",\n \"mongoDBVersion\": \"8.0.5\",\n \"name\": \"{clusterName}\",\n \"paused\": false,\n \"pitEnabled\": false,\n \"redactClientLogData\": false,\n \"replicationSpecs\": [\n {\n \"id\": \"67d01a2c01d3561b07caf756\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 0\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 5\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 2\n },\n \"regionName\": \"US_EAST_1\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf753\",\n \"zoneName\": \"Zone 1\"\n },\n {\n \"id\": \"67d01a2c01d3561b07caf758\",\n \"regionConfigs\": [\n {\n \"analyticsAutoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"analyticsSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M10\",\n \"nodeCount\": 4\n },\n \"autoScaling\": {\n \"compute\": {\n \"enabled\": true,\n \"maxInstanceSize\": \"M30\",\n \"minInstanceSize\": \"M10\",\n \"predictiveEnabled\": false,\n \"scaleDownEnabled\": true\n },\n \"diskGB\": {\n \"enabled\": true\n }\n },\n \"electableSpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M20\",\n \"nodeCount\": 3\n },\n \"priority\": 7,\n \"providerName\": \"AWS\",\n \"readOnlySpecs\": {\n \"diskIOPS\": 3000,\n \"diskSizeGB\": 10,\n \"ebsVolumeType\": \"STANDARD\",\n \"instanceSize\": \"M20\",\n \"nodeCount\": 1\n },\n \"regionName\": \"US_WEST_2\"\n }\n ],\n \"zoneId\": \"67d01a2c01d3561b07caf754\",\n \"zoneName\": \"Zone 2\"\n }\n ],\n \"rootCertType\": \"ISRGROOTX1\",\n \"stateName\": \"DELETING\",\n \"tags\": [],\n \"terminationProtectionEnabled\": false,\n \"versionReleaseSystem\": \"LTS\"\n}" + - response_index: 102 + status: 404 + text: "{\n \"detail\": \"No cluster named {clusterName} exists in group {groupId}.\",\n \"error\": 404,\n \"errorCode\": \"CLUSTER_NOT_FOUND\",\n \"parameters\": [\n \"{clusterName}\",\n \"{groupId}\"\n ],\n \"reason\": \"Not Found\"\n}" + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName} + method: GET + version: '2023-02-01' + text: "" + responses: + - response_index: 90 + status: 400 + text: "{\n \"detail\": \"Asymmetric sharded cluster is not supported by the current API version. Please use the latest API instead. Documentation for the latest API is available at https://docs.atlas.mongodb.com/reference/api/clusters-advanced/.\",\n \"error\": 400,\n \"errorCode\": \"ASYMMETRIC_SHARD_UNSUPPORTED\",\n \"parameters\": [],\n \"reason\": \"Bad Request\"\n}" + - path: /api/atlas/v2/groups/{groupId}/containers?providerName=AWS + method: GET + version: '2023-01-01' + text: "" + responses: + - response_index: 91 + status: 200 + text: "{\n \"links\": [\n {\n \"href\": \"https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/containers?includeCount=true\\u0026providerName=AWS\\u0026pageNum=1\\u0026itemsPerPage=100\",\n \"rel\": \"self\"\n }\n ],\n \"results\": [\n {\n \"atlasCidrBlock\": \"192.168.248.0/21\",\n \"id\": \"67d01a2d01d3561b07caf76c\",\n \"providerName\": \"AWS\",\n \"provisioned\": true,\n \"regionName\": \"US_EAST_1\",\n \"vpcId\": \"vpc-0e0706bcd69b8855a\"\n },\n {\n \"atlasCidrBlock\": \"192.168.240.0/21\",\n \"id\": \"67d01a2d01d3561b07caf76d\",\n \"providerName\": \"AWS\",\n \"provisioned\": true,\n \"regionName\": \"US_WEST_2\",\n \"vpcId\": \"vpc-04a84758be9599707\"\n }\n ],\n \"totalCount\": 2\n}" + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName}/processArgs + method: GET + version: '2023-01-01' + text: "" + responses: + - response_index: 92 + status: 200 + text: "{\n \"changeStreamOptionsPreAndPostImagesExpireAfterSeconds\": null,\n \"chunkMigrationConcurrency\": null,\n \"customOpensslCipherConfigTls12\": [],\n \"defaultMaxTimeMS\": null,\n \"defaultReadConcern\": null,\n \"defaultWriteConcern\": null,\n \"failIndexKeyTooLong\": null,\n \"javascriptEnabled\": true,\n \"minimumEnabledTlsProtocol\": \"TLS1_2\",\n \"noTableScan\": false,\n \"oplogMinRetentionHours\": null,\n \"oplogSizeMB\": null,\n \"queryStatsLogVerbosity\": 1,\n \"sampleRefreshIntervalBIConnector\": null,\n \"sampleSizeBIConnector\": null,\n \"tlsCipherConfigMode\": \"DEFAULT\",\n \"transactionLifetimeLimitSeconds\": null\n}" + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName}/processArgs + method: GET + version: '2024-08-05' + text: "" + responses: + - response_index: 93 + status: 200 + text: "{\n \"changeStreamOptionsPreAndPostImagesExpireAfterSeconds\": null,\n \"chunkMigrationConcurrency\": null,\n \"customOpensslCipherConfigTls12\": [],\n \"defaultMaxTimeMS\": null,\n \"defaultWriteConcern\": null,\n \"javascriptEnabled\": true,\n \"minimumEnabledTlsProtocol\": \"TLS1_2\",\n \"noTableScan\": false,\n \"oplogMinRetentionHours\": null,\n \"oplogSizeMB\": null,\n \"queryStatsLogVerbosity\": 1,\n \"sampleRefreshIntervalBIConnector\": null,\n \"sampleSizeBIConnector\": null,\n \"tlsCipherConfigMode\": \"DEFAULT\",\n \"transactionLifetimeLimitSeconds\": null\n}" + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName} + method: DELETE + version: '2023-02-01' + text: "" + responses: + - response_index: 94 + status: 202 + text: "{}" diff --git a/internal/service/advancedcluster/testdata/TestAccMockableAdvancedCluster_replicasetAdvConfigUpdate/03_01_PATCH__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2024-10-23.json b/internal/service/advancedcluster/testdata/TestAccMockableAdvancedCluster_replicasetAdvConfigUpdate/03_01_PATCH__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2024-10-23.json new file mode 100644 index 0000000000..427e29bab1 --- /dev/null +++ b/internal/service/advancedcluster/testdata/TestAccMockableAdvancedCluster_replicasetAdvConfigUpdate/03_01_PATCH__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2024-10-23.json @@ -0,0 +1,4 @@ +{ + "labels": [], + "tags": [] +} \ No newline at end of file diff --git a/internal/service/advancedcluster/testdata/TestAccMockableAdvancedCluster_replicasetAdvConfigUpdate/03_01_DELETE__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2023-02-01.json b/internal/service/advancedcluster/testdata/TestAccMockableAdvancedCluster_replicasetAdvConfigUpdate/04_01_DELETE__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2023-02-01.json similarity index 100% rename from internal/service/advancedcluster/testdata/TestAccMockableAdvancedCluster_replicasetAdvConfigUpdate/03_01_DELETE__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2023-02-01.json rename to internal/service/advancedcluster/testdata/TestAccMockableAdvancedCluster_replicasetAdvConfigUpdate/04_01_DELETE__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2023-02-01.json diff --git a/internal/service/advancedclustertpf/plan_modifier_test.go b/internal/service/advancedclustertpf/plan_modifier_test.go new file mode 100644 index 0000000000..9c63cd6eaa --- /dev/null +++ b/internal/service/advancedclustertpf/plan_modifier_test.go @@ -0,0 +1,58 @@ +package advancedclustertpf_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/unit" +) + +var ( + repSpec0 = tfjsonpath.New("replication_specs").AtSliceIndex(0) + repSpec1 = tfjsonpath.New("replication_specs").AtSliceIndex(1) + regionConfig0 = repSpec0.AtMapKey("region_configs").AtSliceIndex(0) + regionConfig1 = repSpec1.AtMapKey("region_configs").AtSliceIndex(0) + mockConfig = unit.MockConfigAdvancedClusterTPF +) + +func autoScalingKnownValue(computeEnabled, diskEnabled, scaleDown bool, minInstanceSize, maxInstanceSize string) knownvalue.Check { + return knownvalue.ObjectExact(map[string]knownvalue.Check{ + "compute_enabled": knownvalue.Bool(computeEnabled), + "disk_gb_enabled": knownvalue.Bool(diskEnabled), + "compute_scale_down_enabled": knownvalue.Bool(scaleDown), + "compute_min_instance_size": knownvalue.StringExact(minInstanceSize), + "compute_max_instance_size": knownvalue.StringExact(maxInstanceSize), + }) +} + +func TestMockPlanChecks_ClusterTwoRepSpecsWithAutoScalingAndSpecs(t *testing.T) { + var ( + baseConfig = unit.NewMockPlanChecksConfig(t, &mockConfig, unit.ImportNameClusterTwoRepSpecsWithAutoScalingAndSpecs) + resourceName = baseConfig.ResourceName + ) + testCases := map[string][]plancheck.PlanCheck{ + "removed_blocks_from_config_no_plan_changes": { + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + "removed_blocks_from_config_and_instance_change": { + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + plancheck.ExpectKnownValue(resourceName, regionConfig0.AtMapKey("read_only_specs").AtMapKey("instance_size"), knownvalue.StringExact("M10")), + plancheck.ExpectKnownValue(resourceName, regionConfig1.AtMapKey("read_only_specs").AtMapKey("instance_size"), knownvalue.StringExact("M20")), + plancheck.ExpectKnownValue(resourceName, regionConfig0.AtMapKey("auto_scaling"), autoScalingKnownValue(true, true, true, "M10", "M30")), + plancheck.ExpectKnownValue(resourceName, regionConfig0.AtMapKey("analytics_auto_scaling"), autoScalingKnownValue(true, true, true, "M10", "M30")), + plancheck.ExpectKnownValue(resourceName, regionConfig1.AtMapKey("auto_scaling"), autoScalingKnownValue(true, true, true, "M10", "M30")), + plancheck.ExpectKnownValue(resourceName, regionConfig1.AtMapKey("analytics_auto_scaling"), autoScalingKnownValue(true, true, true, "M10", "M30")), + plancheck.ExpectUnknownValue(resourceName, regionConfig0.AtMapKey("analytics_specs")), + plancheck.ExpectKnownValue(resourceName, regionConfig1.AtMapKey("analytics_specs"), knownvalue.NotNull()), + plancheck.ExpectUnknownValue(resourceName, repSpec0.AtMapKey("id")), + plancheck.ExpectUnknownValue(resourceName, repSpec1.AtMapKey("id")), + }, + } + for name, checks := range testCases { + t.Run(name, func(t *testing.T) { + unit.MockPlanChecksAndRun(t, baseConfig.WithNameAndChecks(name, checks)) + }) + } +} diff --git a/internal/service/advancedclustertpf/testdata/.gitignore b/internal/service/advancedclustertpf/testdata/.gitignore new file mode 100644 index 0000000000..662787149d --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/.gitignore @@ -0,0 +1 @@ +ClusterTwoRepSpecsWithAutoScalingAndSpecs_*.yaml diff --git a/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs.tmpl.yaml b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs.tmpl.yaml new file mode 100644 index 0000000000..5bc808d9a6 --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs.tmpl.yaml @@ -0,0 +1,117 @@ +variables: + clusterName: mocked-cluster + groupId: "111111111111111111111111" +steps: + - config: |- + resource "mongodbatlas_advanced_cluster" "test" { + project_id = "111111111111111111111111" + name = "mocked-cluster" + cluster_type = "GEOSHARDED" + + + replication_specs = [{ + region_configs = [{ + analytics_auto_scaling = { + compute_enabled = true + compute_max_instance_size = "M30" + compute_min_instance_size = "M10" + compute_scale_down_enabled = true + disk_gb_enabled = true + } + auto_scaling = { + compute_enabled = true + compute_max_instance_size = "M30" + compute_min_instance_size = "M10" + compute_scale_down_enabled = true + disk_gb_enabled = true + } + electable_specs = { + instance_size = "M10" + node_count = 5 + } + priority = 7 + provider_name = "AWS" + read_only_specs = { + instance_size = "M10" + node_count = 2 + } + region_name = "US_EAST_1" + }] + zone_name = "Zone 1" + }, { + region_configs = [{ + analytics_auto_scaling = { + compute_enabled = true + compute_max_instance_size = "M30" + compute_min_instance_size = "M10" + compute_scale_down_enabled = true + disk_gb_enabled = true + } + analytics_specs = { + instance_size = "M10" + node_count = 4 + } + auto_scaling = { + compute_enabled = true + compute_max_instance_size = "M30" + compute_min_instance_size = "M10" + compute_scale_down_enabled = true + disk_gb_enabled = true + } + electable_specs = { + instance_size = "M10" + node_count = 3 + } + priority = 7 + provider_name = "AWS" + read_only_specs = { + instance_size = "M10" + node_count = 1 + } + region_name = "US_WEST_2" + }] + zone_name = "Zone 2" + }] + } + diff_requests: [] + request_responses: + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName} + method: GET + version: '2024-08-05' + text: "" + responses: + - response_index: 0 + status: 200 + text: "import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2024-08-05.json" + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName} + method: GET + version: '2023-02-01' + text: "" + responses: + - response_index: 0 + status: 200 + text: "import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2023-02-01.json" + - path: /api/atlas/v2/groups/{groupId}/containers?providerName=AWS + method: GET + version: '2023-01-01' + text: "" + responses: + - response_index: 0 + status: 200 + text: "import_GET__api_atlas_v2_groups_{groupId}_containers?providerName=AWS_2023-01-01.json" + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName}/processArgs + method: GET + version: '2023-01-01' + text: "" + responses: + - response_index: 0 + status: 200 + text: "import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2023-01-01.json" + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName}/processArgs + method: GET + version: '2024-08-05' + text: "" + responses: + - response_index: 0 + status: 200 + text: "import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2024-08-05.json" diff --git a/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2023-02-01.json b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2023-02-01.json new file mode 100644 index 0000000000..7b2708243e --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2023-02-01.json @@ -0,0 +1,165 @@ +{ + "advancedConfiguration": { + "customOpensslCipherConfigTls12": [], + "minimumEnabledTlsProtocol": "TLS1_2", + "tlsCipherConfigMode": "DEFAULT" + }, + "backupEnabled": false, + "biConnector": { + "enabled": false, + "readPreference": "secondary" + }, + "clusterType": "GEOSHARDED", + "configServerManagementMode": "ATLAS_MANAGED", + "configServerType": "DEDICATED", + "connectionStrings": { + "awsPrivateLinkSrv": {}, + "privateEndpoint": [], + "standard": "mongodb://test-acc-tf-c-739884080-shard-00-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-07.gwbdm.mongodb-dev.net:27016/?ssl=true\u0026authSource=admin", + "standardSrv": "mongodb+srv://test-acc-tf-c-739884080.gwbdm.mongodb-dev.net" + }, + "createDate": "2025-03-11T11:10:37Z", + "diskSizeGB": 10, + "diskWarmingMode": "FULLY_WARMED", + "encryptionAtRestProvider": "NONE", + "globalClusterSelfManagedSharding": false, + "groupId": "{groupId}", + "id": "67d01a2d01d3561b07caf76e", + "labels": [], + "links": [ + { + "href": "https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", + "rel": "self" + }, + { + "href": "https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/restoreJobs", + "rel": "https://cloud.mongodb.com/restoreJobs" + }, + { + "href": "https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/snapshots", + "rel": "https://cloud.mongodb.com/snapshots" + } + ], + "mongoDBMajorVersion": "8.0", + "mongoDBVersion": "8.0.5", + "name": "{clusterName}", + "paused": false, + "pitEnabled": false, + "replicationSpecs": [ + { + "id": "67d01a2c01d3561b07caf755", + "numShards": 1, + "regionConfigs": [ + { + "analyticsAutoScaling": { + "compute": { + "enabled": true, + "maxInstanceSize": "M30", + "minInstanceSize": "M10", + "predictiveEnabled": false, + "scaleDownEnabled": true + }, + "diskGB": { + "enabled": true + } + }, + "analyticsSpecs": { + "diskIOPS": 3000, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 0 + }, + "autoScaling": { + "compute": { + "enabled": true, + "maxInstanceSize": "M30", + "minInstanceSize": "M10", + "predictiveEnabled": false, + "scaleDownEnabled": true + }, + "diskGB": { + "enabled": true + } + }, + "electableSpecs": { + "diskIOPS": 3000, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 5 + }, + "priority": 7, + "providerName": "AWS", + "readOnlySpecs": { + "diskIOPS": 3000, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 2 + }, + "regionName": "US_EAST_1" + } + ], + "zoneId": "67d01a2c01d3561b07caf753", + "zoneName": "Zone 1" + }, + { + "id": "67d01a2c01d3561b07caf757", + "numShards": 1, + "regionConfigs": [ + { + "analyticsAutoScaling": { + "compute": { + "enabled": true, + "maxInstanceSize": "M30", + "minInstanceSize": "M10", + "predictiveEnabled": false, + "scaleDownEnabled": true + }, + "diskGB": { + "enabled": true + } + }, + "analyticsSpecs": { + "diskIOPS": 3000, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 4 + }, + "autoScaling": { + "compute": { + "enabled": true, + "maxInstanceSize": "M30", + "minInstanceSize": "M10", + "predictiveEnabled": false, + "scaleDownEnabled": true + }, + "diskGB": { + "enabled": true + } + }, + "electableSpecs": { + "diskIOPS": 3000, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 3 + }, + "priority": 7, + "providerName": "AWS", + "readOnlySpecs": { + "diskIOPS": 3000, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 1 + }, + "regionName": "US_WEST_2" + } + ], + "zoneId": "67d01a2c01d3561b07caf754", + "zoneName": "Zone 2" + } + ], + "rootCertType": "ISRGROOTX1", + "stateName": "IDLE", + "tags": [], + "terminationProtectionEnabled": false, + "versionReleaseSystem": "LTS" +} \ No newline at end of file diff --git a/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2024-08-05.json b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2024-08-05.json new file mode 100644 index 0000000000..9bf27d71cb --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2024-08-05.json @@ -0,0 +1,170 @@ +{ + "advancedConfiguration": { + "customOpensslCipherConfigTls12": [], + "minimumEnabledTlsProtocol": "TLS1_2", + "tlsCipherConfigMode": "DEFAULT" + }, + "backupEnabled": false, + "biConnector": { + "enabled": false, + "readPreference": "secondary" + }, + "clusterType": "GEOSHARDED", + "configServerManagementMode": "ATLAS_MANAGED", + "configServerType": "DEDICATED", + "connectionStrings": { + "awsPrivateLinkSrv": {}, + "privateEndpoint": [], + "standard": "mongodb://test-acc-tf-c-739884080-shard-00-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-00-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-00.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-01.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-02.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-03.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-04.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-05.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-06.gwbdm.mongodb-dev.net:27016,test-acc-tf-c-739884080-shard-01-07.gwbdm.mongodb-dev.net:27016/?ssl=true\u0026authSource=admin", + "standardSrv": "mongodb+srv://test-acc-tf-c-739884080.gwbdm.mongodb-dev.net" + }, + "createDate": "2025-03-11T11:10:37Z", + "diskWarmingMode": "FULLY_WARMED", + "encryptionAtRestProvider": "NONE", + "featureCompatibilityVersion": "8.0", + "globalClusterSelfManagedSharding": false, + "groupId": "{groupId}", + "id": "67d01a2d01d3561b07caf76e", + "labels": [], + "links": [ + { + "href": "https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", + "rel": "self" + }, + { + "href": "https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/restoreJobs", + "rel": "https://cloud.mongodb.com/restoreJobs" + }, + { + "href": "https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/snapshots", + "rel": "https://cloud.mongodb.com/snapshots" + } + ], + "mongoDBMajorVersion": "8.0", + "mongoDBVersion": "8.0.5", + "name": "{clusterName}", + "paused": false, + "pitEnabled": false, + "redactClientLogData": false, + "replicationSpecs": [ + { + "id": "67d01a2c01d3561b07caf756", + "regionConfigs": [ + { + "analyticsAutoScaling": { + "compute": { + "enabled": true, + "maxInstanceSize": "M30", + "minInstanceSize": "M10", + "predictiveEnabled": false, + "scaleDownEnabled": true + }, + "diskGB": { + "enabled": true + } + }, + "analyticsSpecs": { + "diskIOPS": 3000, + "diskSizeGB": 10, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 0 + }, + "autoScaling": { + "compute": { + "enabled": true, + "maxInstanceSize": "M30", + "minInstanceSize": "M10", + "predictiveEnabled": false, + "scaleDownEnabled": true + }, + "diskGB": { + "enabled": true + } + }, + "electableSpecs": { + "diskIOPS": 3000, + "diskSizeGB": 10, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 5 + }, + "priority": 7, + "providerName": "AWS", + "readOnlySpecs": { + "diskIOPS": 3000, + "diskSizeGB": 10, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 2 + }, + "regionName": "US_EAST_1" + } + ], + "zoneId": "67d01a2c01d3561b07caf753", + "zoneName": "Zone 1" + }, + { + "id": "67d01a2c01d3561b07caf758", + "regionConfigs": [ + { + "analyticsAutoScaling": { + "compute": { + "enabled": true, + "maxInstanceSize": "M30", + "minInstanceSize": "M10", + "predictiveEnabled": false, + "scaleDownEnabled": true + }, + "diskGB": { + "enabled": true + } + }, + "analyticsSpecs": { + "diskIOPS": 3000, + "diskSizeGB": 10, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 4 + }, + "autoScaling": { + "compute": { + "enabled": true, + "maxInstanceSize": "M30", + "minInstanceSize": "M10", + "predictiveEnabled": false, + "scaleDownEnabled": true + }, + "diskGB": { + "enabled": true + } + }, + "electableSpecs": { + "diskIOPS": 3000, + "diskSizeGB": 10, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 3 + }, + "priority": 7, + "providerName": "AWS", + "readOnlySpecs": { + "diskIOPS": 3000, + "diskSizeGB": 10, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 1 + }, + "regionName": "US_WEST_2" + } + ], + "zoneId": "67d01a2c01d3561b07caf754", + "zoneName": "Zone 2" + } + ], + "rootCertType": "ISRGROOTX1", + "stateName": "IDLE", + "tags": [], + "terminationProtectionEnabled": false, + "versionReleaseSystem": "LTS" +} \ No newline at end of file diff --git a/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2023-01-01.json b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2023-01-01.json new file mode 100644 index 0000000000..90acae41a2 --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2023-01-01.json @@ -0,0 +1,19 @@ +{ + "changeStreamOptionsPreAndPostImagesExpireAfterSeconds": null, + "chunkMigrationConcurrency": null, + "customOpensslCipherConfigTls12": [], + "defaultMaxTimeMS": null, + "defaultReadConcern": null, + "defaultWriteConcern": null, + "failIndexKeyTooLong": null, + "javascriptEnabled": true, + "minimumEnabledTlsProtocol": "TLS1_2", + "noTableScan": false, + "oplogMinRetentionHours": null, + "oplogSizeMB": null, + "queryStatsLogVerbosity": 1, + "sampleRefreshIntervalBIConnector": null, + "sampleSizeBIConnector": null, + "tlsCipherConfigMode": "DEFAULT", + "transactionLifetimeLimitSeconds": null +} \ No newline at end of file diff --git a/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2024-08-05.json b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2024-08-05.json new file mode 100644 index 0000000000..25d92e3151 --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2024-08-05.json @@ -0,0 +1,17 @@ +{ + "changeStreamOptionsPreAndPostImagesExpireAfterSeconds": null, + "chunkMigrationConcurrency": null, + "customOpensslCipherConfigTls12": [], + "defaultMaxTimeMS": null, + "defaultWriteConcern": null, + "javascriptEnabled": true, + "minimumEnabledTlsProtocol": "TLS1_2", + "noTableScan": false, + "oplogMinRetentionHours": null, + "oplogSizeMB": null, + "queryStatsLogVerbosity": 1, + "sampleRefreshIntervalBIConnector": null, + "sampleSizeBIConnector": null, + "tlsCipherConfigMode": "DEFAULT", + "transactionLifetimeLimitSeconds": null +} \ No newline at end of file diff --git a/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_containers?providerName=AWS_2023-01-01.json b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_containers?providerName=AWS_2023-01-01.json new file mode 100644 index 0000000000..51db6ff2e8 --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/import_GET__api_atlas_v2_groups_{groupId}_containers?providerName=AWS_2023-01-01.json @@ -0,0 +1,27 @@ +{ + "links": [ + { + "href": "https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/containers?includeCount=true\u0026providerName=AWS\u0026pageNum=1\u0026itemsPerPage=100", + "rel": "self" + } + ], + "results": [ + { + "atlasCidrBlock": "192.168.248.0/21", + "id": "67d01a2d01d3561b07caf76c", + "providerName": "AWS", + "provisioned": true, + "regionName": "US_EAST_1", + "vpcId": "vpc-0e0706bcd69b8855a" + }, + { + "atlasCidrBlock": "192.168.240.0/21", + "id": "67d01a2d01d3561b07caf76d", + "providerName": "AWS", + "provisioned": true, + "regionName": "US_WEST_2", + "vpcId": "vpc-04a84758be9599707" + } + ], + "totalCount": 2 +} \ No newline at end of file diff --git a/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/main.tf b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/main.tf new file mode 100644 index 0000000000..3f7a1ce1ec --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/main.tf @@ -0,0 +1,70 @@ +resource "mongodbatlas_advanced_cluster" "test" { + project_id = "111111111111111111111111" + name = "mocked-cluster" + cluster_type = "GEOSHARDED" + + + replication_specs = [{ + region_configs = [{ + analytics_auto_scaling = { + compute_enabled = true + compute_max_instance_size = "M30" + compute_min_instance_size = "M10" + compute_scale_down_enabled = true + disk_gb_enabled = true + } + auto_scaling = { + compute_enabled = true + compute_max_instance_size = "M30" + compute_min_instance_size = "M10" + compute_scale_down_enabled = true + disk_gb_enabled = true + } + electable_specs = { + instance_size = "M10" + node_count = 5 + } + priority = 7 + provider_name = "AWS" + read_only_specs = { + instance_size = "M10" + node_count = 2 + } + region_name = "US_EAST_1" + }] + zone_name = "Zone 1" + }, { + region_configs = [{ + analytics_auto_scaling = { + compute_enabled = true + compute_max_instance_size = "M30" + compute_min_instance_size = "M10" + compute_scale_down_enabled = true + disk_gb_enabled = true + } + analytics_specs = { + instance_size = "M10" + node_count = 4 + } + auto_scaling = { + compute_enabled = true + compute_max_instance_size = "M30" + compute_min_instance_size = "M10" + compute_scale_down_enabled = true + disk_gb_enabled = true + } + electable_specs = { + instance_size = "M10" + node_count = 3 + } + priority = 7 + provider_name = "AWS" + read_only_specs = { + instance_size = "M10" + node_count = 1 + } + region_name = "US_WEST_2" + }] + zone_name = "Zone 2" + }] +} diff --git a/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/main_removed_blocks_from_config_and_instance_change.tf b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/main_removed_blocks_from_config_and_instance_change.tf new file mode 100644 index 0000000000..814e835ca3 --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/main_removed_blocks_from_config_and_instance_change.tf @@ -0,0 +1,30 @@ +resource "mongodbatlas_advanced_cluster" "test" { + project_id = "111111111111111111111111" + name = "mocked-cluster" + cluster_type = "GEOSHARDED" + + + replication_specs = [{ + region_configs = [{ + electable_specs = { + instance_size = "M10" + node_count = 5 + } + priority = 7 + provider_name = "AWS" + region_name = "US_EAST_1" + }] + zone_name = "Zone 1" + }, { + region_configs = [{ + electable_specs = { + instance_size = "M20" + node_count = 3 + } + priority = 7 + provider_name = "AWS" + region_name = "US_WEST_2" + }] + zone_name = "Zone 2" + }] +} diff --git a/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/main_removed_blocks_from_config_no_plan_changes.tf b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/main_removed_blocks_from_config_no_plan_changes.tf new file mode 100644 index 0000000000..2e1e1e8e5f --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/main_removed_blocks_from_config_no_plan_changes.tf @@ -0,0 +1,30 @@ +resource "mongodbatlas_advanced_cluster" "test" { + project_id = "111111111111111111111111" + name = "mocked-cluster" + cluster_type = "GEOSHARDED" + + + replication_specs = [{ + region_configs = [{ + electable_specs = { + instance_size = "M10" + node_count = 5 + } + priority = 7 + provider_name = "AWS" + region_name = "US_EAST_1" + }] + zone_name = "Zone 1" + }, { + region_configs = [{ + electable_specs = { + instance_size = "M10" + node_count = 3 + } + priority = 7 + provider_name = "AWS" + region_name = "US_WEST_2" + }] + zone_name = "Zone 2" + }] +} diff --git a/internal/testutil/unit/http_mock_configs.go b/internal/testutil/unit/http_mock_configs.go new file mode 100644 index 0000000000..e666eb1b3c --- /dev/null +++ b/internal/testutil/unit/http_mock_configs.go @@ -0,0 +1,19 @@ +package unit + +import ( + "time" + + "github.com/mongodb/terraform-provider-mongodbatlas/internal/service/advancedclustertpf" +) + +var ( + shortRefresh = 100 * time.Millisecond + MockConfigAdvancedClusterTPF = MockHTTPDataConfig{AllowMissingRequests: true, SideEffect: shortenClusterTPFRetries, IsDiffMustSubstrings: []string{"/clusters"}, QueryVars: []string{"providerName"}} +) + +func shortenClusterTPFRetries() error { + advancedclustertpf.RetryMinTimeout = shortRefresh + advancedclustertpf.RetryDelay = shortRefresh + advancedclustertpf.RetryPollInterval = shortRefresh + return nil +} diff --git a/internal/testutil/unit/http_mocker.go b/internal/testutil/unit/http_mocker.go index a3027ec6c6..ad331c78bd 100644 --- a/internal/testutil/unit/http_mocker.go +++ b/internal/testutil/unit/http_mocker.go @@ -24,6 +24,8 @@ const ( type MockHTTPDataConfig struct { SideEffect func() error + RequestHandler ManualRequestHandler + FilePathOverride string IsDiffSkipSuffixes []string IsDiffMustSubstrings []string QueryVars []string @@ -101,7 +103,12 @@ func MockConfigFilePath(t *testing.T) string { func ReadMockData(t *testing.T, tfConfigs []string) *MockHTTPData { t.Helper() httpDataPath := MockConfigFilePath(t) - data, err := ParseTestDataConfigYAML(httpDataPath) + return ReadMockDataFile(t, httpDataPath, tfConfigs) +} + +func ReadMockDataFile(t *testing.T, file string, tfConfigs []string) *MockHTTPData { + t.Helper() + data, err := ParseTestDataConfigYAML(file) require.NoError(t, err) oldVariables := data.Variables data.Variables = map[string]string{} @@ -136,12 +143,20 @@ func UpdateMockDataDiffRequest(t *testing.T, stepIndex, diffRequestIndex int, ne func enableReplayForTestCase(t *testing.T, config *MockHTTPDataConfig, testCase *resource.TestCase) error { t.Helper() tfConfigs := extractAndNormalizeConfig(t, testCase) - data := ReadMockData(t, tfConfigs) + var data *MockHTTPData + if config.FilePathOverride != "" { + data = ReadMockDataFile(t, config.FilePathOverride, tfConfigs) + } else { + data = ReadMockData(t, tfConfigs) + } roundTripper, mockRoundTripper := NewMockRoundTripper(t, config, data) httpClientModifier := mockClientModifier{config: config, mockRoundTripper: roundTripper} testCase.ProtoV6ProviderFactories = TestAccProviderV6FactoriesWithMock(t, &httpClientModifier) testCase.PreCheck = func() { if config.SideEffect != nil { + // Mock Configs can share SideEffect, using lock to avoid race conditions. + accClientLock.Lock() + defer accClientLock.Unlock() require.NoError(t, config.SideEffect()) } } diff --git a/internal/testutil/unit/http_mocker_api_paths.go b/internal/testutil/unit/http_mocker_api_paths.go index a8c89718b8..b3bf4ccc2c 100644 --- a/internal/testutil/unit/http_mocker_api_paths.go +++ b/internal/testutil/unit/http_mocker_api_paths.go @@ -87,7 +87,7 @@ func ReadAPISpecPaths() map[string][]APISpecPath { return apiSpecPaths } -func fileExist(fullPath string) bool { +func FileExist(fullPath string) bool { _, err := os.Stat(fullPath) if err == nil { return true @@ -95,7 +95,7 @@ func fileExist(fullPath string) bool { return !os.IsNotExist(err) } -func fullPath(relPath string) string { +func RepoPath(relPath string) string { workDir, err := os.Getwd() if err != nil { panic(fmt.Sprintf("error getting working directory: %s", err)) @@ -106,7 +106,7 @@ func fullPath(relPath string) string { parentCandidate := workdDirParts[:len(workdDirParts)-i] parentCandidate = append(parentCandidate, ".git") gitDir := path.Join(parentCandidate...) - if fileExist(gitDir) { + if FileExist(gitDir) { repoPath, _ := strings.CutSuffix(gitDir, ".git") return path.Join(repoPath, relPath) } @@ -119,9 +119,9 @@ func init() { } func InitializeAPISpecPaths() { - specPath := fullPath(specFileRelPath) + specPath := RepoPath(specFileRelPath) var err error - if !fileExist(specPath) { + if !FileExist(specPath) { err = DownloadOpenAPISpec(atlasAdminAPISpecURL, specPath) if err != nil { panic(fmt.Sprintf("error downloading OpenAPI spec: %s", err)) diff --git a/internal/testutil/unit/http_mocker_config_capture.go b/internal/testutil/unit/http_mocker_config_capture.go index 5396b9aafb..07ff0e7009 100644 --- a/internal/testutil/unit/http_mocker_config_capture.go +++ b/internal/testutil/unit/http_mocker_config_capture.go @@ -143,7 +143,7 @@ func (c *CaptureMockConfigClientModifier) WriteCapturedData(filePath string) err func WriteConfigYaml(filePath, configYaml string) error { dirPath := path.Dir(filePath) - if !fileExist(dirPath) { + if !FileExist(dirPath) { err := os.Mkdir(dirPath, 0o755) if err != nil { return err @@ -189,7 +189,7 @@ func parseRoundTrip(req *http.Request, resp *http.Response, responseIndex, stepN Method: req.Method, Text: requestPayload, }, - Response: statusText{ + Response: StatusText{ Text: responsePayload, Status: resp.StatusCode, ResponseIndex: responseIndex, diff --git a/internal/testutil/unit/http_mocker_data.go b/internal/testutil/unit/http_mocker_data.go index 0899a3bfb6..5488179844 100644 --- a/internal/testutil/unit/http_mocker_data.go +++ b/internal/testutil/unit/http_mocker_data.go @@ -12,14 +12,14 @@ import ( "gopkg.in/yaml.v3" ) -type statusText struct { +type StatusText struct { Text string `yaml:"text"` ResponseIndex int `yaml:"response_index"` Status int `yaml:"status"` DuplicateResponses int `yaml:"duplicate_responses"` } -func (s statusText) MarshalYAML() (interface{}, error) { +func (s StatusText) MarshalYAML() (interface{}, error) { childNodes := []*yaml.Node{ {Kind: yaml.ScalarNode, Value: "response_index"}, {Kind: yaml.ScalarNode, Value: fmt.Sprintf("%d", s.ResponseIndex)}, @@ -43,7 +43,7 @@ func (s statusText) MarshalYAML() (interface{}, error) { }, nil } -func (s *statusText) IncreaseDuplicateResponses() { +func (s *StatusText) IncreaseDuplicateResponses() { s.DuplicateResponses++ } @@ -52,7 +52,7 @@ type RequestInfo struct { Method string `yaml:"method"` Version string `yaml:"version"` Text string `yaml:"text"` - Responses []statusText `yaml:"responses"` + Responses []StatusText `yaml:"responses"` } // Custom marshaling is necessary to use `flow` style only on response fields (text and responses.*.text) @@ -85,10 +85,10 @@ func (i RequestInfo) MarshalYAML() (any, error) { //nolint:gocritic // Using a p } func (i *RequestInfo) id() string { - return fmt.Sprintf("%s_%s", i.idShort(), i.Text) + return fmt.Sprintf("%s_%s", i.IDShort(), i.Text) } -func (i *RequestInfo) idShort() string { +func (i *RequestInfo) IDShort() string { return fmt.Sprintf("%s_%s_%s", i.Method, i.Path, i.Version) } @@ -152,13 +152,13 @@ func (l Literal) MarshalYAML() (any, error) { }, nil } -type stepRequests struct { +type StepRequests struct { Config Literal `yaml:"config,omitempty"` DiffRequests []RequestInfo `yaml:"diff_requests"` RequestResponses []RequestInfo `yaml:"request_responses"` } -func (s *stepRequests) findRequest(request *RequestInfo) (*RequestInfo, bool) { +func (s *StepRequests) findRequest(request *RequestInfo) (*RequestInfo, bool) { for i := range s.RequestResponses { if s.RequestResponses[i].id() == request.id() { return &s.RequestResponses[i], true @@ -167,7 +167,7 @@ func (s *stepRequests) findRequest(request *RequestInfo) (*RequestInfo, bool) { return nil, false } -func (s *stepRequests) AddRequest(request *RequestInfo, isDiff bool) { +func (s *StepRequests) AddRequest(request *RequestInfo, isDiff bool) { if isDiff { s.DiffRequests = append(s.DiffRequests, *request) } @@ -189,13 +189,13 @@ type RoundTrip struct { Variables map[string]string QueryString string Request RequestInfo - Response statusText + Response StatusText StepNumber int } func NewMockHTTPData(t *testing.T, stepCount int, tfConfigs []string) *MockHTTPData { t.Helper() - steps := make([]stepRequests, stepCount) + steps := make([]StepRequests, stepCount) data := MockHTTPData{ Steps: steps, Variables: map[string]string{}, @@ -237,7 +237,7 @@ func (e VariablesChangedError) ChangedValuesMap() map[string]string { type MockHTTPData struct { Variables map[string]string `yaml:"variables"` - Steps []stepRequests `yaml:"steps"` + Steps []StepRequests `yaml:"steps"` } func (m *MockHTTPData) useTFConfigs(t *testing.T, tfConfigs []string) { @@ -300,7 +300,7 @@ func (m *MockHTTPData) AddRoundtrip(t *testing.T, rt *RoundTrip, isDiff bool) er Method: rt.Request.Method, Path: normalizedPath, Text: useVars(rtVariables, rt.Request.Text), - Responses: []statusText{ + Responses: []StatusText{ { Text: useVars(rtVariables, rt.Response.Text), Status: rt.Response.Status, diff --git a/internal/testutil/unit/http_mocker_plan_checks.go b/internal/testutil/unit/http_mocker_plan_checks.go new file mode 100644 index 0000000000..6b4881ba04 --- /dev/null +++ b/internal/testutil/unit/http_mocker_plan_checks.go @@ -0,0 +1,186 @@ +package unit + +import ( + "bytes" + "context" + "errors" + "fmt" + "net/http" + "os" + "path" + "path/filepath" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/require" +) + +const ( + ImportNameClusterTwoRepSpecsWithAutoScalingAndSpecs = "ClusterTwoRepSpecsWithAutoScalingAndSpecs" + MockedClusterName = "mocked-cluster" + MockedProjectID = "111111111111111111111111" +) + +var ( + errToSkipApply = errors.New("avoid full apply by raising an expected error") + + importIDMapping = map[string]string{ + ImportNameClusterTwoRepSpecsWithAutoScalingAndSpecs: fmt.Sprintf("%s-%s", MockedProjectID, MockedClusterName), + } + // later this could be inferred when reading the src main.tf + importResourceNameMapping = map[string]string{ + ImportNameClusterTwoRepSpecsWithAutoScalingAndSpecs: "mongodbatlas_advanced_cluster.test", + } +) + +func NewMockPlanChecksConfig(t *testing.T, mockConfig *MockHTTPDataConfig, importName string) MockPlanChecksConfig { + t.Helper() + importID := importIDMapping[importName] + require.NotEmpty(t, importID, "import ID not found for import name: %s", importName) + resourceName := importResourceNameMapping[importName] + require.NotEmpty(t, resourceName, "resource name not found for import name: %s", importName) + config := MockPlanChecksConfig{ + ImportName: importName, + MockConfig: *mockConfig, + ImportID: importID, + ResourceName: resourceName, + } + return config +} + +type MockPlanChecksConfig struct { + ImportID string + ResourceName string + ImportName string + Name string + Checks []plancheck.PlanCheck + MockConfig MockHTTPDataConfig +} + +func (m *MockPlanChecksConfig) WithNameAndChecks(name string, checks []plancheck.PlanCheck) *MockPlanChecksConfig { + return &MockPlanChecksConfig{ + Checks: checks, + ImportName: m.ImportName, + ImportID: m.ImportID, + ResourceName: m.ResourceName, + MockConfig: m.MockConfig, + Name: name, + } +} + +// MockPlanChecksAndRun creates and runs a UnitTest enabled TestCase for Read to State checks and PlanModifier logic. +// The 1st step is always Import +// The 2nd step is always Plan with runConfig.Checks run. Note: No Update logic is executed as we exit after the PlanModifier has run. +// Instead of having to store full mock data files, we re-use the same GET requests from the directory testdata/{runConfig.ImportName}/import_*.json +// Together with the extra step in `testdata/{ImportName}/main_{runConfig.Name}.tf` we fill the template: testdata/{runConfig.ImportName}.tmpl.yaml +func MockPlanChecksAndRun(t *testing.T, runConfig *MockPlanChecksConfig) { + t.Helper() + importConfig, planConfig, mockDataPath := fillMockDataTemplate(t, runConfig.ImportName, runConfig.Name) + useManualHandler := false + runConfig.Checks = append(runConfig.Checks, &requestHandlerSwitch{useManualHandler: &useManualHandler}) + testCase := &resource.TestCase{ + IsUnitTest: true, + PreventPostDestroyRefresh: true, + Steps: []resource.TestStep{ + { + Config: importConfig, + ResourceName: runConfig.ResourceName, + ImportStateId: runConfig.ImportID, // static ID to import + ImportState: true, + ImportStatePersist: true, // save the state to use it in the next plan + }, + { + // Specifying Config AND using PlanChecks are only available when running test in `Config` mode (see https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/teststep#test-modes for the different modes) + // To avoid doing a full apply we return a known error in the `requestHandlerSwitch` and Expect it in ExpectError + Config: planConfig, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: runConfig.Checks, + }, + ExpectError: regexp.MustCompile(fmt.Sprintf("^Pre-apply plan check\\(s\\) failed:\n%s$", errToSkipApply.Error())), // Notice full match using ^ and $ in case some checks also fails + }, + }, + } + mockConfig := runConfig.MockConfig + // Use FilePathOverride to avoid having to follow the standard `MockConfigFilePath` which depends on the test name + mockConfig.FilePathOverride = mockDataPath + // Custom RequestHandler that runs after PlanModifier is done, to avoid `mock response not found`` errors in test cleanup functions + mockConfig.RequestHandler = func(defaulthHandler RequestHandler, req *http.Request, method string) (*http.Response, error) { + customHandler := func(req *http.Request, method string) (*http.Response, error) { + switch method { + case "GET": + notFoundResponder, err := httpmock.NewJsonResponder(404, map[string]any{ + "errorCode": "RESOURCE_NOT_FOUND", + }) + require.NoError(t, err) + return notFoundResponder(req) + case "DELETE": + return httpmock.NewStringResponder(202, "")(req) + } + return nil, fmt.Errorf("plan check responder doesn't have logic to handle, method: %s, url: %s", method, req.URL) + } + if useManualHandler { + return customHandler(req, method) + } + return defaulthHandler(req, method) + } + err := enableReplayForTestCase( + t, + &mockConfig, + testCase, + ) + require.NoError(t, err) + resource.ParallelTest(t, *testCase) +} + +type requestHandlerSwitch struct { + useManualHandler *bool +} + +func (r *requestHandlerSwitch) CheckPlan(_ context.Context, req plancheck.CheckPlanRequest, resp *plancheck.CheckPlanResponse) { + *r.useManualHandler = true + resp.Error = errToSkipApply +} + +func fillMockDataTemplate(t *testing.T, importName, planName string) (importConfig, planConfig, mockDataFilePath string) { + t.Helper() + templatePath := fmt.Sprintf("testdata/%s.tmpl.yaml", importName) + templateContent, err := os.ReadFile(templatePath) + require.NoError(t, err) + responseDir := fmt.Sprintf("testdata/%s", importName) + responsePaths, err := filepath.Glob(path.Join(responseDir, "*.json")) + require.NoError(t, err) + for _, testFile := range responsePaths { + testFileContent, err := os.ReadFile(testFile) + require.NoError(t, err) + testFileContent = bytes.ReplaceAll(testFileContent, []byte(`"`), []byte(`\"`)) + testFileContent = bytes.ReplaceAll(testFileContent, []byte("\n"), []byte(`\n`)) + templateContent = bytes.ReplaceAll(templateContent, []byte(filepath.Base(testFile)), testFileContent) + } + mockDataPath := fmt.Sprintf("testdata/%s_%s.yaml", importName, planName) + err = os.WriteFile(mockDataPath, templateContent, 0o600) + require.NoError(t, err) + fullImportConfigBytes, err := os.ReadFile(path.Join(responseDir, "main.tf")) + require.NoError(t, err) + fullImportConfig := string(fullImportConfigBytes) + planCheckConfigBytes, err := os.ReadFile(path.Join(responseDir, fmt.Sprintf("main_%s.tf", planName))) + require.NoError(t, err) + planCheckConfig := string(planCheckConfigBytes) + addPlanCheckStepAndReadImportConfig(t, fullImportConfig, planCheckConfig, mockDataPath) + return fullImportConfig, planCheckConfig, mockDataPath +} + +func addPlanCheckStepAndReadImportConfig(t *testing.T, fullImportConfig, planCheckConfig, mockDataPath string) { + t.Helper() + parseData := ReadMockDataFile(t, mockDataPath, []string{fullImportConfig}) + parseData.Steps = append(parseData.Steps, StepRequests{ + Config: Literal(planCheckConfig), + RequestResponses: parseData.Steps[0].RequestResponses, + }) + finalYaml, err := ConfigYaml(parseData) + require.NoError(t, err) + err = os.WriteFile(mockDataPath, []byte(finalYaml), 0o600) + require.NoError(t, err) +} diff --git a/internal/testutil/unit/http_mocker_plan_checks_test.go b/internal/testutil/unit/http_mocker_plan_checks_test.go new file mode 100644 index 0000000000..f527fc477c --- /dev/null +++ b/internal/testutil/unit/http_mocker_plan_checks_test.go @@ -0,0 +1,136 @@ +package unit_test + +import ( + "fmt" + "os" + "path" + "strings" + "testing" + + "github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/unit" + "github.com/stretchr/testify/require" +) + +const ( + pkgAdvancedCluster = "advancedcluster" + pkgAdvancedClusterTPF = "advancedclustertpf" + pkgRelPath = "internal/service" +) + +type importNameConfig struct { + VariableReplacments map[string]string + TestName string + SrcPackage string + DestPackage string + Step int +} + +// Manual test meant for creating the data needed for a MockPlanChecks Test. +func TestConvertMockableTests(t *testing.T) { + if os.Getenv("CONVERT_MOCKABLE_TESTS") == "" { + t.Skip("CONVERT_MOCKABLE_TESTS is not set, avoid running this in CI and by accident") + } + for importName, config := range map[string]importNameConfig{ + unit.ImportNameClusterTwoRepSpecsWithAutoScalingAndSpecs: { + TestName: "TestAccMockableAdvancedCluster_removeBlocksFromConfig", + Step: 1, + VariableReplacments: map[string]string{ + "clusterName": unit.MockedClusterName, + "groupId": unit.MockedProjectID, + }, + SrcPackage: pkgAdvancedCluster, + DestPackage: pkgAdvancedClusterTPF, + }, + } { + srcTestdata := unit.RepoPath(path.Join(pkgRelPath, config.SrcPackage, "testdata")) + destTestdata := unit.RepoPath(path.Join(pkgRelPath, config.DestPackage, "testdata")) + ensureDir(t, destTestdata) + gitIgnorePath := path.Join(destTestdata, ".gitignore") + gitIgnoreExpectedContent := fmt.Sprintf("%s_*.yaml\n", importName) + if unit.FileExist(gitIgnorePath) { + gitIgnoreContent, err := os.ReadFile(gitIgnorePath) + require.NoError(t, err) + require.Contains(t, string(gitIgnoreContent), gitIgnoreExpectedContent, "Missing:\n%s in %s\nused to avoid pushing dynamic planCheck files", gitIgnoreExpectedContent, gitIgnorePath) + } else { + require.NoError(t, os.WriteFile(gitIgnorePath, []byte(gitIgnoreExpectedContent), 0o600)) + } + srcTestdataPath := path.Join(srcTestdata, config.TestName+".yaml") + destTestdataPath := path.Join(destTestdata, importName+".tmpl.yaml") + t.Logf("Converting %s step %d to %s", srcTestdataPath, config.Step, destTestdataPath) + createImportData(t, srcTestdataPath, destTestdataPath, importName, config.Step, config.VariableReplacments) + } +} + +func createImportData(t *testing.T, srcMockFile, destMockFile, importName string, stepNr int, newVars map[string]string) { + t.Helper() + destTestData, _ := path.Split(destMockFile) + require.True(t, strings.HasSuffix(destTestData, "/testdata/")) + ensureDir(t, destTestData) + destOutputDir := ensureDir(t, path.Join(destTestData, importName)) + + templateMockHTTPData := createImportMockData(t, srcMockFile, destOutputDir, stepNr, newVars) + require.NoError(t, templateMockHTTPData.UpdateVariablesIgnoreChanges(t, newVars)) + + templateYaml, err := unit.ConfigYaml(templateMockHTTPData) + require.NoError(t, err) + require.NoError(t, os.WriteFile(destMockFile, []byte(templateYaml), 0o600)) +} + +func createImportMockData(t *testing.T, srcMockFile, destOutputDir string, stepNr int, newVars map[string]string) *unit.MockHTTPData { + t.Helper() + data, err := unit.ParseTestDataConfigYAML(srcMockFile) + require.NoError(t, err) + relevantStep := data.Steps[stepNr-1] + getRequestsInStep := []unit.RequestInfo{} + for _, req := range relevantStep.RequestResponses { + if req.Method == "GET" { + getRequestsInStep = append(getRequestsInStep, req) + } + } + replaceVarsInConfig := func(config string) unit.Literal { + for key, oldValue := range data.Variables { + newValue, ok := newVars[key] + require.True(t, ok, "Missing variable %s in newVars", key) + config = strings.ReplaceAll(config, oldValue, newValue) + } + return unit.Literal(config) + } + templateMockHTTPData := unit.MockHTTPData{ + Steps: []unit.StepRequests{ + { + Config: replaceVarsInConfig(string(relevantStep.Config)), + }, + }, + Variables: newVars, + } + for _, req := range getRequestsInStep { + lastResponse := req.Responses[len(req.Responses)-1] + jsonFileName := strings.ReplaceAll(fmt.Sprintf("import_%s.json", req.IDShort()), "/", "_") + jsonFilePath := path.Join(destOutputDir, jsonFileName) + err = os.WriteFile(jsonFilePath, []byte(lastResponse.Text), 0o600) + require.NoError(t, err) + templateReqResponse := unit.RequestInfo{ + Path: req.Path, + Method: req.Method, + Version: req.Version, + Text: req.Text, + Responses: []unit.StatusText{ + { + Status: lastResponse.Status, + Text: jsonFileName, + }, + }, + } + templateMockHTTPData.Steps[0].RequestResponses = append(templateMockHTTPData.Steps[0].RequestResponses, templateReqResponse) + } + return &templateMockHTTPData +} + +func ensureDir(t *testing.T, dir string) string { + t.Helper() + if !unit.FileExist(dir) { + err := os.Mkdir(dir, 0o755) + require.NoError(t, err) + } + return dir +} diff --git a/internal/testutil/unit/http_mocker_round_tripper.go b/internal/testutil/unit/http_mocker_round_tripper.go index 95dae20732..ed57e86338 100644 --- a/internal/testutil/unit/http_mocker_round_tripper.go +++ b/internal/testutil/unit/http_mocker_round_tripper.go @@ -26,6 +26,7 @@ func NewMockRoundTripper(t *testing.T, config *MockHTTPDataConfig, data *MockHTT if config != nil { tracker.allowMissingRequests = config.AllowMissingRequests tracker.allowOutOfOrder = config.AllowOutOfOrder + tracker.manualRequestHandler = config.RequestHandler } for _, method := range []string{"GET", "POST", "PUT", "DELETE", "PATCH"} { myTransport.RegisterRegexpResponder(method, regexp.MustCompile(".*"), tracker.receiveRequest(method)) @@ -56,16 +57,20 @@ func newMockRoundTripper(t *testing.T, data *MockHTTPData) *MockRoundTripper { } } +type RequestHandler func(req *http.Request, method string) (*http.Response, error) +type ManualRequestHandler func(original RequestHandler, req *http.Request, method string) (*http.Response, error) + type MockRoundTripper struct { t *testing.T g *goldie.Goldie data *MockHTTPData usedResponses map[string]int foundsDiffs map[int]string - currentStepIndex int + manualRequestHandler ManualRequestHandler diffResponseIndex int reReadCounter int - mu sync.Mutex // as requests are in parallel, there is a chance of concurrent modification while reading/updating variables + currentStepIndex int + mu sync.Mutex allowMissingRequests bool allowOutOfOrder bool logRequests bool @@ -113,7 +118,7 @@ func (r *MockRoundTripper) initStep() error { return nil } for index, req := range step.DiffRequests { - err := r.g.Update(r.t, r.requestFilename(req.idShort(), index), []byte(req.Text)) + err := r.g.Update(r.t, r.requestFilename(req.IDShort(), index), []byte(req.Text)) if err != nil { return err } @@ -137,7 +142,7 @@ func (r *MockRoundTripper) nextDiffResponseIndex() { r.diffResponseIndex = 99999 } -func (r *MockRoundTripper) currentStep() *stepRequests { +func (r *MockRoundTripper) currentStep() *StepRequests { if r.currentStepIndex >= len(r.data.Steps) { return nil } @@ -156,7 +161,7 @@ func (r *MockRoundTripper) CheckStepRequests(_ *terraform.State) error { missingIndexes = append(missingIndexes, fmt.Sprintf("%d", req.Responses[missingResponse].ResponseIndex)) } missingIndexesStr := strings.Join(missingIndexes, ", ") - missingRequests = append(missingRequests, fmt.Sprintf("missing %d requests of %s (%s)", missingRequestsCount, req.idShort(), missingIndexesStr)) + missingRequests = append(missingRequests, fmt.Sprintf("missing %d requests of %s (%s)", missingRequestsCount, req.IDShort(), missingIndexesStr)) } } if r.allowMissingRequests { @@ -169,13 +174,13 @@ func (r *MockRoundTripper) CheckStepRequests(_ *terraform.State) error { missingDiffs := []string{} for i, req := range step.DiffRequests { if _, ok := r.foundsDiffs[i]; !ok { - missingDiffs = append(missingDiffs, fmt.Sprintf("missing diff request %s", req.idShort())) + missingDiffs = append(missingDiffs, fmt.Sprintf("missing diff request %s", req.IDShort())) } } assert.Empty(r.t, missingDiffs) for index, payload := range r.foundsDiffs { diff := step.DiffRequests[index] - filename := r.manualFilenameIfExist(diff.idShort(), index) + filename := r.manualFilenameIfExist(diff.IDShort(), index) r.t.Logf("checking diff %s", filename) payloadWithVars := useVars(r.data.Variables, payload) r.g.Assert(r.t, filename, []byte(payloadWithVars)) @@ -191,29 +196,36 @@ func (r *MockRoundTripper) receiveRequest(method string) func(req *http.Request) return func(req *http.Request) (*http.Response, error) { r.mu.Lock() defer r.mu.Unlock() - acceptHeader := req.Header.Get("Accept") - version, err := ExtractVersion(acceptHeader) - if err != nil { - return nil, err - } - _, payload, err := extractAndNormalizePayload(req.Body) - if r.logRequests { - r.t.Logf("received request\n %s %s?%s %s\n%s\n", method, req.URL.Path, req.URL.RawQuery, version, payload) - } - if err != nil { - return nil, err - } - text, status, err := r.matchRequest(method, version, payload, req.URL) - if err != nil { - return nil, err + if r.manualRequestHandler != nil { + return r.manualRequestHandler(r.requestHandler, req, method) } - if r.logRequests { - r.t.Logf("responding with\n%d\n%s\n", status, text) - } - response := httpmock.NewStringResponse(status, text) - response.Header.Set("Content-Type", fmt.Sprintf("application/vnd.atlas.%s+json;charset=utf-8", version)) - return response, nil + return r.requestHandler(req, method) + } +} + +func (r *MockRoundTripper) requestHandler(req *http.Request, method string) (*http.Response, error) { + acceptHeader := req.Header.Get("Accept") + version, err := ExtractVersion(acceptHeader) + if err != nil { + return nil, err + } + _, payload, err := extractAndNormalizePayload(req.Body) + if r.logRequests { + r.t.Logf("received request\n %s %s?%s %s\n%s\n", method, req.URL.Path, req.URL.RawQuery, version, payload) + } + if err != nil { + return nil, err + } + text, status, err := r.matchRequest(method, version, payload, req.URL) + if err != nil { + return nil, err + } + if r.logRequests { + r.t.Logf("responding with\n%d\n%s\n", status, text) } + response := httpmock.NewStringResponse(status, text) + response.Header.Set("Content-Type", fmt.Sprintf("application/vnd.atlas.%s+json;charset=utf-8", version)) + return response, nil } func (r *MockRoundTripper) matchRequest(method, version, payload string, reqURL *url.URL) (response string, statusCode int, err error) { step := r.currentStep() From ed85e27f1a96f69c59b71cee5059f2fb1bbf5b00 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Fri, 21 Mar 2025 09:00:57 +0000 Subject: [PATCH 02/39] fix broken unit test --- internal/testutil/mig/test_case_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/testutil/mig/test_case_test.go b/internal/testutil/mig/test_case_test.go index 1e30407bed..67137a5bfe 100644 --- a/internal/testutil/mig/test_case_test.go +++ b/internal/testutil/mig/test_case_test.go @@ -15,7 +15,7 @@ func TestConvertToMigration(t *testing.T) { var ( preCheckCalled = false checkDestroyCalled = false - config = "someTerraformConfig" + config = "resource \"dummy\" \"this\" {}" ) preCheck := func() { preCheckCalled = true From 9ffd580fcfe3131e6751c944df8f258521bd6a7a Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Fri, 21 Mar 2025 10:17:21 +0000 Subject: [PATCH 03/39] chore: Adds NewUnknownReplacements meant to replace `schemafunc.CopyUnknowns` logic and `schemafunc.NewAttributeChanges` --- internal/common/conversion/path_converter.go | 150 ++++++++ internal/common/conversion/path_helpers.go | 85 +++++ .../common/conversion/path_helpers_test.go | 50 +++ internal/common/conversion/type_conversion.go | 30 ++ internal/common/conversion/unknown.go | 42 +++ .../common/customplanmodifier/find_changes.go | 74 ++++ .../customplanmodifier/find_changes_test.go | 342 ++++++++++++++++++ .../customplanmodifier/plan_modify_differ.go | 303 ++++++++++++++++ .../customplanmodifier/unknown_replacement.go | 80 ++++ 9 files changed, 1156 insertions(+) create mode 100644 internal/common/conversion/path_converter.go create mode 100644 internal/common/conversion/path_helpers.go create mode 100644 internal/common/conversion/path_helpers_test.go create mode 100644 internal/common/conversion/unknown.go create mode 100644 internal/common/customplanmodifier/find_changes.go create mode 100644 internal/common/customplanmodifier/find_changes_test.go create mode 100644 internal/common/customplanmodifier/plan_modify_differ.go create mode 100644 internal/common/customplanmodifier/unknown_replacement.go diff --git a/internal/common/conversion/path_converter.go b/internal/common/conversion/path_converter.go new file mode 100644 index 0000000000..b9c6c9f3d0 --- /dev/null +++ b/internal/common/conversion/path_converter.go @@ -0,0 +1,150 @@ +package conversion + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type TPFSchema interface { + TypeAtTerraformPath(context.Context, *tftypes.AttributePath) (attr.Type, error) +} + +type TPFSrc interface { + GetAttribute(context.Context, path.Path, any) diag.Diagnostics +} + +// AttributePathValue retrieves the value for src (state/plan/config) @ attributePath with converted path.Path, schema is needed to get the correct types.XXX (String/Object/etc.) +func AttributePathValue(ctx context.Context, diags *diag.Diagnostics, attributePath *tftypes.AttributePath, src TPFSrc, schema TPFSchema) (attr.Value, path.Path) { + convertedPath, localDiags := AttributePath(ctx, attributePath, schema) + if localDiags.HasError() { + diags.Append(localDiags...) + return nil, convertedPath + } + attrType, err := schema.TypeAtTerraformPath(ctx, attributePath) + if err != nil { + diags.AddError("Unable to get type for attribute path", fmt.Sprintf("%s: %s", attributePath.String(), err)) + return nil, convertedPath + } + attrValue := attrType.ValueType(ctx) + if localDiags := src.GetAttribute(ctx, convertedPath, &attrValue); localDiags.HasError() { + diags.Append(localDiags...) + return nil, convertedPath + } + return attrValue, convertedPath +} + +// AttributePath similar to the internal function in TPF, but simpler interface as argument and less logging +func AttributePath(ctx context.Context, tfType *tftypes.AttributePath, schema TPFSchema) (path.Path, diag.Diagnostics) { + fwPath := path.Empty() + for tfTypeStepIndex, tfTypeStep := range tfType.Steps() { + currentTfTypeSteps := tfType.Steps()[:tfTypeStepIndex+1] + currentTfTypePath := tftypes.NewAttributePathWithSteps(currentTfTypeSteps) + attrType, err := schema.TypeAtTerraformPath(ctx, currentTfTypePath) + + if err != nil { + return path.Empty(), diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Attribute Path", + "An unexpected error occurred while trying to convert an attribute path. "+ + "This is an error in terraform-plugin-framework used by the provider. "+ + "Please report the following to the provider developers.\n\n"+ + // Since this is an error with the attribute path + // conversion, we cannot return a protocol path-based + // diagnostic. Returning a framework human-readable + // representation seems like the next best thing to do. + fmt.Sprintf("Attribute Path: %s\n", currentTfTypePath.String())+ + fmt.Sprintf("Original Error: %s", err), + ), + } + } + + fwStep, err := AttributePathStep(ctx, tfTypeStep, attrType) + + if err != nil { + return path.Empty(), diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Attribute Path", + "An unexpected error occurred while trying to convert an attribute path. "+ + "This is either an error in terraform-plugin-framework or a custom attribute type used by the provider. "+ + "Please report the following to the provider developers.\n\n"+ + // Since this is an error with the attribute path + // conversion, we cannot return a protocol path-based + // diagnostic. Returning a framework human-readable + // representation seems like the next best thing to do. + fmt.Sprintf("Attribute Path: %s\n", currentTfTypePath.String())+ + fmt.Sprintf("Original Error: %s", err), + ), + } + } + + // In lieu of creating a path.NewPathFromSteps function, this path + // building logic is inlined to not expand the path package API. + switch fwStep := fwStep.(type) { + case path.PathStepAttributeName: + fwPath = fwPath.AtName(string(fwStep)) + case path.PathStepElementKeyInt: + fwPath = fwPath.AtListIndex(int(fwStep)) + case path.PathStepElementKeyString: + fwPath = fwPath.AtMapKey(string(fwStep)) + case path.PathStepElementKeyValue: + fwPath = fwPath.AtSetValue(fwStep.Value) + default: + return fwPath, diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Attribute Path", + "An unexpected error occurred while trying to convert an attribute path. "+ + "This is an error in terraform-plugin-framework used by the provider. "+ + "Please report the following to the provider developers.\n\n"+ + // Since this is an error with the attribute path + // conversion, we cannot return a protocol path-based + // diagnostic. Returning a framework human-readable + // representation seems like the next best thing to do. + fmt.Sprintf("Attribute Path: %s\n", currentTfTypePath.String())+ + fmt.Sprintf("Original Error: unknown path.PathStep type: %#v", fwStep), + ), + } + } + } + + return fwPath, nil +} + +func AttributePathStep(ctx context.Context, tfType tftypes.AttributePathStep, attrType attr.Type) (path.PathStep, error) { + switch tfType := tfType.(type) { + case tftypes.AttributeName: + return path.PathStepAttributeName(string(tfType)), nil + case tftypes.ElementKeyInt: + return path.PathStepElementKeyInt(int64(tfType)), nil + case tftypes.ElementKeyString: + return path.PathStepElementKeyString(string(tfType)), nil + case tftypes.ElementKeyValue: + attrValue, err := Value(ctx, tftypes.Value(tfType), attrType) + + if err != nil { + return nil, fmt.Errorf("unable to create PathStepElementKeyValue from tftypes.Value: %w", err) + } + + return path.PathStepElementKeyValue{Value: attrValue}, nil + default: + return nil, fmt.Errorf("unknown tftypes.AttributePathStep: %#v", tfType) + } +} + +func Value(ctx context.Context, tfType tftypes.Value, attrType attr.Type) (attr.Value, error) { + if attrType == nil { + return nil, fmt.Errorf("unable to convert tftypes.Value (%s) to attr.Value: missing attr.Type", tfType.String()) + } + + attrValue, err := attrType.ValueFromTerraform(ctx, tfType) + + if err != nil { + return nil, fmt.Errorf("unable to convert tftypes.Value (%s) to attr.Value: %w", tfType.String(), err) + } + + return attrValue, nil +} diff --git a/internal/common/conversion/path_helpers.go b/internal/common/conversion/path_helpers.go new file mode 100644 index 0000000000..521504ce40 --- /dev/null +++ b/internal/common/conversion/path_helpers.go @@ -0,0 +1,85 @@ +package conversion + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/path" +) + +func LastPart(p path.Path) string { + parts := strings.Split(p.String(), ".") + return parts[len(parts)-1] +} + +func IsAttributeValueOnly(p path.Path) bool { + return IsMapIndex(p) || IsListIndex(p) || IsSetIndex(p) +} + +func IsListIndex(p path.Path) bool { + lastPart := LastPart(p) + if IsMapIndex(p) { + return false + } + return strings.HasSuffix(lastPart, "]") +} + +func IsMapIndex(p path.Path) bool { + lastPart := LastPart(p) + return strings.HasSuffix(lastPart, "\"]") +} + +func IsSetIndex(p path.Path) bool { + lastPart := LastPart(p) + return strings.Contains(lastPart, "[Value(") +} + +func HasPrefix(p, prefix path.Path) bool { + prefixString := prefix.String() + pString := p.String() + return strings.HasPrefix(pString, prefixString) +} + +func AttributeNameEquals(p path.Path, name string) bool { + noBrackets := StripSquareBrackets(p) + return noBrackets == name || strings.HasSuffix(noBrackets, fmt.Sprintf(".%s", name)) +} + +func AttributeName(p path.Path) string { + noBrackets := StripSquareBrackets(p) + parts := strings.Split(noBrackets, ".") + return parts[len(parts)-1] +} + +func AsAddedIndex(p path.Path) string { + parentString := p.ParentPath().ParentPath().String() + lastPart := LastPart(p) + indexWithSign := strings.Replace(lastPart, "[", "[+", 1) + if parentString == "" { + return indexWithSign + } + return parentString + "." + indexWithSign +} + +func AsRemovedIndex(p path.Path) string { + parentString := p.ParentPath().ParentPath().String() + lastPart := LastPart(p) + indexWithSign := strings.Replace(lastPart, "[", "[-", 1) + if parentString == "" { + return indexWithSign + } + return parentString + "." + indexWithSign +} + +func StripSquareBrackets(p path.Path) string { + if IsListIndex(p) { + return p.ParentPath().String() + } + if IsMapIndex(p) { + return p.ParentPath().String() + } + if IsSetIndex(p) { + return p.ParentPath().String() + } + return p.String() +} diff --git a/internal/common/conversion/path_helpers_test.go b/internal/common/conversion/path_helpers_test.go new file mode 100644 index 0000000000..dd192dd4a4 --- /dev/null +++ b/internal/common/conversion/path_helpers_test.go @@ -0,0 +1,50 @@ +package conversion_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" + "github.com/stretchr/testify/assert" +) + +func TestIsAttributeValueOnly(t *testing.T) { + assert.True(t, conversion.IsAttributeValueOnly(path.Root("replication_specs").AtListIndex(0))) + assert.True(t, conversion.IsAttributeValueOnly(path.Root("replication_specs").AtMapKey("myKey"))) + assert.True(t, conversion.IsAttributeValueOnly(path.Root("replication_specs").AtSetValue(types.StringValue("myKey")))) +} + +func TestAttributeNameEquals(t *testing.T) { + assert.True(t, conversion.AttributeNameEquals(path.Root("replication_specs").AtListIndex(0), "replication_specs")) + assert.True(t, conversion.AttributeNameEquals(path.Root("replication_specs").AtMapKey("myKey"), "replication_specs")) + assert.True(t, conversion.AttributeNameEquals(path.Root("replication_specs"), "replication_specs")) + assert.True(t, conversion.AttributeNameEquals(path.Root("replication_specs").AtListIndex(0).AtName("region_configs").AtListIndex(1), "region_configs")) + assert.False(t, conversion.AttributeNameEquals(path.Root("replication_specs").AtListIndex(0), "region_configs")) + assert.False(t, conversion.AttributeNameEquals(path.Root("replication_specs").AtMapKey("myKey"), "region_configs")) + assert.False(t, conversion.AttributeNameEquals(path.Root("replication_specs"), "region_configs")) +} + +func TestStripSquareBrackets(t *testing.T) { + assert.Equal(t, "replication_specs", conversion.StripSquareBrackets(path.Root("replication_specs").AtListIndex(0))) + assert.Equal(t, "replication_specs", conversion.StripSquareBrackets(path.Root("replication_specs").AtMapKey("myKey"))) + assert.Equal(t, "replication_specs", conversion.StripSquareBrackets(path.Root("replication_specs"))) +} + +func TestIndexMethods(t *testing.T) { + assert.True(t, conversion.IsListIndex(path.Root("replication_specs").AtListIndex(0))) + assert.False(t, conversion.IsListIndex(path.Root("replication_specs").AtName("region_configs"))) + assert.False(t, conversion.IsListIndex(path.Root("replication_specs").AtMapKey("region_configs"))) + assert.Equal(t, "replication_specs[+0]", conversion.AsAddedIndex(path.Root("replication_specs").AtListIndex(0))) + assert.Equal(t, "replication_specs[0].region_configs[+1]", conversion.AsAddedIndex(path.Root("replication_specs").AtListIndex(0).AtName("region_configs").AtListIndex(1))) + assert.Equal(t, "replication_specs[-1]", conversion.AsRemovedIndex(path.Root("replication_specs").AtListIndex(1))) + assert.Equal(t, "replication_specs[0].region_configs[-1]", conversion.AsRemovedIndex(path.Root("replication_specs").AtListIndex(0).AtName("region_configs").AtListIndex(1))) +} + +func TestPathMatches(t *testing.T) { + prefix := path.Root("replication_specs").AtListIndex(0) + assert.True(t, conversion.HasPrefix(path.Root("replication_specs").AtListIndex(0), prefix)) + assert.True(t, conversion.HasPrefix(path.Root("replication_specs").AtListIndex(0).AtName("region_configs"), prefix)) + assert.False(t, conversion.HasPrefix(path.Root("replication_specs").AtListIndex(1), prefix)) + assert.True(t, conversion.HasPrefix(path.Root("replication_specs").AtListIndex(0).AtName("region_configs").AtListIndex(1), path.Empty())) +} diff --git a/internal/common/conversion/type_conversion.go b/internal/common/conversion/type_conversion.go index b88f7cdf48..f8b20ca977 100644 --- a/internal/common/conversion/type_conversion.go +++ b/internal/common/conversion/type_conversion.go @@ -1,10 +1,14 @@ package conversion import ( + "context" "strings" "time" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) func SafeValue[T any](v *T) T { @@ -104,3 +108,29 @@ func NilForUnknownOrEmptyString(primitiveAttr types.String) *string { } return value } + +func TFModelList[T any](ctx context.Context, diags *diag.Diagnostics, input types.List) []T { + elements := make([]T, len(input.Elements())) + if localDiags := input.ElementsAs(ctx, &elements, false); len(localDiags) > 0 { + diags.Append(localDiags...) + return nil + } + return elements +} + +// TFModelObject returns nil if the Terraform object is null or unknown, or casting to T is not valid. However object attributes can be null or unknown. +func TFModelObject[T any](ctx context.Context, input types.Object) *T { + item := new(T) + if diags := input.As(ctx, item, basetypes.ObjectAsOptions{}); diags.HasError() { + return nil + } + return item +} + +func AsObjectValue[T any](ctx context.Context, t T, attrs map[string]attr.Type) types.Object { + objType, diagsLocal := types.ObjectValueFrom(ctx, attrs, t) + if diagsLocal.HasError() { + panic("failed to convert object to model") + } + return objType +} diff --git a/internal/common/conversion/unknown.go b/internal/common/conversion/unknown.go new file mode 100644 index 0000000000..4440429293 --- /dev/null +++ b/internal/common/conversion/unknown.go @@ -0,0 +1,42 @@ +package conversion + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types@v1.13.0 +func AsUnknownValue(ctx context.Context, value attr.Value) attr.Value { + switch v := value.(type) { + case types.List: + return types.ListUnknown(v.ElementType(ctx)) + case types.Object: + return types.ObjectUnknown(v.AttributeTypes(ctx)) + case types.Map: + return types.MapUnknown(v.ElementType(ctx)) + case types.Set: + return types.SetUnknown(v.ElementType(ctx)) + case types.Tuple: + return types.TupleUnknown(v.ElementTypes(ctx)) + case types.String: + return types.StringUnknown() + case types.Bool: + return types.BoolUnknown() + case types.Int64: + return types.Int64Unknown() + case types.Int32: + return types.Int32Unknown() + case types.Float64: + return types.Float64Unknown() + case types.Float32: + return types.Float32Unknown() + case types.Number: + return types.NumberUnknown() + case types.Dynamic: + return types.DynamicUnknown() + } + panic(fmt.Sprintf("Unknown value to create unknown: %v", value)) +} diff --git a/internal/common/customplanmodifier/find_changes.go b/internal/common/customplanmodifier/find_changes.go new file mode 100644 index 0000000000..7f54da37fa --- /dev/null +++ b/internal/common/customplanmodifier/find_changes.go @@ -0,0 +1,74 @@ +package customplanmodifier + +import ( + "fmt" + "strings" +) + +type AttributeChanges []string + +func (a AttributeChanges) LeafChanges() map[string]bool { + return a.leafChanges(true) +} + +func (a AttributeChanges) AttributeChanged(name string) bool { + changes := a.LeafChanges() + changed := changes[name] + return changed +} + +func (a AttributeChanges) KeepUnknown(attributeEffectedMapping map[string][]string) []string { + var keepUnknown []string + for attrChanged, affectedAttributes := range attributeEffectedMapping { + if a.AttributeChanged(attrChanged) { + keepUnknown = append(keepUnknown, attrChanged) + keepUnknown = append(keepUnknown, affectedAttributes...) + } + } + return keepUnknown +} + +// ListIndexChanged returns true if the list at the given index has changed, false if it was added or removed +func (a AttributeChanges) ListIndexChanged(name string, index int) bool { + leafChanges := a.leafChanges(false) + indexPath := fmt.Sprintf("%s[%d]", name, index) + return leafChanges[indexPath] +} + +// NestedListLenChanges accepts a fullPath, e.g., "replication_specs[0].region_configs" and returns true if the length of the nested list has changed +func (a AttributeChanges) NestedListLenChanges(fullPath string) bool { + addPrefix := fmt.Sprintf("%s[+", fullPath) + removePrefix := fmt.Sprintf("%s[-", fullPath) + for _, change := range a { + if strings.HasPrefix(change, addPrefix) || strings.HasPrefix(change, removePrefix) { + return true + } + } + return false +} + +func (a AttributeChanges) ListLenChanges(name string) bool { + leafChanges := a.leafChanges(false) + addPrefix := fmt.Sprintf("%s[+", name) + removePrefix := fmt.Sprintf("%s[-", name) + for change := range leafChanges { + if strings.HasPrefix(change, addPrefix) || strings.HasPrefix(change, removePrefix) { + return true + } + } + return false +} + +func (a AttributeChanges) leafChanges(removeIndex bool) map[string]bool { + leafChanges := map[string]bool{} + for _, change := range a { + var leaf string + parts := strings.Split(change, ".") + leaf = parts[len(parts)-1] + if removeIndex && strings.HasSuffix(leaf, "]") { + leaf = strings.Split(leaf, "[")[0] + } + leafChanges[leaf] = true + } + return leafChanges +} diff --git a/internal/common/customplanmodifier/find_changes_test.go b/internal/common/customplanmodifier/find_changes_test.go new file mode 100644 index 0000000000..b181e39adc --- /dev/null +++ b/internal/common/customplanmodifier/find_changes_test.go @@ -0,0 +1,342 @@ +package customplanmodifier_test + +import ( + "testing" + + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/customplanmodifier" + "github.com/stretchr/testify/assert" +) + +func TestAttributeChanges_LeafChanges(t *testing.T) { + tests := map[string]struct { + expected map[string]bool + changes customplanmodifier.AttributeChanges + }{ + "empty changes": { + changes: []string{}, + expected: map[string]bool{}, + }, + "single level changes": { + changes: []string{"name", "description"}, + expected: map[string]bool{ + "name": true, + "description": true, + }, + }, + "nested changes": { + changes: []string{"config.name", "settings.enabled"}, + expected: map[string]bool{ + "name": true, + "enabled": true, + }, + }, + "mixed level changes": { + changes: []string{"name", "config.type", "settings.auth.enabled"}, + expected: map[string]bool{ + "name": true, + "type": true, + "enabled": true, + }, + }, + "list changes": { + changes: []string{"replication_specs", "replication_specs[0]", "replication_specs[0].zone_name"}, + expected: map[string]bool{ + "replication_specs": true, + "zone_name": true, + }, + }, + "nested list changes": { + changes: []string{"replication_specs", "replication_specs[0]", "replication_specs[0].region_configs", "replication_specs[0].region_configs[0].region_name"}, + expected: map[string]bool{ + "replication_specs": true, + "region_name": true, + "region_configs": true, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + actual := tc.changes.LeafChanges() + assert.Equal(t, tc.expected, actual) + }) + } +} + +func TestAttributeChanges_AttributeChanged(t *testing.T) { + tests := map[string]struct { + attr string + changes customplanmodifier.AttributeChanges + expected bool + }{ + "match found": { + changes: []string{"name", "description"}, + attr: "name", + expected: true, + }, + "match not found": { + changes: []string{"name", "description"}, + attr: "type", + expected: false, + }, + "nested attribute match": { + changes: []string{"config.name", "settings.enabled"}, + attr: "name", + expected: true, + }, + "empty changes": { + changes: []string{}, + attr: "name", + expected: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + actual := tc.changes.AttributeChanged(tc.attr) + assert.Equal(t, tc.expected, actual) + }) + } +} +func TestAttributeChanges_KeepUnknown(t *testing.T) { + tests := map[string]struct { + changes customplanmodifier.AttributeChanges + attributeEffectedMapping map[string][]string + expectedKeepUnknownAttrs []string + }{ + "empty mapping": { + changes: []string{"name", "description"}, + attributeEffectedMapping: map[string][]string{}, + expectedKeepUnknownAttrs: []string{}, + }, + "single mapping with match": { + changes: []string{"name", "config.type"}, + attributeEffectedMapping: map[string][]string{ + "name": {"id", "status"}, + }, + expectedKeepUnknownAttrs: []string{"name", "id", "status"}, + }, + "multiple mappings with matches": { + changes: []string{"name", "type", "config.value"}, + attributeEffectedMapping: map[string][]string{ + "name": {"id"}, + "type": {"category", "version"}, + }, + expectedKeepUnknownAttrs: []string{"name", "id", "type", "category", "version"}, + }, + "no matching changes": { + changes: []string{"description", "status"}, + attributeEffectedMapping: map[string][]string{ + "name": {"id"}, + "type": {"category"}, + }, + expectedKeepUnknownAttrs: []string{}, + }, + "nested attribute changes": { + changes: []string{"config.name", "settings.enabled"}, + attributeEffectedMapping: map[string][]string{ + "name": {"id", "status"}, + "enabled": {"auth_status"}, + }, + expectedKeepUnknownAttrs: []string{"name", "id", "status", "enabled", "auth_status"}, + }, + "list attribute changes": { + changes: []string{"replication_specs[0].zone_name"}, + attributeEffectedMapping: map[string][]string{ + "zone_name": {"priority", "region"}, + }, + expectedKeepUnknownAttrs: []string{"zone_name", "priority", "region"}, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + actual := tc.changes.KeepUnknown(tc.attributeEffectedMapping) + assert.ElementsMatch(t, tc.expectedKeepUnknownAttrs, actual) + }) + } +} +func TestAttributeChanges_ListLenChanges(t *testing.T) { + tests := map[string]struct { + name string + changes customplanmodifier.AttributeChanges + expected bool + }{ + "empty changes": { + name: "replication_specs", + changes: []string{}, + expected: false, + }, + "no list changes": { + name: "replication_specs", + changes: []string{"name", "description"}, + expected: false, + }, + "add element": { + name: "replication_specs", + changes: []string{"replication_specs[+0]", "replication_specs[0].zone_name"}, + expected: true, + }, + "remove element": { + name: "replication_specs", + changes: []string{"replication_specs[-1]", "replication_specs[0].zone_name"}, + expected: true, + }, + "modify without length change": { + name: "replication_specs", + changes: []string{"replication_specs[0].zone_name", "replication_specs[0].priority"}, + expected: false, + }, + "multiple list operations": { + name: "replication_specs", + changes: []string{"replication_specs[+0]", "replication_specs[-1]", "replication_specs[0].zone_name"}, + expected: true, + }, + "different list name": { + name: "other_list", + changes: []string{"replication_specs[+0]", "replication_specs[-1]"}, + expected: false, + }, + "nested list": { + name: "region_configs", + changes: []string{"replication_specs.region_configs[+0]", "replication_specs.region_configs[0].region_name"}, + expected: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + actual := tc.changes.ListLenChanges(tc.name) + assert.Equal(t, tc.expected, actual) + }) + } +} +func TestAttributeChanges_ListIndexChanged(t *testing.T) { + tests := map[string]struct { + name string + changes customplanmodifier.AttributeChanges + index int + expected bool + }{ + "empty changes": { + name: "replication_specs", + index: 0, + changes: []string{}, + expected: false, + }, + "list element modified": { + name: "replication_specs", + index: 0, + changes: []string{"replication_specs[0]", "replication_specs[0].zone_name"}, + expected: true, + }, + "list element added": { + name: "replication_specs", + index: 0, + changes: []string{"replication_specs[+0]"}, + expected: false, + }, + "list element removed": { + name: "replication_specs", + index: 1, + changes: []string{"replication_specs[-1]"}, + expected: false, + }, + "different index": { + name: "replication_specs", + index: 1, + changes: []string{"replication_specs[0]", "replication_specs[0].zone_name"}, + expected: false, + }, + "different list name": { + name: "other_specs", + index: 0, + changes: []string{"replication_specs[0]", "replication_specs[0].zone_name"}, + expected: false, + }, + "nested list": { + name: "region_configs", + index: 0, + changes: []string{"replication_specs.region_configs[0]", "replication_specs.region_configs[0].priority"}, + expected: true, + }, + "nested list false": { + name: "region_configs", + index: 1, + changes: []string{"replication_specs.region_configs[0]", "replication_specs.region_configs[0].priority"}, + expected: false, + }, + "index beyond bounds": { + name: "replication_specs", + index: 5, + changes: []string{"replication_specs[0]", "replication_specs[1]"}, + expected: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + actual := tc.changes.ListIndexChanged(tc.name, tc.index) + assert.Equal(t, tc.expected, actual) + }) + } +} +func TestAttributeChanges_NestedListLenChanges(t *testing.T) { + tests := map[string]struct { + fullPath string + changes customplanmodifier.AttributeChanges + expected bool + }{ + "empty changes": { + fullPath: "replication_specs.region_configs", + changes: []string{}, + expected: false, + }, + "no nested list changes": { + fullPath: "replication_specs.region_configs", + changes: []string{"name", "description", "replication_specs.zone_name"}, + expected: false, + }, + "add nested element": { + fullPath: "replication_specs.region_configs", + changes: []string{"replication_specs.region_configs[+0]", "replication_specs.region_configs.priority"}, + expected: true, + }, + "add nested element add different index should be false": { + fullPath: "replication_specs[0].region_configs", + changes: []string{"replication_specs[1].region_configs[+0]"}, + expected: false, + }, + "remove nested element": { + fullPath: "replication_specs.region_configs", + changes: []string{"replication_specs.region_configs[-1]", "replication_specs.region_configs.region_name"}, + expected: true, + }, + "mixed list operations": { + fullPath: "replication_specs.region_configs", + changes: []string{ + "replication_specs.region_configs[+0]", + "replication_specs.region_configs[-1]", + "replication_specs.region_configs.priority", + }, + expected: true, + }, + "different path": { + fullPath: "other.configs", + changes: []string{"replication_specs.region_configs[+0]", "replication_specs.region_configs[-1]"}, + expected: false, + }, + "multiple nested levels": { + fullPath: "replication_specs.region_configs.zones", + changes: []string{"replication_specs.region_configs.zones[+0]", "replication_specs.region_configs[0].zones.name"}, + expected: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + actual := tc.changes.NestedListLenChanges(tc.fullPath) + assert.Equal(t, tc.expected, actual) + }) + } +} diff --git a/internal/common/customplanmodifier/plan_modify_differ.go b/internal/common/customplanmodifier/plan_modify_differ.go new file mode 100644 index 0000000000..413ca4d682 --- /dev/null +++ b/internal/common/customplanmodifier/plan_modify_differ.go @@ -0,0 +1,303 @@ +package customplanmodifier + +import ( + "context" + "fmt" + "maps" + "slices" + "sort" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" +) + +func NewPlanModifyDiffer(ctx context.Context, req *resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse, schema conversion.TPFSchema) *PlanModifyDiffer { + diags := &resp.Diagnostics + diffStatePlan, err := req.State.Raw.Diff(resp.Plan.Raw) + if err != nil { + diags.AddError("Error diffing state and plan", err.Error()) + return nil + } + diffStateConfig, err := req.State.Raw.Diff(req.Config.Raw) + if err != nil { + diags.AddError("Error diffing state and config", err.Error()) + return nil + } + + attributeChanges := findChanges(ctx, diffStatePlan, diags, schema) + tflog.Info(ctx, fmt.Sprintf("Attribute changes: %s\n", strings.Join(attributeChanges, "\n"))) + return &PlanModifyDiffer{ + req: req, + resp: resp, + stateConfigDiff: diffStateConfig, + statePlanDiff: diffStatePlan, + schema: schema, + AttributeChanges: attributeChanges, + PlanFullyKnown: req.Plan.Raw.IsFullyKnown(), + } +} + +type PlanModifyDiffer struct { + schema conversion.TPFSchema + AttributeChanges AttributeChanges + req *resource.ModifyPlanRequest + resp *resource.ModifyPlanResponse + stateConfigDiff []tftypes.ValueDiff + statePlanDiff []tftypes.ValueDiff + PlanFullyKnown bool +} + +func (d *PlanModifyDiffer) ParentRemoved(p path.Path) bool { + for { + parent := p.ParentPath() + if parent.Equal(path.Empty()) { + return false + } + if slices.Contains(d.AttributeChanges, conversion.AsRemovedIndex(parent)) { + return true + } + p = parent + } +} + +func (d *PlanModifyDiffer) Diff(ctx context.Context, diags *diag.Diagnostics, schema conversion.TPFSchema, isConfig bool) string { + diffList := d.statePlanDiff + if isConfig { + diffList = d.stateConfigDiff + } + diffPaths := make([]string, len(diffList)) + for i, diff := range diffList { + p, localDiags := conversion.AttributePath(ctx, diff.Path, schema) + if localDiags.HasError() { + diags.Append(localDiags...) + return "" + } + diffPaths[i] = p.String() + } + sort.Strings(diffPaths) + name := "plan" + if isConfig { + name = "config" + } + return fmt.Sprintf("DifferStateTo%s\n", name) + strings.Join(diffPaths, "\n") +} + +type UnknownInfo struct { + StateValue attr.Value + UnknownValue attr.Value + AttributeName string + Path path.Path +} + +func (d *PlanModifyDiffer) Unknowns(ctx context.Context, diags *diag.Diagnostics) map[string]UnknownInfo { + unknowns := map[string]UnknownInfo{} + schema := d.schema + for _, diff := range d.statePlanDiff { + stateValue, tpfPath := conversion.AttributePathValue(ctx, diags, diff.Path, d.req.State, schema) + if d.ParentRemoved(tpfPath) { + continue + } + planValue, _ := conversion.AttributePathValue(ctx, diags, diff.Path, d.req.Plan, schema) + if planValue == nil || !planValue.IsUnknown() { + continue + } + unknowns[tpfPath.String()] = UnknownInfo{ + Path: tpfPath, + StateValue: stateValue, + UnknownValue: planValue, + AttributeName: conversion.AttributeName(tpfPath), + } + } + return unknowns +} + +func (d *PlanModifyDiffer) UseStateForUnknown(ctx context.Context, diags *diag.Diagnostics, keepUnknown []string, prefix path.Path) { + // The diff is sorted by the path length, for example read_only_spec is processed before read_only_spec.disk_size_gb + schema := d.schema + for _, diff := range d.statePlanDiff { + stateValue, tpfPath := conversion.AttributePathValue(ctx, diags, diff.Path, d.req.State, schema) + if !conversion.HasPrefix(tpfPath, prefix) || stateValue == nil || conversion.IsAttributeValueOnly(tpfPath) { + continue + } + if d.ParentRemoved(tpfPath) { + continue + } + planValue, _ := conversion.AttributePathValue(ctx, diags, diff.Path, d.req.Plan, schema) + if planValue == nil || !planValue.IsUnknown() { + continue + } + if keepUnknownCall(diff.Path, keepUnknown) { + tflog.Info(ctx, fmt.Sprintf("Keeping unknown value in plan @ %s", tpfPath.String())) + unknownValue := conversion.AsUnknownValue(ctx, stateValue) + UpdatePlanValue(ctx, diags, d, tpfPath, unknownValue) + } else { + tflog.Info(ctx, fmt.Sprintf("Replacing unknown value in plan @ %s", tpfPath.String())) + UpdatePlanValue(ctx, diags, d, tpfPath, stateValue) + d.ensureKeepUnknownRespected(ctx, diags, tpfPath, stateValue, keepUnknown) + } + } +} + +func (d *PlanModifyDiffer) ensureKeepUnknownRespected(ctx context.Context, diags *diag.Diagnostics, tpfPath path.Path, value attr.Value, keepUnknown []string) { + valueObject, ok := value.(types.Object) + if value.IsNull() || value.IsUnknown() || !ok { + return + } + for key, childValue := range valueObject.Attributes() { + if slices.Contains(keepUnknown, key) && !childValue.IsUnknown() { + childPath := tpfPath.AtName(key) + tflog.Info(ctx, fmt.Sprintf("Keeping unknown value in plan @ %s", childPath.String())) + unknownValue := conversion.AsUnknownValue(ctx, childValue) + UpdatePlanValue(ctx, diags, d, childPath, unknownValue) + } + } +} + +func ReadConfigStructValue[T any](ctx context.Context, d *PlanModifyDiffer, p path.Path) *T { + return readSrcStructValue[T](ctx, d.req.Config, p) +} + +func ReadPlanStructValue[T any](ctx context.Context, d *PlanModifyDiffer, p path.Path) *T { + return readSrcStructValue[T](ctx, d.req.Plan, p) +} + +func ReadStateStructValue[T any](ctx context.Context, d *PlanModifyDiffer, p path.Path) *T { + return readSrcStructValue[T](ctx, d.req.State, p) +} + +func readSrcStructValue[T any](ctx context.Context, src conversion.TPFSrc, p path.Path) *T { + var obj types.Object + if localDiags := src.GetAttribute(ctx, p, &obj); localDiags.HasError() { + return nil + } + if obj.IsNull() || obj.IsUnknown() { + return nil + } + return conversion.TFModelObject[T](ctx, obj) +} + +func UpdatePlanValue[T attr.Value](ctx context.Context, diags *diag.Diagnostics, d *PlanModifyDiffer, p path.Path, value T) { + if localDiags := d.resp.Plan.SetAttribute(ctx, p, value); localDiags.HasError() { + diags.Append(localDiags...) + } +} + +type DiffTPF[T any] struct { + Plan *T + State *T + Config *T + Path path.Path + PlanUnknown bool + ConfigUnknown bool +} + +func (d *DiffTPF[T]) Removed() bool { + return d.State != nil && d.Config == nil +} + +func (d *DiffTPF[T]) Changed() bool { + return d.State != nil && d.Config != nil +} + +func (d *DiffTPF[T]) PlanOrStateValue() *T { + if d.Plan != nil { + return d.Plan + } + return d.State +} + +func findChanges(ctx context.Context, diff []tftypes.ValueDiff, diags *diag.Diagnostics, schema conversion.TPFSchema) AttributeChanges { + changes := map[string]bool{} + addChangeAndParentChanges := func(change string) { + changes[change] = true + parts := strings.Split(change, ".") + for i := range parts[:len(parts)-1] { + changes[strings.Join(parts[:len(parts)-1-i], ".")] = true + } + } + for _, d := range diff { + p, localDiags := conversion.AttributePath(ctx, d.Path, schema) + if conversion.IsListIndex(p) { + if d.Value1 == nil { + addChangeAndParentChanges(conversion.AsAddedIndex(p)) + } + if d.Value2 == nil { + addChangeAndParentChanges(conversion.AsRemovedIndex(p)) + } + } + if d.Value2 != nil && d.Value2.IsKnown() && !d.Value2.IsNull() { + if localDiags.HasError() { + diags.Append(localDiags...) + continue + } + addChangeAndParentChanges(p.String()) + } + } + return slices.Sorted(maps.Keys(changes)) +} + +func keepUnknownCall(aPath *tftypes.AttributePath, keepUnknown []string) bool { + for _, step := range aPath.Steps() { + if aName, ok := step.(tftypes.AttributeName); ok { + if slices.Contains(keepUnknown, string(aName)) { + return true + } + } + } + return false +} + +func StateConfigDiffs[T any](ctx context.Context, diags *diag.Diagnostics, d *PlanModifyDiffer, name string, checkNestedAttributes bool) []DiffTPF[T] { + earlyReturn := func(localDiags diag.Diagnostics) []DiffTPF[T] { + diags.Append(localDiags...) + return nil + } + var diffs []DiffTPF[T] + usedPaths := map[string]bool{} + + for _, diff := range d.stateConfigDiff { + p, localDiags := conversion.AttributePath(ctx, diff.Path, d.schema) + if localDiags.HasError() { + return earlyReturn(localDiags) + } + // Never show diff if the parent is removed, for example replication_specs[0] is removed and replication_specs[0].region_configs[0].electable_spec is changed + if d.ParentRemoved(p) { + continue + } + if checkNestedAttributes { + parent := p.ParentPath() + if conversion.AttributeNameEquals(parent, name) { + p = parent + } + } + if _, ok := usedPaths[p.String()]; ok { + continue // already returned + } + if conversion.AttributeNameEquals(p, name) { + usedPaths[p.String()] = true + var configObj, planObj types.Object + if d2 := d.req.Config.GetAttribute(ctx, p, &configObj); d2.HasError() { + return earlyReturn(d2) + } + if d3 := d.req.Plan.GetAttribute(ctx, p, &planObj); d3.HasError() { + return earlyReturn(d3) + } + diffs = append(diffs, DiffTPF[T]{ + Path: p, + State: ReadStateStructValue[T](ctx, d, p), + Config: ReadConfigStructValue[T](ctx, d, p), + Plan: ReadPlanStructValue[T](ctx, d, p), + PlanUnknown: planObj.IsUnknown(), + ConfigUnknown: configObj.IsUnknown(), + }) + } + } + return diffs +} diff --git a/internal/common/customplanmodifier/unknown_replacement.go b/internal/common/customplanmodifier/unknown_replacement.go new file mode 100644 index 0000000000..494445867f --- /dev/null +++ b/internal/common/customplanmodifier/unknown_replacement.go @@ -0,0 +1,80 @@ +package customplanmodifier + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" +) + +func NewUnknownReplacements[ResourceInfo any](ctx context.Context, req *resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse, schema conversion.TPFSchema, info ResourceInfo) *UnknownReplacments[ResourceInfo] { + return &UnknownReplacments[ResourceInfo]{ + Differ: NewPlanModifyDiffer(ctx, req, resp, schema), + Info: info, + Replacements: make(map[string]UnknownReplacementCall[ResourceInfo]), + } +} + +type UnknownReplacementCall[ResourceInfo any] func(ctx context.Context, stateValue ParsedAttrValue, req *UnknownReplacementRequest[ResourceInfo]) attr.Value + +type UnknownReplacments[ResourceInfo any] struct { + Differ *PlanModifyDiffer + Replacements map[string]UnknownReplacementCall[ResourceInfo] + Info ResourceInfo +} + +// ParsedAttrValue is a wrapper around attr.Value that provides type-safe accessors to support using the same signature of functions. +type ParsedAttrValue struct { + Value attr.Value +} + +func (p *ParsedAttrValue) AsString() types.String { + return p.Value.(types.String) +} + +func (p *ParsedAttrValue) AsObject() types.Object { + return p.Value.(types.Object) +} + +type UnknownReplacementRequest[ResourceInfo any] struct { + Info ResourceInfo + Unknown attr.Value + Differ *PlanModifyDiffer + Path path.Path + Changes AttributeChanges +} + +func (u *UnknownReplacments[ResourceInfo]) AddReplacement(name string, call UnknownReplacementCall[ResourceInfo]) { + // todo: Validate the name in the schema + // todo: Validate the name is not already in the replacements + u.Replacements[name] = call +} + +func (u *UnknownReplacments[ResourceInfo]) ApplyReplacments(ctx context.Context, diags *diag.Diagnostics) { + for strPath, unknown := range u.Differ.Unknowns(ctx, diags) { + replacer, ok := u.Replacements[unknown.AttributeName] + if !ok { + continue + } + req := &UnknownReplacementRequest[ResourceInfo]{ + Info: u.Info, + Path: unknown.Path, + Differ: u.Differ, + Changes: u.Differ.AttributeChanges, + Unknown: unknown.UnknownValue, + } + response := replacer(ctx, ParsedAttrValue{Value: unknown.StateValue}, req) + if response.IsUnknown() { + tflog.Info(ctx, fmt.Sprintf("Keeping unknown value in plan @ %s", strPath)) + } else { + tflog.Info(ctx, fmt.Sprintf("Replacing unknown value in plan @ %s", strPath)) + UpdatePlanValue(ctx, diags, u.Differ, unknown.Path, response) + } + } +} From 10d8a82f135ac59df34e8df274f4aed3c8afa7be Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Fri, 21 Mar 2025 10:18:53 +0000 Subject: [PATCH 04/39] refactor: Handle unknownReplacement logic for `mongo_db_version` using the new `ReplaceUnknown` signature --- .../advancedclustertpf/plan_modifier.go | 6 +-- .../advancedclustertpf/plan_modifier2.go | 48 +++++++++++++++++++ .../service/advancedclustertpf/resource.go | 2 +- 3 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 internal/service/advancedclustertpf/plan_modifier2.go diff --git a/internal/service/advancedclustertpf/plan_modifier.go b/internal/service/advancedclustertpf/plan_modifier.go index 8e32c0ffe1..387ddb0d54 100644 --- a/internal/service/advancedclustertpf/plan_modifier.go +++ b/internal/service/advancedclustertpf/plan_modifier.go @@ -15,9 +15,9 @@ import ( var ( // Change mappings uses `attribute_name`, it doesn't care about the nested level. attributeRootChangeMapping = map[string][]string{ - "disk_size_gb": {}, // disk_size_gb can be change at any level/spec - "replication_specs": {}, - "mongo_db_major_version": {"mongo_db_version"}, + "disk_size_gb": {}, // disk_size_gb can be change at any level/spec + "replication_specs": {}, + // "mongo_db_major_version": {"mongo_db_version"}, // Using new plan modifier logic to test this "tls_cipher_config_mode": {"custom_openssl_cipher_config_tls12"}, "cluster_type": {"config_server_management_mode", "config_server_type"}, // computed values of config server change when REPLICA_SET changes to SHARDED } diff --git a/internal/service/advancedclustertpf/plan_modifier2.go b/internal/service/advancedclustertpf/plan_modifier2.go new file mode 100644 index 0000000000..32d866d291 --- /dev/null +++ b/internal/service/advancedclustertpf/plan_modifier2.go @@ -0,0 +1,48 @@ +package advancedclustertpf + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/customplanmodifier" +) + +var attributePlanModifiers = map[string]customplanmodifier.UnknownReplacementCall[PlanModifyResourceInfo]{ + "mongo_db_version": mongoDBVersionReplaceUnknown, +} + +func mongoDBVersionReplaceUnknown(ctx context.Context, state customplanmodifier.ParsedAttrValue, req *customplanmodifier.UnknownReplacementRequest[PlanModifyResourceInfo]) attr.Value { + if req.Changes.AttributeChanged("mongo_db_version") { + return req.Unknown + } + return state.Value +} + +type PlanModifyResourceInfo struct { + AutoScalingComputedUsed bool + AutoScalingDiskUsed bool + isShardingConfigUpgrade bool +} + +func unknownReplacements(ctx context.Context, req *resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var plan, state TFModel + diags := &resp.Diagnostics + diags.Append(req.Plan.Get(ctx, &plan)...) + diags.Append(req.State.Get(ctx, &state)...) + if diags.HasError() { + return + } + computedUsed, diskUsed := autoScalingUsed(ctx, diags, &state, &plan) + shardingConfigUpgrade := isShardingConfigUpgrade(ctx, &state, &plan, diags) + info := PlanModifyResourceInfo{ + AutoScalingComputedUsed: computedUsed, + AutoScalingDiskUsed: diskUsed, + isShardingConfigUpgrade: shardingConfigUpgrade, + } + unknownReplacements := customplanmodifier.NewUnknownReplacements(ctx, req, resp, resourceSchema(ctx), info) + for attrName, replacer := range attributePlanModifiers { + unknownReplacements.AddReplacement(attrName, replacer) + } + unknownReplacements.ApplyReplacments(ctx, diags) +} diff --git a/internal/service/advancedclustertpf/resource.go b/internal/service/advancedclustertpf/resource.go index a264d08f38..0dbe450b16 100644 --- a/internal/service/advancedclustertpf/resource.go +++ b/internal/service/advancedclustertpf/resource.go @@ -108,7 +108,7 @@ func (r *rs) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, res if diags.HasError() { return } - + unknownReplacements(ctx, &req, resp) useStateForUnknowns(ctx, diags, &state, &plan) if diags.HasError() { return From 19a9f900a8474047e10974a6de89b312fb082a71 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Fri, 21 Mar 2025 10:43:42 +0000 Subject: [PATCH 05/39] test: Support testing root level plan modifier changes --- .../advancedclustertpf/plan_modifier.go | 2 +- .../advancedclustertpf/plan_modifier2.go | 2 +- .../advancedclustertpf/plan_modifier_test.go | 22 ++++ .../service/advancedclustertpf/resource.go | 2 +- .../advancedclustertpf/testdata/.gitignore | 1 + .../ClusterReplicasetOneRegion.tmpl.yaml | 89 +++++++++++++++ ..._groups_{groupId}_clusters_2024-08-05.json | 106 ++++++++++++++++++ ...Id}_clusters_{clusterName}_2023-02-01.json | 92 +++++++++++++++ ...Id}_clusters_{clusterName}_2024-08-05.json | 95 ++++++++++++++++ ..._{clusterName}_processArgs_2023-01-01.json | 19 ++++ ..._{clusterName}_processArgs_2024-08-05.json | 17 +++ ...ontainers?providerName=AWS_2023-01-01.json | 19 ++++ ...ups_{groupId}_flexClusters_2024-11-13.json | 10 ++ .../ClusterReplicasetOneRegion/main.tf | 26 +++++ .../main_backup_enabled.tf | 27 +++++ .../main_mongo_db_major_version_changed.tf | 27 +++++ .../testutil/unit/http_mocker_plan_checks.go | 8 +- .../unit/http_mocker_plan_checks_test.go | 27 +++-- 18 files changed, 578 insertions(+), 13 deletions(-) create mode 100644 internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion.tmpl.yaml create mode 100644 internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_2024-08-05.json create mode 100644 internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2023-02-01.json create mode 100644 internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2024-08-05.json create mode 100644 internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2023-01-01.json create mode 100644 internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2024-08-05.json create mode 100644 internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_containers?providerName=AWS_2023-01-01.json create mode 100644 internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_flexClusters_2024-11-13.json create mode 100644 internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main.tf create mode 100644 internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_backup_enabled.tf create mode 100644 internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_mongo_db_major_version_changed.tf diff --git a/internal/service/advancedclustertpf/plan_modifier.go b/internal/service/advancedclustertpf/plan_modifier.go index 387ddb0d54..6f7cc7fda8 100644 --- a/internal/service/advancedclustertpf/plan_modifier.go +++ b/internal/service/advancedclustertpf/plan_modifier.go @@ -62,7 +62,7 @@ func useStateForUnknowns(ctx context.Context, diags *diag.Diagnostics, state, pl return } attributeChanges := schemafunc.NewAttributeChanges(ctx, state, plan) - keepUnknown := []string{"connection_strings", "state_name"} // Volatile attributes, should not be copied from state + keepUnknown := []string{"connection_strings", "state_name", "mongo_db_version"} // Volatile attributes, should not be copied from state keepUnknown = append(keepUnknown, attributeChanges.KeepUnknown(attributeRootChangeMapping)...) keepUnknown = append(keepUnknown, determineKeepUnknownsAutoScaling(ctx, diags, state, plan)...) schemafunc.CopyUnknowns(ctx, state, plan, keepUnknown, nil) diff --git a/internal/service/advancedclustertpf/plan_modifier2.go b/internal/service/advancedclustertpf/plan_modifier2.go index 32d866d291..a5dc147ef7 100644 --- a/internal/service/advancedclustertpf/plan_modifier2.go +++ b/internal/service/advancedclustertpf/plan_modifier2.go @@ -13,7 +13,7 @@ var attributePlanModifiers = map[string]customplanmodifier.UnknownReplacementCal } func mongoDBVersionReplaceUnknown(ctx context.Context, state customplanmodifier.ParsedAttrValue, req *customplanmodifier.UnknownReplacementRequest[PlanModifyResourceInfo]) attr.Value { - if req.Changes.AttributeChanged("mongo_db_version") { + if req.Changes.AttributeChanged("mongo_db_major_version") { return req.Unknown } return state.Value diff --git a/internal/service/advancedclustertpf/plan_modifier_test.go b/internal/service/advancedclustertpf/plan_modifier_test.go index 9c63cd6eaa..4dfdf247b9 100644 --- a/internal/service/advancedclustertpf/plan_modifier_test.go +++ b/internal/service/advancedclustertpf/plan_modifier_test.go @@ -56,3 +56,25 @@ func TestMockPlanChecks_ClusterTwoRepSpecsWithAutoScalingAndSpecs(t *testing.T) }) } } + +func TestMockPlanChecks_ClusterReplicasetOneRegion(t *testing.T) { + var ( + baseConfig = unit.NewMockPlanChecksConfig(t, &mockConfig, unit.ImportNameClusterReplicasetOneRegion) + resourceName = baseConfig.ResourceName + ) + testCases := map[string][]plancheck.PlanCheck{ + "mongo_db_major_version_changed": { + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + plancheck.ExpectUnknownValue(resourceName, tfjsonpath.New("mongo_db_version")), + }, + "backup_enabled": { + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("mongo_db_version"), knownvalue.StringExact("8.0.5")), + }, + } + for name, checks := range testCases { + t.Run(name, func(t *testing.T) { + unit.MockPlanChecksAndRun(t, baseConfig.WithNameAndChecks(name, checks)) + }) + } +} diff --git a/internal/service/advancedclustertpf/resource.go b/internal/service/advancedclustertpf/resource.go index 0dbe450b16..092c2ff63b 100644 --- a/internal/service/advancedclustertpf/resource.go +++ b/internal/service/advancedclustertpf/resource.go @@ -108,12 +108,12 @@ func (r *rs) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, res if diags.HasError() { return } - unknownReplacements(ctx, &req, resp) useStateForUnknowns(ctx, diags, &state, &plan) if diags.HasError() { return } diags.Append(resp.Plan.Set(ctx, plan)...) + unknownReplacements(ctx, &req, resp) } func (r *rs) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { diff --git a/internal/service/advancedclustertpf/testdata/.gitignore b/internal/service/advancedclustertpf/testdata/.gitignore index 662787149d..89b68a3a20 100644 --- a/internal/service/advancedclustertpf/testdata/.gitignore +++ b/internal/service/advancedclustertpf/testdata/.gitignore @@ -1 +1,2 @@ ClusterTwoRepSpecsWithAutoScalingAndSpecs_*.yaml +ClusterReplicasetOneRegion_*.yaml diff --git a/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion.tmpl.yaml b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion.tmpl.yaml new file mode 100644 index 0000000000..7e26b2d059 --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion.tmpl.yaml @@ -0,0 +1,89 @@ +variables: + clusterName: mocked-cluster + groupId: "111111111111111111111111" +steps: + - config: |- + resource "mongodbatlas_advanced_cluster" "test" { + project_id = "111111111111111111111111" + name = "mocked-cluster" + cluster_type = "REPLICASET" + + replication_specs = [{ + region_configs = [{ + auto_scaling = { + compute_enabled = false + compute_scale_down_enabled = false + disk_gb_enabled = true + } + electable_specs = { + disk_size_gb = 10 + instance_size = "M10" + node_count = 3 + } + priority = 7 + provider_name = "AWS" + region_name = "US_EAST_1" + }] + }] + timeouts = { + create = "6000s" + } + } + diff_requests: [] + request_responses: + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName} + method: GET + version: '2024-08-05' + text: "" + responses: + - response_index: 0 + status: 200 + text: "import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2024-08-05.json" + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName} + method: GET + version: '2023-02-01' + text: "" + responses: + - response_index: 0 + status: 200 + text: "import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2023-02-01.json" + - path: /api/atlas/v2/groups/{groupId}/containers?providerName=AWS + method: GET + version: '2023-01-01' + text: "" + responses: + - response_index: 0 + status: 200 + text: "import_GET__api_atlas_v2_groups_{groupId}_containers?providerName=AWS_2023-01-01.json" + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName}/processArgs + method: GET + version: '2023-01-01' + text: "" + responses: + - response_index: 0 + status: 200 + text: "import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2023-01-01.json" + - path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName}/processArgs + method: GET + version: '2024-08-05' + text: "" + responses: + - response_index: 0 + status: 200 + text: "import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2024-08-05.json" + - path: /api/atlas/v2/groups/{groupId}/clusters + method: GET + version: '2024-08-05' + text: "" + responses: + - response_index: 0 + status: 200 + text: "import_GET__api_atlas_v2_groups_{groupId}_clusters_2024-08-05.json" + - path: /api/atlas/v2/groups/{groupId}/flexClusters + method: GET + version: '2024-11-13' + text: "" + responses: + - response_index: 0 + status: 200 + text: "import_GET__api_atlas_v2_groups_{groupId}_flexClusters_2024-11-13.json" diff --git a/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_2024-08-05.json b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_2024-08-05.json new file mode 100644 index 0000000000..7a8bbb0177 --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_2024-08-05.json @@ -0,0 +1,106 @@ +{ + "links": [ + { + "href": "https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters?includeCount=true\u0026includeDeletedWithRetainedBackups=false\u0026pageNum=1\u0026itemsPerPage=100", + "rel": "self" + } + ], + "results": [ + { + "advancedConfiguration": { + "customOpensslCipherConfigTls12": [], + "minimumEnabledTlsProtocol": "TLS1_2", + "tlsCipherConfigMode": "DEFAULT" + }, + "backupEnabled": false, + "biConnector": { + "enabled": false, + "readPreference": "secondary" + }, + "clusterType": "REPLICASET", + "connectionStrings": { + "standard": "mongodb://test-acc-tf-c-814005816-shard-00-00.au0ou.mongodb-dev.net:27017,test-acc-tf-c-814005816-shard-00-01.au0ou.mongodb-dev.net:27017,test-acc-tf-c-814005816-shard-00-02.au0ou.mongodb-dev.net:27017/?ssl=true\u0026authSource=admin\u0026replicaSet=atlas-xkej5d-shard-0", + "standardSrv": "mongodb+srv://test-acc-tf-c-814005816.au0ou.mongodb-dev.net" + }, + "createDate": "2025-03-05T13:14:38Z", + "diskWarmingMode": "FULLY_WARMED", + "encryptionAtRestProvider": "NONE", + "featureCompatibilityVersion": "8.0", + "globalClusterSelfManagedSharding": false, + "groupId": "{groupId}", + "id": "67c84e3ecb1a946d742268da", + "labels": [], + "links": [ + { + "href": "https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", + "rel": "self" + }, + { + "href": "https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/restoreJobs", + "rel": "https://cloud.mongodb.com/restoreJobs" + }, + { + "href": "https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/snapshots", + "rel": "https://cloud.mongodb.com/snapshots" + } + ], + "mongoDBMajorVersion": "8.0", + "mongoDBVersion": "8.0.5", + "name": "{clusterName}", + "paused": false, + "pitEnabled": false, + "redactClientLogData": false, + "replicationSpecs": [ + { + "id": "67c84e3ecb1a946d742268d1", + "regionConfigs": [ + { + "analyticsSpecs": { + "diskIOPS": 3000, + "diskSizeGB": 10, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 0 + }, + "autoScaling": { + "compute": { + "enabled": false, + "predictiveEnabled": false, + "scaleDownEnabled": false + }, + "diskGB": { + "enabled": true + } + }, + "electableSpecs": { + "diskIOPS": 3000, + "diskSizeGB": 10, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 3 + }, + "priority": 7, + "providerName": "AWS", + "readOnlySpecs": { + "diskIOPS": 3000, + "diskSizeGB": 10, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 0 + }, + "regionName": "US_EAST_1" + } + ], + "zoneId": "67c84e3ecb1a946d742268cf", + "zoneName": "ZoneName managed by Terraform" + } + ], + "rootCertType": "ISRGROOTX1", + "stateName": "IDLE", + "tags": [], + "terminationProtectionEnabled": false, + "versionReleaseSystem": "LTS" + } + ], + "totalCount": 1 +} \ No newline at end of file diff --git a/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2023-02-01.json b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2023-02-01.json new file mode 100644 index 0000000000..5f33bbea1f --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2023-02-01.json @@ -0,0 +1,92 @@ +{ + "advancedConfiguration": { + "customOpensslCipherConfigTls12": [], + "minimumEnabledTlsProtocol": "TLS1_2", + "tlsCipherConfigMode": "DEFAULT" + }, + "backupEnabled": false, + "biConnector": { + "enabled": false, + "readPreference": "secondary" + }, + "clusterType": "REPLICASET", + "connectionStrings": { + "standard": "mongodb://test-acc-tf-c-814005816-shard-00-00.au0ou.mongodb-dev.net:27017,test-acc-tf-c-814005816-shard-00-01.au0ou.mongodb-dev.net:27017,test-acc-tf-c-814005816-shard-00-02.au0ou.mongodb-dev.net:27017/?ssl=true\u0026authSource=admin\u0026replicaSet=atlas-xkej5d-shard-0", + "standardSrv": "mongodb+srv://test-acc-tf-c-814005816.au0ou.mongodb-dev.net" + }, + "createDate": "2025-03-05T13:14:38Z", + "diskSizeGB": 10, + "diskWarmingMode": "FULLY_WARMED", + "encryptionAtRestProvider": "NONE", + "globalClusterSelfManagedSharding": false, + "groupId": "{groupId}", + "id": "67c84e3ecb1a946d742268da", + "labels": [], + "links": [ + { + "href": "https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", + "rel": "self" + }, + { + "href": "https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/restoreJobs", + "rel": "https://cloud.mongodb.com/restoreJobs" + }, + { + "href": "https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/snapshots", + "rel": "https://cloud.mongodb.com/snapshots" + } + ], + "mongoDBMajorVersion": "8.0", + "mongoDBVersion": "8.0.5", + "name": "{clusterName}", + "paused": false, + "pitEnabled": false, + "replicationSpecs": [ + { + "id": "67c84e3ecb1a946d742268d0", + "numShards": 1, + "regionConfigs": [ + { + "analyticsSpecs": { + "diskIOPS": 3000, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 0 + }, + "autoScaling": { + "compute": { + "enabled": false, + "predictiveEnabled": false, + "scaleDownEnabled": false + }, + "diskGB": { + "enabled": true + } + }, + "electableSpecs": { + "diskIOPS": 3000, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 3 + }, + "priority": 7, + "providerName": "AWS", + "readOnlySpecs": { + "diskIOPS": 3000, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 0 + }, + "regionName": "US_EAST_1" + } + ], + "zoneId": "67c84e3ecb1a946d742268cf", + "zoneName": "ZoneName managed by Terraform" + } + ], + "rootCertType": "ISRGROOTX1", + "stateName": "IDLE", + "tags": [], + "terminationProtectionEnabled": false, + "versionReleaseSystem": "LTS" +} \ No newline at end of file diff --git a/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2024-08-05.json b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2024-08-05.json new file mode 100644 index 0000000000..9ec5f8c956 --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_2024-08-05.json @@ -0,0 +1,95 @@ +{ + "advancedConfiguration": { + "customOpensslCipherConfigTls12": [], + "minimumEnabledTlsProtocol": "TLS1_2", + "tlsCipherConfigMode": "DEFAULT" + }, + "backupEnabled": false, + "biConnector": { + "enabled": false, + "readPreference": "secondary" + }, + "clusterType": "REPLICASET", + "connectionStrings": { + "standard": "mongodb://test-acc-tf-c-814005816-shard-00-00.au0ou.mongodb-dev.net:27017,test-acc-tf-c-814005816-shard-00-01.au0ou.mongodb-dev.net:27017,test-acc-tf-c-814005816-shard-00-02.au0ou.mongodb-dev.net:27017/?ssl=true\u0026authSource=admin\u0026replicaSet=atlas-xkej5d-shard-0", + "standardSrv": "mongodb+srv://test-acc-tf-c-814005816.au0ou.mongodb-dev.net" + }, + "createDate": "2025-03-05T13:14:38Z", + "diskWarmingMode": "FULLY_WARMED", + "encryptionAtRestProvider": "NONE", + "featureCompatibilityVersion": "8.0", + "globalClusterSelfManagedSharding": false, + "groupId": "{groupId}", + "id": "67c84e3ecb1a946d742268da", + "labels": [], + "links": [ + { + "href": "https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", + "rel": "self" + }, + { + "href": "https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/restoreJobs", + "rel": "https://cloud.mongodb.com/restoreJobs" + }, + { + "href": "https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/snapshots", + "rel": "https://cloud.mongodb.com/snapshots" + } + ], + "mongoDBMajorVersion": "8.0", + "mongoDBVersion": "8.0.5", + "name": "{clusterName}", + "paused": false, + "pitEnabled": false, + "redactClientLogData": false, + "replicationSpecs": [ + { + "id": "67c84e3ecb1a946d742268d1", + "regionConfigs": [ + { + "analyticsSpecs": { + "diskIOPS": 3000, + "diskSizeGB": 10, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 0 + }, + "autoScaling": { + "compute": { + "enabled": false, + "predictiveEnabled": false, + "scaleDownEnabled": false + }, + "diskGB": { + "enabled": true + } + }, + "electableSpecs": { + "diskIOPS": 3000, + "diskSizeGB": 10, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 3 + }, + "priority": 7, + "providerName": "AWS", + "readOnlySpecs": { + "diskIOPS": 3000, + "diskSizeGB": 10, + "ebsVolumeType": "STANDARD", + "instanceSize": "M10", + "nodeCount": 0 + }, + "regionName": "US_EAST_1" + } + ], + "zoneId": "67c84e3ecb1a946d742268cf", + "zoneName": "ZoneName managed by Terraform" + } + ], + "rootCertType": "ISRGROOTX1", + "stateName": "IDLE", + "tags": [], + "terminationProtectionEnabled": false, + "versionReleaseSystem": "LTS" +} \ No newline at end of file diff --git a/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2023-01-01.json b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2023-01-01.json new file mode 100644 index 0000000000..90acae41a2 --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2023-01-01.json @@ -0,0 +1,19 @@ +{ + "changeStreamOptionsPreAndPostImagesExpireAfterSeconds": null, + "chunkMigrationConcurrency": null, + "customOpensslCipherConfigTls12": [], + "defaultMaxTimeMS": null, + "defaultReadConcern": null, + "defaultWriteConcern": null, + "failIndexKeyTooLong": null, + "javascriptEnabled": true, + "minimumEnabledTlsProtocol": "TLS1_2", + "noTableScan": false, + "oplogMinRetentionHours": null, + "oplogSizeMB": null, + "queryStatsLogVerbosity": 1, + "sampleRefreshIntervalBIConnector": null, + "sampleSizeBIConnector": null, + "tlsCipherConfigMode": "DEFAULT", + "transactionLifetimeLimitSeconds": null +} \ No newline at end of file diff --git a/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2024-08-05.json b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2024-08-05.json new file mode 100644 index 0000000000..25d92e3151 --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_clusters_{clusterName}_processArgs_2024-08-05.json @@ -0,0 +1,17 @@ +{ + "changeStreamOptionsPreAndPostImagesExpireAfterSeconds": null, + "chunkMigrationConcurrency": null, + "customOpensslCipherConfigTls12": [], + "defaultMaxTimeMS": null, + "defaultWriteConcern": null, + "javascriptEnabled": true, + "minimumEnabledTlsProtocol": "TLS1_2", + "noTableScan": false, + "oplogMinRetentionHours": null, + "oplogSizeMB": null, + "queryStatsLogVerbosity": 1, + "sampleRefreshIntervalBIConnector": null, + "sampleSizeBIConnector": null, + "tlsCipherConfigMode": "DEFAULT", + "transactionLifetimeLimitSeconds": null +} \ No newline at end of file diff --git a/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_containers?providerName=AWS_2023-01-01.json b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_containers?providerName=AWS_2023-01-01.json new file mode 100644 index 0000000000..11f928bf12 --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_containers?providerName=AWS_2023-01-01.json @@ -0,0 +1,19 @@ +{ + "links": [ + { + "href": "https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/containers?includeCount=true\u0026providerName=AWS\u0026pageNum=1\u0026itemsPerPage=100", + "rel": "self" + } + ], + "results": [ + { + "atlasCidrBlock": "192.168.248.0/21", + "id": "67c84e3ecb1a946d742268d9", + "providerName": "AWS", + "provisioned": true, + "regionName": "US_EAST_1", + "vpcId": "vpc-0dad0b7e53c94b3e0" + } + ], + "totalCount": 1 +} \ No newline at end of file diff --git a/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_flexClusters_2024-11-13.json b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_flexClusters_2024-11-13.json new file mode 100644 index 0000000000..afdba5266f --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/import_GET__api_atlas_v2_groups_{groupId}_flexClusters_2024-11-13.json @@ -0,0 +1,10 @@ +{ + "links": [ + { + "href": "https://cloud-dev.mongodb.com/api/atlas/v2/groups/{groupId}/flexClusters?includeCount=true\u0026pageNum=1\u0026itemsPerPage=100", + "rel": "self" + } + ], + "results": [], + "totalCount": 0 +} \ No newline at end of file diff --git a/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main.tf b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main.tf new file mode 100644 index 0000000000..26b9a539c1 --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main.tf @@ -0,0 +1,26 @@ +resource "mongodbatlas_advanced_cluster" "test" { + project_id = "111111111111111111111111" + name = "mocked-cluster" + cluster_type = "REPLICASET" + + replication_specs = [{ + region_configs = [{ + auto_scaling = { + compute_enabled = false + compute_scale_down_enabled = false + disk_gb_enabled = true + } + electable_specs = { + disk_size_gb = 10 + instance_size = "M10" + node_count = 3 + } + priority = 7 + provider_name = "AWS" + region_name = "US_EAST_1" + }] + }] + timeouts = { + create = "6000s" + } +} diff --git a/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_backup_enabled.tf b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_backup_enabled.tf new file mode 100644 index 0000000000..d1645ecf5a --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_backup_enabled.tf @@ -0,0 +1,27 @@ +resource "mongodbatlas_advanced_cluster" "test" { + project_id = "111111111111111111111111" + name = "mocked-cluster" + cluster_type = "REPLICASET" + backup_enabled = true + + replication_specs = [{ + region_configs = [{ + auto_scaling = { + compute_enabled = false + compute_scale_down_enabled = false + disk_gb_enabled = true + } + electable_specs = { + disk_size_gb = 10 + instance_size = "M10" + node_count = 3 + } + priority = 7 + provider_name = "AWS" + region_name = "US_EAST_1" + }] + }] + timeouts = { + create = "6000s" + } +} diff --git a/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_mongo_db_major_version_changed.tf b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_mongo_db_major_version_changed.tf new file mode 100644 index 0000000000..b5404abd18 --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_mongo_db_major_version_changed.tf @@ -0,0 +1,27 @@ +resource "mongodbatlas_advanced_cluster" "test" { + project_id = "111111111111111111111111" + name = "mocked-cluster" + cluster_type = "REPLICASET" + mongo_db_major_version = "7.0" + + replication_specs = [{ + region_configs = [{ + auto_scaling = { + compute_enabled = false + compute_scale_down_enabled = false + disk_gb_enabled = true + } + electable_specs = { + disk_size_gb = 10 + instance_size = "M10" + node_count = 3 + } + priority = 7 + provider_name = "AWS" + region_name = "US_EAST_1" + }] + }] + timeouts = { + create = "6000s" + } +} diff --git a/internal/testutil/unit/http_mocker_plan_checks.go b/internal/testutil/unit/http_mocker_plan_checks.go index 6b4881ba04..90f207b33b 100644 --- a/internal/testutil/unit/http_mocker_plan_checks.go +++ b/internal/testutil/unit/http_mocker_plan_checks.go @@ -20,19 +20,23 @@ import ( const ( ImportNameClusterTwoRepSpecsWithAutoScalingAndSpecs = "ClusterTwoRepSpecsWithAutoScalingAndSpecs" + ImportNameClusterReplicasetOneRegion = "ClusterReplicasetOneRegion" MockedClusterName = "mocked-cluster" MockedProjectID = "111111111111111111111111" ) var ( - errToSkipApply = errors.New("avoid full apply by raising an expected error") + errToSkipApply = errors.New("avoid full apply by raising an expected error") + clusterImportID = fmt.Sprintf("%s-%s", MockedProjectID, MockedClusterName) importIDMapping = map[string]string{ - ImportNameClusterTwoRepSpecsWithAutoScalingAndSpecs: fmt.Sprintf("%s-%s", MockedProjectID, MockedClusterName), + ImportNameClusterTwoRepSpecsWithAutoScalingAndSpecs: clusterImportID, + ImportNameClusterReplicasetOneRegion: clusterImportID, } // later this could be inferred when reading the src main.tf importResourceNameMapping = map[string]string{ ImportNameClusterTwoRepSpecsWithAutoScalingAndSpecs: "mongodbatlas_advanced_cluster.test", + ImportNameClusterReplicasetOneRegion: "mongodbatlas_advanced_cluster.test", } ) diff --git a/internal/testutil/unit/http_mocker_plan_checks_test.go b/internal/testutil/unit/http_mocker_plan_checks_test.go index f527fc477c..9d26b08b1b 100644 --- a/internal/testutil/unit/http_mocker_plan_checks_test.go +++ b/internal/testutil/unit/http_mocker_plan_checks_test.go @@ -17,6 +17,13 @@ const ( pkgRelPath = "internal/service" ) +var ( + clusterVariableReplacements = map[string]string{ + "clusterName": unit.MockedClusterName, + "groupId": unit.MockedProjectID, + } +) + type importNameConfig struct { VariableReplacments map[string]string TestName string @@ -32,14 +39,18 @@ func TestConvertMockableTests(t *testing.T) { } for importName, config := range map[string]importNameConfig{ unit.ImportNameClusterTwoRepSpecsWithAutoScalingAndSpecs: { - TestName: "TestAccMockableAdvancedCluster_removeBlocksFromConfig", - Step: 1, - VariableReplacments: map[string]string{ - "clusterName": unit.MockedClusterName, - "groupId": unit.MockedProjectID, - }, - SrcPackage: pkgAdvancedCluster, - DestPackage: pkgAdvancedClusterTPF, + TestName: "TestAccMockableAdvancedCluster_removeBlocksFromConfig", + Step: 1, + VariableReplacments: clusterVariableReplacements, + SrcPackage: pkgAdvancedCluster, + DestPackage: pkgAdvancedClusterTPF, + }, + unit.ImportNameClusterReplicasetOneRegion: { + TestName: "TestAccMockableAdvancedCluster_replicasetAdvConfigUpdate", + Step: 1, + VariableReplacments: clusterVariableReplacements, + SrcPackage: pkgAdvancedCluster, + DestPackage: pkgAdvancedClusterTPF, }, } { srcTestdata := unit.RepoPath(path.Join(pkgRelPath, config.SrcPackage, "testdata")) From a2bac7ba2ea0c449d2169a233ddb77ea5aa66ca0 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Fri, 21 Mar 2025 17:05:15 +0000 Subject: [PATCH 06/39] PR suggestions --- internal/common/conversion/path_converter.go | 4 ++-- internal/common/conversion/path_helpers.go | 10 ++-------- internal/common/conversion/path_helpers_test.go | 6 +++--- .../common/customplanmodifier/plan_modify_differ.go | 2 +- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/internal/common/conversion/path_converter.go b/internal/common/conversion/path_converter.go index b9c6c9f3d0..458d0d6a0a 100644 --- a/internal/common/conversion/path_converter.go +++ b/internal/common/conversion/path_converter.go @@ -21,8 +21,8 @@ type TPFSrc interface { // AttributePathValue retrieves the value for src (state/plan/config) @ attributePath with converted path.Path, schema is needed to get the correct types.XXX (String/Object/etc.) func AttributePathValue(ctx context.Context, diags *diag.Diagnostics, attributePath *tftypes.AttributePath, src TPFSrc, schema TPFSchema) (attr.Value, path.Path) { convertedPath, localDiags := AttributePath(ctx, attributePath, schema) - if localDiags.HasError() { - diags.Append(localDiags...) + diags.Append(localDiags...) + if diags.HasError() { return nil, convertedPath } attrType, err := schema.TypeAtTerraformPath(ctx, attributePath) diff --git a/internal/common/conversion/path_helpers.go b/internal/common/conversion/path_helpers.go index 521504ce40..999fd9a90c 100644 --- a/internal/common/conversion/path_helpers.go +++ b/internal/common/conversion/path_helpers.go @@ -12,7 +12,7 @@ func LastPart(p path.Path) string { return parts[len(parts)-1] } -func IsAttributeValueOnly(p path.Path) bool { +func IsIndexValue(p path.Path) bool { return IsMapIndex(p) || IsListIndex(p) || IsSetIndex(p) } @@ -72,13 +72,7 @@ func AsRemovedIndex(p path.Path) string { } func StripSquareBrackets(p path.Path) string { - if IsListIndex(p) { - return p.ParentPath().String() - } - if IsMapIndex(p) { - return p.ParentPath().String() - } - if IsSetIndex(p) { + if IsIndexValue(p) { return p.ParentPath().String() } return p.String() diff --git a/internal/common/conversion/path_helpers_test.go b/internal/common/conversion/path_helpers_test.go index dd192dd4a4..4ce4800b7c 100644 --- a/internal/common/conversion/path_helpers_test.go +++ b/internal/common/conversion/path_helpers_test.go @@ -10,9 +10,9 @@ import ( ) func TestIsAttributeValueOnly(t *testing.T) { - assert.True(t, conversion.IsAttributeValueOnly(path.Root("replication_specs").AtListIndex(0))) - assert.True(t, conversion.IsAttributeValueOnly(path.Root("replication_specs").AtMapKey("myKey"))) - assert.True(t, conversion.IsAttributeValueOnly(path.Root("replication_specs").AtSetValue(types.StringValue("myKey")))) + assert.True(t, conversion.IsIndexValue(path.Root("replication_specs").AtListIndex(0))) + assert.True(t, conversion.IsIndexValue(path.Root("replication_specs").AtMapKey("myKey"))) + assert.True(t, conversion.IsIndexValue(path.Root("replication_specs").AtSetValue(types.StringValue("myKey")))) } func TestAttributeNameEquals(t *testing.T) { diff --git a/internal/common/customplanmodifier/plan_modify_differ.go b/internal/common/customplanmodifier/plan_modify_differ.go index 413ca4d682..21d53ee2e7 100644 --- a/internal/common/customplanmodifier/plan_modify_differ.go +++ b/internal/common/customplanmodifier/plan_modify_differ.go @@ -123,7 +123,7 @@ func (d *PlanModifyDiffer) UseStateForUnknown(ctx context.Context, diags *diag.D schema := d.schema for _, diff := range d.statePlanDiff { stateValue, tpfPath := conversion.AttributePathValue(ctx, diags, diff.Path, d.req.State, schema) - if !conversion.HasPrefix(tpfPath, prefix) || stateValue == nil || conversion.IsAttributeValueOnly(tpfPath) { + if !conversion.HasPrefix(tpfPath, prefix) || stateValue == nil || conversion.IsIndexValue(tpfPath) { continue } if d.ParentRemoved(tpfPath) { From 7e4004ba3111d24d9e9960d5ab9dd26472d273c4 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Mon, 24 Mar 2025 10:42:40 +0000 Subject: [PATCH 07/39] refactor: Simplify plan check tests by consolidating test case execution --- .../advancedclustertpf/plan_modifier_test.go | 12 ++---------- .../testutil/unit/http_mocker_plan_checks.go | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/internal/service/advancedclustertpf/plan_modifier_test.go b/internal/service/advancedclustertpf/plan_modifier_test.go index e5eeeda01e..bed0350169 100644 --- a/internal/service/advancedclustertpf/plan_modifier_test.go +++ b/internal/service/advancedclustertpf/plan_modifier_test.go @@ -57,11 +57,7 @@ func TestPlanChecksClusterTwoRepSpecsWithAutoScalingAndSpecs(t *testing.T) { }, } ) - for _, testCase := range testCases { - t.Run(testCase.ConfigFilename, func(t *testing.T) { - unit.MockPlanChecksAndRun(t, baseConfig.WithPlanCheckTest(testCase)) - }) - } + unit.RunPlanCheckTests(t, baseConfig, testCases) } func TestMockPlanChecks_ClusterReplicasetOneRegion(t *testing.T) { @@ -85,9 +81,5 @@ func TestMockPlanChecks_ClusterReplicasetOneRegion(t *testing.T) { }, } ) - for _, testCase := range testCases { - t.Run(testCase.ConfigFilename, func(t *testing.T) { - unit.MockPlanChecksAndRun(t, baseConfig.WithPlanCheckTest(testCase)) - }) - } + unit.RunPlanCheckTests(t, baseConfig, testCases) } diff --git a/internal/testutil/unit/http_mocker_plan_checks.go b/internal/testutil/unit/http_mocker_plan_checks.go index faee48917c..3f0b88d56d 100644 --- a/internal/testutil/unit/http_mocker_plan_checks.go +++ b/internal/testutil/unit/http_mocker_plan_checks.go @@ -26,7 +26,7 @@ const ( ) var ( - errToSkipApply = errors.New("avoid full apply by raising an expected error") + errToSkipApply = errors.New("avoid full apply by raising an expected error") clusterImportID = fmt.Sprintf("%s-%s", MockedProjectID, MockedClusterName) importIDMapping = map[string]string{ @@ -40,19 +40,18 @@ var ( } ) -func NewMockPlanChecksConfig(t *testing.T, mockConfig *MockHTTPDataConfig, importName string) MockPlanChecksConfig { +func NewMockPlanChecksConfig(t *testing.T, mockConfig *MockHTTPDataConfig, importName string) *MockPlanChecksConfig { t.Helper() importID := importIDMapping[importName] require.NotEmpty(t, importID, "import ID not found for import name: %s", importName) resourceName := importResourceNameMapping[importName] require.NotEmpty(t, resourceName, "resource name not found for import name: %s", importName) - config := MockPlanChecksConfig{ + return &MockPlanChecksConfig{ ImportName: importName, MockConfig: *mockConfig, ImportID: importID, ResourceName: resourceName, } - return config } type MockPlanChecksConfig struct { @@ -80,6 +79,15 @@ type PlanCheckTest struct { Checks []plancheck.PlanCheck } +func RunPlanCheckTests(t *testing.T, baseConfig *MockPlanChecksConfig, tests []PlanCheckTest) { + t.Helper() + for _, testCase := range tests { + t.Run(testCase.ConfigFilename, func(t *testing.T) { + MockPlanChecksAndRun(t, baseConfig.WithPlanCheckTest(testCase)) + }) + } +} + // MockPlanChecksAndRun creates and runs a UnitTest enabled TestCase for Read to State checks and PlanModifier logic. // The 1st step is always Import // The 2nd step is always Plan with runConfig.Checks run. Note: No Update logic is executed as we exit after the PlanModifier has run. From 3e8b7d08d18fdcba2fbb618e8d35700600889011 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Mon, 24 Mar 2025 11:23:39 +0000 Subject: [PATCH 08/39] refactor test checks to map --- .../common/conversion/path_helpers_test.go | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/internal/common/conversion/path_helpers_test.go b/internal/common/conversion/path_helpers_test.go index 4ce4800b7c..67c5de8a02 100644 --- a/internal/common/conversion/path_helpers_test.go +++ b/internal/common/conversion/path_helpers_test.go @@ -16,13 +16,26 @@ func TestIsAttributeValueOnly(t *testing.T) { } func TestAttributeNameEquals(t *testing.T) { - assert.True(t, conversion.AttributeNameEquals(path.Root("replication_specs").AtListIndex(0), "replication_specs")) - assert.True(t, conversion.AttributeNameEquals(path.Root("replication_specs").AtMapKey("myKey"), "replication_specs")) - assert.True(t, conversion.AttributeNameEquals(path.Root("replication_specs"), "replication_specs")) - assert.True(t, conversion.AttributeNameEquals(path.Root("replication_specs").AtListIndex(0).AtName("region_configs").AtListIndex(1), "region_configs")) - assert.False(t, conversion.AttributeNameEquals(path.Root("replication_specs").AtListIndex(0), "region_configs")) - assert.False(t, conversion.AttributeNameEquals(path.Root("replication_specs").AtMapKey("myKey"), "region_configs")) - assert.False(t, conversion.AttributeNameEquals(path.Root("replication_specs"), "region_configs")) + var ( + repSpecPath = path.Root("replication_specs") + regionConfigsPath = repSpecPath.AtListIndex(0).AtName("region_configs") + ) + for expectedAttribute, paths := range map[string][]path.Path{ + "replication_specs": { + repSpecPath, + repSpecPath.AtListIndex(0), + repSpecPath.AtMapKey("myKey"), + }, + "region_configs": { + regionConfigsPath, + regionConfigsPath.AtListIndex(0), + regionConfigsPath.AtMapKey("myKey"), + }, + } { + for _, p := range paths { + assert.True(t, conversion.AttributeNameEquals(p, expectedAttribute)) + } + } } func TestStripSquareBrackets(t *testing.T) { From 77eb77b81d993280f144c9c4c59b936b944c2c3c Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Tue, 25 Mar 2025 07:13:16 +0000 Subject: [PATCH 09/39] refactor: Address PR comments and remove unused code --- internal/common/conversion/path_helpers.go | 6 +- .../common/conversion/path_helpers_test.go | 10 +- .../common/customplanmodifier/find_changes.go | 15 +- .../customplanmodifier/find_changes_test.go | 38 ++--- .../customplanmodifier/plan_modify_differ.go | 160 +----------------- .../customplanmodifier/unknown_replacement.go | 14 +- .../advancedclustertpf/plan_modifier2.go | 2 +- 7 files changed, 49 insertions(+), 196 deletions(-) diff --git a/internal/common/conversion/path_helpers.go b/internal/common/conversion/path_helpers.go index 999fd9a90c..e9daa2483c 100644 --- a/internal/common/conversion/path_helpers.go +++ b/internal/common/conversion/path_helpers.go @@ -41,12 +41,12 @@ func HasPrefix(p, prefix path.Path) bool { } func AttributeNameEquals(p path.Path, name string) bool { - noBrackets := StripSquareBrackets(p) + noBrackets := TrimLastIndex(p) return noBrackets == name || strings.HasSuffix(noBrackets, fmt.Sprintf(".%s", name)) } func AttributeName(p path.Path) string { - noBrackets := StripSquareBrackets(p) + noBrackets := TrimLastIndex(p) parts := strings.Split(noBrackets, ".") return parts[len(parts)-1] } @@ -71,7 +71,7 @@ func AsRemovedIndex(p path.Path) string { return parentString + "." + indexWithSign } -func StripSquareBrackets(p path.Path) string { +func TrimLastIndex(p path.Path) string { if IsIndexValue(p) { return p.ParentPath().String() } diff --git a/internal/common/conversion/path_helpers_test.go b/internal/common/conversion/path_helpers_test.go index 67c5de8a02..2ebf2973e4 100644 --- a/internal/common/conversion/path_helpers_test.go +++ b/internal/common/conversion/path_helpers_test.go @@ -9,10 +9,12 @@ import ( "github.com/stretchr/testify/assert" ) -func TestIsAttributeValueOnly(t *testing.T) { +func TestIsIndexValue(t *testing.T) { assert.True(t, conversion.IsIndexValue(path.Root("replication_specs").AtListIndex(0))) assert.True(t, conversion.IsIndexValue(path.Root("replication_specs").AtMapKey("myKey"))) assert.True(t, conversion.IsIndexValue(path.Root("replication_specs").AtSetValue(types.StringValue("myKey")))) + assert.False(t, conversion.IsIndexValue(path.Root("replication_specs"))) + assert.False(t, conversion.IsIndexValue(path.Root("replication_specs").AtName("id"))) } func TestAttributeNameEquals(t *testing.T) { @@ -39,9 +41,9 @@ func TestAttributeNameEquals(t *testing.T) { } func TestStripSquareBrackets(t *testing.T) { - assert.Equal(t, "replication_specs", conversion.StripSquareBrackets(path.Root("replication_specs").AtListIndex(0))) - assert.Equal(t, "replication_specs", conversion.StripSquareBrackets(path.Root("replication_specs").AtMapKey("myKey"))) - assert.Equal(t, "replication_specs", conversion.StripSquareBrackets(path.Root("replication_specs"))) + assert.Equal(t, "replication_specs", conversion.TrimLastIndex(path.Root("replication_specs").AtListIndex(0))) + assert.Equal(t, "replication_specs", conversion.TrimLastIndex(path.Root("replication_specs").AtMapKey("myKey"))) + assert.Equal(t, "replication_specs", conversion.TrimLastIndex(path.Root("replication_specs"))) } func TestIndexMethods(t *testing.T) { diff --git a/internal/common/customplanmodifier/find_changes.go b/internal/common/customplanmodifier/find_changes.go index 7f54da37fa..f81109c0f5 100644 --- a/internal/common/customplanmodifier/find_changes.go +++ b/internal/common/customplanmodifier/find_changes.go @@ -7,14 +7,14 @@ import ( type AttributeChanges []string -func (a AttributeChanges) LeafChanges() map[string]bool { +func (a AttributeChanges) LeafChanges() map[string]struct{} { return a.leafChanges(true) } func (a AttributeChanges) AttributeChanged(name string) bool { changes := a.LeafChanges() - changed := changes[name] - return changed + _, found := changes[name] + return found } func (a AttributeChanges) KeepUnknown(attributeEffectedMapping map[string][]string) []string { @@ -32,7 +32,8 @@ func (a AttributeChanges) KeepUnknown(attributeEffectedMapping map[string][]stri func (a AttributeChanges) ListIndexChanged(name string, index int) bool { leafChanges := a.leafChanges(false) indexPath := fmt.Sprintf("%s[%d]", name, index) - return leafChanges[indexPath] + _, found := leafChanges[indexPath] + return found } // NestedListLenChanges accepts a fullPath, e.g., "replication_specs[0].region_configs" and returns true if the length of the nested list has changed @@ -59,8 +60,8 @@ func (a AttributeChanges) ListLenChanges(name string) bool { return false } -func (a AttributeChanges) leafChanges(removeIndex bool) map[string]bool { - leafChanges := map[string]bool{} +func (a AttributeChanges) leafChanges(removeIndex bool) map[string]struct{} { + leafChanges := make(map[string]struct{}) for _, change := range a { var leaf string parts := strings.Split(change, ".") @@ -68,7 +69,7 @@ func (a AttributeChanges) leafChanges(removeIndex bool) map[string]bool { if removeIndex && strings.HasSuffix(leaf, "]") { leaf = strings.Split(leaf, "[")[0] } - leafChanges[leaf] = true + leafChanges[leaf] = struct{}{} } return leafChanges } diff --git a/internal/common/customplanmodifier/find_changes_test.go b/internal/common/customplanmodifier/find_changes_test.go index b181e39adc..7b746ad846 100644 --- a/internal/common/customplanmodifier/find_changes_test.go +++ b/internal/common/customplanmodifier/find_changes_test.go @@ -9,48 +9,48 @@ import ( func TestAttributeChanges_LeafChanges(t *testing.T) { tests := map[string]struct { - expected map[string]bool + expected map[string]struct{} changes customplanmodifier.AttributeChanges }{ "empty changes": { changes: []string{}, - expected: map[string]bool{}, + expected: map[string]struct{}{}, }, "single level changes": { changes: []string{"name", "description"}, - expected: map[string]bool{ - "name": true, - "description": true, + expected: map[string]struct{}{ + "name": {}, + "description": {}, }, }, "nested changes": { changes: []string{"config.name", "settings.enabled"}, - expected: map[string]bool{ - "name": true, - "enabled": true, + expected: map[string]struct{}{ + "name": {}, + "enabled": {}, }, }, "mixed level changes": { changes: []string{"name", "config.type", "settings.auth.enabled"}, - expected: map[string]bool{ - "name": true, - "type": true, - "enabled": true, + expected: map[string]struct{}{ + "name": {}, + "type": {}, + "enabled": {}, }, }, "list changes": { changes: []string{"replication_specs", "replication_specs[0]", "replication_specs[0].zone_name"}, - expected: map[string]bool{ - "replication_specs": true, - "zone_name": true, + expected: map[string]struct{}{ + "replication_specs": {}, + "zone_name": {}, }, }, "nested list changes": { changes: []string{"replication_specs", "replication_specs[0]", "replication_specs[0].region_configs", "replication_specs[0].region_configs[0].region_name"}, - expected: map[string]bool{ - "replication_specs": true, - "region_name": true, - "region_configs": true, + expected: map[string]struct{}{ + "replication_specs": {}, + "region_name": {}, + "region_configs": {}, }, }, } diff --git a/internal/common/customplanmodifier/plan_modify_differ.go b/internal/common/customplanmodifier/plan_modify_differ.go index 21d53ee2e7..e8f014ba2a 100644 --- a/internal/common/customplanmodifier/plan_modify_differ.go +++ b/internal/common/customplanmodifier/plan_modify_differ.go @@ -5,7 +5,6 @@ import ( "fmt" "maps" "slices" - "sort" "strings" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -32,7 +31,7 @@ func NewPlanModifyDiffer(ctx context.Context, req *resource.ModifyPlanRequest, r } attributeChanges := findChanges(ctx, diffStatePlan, diags, schema) - tflog.Info(ctx, fmt.Sprintf("Attribute changes: %s\n", strings.Join(attributeChanges, "\n"))) + tflog.Debug(ctx, fmt.Sprintf("Attribute changes: %s\n", strings.Join(attributeChanges, "\n"))) return &PlanModifyDiffer{ req: req, resp: resp, @@ -67,28 +66,6 @@ func (d *PlanModifyDiffer) ParentRemoved(p path.Path) bool { } } -func (d *PlanModifyDiffer) Diff(ctx context.Context, diags *diag.Diagnostics, schema conversion.TPFSchema, isConfig bool) string { - diffList := d.statePlanDiff - if isConfig { - diffList = d.stateConfigDiff - } - diffPaths := make([]string, len(diffList)) - for i, diff := range diffList { - p, localDiags := conversion.AttributePath(ctx, diff.Path, schema) - if localDiags.HasError() { - diags.Append(localDiags...) - return "" - } - diffPaths[i] = p.String() - } - sort.Strings(diffPaths) - name := "plan" - if isConfig { - name = "config" - } - return fmt.Sprintf("DifferStateTo%s\n", name) + strings.Join(diffPaths, "\n") -} - type UnknownInfo struct { StateValue attr.Value UnknownValue attr.Value @@ -118,48 +95,6 @@ func (d *PlanModifyDiffer) Unknowns(ctx context.Context, diags *diag.Diagnostics return unknowns } -func (d *PlanModifyDiffer) UseStateForUnknown(ctx context.Context, diags *diag.Diagnostics, keepUnknown []string, prefix path.Path) { - // The diff is sorted by the path length, for example read_only_spec is processed before read_only_spec.disk_size_gb - schema := d.schema - for _, diff := range d.statePlanDiff { - stateValue, tpfPath := conversion.AttributePathValue(ctx, diags, diff.Path, d.req.State, schema) - if !conversion.HasPrefix(tpfPath, prefix) || stateValue == nil || conversion.IsIndexValue(tpfPath) { - continue - } - if d.ParentRemoved(tpfPath) { - continue - } - planValue, _ := conversion.AttributePathValue(ctx, diags, diff.Path, d.req.Plan, schema) - if planValue == nil || !planValue.IsUnknown() { - continue - } - if keepUnknownCall(diff.Path, keepUnknown) { - tflog.Info(ctx, fmt.Sprintf("Keeping unknown value in plan @ %s", tpfPath.String())) - unknownValue := conversion.AsUnknownValue(ctx, stateValue) - UpdatePlanValue(ctx, diags, d, tpfPath, unknownValue) - } else { - tflog.Info(ctx, fmt.Sprintf("Replacing unknown value in plan @ %s", tpfPath.String())) - UpdatePlanValue(ctx, diags, d, tpfPath, stateValue) - d.ensureKeepUnknownRespected(ctx, diags, tpfPath, stateValue, keepUnknown) - } - } -} - -func (d *PlanModifyDiffer) ensureKeepUnknownRespected(ctx context.Context, diags *diag.Diagnostics, tpfPath path.Path, value attr.Value, keepUnknown []string) { - valueObject, ok := value.(types.Object) - if value.IsNull() || value.IsUnknown() || !ok { - return - } - for key, childValue := range valueObject.Attributes() { - if slices.Contains(keepUnknown, key) && !childValue.IsUnknown() { - childPath := tpfPath.AtName(key) - tflog.Info(ctx, fmt.Sprintf("Keeping unknown value in plan @ %s", childPath.String())) - unknownValue := conversion.AsUnknownValue(ctx, childValue) - UpdatePlanValue(ctx, diags, d, childPath, unknownValue) - } - } -} - func ReadConfigStructValue[T any](ctx context.Context, d *PlanModifyDiffer, p path.Path) *T { return readSrcStructValue[T](ctx, d.req.Config, p) } @@ -183,38 +118,12 @@ func readSrcStructValue[T any](ctx context.Context, src conversion.TPFSrc, p pat return conversion.TFModelObject[T](ctx, obj) } -func UpdatePlanValue[T attr.Value](ctx context.Context, diags *diag.Diagnostics, d *PlanModifyDiffer, p path.Path, value T) { - if localDiags := d.resp.Plan.SetAttribute(ctx, p, value); localDiags.HasError() { - diags.Append(localDiags...) - } -} - -type DiffTPF[T any] struct { - Plan *T - State *T - Config *T - Path path.Path - PlanUnknown bool - ConfigUnknown bool -} - -func (d *DiffTPF[T]) Removed() bool { - return d.State != nil && d.Config == nil -} - -func (d *DiffTPF[T]) Changed() bool { - return d.State != nil && d.Config != nil -} - -func (d *DiffTPF[T]) PlanOrStateValue() *T { - if d.Plan != nil { - return d.Plan - } - return d.State +func UpdatePlanValue(ctx context.Context, diags *diag.Diagnostics, d *PlanModifyDiffer, p path.Path, value attr.Value) { + diags.Append(d.resp.Plan.SetAttribute(ctx, p, value)...) } func findChanges(ctx context.Context, diff []tftypes.ValueDiff, diags *diag.Diagnostics, schema conversion.TPFSchema) AttributeChanges { - changes := map[string]bool{} + changes := make(map[string]bool) addChangeAndParentChanges := func(change string) { changes[change] = true parts := strings.Split(change, ".") @@ -240,64 +149,5 @@ func findChanges(ctx context.Context, diff []tftypes.ValueDiff, diags *diag.Diag addChangeAndParentChanges(p.String()) } } - return slices.Sorted(maps.Keys(changes)) -} - -func keepUnknownCall(aPath *tftypes.AttributePath, keepUnknown []string) bool { - for _, step := range aPath.Steps() { - if aName, ok := step.(tftypes.AttributeName); ok { - if slices.Contains(keepUnknown, string(aName)) { - return true - } - } - } - return false -} - -func StateConfigDiffs[T any](ctx context.Context, diags *diag.Diagnostics, d *PlanModifyDiffer, name string, checkNestedAttributes bool) []DiffTPF[T] { - earlyReturn := func(localDiags diag.Diagnostics) []DiffTPF[T] { - diags.Append(localDiags...) - return nil - } - var diffs []DiffTPF[T] - usedPaths := map[string]bool{} - - for _, diff := range d.stateConfigDiff { - p, localDiags := conversion.AttributePath(ctx, diff.Path, d.schema) - if localDiags.HasError() { - return earlyReturn(localDiags) - } - // Never show diff if the parent is removed, for example replication_specs[0] is removed and replication_specs[0].region_configs[0].electable_spec is changed - if d.ParentRemoved(p) { - continue - } - if checkNestedAttributes { - parent := p.ParentPath() - if conversion.AttributeNameEquals(parent, name) { - p = parent - } - } - if _, ok := usedPaths[p.String()]; ok { - continue // already returned - } - if conversion.AttributeNameEquals(p, name) { - usedPaths[p.String()] = true - var configObj, planObj types.Object - if d2 := d.req.Config.GetAttribute(ctx, p, &configObj); d2.HasError() { - return earlyReturn(d2) - } - if d3 := d.req.Plan.GetAttribute(ctx, p, &planObj); d3.HasError() { - return earlyReturn(d3) - } - diffs = append(diffs, DiffTPF[T]{ - Path: p, - State: ReadStateStructValue[T](ctx, d, p), - Config: ReadConfigStructValue[T](ctx, d, p), - Plan: ReadPlanStructValue[T](ctx, d, p), - PlanUnknown: planObj.IsUnknown(), - ConfigUnknown: configObj.IsUnknown(), - }) - } - } - return diffs + return slices.Sorted(maps.Keys(changes)) // Ensure changes are sorted to support top-down processing, for example read_only_spec is processed before read_only_spec.disk_size_gb } diff --git a/internal/common/customplanmodifier/unknown_replacement.go b/internal/common/customplanmodifier/unknown_replacement.go index 494445867f..a08a10bcf8 100644 --- a/internal/common/customplanmodifier/unknown_replacement.go +++ b/internal/common/customplanmodifier/unknown_replacement.go @@ -13,8 +13,8 @@ import ( "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" ) -func NewUnknownReplacements[ResourceInfo any](ctx context.Context, req *resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse, schema conversion.TPFSchema, info ResourceInfo) *UnknownReplacments[ResourceInfo] { - return &UnknownReplacments[ResourceInfo]{ +func NewUnknownReplacements[ResourceInfo any](ctx context.Context, req *resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse, schema conversion.TPFSchema, info ResourceInfo) *UnknownReplacements[ResourceInfo] { + return &UnknownReplacements[ResourceInfo]{ Differ: NewPlanModifyDiffer(ctx, req, resp, schema), Info: info, Replacements: make(map[string]UnknownReplacementCall[ResourceInfo]), @@ -23,7 +23,7 @@ func NewUnknownReplacements[ResourceInfo any](ctx context.Context, req *resource type UnknownReplacementCall[ResourceInfo any] func(ctx context.Context, stateValue ParsedAttrValue, req *UnknownReplacementRequest[ResourceInfo]) attr.Value -type UnknownReplacments[ResourceInfo any] struct { +type UnknownReplacements[ResourceInfo any] struct { Differ *PlanModifyDiffer Replacements map[string]UnknownReplacementCall[ResourceInfo] Info ResourceInfo @@ -50,13 +50,13 @@ type UnknownReplacementRequest[ResourceInfo any] struct { Changes AttributeChanges } -func (u *UnknownReplacments[ResourceInfo]) AddReplacement(name string, call UnknownReplacementCall[ResourceInfo]) { +func (u *UnknownReplacements[ResourceInfo]) AddReplacement(name string, call UnknownReplacementCall[ResourceInfo]) { // todo: Validate the name in the schema // todo: Validate the name is not already in the replacements u.Replacements[name] = call } -func (u *UnknownReplacments[ResourceInfo]) ApplyReplacments(ctx context.Context, diags *diag.Diagnostics) { +func (u *UnknownReplacements[ResourceInfo]) ApplyReplacements(ctx context.Context, diags *diag.Diagnostics) { for strPath, unknown := range u.Differ.Unknowns(ctx, diags) { replacer, ok := u.Replacements[unknown.AttributeName] if !ok { @@ -71,9 +71,9 @@ func (u *UnknownReplacments[ResourceInfo]) ApplyReplacments(ctx context.Context, } response := replacer(ctx, ParsedAttrValue{Value: unknown.StateValue}, req) if response.IsUnknown() { - tflog.Info(ctx, fmt.Sprintf("Keeping unknown value in plan @ %s", strPath)) + tflog.Debug(ctx, fmt.Sprintf("Keeping unknown value in plan @ %s", strPath)) } else { - tflog.Info(ctx, fmt.Sprintf("Replacing unknown value in plan @ %s", strPath)) + tflog.Debug(ctx, fmt.Sprintf("Replacing unknown value in plan @ %s", strPath)) UpdatePlanValue(ctx, diags, u.Differ, unknown.Path, response) } } diff --git a/internal/service/advancedclustertpf/plan_modifier2.go b/internal/service/advancedclustertpf/plan_modifier2.go index a5dc147ef7..a2b0d87439 100644 --- a/internal/service/advancedclustertpf/plan_modifier2.go +++ b/internal/service/advancedclustertpf/plan_modifier2.go @@ -44,5 +44,5 @@ func unknownReplacements(ctx context.Context, req *resource.ModifyPlanRequest, r for attrName, replacer := range attributePlanModifiers { unknownReplacements.AddReplacement(attrName, replacer) } - unknownReplacements.ApplyReplacments(ctx, diags) + unknownReplacements.ApplyReplacements(ctx, diags) } From dda909b32209d892b3cddcbb18a33b5dea4dab9d Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Tue, 25 Mar 2025 07:52:25 +0000 Subject: [PATCH 10/39] refactor: expose ResourceSchema to allow usage in tests --- internal/service/advancedclustertpf/plan_modifier2.go | 3 ++- internal/service/advancedclustertpf/resource.go | 2 +- internal/service/advancedclustertpf/schema.go | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/service/advancedclustertpf/plan_modifier2.go b/internal/service/advancedclustertpf/plan_modifier2.go index a2b0d87439..7302afca01 100644 --- a/internal/service/advancedclustertpf/plan_modifier2.go +++ b/internal/service/advancedclustertpf/plan_modifier2.go @@ -10,6 +10,7 @@ import ( var attributePlanModifiers = map[string]customplanmodifier.UnknownReplacementCall[PlanModifyResourceInfo]{ "mongo_db_version": mongoDBVersionReplaceUnknown, + // TODO: Add the other computed attributes } func mongoDBVersionReplaceUnknown(ctx context.Context, state customplanmodifier.ParsedAttrValue, req *customplanmodifier.UnknownReplacementRequest[PlanModifyResourceInfo]) attr.Value { @@ -40,7 +41,7 @@ func unknownReplacements(ctx context.Context, req *resource.ModifyPlanRequest, r AutoScalingDiskUsed: diskUsed, isShardingConfigUpgrade: shardingConfigUpgrade, } - unknownReplacements := customplanmodifier.NewUnknownReplacements(ctx, req, resp, resourceSchema(ctx), info) + unknownReplacements := customplanmodifier.NewUnknownReplacements(ctx, req, resp, ResourceSchema(ctx), info) for attrName, replacer := range attributePlanModifiers { unknownReplacements.AddReplacement(attrName, replacer) } diff --git a/internal/service/advancedclustertpf/resource.go b/internal/service/advancedclustertpf/resource.go index 092c2ff63b..117ee39223 100644 --- a/internal/service/advancedclustertpf/resource.go +++ b/internal/service/advancedclustertpf/resource.go @@ -117,7 +117,7 @@ func (r *rs) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, res } func (r *rs) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = resourceSchema(ctx) + resp.Schema = ResourceSchema(ctx) conversion.UpdateSchemaDescription(&resp.Schema) } diff --git a/internal/service/advancedclustertpf/schema.go b/internal/service/advancedclustertpf/schema.go index 614bc3525b..50afba8825 100644 --- a/internal/service/advancedclustertpf/schema.go +++ b/internal/service/advancedclustertpf/schema.go @@ -21,7 +21,7 @@ import ( "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/schemafunc" ) -func resourceSchema(ctx context.Context) schema.Schema { +func ResourceSchema(ctx context.Context) schema.Schema { return schema.Schema{ Version: 2, Attributes: map[string]schema.Attribute{ @@ -329,14 +329,14 @@ func resourceSchema(ctx context.Context) schema.Schema { } func dataSourceSchema(ctx context.Context) dsschema.Schema { - return conversion.DataSourceSchemaFromResource(resourceSchema(ctx), &conversion.DataSourceSchemaRequest{ + return conversion.DataSourceSchemaFromResource(ResourceSchema(ctx), &conversion.DataSourceSchemaRequest{ RequiredFields: []string{"project_id", "name"}, OverridenFields: dataSourceOverridenFields(), }) } func pluralDataSourceSchema(ctx context.Context) dsschema.Schema { - return conversion.PluralDataSourceSchemaFromResource(resourceSchema(ctx), &conversion.PluralDataSourceSchemaRequest{ + return conversion.PluralDataSourceSchemaFromResource(ResourceSchema(ctx), &conversion.PluralDataSourceSchemaRequest{ RequiredFields: []string{"project_id"}, OverridenRootFields: map[string]dsschema.Attribute{ "use_replication_spec_per_shard": useReplicationSpecPerShardSchema(), From b728367530e3f403326dff84b184fadadc11f516 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Tue, 25 Mar 2025 08:53:09 +0000 Subject: [PATCH 11/39] test: Initial support for wrapping a resource to test the plan modifier --- .../unknown_replacement_test.go | 99 +++++++++++++++++++ internal/testutil/unit/http_mocker.go | 27 +++-- .../testutil/unit/http_mocker_api_paths.go | 4 + .../testutil/unit/http_mocker_plan_checks.go | 21 ++-- .../unit/http_mocker_plan_checks_test.go | 5 +- internal/testutil/unit/provider_mock.go | 23 +++-- 6 files changed, 151 insertions(+), 28 deletions(-) create mode 100644 internal/common/customplanmodifier/unknown_replacement_test.go diff --git a/internal/common/customplanmodifier/unknown_replacement_test.go b/internal/common/customplanmodifier/unknown_replacement_test.go new file mode 100644 index 0000000000..8db084c2c0 --- /dev/null +++ b/internal/common/customplanmodifier/unknown_replacement_test.go @@ -0,0 +1,99 @@ +package customplanmodifier_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/service/advancedclustertpf" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/unit" +) + +var _ resource.ResourceWithConfigure = &rs{} +var _ resource.ResourceWithImportState = &rs{} +var _ resource.ResourceWithModifyPlan = &rs{} + +type BaseResourcePlanModify interface { + resource.Resource + resource.ResourceWithConfigure + resource.ResourceWithImportState + resource.ResourceWithModifyPlan +} + +func WrappedResource(base BaseResourcePlanModify) func() resource.Resource { + return func() resource.Resource { + return &rs{base: base} + } +} + +type rs struct { + base BaseResourcePlanModify +} + +func (r *rs) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.base.Metadata(ctx, req, resp) +} + +func (r *rs) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.base.Configure(ctx, req, resp) +} + +func (r *rs) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + r.base.ModifyPlan(ctx, req, resp) +} + +func (r *rs) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + r.base.Schema(ctx, req, resp) +} + +func (r *rs) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + r.base.Create(ctx, req, resp) +} + +func (r *rs) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + r.base.Read(ctx, req, resp) +} + +func (r *rs) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + r.base.Update(ctx, req, resp) +} + +func (r *rs) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + r.base.Delete(ctx, req, resp) +} + +func (r *rs) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.base.ImportState(ctx, req, resp) +} + +func TestWrappingAdvancedClusterTPF(t *testing.T) { + var ( + resources = []func() resource.Resource{ + WrappedResource(advancedclustertpf.Resource().(BaseResourcePlanModify)), + } + mockConfig = unit.MockConfigAdvancedClusterTPF.WithResources(resources) + baseConfig = unit.NewMockPlanChecksConfig(t, &mockConfig, unit.ImportNameClusterReplicasetOneRegion) + resourceName = baseConfig.ResourceName + testCases = []unit.PlanCheckTest{ + { + ConfigFilename: "main_mongo_db_major_version_changed.tf", + Checks: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + plancheck.ExpectUnknownValue(resourceName, tfjsonpath.New("mongo_db_version")), + }, + }, + { + ConfigFilename: "main_backup_enabled.tf", + Checks: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("mongo_db_version"), knownvalue.StringExact("8.0.5")), + }, + }, + } + ) + baseConfig.TestdataPrefix = unit.PackagePath("advancedclustertpf") + unit.RunPlanCheckTests(t, baseConfig, testCases) +} diff --git a/internal/testutil/unit/http_mocker.go b/internal/testutil/unit/http_mocker.go index e5c9bf96b9..d6881dd841 100644 --- a/internal/testutil/unit/http_mocker.go +++ b/internal/testutil/unit/http_mocker.go @@ -8,6 +8,7 @@ import ( "sync" "testing" + fwresource "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/acc" @@ -23,14 +24,15 @@ const ( ) type MockHTTPDataConfig struct { - RunBeforeEach func() error // Run by TestCase.PreCheck. Useful for reducing retry timeouts. - RequestHandler ManualRequestHandler // Allow inspecting or overriding mocking behavior. Can be used to return 404 when a test has completed. - FilePathOverride string // Read mock data file from specific filepath, otherwise using the test name in `MockConfigFilePath` to find mocked responses. - IsDiffSkipSuffixes []string // Can be used when a PATCH/POST request is creating noise for diffs, for example :validate endpoints. - IsDiffMustSubstrings []string // Only include diff request for specific substrings, for example /clusters (avoids project create requests) - QueryVars []string // Substitute this query vars. Useful when differentiating responses based on query args, for example ?providerName=AWS/AZURE returns different responses - AllowMissingRequests bool // When false will require all API calls to be made. - AllowOutOfOrder bool // When true will allow a GET request returned after a POST to be returned before the POST. + RunBeforeEach func() error + RequestHandler ManualRequestHandler + FilePathOverride string + IsDiffSkipSuffixes []string + IsDiffMustSubstrings []string + QueryVars []string + ExplicitResources []func() fwresource.Resource + AllowMissingRequests bool + AllowOutOfOrder bool } func (c MockHTTPDataConfig) WithAllowOutOfOrder() MockHTTPDataConfig { //nolint: gocritic // Want each test run to have its own config (hugeParam: c is heavy (112 bytes); consider passing it by pointer) @@ -38,6 +40,11 @@ func (c MockHTTPDataConfig) WithAllowOutOfOrder() MockHTTPDataConfig { //nolint: return c } +func (c MockHTTPDataConfig) WithResources(resources []func() fwresource.Resource) MockHTTPDataConfig { //nolint: gocritic // Want each test run to have its own config (hugeParam: c is heavy (112 bytes); consider passing it by pointer) + c.ExplicitResources = resources + return c +} + func IsCapture() bool { val, _ := strconv.ParseBool(os.Getenv(EnvNameHTTPMockerCapture)) return val @@ -152,7 +159,7 @@ func enableReplayForTestCase(t *testing.T, config *MockHTTPDataConfig, testCase roundTripper, mockRoundTripper := NewMockRoundTripper(t, config, data) httpClientModifier := mockClientModifier{config: config, mockRoundTripper: roundTripper} testCase.IsUnitTest = true - testCase.ProtoV6ProviderFactories = TestAccProviderV6FactoriesWithMock(t, &httpClientModifier) + testCase.ProtoV6ProviderFactories = TestAccProviderV6FactoriesWithMock(t, &httpClientModifier, config.ExplicitResources) testCase.PreCheck = func() { if config.RunBeforeEach != nil { // Mock Configs can share RunBeforeEach, using lock to avoid race conditions. @@ -193,7 +200,7 @@ func enableCaptureForTestCase(t *testing.T, config *MockHTTPDataConfig, testCase tfConfigs := extractAndNormalizeConfig(t, testCase) capturedData := NewMockHTTPData(t, stepCount, tfConfigs) clientModifier := NewCaptureMockConfigClientModifier(t, config, capturedData) - testCase.ProtoV6ProviderFactories = TestAccProviderV6FactoriesWithMock(t, clientModifier) + testCase.ProtoV6ProviderFactories = TestAccProviderV6FactoriesWithMock(t, clientModifier, config.ExplicitResources) for i := range stepCount { step := &testCase.Steps[i] oldSkip := step.SkipFunc diff --git a/internal/testutil/unit/http_mocker_api_paths.go b/internal/testutil/unit/http_mocker_api_paths.go index b3bf4ccc2c..2c4b4244f8 100644 --- a/internal/testutil/unit/http_mocker_api_paths.go +++ b/internal/testutil/unit/http_mocker_api_paths.go @@ -114,6 +114,10 @@ func RepoPath(relPath string) string { panic("could not find repo root") } +func PackagePath(name string) string { + return RepoPath(path.Join("internal/service", name)) +} + func init() { InitializeAPISpecPaths() } diff --git a/internal/testutil/unit/http_mocker_plan_checks.go b/internal/testutil/unit/http_mocker_plan_checks.go index 3f0b88d56d..a0f634ba9d 100644 --- a/internal/testutil/unit/http_mocker_plan_checks.go +++ b/internal/testutil/unit/http_mocker_plan_checks.go @@ -10,6 +10,7 @@ import ( "path" "path/filepath" "regexp" + "strings" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -59,8 +60,9 @@ type MockPlanChecksConfig struct { ResourceName string ImportName string ConfigFilename string - Checks []plancheck.PlanCheck + TestdataPrefix string MockConfig MockHTTPDataConfig + Checks []plancheck.PlanCheck } func (m *MockPlanChecksConfig) WithPlanCheckTest(testConfig PlanCheckTest) *MockPlanChecksConfig { @@ -71,6 +73,7 @@ func (m *MockPlanChecksConfig) WithPlanCheckTest(testConfig PlanCheckTest) *Mock ImportID: m.ImportID, ResourceName: m.ResourceName, MockConfig: m.MockConfig, + TestdataPrefix: m.TestdataPrefix, } } @@ -95,7 +98,7 @@ func RunPlanCheckTests(t *testing.T, baseConfig *MockPlanChecksConfig, tests []P // Together with the extra step in `testdata/{ImportName}/main_{runConfig.Name}.tf` we fill the template: testdata/{runConfig.ImportName}.tmpl.yaml func MockPlanChecksAndRun(t *testing.T, runConfig *MockPlanChecksConfig) { t.Helper() - importConfig, planConfig, mockDataPath := fillMockDataTemplate(t, runConfig.ImportName, runConfig.ConfigFilename) + importConfig, planConfig, mockDataPath := fillMockDataTemplate(t, runConfig.TestdataPrefix, runConfig.ImportName, runConfig.ConfigFilename) t.Cleanup(func() { require.NoError(t, os.Remove(mockDataPath)) }) @@ -164,12 +167,18 @@ func (r *requestHandlerSwitch) CheckPlan(_ context.Context, req plancheck.CheckP resp.Error = errToSkipApply } -func fillMockDataTemplate(t *testing.T, importName, planConfigFilename string) (importConfig, planCheckConfig, mockDataFilePath string) { +func fillMockDataTemplate(t *testing.T, testdataPrefix, importName, planConfigFilename string) (importConfig, planCheckConfig, mockDataFilePath string) { t.Helper() - templatePath := fmt.Sprintf("testdata/%s.tmpl.yaml", importName) + fullPath := func(testdataRelPath string) string { + if testdataPrefix == "" { + return "testdata/" + testdataRelPath + } + return strings.TrimSuffix(testdataPrefix, "/") + "/testdata/" + testdataRelPath + } + templatePath := fullPath(importName + ".tmpl.yaml") templateContent, err := os.ReadFile(templatePath) require.NoError(t, err) - responseDir := fmt.Sprintf("testdata/%s", importName) + responseDir := fullPath(importName) responsePaths, err := filepath.Glob(path.Join(responseDir, "*.json")) require.NoError(t, err) for _, testFile := range responsePaths { @@ -179,7 +188,7 @@ func fillMockDataTemplate(t *testing.T, importName, planConfigFilename string) ( testFileContent = bytes.ReplaceAll(testFileContent, []byte("\n"), []byte(`\n`)) templateContent = bytes.ReplaceAll(templateContent, []byte(filepath.Base(testFile)), testFileContent) } - mockDataPath := fmt.Sprintf("testdata/%s_%s.yaml", importName, planConfigFilename) + mockDataPath := fullPath(fmt.Sprintf("%s_%s.yaml", importName, planConfigFilename)) err = os.WriteFile(mockDataPath, templateContent, 0o600) require.NoError(t, err) fullImportConfigBytes, err := os.ReadFile(path.Join(responseDir, "main.tf")) diff --git a/internal/testutil/unit/http_mocker_plan_checks_test.go b/internal/testutil/unit/http_mocker_plan_checks_test.go index 303b8a4737..0d4c70eaa1 100644 --- a/internal/testutil/unit/http_mocker_plan_checks_test.go +++ b/internal/testutil/unit/http_mocker_plan_checks_test.go @@ -14,7 +14,6 @@ import ( const ( pkgAdvancedCluster = "advancedcluster" pkgAdvancedClusterTPF = "advancedclustertpf" - pkgRelPath = "internal/service" ) var ( @@ -53,8 +52,8 @@ func TestConvertMockableTests(t *testing.T) { DestPackage: pkgAdvancedClusterTPF, }, } { - srcTestdata := unit.RepoPath(path.Join(pkgRelPath, config.SrcPackage, "testdata")) - destTestdata := unit.RepoPath(path.Join(pkgRelPath, config.DestPackage, "testdata")) + srcTestdata := path.Join(unit.PackagePath(config.SrcPackage), "testdata") + destTestdata := path.Join(unit.PackagePath(config.DestPackage), "testdata") ensureDir(t, destTestdata) srcTestdataPath := path.Join(srcTestdata, config.TestName+".yaml") destTestdataPath := path.Join(destTestdata, importName+".tmpl.yaml") diff --git a/internal/testutil/unit/provider_mock.go b/internal/testutil/unit/provider_mock.go index 5be1eaa890..6e868f149c 100644 --- a/internal/testutil/unit/provider_mock.go +++ b/internal/testutil/unit/provider_mock.go @@ -28,9 +28,10 @@ type HTTPClientModifier interface { } type ProviderMocked struct { - OriginalProvider *provider.MongodbtlasProvider - ClientModifier HTTPClientModifier - t *testing.T + OriginalProvider *provider.MongodbtlasProvider + ClientModifier HTTPClientModifier + t *testing.T + explicitResources []func() resource.Resource } func (p *ProviderMocked) Metadata(ctx context.Context, req fwProvider.MetadataRequest, resp *fwProvider.MetadataResponse) { @@ -62,11 +63,14 @@ func (p *ProviderMocked) DataSources(ctx context.Context) []func() datasource.Da return p.OriginalProvider.DataSources(ctx) } func (p *ProviderMocked) Resources(ctx context.Context) []func() resource.Resource { + if len(p.explicitResources) > 0 { + return p.explicitResources + } return p.OriginalProvider.Resources(ctx) } // Similar to provider.go#muxProviderFactory -func muxProviderFactory(t *testing.T, clientModifier HTTPClientModifier) func() tfprotov6.ProviderServer { +func muxProviderFactory(t *testing.T, clientModifier HTTPClientModifier, explicitResources []func() resource.Resource) func() tfprotov6.ProviderServer { t.Helper() v2Provider := provider.NewSdkV2Provider() v2ProviderConfigureContextFunc := v2Provider.ConfigureContextFunc @@ -89,9 +93,10 @@ func muxProviderFactory(t *testing.T, clientModifier HTTPClientModifier) func() log.Fatal("Failed to cast provider to MongodbtlasProvider") } mockedProvider := &ProviderMocked{ - OriginalProvider: fwProviderInstanceTyped, - ClientModifier: clientModifier, - t: t, + OriginalProvider: fwProviderInstanceTyped, + ClientModifier: clientModifier, + t: t, + explicitResources: explicitResources, } upgradedSdkProvider, err := tf5to6server.UpgradeServer(t.Context(), v2Provider.GRPCProvider) if err != nil { @@ -107,11 +112,11 @@ func muxProviderFactory(t *testing.T, clientModifier HTTPClientModifier) func() return muxServer.ProviderServer } -func TestAccProviderV6FactoriesWithMock(t *testing.T, clientModifier HTTPClientModifier) map[string]func() (tfprotov6.ProviderServer, error) { +func TestAccProviderV6FactoriesWithMock(t *testing.T, clientModifier HTTPClientModifier, explicitResources []func() resource.Resource) map[string]func() (tfprotov6.ProviderServer, error) { t.Helper() return map[string]func() (tfprotov6.ProviderServer, error){ acc.ProviderNameMongoDBAtlas: func() (tfprotov6.ProviderServer, error) { - return muxProviderFactory(t, clientModifier)(), nil + return muxProviderFactory(t, clientModifier, explicitResources)(), nil }, } } From d9c59d45b90258eb01f94bf062899257ed6440a2 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Tue, 25 Mar 2025 09:34:33 +0000 Subject: [PATCH 12/39] test: Add unit tests to the unknown replacement logic --- .../unknown_replacement_test.go | 197 +++++++++++++++--- ...auto_scaling_removed_node_count_changed.tf | 21 ++ .../main_instance_size_changed.tf | 26 +++ .../testutil/unit/http_mocker_plan_checks.go | 13 +- 4 files changed, 227 insertions(+), 30 deletions(-) create mode 100644 internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_auto_scaling_removed_node_count_changed.tf create mode 100644 internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_instance_size_changed.tf diff --git a/internal/common/customplanmodifier/unknown_replacement_test.go b/internal/common/customplanmodifier/unknown_replacement_test.go index 8db084c2c0..0f9cf4e01a 100644 --- a/internal/common/customplanmodifier/unknown_replacement_test.go +++ b/internal/common/customplanmodifier/unknown_replacement_test.go @@ -4,12 +4,17 @@ import ( "context" "testing" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-testing/knownvalue" "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/customplanmodifier" "github.com/mongodb/terraform-provider-mongodbatlas/internal/service/advancedclustertpf" "github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/unit" + "github.com/stretchr/testify/assert" ) var _ resource.ResourceWithConfigure = &rs{} @@ -23,14 +28,28 @@ type BaseResourcePlanModify interface { resource.ResourceWithModifyPlan } -func WrappedResource(base BaseResourcePlanModify) func() resource.Resource { +type planModifyRunData struct { + keepUnknownCalls []string + attributeChanges customplanmodifier.AttributeChanges +} + +type replaceUnknownResourceInfo struct { + anyMap map[string]any +} + +type replaceUnknownTestCall customplanmodifier.UnknownReplacementCall[replaceUnknownResourceInfo] + +func WrappedResource(base BaseResourcePlanModify, info *replaceUnknownResourceInfo, runData *planModifyRunData, attributeReplaceUnknowns map[string]replaceUnknownTestCall) func() resource.Resource { return func() resource.Resource { - return &rs{base: base} + return &rs{base: base, info: info, runData: runData, attributeReplaceUnknowns: attributeReplaceUnknowns} } } type rs struct { - base BaseResourcePlanModify + base BaseResourcePlanModify + runData *planModifyRunData + info *replaceUnknownResourceInfo + attributeReplaceUnknowns map[string]replaceUnknownTestCall } func (r *rs) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -41,8 +60,22 @@ func (r *rs) Configure(ctx context.Context, req resource.ConfigureRequest, resp r.base.Configure(ctx, req, resp) } +// ModifyPlan is the only method overridden in this test. func (r *rs) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { - r.base.ModifyPlan(ctx, req, resp) + schema := advancedclustertpf.ResourceSchema(ctx) + if req.Plan.Raw.IsFullyKnown() { + return + } + unknownReplacements := customplanmodifier.NewUnknownReplacements(ctx, &req, resp, schema, *r.info) + for attrName, replacer := range r.attributeReplaceUnknowns { + modifiedReplacer := func(ctx context.Context, stateValue customplanmodifier.ParsedAttrValue, req *customplanmodifier.UnknownReplacementRequest[replaceUnknownResourceInfo]) attr.Value { + r.runData.keepUnknownCalls = append(r.runData.keepUnknownCalls, req.Path.String()) + return replacer(ctx, stateValue, req) + } + unknownReplacements.AddReplacement(attrName, modifiedReplacer) + } + unknownReplacements.ApplyReplacements(ctx, &resp.Diagnostics) + r.runData.attributeChanges = unknownReplacements.Differ.AttributeChanges } func (r *rs) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { @@ -69,31 +102,143 @@ func (r *rs) ImportState(ctx context.Context, req resource.ImportStateRequest, r r.base.ImportState(ctx, req, resp) } +func configureResources(info *replaceUnknownResourceInfo, runData *planModifyRunData, attributeReplaceUnknowns map[string]replaceUnknownTestCall) []func() resource.Resource { + return []func() resource.Resource{ + WrappedResource(advancedclustertpf.Resource().(BaseResourcePlanModify), info, runData, attributeReplaceUnknowns), + } +} + +type unknownReplacementTestCase struct { + attributeReplaceUnknowns map[string]replaceUnknownTestCall + info replaceUnknownResourceInfo + ImportName string + ConfigFilename string + CheckUnknowns []tfjsonpath.Path + CheckKnownValues []tfjsonpath.Path + ExtraChecks []func(string) plancheck.PlanCheck + expectedAttributeChanges customplanmodifier.AttributeChanges + expectedKeepUnknownCalls []string +} + func TestWrappingAdvancedClusterTPF(t *testing.T) { - var ( - resources = []func() resource.Resource{ - WrappedResource(advancedclustertpf.Resource().(BaseResourcePlanModify)), - } - mockConfig = unit.MockConfigAdvancedClusterTPF.WithResources(resources) - baseConfig = unit.NewMockPlanChecksConfig(t, &mockConfig, unit.ImportNameClusterReplicasetOneRegion) - resourceName = baseConfig.ResourceName - testCases = []unit.PlanCheckTest{ - { - ConfigFilename: "main_mongo_db_major_version_changed.tf", - Checks: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), - plancheck.ExpectUnknownValue(resourceName, tfjsonpath.New("mongo_db_version")), + instanceSizeChanged := customplanmodifier.AttributeChanges{ + "replication_specs[0]", + "replication_specs[0].region_configs[0]", + "replication_specs[0].region_configs[0].electable_specs", + "replication_specs[0].region_configs[0].electable_specs.instance_size", + "timeouts", + "timeouts.create", + } + regionConfigPath := tfjsonpath.New("replication_specs").AtSliceIndex(0).AtMapKey("region_configs").AtSliceIndex(0) + nodeCountChanged := customplanmodifier.AttributeChanges{ + "replication_specs[0]", + "replication_specs[0].region_configs[0]", + "replication_specs[0].region_configs[0].electable_specs", + "replication_specs[0].region_configs[0].electable_specs.node_count", + "timeouts", + "timeouts.create", + } + for name, tc := range map[string]unknownReplacementTestCase{ + "mongo db major version changed should show in attribute changes and mongo_db_version replace unknown should be called": { + ImportName: unit.ImportNameClusterReplicasetOneRegion, + ConfigFilename: "main_mongo_db_major_version_changed.tf", + CheckUnknowns: []tfjsonpath.Path{ + tfjsonpath.New("mongo_db_version"), + }, + attributeReplaceUnknowns: map[string]replaceUnknownTestCall{ + "mongo_db_version": func(ctx context.Context, stateValue customplanmodifier.ParsedAttrValue, req *customplanmodifier.UnknownReplacementRequest[replaceUnknownResourceInfo]) attr.Value { + return req.Unknown }, }, - { - ConfigFilename: "main_backup_enabled.tf", - Checks: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), - plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("mongo_db_version"), knownvalue.StringExact("8.0.5")), + expectedAttributeChanges: customplanmodifier.AttributeChanges{"mongo_db_major_version", "timeouts", "timeouts.create"}, + expectedKeepUnknownCalls: []string{"mongo_db_version"}, + }, + "instance_size changed should show changes in parent attributes too": { + ImportName: unit.ImportNameClusterReplicasetOneRegion, + ConfigFilename: "main_instance_size_changed.tf", + expectedAttributeChanges: instanceSizeChanged, + }, + "auto scaling removed should show changes and call replace unknown": { + ImportName: unit.ImportNameClusterReplicasetOneRegion, + ConfigFilename: "main_auto_scaling_removed_node_count_changed.tf", + CheckUnknowns: []tfjsonpath.Path{ + regionConfigPath.AtMapKey("auto_scaling"), + }, + attributeReplaceUnknowns: map[string]replaceUnknownTestCall{ + "auto_scaling": func(ctx context.Context, stateValue customplanmodifier.ParsedAttrValue, req *customplanmodifier.UnknownReplacementRequest[replaceUnknownResourceInfo]) attr.Value { + return req.Unknown }, }, - } - ) - baseConfig.TestdataPrefix = unit.PackagePath("advancedclustertpf") - unit.RunPlanCheckTests(t, baseConfig, testCases) + expectedKeepUnknownCalls: []string{"replication_specs[0].region_configs[0].auto_scaling"}, + expectedAttributeChanges: nodeCountChanged, + }, + "auto scaling removed but state value returned should update plan": { + ImportName: unit.ImportNameClusterReplicasetOneRegion, + ConfigFilename: "main_auto_scaling_removed_node_count_changed.tf", + attributeReplaceUnknowns: map[string]replaceUnknownTestCall{ + "auto_scaling": func(ctx context.Context, stateValue customplanmodifier.ParsedAttrValue, req *customplanmodifier.UnknownReplacementRequest[replaceUnknownResourceInfo]) attr.Value { + return stateValue.AsObject() + }, + }, + CheckKnownValues: []tfjsonpath.Path{ + regionConfigPath.AtMapKey("auto_scaling"), + }, + expectedKeepUnknownCalls: []string{"replication_specs[0].region_configs[0].auto_scaling"}, + expectedAttributeChanges: nodeCountChanged, + }, + "use resource info in value replacement for read_only_specs": { + ImportName: unit.ImportNameClusterReplicasetOneRegion, + ConfigFilename: "main_instance_size_changed.tf", + attributeReplaceUnknowns: map[string]replaceUnknownTestCall{ + "read_only_specs": func(ctx context.Context, stateValue customplanmodifier.ParsedAttrValue, req *customplanmodifier.UnknownReplacementRequest[replaceUnknownResourceInfo]) attr.Value { + infoValue, found := req.Info.anyMap["node_count"] + if !found { + return req.Unknown + } + newValue := customplanmodifier.ReadStateStructValue[advancedclustertpf.TFSpecsModel](ctx, req.Differ, req.Path) + newValue.NodeCount = types.Int64Value(infoValue.(int64)) + newValue.InstanceSize = types.StringUnknown() + return conversion.AsObjectValue(ctx, newValue, stateValue.AsObject().AttributeTypes(ctx)) + }, + }, + info: replaceUnknownResourceInfo{ + anyMap: map[string]any{ + "node_count": int64(99), + }, + }, + ExtraChecks: []func(string) plancheck.PlanCheck{ + func(resourceName string) plancheck.PlanCheck { + return plancheck.ExpectKnownValue(resourceName, regionConfigPath.AtMapKey("read_only_specs").AtMapKey("node_count"), knownvalue.Int64Exact(99)) + }, + }, + CheckUnknowns: []tfjsonpath.Path{ + regionConfigPath.AtMapKey("read_only_specs").AtMapKey("instance_size"), + }, + expectedKeepUnknownCalls: []string{"replication_specs[0].region_configs[0].read_only_specs"}, + expectedAttributeChanges: instanceSizeChanged, + }, + } { + t.Run(name, func(t *testing.T) { + runData := planModifyRunData{} + mockConfig := unit.MockConfigAdvancedClusterTPF.WithResources(configureResources(&tc.info, &runData, tc.attributeReplaceUnknowns)) + baseConfig := unit.NewMockPlanChecksConfig(t, &mockConfig, tc.ImportName) + baseConfig.TestdataPrefix = unit.PackagePath("advancedclustertpf") + checks := make([]plancheck.PlanCheck, 0, len(tc.CheckUnknowns)+len(tc.CheckKnownValues)+len(tc.ExtraChecks)) + for _, checkUnknown := range tc.CheckUnknowns { + checks = append(checks, plancheck.ExpectUnknownValue(baseConfig.ResourceName, checkUnknown)) + } + for _, checkKnown := range tc.CheckKnownValues { + checks = append(checks, plancheck.ExpectKnownValue(baseConfig.ResourceName, checkKnown, knownvalue.NotNull())) + } + for _, extraCheck := range tc.ExtraChecks { + checks = append(checks, extraCheck(baseConfig.ResourceName)) + } + unit.MockPlanChecksAndRun(t, baseConfig.WithPlanCheckTest(unit.PlanCheckTest{ + ConfigFilename: tc.ConfigFilename, + Checks: checks, + })) + assert.Equal(t, tc.expectedAttributeChanges, runData.attributeChanges) + assert.Equal(t, tc.expectedKeepUnknownCalls, runData.keepUnknownCalls) + }) + } } diff --git a/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_auto_scaling_removed_node_count_changed.tf b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_auto_scaling_removed_node_count_changed.tf new file mode 100644 index 0000000000..5c455951d1 --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_auto_scaling_removed_node_count_changed.tf @@ -0,0 +1,21 @@ +resource "mongodbatlas_advanced_cluster" "test" { + project_id = "111111111111111111111111" + name = "mocked-cluster" + cluster_type = "REPLICASET" + + replication_specs = [{ + region_configs = [{ + electable_specs = { + disk_size_gb = 10 + instance_size = "M10" + node_count = 5 + } + priority = 7 + provider_name = "AWS" + region_name = "US_EAST_1" + }] + }] + timeouts = { + create = "6000s" + } +} diff --git a/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_instance_size_changed.tf b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_instance_size_changed.tf new file mode 100644 index 0000000000..5026194680 --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_instance_size_changed.tf @@ -0,0 +1,26 @@ +resource "mongodbatlas_advanced_cluster" "test" { + project_id = "111111111111111111111111" + name = "mocked-cluster" + cluster_type = "REPLICASET" + + replication_specs = [{ + region_configs = [{ + auto_scaling = { + compute_enabled = false + compute_scale_down_enabled = false + disk_gb_enabled = true + } + electable_specs = { + disk_size_gb = 10 + instance_size = "M20" + node_count = 3 + } + priority = 7 + provider_name = "AWS" + region_name = "US_EAST_1" + }] + }] + timeouts = { + create = "6000s" + } +} diff --git a/internal/testutil/unit/http_mocker_plan_checks.go b/internal/testutil/unit/http_mocker_plan_checks.go index a0f634ba9d..85e3b1dbbf 100644 --- a/internal/testutil/unit/http_mocker_plan_checks.go +++ b/internal/testutil/unit/http_mocker_plan_checks.go @@ -27,8 +27,9 @@ const ( ) var ( - errToSkipApply = errors.New("avoid full apply by raising an expected error") - clusterImportID = fmt.Sprintf("%s-%s", MockedProjectID, MockedClusterName) + errToSkipApply = errors.New("avoid full apply by raising an expected error") + clusterImportID = fmt.Sprintf("%s-%s", MockedProjectID, MockedClusterName) + planCheckTestCounter = 0 importIDMapping = map[string]string{ ImportNameClusterTwoRepSpecsWithAutoScalingAndSpecs: fmt.Sprintf("%s-%s", MockedProjectID, MockedClusterName), @@ -61,8 +62,8 @@ type MockPlanChecksConfig struct { ImportName string ConfigFilename string TestdataPrefix string - MockConfig MockHTTPDataConfig Checks []plancheck.PlanCheck + MockConfig MockHTTPDataConfig } func (m *MockPlanChecksConfig) WithPlanCheckTest(testConfig PlanCheckTest) *MockPlanChecksConfig { @@ -188,7 +189,11 @@ func fillMockDataTemplate(t *testing.T, testdataPrefix, importName, planConfigFi testFileContent = bytes.ReplaceAll(testFileContent, []byte("\n"), []byte(`\n`)) templateContent = bytes.ReplaceAll(templateContent, []byte(filepath.Base(testFile)), testFileContent) } - mockDataPath := fullPath(fmt.Sprintf("%s_%s.yaml", importName, planConfigFilename)) + accClientLock.Lock() + planCheckTestCounter++ + // Create a new mock data file for each test + mockDataPath := fullPath(fmt.Sprintf("%s_%s_%d.yaml", importName, planConfigFilename, planCheckTestCounter)) + accClientLock.Unlock() err = os.WriteFile(mockDataPath, templateContent, 0o600) require.NoError(t, err) fullImportConfigBytes, err := os.ReadFile(path.Join(responseDir, "main.tf")) From ce1e5c5089bb1d64e6a769cf610ecdbb2c35021d Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Tue, 25 Mar 2025 11:07:55 +0000 Subject: [PATCH 13/39] chore: rename test --- internal/common/customplanmodifier/unknown_replacement_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/common/customplanmodifier/unknown_replacement_test.go b/internal/common/customplanmodifier/unknown_replacement_test.go index 0f9cf4e01a..ecd2e7821e 100644 --- a/internal/common/customplanmodifier/unknown_replacement_test.go +++ b/internal/common/customplanmodifier/unknown_replacement_test.go @@ -120,7 +120,7 @@ type unknownReplacementTestCase struct { expectedKeepUnknownCalls []string } -func TestWrappingAdvancedClusterTPF(t *testing.T) { +func TestReplaceUnknownLogicByWrappingAdvancedClusterTPF(t *testing.T) { instanceSizeChanged := customplanmodifier.AttributeChanges{ "replication_specs[0]", "replication_specs[0].region_configs[0]", From 9d3f7699218c17815de98cf7d3630f872692e7a1 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Tue, 25 Mar 2025 15:55:15 +0000 Subject: [PATCH 14/39] test: Add panic test for duplicate replacement names in UnknownReplacements --- .../common/customplanmodifier/unknown_replacement.go | 7 +++++-- .../customplanmodifier/unknown_replacement_test.go | 12 ++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/internal/common/customplanmodifier/unknown_replacement.go b/internal/common/customplanmodifier/unknown_replacement.go index a08a10bcf8..e5c2cc07e9 100644 --- a/internal/common/customplanmodifier/unknown_replacement.go +++ b/internal/common/customplanmodifier/unknown_replacement.go @@ -51,8 +51,11 @@ type UnknownReplacementRequest[ResourceInfo any] struct { } func (u *UnknownReplacements[ResourceInfo]) AddReplacement(name string, call UnknownReplacementCall[ResourceInfo]) { - // todo: Validate the name in the schema - // todo: Validate the name is not already in the replacements + // todo: Validate the name exists in the schema + _, existing := u.Replacements[name] + if existing { + panic(fmt.Sprintf("Replacement already exists for %s", name)) + } u.Replacements[name] = call } diff --git a/internal/common/customplanmodifier/unknown_replacement_test.go b/internal/common/customplanmodifier/unknown_replacement_test.go index ecd2e7821e..6cbf208a26 100644 --- a/internal/common/customplanmodifier/unknown_replacement_test.go +++ b/internal/common/customplanmodifier/unknown_replacement_test.go @@ -242,3 +242,15 @@ func TestReplaceUnknownLogicByWrappingAdvancedClusterTPF(t *testing.T) { }) } } + +func TestUnknownReplacements_AddReplacementSameNameShouldPanic(t *testing.T) { + unknownReplacements := customplanmodifier.UnknownReplacements[replaceUnknownResourceInfo]{ + Differ: nil, + Replacements: map[string]customplanmodifier.UnknownReplacementCall[replaceUnknownResourceInfo]{}, + Info: replaceUnknownResourceInfo{}, + } + unknownReplacements.AddReplacement("name", nil) + assert.Panics(t, func() { + unknownReplacements.AddReplacement("name", nil) + }) +} From 4d70f84698e049b69ee30dde1a53046b03ac65c4 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Wed, 26 Mar 2025 09:31:12 +0000 Subject: [PATCH 15/39] chore: Add function to check if diagnostics are non-empty --- internal/common/conversion/diags.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/common/conversion/diags.go b/internal/common/conversion/diags.go index d9b2b49de2..2ca086d883 100644 --- a/internal/common/conversion/diags.go +++ b/internal/common/conversion/diags.go @@ -49,3 +49,10 @@ func AddJSONBodyErrorToDiagnostics(msgPrefix string, err error, diags *diag.Diag errorJSON := string(errorBytes) diags.AddError(msgPrefix, errorJSON) } + +func DiagsNonEmpty(diags *diag.Diagnostics) bool { + if diags == nil { + return false + } + return len(*diags) > 0 +} From 2233dc52c99b09f37da193f94ad8275868f9a730 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Wed, 26 Mar 2025 09:31:37 +0000 Subject: [PATCH 16/39] chore: Add diagnostics field to UnknownReplacementRequest struct --- internal/common/customplanmodifier/unknown_replacement.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/common/customplanmodifier/unknown_replacement.go b/internal/common/customplanmodifier/unknown_replacement.go index e5c2cc07e9..5ad0c012d6 100644 --- a/internal/common/customplanmodifier/unknown_replacement.go +++ b/internal/common/customplanmodifier/unknown_replacement.go @@ -48,6 +48,7 @@ type UnknownReplacementRequest[ResourceInfo any] struct { Differ *PlanModifyDiffer Path path.Path Changes AttributeChanges + Diags *diag.Diagnostics } func (u *UnknownReplacements[ResourceInfo]) AddReplacement(name string, call UnknownReplacementCall[ResourceInfo]) { @@ -71,6 +72,7 @@ func (u *UnknownReplacements[ResourceInfo]) ApplyReplacements(ctx context.Contex Differ: u.Differ, Changes: u.Differ.AttributeChanges, Unknown: unknown.UnknownValue, + Diags: diags, } response := replacer(ctx, ParsedAttrValue{Value: unknown.StateValue}, req) if response.IsUnknown() { From ec64b97db36f35214ad979760787a855d3a82bb4 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Wed, 26 Mar 2025 09:32:21 +0000 Subject: [PATCH 17/39] feat: Support reading list values from plan --- .../common/customplanmodifier/plan_modify_differ.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/common/customplanmodifier/plan_modify_differ.go b/internal/common/customplanmodifier/plan_modify_differ.go index e8f014ba2a..8ed47ee354 100644 --- a/internal/common/customplanmodifier/plan_modify_differ.go +++ b/internal/common/customplanmodifier/plan_modify_differ.go @@ -117,6 +117,18 @@ func readSrcStructValue[T any](ctx context.Context, src conversion.TPFSrc, p pat } return conversion.TFModelObject[T](ctx, obj) } +func ReadPlanStructValues[T any](ctx context.Context, d *PlanModifyDiffer, p path.Path, diags *diag.Diagnostics) []T { + return readSrcStructValues[T](ctx, d.req.Plan, p, diags) +} + +func readSrcStructValues[T any](ctx context.Context, src conversion.TPFSrc, p path.Path, diags *diag.Diagnostics) []T { + var objList types.List + if localDiags := src.GetAttribute(ctx, p, &objList); len(localDiags) > 0 { + diags.Append(localDiags...) + return nil + } + return conversion.TFModelList[T](ctx, diags, objList) +} func UpdatePlanValue(ctx context.Context, diags *diag.Diagnostics, d *PlanModifyDiffer, p path.Path, value attr.Value) { diags.Append(d.resp.Plan.SetAttribute(ctx, p, value)...) From 9684396a3ac9fec11bf7dc668835ddba3db4c760 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Wed, 26 Mar 2025 09:43:35 +0000 Subject: [PATCH 18/39] chore: Add functions for parent path retrieval --- internal/common/conversion/path_helpers.go | 32 ++++++++++ .../common/conversion/path_helpers_test.go | 59 +++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/internal/common/conversion/path_helpers.go b/internal/common/conversion/path_helpers.go index e9daa2483c..c286ad3fe9 100644 --- a/internal/common/conversion/path_helpers.go +++ b/internal/common/conversion/path_helpers.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" ) @@ -77,3 +78,34 @@ func TrimLastIndex(p path.Path) string { } return p.String() } + +func TrimLastIndexPath(p path.Path) path.Path { + for { + if IsIndexValue(p) { + p = p.ParentPath() + } else { + return p + } + } +} + +func ParentPathWithIndex(p path.Path, attributeName string, diags *diag.Diagnostics) path.Path { + for { + p = p.ParentPath() + if p.Equal(path.Empty()) { + diags.AddError("Parent path not found", fmt.Sprintf("Parent attribute %s not found in path %s", attributeName, p.String())) + return p + } + if AttributeNameEquals(p, attributeName) { + return p + } + } +} + +func ParentPathNoIndex(p path.Path, attributeName string, diags *diag.Diagnostics) path.Path { + parent := ParentPathWithIndex(p, attributeName, diags) + if diags.HasError() { + return parent + } + return TrimLastIndexPath(parent) +} diff --git a/internal/common/conversion/path_helpers_test.go b/internal/common/conversion/path_helpers_test.go index 2ebf2973e4..811e9efc3f 100644 --- a/internal/common/conversion/path_helpers_test.go +++ b/internal/common/conversion/path_helpers_test.go @@ -3,6 +3,7 @@ package conversion_test import ( "testing" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" @@ -63,3 +64,61 @@ func TestPathMatches(t *testing.T) { assert.False(t, conversion.HasPrefix(path.Root("replication_specs").AtListIndex(1), prefix)) assert.True(t, conversion.HasPrefix(path.Root("replication_specs").AtListIndex(0).AtName("region_configs").AtListIndex(1), path.Empty())) } +func TestParentPathWithIndex_Found(t *testing.T) { + diags := new(diag.Diagnostics) + // Build a nested path: resource -> parent -> child + basePath := path.Root("resource") + parentPath := basePath.AtName("parent") + childPath := parentPath.AtName("child") + + assert.Equal(t, parentPath.String(), conversion.ParentPathWithIndex(childPath, "parent", diags).String()) + assert.Equal(t, basePath.String(), conversion.ParentPathWithIndex(childPath, "resource", diags).String()) + assert.Empty(t, diags, "Diagnostics should not have errors") +} + +func TestParentPathWithIndex_FoundIncludesIndex(t *testing.T) { + diags := new(diag.Diagnostics) + // Build a nested path: resource[0] -> parent[0] -> child + basePath := path.Root("resource") + parentPath := basePath.AtListIndex(0).AtName("parent") + childPath := parentPath.AtListIndex(0).AtName("child") + assert.Equal(t, "resource[0].parent[0].child", childPath.String()) + + assert.Equal(t, parentPath.AtListIndex(0).String(), conversion.ParentPathWithIndex(childPath, "parent", diags).String()) + assert.Equal(t, basePath.AtListIndex(0).String(), conversion.ParentPathWithIndex(childPath, "resource", diags).String()) + assert.Empty(t, diags, "Diagnostics should not have errors") +} + +func TestParentPathNoIndex_RemovesIndex(t *testing.T) { + diags := new(diag.Diagnostics) + // Build a nested path: resource[0] -> parent[0] -> child + basePath := path.Root("resource") + parentPath := basePath.AtListIndex(0).AtName("parent") + childPath := parentPath.AtListIndex(0).AtName("child") + assert.Equal(t, "resource[0].parent[0].child", childPath.String()) + + assert.Equal(t, parentPath.String(), conversion.ParentPathNoIndex(childPath, "parent", diags).String()) + assert.Equal(t, basePath.String(), conversion.ParentPathNoIndex(childPath, "resource", diags).String()) + assert.Empty(t, diags, "Diagnostics should not have errors") +} + +func TestParentPathWithIndex_NotFound(t *testing.T) { + diags := new(diag.Diagnostics) + // Build a path: resource -> child + basePath := path.Root("resource") + childPath := basePath.AtName("child") + + result := conversion.ParentPathWithIndex(childPath, "nonexistent", diags) + // The function should traverse to path.Empty() and add an error. + assert.True(t, result.Equal(path.Empty()), "Expected result to be empty if parent not found") + assert.True(t, diags.HasError(), "Diagnostics should have an error when parent attribute is missing") +} + +func TestParentPathWithIndex_EmptyPath(t *testing.T) { + diags := new(diag.Diagnostics) + emptyPath := path.Empty() + result := conversion.ParentPathWithIndex(emptyPath, "any", diags) + // Since the path is empty, it should immediately return empty and add error. + assert.True(t, result.Equal(path.Empty()), "Expected empty path as result from an empty input path") + assert.True(t, diags.HasError(), "Diagnostics should have an error for empty input path") +} From 938c6b4204e8a4614b79f545d130cb124c39fe1d Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Wed, 26 Mar 2025 09:43:46 +0000 Subject: [PATCH 19/39] style: fmt --- internal/common/customplanmodifier/unknown_replacement.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/common/customplanmodifier/unknown_replacement.go b/internal/common/customplanmodifier/unknown_replacement.go index 5ad0c012d6..f6c5a1d8e8 100644 --- a/internal/common/customplanmodifier/unknown_replacement.go +++ b/internal/common/customplanmodifier/unknown_replacement.go @@ -46,9 +46,9 @@ type UnknownReplacementRequest[ResourceInfo any] struct { Info ResourceInfo Unknown attr.Value Differ *PlanModifyDiffer + Diags *diag.Diagnostics Path path.Path Changes AttributeChanges - Diags *diag.Diagnostics } func (u *UnknownReplacements[ResourceInfo]) AddReplacement(name string, call UnknownReplacementCall[ResourceInfo]) { From 7e22345e7b03905a7f88431088237297495935bb Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Thu, 27 Mar 2025 10:36:26 +0000 Subject: [PATCH 20/39] feat: PR comments and enhancements --- internal/common/conversion/diags.go | 7 - internal/common/conversion/path_helpers.go | 120 +++++---- .../common/conversion/path_helpers_test.go | 63 ++--- .../common/customplanmodifier/find_changes.go | 59 ++--- .../customplanmodifier/find_changes_test.go | 120 +-------- .../customplanmodifier/plan_modify_differ.go | 118 ++++----- .../customplanmodifier/unknown_replacement.go | 121 ++++++--- .../unknown_replacement_test.go | 239 ++++++++++-------- 8 files changed, 394 insertions(+), 453 deletions(-) diff --git a/internal/common/conversion/diags.go b/internal/common/conversion/diags.go index 2ca086d883..d9b2b49de2 100644 --- a/internal/common/conversion/diags.go +++ b/internal/common/conversion/diags.go @@ -49,10 +49,3 @@ func AddJSONBodyErrorToDiagnostics(msgPrefix string, err error, diags *diag.Diag errorJSON := string(errorBytes) diags.AddError(msgPrefix, errorJSON) } - -func DiagsNonEmpty(diags *diag.Diagnostics) bool { - if diags == nil { - return false - } - return len(*diags) > 0 -} diff --git a/internal/common/conversion/path_helpers.go b/internal/common/conversion/path_helpers.go index c286ad3fe9..d12833228e 100644 --- a/internal/common/conversion/path_helpers.go +++ b/internal/common/conversion/path_helpers.go @@ -8,85 +8,56 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" ) -func LastPart(p path.Path) string { - parts := strings.Split(p.String(), ".") - return parts[len(parts)-1] -} - -func IsIndexValue(p path.Path) bool { - return IsMapIndex(p) || IsListIndex(p) || IsSetIndex(p) -} - func IsListIndex(p path.Path) bool { - lastPart := LastPart(p) - if IsMapIndex(p) { + lastPart := lastPart(p) + if IsMapIndex(p) || IsSetIndex(p) { return false } return strings.HasSuffix(lastPart, "]") } func IsMapIndex(p path.Path) bool { - lastPart := LastPart(p) + lastPart := lastPart(p) return strings.HasSuffix(lastPart, "\"]") } func IsSetIndex(p path.Path) bool { - lastPart := LastPart(p) + lastPart := lastPart(p) return strings.Contains(lastPart, "[Value(") } -func HasPrefix(p, prefix path.Path) bool { - prefixString := prefix.String() +func HasAncestor(p, ancestor path.Path) bool { + prefixString := ancestor.String() pString := p.String() return strings.HasPrefix(pString, prefixString) } -func AttributeNameEquals(p path.Path, name string) bool { - noBrackets := TrimLastIndex(p) - return noBrackets == name || strings.HasSuffix(noBrackets, fmt.Sprintf(".%s", name)) -} - func AttributeName(p path.Path) string { - noBrackets := TrimLastIndex(p) - parts := strings.Split(noBrackets, ".") + noIndex := trimLastIndex(p) + parts := strings.Split(noIndex, ".") return parts[len(parts)-1] } +// AsAddedIndex returns "" if the path is not an index otherwise it adds `+` before the index func AsAddedIndex(p path.Path) string { - parentString := p.ParentPath().ParentPath().String() - lastPart := LastPart(p) - indexWithSign := strings.Replace(lastPart, "[", "[+", 1) - if parentString == "" { - return indexWithSign + if !isIndexValue(p) { + return "" } - return parentString + "." + indexWithSign + lastPart := lastPart(p) + indexWithSign := strings.Replace(lastPart, "[", "[+", 1) + everythingExceptLast, _ := strings.CutSuffix(p.String(), lastPart) + return everythingExceptLast + indexWithSign } +// AsRemovedIndex returns "" if the path is not an index otherwise it adds `-` before the index func AsRemovedIndex(p path.Path) string { - parentString := p.ParentPath().ParentPath().String() - lastPart := LastPart(p) - indexWithSign := strings.Replace(lastPart, "[", "[-", 1) - if parentString == "" { - return indexWithSign - } - return parentString + "." + indexWithSign -} - -func TrimLastIndex(p path.Path) string { - if IsIndexValue(p) { - return p.ParentPath().String() - } - return p.String() -} - -func TrimLastIndexPath(p path.Path) path.Path { - for { - if IsIndexValue(p) { - p = p.ParentPath() - } else { - return p - } + if !isIndexValue(p) { + return "" } + lastPart := lastPart(p) + lastPartWithRemoveIndex := strings.Replace(lastPart, "[", "[-", 1) + everythingExceptLast, _ := strings.CutSuffix(p.String(), lastPart) + return everythingExceptLast + lastPartWithRemoveIndex } func ParentPathWithIndex(p path.Path, attributeName string, diags *diag.Diagnostics) path.Path { @@ -96,7 +67,7 @@ func ParentPathWithIndex(p path.Path, attributeName string, diags *diag.Diagnost diags.AddError("Parent path not found", fmt.Sprintf("Parent attribute %s not found in path %s", attributeName, p.String())) return p } - if AttributeNameEquals(p, attributeName) { + if attributeNameEquals(p, attributeName) { return p } } @@ -107,5 +78,48 @@ func ParentPathNoIndex(p path.Path, attributeName string, diags *diag.Diagnostic if diags.HasError() { return parent } - return TrimLastIndexPath(parent) + return trimLastIndexPath(parent) +} + +func AncestorPaths(p path.Path) []path.Path { + ancestors := []path.Path{} + for { + ancestor := p.ParentPath() + if ancestor.Equal(path.Empty()) { + return ancestors + } + ancestors = append(ancestors, ancestor) + p = ancestor + } +} + +func lastPart(p path.Path) string { + parts := strings.Split(p.String(), ".") + return parts[len(parts)-1] +} + +func isIndexValue(p path.Path) bool { + return IsMapIndex(p) || IsListIndex(p) || IsSetIndex(p) +} + +func attributeNameEquals(p path.Path, name string) bool { + noBrackets := trimLastIndex(p) + return noBrackets == name || strings.HasSuffix(noBrackets, fmt.Sprintf(".%s", name)) +} + +func trimLastIndex(p path.Path) string { + if isIndexValue(p) { + return p.ParentPath().String() + } + return p.String() +} + +func trimLastIndexPath(p path.Path) path.Path { + for { + if isIndexValue(p) { + p = p.ParentPath() + } else { + return p + } + } } diff --git a/internal/common/conversion/path_helpers_test.go b/internal/common/conversion/path_helpers_test.go index 811e9efc3f..abb6c79362 100644 --- a/internal/common/conversion/path_helpers_test.go +++ b/internal/common/conversion/path_helpers_test.go @@ -10,41 +10,21 @@ import ( "github.com/stretchr/testify/assert" ) -func TestIsIndexValue(t *testing.T) { - assert.True(t, conversion.IsIndexValue(path.Root("replication_specs").AtListIndex(0))) - assert.True(t, conversion.IsIndexValue(path.Root("replication_specs").AtMapKey("myKey"))) - assert.True(t, conversion.IsIndexValue(path.Root("replication_specs").AtSetValue(types.StringValue("myKey")))) - assert.False(t, conversion.IsIndexValue(path.Root("replication_specs"))) - assert.False(t, conversion.IsIndexValue(path.Root("replication_specs").AtName("id"))) -} +func TestIsIndexTypes(t *testing.T) { + listIndexPath := path.Root("replication_specs").AtListIndex(0) + mapIndexPath := path.Root("replication_specs").AtMapKey("myKey") + setIndexPath := path.Root("replication_specs").AtSetValue(types.StringValue("myKey")) + assert.True(t, conversion.IsListIndex(listIndexPath)) + assert.False(t, conversion.IsListIndex(setIndexPath)) + assert.False(t, conversion.IsListIndex(mapIndexPath)) -func TestAttributeNameEquals(t *testing.T) { - var ( - repSpecPath = path.Root("replication_specs") - regionConfigsPath = repSpecPath.AtListIndex(0).AtName("region_configs") - ) - for expectedAttribute, paths := range map[string][]path.Path{ - "replication_specs": { - repSpecPath, - repSpecPath.AtListIndex(0), - repSpecPath.AtMapKey("myKey"), - }, - "region_configs": { - regionConfigsPath, - regionConfigsPath.AtListIndex(0), - regionConfigsPath.AtMapKey("myKey"), - }, - } { - for _, p := range paths { - assert.True(t, conversion.AttributeNameEquals(p, expectedAttribute)) - } - } -} + assert.True(t, conversion.IsSetIndex(setIndexPath)) + assert.False(t, conversion.IsSetIndex(mapIndexPath)) + assert.False(t, conversion.IsSetIndex(listIndexPath)) -func TestStripSquareBrackets(t *testing.T) { - assert.Equal(t, "replication_specs", conversion.TrimLastIndex(path.Root("replication_specs").AtListIndex(0))) - assert.Equal(t, "replication_specs", conversion.TrimLastIndex(path.Root("replication_specs").AtMapKey("myKey"))) - assert.Equal(t, "replication_specs", conversion.TrimLastIndex(path.Root("replication_specs"))) + assert.True(t, conversion.IsMapIndex(mapIndexPath)) + assert.False(t, conversion.IsMapIndex(setIndexPath)) + assert.False(t, conversion.IsMapIndex(listIndexPath)) } func TestIndexMethods(t *testing.T) { @@ -53,17 +33,24 @@ func TestIndexMethods(t *testing.T) { assert.False(t, conversion.IsListIndex(path.Root("replication_specs").AtMapKey("region_configs"))) assert.Equal(t, "replication_specs[+0]", conversion.AsAddedIndex(path.Root("replication_specs").AtListIndex(0))) assert.Equal(t, "replication_specs[0].region_configs[+1]", conversion.AsAddedIndex(path.Root("replication_specs").AtListIndex(0).AtName("region_configs").AtListIndex(1))) + assert.Equal(t, "replication_specs[+\"myKey\"]", conversion.AsAddedIndex(path.Root("replication_specs").AtMapKey("myKey"))) + assert.Equal(t, "replication_specs[+Value(\"myKey\")]", conversion.AsAddedIndex(path.Root("replication_specs").AtSetValue(types.StringValue("myKey")))) assert.Equal(t, "replication_specs[-1]", conversion.AsRemovedIndex(path.Root("replication_specs").AtListIndex(1))) assert.Equal(t, "replication_specs[0].region_configs[-1]", conversion.AsRemovedIndex(path.Root("replication_specs").AtListIndex(0).AtName("region_configs").AtListIndex(1))) + assert.Equal(t, "replication_specs[-\"myKey\"]", conversion.AsRemovedIndex(path.Root("replication_specs").AtMapKey("myKey"))) + assert.Equal(t, "replication_specs[-Value(\"myKey\")]", conversion.AsRemovedIndex(path.Root("replication_specs").AtSetValue(types.StringValue("myKey")))) + assert.Equal(t, "advanced_configuration.custom_openssl_cipher_config_tls12[-Value(\"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\")]", conversion.AsRemovedIndex(path.Root("advanced_configuration").AtName("custom_openssl_cipher_config_tls12").AtSetValue(types.StringValue("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384")))) + assert.Equal(t, "", conversion.AsRemovedIndex(path.Root("replication_specs"))) } -func TestPathMatches(t *testing.T) { +func TestHasAncestor(t *testing.T) { prefix := path.Root("replication_specs").AtListIndex(0) - assert.True(t, conversion.HasPrefix(path.Root("replication_specs").AtListIndex(0), prefix)) - assert.True(t, conversion.HasPrefix(path.Root("replication_specs").AtListIndex(0).AtName("region_configs"), prefix)) - assert.False(t, conversion.HasPrefix(path.Root("replication_specs").AtListIndex(1), prefix)) - assert.True(t, conversion.HasPrefix(path.Root("replication_specs").AtListIndex(0).AtName("region_configs").AtListIndex(1), path.Empty())) + assert.True(t, conversion.HasAncestor(path.Root("replication_specs").AtListIndex(0), prefix)) + assert.True(t, conversion.HasAncestor(path.Root("replication_specs").AtListIndex(0).AtName("region_configs"), prefix)) + assert.False(t, conversion.HasAncestor(path.Root("replication_specs").AtListIndex(1), prefix)) + assert.True(t, conversion.HasAncestor(path.Root("replication_specs").AtListIndex(0).AtName("region_configs").AtListIndex(1), path.Empty())) } + func TestParentPathWithIndex_Found(t *testing.T) { diags := new(diag.Diagnostics) // Build a nested path: resource -> parent -> child diff --git a/internal/common/customplanmodifier/find_changes.go b/internal/common/customplanmodifier/find_changes.go index f81109c0f5..67a944ecf5 100644 --- a/internal/common/customplanmodifier/find_changes.go +++ b/internal/common/customplanmodifier/find_changes.go @@ -2,17 +2,14 @@ package customplanmodifier import ( "fmt" + "slices" "strings" ) type AttributeChanges []string -func (a AttributeChanges) LeafChanges() map[string]struct{} { - return a.leafChanges(true) -} - func (a AttributeChanges) AttributeChanged(name string) bool { - changes := a.LeafChanges() + changes := a.allAttributeNameChanges() _, found := changes[name] return found } @@ -29,17 +26,15 @@ func (a AttributeChanges) KeepUnknown(attributeEffectedMapping map[string][]stri } // ListIndexChanged returns true if the list at the given index has changed, false if it was added or removed -func (a AttributeChanges) ListIndexChanged(name string, index int) bool { - leafChanges := a.leafChanges(false) - indexPath := fmt.Sprintf("%s[%d]", name, index) - _, found := leafChanges[indexPath] - return found +func (a AttributeChanges) ListIndexChanged(fullPath string, index int) bool { + indexPath := fmt.Sprintf("%s[%d]", fullPath, index) + return slices.Contains(a, indexPath) } -// NestedListLenChanges accepts a fullPath, e.g., "replication_specs[0].region_configs" and returns true if the length of the nested list has changed -func (a AttributeChanges) NestedListLenChanges(fullPath string) bool { - addPrefix := fmt.Sprintf("%s[+", fullPath) - removePrefix := fmt.Sprintf("%s[-", fullPath) +// ListLenChanges accepts a fullPath, e.g., "replication_specs[0].region_configs" and returns true if the length of the nested list has changed +func (a AttributeChanges) ListLenChanges(fullPath string) bool { + addPrefix := asAddPrefix(fullPath) + removePrefix := asRemovePrefix(fullPath) for _, change := range a { if strings.HasPrefix(change, addPrefix) || strings.HasPrefix(change, removePrefix) { return true @@ -48,28 +43,22 @@ func (a AttributeChanges) NestedListLenChanges(fullPath string) bool { return false } -func (a AttributeChanges) ListLenChanges(name string) bool { - leafChanges := a.leafChanges(false) - addPrefix := fmt.Sprintf("%s[+", name) - removePrefix := fmt.Sprintf("%s[-", name) - for change := range leafChanges { - if strings.HasPrefix(change, addPrefix) || strings.HasPrefix(change, removePrefix) { - return true - } - } - return false -} - -func (a AttributeChanges) leafChanges(removeIndex bool) map[string]struct{} { - leafChanges := make(map[string]struct{}) +func (a AttributeChanges) allAttributeNameChanges() map[string]struct{} { + nameChanges := make(map[string]struct{}) for _, change := range a { - var leaf string parts := strings.Split(change, ".") - leaf = parts[len(parts)-1] - if removeIndex && strings.HasSuffix(leaf, "]") { - leaf = strings.Split(leaf, "[")[0] - } - leafChanges[leaf] = struct{}{} + attributeName := parts[len(parts)-1] + nameChanges[attributeName] = struct{}{} } - return leafChanges + return nameChanges +} + +// asAddPrefix must match conversion.AsAddedIndex +func asAddPrefix(p string) string { + return fmt.Sprintf("%s[+", p) +} + +// asRemovePrefix must match conversion.AsRemovedIndex +func asRemovePrefix(p string) string { + return fmt.Sprintf("%s[-", p) } diff --git a/internal/common/customplanmodifier/find_changes_test.go b/internal/common/customplanmodifier/find_changes_test.go index 7b746ad846..5c3400466c 100644 --- a/internal/common/customplanmodifier/find_changes_test.go +++ b/internal/common/customplanmodifier/find_changes_test.go @@ -7,62 +7,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestAttributeChanges_LeafChanges(t *testing.T) { - tests := map[string]struct { - expected map[string]struct{} - changes customplanmodifier.AttributeChanges - }{ - "empty changes": { - changes: []string{}, - expected: map[string]struct{}{}, - }, - "single level changes": { - changes: []string{"name", "description"}, - expected: map[string]struct{}{ - "name": {}, - "description": {}, - }, - }, - "nested changes": { - changes: []string{"config.name", "settings.enabled"}, - expected: map[string]struct{}{ - "name": {}, - "enabled": {}, - }, - }, - "mixed level changes": { - changes: []string{"name", "config.type", "settings.auth.enabled"}, - expected: map[string]struct{}{ - "name": {}, - "type": {}, - "enabled": {}, - }, - }, - "list changes": { - changes: []string{"replication_specs", "replication_specs[0]", "replication_specs[0].zone_name"}, - expected: map[string]struct{}{ - "replication_specs": {}, - "zone_name": {}, - }, - }, - "nested list changes": { - changes: []string{"replication_specs", "replication_specs[0]", "replication_specs[0].region_configs", "replication_specs[0].region_configs[0].region_name"}, - expected: map[string]struct{}{ - "replication_specs": {}, - "region_name": {}, - "region_configs": {}, - }, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - actual := tc.changes.LeafChanges() - assert.Equal(t, tc.expected, actual) - }) - } -} - func TestAttributeChanges_AttributeChanged(t *testing.T) { tests := map[string]struct { attr string @@ -156,61 +100,7 @@ func TestAttributeChanges_KeepUnknown(t *testing.T) { }) } } -func TestAttributeChanges_ListLenChanges(t *testing.T) { - tests := map[string]struct { - name string - changes customplanmodifier.AttributeChanges - expected bool - }{ - "empty changes": { - name: "replication_specs", - changes: []string{}, - expected: false, - }, - "no list changes": { - name: "replication_specs", - changes: []string{"name", "description"}, - expected: false, - }, - "add element": { - name: "replication_specs", - changes: []string{"replication_specs[+0]", "replication_specs[0].zone_name"}, - expected: true, - }, - "remove element": { - name: "replication_specs", - changes: []string{"replication_specs[-1]", "replication_specs[0].zone_name"}, - expected: true, - }, - "modify without length change": { - name: "replication_specs", - changes: []string{"replication_specs[0].zone_name", "replication_specs[0].priority"}, - expected: false, - }, - "multiple list operations": { - name: "replication_specs", - changes: []string{"replication_specs[+0]", "replication_specs[-1]", "replication_specs[0].zone_name"}, - expected: true, - }, - "different list name": { - name: "other_list", - changes: []string{"replication_specs[+0]", "replication_specs[-1]"}, - expected: false, - }, - "nested list": { - name: "region_configs", - changes: []string{"replication_specs.region_configs[+0]", "replication_specs.region_configs[0].region_name"}, - expected: true, - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - actual := tc.changes.ListLenChanges(tc.name) - assert.Equal(t, tc.expected, actual) - }) - } -} func TestAttributeChanges_ListIndexChanged(t *testing.T) { tests := map[string]struct { name string @@ -255,15 +145,15 @@ func TestAttributeChanges_ListIndexChanged(t *testing.T) { expected: false, }, "nested list": { - name: "region_configs", + name: "replication_specs[0].region_configs", index: 0, - changes: []string{"replication_specs.region_configs[0]", "replication_specs.region_configs[0].priority"}, + changes: []string{"replication_specs[0].region_configs[0]", "replication_specs[0].region_configs[0].priority"}, expected: true, }, "nested list false": { - name: "region_configs", + name: "replication_specs[0].region_configs", index: 1, - changes: []string{"replication_specs.region_configs[0]", "replication_specs.region_configs[0].priority"}, + changes: []string{"replication_specs[0].region_configs[0]", "replication_specs[0].region_configs[0].priority"}, expected: false, }, "index beyond bounds": { @@ -335,7 +225,7 @@ func TestAttributeChanges_NestedListLenChanges(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { - actual := tc.changes.NestedListLenChanges(tc.fullPath) + actual := tc.changes.ListLenChanges(tc.fullPath) assert.Equal(t, tc.expected, actual) }) } diff --git a/internal/common/customplanmodifier/plan_modify_differ.go b/internal/common/customplanmodifier/plan_modify_differ.go index 8ed47ee354..ad5ec256b7 100644 --- a/internal/common/customplanmodifier/plan_modify_differ.go +++ b/internal/common/customplanmodifier/plan_modify_differ.go @@ -10,101 +10,77 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" ) -func NewPlanModifyDiffer(ctx context.Context, req *resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse, schema conversion.TPFSchema) *PlanModifyDiffer { - diags := &resp.Diagnostics - diffStatePlan, err := req.State.Raw.Diff(resp.Plan.Raw) +func NewPlanModifyDiffer(ctx context.Context, state *tfsdk.State, plan *tfsdk.Plan, diags *diag.Diagnostics, schema conversion.TPFSchema) *PlanModifyDiffer { + diffStatePlan, err := state.Raw.Diff(plan.Raw) if err != nil { diags.AddError("Error diffing state and plan", err.Error()) return nil } - diffStateConfig, err := req.State.Raw.Diff(req.Config.Raw) - if err != nil { - diags.AddError("Error diffing state and config", err.Error()) - return nil - } attributeChanges := findChanges(ctx, diffStatePlan, diags, schema) tflog.Debug(ctx, fmt.Sprintf("Attribute changes: %s\n", strings.Join(attributeChanges, "\n"))) return &PlanModifyDiffer{ - req: req, - resp: resp, - stateConfigDiff: diffStateConfig, statePlanDiff: diffStatePlan, schema: schema, + state: state, + plan: plan, AttributeChanges: attributeChanges, - PlanFullyKnown: req.Plan.Raw.IsFullyKnown(), } } type PlanModifyDiffer struct { schema conversion.TPFSchema AttributeChanges AttributeChanges - req *resource.ModifyPlanRequest - resp *resource.ModifyPlanResponse - stateConfigDiff []tftypes.ValueDiff + state *tfsdk.State + plan *tfsdk.Plan statePlanDiff []tftypes.ValueDiff - PlanFullyKnown bool -} - -func (d *PlanModifyDiffer) ParentRemoved(p path.Path) bool { - for { - parent := p.ParentPath() - if parent.Equal(path.Empty()) { - return false - } - if slices.Contains(d.AttributeChanges, conversion.AsRemovedIndex(parent)) { - return true - } - p = parent - } } type UnknownInfo struct { StateValue attr.Value UnknownValue attr.Value AttributeName string + StrPath string Path path.Path } -func (d *PlanModifyDiffer) Unknowns(ctx context.Context, diags *diag.Diagnostics) map[string]UnknownInfo { - unknowns := map[string]UnknownInfo{} +func (d *PlanModifyDiffer) Unknowns(ctx context.Context, diags *diag.Diagnostics) []UnknownInfo { + unknowns := []UnknownInfo{} schema := d.schema for _, diff := range d.statePlanDiff { - stateValue, tpfPath := conversion.AttributePathValue(ctx, diags, diff.Path, d.req.State, schema) - if d.ParentRemoved(tpfPath) { - continue - } - planValue, _ := conversion.AttributePathValue(ctx, diags, diff.Path, d.req.Plan, schema) + stateValue, tpfPath := conversion.AttributePathValue(ctx, diags, diff.Path, d.state, schema) + strPath := tpfPath.String() + planValue, _ := conversion.AttributePathValue(ctx, diags, diff.Path, d.plan, schema) if planValue == nil || !planValue.IsUnknown() { continue } - unknowns[tpfPath.String()] = UnknownInfo{ + unknowns = append(unknowns, UnknownInfo{ Path: tpfPath, + StrPath: strPath, StateValue: stateValue, UnknownValue: planValue, AttributeName: conversion.AttributeName(tpfPath), - } + }) } + slices.SortFunc(unknowns, func(i, j UnknownInfo) int { + return strings.Compare(i.StrPath, j.StrPath) + }) return unknowns } -func ReadConfigStructValue[T any](ctx context.Context, d *PlanModifyDiffer, p path.Path) *T { - return readSrcStructValue[T](ctx, d.req.Config, p) -} - func ReadPlanStructValue[T any](ctx context.Context, d *PlanModifyDiffer, p path.Path) *T { - return readSrcStructValue[T](ctx, d.req.Plan, p) + return readSrcStructValue[T](ctx, d.plan, p) } func ReadStateStructValue[T any](ctx context.Context, d *PlanModifyDiffer, p path.Path) *T { - return readSrcStructValue[T](ctx, d.req.State, p) + return readSrcStructValue[T](ctx, d.state, p) } func readSrcStructValue[T any](ctx context.Context, src conversion.TPFSrc, p path.Path) *T { @@ -118,7 +94,7 @@ func readSrcStructValue[T any](ctx context.Context, src conversion.TPFSrc, p pat return conversion.TFModelObject[T](ctx, obj) } func ReadPlanStructValues[T any](ctx context.Context, d *PlanModifyDiffer, p path.Path, diags *diag.Diagnostics) []T { - return readSrcStructValues[T](ctx, d.req.Plan, p, diags) + return readSrcStructValues[T](ctx, d.plan, p, diags) } func readSrcStructValues[T any](ctx context.Context, src conversion.TPFSrc, p path.Path, diags *diag.Diagnostics) []T { @@ -131,34 +107,50 @@ func readSrcStructValues[T any](ctx context.Context, src conversion.TPFSrc, p pa } func UpdatePlanValue(ctx context.Context, diags *diag.Diagnostics, d *PlanModifyDiffer, p path.Path, value attr.Value) { - diags.Append(d.resp.Plan.SetAttribute(ctx, p, value)...) + diags.Append(d.plan.SetAttribute(ctx, p, value)...) } func findChanges(ctx context.Context, diff []tftypes.ValueDiff, diags *diag.Diagnostics, schema conversion.TPFSchema) AttributeChanges { changes := make(map[string]bool) - addChangeAndParentChanges := func(change string) { - changes[change] = true - parts := strings.Split(change, ".") - for i := range parts[:len(parts)-1] { - changes[strings.Join(parts[:len(parts)-1-i], ".")] = true + addChangeAndAncestorChanges := func(change path.Path) { + changes[change.String()] = true + for _, p := range conversion.AncestorPaths(change) { + changes[p.String()] = true + } + } + // avoids adding change for removed region_configs inside a removed replication_specs + isAncestorRemoved := func(p path.Path) bool { + for _, a := range conversion.AncestorPaths(p) { + if conversion.IsListIndex(a) { + if _, found := changes[conversion.AsRemovedIndex(a)]; found { + return true + } + } } + return false } for _, d := range diff { p, localDiags := conversion.AttributePath(ctx, d.Path, schema) - if conversion.IsListIndex(p) { - if d.Value1 == nil { - addChangeAndParentChanges(conversion.AsAddedIndex(p)) - } - if d.Value2 == nil { - addChangeAndParentChanges(conversion.AsRemovedIndex(p)) - } + if localDiags.HasError() { + diags.Append(localDiags...) + continue } + // Two types of changes from a diff + // 1. It is defined in the plan AND it is Known and not null + // 2. It is a removed list index. (set or map index we ignore, e.g., replication_specs[0].container_id[-\"AWS:US_EAST_1\"] or advanced_configuration.custom_openssl_cipher_config_tls12[-Value(\"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\")]) + // If we use schema.SetNestedAttribute we might need to see if those changes are detected if d.Value2 != nil && d.Value2.IsKnown() && !d.Value2.IsNull() { - if localDiags.HasError() { - diags.Append(localDiags...) - continue + addChangeAndAncestorChanges(p) + } + if conversion.IsListIndex(p) { + isAdd := d.Value1 == nil + if isAdd { + changes[conversion.AsAddedIndex(p)] = true + } + isRemove := d.Value2 == nil + if isRemove && !isAncestorRemoved(p) { + changes[conversion.AsRemovedIndex(p)] = true } - addChangeAndParentChanges(p.String()) } } return slices.Sorted(maps.Keys(changes)) // Ensure changes are sorted to support top-down processing, for example read_only_spec is processed before read_only_spec.disk_size_gb diff --git a/internal/common/customplanmodifier/unknown_replacement.go b/internal/common/customplanmodifier/unknown_replacement.go index f6c5a1d8e8..1fce64a322 100644 --- a/internal/common/customplanmodifier/unknown_replacement.go +++ b/internal/common/customplanmodifier/unknown_replacement.go @@ -3,19 +3,21 @@ package customplanmodifier import ( "context" "fmt" + "slices" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" ) -func NewUnknownReplacements[ResourceInfo any](ctx context.Context, req *resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse, schema conversion.TPFSchema, info ResourceInfo) *UnknownReplacements[ResourceInfo] { +func NewUnknownReplacements[ResourceInfo any](ctx context.Context, state *tfsdk.State, plan *tfsdk.Plan, diags *diag.Diagnostics, schema conversion.TPFSchema, info ResourceInfo) *UnknownReplacements[ResourceInfo] { + differ := NewPlanModifyDiffer(ctx, state, plan, diags, schema) return &UnknownReplacements[ResourceInfo]{ - Differ: NewPlanModifyDiffer(ctx, req, resp, schema), + Differ: differ, Info: info, Replacements: make(map[string]UnknownReplacementCall[ResourceInfo]), } @@ -27,28 +29,9 @@ type UnknownReplacements[ResourceInfo any] struct { Differ *PlanModifyDiffer Replacements map[string]UnknownReplacementCall[ResourceInfo] Info ResourceInfo -} - -// ParsedAttrValue is a wrapper around attr.Value that provides type-safe accessors to support using the same signature of functions. -type ParsedAttrValue struct { - Value attr.Value -} -func (p *ParsedAttrValue) AsString() types.String { - return p.Value.(types.String) -} - -func (p *ParsedAttrValue) AsObject() types.Object { - return p.Value.(types.Object) -} - -type UnknownReplacementRequest[ResourceInfo any] struct { - Info ResourceInfo - Unknown attr.Value - Differ *PlanModifyDiffer - Diags *diag.Diagnostics - Path path.Path - Changes AttributeChanges + keepUnknownAttributeNames []string // todo: Support validating values when adding attributes + keepUnknownsExtraCalls []func(ctx context.Context, stateValue ParsedAttrValue, req *UnknownReplacementRequest[ResourceInfo]) []string } func (u *UnknownReplacements[ResourceInfo]) AddReplacement(name string, call UnknownReplacementCall[ResourceInfo]) { @@ -60,26 +43,96 @@ func (u *UnknownReplacements[ResourceInfo]) AddReplacement(name string, call Unk u.Replacements[name] = call } +// AddKeepUnknownAlways adds the attribute name to the list of attributes that should always keep unknown values. For example connection_string or state_name. +func (u *UnknownReplacements[ResourceInfo]) AddKeepUnknownAlways(keepUnknown ...string) { + u.keepUnknownAttributeNames = append(u.keepUnknownAttributeNames, keepUnknown...) +} + +// AddKeepUnknownOnChanges adds the attribute changed and its depending attributes to the list of attributes that should keep unknown values. +// However, it does not infer dependencies. For example: instance_size --> disk_size_gb, and disk_gb --> disk_iops, doesn't mean instance_size --> disk_iops. +func (u *UnknownReplacements[ResourceInfo]) AddKeepUnknownOnChanges(attributeEffectedMapping map[string][]string) { + u.keepUnknownAttributeNames = append(u.keepUnknownAttributeNames, u.Differ.AttributeChanges.KeepUnknown(attributeEffectedMapping)...) +} + +// AddKeepUnknownsExtraCall adds a function that returns extra keepUnknown attribute names based on the path/stateValue/req (same arguments as the replacer function). +func (u *UnknownReplacements[ResourceInfo]) AddKeepUnknownsExtraCall(call func(ctx context.Context, stateValue ParsedAttrValue, req *UnknownReplacementRequest[ResourceInfo]) []string) { + u.keepUnknownsExtraCalls = append(u.keepUnknownsExtraCalls, call) +} + +// ApplyReplacements iterates over the unknown values in the plan and applies the replacement function for each unknown value. +// If there is no explicit replacement function, it will use the default replacer that respects the keepUnknown attributes. +// The calls are done top-down, for example replication_specs.*.id before replication_specs.*.region_configs.*.electable_specs +// Same levels are sorted alphabetically, for example ...region_configs.electable_specs before ...region_configs.read_only_specs func (u *UnknownReplacements[ResourceInfo]) ApplyReplacements(ctx context.Context, diags *diag.Diagnostics) { - for strPath, unknown := range u.Differ.Unknowns(ctx, diags) { + replacedPaths := []path.Path{} + ancestorHasProcessed := func(p path.Path) bool { + for _, replacedPath := range replacedPaths { + if conversion.HasAncestor(p, replacedPath) { + return true + } + } + return false + } + for _, unknown := range u.Differ.Unknowns(ctx, diags) { + strPath := unknown.StrPath replacer, ok := u.Replacements[unknown.AttributeName] if !ok { + replacer = u.defaultReplacer + } + if ancestorHasProcessed(unknown.Path) { continue } + replacedPaths = append(replacedPaths, unknown.Path) req := &UnknownReplacementRequest[ResourceInfo]{ - Info: u.Info, - Path: unknown.Path, - Differ: u.Differ, - Changes: u.Differ.AttributeChanges, - Unknown: unknown.UnknownValue, - Diags: diags, + Info: u.Info, + Path: unknown.Path, + Differ: u.Differ, + Changes: u.Differ.AttributeChanges, + Unknown: unknown.UnknownValue, + Diags: diags, + AttributeName: unknown.AttributeName, } - response := replacer(ctx, ParsedAttrValue{Value: unknown.StateValue}, req) - if response.IsUnknown() { + replacement := replacer(ctx, ParsedAttrValue{Value: unknown.StateValue}, req) + if replacement.IsUnknown() { tflog.Debug(ctx, fmt.Sprintf("Keeping unknown value in plan @ %s", strPath)) } else { tflog.Debug(ctx, fmt.Sprintf("Replacing unknown value in plan @ %s", strPath)) - UpdatePlanValue(ctx, diags, u.Differ, unknown.Path, response) + UpdatePlanValue(ctx, diags, u.Differ, unknown.Path, replacement) } } } + +func (u *UnknownReplacements[ResourceInfo]) defaultReplacer(ctx context.Context, stateValue ParsedAttrValue, req *UnknownReplacementRequest[ResourceInfo]) attr.Value { + keepUnknowns := slices.Clone(u.keepUnknownAttributeNames) + for _, call := range u.keepUnknownsExtraCalls { + keepUnknowns = append(keepUnknowns, call(ctx, stateValue, req)...) + } + if slices.Contains(keepUnknowns, req.AttributeName) { + return req.Unknown + } + return stateValue.Value +} + +// ParsedAttrValue is a wrapper around attr.Value that provides type-safe accessors to support using the same signature for all replacment functions regardless of the attribute type. +// New values can be added on demand. +type ParsedAttrValue struct { + Value attr.Value +} + +func (p *ParsedAttrValue) AsString() types.String { + return p.Value.(types.String) +} + +func (p *ParsedAttrValue) AsObject() types.Object { + return p.Value.(types.Object) +} + +type UnknownReplacementRequest[ResourceInfo any] struct { + Info ResourceInfo // Resource specific info, useful for storing shardingConfigUpgrade or other relevant info. + Unknown attr.Value // The unknown value in the plan, useful for returning if no replacement is found. + Differ *PlanModifyDiffer // Used to read the state and plan values. + Diags *diag.Diagnostics + AttributeName string // The name of the attribute, for example javascript_enabled for advanced_configuration.javascript_enabled + Path path.Path // The full path to the attribute in the plan, for example advanced_configuration.javascript_enabled + Changes AttributeChanges // The changes in the plan, useful for checking if a dependent attribute has changed. +} diff --git a/internal/common/customplanmodifier/unknown_replacement_test.go b/internal/common/customplanmodifier/unknown_replacement_test.go index 6cbf208a26..a37bb05fb5 100644 --- a/internal/common/customplanmodifier/unknown_replacement_test.go +++ b/internal/common/customplanmodifier/unknown_replacement_test.go @@ -1,16 +1,13 @@ +// To test the internals of unknownReplacements we create a new resource that wraps advanced_cluster TPF but replace the modify plan call to store the attribute changes and the calls to keepUnknown. package customplanmodifier_test import ( "context" + "slices" "testing" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/plancheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" - "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/customplanmodifier" "github.com/mongodb/terraform-provider-mongodbatlas/internal/service/advancedclustertpf" "github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/unit" @@ -33,9 +30,7 @@ type planModifyRunData struct { attributeChanges customplanmodifier.AttributeChanges } -type replaceUnknownResourceInfo struct { - anyMap map[string]any -} +type replaceUnknownResourceInfo struct{} // used to store specific info about the resource, for example upgrade request or sharding schema upgrade type replaceUnknownTestCall customplanmodifier.UnknownReplacementCall[replaceUnknownResourceInfo] @@ -62,11 +57,11 @@ func (r *rs) Configure(ctx context.Context, req resource.ConfigureRequest, resp // ModifyPlan is the only method overridden in this test. func (r *rs) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { - schema := advancedclustertpf.ResourceSchema(ctx) if req.Plan.Raw.IsFullyKnown() { return } - unknownReplacements := customplanmodifier.NewUnknownReplacements(ctx, &req, resp, schema, *r.info) + schema := advancedclustertpf.ResourceSchema(ctx) + unknownReplacements := customplanmodifier.NewUnknownReplacements(ctx, &req.State, &resp.Plan, &resp.Diagnostics, schema, *r.info) for attrName, replacer := range r.attributeReplaceUnknowns { modifiedReplacer := func(ctx context.Context, stateValue customplanmodifier.ParsedAttrValue, req *customplanmodifier.UnknownReplacementRequest[replaceUnknownResourceInfo]) attr.Value { r.runData.keepUnknownCalls = append(r.runData.keepUnknownCalls, req.Path.String()) @@ -110,134 +105,162 @@ func configureResources(info *replaceUnknownResourceInfo, runData *planModifyRun type unknownReplacementTestCase struct { attributeReplaceUnknowns map[string]replaceUnknownTestCall - info replaceUnknownResourceInfo ImportName string ConfigFilename string - CheckUnknowns []tfjsonpath.Path - CheckKnownValues []tfjsonpath.Path - ExtraChecks []func(string) plancheck.PlanCheck expectedAttributeChanges customplanmodifier.AttributeChanges expectedKeepUnknownCalls []string } +func alwaysUnknown(ctx context.Context, stateValue customplanmodifier.ParsedAttrValue, req *customplanmodifier.UnknownReplacementRequest[replaceUnknownResourceInfo]) attr.Value { + return req.Unknown +} + +func alwaysState(ctx context.Context, stateValue customplanmodifier.ParsedAttrValue, req *customplanmodifier.UnknownReplacementRequest[replaceUnknownResourceInfo]) attr.Value { + return stateValue.AsObject() +} + func TestReplaceUnknownLogicByWrappingAdvancedClusterTPF(t *testing.T) { - instanceSizeChanged := customplanmodifier.AttributeChanges{ - "replication_specs[0]", - "replication_specs[0].region_configs[0]", - "replication_specs[0].region_configs[0].electable_specs", - "replication_specs[0].region_configs[0].electable_specs.instance_size", - "timeouts", - "timeouts.create", - } - regionConfigPath := tfjsonpath.New("replication_specs").AtSliceIndex(0).AtMapKey("region_configs").AtSliceIndex(0) - nodeCountChanged := customplanmodifier.AttributeChanges{ - "replication_specs[0]", - "replication_specs[0].region_configs[0]", - "replication_specs[0].region_configs[0].electable_specs", - "replication_specs[0].region_configs[0].electable_specs.node_count", - "timeouts", - "timeouts.create", - } + var ( + attributeReplaceUnknowns = map[string]replaceUnknownTestCall{ + "auto_scaling": alwaysState, + "analytics_auto_scaling": alwaysUnknown, + "compute_enabled": alwaysUnknown, // Should never be called since auto_scaling/analytics_auto_scaling are called first + "id": alwaysUnknown, + } + defaultReplaceUnknownCalls = []string{ + "replication_specs[0].id", + "replication_specs[0].region_configs[0].analytics_auto_scaling", + } + defaultAttributeChanges = []string{ + "timeouts", + "timeouts.create", + } + ) for name, tc := range map[string]unknownReplacementTestCase{ - "mongo db major version changed should show in attribute changes and mongo_db_version replace unknown should be called": { + "no config changes should show the default changes and unknown calls": { + ImportName: unit.ImportNameClusterReplicasetOneRegion, + ConfigFilename: "main.tf", + expectedKeepUnknownCalls: defaultReplaceUnknownCalls, + expectedAttributeChanges: defaultAttributeChanges, + }, + "root level change should show in attribute changes": { + ImportName: unit.ImportNameClusterReplicasetOneRegion, + ConfigFilename: "main_mongo_db_major_version_changed.tf", + expectedAttributeChanges: slices.Concat([]string{"mongo_db_major_version"}, defaultAttributeChanges), + expectedKeepUnknownCalls: defaultReplaceUnknownCalls, + }, + "nested change should show changes in parent attributes too": { ImportName: unit.ImportNameClusterReplicasetOneRegion, - ConfigFilename: "main_mongo_db_major_version_changed.tf", - CheckUnknowns: []tfjsonpath.Path{ - tfjsonpath.New("mongo_db_version"), - }, - attributeReplaceUnknowns: map[string]replaceUnknownTestCall{ - "mongo_db_version": func(ctx context.Context, stateValue customplanmodifier.ParsedAttrValue, req *customplanmodifier.UnknownReplacementRequest[replaceUnknownResourceInfo]) attr.Value { - return req.Unknown - }, - }, - expectedAttributeChanges: customplanmodifier.AttributeChanges{"mongo_db_major_version", "timeouts", "timeouts.create"}, - expectedKeepUnknownCalls: []string{"mongo_db_version"}, + ConfigFilename: "main_instance_size_changed.tf", + expectedAttributeChanges: slices.Concat([]string{ + "replication_specs", + "replication_specs[0]", + "replication_specs[0].region_configs", + "replication_specs[0].region_configs[0]", + "replication_specs[0].region_configs[0].electable_specs", + "replication_specs[0].region_configs[0].electable_specs.instance_size", + }, defaultAttributeChanges), + expectedKeepUnknownCalls: defaultReplaceUnknownCalls, }, - "instance_size changed should show changes in parent attributes too": { + "auto scaling removed should call replace unknown": { ImportName: unit.ImportNameClusterReplicasetOneRegion, - ConfigFilename: "main_instance_size_changed.tf", - expectedAttributeChanges: instanceSizeChanged, + ConfigFilename: "main_auto_scaling_removed_node_count_changed.tf", + expectedKeepUnknownCalls: slices.Concat(defaultReplaceUnknownCalls, []string{"replication_specs[0].region_configs[0].auto_scaling"}), + expectedAttributeChanges: slices.Concat([]string{ + "replication_specs", + "replication_specs[0]", + "replication_specs[0].region_configs", + "replication_specs[0].region_configs[0]", + "replication_specs[0].region_configs[0].electable_specs", + "replication_specs[0].region_configs[0].electable_specs.node_count", + }, defaultAttributeChanges), }, - "auto scaling removed should show changes and call replace unknown": { + "add a region config should not call replace unknown in the new region config": { ImportName: unit.ImportNameClusterReplicasetOneRegion, - ConfigFilename: "main_auto_scaling_removed_node_count_changed.tf", - CheckUnknowns: []tfjsonpath.Path{ - regionConfigPath.AtMapKey("auto_scaling"), - }, + ConfigFilename: "main_add_region_config.tf", attributeReplaceUnknowns: map[string]replaceUnknownTestCall{ - "auto_scaling": func(ctx context.Context, stateValue customplanmodifier.ParsedAttrValue, req *customplanmodifier.UnknownReplacementRequest[replaceUnknownResourceInfo]) attr.Value { - return req.Unknown - }, + "analytics_auto_scaling": alwaysUnknown, }, - expectedKeepUnknownCalls: []string{"replication_specs[0].region_configs[0].auto_scaling"}, - expectedAttributeChanges: nodeCountChanged, + expectedAttributeChanges: slices.Concat([]string{ + "replication_specs", + "replication_specs[0]", + "replication_specs[0].region_configs", + "replication_specs[0].region_configs[+1]", + "replication_specs[0].region_configs[1]", + }, defaultAttributeChanges), + expectedKeepUnknownCalls: defaultReplaceUnknownCalls, }, - "auto scaling removed but state value returned should update plan": { + "add a replication spec should not call replace unknown in the new replication spec": { ImportName: unit.ImportNameClusterReplicasetOneRegion, - ConfigFilename: "main_auto_scaling_removed_node_count_changed.tf", - attributeReplaceUnknowns: map[string]replaceUnknownTestCall{ - "auto_scaling": func(ctx context.Context, stateValue customplanmodifier.ParsedAttrValue, req *customplanmodifier.UnknownReplacementRequest[replaceUnknownResourceInfo]) attr.Value { - return stateValue.AsObject() - }, - }, - CheckKnownValues: []tfjsonpath.Path{ - regionConfigPath.AtMapKey("auto_scaling"), - }, - expectedKeepUnknownCalls: []string{"replication_specs[0].region_configs[0].auto_scaling"}, - expectedAttributeChanges: nodeCountChanged, + ConfigFilename: "main_add_replication_spec.tf", + expectedAttributeChanges: slices.Concat([]string{ + "replication_specs", + "replication_specs[+1]", + "replication_specs[1]", + }, defaultAttributeChanges), + expectedKeepUnknownCalls: defaultReplaceUnknownCalls, }, - "use resource info in value replacement for read_only_specs": { + "add mapAttribute (tags) should show in attributeChanges but not with '+'": { ImportName: unit.ImportNameClusterReplicasetOneRegion, - ConfigFilename: "main_instance_size_changed.tf", - attributeReplaceUnknowns: map[string]replaceUnknownTestCall{ - "read_only_specs": func(ctx context.Context, stateValue customplanmodifier.ParsedAttrValue, req *customplanmodifier.UnknownReplacementRequest[replaceUnknownResourceInfo]) attr.Value { - infoValue, found := req.Info.anyMap["node_count"] - if !found { - return req.Unknown - } - newValue := customplanmodifier.ReadStateStructValue[advancedclustertpf.TFSpecsModel](ctx, req.Differ, req.Path) - newValue.NodeCount = types.Int64Value(infoValue.(int64)) - newValue.InstanceSize = types.StringUnknown() - return conversion.AsObjectValue(ctx, newValue, stateValue.AsObject().AttributeTypes(ctx)) - }, + ConfigFilename: "main_with_tags.tf", + expectedAttributeChanges: slices.Concat([]string{ + "tags", + "tags[\"id\"]", + }, defaultAttributeChanges), + expectedKeepUnknownCalls: defaultReplaceUnknownCalls, + }, + "add setAttribute (custom_openssl_cipher_config_tls12) should show in attributeChanges but not with '+'": { + ImportName: unit.ImportNameClusterReplicasetOneRegion, + ConfigFilename: "main_tls_cipher_config_mode_with_custom_openssl_cipher_config_tls12.tf", + expectedAttributeChanges: slices.Concat([]string{ + "advanced_configuration", + "advanced_configuration.custom_openssl_cipher_config_tls12", + "advanced_configuration.custom_openssl_cipher_config_tls12[Value(\"ECDHE-RSA-AES256-GCM-SHA384\")]", + "advanced_configuration.tls_cipher_config_mode", + }, defaultAttributeChanges), + expectedKeepUnknownCalls: defaultReplaceUnknownCalls, + }, + // Different ImportName to test multiple replication specs + "remove a replication_spec should not call replace unknown in removed spec": { + ImportName: unit.ImportNameClusterTwoRepSpecsWithAutoScalingAndSpecs, + ConfigFilename: "main_removed_replication_spec.tf", + expectedAttributeChanges: []string{ + "replication_specs", + "replication_specs[-1]", }, - info: replaceUnknownResourceInfo{ - anyMap: map[string]any{ - "node_count": int64(99), - }, + expectedKeepUnknownCalls: []string{ + "replication_specs[0].id", }, - ExtraChecks: []func(string) plancheck.PlanCheck{ - func(resourceName string) plancheck.PlanCheck { - return plancheck.ExpectKnownValue(resourceName, regionConfigPath.AtMapKey("read_only_specs").AtMapKey("node_count"), knownvalue.Int64Exact(99)) - }, + }, + "update replication spec1 should not show changes to replication spec0": { + ImportName: unit.ImportNameClusterTwoRepSpecsWithAutoScalingAndSpecs, + ConfigFilename: "main_removed_blocks_from_config_and_instance_change.tf", + expectedAttributeChanges: []string{ + "replication_specs", + "replication_specs[1]", + "replication_specs[1].region_configs", + "replication_specs[1].region_configs[0]", + "replication_specs[1].region_configs[0].electable_specs", + "replication_specs[1].region_configs[0].electable_specs.instance_size", }, - CheckUnknowns: []tfjsonpath.Path{ - regionConfigPath.AtMapKey("read_only_specs").AtMapKey("instance_size"), + expectedKeepUnknownCalls: []string{ + "replication_specs[0].id", + "replication_specs[0].region_configs[0].analytics_auto_scaling", + "replication_specs[0].region_configs[0].auto_scaling", + "replication_specs[1].id", + "replication_specs[1].region_configs[0].analytics_auto_scaling", + "replication_specs[1].region_configs[0].auto_scaling", }, - expectedKeepUnknownCalls: []string{"replication_specs[0].region_configs[0].read_only_specs"}, - expectedAttributeChanges: instanceSizeChanged, }, } { t.Run(name, func(t *testing.T) { runData := planModifyRunData{} - mockConfig := unit.MockConfigAdvancedClusterTPF.WithResources(configureResources(&tc.info, &runData, tc.attributeReplaceUnknowns)) + mockConfig := unit.MockConfigAdvancedClusterTPF.WithResources(configureResources(&replaceUnknownResourceInfo{}, &runData, attributeReplaceUnknowns)) baseConfig := unit.NewMockPlanChecksConfig(t, &mockConfig, tc.ImportName) baseConfig.TestdataPrefix = unit.PackagePath("advancedclustertpf") - checks := make([]plancheck.PlanCheck, 0, len(tc.CheckUnknowns)+len(tc.CheckKnownValues)+len(tc.ExtraChecks)) - for _, checkUnknown := range tc.CheckUnknowns { - checks = append(checks, plancheck.ExpectUnknownValue(baseConfig.ResourceName, checkUnknown)) - } - for _, checkKnown := range tc.CheckKnownValues { - checks = append(checks, plancheck.ExpectKnownValue(baseConfig.ResourceName, checkKnown, knownvalue.NotNull())) - } - for _, extraCheck := range tc.ExtraChecks { - checks = append(checks, extraCheck(baseConfig.ResourceName)) - } - unit.MockPlanChecksAndRun(t, baseConfig.WithPlanCheckTest(unit.PlanCheckTest{ - ConfigFilename: tc.ConfigFilename, - Checks: checks, - })) + unit.MockPlanChecksAndRun(t, baseConfig.WithPlanCheckTest(unit.PlanCheckTest{ConfigFilename: tc.ConfigFilename})) assert.Equal(t, tc.expectedAttributeChanges, runData.attributeChanges) + slices.Sort(runData.keepUnknownCalls) assert.Equal(t, tc.expectedKeepUnknownCalls, runData.keepUnknownCalls) }) } From 8d9093bf4772a2211f47c44461715e540b686e70 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Thu, 27 Mar 2025 10:38:06 +0000 Subject: [PATCH 21/39] chore: revert changes to tpf package --- .../advancedclustertpf/plan_modifier.go | 8 +-- .../advancedclustertpf/plan_modifier2.go | 49 ------------------- .../advancedclustertpf/plan_modifier_test.go | 30 ++---------- .../service/advancedclustertpf/resource.go | 4 +- 4 files changed, 11 insertions(+), 80 deletions(-) delete mode 100644 internal/service/advancedclustertpf/plan_modifier2.go diff --git a/internal/service/advancedclustertpf/plan_modifier.go b/internal/service/advancedclustertpf/plan_modifier.go index 58ed7d8738..a354723df6 100644 --- a/internal/service/advancedclustertpf/plan_modifier.go +++ b/internal/service/advancedclustertpf/plan_modifier.go @@ -15,9 +15,9 @@ import ( var ( // Change mappings uses `attribute_name`, it doesn't care about the nested level. attributeRootChangeMapping = map[string][]string{ - "disk_size_gb": {}, // disk_size_gb can be change at any level/spec - "replication_specs": {}, - // "mongo_db_major_version": {"mongo_db_version"}, // Using new plan modifier logic to test this + "disk_size_gb": {}, // disk_size_gb can be change at any level/spec + "replication_specs": {}, + "mongo_db_major_version": {"mongo_db_version"}, "tls_cipher_config_mode": {"custom_openssl_cipher_config_tls12"}, "cluster_type": {"config_server_management_mode", "config_server_type"}, // computed values of config server change when REPLICA_SET changes to SHARDED } @@ -62,7 +62,7 @@ func useStateForUnknowns(ctx context.Context, diags *diag.Diagnostics, state, pl return } attributeChanges := schemafunc.NewAttributeChanges(ctx, state, plan) - keepUnknown := []string{"connection_strings", "state_name", "mongo_db_version"} // Volatile attributes, should not be copied from state + keepUnknown := []string{"connection_strings", "state_name"} // Volatile attributes, should not be copied from state keepUnknown = append(keepUnknown, attributeChanges.KeepUnknown(attributeRootChangeMapping)...) keepUnknown = append(keepUnknown, determineKeepUnknownsAutoScaling(ctx, diags, state, plan)...) schemafunc.CopyUnknowns(ctx, state, plan, keepUnknown, nil) diff --git a/internal/service/advancedclustertpf/plan_modifier2.go b/internal/service/advancedclustertpf/plan_modifier2.go deleted file mode 100644 index 7302afca01..0000000000 --- a/internal/service/advancedclustertpf/plan_modifier2.go +++ /dev/null @@ -1,49 +0,0 @@ -package advancedclustertpf - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/customplanmodifier" -) - -var attributePlanModifiers = map[string]customplanmodifier.UnknownReplacementCall[PlanModifyResourceInfo]{ - "mongo_db_version": mongoDBVersionReplaceUnknown, - // TODO: Add the other computed attributes -} - -func mongoDBVersionReplaceUnknown(ctx context.Context, state customplanmodifier.ParsedAttrValue, req *customplanmodifier.UnknownReplacementRequest[PlanModifyResourceInfo]) attr.Value { - if req.Changes.AttributeChanged("mongo_db_major_version") { - return req.Unknown - } - return state.Value -} - -type PlanModifyResourceInfo struct { - AutoScalingComputedUsed bool - AutoScalingDiskUsed bool - isShardingConfigUpgrade bool -} - -func unknownReplacements(ctx context.Context, req *resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { - var plan, state TFModel - diags := &resp.Diagnostics - diags.Append(req.Plan.Get(ctx, &plan)...) - diags.Append(req.State.Get(ctx, &state)...) - if diags.HasError() { - return - } - computedUsed, diskUsed := autoScalingUsed(ctx, diags, &state, &plan) - shardingConfigUpgrade := isShardingConfigUpgrade(ctx, &state, &plan, diags) - info := PlanModifyResourceInfo{ - AutoScalingComputedUsed: computedUsed, - AutoScalingDiskUsed: diskUsed, - isShardingConfigUpgrade: shardingConfigUpgrade, - } - unknownReplacements := customplanmodifier.NewUnknownReplacements(ctx, req, resp, ResourceSchema(ctx), info) - for attrName, replacer := range attributePlanModifiers { - unknownReplacements.AddReplacement(attrName, replacer) - } - unknownReplacements.ApplyReplacements(ctx, diags) -} diff --git a/internal/service/advancedclustertpf/plan_modifier_test.go b/internal/service/advancedclustertpf/plan_modifier_test.go index 862b67de53..c0ce795bd9 100644 --- a/internal/service/advancedclustertpf/plan_modifier_test.go +++ b/internal/service/advancedclustertpf/plan_modifier_test.go @@ -69,29 +69,9 @@ func TestPlanChecksClusterTwoRepSpecsWithAutoScalingAndSpecs(t *testing.T) { }, } ) - unit.RunPlanCheckTests(t, baseConfig, testCases) -} - -func TestMockPlanChecks_ClusterReplicasetOneRegion(t *testing.T) { - var ( - baseConfig = unit.NewMockPlanChecksConfig(t, &mockConfig, unit.ImportNameClusterReplicasetOneRegion) - resourceName = baseConfig.ResourceName - testCases = []unit.PlanCheckTest{ - { - ConfigFilename: "main_mongo_db_major_version_changed.tf", - Checks: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), - plancheck.ExpectUnknownValue(resourceName, tfjsonpath.New("mongo_db_version")), - }, - }, - { - ConfigFilename: "main_backup_enabled.tf", - Checks: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), - plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("mongo_db_version"), knownvalue.StringExact("8.0.5")), - }, - }, - } - ) - unit.RunPlanCheckTests(t, baseConfig, testCases) + for _, testCase := range testCases { + t.Run(testCase.ConfigFilename, func(t *testing.T) { + unit.MockPlanChecksAndRun(t, baseConfig.WithPlanCheckTest(testCase)) + }) + } } diff --git a/internal/service/advancedclustertpf/resource.go b/internal/service/advancedclustertpf/resource.go index 117ee39223..a264d08f38 100644 --- a/internal/service/advancedclustertpf/resource.go +++ b/internal/service/advancedclustertpf/resource.go @@ -108,16 +108,16 @@ func (r *rs) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, res if diags.HasError() { return } + useStateForUnknowns(ctx, diags, &state, &plan) if diags.HasError() { return } diags.Append(resp.Plan.Set(ctx, plan)...) - unknownReplacements(ctx, &req, resp) } func (r *rs) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = ResourceSchema(ctx) + resp.Schema = resourceSchema(ctx) conversion.UpdateSchemaDescription(&resp.Schema) } From d710412aeff049c3e3530a242210965e705ac513 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Thu, 27 Mar 2025 10:42:32 +0000 Subject: [PATCH 22/39] chore: Add test files and refactor --- .../service/advancedclustertpf/resource.go | 2 +- .../advancedclustertpf/testdata/.gitignore | 2 - .../main_add_region_config.tf | 43 +++++++++++++++++++ .../main_add_replication_spec.tf | 43 +++++++++++++++++++ ...with_custom_openssl_cipher_config_tls12.tf | 30 +++++++++++++ .../main_with_tags.tf | 29 +++++++++++++ .../main_removed_replication_spec.tf | 38 ++++++++++++++++ 7 files changed, 184 insertions(+), 3 deletions(-) delete mode 100644 internal/service/advancedclustertpf/testdata/.gitignore create mode 100644 internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_add_region_config.tf create mode 100644 internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_add_replication_spec.tf create mode 100644 internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_tls_cipher_config_mode_with_custom_openssl_cipher_config_tls12.tf create mode 100644 internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_with_tags.tf create mode 100644 internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/main_removed_replication_spec.tf diff --git a/internal/service/advancedclustertpf/resource.go b/internal/service/advancedclustertpf/resource.go index a264d08f38..1559736cbc 100644 --- a/internal/service/advancedclustertpf/resource.go +++ b/internal/service/advancedclustertpf/resource.go @@ -117,7 +117,7 @@ func (r *rs) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, res } func (r *rs) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = resourceSchema(ctx) + resp.Schema = ResourceSchema(ctx) conversion.UpdateSchemaDescription(&resp.Schema) } diff --git a/internal/service/advancedclustertpf/testdata/.gitignore b/internal/service/advancedclustertpf/testdata/.gitignore deleted file mode 100644 index 89b68a3a20..0000000000 --- a/internal/service/advancedclustertpf/testdata/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -ClusterTwoRepSpecsWithAutoScalingAndSpecs_*.yaml -ClusterReplicasetOneRegion_*.yaml diff --git a/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_add_region_config.tf b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_add_region_config.tf new file mode 100644 index 0000000000..016a7f9ba4 --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_add_region_config.tf @@ -0,0 +1,43 @@ +resource "mongodbatlas_advanced_cluster" "test" { + project_id = "111111111111111111111111" + name = "mocked-cluster" + cluster_type = "REPLICASET" + + replication_specs = [{ + region_configs = [ + { + auto_scaling = { + compute_enabled = false + compute_scale_down_enabled = false + disk_gb_enabled = true + } + electable_specs = { + disk_size_gb = 10 + instance_size = "M10" + node_count = 3 + } + priority = 7 + provider_name = "AWS" + region_name = "US_EAST_1" + }, + { + auto_scaling = { + compute_enabled = false + compute_scale_down_enabled = false + disk_gb_enabled = true + } + electable_specs = { + disk_size_gb = 10 + instance_size = "M10" + node_count = 3 + } + priority = 7 + provider_name = "AWS" + region_name = "US_EAST_2" + } + ] + }] + timeouts = { + create = "6000s" + } +} diff --git a/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_add_replication_spec.tf b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_add_replication_spec.tf new file mode 100644 index 0000000000..35acf81bac --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_add_replication_spec.tf @@ -0,0 +1,43 @@ +resource "mongodbatlas_advanced_cluster" "test" { + project_id = "111111111111111111111111" + name = "mocked-cluster" + cluster_type = "REPLICASET" + + replication_specs = [{ + region_configs = [{ + auto_scaling = { + compute_enabled = false + compute_scale_down_enabled = false + disk_gb_enabled = true + } + electable_specs = { + disk_size_gb = 10 + instance_size = "M10" + node_count = 3 + } + priority = 7 + provider_name = "AWS" + region_name = "US_EAST_1" + }] + }, + { + region_configs = [{ + auto_scaling = { + compute_enabled = false + compute_scale_down_enabled = false + disk_gb_enabled = true + } + electable_specs = { + disk_size_gb = 10 + instance_size = "M10" + node_count = 3 + } + priority = 7 + provider_name = "AWS" + region_name = "US_EAST_1" + }] + }] + timeouts = { + create = "6000s" + } +} diff --git a/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_tls_cipher_config_mode_with_custom_openssl_cipher_config_tls12.tf b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_tls_cipher_config_mode_with_custom_openssl_cipher_config_tls12.tf new file mode 100644 index 0000000000..d3806d8cae --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_tls_cipher_config_mode_with_custom_openssl_cipher_config_tls12.tf @@ -0,0 +1,30 @@ +resource "mongodbatlas_advanced_cluster" "test" { + project_id = "111111111111111111111111" + name = "mocked-cluster" + cluster_type = "REPLICASET" + + replication_specs = [{ + region_configs = [{ + auto_scaling = { + compute_enabled = false + compute_scale_down_enabled = false + disk_gb_enabled = true + } + electable_specs = { + disk_size_gb = 10 + instance_size = "M10" + node_count = 3 + } + priority = 7 + provider_name = "AWS" + region_name = "US_EAST_1" + }] + }] + timeouts = { + create = "6000s" + } + advanced_configuration = { + tls_cipher_config_mode = "CUSTOM" + custom_openssl_cipher_config_tls12 = ["ECDHE-RSA-AES256-GCM-SHA384"] + } +} diff --git a/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_with_tags.tf b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_with_tags.tf new file mode 100644 index 0000000000..98ae87a56e --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterReplicasetOneRegion/main_with_tags.tf @@ -0,0 +1,29 @@ +resource "mongodbatlas_advanced_cluster" "test" { + project_id = "111111111111111111111111" + name = "mocked-cluster" + cluster_type = "REPLICASET" + + replication_specs = [{ + region_configs = [{ + auto_scaling = { + compute_enabled = false + compute_scale_down_enabled = false + disk_gb_enabled = true + } + electable_specs = { + disk_size_gb = 10 + instance_size = "M10" + node_count = 3 + } + priority = 7 + provider_name = "AWS" + region_name = "US_EAST_1" + }] + }] + timeouts = { + create = "6000s" + } + tags = { + id = "value" + } +} diff --git a/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/main_removed_replication_spec.tf b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/main_removed_replication_spec.tf new file mode 100644 index 0000000000..dd4812441e --- /dev/null +++ b/internal/service/advancedclustertpf/testdata/ClusterTwoRepSpecsWithAutoScalingAndSpecs/main_removed_replication_spec.tf @@ -0,0 +1,38 @@ +resource "mongodbatlas_advanced_cluster" "test" { + project_id = "111111111111111111111111" + name = "mocked-cluster" + cluster_type = "GEOSHARDED" + + + replication_specs = [{ + region_configs = [{ + analytics_auto_scaling = { + compute_enabled = true + compute_max_instance_size = "M30" + compute_min_instance_size = "M10" + compute_scale_down_enabled = true + disk_gb_enabled = true + } + auto_scaling = { + compute_enabled = true + compute_max_instance_size = "M30" + compute_min_instance_size = "M10" + compute_scale_down_enabled = true + disk_gb_enabled = true + } + electable_specs = { + instance_size = "M10" + node_count = 5 + } + priority = 7 + provider_name = "AWS" + read_only_specs = { + instance_size = "M10" + node_count = 2 + } + region_name = "US_EAST_1" + }] + zone_name = "Zone 1" + } + ] +} From 3bbec6198174fd4beedaa9ba44ee1e50f06ecd8e Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Thu, 27 Mar 2025 11:08:12 +0000 Subject: [PATCH 23/39] review comments --- internal/common/conversion/path_helpers.go | 24 +++++++------------ .../common/conversion/path_helpers_test.go | 20 +++++++++------- .../customplanmodifier/unknown_replacement.go | 4 ++-- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/internal/common/conversion/path_helpers.go b/internal/common/conversion/path_helpers.go index d12833228e..2fdb2979f3 100644 --- a/internal/common/conversion/path_helpers.go +++ b/internal/common/conversion/path_helpers.go @@ -60,7 +60,7 @@ func AsRemovedIndex(p path.Path) string { return everythingExceptLast + lastPartWithRemoveIndex } -func ParentPathWithIndex(p path.Path, attributeName string, diags *diag.Diagnostics) path.Path { +func AncestorPathWithIndex(p path.Path, attributeName string, diags *diag.Diagnostics) path.Path { for { p = p.ParentPath() if p.Equal(path.Empty()) { @@ -73,8 +73,8 @@ func ParentPathWithIndex(p path.Path, attributeName string, diags *diag.Diagnost } } -func ParentPathNoIndex(p path.Path, attributeName string, diags *diag.Diagnostics) path.Path { - parent := ParentPathWithIndex(p, attributeName, diags) +func AncestorPathNoIndex(p path.Path, attributeName string, diags *diag.Diagnostics) path.Path { + parent := AncestorPathWithIndex(p, attributeName, diags) if diags.HasError() { return parent } @@ -103,23 +103,17 @@ func isIndexValue(p path.Path) bool { } func attributeNameEquals(p path.Path, name string) bool { - noBrackets := trimLastIndex(p) - return noBrackets == name || strings.HasSuffix(noBrackets, fmt.Sprintf(".%s", name)) + return AttributeName(p) == name } func trimLastIndex(p path.Path) string { - if isIndexValue(p) { - return p.ParentPath().String() - } - return p.String() + return trimLastIndexPath(p).String() } func trimLastIndexPath(p path.Path) path.Path { - for { - if isIndexValue(p) { - p = p.ParentPath() - } else { - return p - } + if isIndexValue(p) { + return p.ParentPath() + } else { + return p } } diff --git a/internal/common/conversion/path_helpers_test.go b/internal/common/conversion/path_helpers_test.go index abb6c79362..a3bb9ba2fd 100644 --- a/internal/common/conversion/path_helpers_test.go +++ b/internal/common/conversion/path_helpers_test.go @@ -39,7 +39,9 @@ func TestIndexMethods(t *testing.T) { assert.Equal(t, "replication_specs[0].region_configs[-1]", conversion.AsRemovedIndex(path.Root("replication_specs").AtListIndex(0).AtName("region_configs").AtListIndex(1))) assert.Equal(t, "replication_specs[-\"myKey\"]", conversion.AsRemovedIndex(path.Root("replication_specs").AtMapKey("myKey"))) assert.Equal(t, "replication_specs[-Value(\"myKey\")]", conversion.AsRemovedIndex(path.Root("replication_specs").AtSetValue(types.StringValue("myKey")))) - assert.Equal(t, "advanced_configuration.custom_openssl_cipher_config_tls12[-Value(\"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\")]", conversion.AsRemovedIndex(path.Root("advanced_configuration").AtName("custom_openssl_cipher_config_tls12").AtSetValue(types.StringValue("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384")))) + setIndex := path.Root("advanced_configuration").AtName("custom_openssl_cipher_config_tls12").AtSetValue(types.StringValue("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384")) + assert.Equal(t, "advanced_configuration.custom_openssl_cipher_config_tls12[-Value(\"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\")]", conversion.AsRemovedIndex(setIndex)) + assert.Equal(t, "advanced_configuration.custom_openssl_cipher_config_tls12", conversion.AncestorPathNoIndex(setIndex, "custom_openssl_cipher_config_tls12", new(diag.Diagnostics)).String()) assert.Equal(t, "", conversion.AsRemovedIndex(path.Root("replication_specs"))) } @@ -58,8 +60,8 @@ func TestParentPathWithIndex_Found(t *testing.T) { parentPath := basePath.AtName("parent") childPath := parentPath.AtName("child") - assert.Equal(t, parentPath.String(), conversion.ParentPathWithIndex(childPath, "parent", diags).String()) - assert.Equal(t, basePath.String(), conversion.ParentPathWithIndex(childPath, "resource", diags).String()) + assert.Equal(t, parentPath.String(), conversion.AncestorPathWithIndex(childPath, "parent", diags).String()) + assert.Equal(t, basePath.String(), conversion.AncestorPathWithIndex(childPath, "resource", diags).String()) assert.Empty(t, diags, "Diagnostics should not have errors") } @@ -71,8 +73,8 @@ func TestParentPathWithIndex_FoundIncludesIndex(t *testing.T) { childPath := parentPath.AtListIndex(0).AtName("child") assert.Equal(t, "resource[0].parent[0].child", childPath.String()) - assert.Equal(t, parentPath.AtListIndex(0).String(), conversion.ParentPathWithIndex(childPath, "parent", diags).String()) - assert.Equal(t, basePath.AtListIndex(0).String(), conversion.ParentPathWithIndex(childPath, "resource", diags).String()) + assert.Equal(t, parentPath.AtListIndex(0).String(), conversion.AncestorPathWithIndex(childPath, "parent", diags).String()) + assert.Equal(t, basePath.AtListIndex(0).String(), conversion.AncestorPathWithIndex(childPath, "resource", diags).String()) assert.Empty(t, diags, "Diagnostics should not have errors") } @@ -84,8 +86,8 @@ func TestParentPathNoIndex_RemovesIndex(t *testing.T) { childPath := parentPath.AtListIndex(0).AtName("child") assert.Equal(t, "resource[0].parent[0].child", childPath.String()) - assert.Equal(t, parentPath.String(), conversion.ParentPathNoIndex(childPath, "parent", diags).String()) - assert.Equal(t, basePath.String(), conversion.ParentPathNoIndex(childPath, "resource", diags).String()) + assert.Equal(t, parentPath.String(), conversion.AncestorPathNoIndex(childPath, "parent", diags).String()) + assert.Equal(t, basePath.String(), conversion.AncestorPathNoIndex(childPath, "resource", diags).String()) assert.Empty(t, diags, "Diagnostics should not have errors") } @@ -95,7 +97,7 @@ func TestParentPathWithIndex_NotFound(t *testing.T) { basePath := path.Root("resource") childPath := basePath.AtName("child") - result := conversion.ParentPathWithIndex(childPath, "nonexistent", diags) + result := conversion.AncestorPathWithIndex(childPath, "nonexistent", diags) // The function should traverse to path.Empty() and add an error. assert.True(t, result.Equal(path.Empty()), "Expected result to be empty if parent not found") assert.True(t, diags.HasError(), "Diagnostics should have an error when parent attribute is missing") @@ -104,7 +106,7 @@ func TestParentPathWithIndex_NotFound(t *testing.T) { func TestParentPathWithIndex_EmptyPath(t *testing.T) { diags := new(diag.Diagnostics) emptyPath := path.Empty() - result := conversion.ParentPathWithIndex(emptyPath, "any", diags) + result := conversion.AncestorPathWithIndex(emptyPath, "any", diags) // Since the path is empty, it should immediately return empty and add error. assert.True(t, result.Equal(path.Empty()), "Expected empty path as result from an empty input path") assert.True(t, diags.HasError(), "Diagnostics should have an error for empty input path") diff --git a/internal/common/customplanmodifier/unknown_replacement.go b/internal/common/customplanmodifier/unknown_replacement.go index 1fce64a322..43c7b5b077 100644 --- a/internal/common/customplanmodifier/unknown_replacement.go +++ b/internal/common/customplanmodifier/unknown_replacement.go @@ -36,8 +36,8 @@ type UnknownReplacements[ResourceInfo any] struct { func (u *UnknownReplacements[ResourceInfo]) AddReplacement(name string, call UnknownReplacementCall[ResourceInfo]) { // todo: Validate the name exists in the schema - _, existing := u.Replacements[name] - if existing { + _, found := u.Replacements[name] + if found { panic(fmt.Sprintf("Replacement already exists for %s", name)) } u.Replacements[name] = call From 9e377255bce2348f896d58bb705fc6896a79cc38 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Thu, 27 Mar 2025 11:11:11 +0000 Subject: [PATCH 24/39] refactor: Replace ParsedAttrValue with attr.Value in custom plan modifier --- .../customplanmodifier/unknown_replacement.go | 27 +++++-------------- .../unknown_replacement_test.go | 8 +++--- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/internal/common/customplanmodifier/unknown_replacement.go b/internal/common/customplanmodifier/unknown_replacement.go index 43c7b5b077..9e2f7c58fa 100644 --- a/internal/common/customplanmodifier/unknown_replacement.go +++ b/internal/common/customplanmodifier/unknown_replacement.go @@ -9,7 +9,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" ) @@ -23,7 +22,7 @@ func NewUnknownReplacements[ResourceInfo any](ctx context.Context, state *tfsdk. } } -type UnknownReplacementCall[ResourceInfo any] func(ctx context.Context, stateValue ParsedAttrValue, req *UnknownReplacementRequest[ResourceInfo]) attr.Value +type UnknownReplacementCall[ResourceInfo any] func(ctx context.Context, stateValue attr.Value, req *UnknownReplacementRequest[ResourceInfo]) attr.Value type UnknownReplacements[ResourceInfo any] struct { Differ *PlanModifyDiffer @@ -31,7 +30,7 @@ type UnknownReplacements[ResourceInfo any] struct { Info ResourceInfo keepUnknownAttributeNames []string // todo: Support validating values when adding attributes - keepUnknownsExtraCalls []func(ctx context.Context, stateValue ParsedAttrValue, req *UnknownReplacementRequest[ResourceInfo]) []string + keepUnknownsExtraCalls []func(ctx context.Context, stateValue attr.Value, req *UnknownReplacementRequest[ResourceInfo]) []string } func (u *UnknownReplacements[ResourceInfo]) AddReplacement(name string, call UnknownReplacementCall[ResourceInfo]) { @@ -55,7 +54,7 @@ func (u *UnknownReplacements[ResourceInfo]) AddKeepUnknownOnChanges(attributeEff } // AddKeepUnknownsExtraCall adds a function that returns extra keepUnknown attribute names based on the path/stateValue/req (same arguments as the replacer function). -func (u *UnknownReplacements[ResourceInfo]) AddKeepUnknownsExtraCall(call func(ctx context.Context, stateValue ParsedAttrValue, req *UnknownReplacementRequest[ResourceInfo]) []string) { +func (u *UnknownReplacements[ResourceInfo]) AddKeepUnknownsExtraCall(call func(ctx context.Context, stateValue attr.Value, req *UnknownReplacementRequest[ResourceInfo]) []string) { u.keepUnknownsExtraCalls = append(u.keepUnknownsExtraCalls, call) } @@ -92,7 +91,7 @@ func (u *UnknownReplacements[ResourceInfo]) ApplyReplacements(ctx context.Contex Diags: diags, AttributeName: unknown.AttributeName, } - replacement := replacer(ctx, ParsedAttrValue{Value: unknown.StateValue}, req) + replacement := replacer(ctx, unknown.StateValue, req) if replacement.IsUnknown() { tflog.Debug(ctx, fmt.Sprintf("Keeping unknown value in plan @ %s", strPath)) } else { @@ -102,7 +101,7 @@ func (u *UnknownReplacements[ResourceInfo]) ApplyReplacements(ctx context.Contex } } -func (u *UnknownReplacements[ResourceInfo]) defaultReplacer(ctx context.Context, stateValue ParsedAttrValue, req *UnknownReplacementRequest[ResourceInfo]) attr.Value { +func (u *UnknownReplacements[ResourceInfo]) defaultReplacer(ctx context.Context, stateValue attr.Value, req *UnknownReplacementRequest[ResourceInfo]) attr.Value { keepUnknowns := slices.Clone(u.keepUnknownAttributeNames) for _, call := range u.keepUnknownsExtraCalls { keepUnknowns = append(keepUnknowns, call(ctx, stateValue, req)...) @@ -110,21 +109,7 @@ func (u *UnknownReplacements[ResourceInfo]) defaultReplacer(ctx context.Context, if slices.Contains(keepUnknowns, req.AttributeName) { return req.Unknown } - return stateValue.Value -} - -// ParsedAttrValue is a wrapper around attr.Value that provides type-safe accessors to support using the same signature for all replacment functions regardless of the attribute type. -// New values can be added on demand. -type ParsedAttrValue struct { - Value attr.Value -} - -func (p *ParsedAttrValue) AsString() types.String { - return p.Value.(types.String) -} - -func (p *ParsedAttrValue) AsObject() types.Object { - return p.Value.(types.Object) + return stateValue } type UnknownReplacementRequest[ResourceInfo any] struct { diff --git a/internal/common/customplanmodifier/unknown_replacement_test.go b/internal/common/customplanmodifier/unknown_replacement_test.go index a37bb05fb5..143bf2efb8 100644 --- a/internal/common/customplanmodifier/unknown_replacement_test.go +++ b/internal/common/customplanmodifier/unknown_replacement_test.go @@ -63,7 +63,7 @@ func (r *rs) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, res schema := advancedclustertpf.ResourceSchema(ctx) unknownReplacements := customplanmodifier.NewUnknownReplacements(ctx, &req.State, &resp.Plan, &resp.Diagnostics, schema, *r.info) for attrName, replacer := range r.attributeReplaceUnknowns { - modifiedReplacer := func(ctx context.Context, stateValue customplanmodifier.ParsedAttrValue, req *customplanmodifier.UnknownReplacementRequest[replaceUnknownResourceInfo]) attr.Value { + modifiedReplacer := func(ctx context.Context, stateValue attr.Value, req *customplanmodifier.UnknownReplacementRequest[replaceUnknownResourceInfo]) attr.Value { r.runData.keepUnknownCalls = append(r.runData.keepUnknownCalls, req.Path.String()) return replacer(ctx, stateValue, req) } @@ -111,12 +111,12 @@ type unknownReplacementTestCase struct { expectedKeepUnknownCalls []string } -func alwaysUnknown(ctx context.Context, stateValue customplanmodifier.ParsedAttrValue, req *customplanmodifier.UnknownReplacementRequest[replaceUnknownResourceInfo]) attr.Value { +func alwaysUnknown(ctx context.Context, stateValue attr.Value, req *customplanmodifier.UnknownReplacementRequest[replaceUnknownResourceInfo]) attr.Value { return req.Unknown } -func alwaysState(ctx context.Context, stateValue customplanmodifier.ParsedAttrValue, req *customplanmodifier.UnknownReplacementRequest[replaceUnknownResourceInfo]) attr.Value { - return stateValue.AsObject() +func alwaysState(ctx context.Context, stateValue attr.Value, req *customplanmodifier.UnknownReplacementRequest[replaceUnknownResourceInfo]) attr.Value { + return stateValue } func TestReplaceUnknownLogicByWrappingAdvancedClusterTPF(t *testing.T) { From c8ce3cdd0fd06b98296a4f2b04b51a3675ce47ca Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Thu, 27 Mar 2025 11:14:16 +0000 Subject: [PATCH 25/39] refactor: Simplify trimLastIndexPath function and introduce AddKeepUnknownsCall type --- internal/common/conversion/path_helpers.go | 3 +-- internal/common/customplanmodifier/unknown_replacement.go | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/common/conversion/path_helpers.go b/internal/common/conversion/path_helpers.go index 2fdb2979f3..2da24d631f 100644 --- a/internal/common/conversion/path_helpers.go +++ b/internal/common/conversion/path_helpers.go @@ -113,7 +113,6 @@ func trimLastIndex(p path.Path) string { func trimLastIndexPath(p path.Path) path.Path { if isIndexValue(p) { return p.ParentPath() - } else { - return p } + return p } diff --git a/internal/common/customplanmodifier/unknown_replacement.go b/internal/common/customplanmodifier/unknown_replacement.go index 9e2f7c58fa..c05e1839e6 100644 --- a/internal/common/customplanmodifier/unknown_replacement.go +++ b/internal/common/customplanmodifier/unknown_replacement.go @@ -23,6 +23,7 @@ func NewUnknownReplacements[ResourceInfo any](ctx context.Context, state *tfsdk. } type UnknownReplacementCall[ResourceInfo any] func(ctx context.Context, stateValue attr.Value, req *UnknownReplacementRequest[ResourceInfo]) attr.Value +type AddKeepUnknownsCall[ResourceInfo any] func(ctx context.Context, stateValue attr.Value, req *UnknownReplacementRequest[ResourceInfo]) []string type UnknownReplacements[ResourceInfo any] struct { Differ *PlanModifyDiffer @@ -54,7 +55,7 @@ func (u *UnknownReplacements[ResourceInfo]) AddKeepUnknownOnChanges(attributeEff } // AddKeepUnknownsExtraCall adds a function that returns extra keepUnknown attribute names based on the path/stateValue/req (same arguments as the replacer function). -func (u *UnknownReplacements[ResourceInfo]) AddKeepUnknownsExtraCall(call func(ctx context.Context, stateValue attr.Value, req *UnknownReplacementRequest[ResourceInfo]) []string) { +func (u *UnknownReplacements[ResourceInfo]) AddKeepUnknownsExtraCall(call AddKeepUnknownsCall[ResourceInfo]) { u.keepUnknownsExtraCalls = append(u.keepUnknownsExtraCalls, call) } From c1ac25b6aba4411ef9065b859c76214af04a26d4 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Thu, 27 Mar 2025 12:45:43 +0000 Subject: [PATCH 26/39] refactor: Replace attributeNameEquals function with direct comparison in AncestorPathWithIndex --- internal/common/conversion/path_helpers.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/internal/common/conversion/path_helpers.go b/internal/common/conversion/path_helpers.go index 2da24d631f..c2aeae4236 100644 --- a/internal/common/conversion/path_helpers.go +++ b/internal/common/conversion/path_helpers.go @@ -67,7 +67,7 @@ func AncestorPathWithIndex(p path.Path, attributeName string, diags *diag.Diagno diags.AddError("Parent path not found", fmt.Sprintf("Parent attribute %s not found in path %s", attributeName, p.String())) return p } - if attributeNameEquals(p, attributeName) { + if AttributeName(p) == attributeName { return p } } @@ -102,10 +102,6 @@ func isIndexValue(p path.Path) bool { return IsMapIndex(p) || IsListIndex(p) || IsSetIndex(p) } -func attributeNameEquals(p path.Path, name string) bool { - return AttributeName(p) == name -} - func trimLastIndex(p path.Path) string { return trimLastIndexPath(p).String() } From 19d305b6def8761ab9690520e0e9a0d504f5a6d6 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Thu, 27 Mar 2025 13:44:32 +0000 Subject: [PATCH 27/39] refactor: Remove unused AsUnknownValue function from conversion package --- internal/common/conversion/unknown.go | 42 --------------------------- 1 file changed, 42 deletions(-) delete mode 100644 internal/common/conversion/unknown.go diff --git a/internal/common/conversion/unknown.go b/internal/common/conversion/unknown.go deleted file mode 100644 index 4440429293..0000000000 --- a/internal/common/conversion/unknown.go +++ /dev/null @@ -1,42 +0,0 @@ -package conversion - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" -) - -// https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types@v1.13.0 -func AsUnknownValue(ctx context.Context, value attr.Value) attr.Value { - switch v := value.(type) { - case types.List: - return types.ListUnknown(v.ElementType(ctx)) - case types.Object: - return types.ObjectUnknown(v.AttributeTypes(ctx)) - case types.Map: - return types.MapUnknown(v.ElementType(ctx)) - case types.Set: - return types.SetUnknown(v.ElementType(ctx)) - case types.Tuple: - return types.TupleUnknown(v.ElementTypes(ctx)) - case types.String: - return types.StringUnknown() - case types.Bool: - return types.BoolUnknown() - case types.Int64: - return types.Int64Unknown() - case types.Int32: - return types.Int32Unknown() - case types.Float64: - return types.Float64Unknown() - case types.Float32: - return types.Float32Unknown() - case types.Number: - return types.NumberUnknown() - case types.Dynamic: - return types.DynamicUnknown() - } - panic(fmt.Sprintf("Unknown value to create unknown: %v", value)) -} From 7229da8a952cc379d564770aadb5d230063fcde2 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Fri, 28 Mar 2025 08:54:29 +0000 Subject: [PATCH 28/39] pr comments --- internal/common/conversion/path_converter.go | 4 +++- internal/common/customplanmodifier/unknown_replacement.go | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/common/conversion/path_converter.go b/internal/common/conversion/path_converter.go index 458d0d6a0a..9dde96188b 100644 --- a/internal/common/conversion/path_converter.go +++ b/internal/common/conversion/path_converter.go @@ -38,7 +38,8 @@ func AttributePathValue(ctx context.Context, diags *diag.Diagnostics, attributeP return attrValue, convertedPath } -// AttributePath similar to the internal function in TPF, but simpler interface as argument and less logging +// AttributePath similar to the internal function in TPF, but simpler interface as argument and less logging. +// AttributePath in TPF repo is internal and cannot be used: https://github.com/hashicorp/terraform-plugin-framework/blob/e09ec9d169c581d2606372ecdfb0113be7e3b34f/internal/fromtftypes/attribute_path.go#L17 func AttributePath(ctx context.Context, tfType *tftypes.AttributePath, schema TPFSchema) (path.Path, diag.Diagnostics) { fwPath := path.Empty() for tfTypeStepIndex, tfTypeStep := range tfType.Steps() { @@ -114,6 +115,7 @@ func AttributePath(ctx context.Context, tfType *tftypes.AttributePath, schema TP return fwPath, nil } +// AttributePathStep in TPF repo is internal and cannot be used: https://github.com/hashicorp/terraform-plugin-framework/blob/e09ec9d169c581d2606372ecdfb0113be7e3b34f/internal/fromtftypes/attribute_path_step.go#L19 func AttributePathStep(ctx context.Context, tfType tftypes.AttributePathStep, attrType attr.Type) (path.PathStep, error) { switch tfType := tfType.(type) { case tftypes.AttributeName: diff --git a/internal/common/customplanmodifier/unknown_replacement.go b/internal/common/customplanmodifier/unknown_replacement.go index c05e1839e6..d8ff4058ae 100644 --- a/internal/common/customplanmodifier/unknown_replacement.go +++ b/internal/common/customplanmodifier/unknown_replacement.go @@ -50,8 +50,8 @@ func (u *UnknownReplacements[ResourceInfo]) AddKeepUnknownAlways(keepUnknown ... // AddKeepUnknownOnChanges adds the attribute changed and its depending attributes to the list of attributes that should keep unknown values. // However, it does not infer dependencies. For example: instance_size --> disk_size_gb, and disk_gb --> disk_iops, doesn't mean instance_size --> disk_iops. -func (u *UnknownReplacements[ResourceInfo]) AddKeepUnknownOnChanges(attributeEffectedMapping map[string][]string) { - u.keepUnknownAttributeNames = append(u.keepUnknownAttributeNames, u.Differ.AttributeChanges.KeepUnknown(attributeEffectedMapping)...) +func (u *UnknownReplacements[ResourceInfo]) AddKeepUnknownOnChanges(attributeAffectedMapping map[string][]string) { + u.keepUnknownAttributeNames = append(u.keepUnknownAttributeNames, u.Differ.AttributeChanges.KeepUnknown(attributeAffectedMapping)...) } // AddKeepUnknownsExtraCall adds a function that returns extra keepUnknown attribute names based on the path/stateValue/req (same arguments as the replacer function). From 0fe27800a8fca77d7a6f69bec354fa77ec5d407a Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Fri, 28 Mar 2025 15:25:08 +0000 Subject: [PATCH 29/39] doc: Specify ResourceInfo usage --- internal/common/customplanmodifier/unknown_replacement.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/common/customplanmodifier/unknown_replacement.go b/internal/common/customplanmodifier/unknown_replacement.go index d8ff4058ae..af244c41d9 100644 --- a/internal/common/customplanmodifier/unknown_replacement.go +++ b/internal/common/customplanmodifier/unknown_replacement.go @@ -13,6 +13,7 @@ import ( "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" ) +// NewUnknownReplacements creates a new UnknownReplacements instance. ResourceInfo is a struct for storing custom resource specific data. For example, `advanced_cluster` ResourceInfo will differ from `search_deployment` or `project` ResourceInfo func NewUnknownReplacements[ResourceInfo any](ctx context.Context, state *tfsdk.State, plan *tfsdk.Plan, diags *diag.Diagnostics, schema conversion.TPFSchema, info ResourceInfo) *UnknownReplacements[ResourceInfo] { differ := NewPlanModifyDiffer(ctx, state, plan, diags, schema) return &UnknownReplacements[ResourceInfo]{ From 8d87c2375af0584f9c4765e51d1765ee498c1736 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Fri, 28 Mar 2025 15:36:21 +0000 Subject: [PATCH 30/39] pr suggestions and clarifications --- internal/common/customplanmodifier/find_changes.go | 4 ++-- internal/common/customplanmodifier/find_changes_test.go | 2 +- internal/common/customplanmodifier/plan_modify_differ.go | 2 +- internal/common/customplanmodifier/unknown_replacement.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/common/customplanmodifier/find_changes.go b/internal/common/customplanmodifier/find_changes.go index 67a944ecf5..95c0e34feb 100644 --- a/internal/common/customplanmodifier/find_changes.go +++ b/internal/common/customplanmodifier/find_changes.go @@ -31,8 +31,8 @@ func (a AttributeChanges) ListIndexChanged(fullPath string, index int) bool { return slices.Contains(a, indexPath) } -// ListLenChanges accepts a fullPath, e.g., "replication_specs[0].region_configs" and returns true if the length of the nested list has changed -func (a AttributeChanges) ListLenChanges(fullPath string) bool { +// ListLenChanged accepts a fullPath, e.g., "replication_specs[0].region_configs" and returns true if the length of the nested list has changed +func (a AttributeChanges) ListLenChanged(fullPath string) bool { addPrefix := asAddPrefix(fullPath) removePrefix := asRemovePrefix(fullPath) for _, change := range a { diff --git a/internal/common/customplanmodifier/find_changes_test.go b/internal/common/customplanmodifier/find_changes_test.go index 5c3400466c..bbaf1bbb3f 100644 --- a/internal/common/customplanmodifier/find_changes_test.go +++ b/internal/common/customplanmodifier/find_changes_test.go @@ -225,7 +225,7 @@ func TestAttributeChanges_NestedListLenChanges(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { - actual := tc.changes.ListLenChanges(tc.fullPath) + actual := tc.changes.ListLenChanged(tc.fullPath) assert.Equal(t, tc.expected, actual) }) } diff --git a/internal/common/customplanmodifier/plan_modify_differ.go b/internal/common/customplanmodifier/plan_modify_differ.go index ad5ec256b7..e67dd05ba9 100644 --- a/internal/common/customplanmodifier/plan_modify_differ.go +++ b/internal/common/customplanmodifier/plan_modify_differ.go @@ -122,7 +122,7 @@ func findChanges(ctx context.Context, diff []tftypes.ValueDiff, diags *diag.Diag isAncestorRemoved := func(p path.Path) bool { for _, a := range conversion.AncestorPaths(p) { if conversion.IsListIndex(a) { - if _, found := changes[conversion.AsRemovedIndex(a)]; found { + if changes[conversion.AsRemovedIndex(a)]{ return true } } diff --git a/internal/common/customplanmodifier/unknown_replacement.go b/internal/common/customplanmodifier/unknown_replacement.go index af244c41d9..f8e71ba73b 100644 --- a/internal/common/customplanmodifier/unknown_replacement.go +++ b/internal/common/customplanmodifier/unknown_replacement.go @@ -36,7 +36,7 @@ type UnknownReplacements[ResourceInfo any] struct { } func (u *UnknownReplacements[ResourceInfo]) AddReplacement(name string, call UnknownReplacementCall[ResourceInfo]) { - // todo: Validate the name exists in the schema + // todo: Validate the name exists in the schema CLOUDP-309460 _, found := u.Replacements[name] if found { panic(fmt.Sprintf("Replacement already exists for %s", name)) From 47aa17fa12e8b64bd509ce7ead4ba9b168af5a04 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Fri, 28 Mar 2025 15:44:23 +0000 Subject: [PATCH 31/39] chore: lint fixes and formatting --- internal/common/conversion/path_helpers_test.go | 2 +- internal/common/customplanmodifier/plan_modify_differ.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/common/conversion/path_helpers_test.go b/internal/common/conversion/path_helpers_test.go index a3bb9ba2fd..28ba2fdc29 100644 --- a/internal/common/conversion/path_helpers_test.go +++ b/internal/common/conversion/path_helpers_test.go @@ -42,7 +42,7 @@ func TestIndexMethods(t *testing.T) { setIndex := path.Root("advanced_configuration").AtName("custom_openssl_cipher_config_tls12").AtSetValue(types.StringValue("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384")) assert.Equal(t, "advanced_configuration.custom_openssl_cipher_config_tls12[-Value(\"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\")]", conversion.AsRemovedIndex(setIndex)) assert.Equal(t, "advanced_configuration.custom_openssl_cipher_config_tls12", conversion.AncestorPathNoIndex(setIndex, "custom_openssl_cipher_config_tls12", new(diag.Diagnostics)).String()) - assert.Equal(t, "", conversion.AsRemovedIndex(path.Root("replication_specs"))) + assert.Empty(t, conversion.AsRemovedIndex(path.Root("replication_specs"))) } func TestHasAncestor(t *testing.T) { diff --git a/internal/common/customplanmodifier/plan_modify_differ.go b/internal/common/customplanmodifier/plan_modify_differ.go index e67dd05ba9..957f9029e0 100644 --- a/internal/common/customplanmodifier/plan_modify_differ.go +++ b/internal/common/customplanmodifier/plan_modify_differ.go @@ -122,7 +122,7 @@ func findChanges(ctx context.Context, diff []tftypes.ValueDiff, diags *diag.Diag isAncestorRemoved := func(p path.Path) bool { for _, a := range conversion.AncestorPaths(p) { if conversion.IsListIndex(a) { - if changes[conversion.AsRemovedIndex(a)]{ + if changes[conversion.AsRemovedIndex(a)] { return true } } From df15b832023aaa0ae8d071b13507db37bde14919 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Tue, 1 Apr 2025 16:46:53 +0200 Subject: [PATCH 32/39] cherry-pick changes: efed2ea2e19abe31e01229fc1d418afd3149a078 --- internal/common/customplanmodifier/unknown_replacement.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/common/customplanmodifier/unknown_replacement.go b/internal/common/customplanmodifier/unknown_replacement.go index f8e71ba73b..ee2249f978 100644 --- a/internal/common/customplanmodifier/unknown_replacement.go +++ b/internal/common/customplanmodifier/unknown_replacement.go @@ -35,8 +35,9 @@ type UnknownReplacements[ResourceInfo any] struct { keepUnknownsExtraCalls []func(ctx context.Context, stateValue attr.Value, req *UnknownReplacementRequest[ResourceInfo]) []string } +// AddReplacement call will only be used if the attribute is Unknown in the plan. Only valid for `computed` attributes. func (u *UnknownReplacements[ResourceInfo]) AddReplacement(name string, call UnknownReplacementCall[ResourceInfo]) { - // todo: Validate the name exists in the schema CLOUDP-309460 + // todo: Validate the name exists in the schema and that the attribute is marked with `computed` CLOUDP-309460 _, found := u.Replacements[name] if found { panic(fmt.Sprintf("Replacement already exists for %s", name)) From 13825e5ab72cfd12a8c8f9f2deea3088cf57c393 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Mon, 31 Mar 2025 11:57:39 +0200 Subject: [PATCH 33/39] refactor: Update AncestorPath functions to return diagnostics alongside paths --- internal/common/conversion/diags.go | 27 ++++++++ internal/common/conversion/diags_test.go | 82 ++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/internal/common/conversion/diags.go b/internal/common/conversion/diags.go index d9b2b49de2..4282bc8ec4 100644 --- a/internal/common/conversion/diags.go +++ b/internal/common/conversion/diags.go @@ -2,6 +2,7 @@ package conversion import ( "encoding/json" + "strings" "github.com/hashicorp/terraform-plugin-framework/diag" sdkv2diag "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -49,3 +50,29 @@ func AddJSONBodyErrorToDiagnostics(msgPrefix string, err error, diags *diag.Diag errorJSON := string(errorBytes) diags.AddError(msgPrefix, errorJSON) } + +func FormatDiags(diags *diag.Diagnostics) string { + if diags == nil { + return "" + } + summary := []string{} + + addDiag := func(d diag.Diagnostic) { + summary = append(summary, d.Summary(), "\t detail: "+d.Detail()) + } + + for _, errorDiag := range diags.Errors() { + addDiag(errorDiag) + } + if diags.WarningsCount() > 0 { + if len(summary) > 0 { + summary = append(summary, "\nWarnings:") + } else { + summary = append(summary, "Warnings:") + } + for _, warningDiag := range diags.Warnings() { + addDiag(warningDiag) + } + } + return strings.Join(summary, "\n") +} diff --git a/internal/common/conversion/diags_test.go b/internal/common/conversion/diags_test.go index 5490134fbb..b57db3c62d 100644 --- a/internal/common/conversion/diags_test.go +++ b/internal/common/conversion/diags_test.go @@ -66,3 +66,85 @@ func TestFromTPFDiagsToSDKV2Diags(t *testing.T) { }) } } +func TestFormatDiags(t *testing.T) { + testCases := map[string]struct { + setupDiags func() *diag.Diagnostics + expectedText string + }{ + "nil diagnostics": { + setupDiags: func() *diag.Diagnostics { + return nil + }, + expectedText: "", + }, + "empty diagnostics": { + setupDiags: func() *diag.Diagnostics { + var diags diag.Diagnostics + return &diags + }, + expectedText: "", + }, + "single error": { + setupDiags: func() *diag.Diagnostics { + var diags diag.Diagnostics + diags.AddError("Error summary", "Error detail") + return &diags + }, + expectedText: "Error summary\n\t detail: Error detail", + }, + "multiple errors": { + setupDiags: func() *diag.Diagnostics { + var diags diag.Diagnostics + diags.AddError("First error", "Error detail 1") + diags.AddError("Second error", "Error detail 2") + return &diags + }, + expectedText: "First error\n\t detail: Error detail 1\nSecond error\n\t detail: Error detail 2", + }, + "single warning": { + setupDiags: func() *diag.Diagnostics { + var diags diag.Diagnostics + diags.AddWarning("Warning summary", "Warning detail") + return &diags + }, + expectedText: "Warnings:\nWarning summary\n\t detail: Warning detail", + }, + "multiple warnings": { + setupDiags: func() *diag.Diagnostics { + var diags diag.Diagnostics + diags.AddWarning("First warning", "Warning detail 1") + diags.AddWarning("Second warning", "Warning detail 2") + return &diags + }, + expectedText: "Warnings:\nFirst warning\n\t detail: Warning detail 1\nSecond warning\n\t detail: Warning detail 2", + }, + "errors and warnings": { + setupDiags: func() *diag.Diagnostics { + var diags diag.Diagnostics + diags.AddError("Error summary", "Error detail") + diags.AddWarning("Warning summary", "Warning detail") + return &diags + }, + expectedText: "Error summary\n\t detail: Error detail\n\nWarnings:\nWarning summary\n\t detail: Warning detail", + }, + "multiple errors and warnings": { + setupDiags: func() *diag.Diagnostics { + var diags diag.Diagnostics + diags.AddError("First error", "Error detail 1") + diags.AddError("Second error", "Error detail 2") + diags.AddWarning("First warning", "Warning detail 1") + diags.AddWarning("Second warning", "Warning detail 2") + return &diags + }, + expectedText: "First error\n\t detail: Error detail 1\nSecond error\n\t detail: Error detail 2\n\nWarnings:\nFirst warning\n\t detail: Warning detail 1\nSecond warning\n\t detail: Warning detail 2", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + diags := tc.setupDiags() + result := conversion.FormatDiags(diags) + assert.Equal(t, tc.expectedText, result) + }) + } +} From bc6aeb11457794a8cfcb835ce4c515c402311cff Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Mon, 31 Mar 2025 11:59:14 +0200 Subject: [PATCH 34/39] refactor: Modify AncestorPath functions to return diagnostics alongside paths --- internal/common/conversion/path_helpers.go | 17 ++++---- .../common/conversion/path_helpers_test.go | 42 ++++++++++++------- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/internal/common/conversion/path_helpers.go b/internal/common/conversion/path_helpers.go index c2aeae4236..6802e64d76 100644 --- a/internal/common/conversion/path_helpers.go +++ b/internal/common/conversion/path_helpers.go @@ -60,25 +60,26 @@ func AsRemovedIndex(p path.Path) string { return everythingExceptLast + lastPartWithRemoveIndex } -func AncestorPathWithIndex(p path.Path, attributeName string, diags *diag.Diagnostics) path.Path { +func AncestorPathWithIndex(p path.Path, attributeName string) (path.Path, diag.Diagnostics) { + var diags diag.Diagnostics for { p = p.ParentPath() if p.Equal(path.Empty()) { - diags.AddError("Parent path not found", fmt.Sprintf("Parent attribute %s not found in path %s", attributeName, p.String())) - return p + diags.AddError("Ancestor path not found", fmt.Sprintf("Ancestor attribute %s not found in path %s", attributeName, p.String())) + return p, diags } if AttributeName(p) == attributeName { - return p + return p, diags } } } -func AncestorPathNoIndex(p path.Path, attributeName string, diags *diag.Diagnostics) path.Path { - parent := AncestorPathWithIndex(p, attributeName, diags) +func AncestorPathNoIndex(p path.Path, attributeName string) (path.Path, diag.Diagnostics) { + parent, diags := AncestorPathWithIndex(p, attributeName) if diags.HasError() { - return parent + return parent, diags } - return trimLastIndexPath(parent) + return trimLastIndexPath(parent), diags } func AncestorPaths(p path.Path) []path.Path { diff --git a/internal/common/conversion/path_helpers_test.go b/internal/common/conversion/path_helpers_test.go index 28ba2fdc29..ed85f00aea 100644 --- a/internal/common/conversion/path_helpers_test.go +++ b/internal/common/conversion/path_helpers_test.go @@ -3,7 +3,6 @@ package conversion_test import ( "testing" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" @@ -41,7 +40,9 @@ func TestIndexMethods(t *testing.T) { assert.Equal(t, "replication_specs[-Value(\"myKey\")]", conversion.AsRemovedIndex(path.Root("replication_specs").AtSetValue(types.StringValue("myKey")))) setIndex := path.Root("advanced_configuration").AtName("custom_openssl_cipher_config_tls12").AtSetValue(types.StringValue("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384")) assert.Equal(t, "advanced_configuration.custom_openssl_cipher_config_tls12[-Value(\"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\")]", conversion.AsRemovedIndex(setIndex)) - assert.Equal(t, "advanced_configuration.custom_openssl_cipher_config_tls12", conversion.AncestorPathNoIndex(setIndex, "custom_openssl_cipher_config_tls12", new(diag.Diagnostics)).String()) + setNoIndex, diags := conversion.AncestorPathNoIndex(setIndex, "custom_openssl_cipher_config_tls12") + assert.Empty(t, diags) + assert.Equal(t, "advanced_configuration.custom_openssl_cipher_config_tls12", setNoIndex) assert.Empty(t, conversion.AsRemovedIndex(path.Root("replication_specs"))) } @@ -54,59 +55,68 @@ func TestHasAncestor(t *testing.T) { } func TestParentPathWithIndex_Found(t *testing.T) { - diags := new(diag.Diagnostics) // Build a nested path: resource -> parent -> child basePath := path.Root("resource") parentPath := basePath.AtName("parent") childPath := parentPath.AtName("child") - assert.Equal(t, parentPath.String(), conversion.AncestorPathWithIndex(childPath, "parent", diags).String()) - assert.Equal(t, basePath.String(), conversion.AncestorPathWithIndex(childPath, "resource", diags).String()) + parentPathActual, diags := conversion.AncestorPathWithIndex(childPath, "parent") + assert.Empty(t, diags) + assert.Equal(t, parentPath.String(), parentPathActual.String()) + + basePathActual, diags := conversion.AncestorPathWithIndex(childPath, "resource") + assert.Equal(t, basePath.String(), basePathActual) assert.Empty(t, diags, "Diagnostics should not have errors") } func TestParentPathWithIndex_FoundIncludesIndex(t *testing.T) { - diags := new(diag.Diagnostics) // Build a nested path: resource[0] -> parent[0] -> child basePath := path.Root("resource") parentPath := basePath.AtListIndex(0).AtName("parent") childPath := parentPath.AtListIndex(0).AtName("child") assert.Equal(t, "resource[0].parent[0].child", childPath.String()) - assert.Equal(t, parentPath.AtListIndex(0).String(), conversion.AncestorPathWithIndex(childPath, "parent", diags).String()) - assert.Equal(t, basePath.AtListIndex(0).String(), conversion.AncestorPathWithIndex(childPath, "resource", diags).String()) + parentPathActual, diags := conversion.AncestorPathWithIndex(childPath, "parent") + assert.Empty(t, diags) + assert.Equal(t, parentPath.AtListIndex(0).String(), parentPathActual) + + basePathActual, diags := conversion.AncestorPathWithIndex(childPath, "resource") + assert.Empty(t, diags) + assert.Equal(t, basePath.AtListIndex(0).String(), basePathActual) assert.Empty(t, diags, "Diagnostics should not have errors") } func TestParentPathNoIndex_RemovesIndex(t *testing.T) { - diags := new(diag.Diagnostics) // Build a nested path: resource[0] -> parent[0] -> child basePath := path.Root("resource") parentPath := basePath.AtListIndex(0).AtName("parent") childPath := parentPath.AtListIndex(0).AtName("child") assert.Equal(t, "resource[0].parent[0].child", childPath.String()) - assert.Equal(t, parentPath.String(), conversion.AncestorPathNoIndex(childPath, "parent", diags).String()) - assert.Equal(t, basePath.String(), conversion.AncestorPathNoIndex(childPath, "resource", diags).String()) - assert.Empty(t, diags, "Diagnostics should not have errors") + parentPathActual, diags := conversion.AncestorPathNoIndex(childPath, "parent") + assert.Empty(t, diags) + assert.Equal(t, parentPath.String(), parentPathActual.String()) + + // Get base path without index + basePathActual, diags := conversion.AncestorPathNoIndex(childPath, "resource") + assert.Empty(t, diags) + assert.Equal(t, basePath.String(), basePathActual.String()) } func TestParentPathWithIndex_NotFound(t *testing.T) { - diags := new(diag.Diagnostics) // Build a path: resource -> child basePath := path.Root("resource") childPath := basePath.AtName("child") - result := conversion.AncestorPathWithIndex(childPath, "nonexistent", diags) + result, diags := conversion.AncestorPathWithIndex(childPath, "nonexistent") // The function should traverse to path.Empty() and add an error. assert.True(t, result.Equal(path.Empty()), "Expected result to be empty if parent not found") assert.True(t, diags.HasError(), "Diagnostics should have an error when parent attribute is missing") } func TestParentPathWithIndex_EmptyPath(t *testing.T) { - diags := new(diag.Diagnostics) emptyPath := path.Empty() - result := conversion.AncestorPathWithIndex(emptyPath, "any", diags) + result, diags := conversion.AncestorPathWithIndex(emptyPath, "any") // Since the path is empty, it should immediately return empty and add error. assert.True(t, result.Equal(path.Empty()), "Expected empty path as result from an empty input path") assert.True(t, diags.HasError(), "Diagnostics should have an error for empty input path") From 02cae8f6a8a9469cc6fb7215fa761971e4e74d6e Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Mon, 31 Mar 2025 11:59:58 +0200 Subject: [PATCH 35/39] refactor: Enhance error logging in plan modification functions instead of global failure --- .../customplanmodifier/plan_modify_differ.go | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/internal/common/customplanmodifier/plan_modify_differ.go b/internal/common/customplanmodifier/plan_modify_differ.go index 957f9029e0..5afb5aa2ad 100644 --- a/internal/common/customplanmodifier/plan_modify_differ.go +++ b/internal/common/customplanmodifier/plan_modify_differ.go @@ -85,7 +85,8 @@ func ReadStateStructValue[T any](ctx context.Context, d *PlanModifyDiffer, p pat func readSrcStructValue[T any](ctx context.Context, src conversion.TPFSrc, p path.Path) *T { var obj types.Object - if localDiags := src.GetAttribute(ctx, p, &obj); localDiags.HasError() { + if localDiags := src.GetAttribute(ctx, p, &obj); len(localDiags) > 0 { + tflog.Error(ctx, conversion.FormatDiags(&localDiags)) return nil } if obj.IsNull() || obj.IsUnknown() { @@ -93,17 +94,23 @@ func readSrcStructValue[T any](ctx context.Context, src conversion.TPFSrc, p pat } return conversion.TFModelObject[T](ctx, obj) } -func ReadPlanStructValues[T any](ctx context.Context, d *PlanModifyDiffer, p path.Path, diags *diag.Diagnostics) []T { - return readSrcStructValues[T](ctx, d.plan, p, diags) + +func ReadPlanStructValues[T any](ctx context.Context, d *PlanModifyDiffer, p path.Path) []T { + return readSrcStructValues[T](ctx, d.plan, p) } -func readSrcStructValues[T any](ctx context.Context, src conversion.TPFSrc, p path.Path, diags *diag.Diagnostics) []T { +func readSrcStructValues[T any](ctx context.Context, src conversion.TPFSrc, p path.Path) []T { var objList types.List - if localDiags := src.GetAttribute(ctx, p, &objList); len(localDiags) > 0 { - diags.Append(localDiags...) + var localDiags diag.Diagnostics + if localDiags = src.GetAttribute(ctx, p, &objList); len(localDiags) > 0 { + tflog.Error(ctx, conversion.FormatDiags(&localDiags)) return nil } - return conversion.TFModelList[T](ctx, diags, objList) + result := conversion.TFModelList[T](ctx, &localDiags, objList) + if len(localDiags) > 0 { + tflog.Error(ctx, conversion.FormatDiags(&localDiags)) + } + return result } func UpdatePlanValue(ctx context.Context, diags *diag.Diagnostics, d *PlanModifyDiffer, p path.Path, value attr.Value) { From e9583612276aee0dfff81ca4a6b42b35bc2b5c04 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Mon, 31 Mar 2025 12:00:29 +0200 Subject: [PATCH 36/39] refactor: Clarify comments in ApplyReplacements function regarding ancestor path behavior --- internal/common/customplanmodifier/unknown_replacement.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/common/customplanmodifier/unknown_replacement.go b/internal/common/customplanmodifier/unknown_replacement.go index ee2249f978..9569371c11 100644 --- a/internal/common/customplanmodifier/unknown_replacement.go +++ b/internal/common/customplanmodifier/unknown_replacement.go @@ -65,6 +65,8 @@ func (u *UnknownReplacements[ResourceInfo]) AddKeepUnknownsExtraCall(call AddKee // If there is no explicit replacement function, it will use the default replacer that respects the keepUnknown attributes. // The calls are done top-down, for example replication_specs.*.id before replication_specs.*.region_configs.*.electable_specs // Same levels are sorted alphabetically, for example ...region_configs.electable_specs before ...region_configs.read_only_specs +// If the replacement function is called for a path that is an ancestor of another path, it will skip the replacement for the child path. +// For example: if ..read_only_specs has a replacement function and is called then ..read_only_specs.disk_iops will be left as is. func (u *UnknownReplacements[ResourceInfo]) ApplyReplacements(ctx context.Context, diags *diag.Diagnostics) { replacedPaths := []path.Path{} ancestorHasProcessed := func(p path.Path) bool { From 10ebaf4ae834a8920c6afde6385cb4e3ae4821ba Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Tue, 1 Apr 2025 12:40:31 +0200 Subject: [PATCH 37/39] doc: Add docstring to more public functions --- internal/common/customplanmodifier/plan_modify_differ.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/common/customplanmodifier/plan_modify_differ.go b/internal/common/customplanmodifier/plan_modify_differ.go index 5afb5aa2ad..a35fd37812 100644 --- a/internal/common/customplanmodifier/plan_modify_differ.go +++ b/internal/common/customplanmodifier/plan_modify_differ.go @@ -75,10 +75,12 @@ func (d *PlanModifyDiffer) Unknowns(ctx context.Context, diags *diag.Diagnostics return unknowns } +// ReadPlanStructValue reads a struct value from the plan, returns nil if the value is null or unknown, logs any error getting the attribute. func ReadPlanStructValue[T any](ctx context.Context, d *PlanModifyDiffer, p path.Path) *T { return readSrcStructValue[T](ctx, d.plan, p) } +// ReadStateStructValue reads a struct value from the state, returns nil if the value is null or unknown, logs any error getting the attribute. func ReadStateStructValue[T any](ctx context.Context, d *PlanModifyDiffer, p path.Path) *T { return readSrcStructValue[T](ctx, d.state, p) } @@ -95,6 +97,7 @@ func readSrcStructValue[T any](ctx context.Context, src conversion.TPFSrc, p pat return conversion.TFModelObject[T](ctx, obj) } +// ReadPlanStructValues reads a list of struct values from the plan, returns nil if conversion fails, logs any error getting the attribute. func ReadPlanStructValues[T any](ctx context.Context, d *PlanModifyDiffer, p path.Path) []T { return readSrcStructValues[T](ctx, d.plan, p) } From d7deeb37a0f61943ed2d509d719fddf203a1059b Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Tue, 1 Apr 2025 13:09:34 +0200 Subject: [PATCH 38/39] test: fix broken unit test --- internal/common/conversion/path_helpers_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/common/conversion/path_helpers_test.go b/internal/common/conversion/path_helpers_test.go index ed85f00aea..8d5f238517 100644 --- a/internal/common/conversion/path_helpers_test.go +++ b/internal/common/conversion/path_helpers_test.go @@ -42,7 +42,7 @@ func TestIndexMethods(t *testing.T) { assert.Equal(t, "advanced_configuration.custom_openssl_cipher_config_tls12[-Value(\"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\")]", conversion.AsRemovedIndex(setIndex)) setNoIndex, diags := conversion.AncestorPathNoIndex(setIndex, "custom_openssl_cipher_config_tls12") assert.Empty(t, diags) - assert.Equal(t, "advanced_configuration.custom_openssl_cipher_config_tls12", setNoIndex) + assert.Equal(t, "advanced_configuration.custom_openssl_cipher_config_tls12", setNoIndex.String()) assert.Empty(t, conversion.AsRemovedIndex(path.Root("replication_specs"))) } @@ -65,7 +65,7 @@ func TestParentPathWithIndex_Found(t *testing.T) { assert.Equal(t, parentPath.String(), parentPathActual.String()) basePathActual, diags := conversion.AncestorPathWithIndex(childPath, "resource") - assert.Equal(t, basePath.String(), basePathActual) + assert.Equal(t, basePath.String(), basePathActual.String()) assert.Empty(t, diags, "Diagnostics should not have errors") } @@ -78,11 +78,11 @@ func TestParentPathWithIndex_FoundIncludesIndex(t *testing.T) { parentPathActual, diags := conversion.AncestorPathWithIndex(childPath, "parent") assert.Empty(t, diags) - assert.Equal(t, parentPath.AtListIndex(0).String(), parentPathActual) + assert.Equal(t, parentPath.AtListIndex(0).String(), parentPathActual.String()) basePathActual, diags := conversion.AncestorPathWithIndex(childPath, "resource") assert.Empty(t, diags) - assert.Equal(t, basePath.AtListIndex(0).String(), basePathActual) + assert.Equal(t, basePath.AtListIndex(0).String(), basePathActual.String()) assert.Empty(t, diags, "Diagnostics should not have errors") } From 40a66a9adf250ffcbde9c23a96f5a7ab1957f03b Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Tue, 1 Apr 2025 16:52:59 +0200 Subject: [PATCH 39/39] refactor: sync changes from stack top --- .../common/customplanmodifier/find_changes.go | 14 ++-- .../customplanmodifier/find_changes_test.go | 68 +++++++++---------- .../customplanmodifier/plan_modify_differ.go | 2 +- .../customplanmodifier/unknown_replacement.go | 13 ---- .../unknown_replacement_test.go | 1 - 5 files changed, 40 insertions(+), 58 deletions(-) diff --git a/internal/common/customplanmodifier/find_changes.go b/internal/common/customplanmodifier/find_changes.go index 95c0e34feb..d6f534f554 100644 --- a/internal/common/customplanmodifier/find_changes.go +++ b/internal/common/customplanmodifier/find_changes.go @@ -4,6 +4,8 @@ import ( "fmt" "slices" "strings" + + "github.com/hashicorp/terraform-plugin-framework/path" ) type AttributeChanges []string @@ -25,16 +27,14 @@ func (a AttributeChanges) KeepUnknown(attributeEffectedMapping map[string][]stri return keepUnknown } -// ListIndexChanged returns true if the list at the given index has changed, false if it was added or removed -func (a AttributeChanges) ListIndexChanged(fullPath string, index int) bool { - indexPath := fmt.Sprintf("%s[%d]", fullPath, index) - return slices.Contains(a, indexPath) +func (a AttributeChanges) PathChanged(path path.Path) bool { + return slices.Contains(a, path.String()) } // ListLenChanged accepts a fullPath, e.g., "replication_specs[0].region_configs" and returns true if the length of the nested list has changed -func (a AttributeChanges) ListLenChanged(fullPath string) bool { - addPrefix := asAddPrefix(fullPath) - removePrefix := asRemovePrefix(fullPath) +func (a AttributeChanges) ListLenChanged(p path.Path) bool { + addPrefix := asAddPrefix(p.String()) + removePrefix := asRemovePrefix(p.String()) for _, change := range a { if strings.HasPrefix(change, addPrefix) || strings.HasPrefix(change, removePrefix) { return true diff --git a/internal/common/customplanmodifier/find_changes_test.go b/internal/common/customplanmodifier/find_changes_test.go index bbaf1bbb3f..9ee8bff7dc 100644 --- a/internal/common/customplanmodifier/find_changes_test.go +++ b/internal/common/customplanmodifier/find_changes_test.go @@ -3,6 +3,7 @@ package customplanmodifier_test import ( "testing" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/customplanmodifier" "github.com/stretchr/testify/assert" ) @@ -101,64 +102,58 @@ func TestAttributeChanges_KeepUnknown(t *testing.T) { } } -func TestAttributeChanges_ListIndexChanged(t *testing.T) { +func TestAttributeChanges_PathChanged(t *testing.T) { + var ( + root = path.Root("replication_specs") + rootIndex0 = root.AtListIndex(0) + ) tests := map[string]struct { - name string + path path.Path changes customplanmodifier.AttributeChanges - index int expected bool }{ "empty changes": { - name: "replication_specs", - index: 0, + path: root, changes: []string{}, expected: false, }, "list element modified": { - name: "replication_specs", - index: 0, + path: rootIndex0, changes: []string{"replication_specs[0]", "replication_specs[0].zone_name"}, expected: true, }, - "list element added": { - name: "replication_specs", - index: 0, + "list element added don't match exact index": { + path: rootIndex0, changes: []string{"replication_specs[+0]"}, expected: false, }, - "list element removed": { - name: "replication_specs", - index: 1, - changes: []string{"replication_specs[-1]"}, + "list element removed don't match exact index": { + path: rootIndex0, + changes: []string{"replication_specs[-0]"}, expected: false, }, "different index": { - name: "replication_specs", - index: 1, - changes: []string{"replication_specs[0]", "replication_specs[0].zone_name"}, + path: rootIndex0, + changes: []string{"replication_specs[1]", "replication_specs[0].zone_name"}, expected: false, }, "different list name": { - name: "other_specs", - index: 0, + path: path.Root("replication_specs2"), changes: []string{"replication_specs[0]", "replication_specs[0].zone_name"}, expected: false, }, "nested list": { - name: "replication_specs[0].region_configs", - index: 0, + path: rootIndex0.AtName("region_configs").AtListIndex(0), changes: []string{"replication_specs[0].region_configs[0]", "replication_specs[0].region_configs[0].priority"}, expected: true, }, "nested list false": { - name: "replication_specs[0].region_configs", - index: 1, + path: rootIndex0.AtName("region_configs").AtListIndex(1), changes: []string{"replication_specs[0].region_configs[0]", "replication_specs[0].region_configs[0].priority"}, expected: false, }, "index beyond bounds": { - name: "replication_specs", - index: 5, + path: root.AtListIndex(5), changes: []string{"replication_specs[0]", "replication_specs[1]"}, expected: false, }, @@ -166,44 +161,45 @@ func TestAttributeChanges_ListIndexChanged(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { - actual := tc.changes.ListIndexChanged(tc.name, tc.index) + actual := tc.changes.PathChanged(tc.path) assert.Equal(t, tc.expected, actual) }) } } -func TestAttributeChanges_NestedListLenChanges(t *testing.T) { +func TestAttributeChanges_ListLenChanges(t *testing.T) { + regionConfigs := path.Root("replication_specs").AtName("region_configs") tests := map[string]struct { - fullPath string + fullPath path.Path changes customplanmodifier.AttributeChanges expected bool }{ "empty changes": { - fullPath: "replication_specs.region_configs", + fullPath: regionConfigs, changes: []string{}, expected: false, }, "no nested list changes": { - fullPath: "replication_specs.region_configs", + fullPath: regionConfigs, changes: []string{"name", "description", "replication_specs.zone_name"}, expected: false, }, "add nested element": { - fullPath: "replication_specs.region_configs", + fullPath: regionConfigs, changes: []string{"replication_specs.region_configs[+0]", "replication_specs.region_configs.priority"}, expected: true, }, "add nested element add different index should be false": { - fullPath: "replication_specs[0].region_configs", + fullPath: path.Root("replication_specs").AtListIndex(0).AtName("region_configs"), changes: []string{"replication_specs[1].region_configs[+0]"}, expected: false, }, "remove nested element": { - fullPath: "replication_specs.region_configs", + fullPath: regionConfigs, changes: []string{"replication_specs.region_configs[-1]", "replication_specs.region_configs.region_name"}, expected: true, }, "mixed list operations": { - fullPath: "replication_specs.region_configs", + fullPath: regionConfigs, changes: []string{ "replication_specs.region_configs[+0]", "replication_specs.region_configs[-1]", @@ -212,12 +208,12 @@ func TestAttributeChanges_NestedListLenChanges(t *testing.T) { expected: true, }, "different path": { - fullPath: "other.configs", + fullPath: path.Root("other").AtName("configs"), changes: []string{"replication_specs.region_configs[+0]", "replication_specs.region_configs[-1]"}, expected: false, }, "multiple nested levels": { - fullPath: "replication_specs.region_configs.zones", + fullPath: regionConfigs.AtName("zones"), changes: []string{"replication_specs.region_configs.zones[+0]", "replication_specs.region_configs[0].zones.name"}, expected: true, }, diff --git a/internal/common/customplanmodifier/plan_modify_differ.go b/internal/common/customplanmodifier/plan_modify_differ.go index a35fd37812..b96c35bc94 100644 --- a/internal/common/customplanmodifier/plan_modify_differ.go +++ b/internal/common/customplanmodifier/plan_modify_differ.go @@ -163,5 +163,5 @@ func findChanges(ctx context.Context, diff []tftypes.ValueDiff, diags *diag.Diag } } } - return slices.Sorted(maps.Keys(changes)) // Ensure changes are sorted to support top-down processing, for example read_only_spec is processed before read_only_spec.disk_size_gb + return slices.Sorted(maps.Keys(changes)) // prettier attribute changes when they are sorted alphabetically } diff --git a/internal/common/customplanmodifier/unknown_replacement.go b/internal/common/customplanmodifier/unknown_replacement.go index 9569371c11..16232ee874 100644 --- a/internal/common/customplanmodifier/unknown_replacement.go +++ b/internal/common/customplanmodifier/unknown_replacement.go @@ -68,25 +68,12 @@ func (u *UnknownReplacements[ResourceInfo]) AddKeepUnknownsExtraCall(call AddKee // If the replacement function is called for a path that is an ancestor of another path, it will skip the replacement for the child path. // For example: if ..read_only_specs has a replacement function and is called then ..read_only_specs.disk_iops will be left as is. func (u *UnknownReplacements[ResourceInfo]) ApplyReplacements(ctx context.Context, diags *diag.Diagnostics) { - replacedPaths := []path.Path{} - ancestorHasProcessed := func(p path.Path) bool { - for _, replacedPath := range replacedPaths { - if conversion.HasAncestor(p, replacedPath) { - return true - } - } - return false - } for _, unknown := range u.Differ.Unknowns(ctx, diags) { strPath := unknown.StrPath replacer, ok := u.Replacements[unknown.AttributeName] if !ok { replacer = u.defaultReplacer } - if ancestorHasProcessed(unknown.Path) { - continue - } - replacedPaths = append(replacedPaths, unknown.Path) req := &UnknownReplacementRequest[ResourceInfo]{ Info: u.Info, Path: unknown.Path, diff --git a/internal/common/customplanmodifier/unknown_replacement_test.go b/internal/common/customplanmodifier/unknown_replacement_test.go index 143bf2efb8..f6ee544365 100644 --- a/internal/common/customplanmodifier/unknown_replacement_test.go +++ b/internal/common/customplanmodifier/unknown_replacement_test.go @@ -260,7 +260,6 @@ func TestReplaceUnknownLogicByWrappingAdvancedClusterTPF(t *testing.T) { baseConfig.TestdataPrefix = unit.PackagePath("advancedclustertpf") unit.MockPlanChecksAndRun(t, baseConfig.WithPlanCheckTest(unit.PlanCheckTest{ConfigFilename: tc.ConfigFilename})) assert.Equal(t, tc.expectedAttributeChanges, runData.attributeChanges) - slices.Sort(runData.keepUnknownCalls) assert.Equal(t, tc.expectedKeepUnknownCalls, runData.keepUnknownCalls) }) }