Skip to content

Commit 072b153

Browse files
committed
Merge branch 'master' into CLOUDP-274025-dev-docs-examples
* master: chore: Updates CHANGELOG.md for #3060 fix: Handles updates of `mongodbatlas_global_cluster_config` (#3060) chore: Updates CHANGELOG.md for #3069 fix: Handle the deleted on the backend case correctly for `mongodbatlas_database_user` resource (#3069) build(deps): bump go.mongodb.org/atlas-sdk (#3082)
2 parents 7b2d1d5 + 363badf commit 072b153

File tree

9 files changed

+187
-11
lines changed

9 files changed

+187
-11
lines changed

.changelog/3060.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
resource/mongodbatlas_global_cluster_config: Supports update operation
3+
```

.changelog/3069.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:bug
2+
resource/mongodbatlas_database_user: Avoids error in read if resource no longer exists
3+
```

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
## (Unreleased)
22

3+
ENHANCEMENTS:
4+
5+
* resource/mongodbatlas_global_cluster_config: Supports update operation ([#3060](https://github.com/mongodb/terraform-provider-mongodbatlas/pull/3060))
6+
37
BUG FIXES:
48

59
* resource/mongodbatlas_alert_configuration: Removes UseStateForUnknown plan modifier for interval_min ([#3051](https://github.com/mongodb/terraform-provider-mongodbatlas/pull/3051))
10+
* resource/mongodbatlas_database_user: Avoids error in read if resource no longer exists ([#3069](https://github.com/mongodb/terraform-provider-mongodbatlas/pull/3069))
611

712
## 1.26.1 (February 07, 2025)
813

docs/resources/global_cluster_config.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
-> **NOTE:** This resource can only be used with Atlas-managed clusters. See doc for `global_cluster_self_managed_sharding` attribute in [`mongodbatlas_advanced_cluster` resource](https://registry.terraform.io/providers/mongodb/mongodbatlas/latest/docs/resources/advanced_cluster) for more info.
88

9-
~> **IMPORTANT:** A Global Cluster Configuration, once created, can only be deleted. You can recreate the Global Cluster with the same data only in the Atlas UI. This is because the configuration and its related collection with shard key and indexes are managed separately and they would end up in an inconsistent state. [Read more about Global Cluster Configuration](https://www.mongodb.com/docs/atlas/global-clusters/)
9+
~> **IMPORTANT:** You can update a Global Cluster Configuration to add new custom zone mappings and managed namespaces. However, once configured, you can't modify or partially delete custom zone mappings (you must remove them all at once). You can add or remove, but can't modify, managed namespaces. Any update that changes an existing managed namespace results in an error. [Read more about Global Cluster Configuration](https://www.mongodb.com/docs/atlas/global-clusters/). For more details, see [Global Clusters API](https://www.mongodb.com/docs/atlas/reference/api-resources-spec/v2/#tag/Global-Clusters)
1010

1111
## Examples Usage
1212

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ require (
2424
github.com/huandu/xstrings v1.5.0
2525
github.com/jarcoal/httpmock v1.3.1
2626
github.com/mongodb-forks/digest v1.1.0
27-
github.com/mongodb/atlas-sdk-go v1.0.1-0.20250210083648-31e2c436c287
27+
github.com/mongodb/atlas-sdk-go v1.0.1-0.20250217083801-6e38b3b1124c
2828
github.com/pb33f/libopenapi v0.21.2
2929
github.com/sebdah/goldie/v2 v2.5.5
3030
github.com/spf13/cast v1.7.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -654,8 +654,8 @@ github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx
654654
github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM=
655655
github.com/mongodb-forks/digest v1.1.0 h1:7eUdsR1BtqLv0mdNm4OXs6ddWvR4X2/OsLwdKksrOoc=
656656
github.com/mongodb-forks/digest v1.1.0/go.mod h1:rb+EX8zotClD5Dj4NdgxnJXG9nwrlx3NWKJ8xttz1Dg=
657-
github.com/mongodb/atlas-sdk-go v1.0.1-0.20250210083648-31e2c436c287 h1:4fcg+dcr1k1hHbzXbirdlDvJIiSVJz8iAzsIF8FPHQg=
658-
github.com/mongodb/atlas-sdk-go v1.0.1-0.20250210083648-31e2c436c287/go.mod h1:gjkhjqMH7Mzk0Rd9utdjDGpgDbW8x48OzYNw3DRVKN4=
657+
github.com/mongodb/atlas-sdk-go v1.0.1-0.20250217083801-6e38b3b1124c h1:NT76vwa9MZiRFlK7iOTX8Pmv8QrIA1T04dKxiLwlLR0=
658+
github.com/mongodb/atlas-sdk-go v1.0.1-0.20250217083801-6e38b3b1124c/go.mod h1:gjkhjqMH7Mzk0Rd9utdjDGpgDbW8x48OzYNw3DRVKN4=
659659
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
660660
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
661661
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=

internal/service/databaseuser/resource_database_user.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,6 @@ func (r *databaseUserRS) Read(ctx context.Context, req resource.ReadRequest, res
267267
// deleted in the backend case
268268
if validate.StatusNotFound(httpResponse) {
269269
resp.State.RemoveResource(ctx)
270-
resp.Diagnostics.AddError("resource not found", err.Error())
271270
return
272271
}
273272
resp.Diagnostics.AddError("error getting database user information", err.Error())

internal/service/globalclusterconfig/resource_global_cluster_config.go

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"reflect"
78
"strings"
89
"time"
910

@@ -211,9 +212,38 @@ func resourceRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Di
211212
}
212213

213214
func resourceUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
214-
return diag.Errorf("Updating a global cluster configuration resource is not allowed as it would " +
215-
"leave the index and shard key on the related collection in an inconsistent state.\n" +
216-
"Please read our official documentation for more information.")
215+
connV2 := meta.(*config.MongoDBClient).AtlasV2
216+
ids := conversion.DecodeStateID(d.Id())
217+
projectID := ids["project_id"]
218+
clusterName := ids["cluster_name"]
219+
220+
if d.HasChange("managed_namespaces") {
221+
oldMN, newMN := d.GetChange("managed_namespaces")
222+
oldList := oldMN.(*schema.Set).List()
223+
newList := newMN.(*schema.Set).List()
224+
if err := updateManagedNamespaces(ctx, connV2, projectID, clusterName, oldList, newList); err != nil {
225+
return diag.FromErr(fmt.Errorf(errorGlobalClusterUpdate, clusterName, err))
226+
}
227+
}
228+
229+
if d.HasChange("custom_zone_mappings") {
230+
oldZN, newZN := d.GetChange("custom_zone_mappings")
231+
oldSet := oldZN.(*schema.Set)
232+
newSet := newZN.(*schema.Set)
233+
if err := updateCustomZoneMappings(ctx, connV2, projectID, clusterName, oldSet, newSet); err != nil {
234+
return diag.FromErr(fmt.Errorf(errorGlobalClusterUpdate, clusterName, err))
235+
}
236+
}
237+
return resourceRead(ctx, d, meta)
238+
}
239+
240+
// convertInterfaceSlice is a helper function that converts []map[string]any into []any
241+
func convertInterfaceSlice(input []map[string]any) []any {
242+
var out []any
243+
for _, v := range input {
244+
out = append(out, v)
245+
}
246+
return out
217247
}
218248

219249
func resourceDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
@@ -335,3 +365,103 @@ func newCustomZoneMappings(tfList []any) *[]admin.ZoneMapping {
335365

336366
return &apiObjects
337367
}
368+
369+
func addManagedNamespaces(ctx context.Context, connV2 *admin.APIClient, add []any, projectID, clusterName string) error {
370+
for _, m := range add {
371+
mn := m.(map[string]any)
372+
373+
addManagedNamespace := &admin.ManagedNamespaces{
374+
Collection: mn["collection"].(string),
375+
Db: mn["db"].(string),
376+
CustomShardKey: mn["custom_shard_key"].(string),
377+
}
378+
if isCustomShardKeyHashed, okCustomShard := mn["is_custom_shard_key_hashed"]; okCustomShard {
379+
addManagedNamespace.IsCustomShardKeyHashed = conversion.Pointer[bool](isCustomShardKeyHashed.(bool))
380+
}
381+
if isShardKeyUnique, okShard := mn["is_shard_key_unique"]; okShard {
382+
addManagedNamespace.IsShardKeyUnique = conversion.Pointer[bool](isShardKeyUnique.(bool))
383+
}
384+
_, _, err := connV2.GlobalClustersApi.CreateManagedNamespace(ctx, projectID, clusterName, addManagedNamespace).Execute()
385+
if err != nil {
386+
return err
387+
}
388+
}
389+
return nil
390+
}
391+
392+
// buildManagedNamespacesMap converts a list of managed_namespace entries into a map keyed by "collection:db"
393+
func buildManagedNamespacesMap(list []any) map[string]map[string]any {
394+
namespacesMap := make(map[string]map[string]any)
395+
for _, item := range list {
396+
m := item.(map[string]any)
397+
key := fmt.Sprintf("%s:%s", m["collection"].(string), m["db"].(string))
398+
namespacesMap[key] = m
399+
}
400+
return namespacesMap
401+
}
402+
403+
// diffManagedNamespaces calculates the difference between old and new managed_namespaces.
404+
// Returns slices of namespaces to add and remove; errors out on modifications.
405+
func diffManagedNamespaces(oldList, newList []any) (toAdd, toRemove []map[string]any, err error) {
406+
oldMap := buildManagedNamespacesMap(oldList)
407+
newMap := buildManagedNamespacesMap(newList)
408+
for key, oldEntry := range oldMap {
409+
if newEntry, exists := newMap[key]; exists {
410+
// Modification is not allowed.
411+
if !reflect.DeepEqual(oldEntry, newEntry) {
412+
return nil, nil, fmt.Errorf("managed namespace for collection '%s' in db '%s' cannot be modified", oldEntry["collection"], oldEntry["db"])
413+
}
414+
} else {
415+
toRemove = append(toRemove, oldEntry)
416+
}
417+
}
418+
for key, newEntry := range newMap {
419+
if _, exists := oldMap[key]; !exists {
420+
toAdd = append(toAdd, newEntry)
421+
}
422+
}
423+
return toAdd, toRemove, nil
424+
}
425+
426+
// updateManagedNamespaces encapsulates diffing and applying removals/additions.
427+
func updateManagedNamespaces(ctx context.Context, connV2 *admin.APIClient, projectID, clusterName string, oldList, newList []any) error {
428+
toAdd, toRemove, err := diffManagedNamespaces(oldList, newList)
429+
if err != nil {
430+
return err
431+
}
432+
if len(toRemove) > 0 {
433+
if err := removeManagedNamespaces(ctx, connV2, convertInterfaceSlice(toRemove), projectID, clusterName); err != nil {
434+
return err
435+
}
436+
}
437+
if len(toAdd) > 0 {
438+
if err := addManagedNamespaces(ctx, connV2, convertInterfaceSlice(toAdd), projectID, clusterName); err != nil {
439+
return err
440+
}
441+
}
442+
return nil
443+
}
444+
445+
// updateCustomZoneMappings encapsulates diffing and applying changes for custom_zone_mappings.
446+
func updateCustomZoneMappings(ctx context.Context, connV2 *admin.APIClient, projectID, clusterName string, oldSet, newSet *schema.Set) error {
447+
removed := oldSet.Difference(newSet).List()
448+
added := newSet.Difference(oldSet).List()
449+
450+
if len(removed) > 0 {
451+
// Allow deletion only if all mappings are removed.
452+
if newSet.Len() != 0 {
453+
return fmt.Errorf("partial deletion of custom_zone_mappings is not allowed; remove either all mappings or none")
454+
}
455+
if _, _, err := connV2.GlobalClustersApi.DeleteAllCustomZoneMappings(ctx, projectID, clusterName).Execute(); err != nil {
456+
return err
457+
}
458+
}
459+
if len(added) > 0 {
460+
if _, _, err := connV2.GlobalClustersApi.CreateCustomZoneMapping(ctx, projectID, clusterName, &admin.CustomZoneMappings{
461+
CustomZoneMappings: newCustomZoneMappings(added),
462+
}).Execute(); err != nil {
463+
return err
464+
}
465+
}
466+
return nil
467+
}

internal/service/globalclusterconfig/resource_global_cluster_config_test.go

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func basicTestCase(tb testing.TB, checkZoneID, withBackup bool) *resource.TestCa
6161
},
6262
{
6363
Config: configBasic(&clusterInfo, true, false),
64-
ExpectError: regexp.MustCompile("Updating a global cluster configuration resource is not allowed"),
64+
ExpectError: regexp.MustCompile("managed namespace for collection 'publishers' in db 'mydata' cannot be modified"),
6565
},
6666
},
6767
}
@@ -137,8 +137,44 @@ func TestAccGlobalClusterConfig_database(t *testing.T) {
137137
),
138138
},
139139
{
140-
Config: configWithDBConfig(&clusterInfo, customZoneUpdated),
141-
ExpectError: regexp.MustCompile("Updating a global cluster configuration resource is not allowed"),
140+
Config: configWithDBConfig(&clusterInfo, customZoneUpdated),
141+
Check: resource.ComposeAggregateTestCheckFunc(
142+
checkExists(resourceName),
143+
checkZone(0, "US", clusterInfo.ResourceName, true),
144+
checkZone(1, "IE", clusterInfo.ResourceName, true),
145+
checkZone(2, "DE", clusterInfo.ResourceName, true),
146+
checkZone(3, "JP", clusterInfo.ResourceName, true),
147+
acc.CheckRSAndDS(resourceName, conversion.Pointer(dataSourceName), nil,
148+
[]string{"project_id"},
149+
map[string]string{
150+
"cluster_name": clusterInfo.Name,
151+
"managed_namespaces.#": "5",
152+
"managed_namespaces.0.is_custom_shard_key_hashed": "false",
153+
"managed_namespaces.0.is_shard_key_unique": "false",
154+
"custom_zone_mapping_zone_id.%": "4",
155+
"custom_zone_mapping.%": "4",
156+
}),
157+
),
158+
},
159+
{
160+
Config: configWithDBConfig(&clusterInfo, customZone),
161+
ExpectError: regexp.MustCompile("partial deletion of custom_zone_mappings is not allowed; remove either all mappings or none"),
162+
},
163+
{
164+
Config: configWithDBConfig(&clusterInfo, ""),
165+
Check: resource.ComposeAggregateTestCheckFunc(
166+
checkExists(resourceName),
167+
acc.CheckRSAndDS(resourceName, conversion.Pointer(dataSourceName), nil,
168+
[]string{"project_id"},
169+
map[string]string{
170+
"cluster_name": clusterInfo.Name,
171+
"managed_namespaces.#": "5",
172+
"managed_namespaces.0.is_custom_shard_key_hashed": "false",
173+
"managed_namespaces.0.is_shard_key_unique": "false",
174+
"custom_zone_mapping_zone_id.%": "0",
175+
"custom_zone_mapping.%": "0",
176+
}),
177+
),
142178
},
143179
{
144180
ResourceName: resourceName,

0 commit comments

Comments
 (0)