diff --git a/internal/services/azurestackhci/stack_hci_cluster_data_source.go b/internal/services/azurestackhci/stack_hci_cluster_data_source.go index 2c7ed6bd8943d..cc0a49b211b6f 100644 --- a/internal/services/azurestackhci/stack_hci_cluster_data_source.go +++ b/internal/services/azurestackhci/stack_hci_cluster_data_source.go @@ -11,6 +11,7 @@ import ( "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/identity" "github.com/hashicorp/go-azure-helpers/resourcemanager/location" "github.com/hashicorp/go-azure-helpers/resourcemanager/tags" "github.com/hashicorp/go-azure-sdk/resource-manager/azurestackhci/2023-08-01/clusters" @@ -35,13 +36,17 @@ func (r StackHCIClusterDataSource) ModelObject() interface{} { } type StackHCIClusterDataSourceModel struct { - Name string `tfschema:"name"` - ResourceGroupName string `tfschema:"resource_group_name"` - Location string `tfschema:"location"` - ClientId string `tfschema:"client_id"` - TenantId string `tfschema:"tenant_id"` - AutomanageConfigurationId string `tfschema:"automanage_configuration_id"` - Tags map[string]interface{} `tfschema:"tags"` + Name string `tfschema:"name"` + ResourceGroupName string `tfschema:"resource_group_name"` + Location string `tfschema:"location"` + ClientId string `tfschema:"client_id"` + TenantId string `tfschema:"tenant_id"` + AutomanageConfigurationId string `tfschema:"automanage_configuration_id"` + CloudId string `tfschema:"cloud_id"` + ServiceEndpoint string `tfschema:"service_endpoint"` + ResourceProviderObjectId string `tfschema:"resource_provider_object_id"` + Identity []identity.ModelSystemAssigned `tfschema:"identity"` + Tags map[string]interface{} `tfschema:"tags"` } func (r StackHCIClusterDataSource) Arguments() map[string]*schema.Schema { @@ -75,6 +80,23 @@ func (r StackHCIClusterDataSource) Attributes() map[string]*schema.Schema { Computed: true, }, + "cloud_id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "service_endpoint": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "resource_provider_object_id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "identity": commonschema.SystemAssignedIdentityComputed(), + "tags": commonschema.TagsDataSource(), } } @@ -108,6 +130,10 @@ func (r StackHCIClusterDataSource) Read() sdk.ResourceFunc { if props := model.Properties; props != nil { cluster.ClientId = pointer.From(props.AadClientId) + cluster.CloudId = pointer.From(props.CloudId) + cluster.Identity = flattenSystemAssignedToModel(model.Identity) + cluster.ResourceProviderObjectId = pointer.From(props.ResourceProviderObjectId) + cluster.ServiceEndpoint = pointer.From(props.ServiceEndpoint) cluster.TenantId = pointer.From(props.AadTenantId) assignmentResp, err := hciAssignmentClient.Get(ctx, id.ResourceGroupName, id.ClusterName, "default") @@ -132,3 +158,21 @@ func (r StackHCIClusterDataSource) Read() sdk.ResourceFunc { }, } } + +func flattenSystemAssignedToModel(input *identity.SystemAndUserAssignedMap) []identity.ModelSystemAssigned { + if input == nil { + return []identity.ModelSystemAssigned{} + } + + if input.Type == identity.TypeNone { + return []identity.ModelSystemAssigned{} + } + + return []identity.ModelSystemAssigned{ + { + Type: input.Type, + PrincipalId: input.PrincipalId, + TenantId: input.TenantId, + }, + } +} diff --git a/internal/services/azurestackhci/stack_hci_cluster_data_source_test.go b/internal/services/azurestackhci/stack_hci_cluster_data_source_test.go index 9fbe1b820be44..b802041dffaab 100644 --- a/internal/services/azurestackhci/stack_hci_cluster_data_source_test.go +++ b/internal/services/azurestackhci/stack_hci_cluster_data_source_test.go @@ -24,6 +24,25 @@ func TestAccStackHCIClusterDataSource_basic(t *testing.T) { check.That(data.ResourceName).Key("location").IsNotEmpty(), check.That(data.ResourceName).Key("client_id").IsNotEmpty(), check.That(data.ResourceName).Key("tenant_id").IsNotEmpty(), + check.That(data.ResourceName).Key("cloud_id").IsNotEmpty(), + check.That(data.ResourceName).Key("service_endpoint").IsNotEmpty(), + check.That(data.ResourceName).Key("resource_provider_object_id").IsNotEmpty(), + ), + }, + }) +} + +func TestAccStackHCIClusterDataSource_identity(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_stack_hci_cluster", "test") + r := StackHCIClusterDataSource{} + + data.DataSourceTest(t, []acceptance.TestStep{ + { + Config: r.identity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("identity.0.type").HasValue("SystemAssigned"), + check.That(data.ResourceName).Key("identity.0.principal_id").IsNotEmpty(), + check.That(data.ResourceName).Key("identity.0.tenant_id").IsNotEmpty(), ), }, }) @@ -39,3 +58,14 @@ data "azurerm_stack_hci_cluster" "test" { } `, StackHCIClusterResource{}.basic(data)) } + +func (d StackHCIClusterDataSource) identity(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +data "azurerm_stack_hci_cluster" "test" { + name = azurerm_stack_hci_cluster.test.name + resource_group_name = azurerm_stack_hci_cluster.test.resource_group_name +} +`, StackHCIClusterResource{}.systemAssignedIdentity(data)) +} diff --git a/internal/services/azurestackhci/stack_hci_cluster_resource.go b/internal/services/azurestackhci/stack_hci_cluster_resource.go index 5a73997ee9a26..413d978b20fbe 100644 --- a/internal/services/azurestackhci/stack_hci_cluster_resource.go +++ b/internal/services/azurestackhci/stack_hci_cluster_resource.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/go-azure-helpers/lang/response" "github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema" + "github.com/hashicorp/go-azure-helpers/resourcemanager/identity" "github.com/hashicorp/go-azure-helpers/resourcemanager/location" "github.com/hashicorp/go-azure-helpers/resourcemanager/tags" "github.com/hashicorp/go-azure-sdk/resource-manager/azurestackhci/2023-08-01/clusters" @@ -77,6 +78,23 @@ func resourceArmStackHCICluster() *pluginsdk.Resource { ValidateFunc: autoVal.AutomanageConfigurationID, }, + "cloud_id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "service_endpoint": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "resource_provider_object_id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "identity": commonschema.SystemAssignedIdentityOptional(), + "tags": commonschema.Tags(), }, } @@ -108,6 +126,10 @@ func resourceArmStackHCIClusterCreate(d *pluginsdk.ResourceData, meta interface{ Tags: tags.Expand(d.Get("tags").(map[string]interface{})), } + if v, ok := d.GetOk("identity"); ok { + cluster.Identity = expandSystemAssigned(v.([]interface{})) + } + if v, ok := d.GetOk("tenant_id"); ok { cluster.Properties.AadTenantId = utils.String(v.(string)) } else { @@ -185,10 +207,14 @@ func resourceArmStackHCIClusterRead(d *pluginsdk.ResourceData, meta interface{}) if model := resp.Model; model != nil { d.Set("location", location.Normalize(model.Location)) + d.Set("identity", flattenSystemAssigned(model.Identity)) if props := model.Properties; props != nil { d.Set("client_id", props.AadClientId) d.Set("tenant_id", props.AadTenantId) + d.Set("cloud_id", props.CloudId) + d.Set("service_endpoint", props.ServiceEndpoint) + d.Set("resource_provider_object_id", props.ResourceProviderObjectId) assignmentResp, err := hciAssignmentClient.Get(ctx, id.ResourceGroupName, id.ClusterName, "default") if err != nil && !utils.ResponseWasNotFound(assignmentResp.Response) { @@ -231,6 +257,10 @@ func resourceArmStackHCIClusterUpdate(d *pluginsdk.ResourceData, meta interface{ cluster.Tags = tags.Expand(d.Get("tags").(map[string]interface{})) } + if d.HasChange("identity") { + cluster.Identity = expandSystemAssigned(d.Get("identity").([]interface{})) + } + if _, err := client.Update(ctx, *id, cluster); err != nil { return fmt.Errorf("updating %s: %+v", *id, err) } @@ -274,7 +304,6 @@ func resourceArmStackHCIClusterUpdate(d *pluginsdk.ResourceData, meta interface{ } } } - } return resourceArmStackHCIClusterRead(d, meta) @@ -308,3 +337,34 @@ func resourceArmStackHCIClusterDelete(d *pluginsdk.ResourceData, meta interface{ return nil } + +// API does not accept userAssignedIdentity as in swagger https://github.com/Azure/azure-rest-api-specs/issues/28260 +func expandSystemAssigned(input []interface{}) *identity.SystemAndUserAssignedMap { + if len(input) == 0 || input[0] == nil { + return &identity.SystemAndUserAssignedMap{ + Type: identity.TypeNone, + } + } + + return &identity.SystemAndUserAssignedMap{ + Type: identity.TypeSystemAssigned, + } +} + +func flattenSystemAssigned(input *identity.SystemAndUserAssignedMap) []interface{} { + if input == nil { + return []interface{}{} + } + + if input.Type == identity.TypeNone { + return []interface{}{} + } + + return []interface{}{ + map[string]interface{}{ + "type": input.Type, + "principal_id": input.PrincipalId, + "tenant_id": input.TenantId, + }, + } +} diff --git a/internal/services/azurestackhci/stack_hci_cluster_resource_test.go b/internal/services/azurestackhci/stack_hci_cluster_resource_test.go index fcd11d945efd7..51ae137d96cf8 100644 --- a/internal/services/azurestackhci/stack_hci_cluster_resource_test.go +++ b/internal/services/azurestackhci/stack_hci_cluster_resource_test.go @@ -28,6 +28,9 @@ func TestAccStackHCICluster_basic(t *testing.T) { Config: r.basic(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("cloud_id").IsNotEmpty(), + check.That(data.ResourceName).Key("service_endpoint").IsNotEmpty(), + check.That(data.ResourceName).Key("resource_provider_object_id").IsNotEmpty(), ), }, data.ImportStep(), @@ -49,6 +52,53 @@ func TestAccStackHCICluster_requiresImport(t *testing.T) { }) } +func TestAccStackHCICluster_systemAssignedIdentity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_stack_hci_cluster", "test") + r := StackHCIClusterResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.systemAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("identity.0.type").HasValue("SystemAssigned"), + check.That(data.ResourceName).Key("identity.0.principal_id").IsNotEmpty(), + check.That(data.ResourceName).Key("identity.0.tenant_id").IsNotEmpty(), + ), + }, + data.ImportStep(), + }) +} + +func TestAccStackHCICluster_systemAssignedIdentityUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_stack_hci_cluster", "test") + r := StackHCIClusterResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.systemAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + func TestAccStackHCICluster_complete(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_stack_hci_cluster", "test") r := StackHCIClusterResource{} @@ -158,6 +208,24 @@ resource "azurerm_stack_hci_cluster" "import" { `, config) } +func (r StackHCIClusterResource) systemAssignedIdentity(data acceptance.TestData) string { + template := r.template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_stack_hci_cluster" "test" { + name = "acctest-StackHCICluster-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + client_id = azuread_application.test.application_id + tenant_id = data.azurerm_client_config.current.tenant_id + identity { + type = "SystemAssigned" + } +} +`, template, data.RandomInteger) +} + func (r StackHCIClusterResource) complete(data acceptance.TestData) string { template := r.template(data) return fmt.Sprintf(` @@ -169,6 +237,9 @@ resource "azurerm_stack_hci_cluster" "test" { location = azurerm_resource_group.test.location client_id = azuread_application.test.application_id tenant_id = data.azurerm_client_config.current.tenant_id + identity { + type = "SystemAssigned" + } tags = { ENV = "Test" diff --git a/website/docs/d/stack_hci_cluster.html.markdown b/website/docs/d/stack_hci_cluster.html.markdown index 774bee7794a23..43637878ff383 100644 --- a/website/docs/d/stack_hci_cluster.html.markdown +++ b/website/docs/d/stack_hci_cluster.html.markdown @@ -47,15 +47,33 @@ In addition to the Arguments listed above - the following Attributes are exporte * `id` - The ID of the Azure Stack HCI Cluster. -* `location` - The Azure Region where the Azure Stack HCI Cluster exists. +* `automanage_configuration_id` - The ID of the Automanage Configuration assigned to the Azure Stack HCI Cluster. * `client_id` - The Client ID of the Azure Active Directory used by the Azure Stack HCI Cluster. +* `cloud_id` - An immutable UUID for the Azure Stack HCI Cluster. + +* `identity` - An `identity` block as defined below. + +* `location` - The Azure Region where the Azure Stack HCI Cluster exists. + +* `resource_provider_object_id` - The object ID of the Resource Provider Service Principal. + +* `service_endpoint` - The region specific Data Path Endpoint of the Azure Stack HCI Cluster. + * `tenant_id` - The Tenant ID of the Azure Active Directory used by the Azure Stack HCI Cluster. * `tags` - A mapping of tags assigned to the Azure Stack HCI Cluster. -* `automanage_configuration_id` - The ID of the Automanage Configuration assigned to the Azure Stack HCI Cluster. +--- + +An `identity` block exports the following: + +* `type` - (Required) The type of Managed Service Identity configured on the Azure Stack HCI Cluster. + +* `principal_id` - The Principal ID associated with this Managed Service Identity. + +* `tenant_id` - The Tenant ID associated with this Managed Service Identity. ## Timeouts diff --git a/website/docs/r/stack_hci_cluster.html.markdown b/website/docs/r/stack_hci_cluster.html.markdown index b5550481859ab..e5bd9734d3cec 100644 --- a/website/docs/r/stack_hci_cluster.html.markdown +++ b/website/docs/r/stack_hci_cluster.html.markdown @@ -30,6 +30,9 @@ resource "azurerm_stack_hci_cluster" "example" { location = azurerm_resource_group.example.location client_id = data.azuread_application.example.application_id tenant_id = data.azurerm_client_config.current.tenant_id + identity { + type = "SystemAssigned" + } } ``` @@ -45,6 +48,8 @@ The following arguments are supported: * `client_id` - (Required) The Client ID of the Azure Active Directory which is used by the Azure Stack HCI Cluster. Changing this forces a new resource to be created. +* `identity` - (Optional) An `identity` block as defined below. + * `tenant_id` - (Optional) The Tenant ID of the Azure Active Directory which is used by the Azure Stack HCI Cluster. Changing this forces a new resource to be created. ~> **NOTE** If unspecified the Tenant ID of the Provider will be used. @@ -53,11 +58,35 @@ The following arguments are supported: * `automanage_configuration_id` - (Optional) The ID of the Automanage Configuration assigned to the Azure Stack HCI Cluster. +--- + +An `identity` block supports the following: + +* `type` - (Required) Specifies the type of Managed Service Identity that should be configured on the Azure Stack HCI Cluster. Possible value is `SystemAssigned`. + ## Attributes Reference In addition to the Arguments listed above - the following Attributes are exported: -* `id` - The ID of the Azure Stack HCI Cluster. +* `id` - The resource ID of the Azure Stack HCI Cluster. + +* `cloud_id` - An immutable UUID for the Azure Stack HCI Cluster. + +* `resource_provider_object_id` - The object ID of the Resource Provider Service Principal. + +* `identity` - An `identity` block as defined below. + +* `service_endpoint` - The region specific Data Path Endpoint of the Azure Stack HCI Cluster. + +--- + +An `identity` block exports the following: + +* `principal_id` - The Principal ID associated with this Managed Service Identity. + +* `tenant_id` - The Tenant ID associated with this Managed Service Identity. + +-> You can access the Principal ID via `azurerm_stack_hci_cluster.example.identity.0.principal_id` ## Timeouts