Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions internal/services/cosmos/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/hashicorp/go-azure-sdk/resource-manager/cosmosdb/2022-05-15/sqldedicatedgateway"
"github.com/hashicorp/go-azure-sdk/resource-manager/cosmosdb/2023-04-15/managedcassandras"
"github.com/hashicorp/go-azure-sdk/resource-manager/cosmosdb/2024-08-15/cosmosdb"
"github.com/hashicorp/go-azure-sdk/resource-manager/cosmosdb/2025-10-15/fleets"
"github.com/hashicorp/go-azure-sdk/resource-manager/cosmosdb/2025-10-15/mongorbacs"
"github.com/hashicorp/go-azure-sdk/resource-manager/postgresqlhsc/2022-11-08/clusters"
"github.com/hashicorp/go-azure-sdk/resource-manager/postgresqlhsc/2022-11-08/configurations"
Expand All @@ -23,6 +24,7 @@ type Client struct {
ConfigurationsClient *configurations.ConfigurationsClient
CosmosDBClient *cosmosdb.CosmosDBClient
FirewallRulesClient *firewallrules.FirewallRulesClient
FleetsClient *fleets.FleetsClient
ManagedCassandraClient *managedcassandras.ManagedCassandrasClient
MongoRBACClient *mongorbacs.MongorbacsClient
RolesClient *roles.RolesClient
Expand Down Expand Up @@ -71,6 +73,12 @@ func NewClient(o *common.ClientOptions) (*Client, error) {
}
o.Configure(firewallRulesClient.Client, o.Authorizers.ResourceManager)

fleetsClient, err := fleets.NewFleetsClientWithBaseURI(o.Environment.ResourceManager)
if err != nil {
return nil, fmt.Errorf("building Fleets Client: %+v", err)
}
o.Configure(fleetsClient.Client, o.Authorizers.ResourceManager)

mongorbacsClient, err := mongorbacs.NewMongorbacsClientWithBaseURI(o.Environment.ResourceManager)
if err != nil {
return nil, fmt.Errorf("building Mongorbacs client: %+v", err)
Expand Down Expand Up @@ -123,6 +131,7 @@ func NewClient(o *common.ClientOptions) (*Client, error) {
ConfigurationsClient: configurationsClient,
CosmosDBClient: cosmosdbClient,
FirewallRulesClient: firewallRulesClient,
FleetsClient: fleetsClient,
MongoRBACClient: mongorbacsClient,
RolesClient: rolesClient,
SqlDedicatedGatewayClient: sqlDedicatedGatewayClient,
Expand Down
177 changes: 177 additions & 0 deletions internal/services/cosmos/cosmosdb_fleet_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright IBM Corp. 2014, 2025
// SPDX-License-Identifier: MPL-2.0

package cosmos

import (
"context"
"fmt"
"time"

"github.com/hashicorp/go-azure-helpers/lang/pointer"
"github.com/hashicorp/go-azure-helpers/lang/response"
"github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema"
"github.com/hashicorp/go-azure-helpers/resourcemanager/location"
"github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids"
"github.com/hashicorp/go-azure-helpers/resourcemanager/tags"
"github.com/hashicorp/go-azure-sdk/resource-manager/cosmosdb/2025-10-15/fleets"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-provider-azurerm/internal/sdk"
"github.com/hashicorp/terraform-provider-azurerm/internal/services/cosmos/validate"
"github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk"
)

//go:generate go run ../../tools/generator-tests resourceidentity -resource-name cosmosdb_fleet -service-package-name cosmos -properties "name,resource_group_name" -known-values "subscription_id:data.Subscriptions.Primary"

type CosmosDbFleetResource struct{}

var _ sdk.ResourceWithIdentity = CosmosDbFleetResource{}

type CosmosDbFleetModel struct {
Name string `tfschema:"name"`
ResourceGroupName string `tfschema:"resource_group_name"`
Location string `tfschema:"location"`
Tags map[string]string `tfschema:"tags"`
}

func (CosmosDbFleetResource) Arguments() map[string]*pluginsdk.Schema {
return map[string]*pluginsdk.Schema{
"name": {
Type: pluginsdk.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validate.FleetName,
},

"resource_group_name": commonschema.ResourceGroupName(),

"location": commonschema.Location(),

"tags": {
Type: schema.TypeMap,
Optional: true,
// `ForceNew` behavior is added as `tags` property is absent in update model
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Create" SDK method uses PUT HTTP method, can you please check if we can use this for update?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By applying a typical Update method and test which involves only change in Tags property, an error occurs mentioning that it is unable to create a resource with same ID. Following are the REST API request and response.

Request body before update

{
  "location": "southeastasia",
  "tags": null
}

Request body during update

{
  "id": "/subscriptions/REDACTED/resourceGroups/REDACTED/providers/Microsoft.DocumentDB/fleets/REDACTED",
  "location": "Southeast Asia",
  "name": "REDACTED",
  "properties": {
    "provisioningState": "Succeeded"
  },
  "tags": {
    "env": "test"
  },
  "type": "Microsoft.DocumentDB/fleets"
}

Error from response body during update

{
  "code": "BadRequest",
  "message": "Exception occurred trying to create Fleet with Name: REDACTED and SubscriptionID: REDACTED, Message: Entity with the specified id already exists in the system.\r\nActivityId: REDACTED, Microsoft.Azure.Documents.Common/2.14.0, Microsoft.Azure.Documents.Common/2.14.0"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that is not useful @sesmyrnov , the only updatable property (tags) is not supported by that PATCH API.

ForceNew: true,
ValidateFunc: tags.Validate,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
}
}

func (CosmosDbFleetResource) Attributes() map[string]*pluginsdk.Schema {
return map[string]*pluginsdk.Schema{}
}

func (CosmosDbFleetResource) ModelObject() interface{} {
return &CosmosDbFleetModel{}
}

func (CosmosDbFleetResource) ResourceType() string {
return "azurerm_cosmosdb_fleet"
}

func (r CosmosDbFleetResource) Create() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 30 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.Cosmos.FleetsClient
subscriptionId := metadata.Client.Account.SubscriptionId

var config CosmosDbFleetModel
if err := metadata.Decode(&config); err != nil {
return fmt.Errorf("decoding: %+v", err)
}
id := fleets.NewFleetID(subscriptionId, config.ResourceGroupName, config.Name)

existing, err := client.FleetGet(ctx, id)
if err != nil && !response.WasNotFound(existing.HttpResponse) {
return fmt.Errorf("checking for presence of existing %s: %+v", id, err)
}
if !response.WasNotFound(existing.HttpResponse) {
return metadata.ResourceRequiresImport(r.ResourceType(), id)
}

param := fleets.FleetResource{
Location: location.Normalize(config.Location),
Tags: pointer.To(config.Tags),
}
if _, err := client.FleetCreate(ctx, id, param); err != nil {
return fmt.Errorf("creating %s: %+v", id, err)
}

metadata.SetID(id)
if err := pluginsdk.SetResourceIdentityData(metadata.ResourceData, &id); err != nil {
return err
}

return nil
},
}
}

func (r CosmosDbFleetResource) Read() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 5 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.Cosmos.FleetsClient
id, err := fleets.ParseFleetID(metadata.ResourceData.Id())
if err != nil {
return err
}

resp, err := client.FleetGet(ctx, *id)
if err != nil {
if response.WasNotFound(resp.HttpResponse) {
return metadata.MarkAsGone(id)
}

return fmt.Errorf("retrieving %s: %+v", id, err)
}

state := CosmosDbFleetModel{
Name: id.FleetName,
ResourceGroupName: id.ResourceGroupName,
}

if model := resp.Model; model != nil {
state.Location = location.NormalizeNilable(&model.Location)
state.Tags = pointer.From(model.Tags)
}

if err := pluginsdk.SetResourceIdentityData(metadata.ResourceData, id); err != nil {
return err
}

return metadata.Encode(&state)
},
}
}

func (CosmosDbFleetResource) Delete() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 30 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.Cosmos.FleetsClient

id, err := fleets.ParseFleetID(metadata.ResourceData.Id())
if err != nil {
return err
}

if err := client.FleetDeleteThenPoll(ctx, *id); err != nil {
return fmt.Errorf("deleting %s: %+v", *id, err)
}
return nil
},
}
}

func (CosmosDbFleetResource) IDValidationFunc() pluginsdk.SchemaValidateFunc {
return fleets.ValidateFleetID
}

func (CosmosDbFleetResource) Identity() resourceids.ResourceId {
return &fleets.FleetId{}
}
131 changes: 131 additions & 0 deletions internal/services/cosmos/cosmosdb_fleet_resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright IBM Corp. 2014, 2025
// SPDX-License-Identifier: MPL-2.0

package cosmos_test

import (
"context"
"fmt"
"testing"

"github.com/hashicorp/go-azure-helpers/lang/pointer"
"github.com/hashicorp/go-azure-sdk/resource-manager/cosmosdb/2025-10-15/fleets"
"github.com/hashicorp/terraform-provider-azurerm/internal/acceptance"
"github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check"
"github.com/hashicorp/terraform-provider-azurerm/internal/clients"
"github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk"
)

type CosmosDbFleetResource struct{}

func TestAccCosmosDbFleet_basic(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_cosmosdb_fleet", "test")
r := CosmosDbFleetResource{}

data.ResourceTest(t, r, []acceptance.TestStep{
{
Config: r.basic(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
data.ImportStep(),
})
}

func TestAccCosmosDbFleet_requiresImport(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_cosmosdb_fleet", "test")
r := CosmosDbFleetResource{}

data.ResourceTest(t, r, []acceptance.TestStep{
{
Config: r.basic(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
data.RequiresImportErrorStep(r.requiresImport),
})
}

func TestAccCosmosDbFleet_complete(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_cosmosdb_fleet", "test")
r := CosmosDbFleetResource{}

data.ResourceTest(t, r, []acceptance.TestStep{
{
Config: r.complete(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
data.ImportStep(),
})
}

func (CosmosDbFleetResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) {
id, err := fleets.ParseFleetID(state.ID)
if err != nil {
return nil, err
}

resp, err := client.Cosmos.FleetsClient.FleetGet(ctx, *id)
if err != nil {
return nil, fmt.Errorf("retrieving %s: %+v", *id, err)
}

return pointer.To(resp.Model != nil), nil
}

func (CosmosDbFleetResource) template(data acceptance.TestData) string {
return fmt.Sprintf(`
provider "azurerm" {
features {}
}

resource "azurerm_resource_group" "test" {
name = "acctest-cosmos-%d"
location = "%s"
}
`, data.RandomInteger, data.Locations.Primary)
}

func (r CosmosDbFleetResource) basic(data acceptance.TestData) string {
return fmt.Sprintf(`
%s

resource "azurerm_cosmosdb_fleet" "test" {
name = "acctest-cosfleet-%d"
resource_group_name = azurerm_resource_group.test.name
location = azurerm_resource_group.test.location
}
`, r.template(data), data.RandomInteger)
}

func (r CosmosDbFleetResource) requiresImport(data acceptance.TestData) string {
return fmt.Sprintf(`
%s

resource "azurerm_cosmosdb_fleet" "import" {
name = azurerm_cosmosdb_fleet.test.name
resource_group_name = azurerm_cosmosdb_fleet.test.resource_group_name
location = azurerm_cosmosdb_fleet.test.location
}
`, r.basic(data))
}

func (r CosmosDbFleetResource) complete(data acceptance.TestData) string {
return fmt.Sprintf(`
%s

resource "azurerm_cosmosdb_fleet" "test" {
name = "acctest-cosfleet-%d"
resource_group_name = azurerm_resource_group.test.name
location = azurerm_resource_group.test.location

tags = {
env = "test"
}
}
`, r.template(data), data.RandomInteger)
}
3 changes: 2 additions & 1 deletion internal/services/cosmos/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ func (r Registration) DataSources() []sdk.DataSource {

func (r Registration) Resources() []sdk.Resource {
return []sdk.Resource{
CosmosDbFleetResource{},
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has to be sorted alphabetically. Please run azurerm-linter and check violations.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

CosmosDbMongoRoleDefinitionResource{},
CosmosDbMongoUserDefinitionResource{},
CosmosDbPostgreSQLClusterResource{},
CosmosDbPostgreSQLCoordinatorConfigurationResource{},
CosmosDbPostgreSQLFirewallRuleResource{},
CosmosDbPostgreSQLNodeConfigurationResource{},
CosmosDbPostgreSQLRoleResource{},
CosmosDbSqlDedicatedGatewayResource{},
CosmosDbMongoRoleDefinitionResource{},
}
}

Expand Down
23 changes: 23 additions & 0 deletions internal/services/cosmos/validate/fleet_name.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright IBM Corp. 2014, 2025
// SPDX-License-Identifier: MPL-2.0

package validate

import (
"fmt"
"regexp"
)

// Validate according to https://learn.microsoft.com/en-us/rest/api/cosmos-db-resource-provider/fleet/get?view=rest-cosmos-db-resource-provider-2025-10-15&tabs=HTTP
func FleetName(v interface{}, k string) (warnings []string, errors []error) {
name := v.(string)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put the link as a comment, so other people will know where those restrictions are from.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

if len(name) < 3 || len(name) > 50 {
errors = append(errors, fmt.Errorf("length of %q must be between 3 to 50 (inclusive)", k))
}

if !regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`).MatchString(name) {
errors = append(errors, fmt.Errorf("%q must consist of lower case letters, digits, and hyphens. The first and last character must be a letter or digit", k))
}

return warnings, errors
}
Loading
Loading