diff --git a/internal/services/iothub/iothub_resource.go b/internal/services/iothub/iothub_resource.go index 31504ba416fd..0f43be171322 100644 --- a/internal/services/iothub/iothub_resource.go +++ b/internal/services/iothub/iothub_resource.go @@ -11,6 +11,8 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/services/iothub/mgmt/2021-03-31/devices" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema" + "github.com/hashicorp/go-azure-helpers/resourcemanager/identity" "github.com/hashicorp/terraform-provider-azurerm/helpers/azure" "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" "github.com/hashicorp/terraform-provider-azurerm/helpers/validate" @@ -18,6 +20,7 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/internal/locks" "github.com/hashicorp/terraform-provider-azurerm/internal/services/iothub/parse" iothubValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/iothub/validate" + msiparse "github.com/hashicorp/terraform-provider-azurerm/internal/services/msi/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/tags" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/suppress" @@ -538,6 +541,8 @@ func resourceIotHub() *pluginsdk.Resource { Computed: true, }, + "identity": commonschema.SystemAssignedUserAssignedIdentity(), + "tags": tags.Schema(), }, } @@ -608,6 +613,12 @@ func resourceIotHubCreateUpdate(d *pluginsdk.ResourceData, meta interface{}) err cloudToDeviceProperties = expandIoTHubCloudToDevice(d) } + identityRaw := d.Get("identity").([]interface{}) + identity, err := expandIotHubIdentity(identityRaw) + if err != nil { + return fmt.Errorf("expanding `identity`: %+v", err) + } + props := devices.IotHubDescription{ Name: utils.String(id.Name), Location: utils.String(azure.NormalizeLocation(d.Get("location").(string))), @@ -620,7 +631,8 @@ func resourceIotHubCreateUpdate(d *pluginsdk.ResourceData, meta interface{}) err EnableFileUploadNotifications: &enableFileUploadNotifications, CloudToDevice: cloudToDeviceProperties, }, - Tags: tags.Expand(d.Get("tags").(map[string]interface{})), + Identity: identity, + Tags: tags.Expand(d.Get("tags").(map[string]interface{})), } // nolint staticcheck @@ -767,6 +779,14 @@ func resourceIotHubRead(d *pluginsdk.ResourceData, meta interface{}) error { d.Set("min_tls_version", properties.MinTLSVersion) } + identity, err := flattenIotHubIdentity(hub.Identity) + if err != nil { + return err + } + if err := d.Set("identity", identity); err != nil { + return fmt.Errorf("setting `identity`: %+v", err) + } + d.Set("name", id.Name) d.Set("resource_group_name", id.ResourceGroup) if location := hub.Location; location != nil { @@ -1422,6 +1442,61 @@ func flattenIPFilterRules(in *[]devices.IPFilterRule) []interface{} { return rules } +func expandIotHubIdentity(input []interface{}) (*devices.ArmIdentity, error) { + config, err := identity.ExpandSystemAndUserAssignedMap(input) + if err != nil { + return nil, err + } + + identity := devices.ArmIdentity{ + Type: devices.ResourceIdentityType(config.Type), + } + + if len(config.IdentityIds) != 0 { + identityIds := make(map[string]*devices.ArmUserIdentity, len(config.IdentityIds)) + for id := range config.IdentityIds { + identityIds[id] = &devices.ArmUserIdentity{} + } + identity.UserAssignedIdentities = identityIds + } + + return &identity, nil +} + +func flattenIotHubIdentity(input *devices.ArmIdentity) (*[]interface{}, error) { + var config *identity.SystemAndUserAssignedMap + if input != nil { + identityIds := map[string]identity.UserAssignedIdentityDetails{} + for id := range input.UserAssignedIdentities { + parsedId, err := msiparse.UserAssignedIdentityIDInsensitively(id) + if err != nil { + return nil, err + } + identityIds[parsedId.ID()] = identity.UserAssignedIdentityDetails{ + // intentionally empty + } + } + + principalId := "" + if input.PrincipalID != nil { + principalId = *input.PrincipalID + } + + tenantId := "" + if input.TenantID != nil { + tenantId = *input.TenantID + } + + config = &identity.SystemAndUserAssignedMap{ + Type: identity.Type(string(input.Type)), + PrincipalId: principalId, + TenantId: tenantId, + IdentityIds: identityIds, + } + } + return identity.FlattenSystemAndUserAssignedMap(config) +} + func fileUploadConnectionStringDiffSuppress(k, old, new string, d *pluginsdk.ResourceData) bool { // The access keys are always masked by Azure and the ordering of the parameters in the connection string // differs across services, so we will compare the fields individually instead. diff --git a/internal/services/iothub/iothub_resource_test.go b/internal/services/iothub/iothub_resource_test.go index 51ab3bbc7ec2..8ae8cb6a0aa6 100644 --- a/internal/services/iothub/iothub_resource_test.go +++ b/internal/services/iothub/iothub_resource_test.go @@ -282,6 +282,101 @@ func TestAccIotHub_cloudToDevice(t *testing.T) { }) } +func TestAccIotHub_identitySystemAssigned(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub", "test") + r := IotHubResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.identitySystemAssigned(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccIotHub_identitySystemAssignedUserAssigned(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub", "test") + r := IotHubResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.identitySystemAssignedUserAssigned(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccIotHub_identityUserAssigned(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub", "test") + r := IotHubResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.identityUserAssigned(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.identityUserAssignedUpdated(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccIotHub_identityUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub", "test") + r := IotHubResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.identitySystemAssigned(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.identityUserAssigned(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.identitySystemAssignedUserAssigned(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 (t IotHubResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { id, err := parse.IotHubID(state.ID) if err != nil { @@ -1084,3 +1179,141 @@ resource "azurerm_iothub" "test" { } `, data.RandomInteger, data.Locations.Primary, data.RandomInteger) } + +func (IotHubResource) identitySystemAssigned(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} +resource "azurerm_resource_group" "test" { + name = "acctestRG-iothub-%d" + location = "%s" +} +resource "azurerm_iothub" "test" { + name = "acctestIoTHub-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + sku { + name = "B1" + capacity = "1" + } + identity { + type = "SystemAssigned" + } + tags = { + purpose = "testing" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} + +func (IotHubResource) identitySystemAssignedUserAssigned(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} +resource "azurerm_resource_group" "test" { + name = "acctestRG-iothub-%d" + location = "%s" +} +resource "azurerm_user_assigned_identity" "test" { + name = "acctestuai-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} +resource "azurerm_iothub" "test" { + name = "acctestIoTHub-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + sku { + name = "B1" + capacity = "1" + } + identity { + type = "SystemAssigned, UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id, + ] + } + tags = { + purpose = "testing" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger) +} + +func (IotHubResource) identityUserAssigned(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} +resource "azurerm_resource_group" "test" { + name = "acctestRG-iothub-%d" + location = "%s" +} +resource "azurerm_user_assigned_identity" "test" { + name = "acctestuai-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} +resource "azurerm_iothub" "test" { + name = "acctestIoTHub-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + sku { + name = "B1" + capacity = "1" + } + identity { + type = "UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id, + ] + } + tags = { + purpose = "testing" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger) +} + +func (IotHubResource) identityUserAssignedUpdated(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} +resource "azurerm_resource_group" "test" { + name = "acctestRG-iothub-%d" + location = "%s" +} +resource "azurerm_user_assigned_identity" "test" { + name = "acctestuai-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} +resource "azurerm_user_assigned_identity" "other" { + name = "acctestuai2-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} +resource "azurerm_iothub" "test" { + name = "acctestIoTHub-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + sku { + name = "B1" + capacity = "1" + } + identity { + type = "UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id, + azurerm_user_assigned_identity.other.id, + ] + } + tags = { + purpose = "testing" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger, data.RandomInteger) +} diff --git a/website/docs/r/iothub.html.markdown b/website/docs/r/iothub.html.markdown index 752ff5a3de68..d9b9089e71de 100644 --- a/website/docs/r/iothub.html.markdown +++ b/website/docs/r/iothub.html.markdown @@ -152,6 +152,8 @@ The following arguments are supported: * `file_upload` - (Optional) A `file_upload` block as defined below. +* `identity` - (Optional) An `identity` block as defined below. + * `ip_filter_rule` - (Optional) One or more `ip_filter_rule` blocks as defined below. * `route` - (Optional) A `route` block as defined below. @@ -200,6 +202,14 @@ An `endpoint` block supports the following: --- +A `identity` block supports the following: + +* `type` - (Required) The type of Managed Identity which should be assigned to the Iot Hub. Possible values are `SystemAssigned`, `UserAssigned` and `SystemAssigned, UserAssigned`. + +* `identity_ids` - (Optional) A list of User Managed Identity ID's which should be assigned to the Iot Hub. + +--- + An `ip_filter_rule` block supports the following: * `name` - (Required) The name of the filter. @@ -297,10 +307,20 @@ The following attributes are exported: * `hostname` - The hostname of the IotHub Resource. +* `identity` - An `identity` block as documented below. + * `shared_access_policy` - One or more `shared_access_policy` blocks as defined below. --- +An `identity` block exports the following: + +* `principal_id` - The ID of the System Managed Service Principal. + +* `tenant_id` - The ID of the Tenant the System Managed Service Principal is assigned in. + +--- + A `shared access policy` block contains the following: * `key_name` - The name of the shared access policy.