From a914e1386f182c549822adbeb2ba916f1cee1e1e Mon Sep 17 00:00:00 2001 From: Yichun Ma Date: Tue, 15 Feb 2022 14:48:14 +0800 Subject: [PATCH] `r\iothub`: Extract `file_upload` and support `identityBased` auth --- .../iothub/iothub_file_upload_resource.go | 327 ++++++++++++++++ .../iothub_file_upload_resource_test.go | 353 ++++++++++++++++++ internal/services/iothub/iothub_resource.go | 51 ++- .../services/iothub/iothub_resource_test.go | 258 ++++++++++++- internal/services/iothub/parse/file_upload.go | 75 ++++ .../services/iothub/parse/file_upload_test.go | 128 +++++++ internal/services/iothub/registration.go | 1 + internal/services/iothub/resourceids.go | 1 + .../iothub/validate/file_upload_id.go | 23 ++ .../iothub/validate/file_upload_id_test.go | 88 +++++ website/docs/r/iothub.html.markdown | 10 + .../docs/r/iothub_file_upload.html.markdown | 77 ++++ 12 files changed, 1382 insertions(+), 10 deletions(-) create mode 100644 internal/services/iothub/iothub_file_upload_resource.go create mode 100644 internal/services/iothub/iothub_file_upload_resource_test.go create mode 100644 internal/services/iothub/parse/file_upload.go create mode 100644 internal/services/iothub/parse/file_upload_test.go create mode 100644 internal/services/iothub/validate/file_upload_id.go create mode 100644 internal/services/iothub/validate/file_upload_id_test.go create mode 100644 website/docs/r/iothub_file_upload.html.markdown diff --git a/internal/services/iothub/iothub_file_upload_resource.go b/internal/services/iothub/iothub_file_upload_resource.go new file mode 100644 index 0000000000000..ab0c33e00d679 --- /dev/null +++ b/internal/services/iothub/iothub_file_upload_resource.go @@ -0,0 +1,327 @@ +package iothub + +import ( + "fmt" + "time" + + "github.com/Azure/azure-sdk-for-go/services/iothub/mgmt/2021-07-02/devices" + "github.com/hashicorp/terraform-provider-azurerm/helpers/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "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" + msivalidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/msi/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/internal/timeouts" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +func resourceIotHubFileUpload() *pluginsdk.Resource { + return &pluginsdk.Resource{ + Create: resourceIotHubFileUploadCreateUpdate, + Read: resourceIotHubFileUploadRead, + Update: resourceIotHubFileUploadCreateUpdate, + Delete: resourceIotHubFileUploadDelete, + + Importer: pluginsdk.ImporterValidatingResourceId(func(id string) error { + _, err := parse.FileUploadID(id) + return err + }), + + Timeouts: &pluginsdk.ResourceTimeout{ + Create: pluginsdk.DefaultTimeout(30 * time.Minute), + Read: pluginsdk.DefaultTimeout(5 * time.Minute), + Update: pluginsdk.DefaultTimeout(30 * time.Minute), + Delete: pluginsdk.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*pluginsdk.Schema{ + "iothub_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: iothubValidate.IotHubID, + }, + + "connection_string": { + Type: pluginsdk.TypeString, + Required: true, + DiffSuppressFunc: fileUploadConnectionStringDiffSuppress, + Sensitive: true, + }, + + "container_name": { + Type: pluginsdk.TypeString, + Required: true, + }, + + "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: msivalidate.UserAssignedIdentityID, + }, + + "notifications": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + + "max_delivery_count": { + Type: pluginsdk.TypeInt, + Optional: true, + Default: 10, + ValidateFunc: validation.IntBetween(1, 100), + }, + + "sas_ttl": { + Type: pluginsdk.TypeString, + Optional: true, + Default: "PT1H", + ValidateFunc: validate.ISO8601DurationBetween("PT1M", "P1D"), + }, + + "default_ttl": { + Type: pluginsdk.TypeString, + Optional: true, + Default: "PT1H", + ValidateFunc: validate.ISO8601DurationBetween("PT1M", "P2D"), + }, + + "lock_duration": { + Type: pluginsdk.TypeString, + Optional: true, + Default: "PT1M", + ValidateFunc: validate.ISO8601DurationBetween("PT5S", "PT5M"), + }, + }, + } +} + +func resourceIotHubFileUploadCreateUpdate(d *pluginsdk.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).IoTHub.ResourceClient + subscriptionId := meta.(*clients.Client).IoTHub.ResourceClient.SubscriptionID + ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) + defer cancel() + + iothubId, err := parse.IotHubID(d.Get("iothub_id").(string)) + if err != nil { + return err + } + iotHubName := iothubId.Name + iotHubRG := iothubId.ResourceGroup + + id := parse.NewFileUploadID(subscriptionId, iotHubRG, iotHubName, "default") + + locks.ByName(iotHubName, IothubResourceName) + defer locks.UnlockByName(iotHubName, IothubResourceName) + + iothub, err := client.Get(ctx, iotHubRG, iotHubName) + if err != nil { + if utils.ResponseWasNotFound(iothub.Response) { + return fmt.Errorf("IotHub %q (Resource Group %q) was not found", iotHubName, iotHubRG) + } + + return fmt.Errorf("loading IotHub %q (Resource Group %q): %+v", iotHubName, iotHubRG, err) + } + + authenticationType := devices.AuthenticationType(d.Get("authentication_type").(string)) + identityId := d.Get("identity_id").(string) + + storageEndpoints := iothub.Properties.StorageEndpoints + if storageEndpoints == nil { + storageEndpoints = make(map[string]*devices.StorageEndpointProperties) + } + + storageEndpoints["$default"] = &devices.StorageEndpointProperties{ + SasTTLAsIso8601: utils.String(d.Get("sas_ttl").(string)), + AuthenticationType: authenticationType, + ConnectionString: utils.String(d.Get("connection_string").(string)), + ContainerName: utils.String(d.Get("container_name").(string)), + } + + if identityId != "" { + if authenticationType != devices.AuthenticationTypeIdentityBased { + return fmt.Errorf("`identity_id` can only be specified when `authentication_type` is `identityBased`") + } + storageEndpoints["$default"].Identity = &devices.ManagedIdentity{ + UserAssignedIdentity: &identityId, + } + } + + messagingEndpoints := iothub.Properties.MessagingEndpoints + if messagingEndpoints == nil { + messagingEndpoints = make(map[string]*devices.MessagingEndpointProperties) + } + + messagingEndpoints["fileNotifications"] = &devices.MessagingEndpointProperties{ + LockDurationAsIso8601: utils.String(d.Get("lock_duration").(string)), + TTLAsIso8601: utils.String(d.Get("default_ttl").(string)), + MaxDeliveryCount: utils.Int32(int32(d.Get("max_delivery_count").(int))), + } + + iothub.Properties.EnableFileUploadNotifications = utils.Bool(d.Get("notifications").(bool)) + + future, err := client.CreateOrUpdate(ctx, iotHubRG, iotHubName, iothub, "") + if err != nil { + return fmt.Errorf("creating/updating %s: %+v", id, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for the completion of the creating/updating of %s: %+v", id, err) + } + + d.SetId(id.ID()) + + return resourceIotHubFileUploadRead(d, meta) +} + +func resourceIotHubFileUploadRead(d *pluginsdk.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).IoTHub.ResourceClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.FileUploadID(d.Id()) + if err != nil { + return err + } + + iothub, err := client.Get(ctx, id.ResourceGroup, id.IotHubName) + if err != nil { + return fmt.Errorf("loading IotHub %q (Resource Group %q): %+v", id.IotHubName, id.ResourceGroup, err) + } + + iotHubId := parse.NewIotHubID(id.SubscriptionId, id.ResourceGroup, id.IotHubName) + d.Set("iothub_id", iotHubId.ID()) + + if props := iothub.Properties; props != nil { + connectionString := "" + containerName := "" + sasTTL := "" + authenticationType := string(devices.AuthenticationTypeKeyBased) + identityId := "" + if storageEndpointProperties, ok := props.StorageEndpoints["$default"]; ok { + if v := storageEndpointProperties.ConnectionString; v != nil { + connectionString = *v + } + + if v := storageEndpointProperties.ContainerName; v != nil { + containerName = *v + } + + if v := storageEndpointProperties.SasTTLAsIso8601; v != nil { + sasTTL = *v + } + + if v := string(storageEndpointProperties.AuthenticationType); v != "" { + authenticationType = v + } + + if storageEndpointProperties.Identity != nil && storageEndpointProperties.Identity.UserAssignedIdentity != nil { + identityId = *storageEndpointProperties.Identity.UserAssignedIdentity + } + } + d.Set("connection_string", connectionString) + d.Set("container_name", containerName) + d.Set("sas_ttl", sasTTL) + d.Set("authentication_type", authenticationType) + d.Set("identity_id", identityId) + + lockDuration := "" + defaultTTL := "" + var maxDeliveryCount int32 = 10 + if messagingEndpointProperties, ok := props.MessagingEndpoints["fileNotifications"]; ok { + if v := messagingEndpointProperties.LockDurationAsIso8601; v != nil { + lockDuration = *v + } + + if v := messagingEndpointProperties.TTLAsIso8601; v != nil { + defaultTTL = *v + } + + if v := messagingEndpointProperties.MaxDeliveryCount; v != nil { + maxDeliveryCount = *v + } + } + d.Set("lock_duration", lockDuration) + d.Set("default_ttl", defaultTTL) + d.Set("max_delivery_count", maxDeliveryCount) + + enableFileUploadNotifications := false + if v := props.EnableFileUploadNotifications; v != nil { + enableFileUploadNotifications = *v + } + d.Set("notifications", enableFileUploadNotifications) + } + + return nil +} + +func resourceIotHubFileUploadDelete(d *pluginsdk.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).IoTHub.ResourceClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.FileUploadID(d.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 fmt.Errorf("IotHub %q (Resource Group %q) was not found", id.IotHubName, id.ResourceGroup) + } + + return fmt.Errorf("loading IotHub %q (Resource Group %q): %+v", id.IotHubName, id.ResourceGroup, err) + } + + if iothub.Properties == nil { + return nil + } + + shouldUpdate := false + if iothub.Properties.StorageEndpoints != nil { + shouldUpdate = true + iothub.Properties.StorageEndpoints = make(map[string]*devices.StorageEndpointProperties) + } + + if iothub.Properties.MessagingEndpoints != nil { + shouldUpdate = true + iothub.Properties.MessagingEndpoints["fileNotifications"] = &devices.MessagingEndpointProperties{} + } + + if iothub.Properties.EnableFileUploadNotifications != nil { + shouldUpdate = true + iothub.Properties.EnableFileUploadNotifications = nil + } + + if !shouldUpdate { + return nil + } + + 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 %s to finish updating: %+v", id, err) + } + + return nil +} diff --git a/internal/services/iothub/iothub_file_upload_resource_test.go b/internal/services/iothub/iothub_file_upload_resource_test.go new file mode 100644 index 0000000000000..b9185b14ba986 --- /dev/null +++ b/internal/services/iothub/iothub_file_upload_resource_test.go @@ -0,0 +1,353 @@ +package iothub_test + +import ( + "context" + "fmt" + "testing" + + "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" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +type IotHubFileUploadResource struct{} + +// NOTE: this resource intentionally doesn't support Requires Import +// since a File Upload is created by default + +func TestAccIotHubFileUpload_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub_file_upload", "test") + r := IotHubFileUploadResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccIotHubFileUpload_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub_file_upload", "test") + r := IotHubFileUploadResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.update(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccIotHubFileUpload_authenticationTypeSystemAssignedIdentity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub_file_upload", "test") + r := IotHubFileUploadResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.authenticationTypeSystemAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccIotHubFileUpload_authenticationTypeUserAssignedIdentity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub_file_upload", "test") + r := IotHubFileUploadResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.authenticationTypeUserAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccIotHubFileUpload_authenticationTypeUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub_file_upload", "test") + r := IotHubFileUploadResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.authenticationTypeDefault(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + 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(), + }) +} + +func (IotHubFileUploadResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := parse.FileUploadID(state.ID) + if err != nil { + return nil, err + } + + resp, err := clients.IoTHub.ResourceClient.Get(ctx, id.ResourceGroup, id.IotHubName) + if err != nil || resp.Properties == nil || resp.Properties.StorageEndpoints["$default"] == nil || resp.Properties.MessagingEndpoints["fileNotifications"] == nil { + return nil, fmt.Errorf("reading IotHub File Upload (%s): %+v", id, err) + } + + return utils.Bool(true), nil +} + +func (IotHubFileUploadResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-iothub-%[1]d" + location = "%[2]s" +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%[3]s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_storage_container" "test" { + name = "test-%[1]d" + storage_account_name = azurerm_storage_account.test.name + container_access_type = "private" +} + +resource "azurerm_iothub" "test" { + name = "acctestIoTHub-%[1]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + + sku { + name = "S1" + capacity = "1" + } + + tags = { + purpose = "testing" + } +} + +resource "azurerm_iothub_file_upload" "test" { + iothub_id = azurerm_iothub.test.id + connection_string = azurerm_storage_account.test.primary_blob_connection_string + container_name = azurerm_storage_container.test.name +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString) +} + +func (IotHubFileUploadResource) update(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-iothub-%[1]d" + location = "%[2]s" +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%[3]s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_storage_container" "test" { + name = "test-%[1]d" + storage_account_name = azurerm_storage_account.test.name + container_access_type = "private" +} + +resource "azurerm_iothub" "test" { + name = "acctestIoTHub-%[1]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + + sku { + name = "S1" + capacity = "1" + } + + tags = { + purpose = "testing" + } +} + +resource "azurerm_iothub_file_upload" "test" { + iothub_id = azurerm_iothub.test.id + connection_string = azurerm_storage_account.test.primary_blob_connection_string + container_name = azurerm_storage_container.test.name + notifications = true + max_delivery_count = 100 + sas_ttl = "P1D" + default_ttl = "P2D" + lock_duration = "PT5M" +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString) +} + +func (r IotHubFileUploadResource) authenticationTypeDefault(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_iothub_file_upload" "test" { + iothub_id = azurerm_iothub.test.id + connection_string = azurerm_storage_account.test.primary_blob_connection_string + container_name = azurerm_storage_container.test.name +} +`, r.authenticationTemplate(data)) +} + +func (r IotHubFileUploadResource) authenticationTypeSystemAssignedIdentity(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_iothub_file_upload" "test" { + iothub_id = azurerm_iothub.test.id + connection_string = azurerm_storage_account.test.primary_blob_connection_string + container_name = azurerm_storage_container.test.name + + authentication_type = "identityBased" + + depends_on = [ + azurerm_role_assignment.test_storage_blob_data_contrib_system, + ] +} +`, r.authenticationTemplate(data)) +} + +func (r IotHubFileUploadResource) authenticationTypeUserAssignedIdentity(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_iothub_file_upload" "test" { + iothub_id = azurerm_iothub.test.id + connection_string = azurerm_storage_account.test.primary_blob_connection_string + container_name = azurerm_storage_container.test.name + + authentication_type = "identityBased" + identity_id = azurerm_user_assigned_identity.test.id +} +`, r.authenticationTemplate(data)) +} + +func (IotHubFileUploadResource) authenticationTemplate(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-iothub-%[1]d" + location = "%[2]s" +} + +resource "azurerm_user_assigned_identity" "test" { + name = "acctestuai-%[1]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} + +resource "azurerm_role_assignment" "test_storage_blob_data_contrib_user" { + role_definition_name = "Storage Blob Data Contributor" + scope = azurerm_storage_account.test.id + principal_id = azurerm_user_assigned_identity.test.principal_id +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%[3]s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_storage_container" "test" { + name = "test-%[1]d" + storage_account_name = azurerm_storage_account.test.name + container_access_type = "private" +} + +resource "azurerm_iothub" "test" { + name = "acctestIoTHub-%[1]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + + sku { + name = "S1" + capacity = "1" + } + + tags = { + purpose = "testing" + } + + identity { + type = "SystemAssigned, UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id, + ] + } + + depends_on = [ + azurerm_role_assignment.test_storage_blob_data_contrib_user, + ] +} + +resource "azurerm_role_assignment" "test_storage_blob_data_contrib_system" { + role_definition_name = "Storage Blob Data Contributor" + scope = azurerm_storage_account.test.id + principal_id = azurerm_iothub.test.identity[0].principal_id +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString) +} diff --git a/internal/services/iothub/iothub_resource.go b/internal/services/iothub/iothub_resource.go index c4050454486cd..c6f9179e5a471 100644 --- a/internal/services/iothub/iothub_resource.go +++ b/internal/services/iothub/iothub_resource.go @@ -172,6 +172,7 @@ func resourceIotHub() *pluginsdk.Resource { Type: pluginsdk.TypeList, MaxItems: 1, Optional: true, + Computed: true, Elem: &pluginsdk.Resource{ Schema: map[string]*pluginsdk.Schema{ "connection_string": { @@ -184,6 +185,20 @@ func resourceIotHub() *pluginsdk.Resource { Type: pluginsdk.TypeString, Required: true, }, + "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: msivalidate.UserAssignedIdentityID, + }, "notifications": { Type: pluginsdk.TypeBool, Optional: true, @@ -664,7 +679,7 @@ func resourceIotHubCreateUpdate(d *pluginsdk.ResourceData, meta interface{}) err } } - storageEndpoints, messagingEndpoints, enableFileUploadNotifications := expandIoTHubFileUpload(d) + storageEndpoints, messagingEndpoints, enableFileUploadNotifications, err := expandIoTHubFileUpload(d) if err != nil { return fmt.Errorf("expanding `file_upload`: %+v", err) } @@ -1008,7 +1023,7 @@ func expandIoTHubEnrichments(d *pluginsdk.ResourceData) *[]devices.EnrichmentPro return &enrichmentProperties } -func expandIoTHubFileUpload(d *pluginsdk.ResourceData) (map[string]*devices.StorageEndpointProperties, map[string]*devices.MessagingEndpointProperties, bool) { +func expandIoTHubFileUpload(d *pluginsdk.ResourceData) (map[string]*devices.StorageEndpointProperties, map[string]*devices.MessagingEndpointProperties, bool, error) { fileUploadList := d.Get("file_upload").([]interface{}) storageEndpointProperties := make(map[string]*devices.StorageEndpointProperties) @@ -1018,6 +1033,8 @@ func expandIoTHubFileUpload(d *pluginsdk.ResourceData) (map[string]*devices.Stor if len(fileUploadList) > 0 { fileUploadMap := fileUploadList[0].(map[string]interface{}) + authenticationType := devices.AuthenticationType(fileUploadMap["authentication_type"].(string)) + identityId := fileUploadMap["identity_id"].(string) connectionStr := fileUploadMap["connection_string"].(string) containerName := fileUploadMap["container_name"].(string) notifications = fileUploadMap["notifications"].(bool) @@ -1027,9 +1044,19 @@ func expandIoTHubFileUpload(d *pluginsdk.ResourceData) (map[string]*devices.Stor lockDuration := fileUploadMap["lock_duration"].(string) storageEndpointProperties["$default"] = &devices.StorageEndpointProperties{ - SasTTLAsIso8601: &sasTTL, - ConnectionString: &connectionStr, - ContainerName: &containerName, + SasTTLAsIso8601: &sasTTL, + AuthenticationType: authenticationType, + ConnectionString: &connectionStr, + ContainerName: &containerName, + } + + if identityId != "" { + if authenticationType != devices.AuthenticationTypeIdentityBased { + return nil, nil, false, fmt.Errorf("`identity_id` can only be specified when `authentication_type` is `identityBased`") + } + storageEndpointProperties["$default"].Identity = &devices.ManagedIdentity{ + UserAssignedIdentity: &identityId, + } } messagingEndpointProperties["fileNotifications"] = &devices.MessagingEndpointProperties{ @@ -1039,7 +1066,7 @@ func expandIoTHubFileUpload(d *pluginsdk.ResourceData) (map[string]*devices.Stor } } - return storageEndpointProperties, messagingEndpointProperties, notifications + return storageEndpointProperties, messagingEndpointProperties, notifications, nil } func expandIoTHubEndpoints(d *pluginsdk.ResourceData, subscriptionId string) (*devices.RoutingEndpoints, error) { @@ -1289,6 +1316,18 @@ func flattenIoTHubFileUpload(storageEndpoints map[string]*devices.StorageEndpoin output["sas_ttl"] = *sasTTLAsIso8601 } + authenticationType := string(devices.AuthenticationTypeKeyBased) + if v := string(storageEndpointProperties.AuthenticationType); v != "" { + authenticationType = v + } + output["authentication_type"] = authenticationType + + identityId := "" + if storageEndpointProperties.Identity != nil && storageEndpointProperties.Identity.UserAssignedIdentity != nil { + identityId = *storageEndpointProperties.Identity.UserAssignedIdentity + } + output["identity_id"] = identityId + if messagingEndpointProperties, ok := messagingEndpoints["fileNotifications"]; ok { if lockDurationAsIso8601 := messagingEndpointProperties.LockDurationAsIso8601; lockDurationAsIso8601 != nil { output["lock_duration"] = *lockDurationAsIso8601 diff --git a/internal/services/iothub/iothub_resource_test.go b/internal/services/iothub/iothub_resource_test.go index bbd6999430c81..4d05276f94fbf 100644 --- a/internal/services/iothub/iothub_resource_test.go +++ b/internal/services/iothub/iothub_resource_test.go @@ -155,7 +155,14 @@ func TestAccIotHub_fileUpload(t *testing.T) { data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.fileUpload(data), + Config: r.fileUploadBasic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.fileUploadUpdate(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), check.That(data.ResourceName).Key("file_upload.#").HasValue("1"), @@ -166,6 +173,57 @@ func TestAccIotHub_fileUpload(t *testing.T) { }) } +func TestAccIotHub_fileUploadAuthenticationTypeUserAssignedIdentity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub", "test") + r := IotHubResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.fileUploadAuthenticationTypeUserAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccIotHub_fileUploadAuthenticationTypeUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub", "test") + r := IotHubResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.fileUploadAuthenticationTypeDefault(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.fileUploadAuthenticationTypeUserAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.fileUploadAuthenticationTypeSystemAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.fileUploadAuthenticationTypeDefault(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + func TestAccIotHub_withDifferentEndpointResourceGroup(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_iothub", "test") r := IotHubResource{} @@ -376,7 +434,7 @@ func TestAccIotHub_identityUpdate(t *testing.T) { }) } -func TestAccIotHub_AuthenticationTypeUserAssignedIdentity(t *testing.T) { +func TestAccIotHub_endpointAuthenticationTypeUserAssignedIdentity(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_iothub", "test") r := IotHubResource{} @@ -391,7 +449,7 @@ func TestAccIotHub_AuthenticationTypeUserAssignedIdentity(t *testing.T) { }) } -func TestAccIotHub_AuthenticationTypeUpdate(t *testing.T) { +func TestAccIotHub_endpointAuthenticationTypeUpdate(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_iothub", "test") r := IotHubResource{} @@ -928,7 +986,50 @@ resource "azurerm_iothub" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomInteger) } -func (IotHubResource) fileUpload(data acceptance.TestData) string { +func (IotHubResource) fileUploadBasic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_storage_container" "test" { + name = "test" + storage_account_name = azurerm_storage_account.test.name + container_access_type = "private" +} + +resource "azurerm_iothub" "test" { + name = "acctestIoTHub-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + + sku { + name = "S1" + capacity = "1" + } + + file_upload { + connection_string = azurerm_storage_account.test.primary_blob_connection_string + container_name = azurerm_storage_container.test.name + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString, data.RandomInteger) +} + +func (IotHubResource) fileUploadUpdate(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" { features {} @@ -976,6 +1077,155 @@ resource "azurerm_iothub" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomString, data.RandomInteger) } +func (r IotHubResource) fileUploadAuthenticationTypeDefault(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_iothub" "test" { + name = "acctestIoTHub-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + + sku { + name = "S1" + capacity = "1" + } + + file_upload { + connection_string = azurerm_storage_account.test.primary_blob_connection_string + container_name = azurerm_storage_container.test.name + } + + identity { + type = "SystemAssigned, UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id, + ] + } + + depends_on = [ + azurerm_role_assignment.test_storage_blob_data_contrib_user, + ] +} +`, r.fileUploadAuthenticationTypeTemplate(data), data.RandomInteger) +} + +func (r IotHubResource) fileUploadAuthenticationTypeSystemAssignedIdentity(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_iothub" "test" { + name = "acctestIoTHub-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + + sku { + name = "S1" + capacity = "1" + } + + file_upload { + connection_string = azurerm_storage_account.test.primary_blob_connection_string + container_name = azurerm_storage_container.test.name + + authentication_type = "identityBased" + } + + identity { + type = "SystemAssigned, UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id, + ] + } + + depends_on = [ + azurerm_role_assignment.test_storage_blob_data_contrib_user, + ] +} +`, r.fileUploadAuthenticationTypeTemplate(data), data.RandomInteger) +} + +func (r IotHubResource) fileUploadAuthenticationTypeUserAssignedIdentity(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_iothub" "test" { + name = "acctestIoTHub-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + + sku { + name = "S1" + capacity = "1" + } + + file_upload { + connection_string = azurerm_storage_account.test.primary_blob_connection_string + container_name = azurerm_storage_container.test.name + + authentication_type = "identityBased" + identity_id = azurerm_user_assigned_identity.test.id + } + + identity { + type = "SystemAssigned, UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id, + ] + } + + depends_on = [ + azurerm_role_assignment.test_storage_blob_data_contrib_user, + ] +} +`, r.fileUploadAuthenticationTypeTemplate(data), data.RandomInteger) +} + +func (IotHubResource) fileUploadAuthenticationTypeTemplate(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_storage_container" "test" { + name = "test" + storage_account_name = azurerm_storage_account.test.name + container_access_type = "private" +} + +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_role_assignment" "test_storage_blob_data_contrib_user" { + role_definition_name = "Storage Blob Data Contributor" + scope = azurerm_storage_account.test.id + principal_id = azurerm_user_assigned_identity.test.principal_id +} + +resource "azurerm_role_assignment" "test_storage_blob_data_contrib_system" { + role_definition_name = "Storage Blob Data Contributor" + scope = azurerm_storage_account.test.id + principal_id = azurerm_iothub.test.identity[0].principal_id +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString, data.RandomInteger) +} + func (IotHubResource) publicAccessEnabled(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" { diff --git a/internal/services/iothub/parse/file_upload.go b/internal/services/iothub/parse/file_upload.go new file mode 100644 index 0000000000000..17709a9af2ef8 --- /dev/null +++ b/internal/services/iothub/parse/file_upload.go @@ -0,0 +1,75 @@ +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 FileUploadId struct { + SubscriptionId string + ResourceGroup string + IotHubName string + FileUploadName string +} + +func NewFileUploadID(subscriptionId, resourceGroup, iotHubName, fileUploadName string) FileUploadId { + return FileUploadId{ + SubscriptionId: subscriptionId, + ResourceGroup: resourceGroup, + IotHubName: iotHubName, + FileUploadName: fileUploadName, + } +} + +func (id FileUploadId) String() string { + segments := []string{ + fmt.Sprintf("File Upload Name %q", id.FileUploadName), + fmt.Sprintf("Iot Hub Name %q", id.IotHubName), + fmt.Sprintf("Resource Group %q", id.ResourceGroup), + } + segmentsStr := strings.Join(segments, " / ") + return fmt.Sprintf("%s: (%s)", "File Upload", segmentsStr) +} + +func (id FileUploadId) ID() string { + fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Devices/IotHubs/%s/FileUpload/%s" + return fmt.Sprintf(fmtString, id.SubscriptionId, id.ResourceGroup, id.IotHubName, id.FileUploadName) +} + +// FileUploadID parses a FileUpload ID into an FileUploadId struct +func FileUploadID(input string) (*FileUploadId, error) { + id, err := resourceids.ParseAzureResourceID(input) + if err != nil { + return nil, err + } + + resourceId := FileUploadId{ + 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.FileUploadName, err = id.PopSegment("FileUpload"); 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/file_upload_test.go b/internal/services/iothub/parse/file_upload_test.go new file mode 100644 index 0000000000000..32af5511797e3 --- /dev/null +++ b/internal/services/iothub/parse/file_upload_test.go @@ -0,0 +1,128 @@ +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 = FileUploadId{} + +func TestFileUploadIDFormatter(t *testing.T) { + actual := NewFileUploadID("12345678-1234-9876-4563-123456789012", "resGroup1", "hub1", "default").ID() + expected := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/IotHubs/hub1/FileUpload/default" + if actual != expected { + t.Fatalf("Expected %q but got %q", expected, actual) + } +} + +func TestFileUploadID(t *testing.T) { + testData := []struct { + Input string + Error bool + Expected *FileUploadId + }{ + + { + // 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 FileUploadName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/IotHubs/hub1/", + Error: true, + }, + + { + // missing value for FileUploadName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/IotHubs/hub1/FileUpload/", + Error: true, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/IotHubs/hub1/FileUpload/default", + Expected: &FileUploadId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + IotHubName: "hub1", + FileUploadName: "default", + }, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.DEVICES/IOTHUBS/HUB1/FILEUPLOAD/DEFAULT", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Input) + + actual, err := FileUploadID(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.FileUploadName != v.Expected.FileUploadName { + t.Fatalf("Expected %q but got %q for FileUploadName", v.Expected.FileUploadName, actual.FileUploadName) + } + } +} diff --git a/internal/services/iothub/registration.go b/internal/services/iothub/registration.go index c8310aaf84837..3fc4e24be5a1d 100644 --- a/internal/services/iothub/registration.go +++ b/internal/services/iothub/registration.go @@ -45,6 +45,7 @@ func (r Registration) SupportedResources() map[string]*pluginsdk.Resource { "azurerm_iothub_consumer_group": resourceIotHubConsumerGroup(), "azurerm_iothub": resourceIotHub(), "azurerm_iothub_fallback_route": resourceIotHubFallbackRoute(), + "azurerm_iothub_file_upload": resourceIotHubFileUpload(), "azurerm_iothub_enrichment": resourceIotHubEnrichment(), "azurerm_iothub_route": resourceIotHubRoute(), "azurerm_iothub_endpoint_eventhub": resourceIotHubEndpointEventHub(), diff --git a/internal/services/iothub/resourceids.go b/internal/services/iothub/resourceids.go index 421d6ead9cfc1..988645dc9a41d 100644 --- a/internal/services/iothub/resourceids.go +++ b/internal/services/iothub/resourceids.go @@ -14,3 +14,4 @@ package iothub //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=DpsCertificate -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/provisioningServices/provisioningService1/certificates/certificate1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=DpsSharedAccessPolicy -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/provisioningServices/provisioningService1/keys/sharedAccessPolicy1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=IotHubCertificate -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/IotHubs/hub1/Certificates/cert1 +//go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=FileUpload -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/IotHubs/hub1/FileUpload/default diff --git a/internal/services/iothub/validate/file_upload_id.go b/internal/services/iothub/validate/file_upload_id.go new file mode 100644 index 0000000000000..ccc4dd07b20a1 --- /dev/null +++ b/internal/services/iothub/validate/file_upload_id.go @@ -0,0 +1,23 @@ +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 FileUploadID(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.FileUploadID(v); err != nil { + errors = append(errors, err) + } + + return +} diff --git a/internal/services/iothub/validate/file_upload_id_test.go b/internal/services/iothub/validate/file_upload_id_test.go new file mode 100644 index 0000000000000..9aaddbcde9b94 --- /dev/null +++ b/internal/services/iothub/validate/file_upload_id_test.go @@ -0,0 +1,88 @@ +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import "testing" + +func TestFileUploadID(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 FileUploadName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/IotHubs/hub1/", + Valid: false, + }, + + { + // missing value for FileUploadName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/IotHubs/hub1/FileUpload/", + Valid: false, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Devices/IotHubs/hub1/FileUpload/default", + Valid: true, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.DEVICES/IOTHUBS/HUB1/FILEUPLOAD/DEFAULT", + Valid: false, + }, + } + for _, tc := range cases { + t.Logf("[DEBUG] Testing Value %s", tc.Input) + _, errors := FileUploadID(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.html.markdown b/website/docs/r/iothub.html.markdown index 5de5eadac33dd..90db48c8e5958 100644 --- a/website/docs/r/iothub.html.markdown +++ b/website/docs/r/iothub.html.markdown @@ -18,6 +18,8 @@ Manages an IotHub ~> **NOTE:** Fallback route can be defined either directly on the `azurerm_iothub` resource, or using the `azurerm_iothub_fallback_route` resource - but the two cannot be used together. If both are used against the same IoTHub, spurious changes will occur. +~> **NOTE:** File Upload can be defined either directly on the `azurerm_iothub` resource, or using the `azurerm_iothub_file_upload` resource - but the two cannot be used together. If both are used against the same IoTHub, spurious changes will occur. + ## Example Usage ```hcl @@ -270,6 +272,14 @@ A `fallback_route` block supports the following: A `file_upload` block supports the following: +* `authentication_type` - (Optional) Type used to authenticate against the storage account. Possible values are `keyBased` and `identityBased`. Defaults to `keyBased`. + +* `identity_id` - (Optional) ID of the User Managed Identity used to authenticate against the storage account. + +-> **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. + +~> **NOTE:** System Assigned Managed Identity can only be used in an update because access to the endpoint cannot be granted before the creation is done. The extracted resources `azurerm_iothub_file_upload` can be used to configure File Upload with System Assigned Managed Identity without the need of an update. + * `connection_string` - (Required) The connection string for the Azure Storage account to which files are uploaded. * `container_name` - (Required) The name of the root container where you upload files. The container need not exist but should be creatable using the connection_string specified. diff --git a/website/docs/r/iothub_file_upload.html.markdown b/website/docs/r/iothub_file_upload.html.markdown new file mode 100644 index 0000000000000..b0e3e29689d61 --- /dev/null +++ b/website/docs/r/iothub_file_upload.html.markdown @@ -0,0 +1,77 @@ +--- +subcategory: "IoT Hub" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_iothub_file_upload" +description: |- + Manages a IotHub File Upload. +--- + +# azurerm_iothub_file_upload + +Manages a IotHub File Upload. + +~> **NOTE:** File Upload can be defined either directly on the `azurerm_iothub` resource, or using the `azurerm_iothub_file_upload` resource - but the two cannot be used together. If both are used against the same IoTHub, spurious changes will occur. + +~> **Note:** Since this resource is provisioned by default, the Azure Provider will not check for the presence of an existing resource prior to attempting to create it. + +## Example Usage + +```hcl +resource "azurerm_iothub_file_upload" "example" { + connection_string = "TODO" + container_name = "example" + iothub_id = "TODO" +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `connection_string` - (Required) The connection string for the Azure Storage account to which files are uploaded. + +* `container_name` - (Required) The name of the root container where you upload files. The container need not exist but should be creatable using the connection_string specified. + +* `iothub_id` - (Required) The ID of the IoTHub. Changing this forces a new IotHub File Upload to be created. + +--- + +* `authentication_type` - (Optional) Type used to authenticate against the storage account. Possible values are `keyBased` and `identityBased`. Defaults to `keyBased`. + +* `default_ttl` - (Optional) The period of time for which a file upload notification message is available to consume before it is expired by the IoT hub, specified as an [ISO 8601 timespan duration](https://en.wikipedia.org/wiki/ISO_8601#Durations). This value must be between 1 minute and 48 hours, and evaluates to `PT1H` by default. + +* `identity_id` - (Optional) ID of the User Managed Identity used to authenticate against the storage account. + +-> **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. + +* `lock_duration` - (Optional) The lock duration for the file upload notifications queue, specified as an [ISO 8601 timespan duration](https://en.wikipedia.org/wiki/ISO_8601#Durations). This value must be between 5 and 300 seconds, and evaluates to `PT1M` by default. + +* `max_delivery_count` - (Optional) The number of times the IoT hub attempts to deliver a file upload notification message. It evaluates to `10` by default. + +* `notifications` - (Optional) Used to specify whether file notifications are sent to IoT Hub on upload. It evaluates to false by default. + +* `sas_ttl` - (Optional) The period of time for which the SAS URI generated by IoT Hub for file upload is valid, specified as an [ISO 8601 timespan duration](https://en.wikipedia.org/wiki/ISO_8601#Durations). This value must be between 1 minute and 24 hours, and evaluates to `PT1H` by default. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the IotHub File Upload. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the IotHub File Upload. +* `update` - (Defaults to 30 minutes) Used when updating the IotHub File Upload. +* `read` - (Defaults to 5 minutes) Used when retrieving the IotHub File Upload. +* `delete` - (Defaults to 30 minutes) Used when deleting the IotHub File Upload. + +## Import + +IotHub File Uploads can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_iothub_file_upload.file_upload1 /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.Devices/IotHubs/hub1/FileUpload/default +``` +~> **NOTE:** As there may only be a single fallback route per IoTHub, the id always ends with `/FileUpload/default`.