From e4e30f2637397b58ba76a9b6f278e445d7744a19 Mon Sep 17 00:00:00 2001 From: Agustin Bettati Date: Tue, 12 Nov 2024 10:42:12 +0100 Subject: [PATCH] add resource and data source implementations --- .changelog/2793.txt | 11 ++ docs/data-sources/cluster.md | 6 + docs/data-sources/clusters.md | 5 + docs/resources/cluster.md | 6 + .../data_source_advanced_clusters.go | 4 +- .../advancedcluster/model_advanced_cluster.go | 2 +- .../resource_advanced_cluster.go | 16 +-- .../service/cluster/data_source_cluster.go | 33 ++++- .../service/cluster/data_source_clusters.go | 28 +++- internal/service/cluster/new_atlas.go | 12 +- internal/service/cluster/resource_cluster.go | 56 ++++++-- .../service/cluster/resource_cluster_test.go | 125 ++++++++++++++++++ 12 files changed, 266 insertions(+), 38 deletions(-) create mode 100644 .changelog/2793.txt diff --git a/.changelog/2793.txt b/.changelog/2793.txt new file mode 100644 index 0000000000..c9e55b350d --- /dev/null +++ b/.changelog/2793.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/mongodbatlas_cluster: Adds `pinned_fcv` attribute +``` + +```release-note:enhancement +data-source/mongodbatlas_cluster: Adds `pinned_fcv` attribute +``` + +```release-note:enhancement +data-source/mongodbatlas_clusters: Adds `pinned_fcv` attribute +``` diff --git a/docs/data-sources/cluster.md b/docs/data-sources/cluster.md index e8f96a3e9d..0541f0df68 100644 --- a/docs/data-sources/cluster.md +++ b/docs/data-sources/cluster.md @@ -104,6 +104,7 @@ In addition to all arguments above, the following attributes are exported: * `tags` - Set that contains key-value pairs between 1 to 255 characters in length for tagging and categorizing the cluster. See [below](#tags). * `labels` - Set that contains key-value pairs between 1 to 255 characters in length for tagging and categorizing the cluster. See [below](#labels). **DEPRECATED** Use `tags` instead. * `mongo_db_major_version` - Indicates the version of the cluster to deploy. +* `pinned_fcv` - The pinned Feature Compatibility Version (FCV) with its associated expiration date. See [below](#pinned-fcv). * `num_shards` - Indicates whether the cluster is a replica set or a sharded cluster. * `cloud_backup` - Flag indicating if the cluster uses Cloud Backup Snapshots for backups. * `termination_protection_enabled` - Flag that indicates whether termination protection is enabled on the cluster. If set to true, MongoDB Cloud won't delete the cluster. If set to false, MongoDB Cloud will delete the cluster. @@ -233,4 +234,9 @@ Contains a key-value pair that tags that the cluster was created by a Terraform * `transaction_lifetime_limit_seconds` - Lifetime, in seconds, of multi-document transactions. Defaults to 60 seconds. * `change_stream_options_pre_and_post_images_expire_after_seconds` - (Optional) The minimum pre- and post-image retention time in seconds. This parameter is only supported for MongoDB version 6.0 and above. Defaults to `-1`(off). +### Pinned FCV + +* `expiration_date` - Expiration date of the fixed FCV. This value is in the ISO 8601 timestamp format (e.g. "2024-12-04T16:25:00Z"). +* `version` - Feature compatibility version of the cluster. + See detailed information for arguments and attributes: [MongoDB API Clusters](https://docs.atlas.mongodb.com/reference/api/clusters-create-one/) diff --git a/docs/data-sources/clusters.md b/docs/data-sources/clusters.md index 4cef071990..73d2485726 100644 --- a/docs/data-sources/clusters.md +++ b/docs/data-sources/clusters.md @@ -94,6 +94,7 @@ In addition to all arguments above, the following attributes are exported: * `tags` - Set that contains key-value pairs between 1 to 255 characters in length for tagging and categorizing the cluster. See [below](#tags). * `labels` - Set that contains key-value pairs between 1 to 255 characters in length for tagging and categorizing the cluster. See [below](#labels). **DEPRECATED** Use `tags` instead. * `mongo_db_major_version` - Indicates the version of the cluster to deploy. +* `pinned_fcv` - The pinned Feature Compatibility Version (FCV) with its associated expiration date. See [below](#pinned-fcv). * `num_shards` - Indicates whether the cluster is a replica set or a sharded cluster. * `provider_backup_enabled` - Flag indicating if the cluster uses Cloud Backup Snapshots for backups. **DEPRECATED** Use `cloud_backup` instead. * `cloud_backup` - Flag indicating if the cluster uses Cloud Backup Snapshots for backups. @@ -220,5 +221,9 @@ Contains a key-value pair that tags that the cluster was created by a Terraform * `sample_refresh_interval_bi_connector` - Interval in seconds at which the mongosqld process re-samples data to create its relational schema. The default value is 300. The specified value must be a positive integer. Available only for Atlas deployments in which BI Connector for Atlas is enabled. * `change_stream_options_pre_and_post_images_expire_after_seconds` - (Optional) The minimum pre- and post-image retention time in seconds. This parameter is only supported for MongoDB version 6.0 and above. Defaults to `-1`(off). +### Pinned FCV + +* `expiration_date` - Expiration date of the fixed FCV. This value is in the ISO 8601 timestamp format (e.g. "2024-12-04T16:25:00Z"). +* `version` - Feature compatibility version of the cluster. See detailed information for arguments and attributes: [MongoDB API Clusters](https://docs.atlas.mongodb.com/reference/api/clusters-create-one/) diff --git a/docs/resources/cluster.md b/docs/resources/cluster.md index 25b7bd76e6..f2267fffe1 100644 --- a/docs/resources/cluster.md +++ b/docs/resources/cluster.md @@ -340,6 +340,7 @@ But in order to explicitly change `provider_instance_size_name` comment the `lif * `tags` - (Optional) Set that contains key-value pairs between 1 to 255 characters in length for tagging and categorizing the cluster. See [below](#tags). * `labels` - (Optional) Set that contains key-value pairs between 1 to 255 characters in length for tagging and categorizing the cluster. See [below](#labels). **DEPRECATED** Use `tags` instead. * `mongo_db_major_version` - (Optional) Version of the cluster to deploy. Atlas supports all the MongoDB versions that have **not** reached [End of Live](https://www.mongodb.com/legal/support-policy/lifecycles) for M10+ clusters. If omitted, Atlas deploys the cluster with the default version. For more details, see [documentation](https://www.mongodb.com/docs/atlas/reference/faq/database/#which-versions-of-mongodb-do-service-clusters-use-). Atlas always deploys the cluster with the latest stable release of the specified version. See [Release Notes](https://www.mongodb.com/docs/upcoming/release-notes/) for latest Current Stable Release. +* `pinned_fcv` - (Optional) Pins the Feature Compatibility Version (FCV) to the current MongoDB version with a provided expiration date. To unpin the FCV the `pinned_fcv` attribute must be removed, this operation can take several minutes as the request processes through the MongoDB data plane. Once FCV is unpinned it will not be possible to downgrade the `mongo_db_major_version`. It is advised that updates to `pinned_fcv` are done isolated from other cluster changes, and if a plan contains multiple changes FCV change will be applied first. If FCV has unpinned due to expiration date `pinned_fcv` attribute must be removed. The following [knowledge hub article](https://kb.corp.mongodb.com/article/000021785/) can be referenced for more details. See [below](#pinned_fcv). * `num_shards` - (Optional) Selects whether the cluster is a replica set or a sharded cluster. If you use the replicationSpecs parameter, you must set num_shards. * `pit_enabled` - (Optional) - Flag that indicates if the cluster uses Continuous Cloud Backup. If set to true, cloud_backup must also be set to true. * `cloud_backup` - (Optional) Flag indicating if the cluster uses Cloud Backup for backups. @@ -539,6 +540,11 @@ To learn more, see [Resource Tags](https://dochub.mongodb.org/core/add-cluster-t -> **NOTE:** MongoDB Atlas doesn't display your labels. +### Pinned FCV + +* `expiration_date` - (Required) Expiration date of the fixed FCV. This value is in the ISO 8601 timestamp format (e.g. "2024-12-04T16:25:00Z"). Note that this field cannot exceed 4 weeks from the pinned date. +* `version` - Feature compatibility version of the cluster. + ## Attributes Reference In addition to all arguments above, the following attributes are exported: diff --git a/internal/service/advancedcluster/data_source_advanced_clusters.go b/internal/service/advancedcluster/data_source_advanced_clusters.go index df26764377..ad09c8b7a3 100644 --- a/internal/service/advancedcluster/data_source_advanced_clusters.go +++ b/internal/service/advancedcluster/data_source_advanced_clusters.go @@ -391,7 +391,7 @@ func flattenAdvancedClusters(ctx context.Context, connV220240530 *admin20240530. "redact_client_log_data": cluster.GetRedactClientLogData(), "config_server_management_mode": cluster.GetConfigServerManagementMode(), "config_server_type": cluster.GetConfigServerType(), - "pinned_fcv": flattenPinnedFCV(cluster), + "pinned_fcv": FlattenPinnedFCV(cluster), } results = append(results, result) } @@ -451,7 +451,7 @@ func flattenAdvancedClustersOldSDK(ctx context.Context, connV20240530 *admin2024 "redact_client_log_data": clusterDescNew.GetRedactClientLogData(), "config_server_management_mode": clusterDescNew.GetConfigServerManagementMode(), "config_server_type": clusterDescNew.GetConfigServerType(), - "pinned_fcv": flattenPinnedFCV(clusterDescNew), + "pinned_fcv": FlattenPinnedFCV(clusterDescNew), } results = append(results, result) } diff --git a/internal/service/advancedcluster/model_advanced_cluster.go b/internal/service/advancedcluster/model_advanced_cluster.go index b9ab22c94b..0906a092b8 100644 --- a/internal/service/advancedcluster/model_advanced_cluster.go +++ b/internal/service/advancedcluster/model_advanced_cluster.go @@ -426,7 +426,7 @@ func CheckRegionConfigsPriorityOrderOld(regionConfigs []admin20240530.Replicatio return nil } -func flattenPinnedFCV(cluster *admin.ClusterDescription20240805) []map[string]string { +func FlattenPinnedFCV(cluster *admin.ClusterDescription20240805) []map[string]string { if cluster.FeatureCompatibilityVersionExpirationDate == nil { // pinned_fcv is defined in state only if featureCompatibilityVersionExpirationDate is present in cluster response return nil } diff --git a/internal/service/advancedcluster/resource_advanced_cluster.go b/internal/service/advancedcluster/resource_advanced_cluster.go index 02eaa4d0d9..dda3423be1 100644 --- a/internal/service/advancedcluster/resource_advanced_cluster.go +++ b/internal/service/advancedcluster/resource_advanced_cluster.go @@ -539,7 +539,7 @@ func resourceCreate(ctx context.Context, d *schema.ResourceData, meta any) diag. } if pinnedFCVBlock, ok := d.Get("pinned_fcv").([]any); ok && len(pinnedFCVBlock) > 0 { - if diags := pinFCV(ctx, connV2, projectID, cluster.GetName(), pinnedFCVBlock[0]); diags.HasError() { + if diags := PinFCV(ctx, connV2, projectID, cluster.GetName(), pinnedFCVBlock[0]); diags.HasError() { return diags } waitForChanges = true @@ -642,7 +642,7 @@ func resourceRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Di clusterResp = cluster } - warning := warningIfFCVExpiredOrUnpinnedExternally(d, clusterResp) // has to be called before pinned_fcv value is updated in ResourceData to know prior state value + warning := WarningIfFCVExpiredOrUnpinnedExternally(d, clusterResp) // has to be called before pinned_fcv value is updated in ResourceData to know prior state value diags := setRootFields(d, clusterResp, true) if diags.HasError() { return diags @@ -800,14 +800,14 @@ func setRootFields(d *schema.ResourceData, cluster *admin.ClusterDescription2024 return diag.FromErr(fmt.Errorf(ErrorClusterAdvancedSetting, "config_server_management_mode", clusterName, err)) } - if err := d.Set("pinned_fcv", flattenPinnedFCV(cluster)); err != nil { + if err := d.Set("pinned_fcv", FlattenPinnedFCV(cluster)); err != nil { return diag.FromErr(fmt.Errorf(ErrorClusterAdvancedSetting, "pinned_fcv", clusterName, err)) } return nil } -func warningIfFCVExpiredOrUnpinnedExternally(d *schema.ResourceData, cluster *admin.ClusterDescription20240805) diag.Diagnostics { +func WarningIfFCVExpiredOrUnpinnedExternally(d *schema.ResourceData, cluster *admin.ClusterDescription20240805) diag.Diagnostics { pinnedFCVBlock, ok := d.Get("pinned_fcv").([]any) presentInState := ok && len(pinnedFCVBlock) > 0 expirationDatePresent := cluster.FeatureCompatibilityVersionExpirationDate != nil @@ -895,7 +895,7 @@ func resourceUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag. timeout := d.Timeout(schema.TimeoutUpdate) // FCV update is intentionally handled before other cluster updates, and will wait for cluster to reach IDLE state before continuing - if diags := handlePinnedFCVUpdate(ctx, connV2, projectID, clusterName, d, timeout); diags != nil { + if diags := HandlePinnedFCVUpdate(ctx, connV2, projectID, clusterName, d, timeout); diags != nil { return diags } @@ -997,10 +997,10 @@ func resourceUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag. return resourceRead(ctx, d, meta) } -func handlePinnedFCVUpdate(ctx context.Context, connV2 *admin.APIClient, projectID, clusterName string, d *schema.ResourceData, timeout time.Duration) diag.Diagnostics { +func HandlePinnedFCVUpdate(ctx context.Context, connV2 *admin.APIClient, projectID, clusterName string, d *schema.ResourceData, timeout time.Duration) diag.Diagnostics { if d.HasChange("pinned_fcv") { if pinnedFCVBlock, ok := d.Get("pinned_fcv").([]any); ok && len(pinnedFCVBlock) > 0 { - if diags := pinFCV(ctx, connV2, projectID, clusterName, pinnedFCVBlock[0]); diags.HasError() { + if diags := PinFCV(ctx, connV2, projectID, clusterName, pinnedFCVBlock[0]); diags.HasError() { return diags } } else { @@ -1017,7 +1017,7 @@ func handlePinnedFCVUpdate(ctx context.Context, connV2 *admin.APIClient, project return nil } -func pinFCV(ctx context.Context, connV2 *admin.APIClient, projectID, clusterName string, fcvBlock any) diag.Diagnostics { +func PinFCV(ctx context.Context, connV2 *admin.APIClient, projectID, clusterName string, fcvBlock any) diag.Diagnostics { req := admin.PinFCV{} if nestedObj, ok := fcvBlock.(map[string]any); ok { expDateStrPtr := conversion.StringPtr(cast.ToString(nestedObj["expiration_date"])) diff --git a/internal/service/cluster/data_source_cluster.go b/internal/service/cluster/data_source_cluster.go index a1154e48ca..6f92f4101d 100644 --- a/internal/service/cluster/data_source_cluster.go +++ b/internal/service/cluster/data_source_cluster.go @@ -319,6 +319,22 @@ func DataSource() *schema.Resource { Type: schema.TypeBool, Computed: true, }, + "pinned_fcv": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "version": { + Type: schema.TypeString, + Computed: true, + }, + "expiration_date": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, }, } } @@ -386,10 +402,6 @@ func dataSourceRead(ctx context.Context, d *schema.ResourceData, meta any) diag. return diag.FromErr(fmt.Errorf(advancedcluster.ErrorClusterSetting, "encryption_at_rest_provider", clusterName, err)) } - if err := d.Set("mongo_db_major_version", cluster.MongoDBMajorVersion); err != nil { - return diag.FromErr(fmt.Errorf(advancedcluster.ErrorClusterSetting, "mongo_db_major_version", clusterName, err)) - } - // Avoid Global Cluster issues. (NumShards is not present in Global Clusters) if cluster.NumShards != nil { if err := d.Set("num_shards", cluster.NumShards); err != nil { @@ -491,14 +503,23 @@ func dataSourceRead(ctx context.Context, d *schema.ResourceData, meta any) diag. return diag.FromErr(err) } - redactClientLogData, err := newAtlasGet(ctx, connV2, projectID, clusterName) + latestClusterModel, err := newAtlasGet(ctx, connV2, projectID, clusterName) if err != nil { return diag.FromErr(fmt.Errorf(errorClusterRead, clusterName, err)) } - if err := d.Set("redact_client_log_data", redactClientLogData); err != nil { + + if err := d.Set("mongo_db_major_version", latestClusterModel.MongoDBMajorVersion); err != nil { // uses 2024-08-05 or above as it has fix for correct value when FCV is active + return diag.FromErr(fmt.Errorf(advancedcluster.ErrorClusterSetting, "mongo_db_major_version", clusterName, err)) + } + + if err := d.Set("redact_client_log_data", latestClusterModel.GetRedactClientLogData()); err != nil { return diag.FromErr(fmt.Errorf(advancedcluster.ErrorClusterSetting, "redact_client_log_data", clusterName, err)) } + if err := d.Set("pinned_fcv", advancedcluster.FlattenPinnedFCV(latestClusterModel)); err != nil { + return diag.FromErr(fmt.Errorf(advancedcluster.ErrorClusterSetting, "pinned_fcv", clusterName, err)) + } + d.SetId(cluster.ID) return nil diff --git a/internal/service/cluster/data_source_clusters.go b/internal/service/cluster/data_source_clusters.go index 7012274d78..ae9f4f7410 100644 --- a/internal/service/cluster/data_source_clusters.go +++ b/internal/service/cluster/data_source_clusters.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/mongodb/terraform-provider-mongodbatlas/internal/config" "github.com/mongodb/terraform-provider-mongodbatlas/internal/service/advancedcluster" + "go.mongodb.org/atlas-sdk/v20241113001/admin" matlas "go.mongodb.org/atlas/mongodbatlas" ) @@ -322,6 +323,22 @@ func PluralDataSource() *schema.Resource { Type: schema.TypeBool, Computed: true, }, + "pinned_fcv": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "version": { + Type: schema.TypeString, + Computed: true, + }, + "expiration_date": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, }, }, }, @@ -343,7 +360,7 @@ func dataSourcePluralRead(ctx context.Context, d *schema.ResourceData, meta any) return diag.FromErr(fmt.Errorf("error reading cluster list for project(%s): %s", projectID, err)) } - redactClientLogDataMap, err := newAtlasList(ctx, connV2, projectID) + latestClusterModels, err := newAtlasList(ctx, connV2, projectID) if err != nil { if resp != nil && resp.StatusCode == http.StatusNotFound { return nil @@ -351,14 +368,14 @@ func dataSourcePluralRead(ctx context.Context, d *schema.ResourceData, meta any) return diag.FromErr(fmt.Errorf("error reading new cluster list for project(%s): %s", projectID, err)) } - if err := d.Set("results", flattenClusters(ctx, d, conn, clusters, redactClientLogDataMap)); err != nil { + if err := d.Set("results", flattenClusters(ctx, d, conn, clusters, latestClusterModels)); err != nil { return diag.FromErr(fmt.Errorf(advancedcluster.ErrorClusterSetting, "results", d.Id(), err)) } return nil } -func flattenClusters(ctx context.Context, d *schema.ResourceData, conn *matlas.Client, clusters []matlas.Cluster, redactClientLogDataMap map[string]bool) []map[string]any { +func flattenClusters(ctx context.Context, d *schema.ResourceData, conn *matlas.Client, clusters []matlas.Cluster, latestClusterModels map[string]*admin.ClusterDescription20240805) []map[string]any { results := make([]map[string]any, 0) for i := range clusters { @@ -391,7 +408,7 @@ func flattenClusters(ctx context.Context, d *schema.ResourceData, conn *matlas.C "connection_strings": flattenConnectionStrings(clusters[i].ConnectionStrings), "disk_size_gb": clusters[i].DiskSizeGB, "encryption_at_rest_provider": clusters[i].EncryptionAtRestProvider, - "mongo_db_major_version": clusters[i].MongoDBMajorVersion, + "mongo_db_major_version": latestClusterModels[clusters[i].Name].MongoDBMajorVersion, // uses 2024-08-05 or above as it has fix for correct value when FCV is active "name": clusters[i].Name, "num_shards": clusters[i].NumShards, "mongo_db_version": clusters[i].MongoDBVersion, @@ -420,7 +437,8 @@ func flattenClusters(ctx context.Context, d *schema.ResourceData, conn *matlas.C "termination_protection_enabled": clusters[i].TerminationProtectionEnabled, "version_release_system": clusters[i].VersionReleaseSystem, "container_id": containerID, - "redact_client_log_data": redactClientLogDataMap[clusters[i].Name], + "redact_client_log_data": latestClusterModels[clusters[i].Name].GetRedactClientLogData(), + "pinned_fcv": advancedcluster.FlattenPinnedFCV(latestClusterModels[clusters[i].Name]), } results = append(results, result) } diff --git a/internal/service/cluster/new_atlas.go b/internal/service/cluster/new_atlas.go index 93335cd3fc..78b904bacb 100644 --- a/internal/service/cluster/new_atlas.go +++ b/internal/service/cluster/new_atlas.go @@ -14,7 +14,7 @@ func newAtlasUpdate(ctx context.Context, timeout time.Duration, connV2 *admin.AP if err != nil { return err } - if current == redactClientLogData { + if current.GetRedactClientLogData() == redactClientLogData { return nil } req := &admin20240805.ClusterDescription20240805{ @@ -31,20 +31,20 @@ func newAtlasUpdate(ctx context.Context, timeout time.Duration, connV2 *admin.AP return nil } -func newAtlasGet(ctx context.Context, connV2 *admin.APIClient, projectID, clusterName string) (redactClientLogData bool, err error) { +func newAtlasGet(ctx context.Context, connV2 *admin.APIClient, projectID, clusterName string) (*admin.ClusterDescription20240805, error) { cluster, _, err := connV2.ClustersApi.GetCluster(ctx, projectID, clusterName).Execute() - return cluster.GetRedactClientLogData(), err + return cluster, err } -func newAtlasList(ctx context.Context, connV2 *admin.APIClient, projectID string) (map[string]bool, error) { +func newAtlasList(ctx context.Context, connV2 *admin.APIClient, projectID string) (map[string]*admin.ClusterDescription20240805, error) { clusters, _, err := connV2.ClustersApi.ListClusters(ctx, projectID).Execute() if err != nil { return nil, err } results := clusters.GetResults() - list := make(map[string]bool) + list := make(map[string]*admin.ClusterDescription20240805) for i := range results { - list[results[i].GetName()] = results[i].GetRedactClientLogData() + list[results[i].GetName()] = &results[i] } return list, nil } diff --git a/internal/service/cluster/resource_cluster.go b/internal/service/cluster/resource_cluster.go index abf7559a8e..dd9833e0d0 100644 --- a/internal/service/cluster/resource_cluster.go +++ b/internal/service/cluster/resource_cluster.go @@ -354,6 +354,23 @@ func Resource() *schema.Resource { Optional: true, Computed: true, }, + "pinned_fcv": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "version": { + Type: schema.TypeString, + Computed: true, + }, + "expiration_date": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, }, CustomizeDiff: resourceClusterCustomizeDiff, Timeouts: &schema.ResourceTimeout{ @@ -570,7 +587,6 @@ func resourceCreate(ctx context.Context, d *schema.ResourceData, meta any) diag. clusterRequest = &matlas.Cluster{ Paused: conversion.Pointer(v), } - _, _, err = updateCluster(ctx, conn, connV2, clusterRequest, projectID, clusterName, timeout) if err != nil { return diag.FromErr(fmt.Errorf(errorClusterUpdate, clusterName, err)) @@ -583,6 +599,16 @@ func resourceCreate(ctx context.Context, d *schema.ResourceData, meta any) diag. } } + if pinnedFCVBlock, ok := d.Get("pinned_fcv").([]any); ok && len(pinnedFCVBlock) > 0 { + if diags := advancedcluster.PinFCV(ctx, connV2, projectID, clusterName, pinnedFCVBlock[0]); diags.HasError() { + return diags + } + stateConf := advancedcluster.CreateStateChangeConfig(ctx, connV2, projectID, clusterName, timeout) + if _, err = stateConf.WaitForStateContext(ctx); err != nil { + return diag.FromErr(fmt.Errorf(errorClusterUpdate, clusterName, err)) + } + } + d.SetId(conversion.EncodeStateID(map[string]string{ "cluster_id": cluster.ID, "project_id": projectID, @@ -659,10 +685,6 @@ func resourceRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Di return diag.FromErr(fmt.Errorf(advancedcluster.ErrorClusterSetting, "encryption_at_rest_provider", clusterName, err)) } - if err := d.Set("mongo_db_major_version", cluster.MongoDBMajorVersion); err != nil { - return diag.FromErr(fmt.Errorf(advancedcluster.ErrorClusterSetting, "mongo_db_major_version", clusterName, err)) - } - // Avoid Global Cluster issues. (NumShards is not present in Global Clusters) if cluster.NumShards != nil { if err := d.Set("num_shards", cluster.NumShards); err != nil { @@ -776,15 +798,25 @@ func resourceRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Di return diag.FromErr(err) } - redactClientLogData, err := newAtlasGet(ctx, connV2, projectID, clusterName) + latestClusterModel, err := newAtlasGet(ctx, connV2, projectID, clusterName) if err != nil { return diag.FromErr(fmt.Errorf(errorClusterRead, clusterName, err)) } - if err := d.Set("redact_client_log_data", redactClientLogData); err != nil { + if err := d.Set("redact_client_log_data", latestClusterModel.GetRedactClientLogData()); err != nil { return diag.FromErr(fmt.Errorf(advancedcluster.ErrorClusterSetting, "redact_client_log_data", clusterName, err)) } - return nil + if err := d.Set("mongo_db_major_version", latestClusterModel.MongoDBMajorVersion); err != nil { // uses 2024-08-05 or above as it has fix for correct value when FCV is active + return diag.FromErr(fmt.Errorf(advancedcluster.ErrorClusterSetting, "mongo_db_major_version", clusterName, err)) + } + + warning := advancedcluster.WarningIfFCVExpiredOrUnpinnedExternally(d, latestClusterModel) // has to be called before pinned_fcv value is updated in ResourceData to know prior state value + + if err := d.Set("pinned_fcv", advancedcluster.FlattenPinnedFCV(latestClusterModel)); err != nil { + return diag.FromErr(fmt.Errorf(advancedcluster.ErrorClusterSetting, "pinned_fcv", clusterName, err)) + } + + return warning } func resourceUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { @@ -795,6 +827,7 @@ func resourceUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag. ids = conversion.DecodeStateID(d.Id()) projectID = ids["project_id"] clusterName = ids["cluster_name"] + timeout = d.Timeout(schema.TimeoutUpdate) cluster = new(matlas.Cluster) clusterChangeDetect = &matlas.Cluster{ AutoScaling: &matlas.AutoScaling{ @@ -803,6 +836,11 @@ func resourceUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag. } ) + // FCV update is intentionally handled before other cluster updates, and will wait for cluster to reach IDLE state before continuing + if diags := advancedcluster.HandlePinnedFCVUpdate(ctx, connV2, projectID, clusterName, d, timeout); diags != nil { + return diags + } + if d.HasChange("name") { cluster.Name, _ = d.Get("name").(string) } @@ -933,8 +971,6 @@ func resourceUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag. cluster.Paused = conversion.Pointer(d.Get("paused").(bool)) } - timeout := d.Timeout(schema.TimeoutUpdate) - /* Check if advaced configuration option has a changes to update it */ diff --git a/internal/service/cluster/resource_cluster_test.go b/internal/service/cluster/resource_cluster_test.go index 9e4789fc5c..88024fd4f1 100644 --- a/internal/service/cluster/resource_cluster_test.go +++ b/internal/service/cluster/resource_cluster_test.go @@ -7,12 +7,14 @@ import ( "os" "regexp" "testing" + "time" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" clustersvc "github.com/mongodb/terraform-provider-mongodbatlas/internal/service/cluster" "github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/acc" + "go.mongodb.org/atlas-sdk/v20241113001/admin" matlas "go.mongodb.org/atlas/mongodbatlas" ) @@ -1402,6 +1404,81 @@ func TestAccCluster_create_RedactClientLogData(t *testing.T) { }) } +func TestAccCluster_pinnedFCVWithVersionUpgradeAndDowngrade(t *testing.T) { + var ( + orgID = os.Getenv("MONGODB_ATLAS_ORG_ID") + projectName = acc.RandomProjectName() // Using single project to assert plural data source + clusterName = acc.RandomClusterName() + ) + + now := time.Now() + // Time 7 days from now, truncated to the beginning of the day + sevenDaysFromNow := now.AddDate(0, 0, 7).Truncate(24 * time.Hour) + firstExpirationDate := conversion.TimeToString(sevenDaysFromNow) + // Time 8 days from now + eightDaysFromNow := sevenDaysFromNow.AddDate(0, 0, 1) + updatedExpirationDate := conversion.TimeToString(eightDaysFromNow) + invalidDateFormat := "invalid" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.PreCheckBasic(t) }, + ProtoV6ProviderFactories: acc.TestAccProviderV6Factories, + CheckDestroy: acc.CheckDestroyCluster, + Steps: []resource.TestStep{ + { + Config: configFCVPinning(orgID, projectName, clusterName, nil, "7.0"), + Check: acc.CheckRSAndDS(resourceName, admin.PtrString(dataSourceName), admin.PtrString(dataSourcePluralName), []string{}, map[string]string{ + "pinned_fcv.#": "0", + "mongo_db_major_version": "7.0", + }), + }, + { // pins fcv + Config: configFCVPinning(orgID, projectName, clusterName, &firstExpirationDate, "7.0"), + Check: acc.CheckRSAndDS(resourceName, admin.PtrString(dataSourceName), admin.PtrString(dataSourcePluralName), []string{}, map[string]string{ + "pinned_fcv.0.version": "7.0", + "pinned_fcv.0.expiration_date": firstExpirationDate, + "mongo_db_major_version": "7.0", + }), + }, + { // using incorrect format + Config: configFCVPinning(orgID, projectName, clusterName, &invalidDateFormat, "7.0"), + ExpectError: regexp.MustCompile("expiration_date format is incorrect: " + invalidDateFormat), + }, + { // updates expiration date of fcv + Config: configFCVPinning(orgID, projectName, clusterName, &updatedExpirationDate, "7.0"), + Check: acc.CheckRSAndDS(resourceName, admin.PtrString(dataSourceName), admin.PtrString(dataSourcePluralName), []string{}, map[string]string{ + "pinned_fcv.0.version": "7.0", + "pinned_fcv.0.expiration_date": updatedExpirationDate, + "mongo_db_major_version": "7.0", + }), + }, + { // upgrade mongodb version with fcv pinned + Config: configFCVPinning(orgID, projectName, clusterName, &updatedExpirationDate, "8.0"), + Check: acc.CheckRSAndDS(resourceName, admin.PtrString(dataSourceName), admin.PtrString(dataSourcePluralName), []string{}, map[string]string{ + "pinned_fcv.0.version": "7.0", + "pinned_fcv.0.expiration_date": updatedExpirationDate, + "mongo_db_major_version": "8.0", + }, resource.TestCheckResourceAttrWith(resourceName, "mongo_db_version", acc.MatchesExpression("8..*"))), + }, + { // downgrade mongodb version with fcv pinned + Config: configFCVPinning(orgID, projectName, clusterName, &updatedExpirationDate, "7.0"), + Check: acc.CheckRSAndDS(resourceName, admin.PtrString(dataSourceName), admin.PtrString(dataSourcePluralName), []string{}, map[string]string{ + "pinned_fcv.0.version": "7.0", + "pinned_fcv.0.expiration_date": updatedExpirationDate, + "mongo_db_major_version": "7.0", + }, resource.TestCheckResourceAttrWith(resourceName, "mongo_db_version", acc.MatchesExpression("7..*"))), + }, + { // unpins fcv + Config: configFCVPinning(orgID, projectName, clusterName, nil, "7.0"), + Check: acc.CheckRSAndDS(resourceName, admin.PtrString(dataSourceName), admin.PtrString(dataSourcePluralName), []string{}, map[string]string{ + "pinned_fcv.#": "0", + "mongo_db_major_version": "7.0", + }), + }, + }, + }) +} + func checkExists(resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] @@ -2416,6 +2493,54 @@ func configClusterGlobal(orgID, projectName, clusterName string) string { `, orgID, projectName, clusterName) } +func configFCVPinning(orgID, projectName, clusterName string, pinningExpirationDate *string, mongoDBMajorVersion string) string { + var pinnedFCVAttr string + if pinningExpirationDate != nil { + pinnedFCVAttr = fmt.Sprintf(` + pinned_fcv { + expiration_date = %q + } + `, *pinningExpirationDate) + } + + return fmt.Sprintf(` + resource "mongodbatlas_project" "test" { + org_id = %[1]q + name = %[2]q + } + + resource "mongodbatlas_cluster" "test" { + project_id = mongodbatlas_project.test.id + name = %[3]q + cluster_type = "REPLICASET" + provider_name = "AWS" + provider_instance_size_name = "M10" + + mongo_db_major_version = %[4]q + + %[5]s + + replication_specs { + num_shards = 1 + regions_config { + region_name = "US_WEST_2" + electable_nodes = 3 + priority = 7 + } + } + } + + data "mongodbatlas_cluster" "test" { + project_id = mongodbatlas_cluster.test.project_id + name = mongodbatlas_cluster.test.name + } + + data "mongodbatlas_clusters" "test" { + project_id = mongodbatlas_cluster.test.project_id + } + `, orgID, projectName, clusterName, mongoDBMajorVersion, pinnedFCVAttr) +} + func TestIsMultiRegionCluster(t *testing.T) { tests := []struct { name string