From 0a9e41b3c4a092343dcb24d3209acc75d1396693 Mon Sep 17 00:00:00 2001 From: magodo Date: Wed, 26 Feb 2025 17:10:25 +1100 Subject: [PATCH 1/5] `azurerm_mongo_cluster` - Expose `connection_strings` --- .../mongocluster/mongo_cluster_resource.go | 147 +++++++++++++----- .../mongo_cluster_resource_test.go | 14 +- 2 files changed, 116 insertions(+), 45 deletions(-) diff --git a/internal/services/mongocluster/mongo_cluster_resource.go b/internal/services/mongocluster/mongo_cluster_resource.go index caec0515382c..1f8064e6c0b1 100644 --- a/internal/services/mongocluster/mongo_cluster_resource.go +++ b/internal/services/mongocluster/mongo_cluster_resource.go @@ -6,6 +6,7 @@ package mongocluster import ( "context" "fmt" + "net/url" "regexp" "time" @@ -14,7 +15,6 @@ import ( "github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema" "github.com/hashicorp/go-azure-helpers/resourcemanager/location" "github.com/hashicorp/go-azure-sdk/resource-manager/mongocluster/2024-07-01/mongoclusters" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" @@ -27,22 +27,29 @@ var _ sdk.ResourceWithUpdate = MongoClusterResource{} var _ sdk.ResourceWithCustomizeDiff = MongoClusterResource{} type MongoClusterResourceModel struct { - Name string `tfschema:"name"` - ResourceGroupName string `tfschema:"resource_group_name"` - Location string `tfschema:"location"` - AdministratorUserName string `tfschema:"administrator_username"` - AdministratorPassword string `tfschema:"administrator_password"` - CreateMode string `tfschema:"create_mode"` - ShardCount int64 `tfschema:"shard_count"` - SourceLocation string `tfschema:"source_location"` - SourceServerId string `tfschema:"source_server_id"` - ComputeTier string `tfschema:"compute_tier"` - HighAvailabilityMode string `tfschema:"high_availability_mode"` - PublicNetworkAccess string `tfschema:"public_network_access"` - PreviewFeatures []string `tfschema:"preview_features"` - StorageSizeInGb int64 `tfschema:"storage_size_in_gb"` - Tags map[string]string `tfschema:"tags"` - Version string `tfschema:"version"` + Name string `tfschema:"name"` + ResourceGroupName string `tfschema:"resource_group_name"` + Location string `tfschema:"location"` + AdministratorUserName string `tfschema:"administrator_username"` + AdministratorPassword string `tfschema:"administrator_password"` + CreateMode string `tfschema:"create_mode"` + ShardCount int64 `tfschema:"shard_count"` + SourceLocation string `tfschema:"source_location"` + SourceServerId string `tfschema:"source_server_id"` + ComputeTier string `tfschema:"compute_tier"` + HighAvailabilityMode string `tfschema:"high_availability_mode"` + PublicNetworkAccess string `tfschema:"public_network_access"` + PreviewFeatures []string `tfschema:"preview_features"` + StorageSizeInGb int64 `tfschema:"storage_size_in_gb"` + ConnectionStrings []MongoClusterConnectionString `tfschema:"connection_strings"` + Tags map[string]string `tfschema:"tags"` + Version string `tfschema:"version"` +} + +type MongoClusterConnectionString struct { + ConnectionString string `tfschema:"connection_string"` + Description string `tfschema:"description"` + Name string `tfschema:"name"` } func (r MongoClusterResource) ModelObject() interface{} { @@ -57,12 +64,12 @@ func (r MongoClusterResource) ResourceType() string { return "azurerm_mongo_cluster" } -func (r MongoClusterResource) Arguments() map[string]*schema.Schema { - return map[string]*schema.Schema{ +func (r MongoClusterResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ "name": { ForceNew: true, Required: true, - Type: schema.TypeString, + Type: pluginsdk.TypeString, ValidateFunc: validation.StringMatch( regexp.MustCompile(`^[a-z\d]([-a-z\d]{1,38}[a-z\d])$`), "`name` must be between 3 and 40 characters. It can contain only lowercase letters, numbers, and hyphens (-). It must start and end with a lowercase letter or number.", @@ -74,7 +81,7 @@ func (r MongoClusterResource) Arguments() map[string]*schema.Schema { "location": commonschema.Location(), "administrator_username": { - Type: schema.TypeString, + Type: pluginsdk.TypeString, Optional: true, ForceNew: true, ValidateFunc: validation.StringIsNotEmpty, @@ -82,7 +89,7 @@ func (r MongoClusterResource) Arguments() map[string]*schema.Schema { }, "create_mode": { - Type: schema.TypeString, + Type: pluginsdk.TypeString, Optional: true, ForceNew: true, Default: string(mongoclusters.CreateModeDefault), @@ -94,24 +101,24 @@ func (r MongoClusterResource) Arguments() map[string]*schema.Schema { }, "preview_features": { - Type: schema.TypeList, + Type: pluginsdk.TypeList, Optional: true, ForceNew: true, - Elem: &schema.Schema{ - Type: schema.TypeString, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, ValidateFunc: validation.StringInSlice(mongoclusters.PossibleValuesForPreviewFeature(), false), }, }, "shard_count": { - Type: schema.TypeInt, + Type: pluginsdk.TypeInt, Optional: true, ValidateFunc: validation.IntAtLeast(1), ForceNew: true, }, "source_location": { - Type: schema.TypeString, + Type: pluginsdk.TypeString, Optional: true, ForceNew: true, StateFunc: location.StateFunc, @@ -121,14 +128,14 @@ func (r MongoClusterResource) Arguments() map[string]*schema.Schema { }, "source_server_id": { - Type: schema.TypeString, + Type: pluginsdk.TypeString, Optional: true, ForceNew: true, ValidateFunc: mongoclusters.ValidateMongoClusterID, }, "administrator_password": { - Type: schema.TypeString, + Type: pluginsdk.TypeString, Optional: true, Sensitive: true, ValidateFunc: validation.StringIsNotEmpty, @@ -136,7 +143,7 @@ func (r MongoClusterResource) Arguments() map[string]*schema.Schema { }, "compute_tier": { - Type: schema.TypeString, + Type: pluginsdk.TypeString, Optional: true, ValidateFunc: validation.StringInSlice([]string{ "Free", @@ -150,7 +157,7 @@ func (r MongoClusterResource) Arguments() map[string]*schema.Schema { }, "high_availability_mode": { - Type: schema.TypeString, + Type: pluginsdk.TypeString, Optional: true, ValidateFunc: validation.StringInSlice([]string{ // Confirmed with service team the `SameZone` is currently not supported. @@ -160,14 +167,14 @@ func (r MongoClusterResource) Arguments() map[string]*schema.Schema { }, "public_network_access": { - Type: schema.TypeString, + Type: pluginsdk.TypeString, Optional: true, Default: string(mongoclusters.PublicNetworkAccessEnabled), ValidateFunc: validation.StringInSlice(mongoclusters.PossibleValuesForPublicNetworkAccess(), false), }, "storage_size_in_gb": { - Type: schema.TypeInt, + Type: pluginsdk.TypeInt, Optional: true, ValidateFunc: validation.IntBetween(32, 16384), }, @@ -175,7 +182,7 @@ func (r MongoClusterResource) Arguments() map[string]*schema.Schema { "tags": commonschema.Tags(), "version": { - Type: schema.TypeString, + Type: pluginsdk.TypeString, Optional: true, ValidateFunc: validation.StringInSlice([]string{ "5.0", @@ -186,8 +193,30 @@ func (r MongoClusterResource) Arguments() map[string]*schema.Schema { } } -func (r MongoClusterResource) Attributes() map[string]*schema.Schema { - return map[string]*schema.Schema{} +func (r MongoClusterResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "connection_strings": { + Type: pluginsdk.TypeList, + Sensitive: true, + Computed: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "description": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "connection_string": { + Type: pluginsdk.TypeString, + Computed: true, + }, + }, + }, + }, + } } func (r MongoClusterResource) Create() sdk.ResourceFunc { @@ -452,6 +481,14 @@ func (r MongoClusterResource) Read() sdk.ResourceFunc { state.Tags = pointer.From(model.Tags) } + csResp, err := client.ListConnectionStrings(ctx, *id) + if err != nil { + return fmt.Errorf("listing connection string for %s: %+v", *id, err) + } + if model := csResp.Model; model != nil { + state.ConnectionStrings = flattenMongoClusterConnectionString(model.ConnectionStrings, state.AdministratorUserName, state.AdministratorPassword) + } + return metadata.Encode(&state) }, } @@ -570,3 +607,41 @@ func flattenMongoClusterPreviewFeatures(input *[]mongoclusters.PreviewFeature) [ return results } + +func flattenMongoClusterConnectionString(input *[]mongoclusters.ConnectionString, userName, userPassword string) []MongoClusterConnectionString { + if input == nil { + return nil + } + + var results []MongoClusterConnectionString + for _, cs := range *input { + var name string + if cs.Name != nil { + name = *cs.Name + } + + var description string + if cs.Description != nil { + description = *cs.Description + } + + var connectionString string + if cs.ConnectionString != nil { + connectionString = *cs.ConnectionString + } + + // Password can be empty if it isn't available in the state file (e.g. during import). + // In this case, we simply leave the placeholder unchanged. + if userPassword != "" { + connectionString = regexp.MustCompile(`:`).ReplaceAllString(connectionString, url.UserPassword(userName, userPassword).String()) + } + + results = append(results, MongoClusterConnectionString{ + Name: name, + Description: description, + ConnectionString: connectionString, + }) + } + + return results +} diff --git a/internal/services/mongocluster/mongo_cluster_resource_test.go b/internal/services/mongocluster/mongo_cluster_resource_test.go index 3b27167d70a6..aea1645ca88c 100644 --- a/internal/services/mongocluster/mongo_cluster_resource_test.go +++ b/internal/services/mongocluster/mongo_cluster_resource_test.go @@ -36,9 +36,12 @@ func testAccMongoCluster_basic(t *testing.T) { Config: r.basic(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("connection_strings.0.connection_string").HasValue( + fmt.Sprintf(`mongodb+srv://adminTerraform:QAZwsx123basic@acctest-mc%d.mongocluster.cosmos.azure.com/?tls=true&authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000`, + data.RandomInteger)), ), }, - data.ImportStep("administrator_password", "create_mode"), + data.ImportStep("administrator_password", "create_mode", "connection_strings.0.connection_string"), }) } @@ -134,19 +137,12 @@ func (r MongoClusterResource) basic(data acceptance.TestData) string { return fmt.Sprintf(` %s -resource "random_password" "test" { - length = 8 - numeric = true - upper = true - lower = true -} - resource "azurerm_mongo_cluster" "test" { name = "acctest-mc%d" resource_group_name = azurerm_resource_group.test.name location = azurerm_resource_group.test.location administrator_username = "adminTerraform" - administrator_password = random_password.test.result + administrator_password = "QAZwsx123basic" shard_count = "1" compute_tier = "Free" high_availability_mode = "Disabled" From f0237c5353594f19fa27a900f4df2c81886a1969 Mon Sep 17 00:00:00 2001 From: magodo Date: Wed, 26 Feb 2025 17:20:32 +1100 Subject: [PATCH 2/5] doc --- website/docs/r/mongo_cluster.html.markdown | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/website/docs/r/mongo_cluster.html.markdown b/website/docs/r/mongo_cluster.html.markdown index 11efa3124ec2..8ed0cb4d09b2 100644 --- a/website/docs/r/mongo_cluster.html.markdown +++ b/website/docs/r/mongo_cluster.html.markdown @@ -111,6 +111,18 @@ In addition to the Arguments listed above - the following Attributes are exporte * `id` - The ID of the MongoDB Cluster. +* `connection_strings` - The list of `connection_strings` blocks as defined below. + +--- + +A `connection_strings` exports the following: + +* `name` - The name of the connection string. + +* `description` - The description of the connection string. + +* `connection_string` - The Mongo Cluster connection string. The the `:` place holder returned from API will be replaced by the real `administrator_username` and `administrator_password` if available in the state. + ## Timeouts The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: From cd2a3ea123baacbc631dff9eb4c1c7d5a06fe4d3 Mon Sep 17 00:00:00 2001 From: magodo Date: Wed, 26 Feb 2025 19:06:34 +1100 Subject: [PATCH 3/5] golangci --- internal/services/mongocluster/mongo_cluster_resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/services/mongocluster/mongo_cluster_resource.go b/internal/services/mongocluster/mongo_cluster_resource.go index 1f8064e6c0b1..eb3f9c6c5b30 100644 --- a/internal/services/mongocluster/mongo_cluster_resource.go +++ b/internal/services/mongocluster/mongo_cluster_resource.go @@ -613,7 +613,7 @@ func flattenMongoClusterConnectionString(input *[]mongoclusters.ConnectionString return nil } - var results []MongoClusterConnectionString + results := make([]MongoClusterConnectionString, 0) for _, cs := range *input { var name string if cs.Name != nil { From 155feb3c41a88f1a14365ddabdbdfdfdd51c258a Mon Sep 17 00:00:00 2001 From: magodo Date: Thu, 27 Feb 2025 10:05:05 +1100 Subject: [PATCH 4/5] typo --- website/docs/r/mongo_cluster.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/r/mongo_cluster.html.markdown b/website/docs/r/mongo_cluster.html.markdown index 8ed0cb4d09b2..2b3c18a1f209 100644 --- a/website/docs/r/mongo_cluster.html.markdown +++ b/website/docs/r/mongo_cluster.html.markdown @@ -121,7 +121,7 @@ A `connection_strings` exports the following: * `description` - The description of the connection string. -* `connection_string` - The Mongo Cluster connection string. The the `:` place holder returned from API will be replaced by the real `administrator_username` and `administrator_password` if available in the state. +* `connection_string` - The Mongo Cluster connection string. The `:` place holder returned from API will be replaced by the real `administrator_username` and `administrator_password` if available in the state. ## Timeouts From 9644b1f95e5f5866fe1d480de1af6f624963fd72 Mon Sep 17 00:00:00 2001 From: magodo Date: Fri, 28 Feb 2025 11:21:30 +1100 Subject: [PATCH 5/5] Resolving review comments --- .../mongocluster/mongo_cluster_resource.go | 43 ++++++------------- .../mongo_cluster_resource_test.go | 14 +++--- website/docs/r/mongo_cluster.html.markdown | 2 +- 3 files changed, 22 insertions(+), 37 deletions(-) diff --git a/internal/services/mongocluster/mongo_cluster_resource.go b/internal/services/mongocluster/mongo_cluster_resource.go index eb3f9c6c5b30..ccbc65bf8066 100644 --- a/internal/services/mongocluster/mongo_cluster_resource.go +++ b/internal/services/mongocluster/mongo_cluster_resource.go @@ -47,9 +47,9 @@ type MongoClusterResourceModel struct { } type MongoClusterConnectionString struct { - ConnectionString string `tfschema:"connection_string"` - Description string `tfschema:"description"` - Name string `tfschema:"name"` + Value string `tfschema:"value"` + Description string `tfschema:"description"` + Name string `tfschema:"name"` } func (r MongoClusterResource) ModelObject() interface{} { @@ -209,7 +209,7 @@ func (r MongoClusterResource) Attributes() map[string]*pluginsdk.Schema { Type: pluginsdk.TypeString, Computed: true, }, - "connection_string": { + "value": { Type: pluginsdk.TypeString, Computed: true, }, @@ -483,10 +483,10 @@ func (r MongoClusterResource) Read() sdk.ResourceFunc { csResp, err := client.ListConnectionStrings(ctx, *id) if err != nil { - return fmt.Errorf("listing connection string for %s: %+v", *id, err) + return fmt.Errorf("listing connection strings for %s: %+v", *id, err) } if model := csResp.Model; model != nil { - state.ConnectionStrings = flattenMongoClusterConnectionString(model.ConnectionStrings, state.AdministratorUserName, state.AdministratorPassword) + state.ConnectionStrings = flattenMongoClusterConnectionStrings(model.ConnectionStrings, state.AdministratorUserName, state.AdministratorPassword) } return metadata.Encode(&state) @@ -608,38 +608,23 @@ func flattenMongoClusterPreviewFeatures(input *[]mongoclusters.PreviewFeature) [ return results } -func flattenMongoClusterConnectionString(input *[]mongoclusters.ConnectionString, userName, userPassword string) []MongoClusterConnectionString { +func flattenMongoClusterConnectionStrings(input *[]mongoclusters.ConnectionString, userName, userPassword string) []MongoClusterConnectionString { + results := make([]MongoClusterConnectionString, 0) if input == nil { - return nil + return results } - - results := make([]MongoClusterConnectionString, 0) for _, cs := range *input { - var name string - if cs.Name != nil { - name = *cs.Name - } - - var description string - if cs.Description != nil { - description = *cs.Description - } - - var connectionString string - if cs.ConnectionString != nil { - connectionString = *cs.ConnectionString - } - + value := pointer.From(cs.ConnectionString) // Password can be empty if it isn't available in the state file (e.g. during import). // In this case, we simply leave the placeholder unchanged. if userPassword != "" { - connectionString = regexp.MustCompile(`:`).ReplaceAllString(connectionString, url.UserPassword(userName, userPassword).String()) + value = regexp.MustCompile(`:`).ReplaceAllString(value, url.UserPassword(userName, userPassword).String()) } results = append(results, MongoClusterConnectionString{ - Name: name, - Description: description, - ConnectionString: connectionString, + Name: pointer.From(cs.Name), + Description: pointer.From(cs.Description), + Value: value, }) } diff --git a/internal/services/mongocluster/mongo_cluster_resource_test.go b/internal/services/mongocluster/mongo_cluster_resource_test.go index aea1645ca88c..abd184e72c47 100644 --- a/internal/services/mongocluster/mongo_cluster_resource_test.go +++ b/internal/services/mongocluster/mongo_cluster_resource_test.go @@ -36,12 +36,12 @@ func testAccMongoCluster_basic(t *testing.T) { Config: r.basic(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), - check.That(data.ResourceName).Key("connection_strings.0.connection_string").HasValue( + check.That(data.ResourceName).Key("connection_strings.0.value").HasValue( fmt.Sprintf(`mongodb+srv://adminTerraform:QAZwsx123basic@acctest-mc%d.mongocluster.cosmos.azure.com/?tls=true&authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000`, data.RandomInteger)), ), }, - data.ImportStep("administrator_password", "create_mode", "connection_strings.0.connection_string"), + data.ImportStep("administrator_password", "create_mode", "connection_strings.0.value"), }) } @@ -56,14 +56,14 @@ func testAccMongoCluster_update(t *testing.T) { check.That(data.ResourceName).ExistsInAzure(r), ), }, - data.ImportStep("administrator_password", "create_mode"), + data.ImportStep("administrator_password", "create_mode", "connection_strings.0.value"), { Config: r.update(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), }, - data.ImportStep("administrator_password", "create_mode"), + data.ImportStep("administrator_password", "create_mode", "connection_strings.0.value"), }) } @@ -93,14 +93,14 @@ func TestAccMongoCluster_previewFeature(t *testing.T) { check.That(data.ResourceName).ExistsInAzure(r), ), }, - data.ImportStep("administrator_password", "create_mode"), + data.ImportStep("administrator_password", "create_mode", "connection_strings.0.value"), { Config: r.geoReplica(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), }, - data.ImportStep("administrator_password", "create_mode", "source_location"), + data.ImportStep("administrator_password", "create_mode", "source_location", "connection_strings.0.value"), }) } @@ -115,7 +115,7 @@ func TestAccMongoCluster_geoReplica(t *testing.T) { check.That(data.ResourceName).ExistsInAzure(r), ), }, - data.ImportStep("administrator_password", "create_mode", "source_location"), + data.ImportStep("administrator_password", "create_mode", "source_location", "connection_strings.0.value"), }) } diff --git a/website/docs/r/mongo_cluster.html.markdown b/website/docs/r/mongo_cluster.html.markdown index 2b3c18a1f209..094d25ed63e0 100644 --- a/website/docs/r/mongo_cluster.html.markdown +++ b/website/docs/r/mongo_cluster.html.markdown @@ -121,7 +121,7 @@ A `connection_strings` exports the following: * `description` - The description of the connection string. -* `connection_string` - The Mongo Cluster connection string. The `:` place holder returned from API will be replaced by the real `administrator_username` and `administrator_password` if available in the state. +* `value` - The value of the Mongo Cluster connection string. The `:` placeholder returned from API will be replaced by the real `administrator_username` and `administrator_password` if available in the state. ## Timeouts