From 6de1996c3c61e165e4a8938e788b004513cec80c Mon Sep 17 00:00:00 2001 From: Yichun Ma Date: Fri, 18 Aug 2023 17:02:11 +0800 Subject: [PATCH 1/2] New resource `azurerm_iothub_endpoint_cosmosdb_account` --- ...thub_endpoint_cosmosdb_account_resource.go | 470 ++++++++++++++++++ ...endpoint_cosmosdb_account_resource_test.go | 412 +++++++++++++++ .../parse/endpoint_cosmos_db_account.go | 134 +++++ .../parse/endpoint_cosmos_db_account_test.go | 267 ++++++++++ internal/services/iothub/registration.go | 1 + internal/services/iothub/resourceids.go | 1 + .../validate/endpoint_cosmos_db_account_id.go | 26 + .../endpoint_cosmos_db_account_id_test.go | 91 ++++ ...ub_endpoint_cosmosdb_account.html.markdown | 136 +++++ 9 files changed, 1538 insertions(+) create mode 100644 internal/services/iothub/iothub_endpoint_cosmosdb_account_resource.go create mode 100644 internal/services/iothub/iothub_endpoint_cosmosdb_account_resource_test.go create mode 100644 internal/services/iothub/parse/endpoint_cosmos_db_account.go create mode 100644 internal/services/iothub/parse/endpoint_cosmos_db_account_test.go create mode 100644 internal/services/iothub/validate/endpoint_cosmos_db_account_id.go create mode 100644 internal/services/iothub/validate/endpoint_cosmos_db_account_id_test.go create mode 100644 website/docs/r/iothub_endpoint_cosmosdb_account.html.markdown diff --git a/internal/services/iothub/iothub_endpoint_cosmosdb_account_resource.go b/internal/services/iothub/iothub_endpoint_cosmosdb_account_resource.go new file mode 100644 index 000000000000..a7f6fbfad1cb --- /dev/null +++ b/internal/services/iothub/iothub_endpoint_cosmosdb_account_resource.go @@ -0,0 +1,470 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package iothub + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema" + "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" + "github.com/hashicorp/terraform-provider-azurerm/internal/locks" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/iothub/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/iothub/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" + "github.com/hashicorp/terraform-provider-azurerm/utils" + devices "github.com/tombuildsstuff/kermit/sdk/iothub/2022-04-30-preview/iothub" +) + +type IotHubEndpointCosmosDBAccountResource struct{} + +var ( + _ sdk.ResourceWithUpdate = IotHubEndpointCosmosDBAccountResource{} +) + +type IotHubEndpointCosmosDBAccountModel struct { + Name string `tfschema:"name"` + ResourceGroupName string `tfschema:"resource_group_name"` + AuthenticationType string `tfschema:"authentication_type"` + ContainerName string `tfschema:"container_name"` + DatabaseName string `tfschema:"database_name"` + EndpointUri string `tfschema:"endpoint_uri"` + IdentityId string `tfschema:"identity_id"` + IothubId string `tfschema:"iothub_id"` + PartitionKeyName string `tfschema:"partition_key_name"` + PartitionKeyTemplate string `tfschema:"partition_key_template"` + PrimaryKey string `tfschema:"primary_key"` + SecondaryKey string `tfschema:"secondary_key"` +} + +func (r IotHubEndpointCosmosDBAccountResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.IoTHubEndpointName, + }, + + "resource_group_name": commonschema.ResourceGroupName(), + + "iothub_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.IotHubID, + }, + + "container_name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "database_name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "endpoint_uri": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "authentication_type": { + Type: pluginsdk.TypeString, + Optional: true, + Default: string(devices.AuthenticationTypeKeyBased), + ValidateFunc: validation.StringInSlice([]string{ + string(devices.AuthenticationTypeKeyBased), + string(devices.AuthenticationTypeIdentityBased), + }, false), + }, + + "identity_id": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: commonids.ValidateUserAssignedIdentityID, + ConflictsWith: []string{"primary_key", "secondary_key"}, + }, + + "partition_key_name": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + RequiredWith: []string{"partition_key_template"}, + }, + + "partition_key_template": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + RequiredWith: []string{"partition_key_name"}, + }, + + "primary_key": { + Type: pluginsdk.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validation.StringIsNotEmpty, + ConflictsWith: []string{"identity_id"}, + RequiredWith: []string{"secondary_key"}, + }, + + "secondary_key": { + Type: pluginsdk.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validation.StringIsNotEmpty, + ConflictsWith: []string{"identity_id"}, + RequiredWith: []string{"primary_key"}, + }, + } +} + +func (r IotHubEndpointCosmosDBAccountResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{} +} + +func (r IotHubEndpointCosmosDBAccountResource) ResourceType() string { + return "azurerm_iothub_endpoint_cosmosdb_account" +} + +func (r IotHubEndpointCosmosDBAccountResource) ModelObject() interface{} { + return &IotHubEndpointCosmosDBAccountResource{} +} + +func (r IotHubEndpointCosmosDBAccountResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return validate.EndpointCosmosDBAccountID +} + +func (r IotHubEndpointCosmosDBAccountResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + var state IotHubEndpointCosmosDBAccountModel + if err := metadata.Decode(&state); err != nil { + return err + } + + client := metadata.Client.IoTHub.ResourceClient + subscriptionId := metadata.Client.Account.SubscriptionId + + iotHubId, err := parse.IotHubID(state.IothubId) + if err != nil { + return err + } + + id := parse.NewEndpointCosmosDBAccountID(subscriptionId, iotHubId.ResourceGroup, iotHubId.Name, state.Name) + + locks.ByName(iotHubId.Name, IothubResourceName) + defer locks.UnlockByName(iotHubId.Name, IothubResourceName) + + iothub, err := client.Get(ctx, iotHubId.ResourceGroup, iotHubId.Name) + if err != nil { + if utils.ResponseWasNotFound(iothub.Response) { + return fmt.Errorf("%q was not found", iotHubId) + } + + return fmt.Errorf("retrieving %q: %+v", iotHubId, err) + } + + authenticationType := devices.AuthenticationType(state.AuthenticationType) + cosmosDBAccountEndpoint := devices.RoutingCosmosDBSQLAPIProperties{ + Name: pointer.To(id.EndpointName), + SubscriptionID: pointer.To(subscriptionId), + ResourceGroup: pointer.To(state.ResourceGroupName), + AuthenticationType: authenticationType, + CollectionName: pointer.To(state.ContainerName), + DatabaseName: pointer.To(state.DatabaseName), + EndpointURI: pointer.To(state.EndpointUri), + } + + if state.PartitionKeyName != "" { + cosmosDBAccountEndpoint.PartitionKeyName = pointer.To(state.PartitionKeyName) + } + + if state.PartitionKeyTemplate != "" { + cosmosDBAccountEndpoint.PartitionKeyTemplate = pointer.To(state.PartitionKeyTemplate) + } + + if authenticationType == devices.AuthenticationTypeKeyBased { + if state.PrimaryKey == "" || state.SecondaryKey == "" { + return fmt.Errorf("`primary_key` and `secondary_key` must be specified when `authentication_type` is `keyBased`") + } + cosmosDBAccountEndpoint.PrimaryKey = pointer.To(state.PrimaryKey) + cosmosDBAccountEndpoint.SecondaryKey = pointer.To(state.PrimaryKey) + } else { + if state.PrimaryKey != "" || state.SecondaryKey != "" { + return fmt.Errorf("`primary_key` or `secondary_key` cannot be specified when `authentication_type` is `identityBased`") + } + + if state.IdentityId != "" { + cosmosDBAccountEndpoint.Identity = &devices.ManagedIdentity{ + UserAssignedIdentity: pointer.To(state.IdentityId), + } + } + } + + routing := iothub.Properties.Routing + if routing == nil { + routing = &devices.RoutingProperties{} + } + + if routing.Endpoints == nil { + routing.Endpoints = &devices.RoutingEndpoints{} + } + + if routing.Endpoints.CosmosDBSQLCollections == nil { + cosmosDBAccounts := make([]devices.RoutingCosmosDBSQLAPIProperties, 0) + routing.Endpoints.CosmosDBSQLCollections = &cosmosDBAccounts + } + + endpoints := make([]devices.RoutingCosmosDBSQLAPIProperties, 0) + + for _, existingEndpoint := range pointer.From(routing.Endpoints.CosmosDBSQLCollections) { + if strings.EqualFold(pointer.From(existingEndpoint.Name), id.EndpointName) { + return tf.ImportAsExistsError(r.ResourceType(), id.ID()) + } + endpoints = append(endpoints, existingEndpoint) + } + + endpoints = append(endpoints, cosmosDBAccountEndpoint) + routing.Endpoints.CosmosDBSQLCollections = &endpoints + + future, err := client.CreateOrUpdate(ctx, iotHubId.ResourceGroup, iotHubId.Name, iothub, "") + if err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for the completion of the creation of %s: %+v", id, err) + } + + metadata.SetID(id) + return nil + }, + Timeout: 30 * time.Minute, + } +} + +func (r IotHubEndpointCosmosDBAccountResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.IoTHub.ResourceClient + id, err := parse.EndpointCosmosDBAccountID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + var oldState IotHubEndpointCosmosDBAccountModel + if err = metadata.Decode(&oldState); err != nil { + return err + } + + iothub, err := client.Get(ctx, id.ResourceGroup, id.IotHubName) + if err != nil { + if utils.ResponseWasNotFound(iothub.Response) { + return metadata.MarkAsGone(id) + } + return fmt.Errorf("retrieving %q: %+v", id, err) + } + + if iothub.Properties == nil || iothub.Properties.Routing == nil || iothub.Properties.Routing.Endpoints == nil || iothub.Properties.Routing.Endpoints.CosmosDBSQLCollections == nil { + return metadata.MarkAsGone(id) + } + + for _, endpoint := range pointer.From(iothub.Properties.Routing.Endpoints.CosmosDBSQLCollections) { + if strings.EqualFold(pointer.From(endpoint.Name), id.EndpointName) { + state := &IotHubEndpointCosmosDBAccountModel{ + Name: id.EndpointName, + ResourceGroupName: pointer.From(endpoint.ResourceGroup), + IothubId: parse.NewIotHubID(id.SubscriptionId, id.ResourceGroup, id.IotHubName).ID(), + ContainerName: pointer.From(endpoint.CollectionName), + DatabaseName: pointer.From(endpoint.DatabaseName), + EndpointUri: pointer.From(endpoint.EndpointURI), + PartitionKeyName: pointer.From(endpoint.PartitionKeyName), + PartitionKeyTemplate: pointer.From(endpoint.PartitionKeyTemplate), + PrimaryKey: oldState.PrimaryKey, + SecondaryKey: oldState.SecondaryKey, + } + + authenticationType := string(devices.AuthenticationTypeKeyBased) + if string(endpoint.AuthenticationType) != "" { + authenticationType = string(endpoint.AuthenticationType) + } + state.AuthenticationType = authenticationType + + identityId := "" + if endpoint.Identity != nil && endpoint.Identity.UserAssignedIdentity != nil { + identityId = pointer.From(endpoint.Identity.UserAssignedIdentity) + } + state.IdentityId = identityId + + return metadata.Encode(state) + } + } + + return metadata.MarkAsGone(id) + }, + Timeout: 5 * time.Minute, + } +} + +func (r IotHubEndpointCosmosDBAccountResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.IoTHub.ResourceClient + + id, err := parse.EndpointCosmosDBAccountID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + locks.ByName(id.IotHubName, IothubResourceName) + defer locks.UnlockByName(id.IotHubName, IothubResourceName) + + var state IotHubEndpointCosmosDBAccountModel + if err = metadata.Decode(&state); err != nil { + return err + } + + iothub, err := client.Get(ctx, id.ResourceGroup, id.IotHubName) + if err != nil { + if utils.ResponseWasNotFound(iothub.Response) { + return fmt.Errorf("%q was not found", id) + } + + return fmt.Errorf("retrieving %q: %+v", id, err) + } + + if iothub.Properties == nil || iothub.Properties.Routing == nil || iothub.Properties.Routing.Endpoints == nil || iothub.Properties.Routing.Endpoints.CosmosDBSQLCollections == nil { + return fmt.Errorf("%q was not found", id) + } + + for i, endpoint := range pointer.From(iothub.Properties.Routing.Endpoints.CosmosDBSQLCollections) { + if strings.EqualFold(pointer.From(endpoint.Name), id.EndpointName) { + authenticationType := devices.AuthenticationType(state.AuthenticationType) + + if authenticationType == devices.AuthenticationTypeKeyBased { + if state.PrimaryKey == "" || state.SecondaryKey == "" { + return fmt.Errorf("`primary_key` and `secondary_key` must be specified when `authentication_type` is `keyBased`") + } + + endpoint.PrimaryKey = pointer.To(state.PrimaryKey) + endpoint.SecondaryKey = pointer.To(state.PrimaryKey) + endpoint.Identity = nil + } else { + if state.PrimaryKey != "" || state.SecondaryKey != "" { + return fmt.Errorf("`primary_key` or `secondary_key` cannot be specified when `authentication_type` is `identityBased`") + } + + endpoint.PrimaryKey = nil + endpoint.SecondaryKey = nil + if state.IdentityId != "" { + endpoint.Identity = &devices.ManagedIdentity{ + UserAssignedIdentity: pointer.To(state.IdentityId), + } + } else { + endpoint.Identity = nil + } + } + + if metadata.ResourceData.HasChange("authentication_type") { + endpoint.AuthenticationType = authenticationType + } + + if metadata.ResourceData.HasChange("partition_key_name") { + if state.PartitionKeyName == "" { + endpoint.PartitionKeyName = nil + } else { + endpoint.PartitionKeyName = pointer.To(state.PartitionKeyName) + } + } + + if metadata.ResourceData.HasChange("partition_key_template") { + if state.PartitionKeyTemplate == "" { + endpoint.PartitionKeyTemplate = nil + } else { + endpoint.PartitionKeyTemplate = pointer.To(state.PartitionKeyTemplate) + } + } + + (*iothub.Properties.Routing.Endpoints.CosmosDBSQLCollections)[i] = endpoint + + future, err := client.CreateOrUpdate(ctx, id.ResourceGroup, id.IotHubName, iothub, "") + if err != nil { + return fmt.Errorf("updating %s: %+v", id, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for the completion of the update of %s: %+v", id, err) + } + + return nil + } + } + + return fmt.Errorf("%q was not found", id) + }, + Timeout: 30 * time.Minute, + } +} + +func (r IotHubEndpointCosmosDBAccountResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.IoTHub.ResourceClient + id, err := parse.EndpointCosmosDBAccountID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + locks.ByName(id.IotHubName, IothubResourceName) + defer locks.UnlockByName(id.IotHubName, IothubResourceName) + + iothub, err := client.Get(ctx, id.ResourceGroup, id.IotHubName) + if err != nil { + if utils.ResponseWasNotFound(iothub.Response) { + return nil + } + return fmt.Errorf("retrieving %q: %+v", id, err) + } + + if iothub.Properties == nil || iothub.Properties.Routing == nil || iothub.Properties.Routing.Endpoints == nil || iothub.Properties.Routing.Endpoints.CosmosDBSQLCollections == nil { + return nil + } + + updatedEndpoints := make([]devices.RoutingCosmosDBSQLAPIProperties, 0) + for _, endpoint := range pointer.From(iothub.Properties.Routing.Endpoints.CosmosDBSQLCollections) { + if !strings.EqualFold(pointer.From(endpoint.Name), id.EndpointName) { + updatedEndpoints = append(updatedEndpoints, endpoint) + } + } + iothub.Properties.Routing.Endpoints.CosmosDBSQLCollections = &updatedEndpoints + + future, err := client.CreateOrUpdate(ctx, id.ResourceGroup, id.IotHubName, iothub, "") + if err != nil { + return fmt.Errorf("deleting %s: %+v", id, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for the deletion of %s: %+v", id, err) + } + + return nil + }, + Timeout: 30 * time.Minute, + } +} diff --git a/internal/services/iothub/iothub_endpoint_cosmosdb_account_resource_test.go b/internal/services/iothub/iothub_endpoint_cosmosdb_account_resource_test.go new file mode 100644 index 000000000000..31bc925c0f6b --- /dev/null +++ b/internal/services/iothub/iothub_endpoint_cosmosdb_account_resource_test.go @@ -0,0 +1,412 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package iothub_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "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/services/iothub/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +type IotHubEndpointCosmosDBAccountResource struct{} + +func TestAccIotHubEndpointCosmosDBAccount_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub_endpoint_cosmosdb_account", "test") + r := IotHubEndpointCosmosDBAccountResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("primary_key", "secondary_key"), + }) +} + +func TestAccIotHubEndpointCosmosDBAccount_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub_endpoint_cosmosdb_account", "test") + r := IotHubEndpointCosmosDBAccountResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + { + Config: r.requiresImport(data), + ExpectError: acceptance.RequiresImportError("azurerm_iothub_endpoint_cosmosdb_account"), + }, + }) +} + +func TestAccIotHubEndpointCosmosDBAccount_authenticationTypeSystemAssignedIdentity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub_endpoint_cosmosdb_account", "test") + r := IotHubEndpointCosmosDBAccountResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.authenticationTypeSystemAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccIotHubEndpointCosmosDBAccount_authenticationTypeUserAssignedIdentity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub_endpoint_cosmosdb_account", "test") + r := IotHubEndpointCosmosDBAccountResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.authenticationTypeUserAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccIotHubEndpointCosmosDBAccount_authenticationTypeUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub_endpoint_cosmosdb_account", "test") + r := IotHubEndpointCosmosDBAccountResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.authenticationTypeDefault(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("primary_key", "secondary_key"), + { + Config: r.authenticationTypeUserAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.authenticationTypeSystemAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.authenticationTypeDefault(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("primary_key", "secondary_key"), + }) +} + +func TestAccIotHubEndpointCosmosDBAccount_partitionKey(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub_endpoint_cosmosdb_account", "test") + r := IotHubEndpointCosmosDBAccountResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basicWithPartitionKey(data, "keyName1", "{deviceid}-{YYYY}-{MM}"), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("primary_key", "secondary_key"), + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("primary_key", "secondary_key"), + { + Config: r.basicWithPartitionKey(data, "keyName2", "{deviceid}-{MM}-{YYYY}"), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("primary_key", "secondary_key"), + }) +} + +func (r IotHubEndpointCosmosDBAccountResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_iothub_endpoint_cosmosdb_account" "test" { + name = "acctest" + resource_group_name = azurerm_resource_group.endpoint.name + iothub_id = azurerm_iothub.test.id + container_name = azurerm_cosmosdb_sql_container.test.name + database_name = azurerm_cosmosdb_sql_database.test.name + endpoint_uri = azurerm_cosmosdb_account.test.endpoint + primary_key = azurerm_cosmosdb_account.test.primary_key + secondary_key = azurerm_cosmosdb_account.test.secondary_key +} +`, r.template(data)) +} + +func (r IotHubEndpointCosmosDBAccountResource) requiresImport(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_iothub_endpoint_cosmosdb_account" "import" { + name = azurerm_iothub_endpoint_cosmosdb_account.test.name + resource_group_name = azurerm_iothub_endpoint_cosmosdb_account.test.resource_group_name + iothub_id = azurerm_iothub_endpoint_cosmosdb_account.test.iothub_id + container_name = azurerm_iothub_endpoint_cosmosdb_account.test.container_name + database_name = azurerm_iothub_endpoint_cosmosdb_account.test.database_name + endpoint_uri = azurerm_iothub_endpoint_cosmosdb_account.test.endpoint_uri + primary_key = azurerm_iothub_endpoint_cosmosdb_account.test.primary_key + secondary_key = azurerm_iothub_endpoint_cosmosdb_account.test.secondary_key +} +`, r.basic(data)) +} + +func (r IotHubEndpointCosmosDBAccountResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := parse.EndpointCosmosDBAccountID(state.ID) + if err != nil { + return nil, err + } + + iothub, err := clients.IoTHub.ResourceClient.Get(ctx, id.ResourceGroup, id.IotHubName) + if err != nil || iothub.Properties == nil || iothub.Properties.Routing == nil || iothub.Properties.Routing.Endpoints == nil { + return nil, fmt.Errorf("reading %s: %+v", *id, err) + } + + if endpoints := iothub.Properties.Routing.Endpoints.CosmosDBSQLCollections; endpoints != nil { + for _, endpoint := range pointer.From(endpoints) { + if strings.EqualFold(pointer.From(endpoint.Name), id.EndpointName) { + return pointer.To(true), nil + } + } + } + + return pointer.To(false), nil +} + +func (r IotHubEndpointCosmosDBAccountResource) authenticationTypeDefault(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_iothub_endpoint_cosmosdb_account" "test" { + name = "acctest" + resource_group_name = azurerm_resource_group.endpoint.name + iothub_id = azurerm_iothub.test.id + container_name = azurerm_cosmosdb_sql_container.test.name + database_name = azurerm_cosmosdb_sql_database.test.name + endpoint_uri = azurerm_cosmosdb_account.test.endpoint + + primary_key = azurerm_cosmosdb_account.test.primary_key + secondary_key = azurerm_cosmosdb_account.test.secondary_key +} +`, r.authenticationTemplate(data)) +} + +func (r IotHubEndpointCosmosDBAccountResource) authenticationTypeSystemAssignedIdentity(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_iothub_endpoint_cosmosdb_account" "test" { + name = "acctest" + resource_group_name = azurerm_resource_group.endpoint.name + iothub_id = azurerm_iothub.test.id + container_name = azurerm_cosmosdb_sql_container.test.name + database_name = azurerm_cosmosdb_sql_database.test.name + endpoint_uri = azurerm_cosmosdb_account.test.endpoint + + authentication_type = "identityBased" + + depends_on = [ + azurerm_cosmosdb_sql_role_assignment.system, + ] +} +`, r.authenticationTemplate(data)) +} + +func (r IotHubEndpointCosmosDBAccountResource) authenticationTypeUserAssignedIdentity(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_iothub_endpoint_cosmosdb_account" "test" { + name = "acctest" + resource_group_name = azurerm_resource_group.endpoint.name + iothub_id = azurerm_iothub.test.id + container_name = azurerm_cosmosdb_sql_container.test.name + database_name = azurerm_cosmosdb_sql_database.test.name + endpoint_uri = azurerm_cosmosdb_account.test.endpoint + + authentication_type = "identityBased" + identity_id = azurerm_user_assigned_identity.test.id + + depends_on = [ + azurerm_cosmosdb_sql_role_assignment.user, + ] +} +`, r.authenticationTemplate(data)) +} + +func (r IotHubEndpointCosmosDBAccountResource) authenticationTemplate(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +resource "azurerm_user_assigned_identity" "test" { + name = "acctestuai-%[2]d" + resource_group_name = azurerm_resource_group.iothub.name + location = azurerm_resource_group.iothub.location +} + +resource "azurerm_iothub" "test" { + name = "acctestIoTHub-%[2]d" + resource_group_name = azurerm_resource_group.iothub.name + location = azurerm_resource_group.iothub.location + + sku { + name = "B1" + capacity = "1" + } + + identity { + type = "SystemAssigned, UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id, + ] + } +} + +resource "azurerm_cosmosdb_sql_role_definition" "test" { + name = "acctestsqlrole%[3]s" + resource_group_name = azurerm_resource_group.endpoint.name + account_name = azurerm_cosmosdb_account.test.name + assignable_scopes = [ + azurerm_cosmosdb_account.test.id, + ] + + permissions { + data_actions = [ + "Microsoft.DocumentDB/databaseAccounts/readMetadata", + ] + } +} + +resource "azurerm_cosmosdb_sql_role_assignment" "system" { + resource_group_name = azurerm_resource_group.endpoint.name + account_name = azurerm_cosmosdb_account.test.name + role_definition_id = azurerm_cosmosdb_sql_role_definition.test.id + principal_id = azurerm_iothub.test.identity[0].principal_id + scope = azurerm_cosmosdb_account.test.id +} + +resource "azurerm_cosmosdb_sql_role_assignment" "user" { + resource_group_name = azurerm_resource_group.endpoint.name + account_name = azurerm_cosmosdb_account.test.name + role_definition_id = azurerm_cosmosdb_sql_role_definition.test.id + principal_id = azurerm_user_assigned_identity.test.principal_id + scope = azurerm_cosmosdb_account.test.id +} +`, r.dependencies(data), data.RandomInteger, data.RandomString) +} + +func (r IotHubEndpointCosmosDBAccountResource) basicWithPartitionKey(data acceptance.TestData, partitionKeyName string, partitionKeyTemplate string) string { + return fmt.Sprintf(` +%s + +resource "azurerm_iothub_endpoint_cosmosdb_account" "test" { + name = "acctest" + resource_group_name = azurerm_resource_group.endpoint.name + iothub_id = azurerm_iothub.test.id + container_name = azurerm_cosmosdb_sql_container.test.name + database_name = azurerm_cosmosdb_sql_database.test.name + endpoint_uri = azurerm_cosmosdb_account.test.endpoint + primary_key = azurerm_cosmosdb_account.test.primary_key + secondary_key = azurerm_cosmosdb_account.test.secondary_key + + partition_key_name = "%s" + partition_key_template = "%s" +} +`, r.template(data), partitionKeyName, partitionKeyTemplate) +} + +func (r IotHubEndpointCosmosDBAccountResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +resource "azurerm_iothub" "test" { + name = "acctestIoTHub-%[2]d" + resource_group_name = azurerm_resource_group.iothub.name + location = azurerm_resource_group.iothub.location + + sku { + name = "B1" + capacity = "1" + } +} +`, r.dependencies(data), data.RandomInteger) +} + +func (IotHubEndpointCosmosDBAccountResource) dependencies(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "iothub" { + name = "acctestRG-iothub-%[2]d" + location = "%[1]s" +} + +resource "azurerm_resource_group" "endpoint" { + name = "acctestRG-iothub-endpoint-%[2]d" + location = "%[1]s" +} + +resource "azurerm_cosmosdb_account" "test" { + name = "acctest-ca-%[2]d" + location = azurerm_resource_group.endpoint.location + resource_group_name = azurerm_resource_group.endpoint.name + offer_type = "Standard" + kind = "GlobalDocumentDB" + + consistency_policy { + consistency_level = "Strong" + } + + geo_location { + location = azurerm_resource_group.endpoint.location + failover_priority = 0 + } +} + +resource "azurerm_cosmosdb_sql_database" "test" { + name = "acctest-%[2]d" + resource_group_name = azurerm_cosmosdb_account.test.resource_group_name + account_name = azurerm_cosmosdb_account.test.name +} + +resource "azurerm_cosmosdb_sql_container" "test" { + name = "acctest-CSQLC-%[2]d" + resource_group_name = azurerm_cosmosdb_account.test.resource_group_name + account_name = azurerm_cosmosdb_account.test.name + database_name = azurerm_cosmosdb_sql_database.test.name + partition_key_path = "/definition/id" +} +`, data.Locations.Primary, data.RandomInteger) +} diff --git a/internal/services/iothub/parse/endpoint_cosmos_db_account.go b/internal/services/iothub/parse/endpoint_cosmos_db_account.go new file mode 100644 index 000000000000..fe7f715295e0 --- /dev/null +++ b/internal/services/iothub/parse/endpoint_cosmos_db_account.go @@ -0,0 +1,134 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +type EndpointCosmosDBAccountId struct { + SubscriptionId string + ResourceGroup string + IotHubName string + EndpointName string +} + +func NewEndpointCosmosDBAccountID(subscriptionId, resourceGroup, iotHubName, endpointName string) EndpointCosmosDBAccountId { + return EndpointCosmosDBAccountId{ + SubscriptionId: subscriptionId, + ResourceGroup: resourceGroup, + IotHubName: iotHubName, + EndpointName: endpointName, + } +} + +func (id EndpointCosmosDBAccountId) String() string { + segments := []string{ + fmt.Sprintf("Endpoint Name %q", id.EndpointName), + fmt.Sprintf("Iot Hub Name %q", id.IotHubName), + fmt.Sprintf("Resource Group %q", id.ResourceGroup), + } + segmentsStr := strings.Join(segments, " / ") + return fmt.Sprintf("%s: (%s)", "Endpoint Cosmos D B Account", segmentsStr) +} + +func (id EndpointCosmosDBAccountId) ID() string { + fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Devices/iotHubs/%s/endpoints/%s" + return fmt.Sprintf(fmtString, id.SubscriptionId, id.ResourceGroup, id.IotHubName, id.EndpointName) +} + +// EndpointCosmosDBAccountID parses a EndpointCosmosDBAccount ID into an EndpointCosmosDBAccountId struct +func EndpointCosmosDBAccountID(input string) (*EndpointCosmosDBAccountId, error) { + id, err := resourceids.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("parsing %q as an EndpointCosmosDBAccount ID: %+v", input, err) + } + + resourceId := EndpointCosmosDBAccountId{ + SubscriptionId: id.SubscriptionID, + ResourceGroup: id.ResourceGroup, + } + + if resourceId.SubscriptionId == "" { + return nil, fmt.Errorf("ID was missing the 'subscriptions' element") + } + + if resourceId.ResourceGroup == "" { + return nil, fmt.Errorf("ID was missing the 'resourceGroups' element") + } + + if resourceId.IotHubName, err = id.PopSegment("iotHubs"); err != nil { + return nil, err + } + if resourceId.EndpointName, err = id.PopSegment("endpoints"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &resourceId, nil +} + +// EndpointCosmosDBAccountIDInsensitively parses an EndpointCosmosDBAccount ID into an EndpointCosmosDBAccountId struct, insensitively +// This should only be used to parse an ID for rewriting, the EndpointCosmosDBAccountID +// method should be used instead for validation etc. +// +// Whilst this may seem strange, this enables Terraform have consistent casing +// which works around issues in Core, whilst handling broken API responses. +func EndpointCosmosDBAccountIDInsensitively(input string) (*EndpointCosmosDBAccountId, error) { + id, err := resourceids.ParseAzureResourceID(input) + if err != nil { + return nil, err + } + + resourceId := EndpointCosmosDBAccountId{ + SubscriptionId: id.SubscriptionID, + ResourceGroup: id.ResourceGroup, + } + + if resourceId.SubscriptionId == "" { + return nil, fmt.Errorf("ID was missing the 'subscriptions' element") + } + + if resourceId.ResourceGroup == "" { + return nil, fmt.Errorf("ID was missing the 'resourceGroups' element") + } + + // find the correct casing for the 'iotHubs' segment + iotHubsKey := "iotHubs" + for key := range id.Path { + if strings.EqualFold(key, iotHubsKey) { + iotHubsKey = key + break + } + } + if resourceId.IotHubName, err = id.PopSegment(iotHubsKey); err != nil { + return nil, err + } + + // find the correct casing for the 'endpoints' segment + endpointsKey := "endpoints" + for key := range id.Path { + if strings.EqualFold(key, endpointsKey) { + endpointsKey = key + break + } + } + if resourceId.EndpointName, err = id.PopSegment(endpointsKey); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &resourceId, nil +} diff --git a/internal/services/iothub/parse/endpoint_cosmos_db_account_test.go b/internal/services/iothub/parse/endpoint_cosmos_db_account_test.go new file mode 100644 index 000000000000..3f20debf3675 --- /dev/null +++ b/internal/services/iothub/parse/endpoint_cosmos_db_account_test.go @@ -0,0 +1,267 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "testing" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +var _ resourceids.Id = EndpointCosmosDBAccountId{} + +func TestEndpointCosmosDBAccountIDFormatter(t *testing.T) { + actual := NewEndpointCosmosDBAccountID("12345678-1234-9876-4563-123456789012", "resGroup1", "hub1", "cosmosDBAccountEndpoint1").ID() + expected := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/hub1/endpoints/cosmosDBAccountEndpoint1" + if actual != expected { + t.Fatalf("Expected %q but got %q", expected, actual) + } +} + +func TestEndpointCosmosDBAccountID(t *testing.T) { + testData := []struct { + Input string + Error bool + Expected *EndpointCosmosDBAccountId + }{ + + { + // empty + Input: "", + Error: true, + }, + + { + // missing SubscriptionId + Input: "/", + Error: true, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Error: true, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Error: true, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Error: true, + }, + + { + // missing IotHubName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/", + Error: true, + }, + + { + // missing value for IotHubName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/", + Error: true, + }, + + { + // missing EndpointName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/hub1/", + Error: true, + }, + + { + // missing value for EndpointName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/hub1/endpoints/", + Error: true, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/hub1/endpoints/cosmosDBAccountEndpoint1", + Expected: &EndpointCosmosDBAccountId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + IotHubName: "hub1", + EndpointName: "cosmosDBAccountEndpoint1", + }, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.DEVICES/IOTHUBS/HUB1/ENDPOINTS/COSMOSDBACCOUNTENDPOINT1", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Input) + + actual, err := EndpointCosmosDBAccountID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expect a value but got an error: %s", err) + } + if v.Error { + t.Fatal("Expect an error but didn't get one") + } + + if actual.SubscriptionId != v.Expected.SubscriptionId { + t.Fatalf("Expected %q but got %q for SubscriptionId", v.Expected.SubscriptionId, actual.SubscriptionId) + } + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for ResourceGroup", v.Expected.ResourceGroup, actual.ResourceGroup) + } + if actual.IotHubName != v.Expected.IotHubName { + t.Fatalf("Expected %q but got %q for IotHubName", v.Expected.IotHubName, actual.IotHubName) + } + if actual.EndpointName != v.Expected.EndpointName { + t.Fatalf("Expected %q but got %q for EndpointName", v.Expected.EndpointName, actual.EndpointName) + } + } +} + +func TestEndpointCosmosDBAccountIDInsensitively(t *testing.T) { + testData := []struct { + Input string + Error bool + Expected *EndpointCosmosDBAccountId + }{ + + { + // empty + Input: "", + Error: true, + }, + + { + // missing SubscriptionId + Input: "/", + Error: true, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Error: true, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Error: true, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Error: true, + }, + + { + // missing IotHubName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/", + Error: true, + }, + + { + // missing value for IotHubName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/", + Error: true, + }, + + { + // missing EndpointName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/hub1/", + Error: true, + }, + + { + // missing value for EndpointName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/hub1/endpoints/", + Error: true, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/hub1/endpoints/cosmosDBAccountEndpoint1", + Expected: &EndpointCosmosDBAccountId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + IotHubName: "hub1", + EndpointName: "cosmosDBAccountEndpoint1", + }, + }, + + { + // lower-cased segment names + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iothubs/hub1/endpoints/cosmosDBAccountEndpoint1", + Expected: &EndpointCosmosDBAccountId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + IotHubName: "hub1", + EndpointName: "cosmosDBAccountEndpoint1", + }, + }, + + { + // upper-cased segment names + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/IOTHUBS/hub1/ENDPOINTS/cosmosDBAccountEndpoint1", + Expected: &EndpointCosmosDBAccountId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + IotHubName: "hub1", + EndpointName: "cosmosDBAccountEndpoint1", + }, + }, + + { + // mixed-cased segment names + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/IoThUbS/hub1/EnDpOiNtS/cosmosDBAccountEndpoint1", + Expected: &EndpointCosmosDBAccountId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + IotHubName: "hub1", + EndpointName: "cosmosDBAccountEndpoint1", + }, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Input) + + actual, err := EndpointCosmosDBAccountIDInsensitively(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expect a value but got an error: %s", err) + } + if v.Error { + t.Fatal("Expect an error but didn't get one") + } + + if actual.SubscriptionId != v.Expected.SubscriptionId { + t.Fatalf("Expected %q but got %q for SubscriptionId", v.Expected.SubscriptionId, actual.SubscriptionId) + } + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for ResourceGroup", v.Expected.ResourceGroup, actual.ResourceGroup) + } + if actual.IotHubName != v.Expected.IotHubName { + t.Fatalf("Expected %q but got %q for IotHubName", v.Expected.IotHubName, actual.IotHubName) + } + if actual.EndpointName != v.Expected.EndpointName { + t.Fatalf("Expected %q but got %q for EndpointName", v.Expected.EndpointName, actual.EndpointName) + } + } +} diff --git a/internal/services/iothub/registration.go b/internal/services/iothub/registration.go index e4173a175b14..4cc375619b38 100644 --- a/internal/services/iothub/registration.go +++ b/internal/services/iothub/registration.go @@ -67,5 +67,6 @@ func (r Registration) Resources() []sdk.Resource { IotHubDeviceUpdateAccountResource{}, IotHubDeviceUpdateInstanceResource{}, IotHubFileUploadResource{}, + IotHubEndpointCosmosDBAccountResource{}, } } diff --git a/internal/services/iothub/resourceids.go b/internal/services/iothub/resourceids.go index 86659b71f7e3..e0853d4d72c6 100644 --- a/internal/services/iothub/resourceids.go +++ b/internal/services/iothub/resourceids.go @@ -9,6 +9,7 @@ package iothub //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=FallbackRoute -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/hub1/fallbackRoute/default -rewrite=true //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=Route -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/hub1/routes/route1 -rewrite=true //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=SharedAccessPolicy -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/hub1/iotHubKeys/sharedAccessPolicy1 -rewrite=true +//go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=EndpointCosmosDBAccount -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/hub1/endpoints/cosmosDBAccountEndpoint1 -rewrite=true //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=EndpointStorageContainer -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/hub1/endpoints/storageContainerEndpoint1 -rewrite=true //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=EndpointServiceBusTopic -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/hub1/endpoints/serviceBusTopicEndpoint1 -rewrite=true //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=EndpointServiceBusQueue -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/hub1/endpoints/serviceBusQueueEndpoint1 -rewrite=true diff --git a/internal/services/iothub/validate/endpoint_cosmos_db_account_id.go b/internal/services/iothub/validate/endpoint_cosmos_db_account_id.go new file mode 100644 index 000000000000..b3e4901a0a0b --- /dev/null +++ b/internal/services/iothub/validate/endpoint_cosmos_db_account_id.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + + "github.com/hashicorp/terraform-provider-azurerm/internal/services/iothub/parse" +) + +func EndpointCosmosDBAccountID(input interface{}, key string) (warnings []string, errors []error) { + v, ok := input.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected %q to be a string", key)) + return + } + + if _, err := parse.EndpointCosmosDBAccountID(v); err != nil { + errors = append(errors, err) + } + + return +} diff --git a/internal/services/iothub/validate/endpoint_cosmos_db_account_id_test.go b/internal/services/iothub/validate/endpoint_cosmos_db_account_id_test.go new file mode 100644 index 000000000000..a48f5fbdd44b --- /dev/null +++ b/internal/services/iothub/validate/endpoint_cosmos_db_account_id_test.go @@ -0,0 +1,91 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import "testing" + +func TestEndpointCosmosDBAccountID(t *testing.T) { + cases := []struct { + Input string + Valid bool + }{ + + { + // empty + Input: "", + Valid: false, + }, + + { + // missing SubscriptionId + Input: "/", + Valid: false, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Valid: false, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Valid: false, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Valid: false, + }, + + { + // missing IotHubName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/", + Valid: false, + }, + + { + // missing value for IotHubName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/", + Valid: false, + }, + + { + // missing EndpointName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/hub1/", + Valid: false, + }, + + { + // missing value for EndpointName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/hub1/endpoints/", + Valid: false, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/iotHubs/hub1/endpoints/cosmosDBAccountEndpoint1", + Valid: true, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.DEVICES/IOTHUBS/HUB1/ENDPOINTS/COSMOSDBACCOUNTENDPOINT1", + Valid: false, + }, + } + for _, tc := range cases { + t.Logf("[DEBUG] Testing Value %s", tc.Input) + _, errors := EndpointCosmosDBAccountID(tc.Input, "test") + valid := len(errors) == 0 + + if tc.Valid != valid { + t.Fatalf("Expected %t but got %t", tc.Valid, valid) + } + } +} diff --git a/website/docs/r/iothub_endpoint_cosmosdb_account.html.markdown b/website/docs/r/iothub_endpoint_cosmosdb_account.html.markdown new file mode 100644 index 000000000000..1a89224d2636 --- /dev/null +++ b/website/docs/r/iothub_endpoint_cosmosdb_account.html.markdown @@ -0,0 +1,136 @@ +--- +subcategory: "IoT Hub" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_iothub_endpoint_cosmosdb_account" +description: |- + Manages an IotHub Cosmos DB Account Endpoint +--- + +# azurerm_iothub_endpoint_cosmosdb_account + +Manages an IotHub Cosmos DB Account Endpoint + +~> **NOTE:** Endpoints can be defined either directly on the `azurerm_iothub` resource, or using the `azurerm_iothub_endpoint_*` resources - but the two ways of defining the endpoints cannot be used together. If both are used against the same IoTHub, spurious changes will occur. Also, defining a `azurerm_iothub_endpoint_*` resource and another endpoint of a different type directly on the `azurerm_iothub` resource is not supported. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "example-resources" + location = "West Europe" +} + +resource "azurerm_iothub" "example" { + name = "exampleIothub" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + + sku { + name = "B1" + capacity = "1" + } + + tags = { + purpose = "example" + } +} + +resource "azurerm_cosmosdb_account" "example" { + name = "cosmosdb-account" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + offer_type = "Standard" + kind = "GlobalDocumentDB" + + consistency_policy { + consistency_level = "Strong" + } + + geo_location { + location = azurerm_resource_group.example.location + failover_priority = 0 + } +} + +resource "azurerm_cosmosdb_sql_database" "example" { + name = "cosmos-sql-db" + resource_group_name = azurerm_cosmosdb_account.example.resource_group_name + account_name = azurerm_cosmosdb_account.example.name +} + +resource "azurerm_cosmosdb_sql_container" "example" { + name = "example-container" + resource_group_name = azurerm_cosmosdb_account.example.resource_group_name + account_name = azurerm_cosmosdb_account.example.name + database_name = azurerm_cosmosdb_sql_database.example.name + partition_key_path = "/definition/id" +} + +resource "azurerm_iothub_endpoint_cosmosdb_account" "example" { + name = "example" + resource_group_name = azurerm_resource_group.example.name + iothub_id = azurerm_iothub.example.id + container_name = azurerm_cosmosdb_sql_container.example.name + database_name = azurerm_cosmosdb_sql_database.example.name + endpoint_uri = azurerm_cosmosdb_account.example.endpoint + primary_key = azurerm_cosmosdb_account.example.primary_key + secondary_key = azurerm_cosmosdb_account.example.secondary_key +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the endpoint. The name must be unique across endpoint types. The following names are reserved: `events`, `operationsMonitoringEvents`, `fileNotifications` and `$default`. Changing this forces a new resource to be created. + +* `resource_group_name` - (Required) The name of the resource group under which the Cosmos DB Account has been created. Changing this forces a new resource to be created. + +* `iothub_id` - (Required) The ID of the IoT Hub to create the endpoint. Changing this forces a new resource to be created. + +* `container_name` - (Required) The name of the Cosmos DB Container in the Cosmos DB Database. Changing this forces a new resource to be created. + +* `database_name` - (Required) The name of the Cosmos DB Database in the Cosmos DB Account. Changing this forces a new resource to be created. + +* `endpoint_uri` - (Required) The URI of the Cosmos DB Account. Changing this forces a new resource to be created. + +* `authentication_type` - (Optional) The type used to authenticate against the Cosmos DB Account endpoint. Possible values are `keyBased` and `identityBased`. Defaults to `keyBased`. + +* `identity_id` - (Optional) The ID of the User Managed Identity used to authenticate against the Cosmos DB Account endpoint. + +~> **NOTE:** `identity_id` can only be specified when `authentication_type` is `identityBased`. It must be one of the `identity_ids` of the Iot Hub. If not specified when `authentication_type` is `identityBased`, System Assigned Managed Identity of the Iot Hub will be used. + +* `partition_key_name` - (Optional) The name of the partition key associated with the Cosmos DB Container. + +* `partition_key_template` - (Optional) The template for generating a synthetic partition key value for use within the Cosmos DB Container. + +* `primary_key` - (Optional) The primary key of the Cosmos DB Account. + +~> **NOTE:** `primary_key` must and can only be specified when `authentication_type` is `keyBased`. + +* `secondary_key` - (Optional) The secondary key of the Cosmos DB Account. + +~> **NOTE:** `secondary_key` must and can only be specified when `authentication_type` is `keyBased`. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the IoTHub Cosmos DB Account Endpoint. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the IotHub Cosmos DB Account Endpoint. +* `update` - (Defaults to 30 minutes) Used when updating the IotHub Cosmos DB Account Endpoint. +* `read` - (Defaults to 5 minutes) Used when retrieving the IotHub Cosmos DB Account Endpoint. +* `delete` - (Defaults to 30 minutes) Used when deleting the IotHub Cosmos DB Account Endpoint. + +## Import + +IoTHub Cosmos DB Account Endpoint can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_iothub_endpoint_cosmosdb_account.endpoint1 /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.Devices/iotHubs/hub1/endpoints/cosmosDBAccountEndpoint1 +``` From 8a9f780dca6fde2b4f258aeb4dde3428139b3753 Mon Sep 17 00:00:00 2001 From: Yichun Ma Date: Wed, 6 Sep 2023 12:02:27 -0400 Subject: [PATCH 2/2] resolve comments --- .../iothub/iothub_endpoint_cosmosdb_account_resource.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/services/iothub/iothub_endpoint_cosmosdb_account_resource.go b/internal/services/iothub/iothub_endpoint_cosmosdb_account_resource.go index a7f6fbfad1cb..b329fc333da0 100644 --- a/internal/services/iothub/iothub_endpoint_cosmosdb_account_resource.go +++ b/internal/services/iothub/iothub_endpoint_cosmosdb_account_resource.go @@ -204,7 +204,7 @@ func (r IotHubEndpointCosmosDBAccountResource) Create() sdk.ResourceFunc { return fmt.Errorf("`primary_key` and `secondary_key` must be specified when `authentication_type` is `keyBased`") } cosmosDBAccountEndpoint.PrimaryKey = pointer.To(state.PrimaryKey) - cosmosDBAccountEndpoint.SecondaryKey = pointer.To(state.PrimaryKey) + cosmosDBAccountEndpoint.SecondaryKey = pointer.To(state.SecondaryKey) } else { if state.PrimaryKey != "" || state.SecondaryKey != "" { return fmt.Errorf("`primary_key` or `secondary_key` cannot be specified when `authentication_type` is `identityBased`") @@ -363,7 +363,7 @@ func (r IotHubEndpointCosmosDBAccountResource) Update() sdk.ResourceFunc { } endpoint.PrimaryKey = pointer.To(state.PrimaryKey) - endpoint.SecondaryKey = pointer.To(state.PrimaryKey) + endpoint.SecondaryKey = pointer.To(state.SecondaryKey) endpoint.Identity = nil } else { if state.PrimaryKey != "" || state.SecondaryKey != "" { @@ -437,7 +437,7 @@ func (r IotHubEndpointCosmosDBAccountResource) Delete() sdk.ResourceFunc { iothub, err := client.Get(ctx, id.ResourceGroup, id.IotHubName) if err != nil { if utils.ResponseWasNotFound(iothub.Response) { - return nil + return fmt.Errorf("%q was not found", id) } return fmt.Errorf("retrieving %q: %+v", id, err) }