diff --git a/azurerm/resource_arm_iothub.go b/azurerm/resource_arm_iothub.go index 40c5e084cd9c..8623ae0ac090 100755 --- a/azurerm/resource_arm_iothub.go +++ b/azurerm/resource_arm_iothub.go @@ -161,6 +161,64 @@ func resourceArmIotHub() *schema.Resource { }, }, + "file_upload": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "connection_string": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + secretKeyRegex := regexp.MustCompile("(SharedAccessKey|AccountKey)=[^;]+") + sbProtocolRegex := regexp.MustCompile("sb://([^:]+)(:5671)?/;") + + // Azure will always mask the Access Keys and will include the port number in the GET response + // 5671 is the default port for Azure Service Bus connections + maskedNew := sbProtocolRegex.ReplaceAllString(new, "sb://$1:5671/;") + maskedNew = secretKeyRegex.ReplaceAllString(maskedNew, "$1=****") + return (new == d.Get(k).(string)) && (maskedNew == old) + }, + Sensitive: true, + }, + "container_name": { + Type: schema.TypeString, + Required: true, + }, + "notifications": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "max_delivery_count": { + Type: schema.TypeInt, + Optional: true, + Default: 10, + ValidateFunc: validation.IntBetween(1, 100), + }, + "sas_ttl": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validateIso8601Duration(), + }, + "default_ttl": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validateIso8601Duration(), + }, + "lock_duration": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validateIso8601Duration(), + }, + }, + }, + }, + "endpoint": { Type: schema.TypeList, Optional: true, @@ -407,6 +465,11 @@ func resourceArmIotHubCreateUpdate(d *schema.ResourceData, meta interface{}) err return fmt.Errorf("Error expanding `endpoint`: %+v", err) } + storageEndpoints, messagingEndpoints, enableFileUploadNotifications, err := expandIoTHubFileUpload(d) + if err != nil { + return fmt.Errorf("Error expanding `file_upload`: %+v", err) + } + routes := expandIoTHubRoutes(d) ipFilterRules := expandIPFilterRules(d) @@ -421,6 +484,9 @@ func resourceArmIotHubCreateUpdate(d *schema.ResourceData, meta interface{}) err Routes: routes, FallbackRoute: fallbackRoute, }, + StorageEndpoints: storageEndpoints, + MessagingEndpoints: messagingEndpoints, + EnableFileUploadNotifications: &enableFileUploadNotifications, }, Tags: expandTags(tags), } @@ -517,6 +583,10 @@ func resourceArmIotHubRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("Error setting `ip_filter_rule` in IoTHub %q: %+v", name, err) } + fileUpload := flattenIoTHubFileUpload(properties.StorageEndpoints, properties.MessagingEndpoints, properties.EnableFileUploadNotifications) + if err := d.Set("file_upload", fileUpload); err != nil { + return fmt.Errorf("Error setting `file_upload` in IoTHub %q: %+v", name, err) + } } d.Set("name", name) @@ -621,6 +691,40 @@ func expandIoTHubRoutes(d *schema.ResourceData) *[]devices.RouteProperties { return &routeProperties } +func expandIoTHubFileUpload(d *schema.ResourceData) (map[string]*devices.StorageEndpointProperties, map[string]*devices.MessagingEndpointProperties, bool, error) { + fileUploadList := d.Get("file_upload").([]interface{}) + + storageEndpointProperties := make(map[string]*devices.StorageEndpointProperties) + messagingEndpointProperties := make(map[string]*devices.MessagingEndpointProperties) + notifications := false + + if len(fileUploadList) > 0 { + fileUploadMap := fileUploadList[0].(map[string]interface{}) + + connectionStr := fileUploadMap["connection_string"].(string) + containerName := fileUploadMap["container_name"].(string) + notifications = fileUploadMap["notifications"].(bool) + maxDeliveryCount := int32(fileUploadMap["max_delivery_count"].(int)) + sasTTL := fileUploadMap["sas_ttl"].(string) + defaultTTL := fileUploadMap["default_ttl"].(string) + lockDuration := fileUploadMap["lock_duration"].(string) + + storageEndpointProperties["$default"] = &devices.StorageEndpointProperties{ + SasTTLAsIso8601: &sasTTL, + ConnectionString: &connectionStr, + ContainerName: &containerName, + } + + messagingEndpointProperties["fileNotifications"] = &devices.MessagingEndpointProperties{ + LockDurationAsIso8601: &lockDuration, + TTLAsIso8601: &defaultTTL, + MaxDeliveryCount: &maxDeliveryCount, + } + } + + return storageEndpointProperties, messagingEndpointProperties, notifications, nil +} + func expandIoTHubEndpoints(d *schema.ResourceData, subscriptionId string) (*devices.RoutingEndpoints, error) { routeEndpointList := d.Get("endpoint").([]interface{}) @@ -770,6 +874,43 @@ func flattenIoTHubSharedAccessPolicy(input *[]devices.SharedAccessSignatureAutho return results } +func flattenIoTHubFileUpload(storageEndpoints map[string]*devices.StorageEndpointProperties, messagingEndpoints map[string]*devices.MessagingEndpointProperties, enableFileUploadNotifications *bool) []interface{} { + results := make([]interface{}, 0) + output := make(map[string]interface{}) + + if storageEndpointProperties, ok := storageEndpoints["$default"]; ok { + if connString := storageEndpointProperties.ConnectionString; connString != nil { + output["connection_string"] = *connString + } + if containerName := storageEndpointProperties.ContainerName; containerName != nil { + output["container_name"] = *containerName + } + if sasTTLAsIso8601 := storageEndpointProperties.SasTTLAsIso8601; sasTTLAsIso8601 != nil { + output["sas_ttl"] = *sasTTLAsIso8601 + } + + if messagingEndpointProperties, ok := messagingEndpoints["fileNotifications"]; ok { + if lockDurationAsIso8601 := messagingEndpointProperties.LockDurationAsIso8601; lockDurationAsIso8601 != nil { + output["lock_duration"] = *lockDurationAsIso8601 + } + if ttlAsIso8601 := messagingEndpointProperties.TTLAsIso8601; ttlAsIso8601 != nil { + output["default_ttl"] = *ttlAsIso8601 + } + if maxDeliveryCount := messagingEndpointProperties.MaxDeliveryCount; maxDeliveryCount != nil { + output["max_delivery_count"] = *maxDeliveryCount + } + } + + if enableFileUploadNotifications != nil { + output["notifications"] = *enableFileUploadNotifications + } + + results = append(results, output) + } + + return results +} + func flattenIoTHubEndpoint(input *devices.RoutingProperties) []interface{} { results := make([]interface{}, 0) diff --git a/azurerm/resource_arm_iothub_test.go b/azurerm/resource_arm_iothub_test.go index 2a122d2da913..5f2ce472904e 100644 --- a/azurerm/resource_arm_iothub_test.go +++ b/azurerm/resource_arm_iothub_test.go @@ -141,6 +141,33 @@ func TestAccAzureRMIotHub_customRoutes(t *testing.T) { }) } +func TestAccAzureRMIotHub_fileUpload(t *testing.T) { + resourceName := "azurerm_iothub.test" + rInt := tf.AccRandTimeInt() + rStr := acctest.RandString(5) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMIotHubDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMIotHub_fileUpload(rInt, rStr, testLocation()), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMIotHubExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "file_upload.#", "1"), + resource.TestCheckResourceAttr(resourceName, "file_upload.0.lock_duration", "PT5M"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccAzureRMIotHub_fallbackRoute(t *testing.T) { resourceName := "azurerm_iothub.test" rInt := tf.AccRandTimeInt() @@ -452,3 +479,49 @@ resource "azurerm_iothub" "test" { } `, rInt, location, rInt) } + +func testAccAzureRMIotHub_fileUpload(rInt int, rStr string, location string) string { + return fmt.Sprintf(` +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" + resource_group_name = "${azurerm_resource_group.test.name}" + 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" + tier = "Standard" + capacity = "1" + } + + file_upload { + connection_string = "${azurerm_storage_account.test.primary_blob_connection_string}" + container_name = "${azurerm_storage_container.test.name}" + notifications = true + max_delivery_count = 12 + sas_ttl = "PT2H" + default_ttl = "PT3H" + lock_duration = "PT5M" + } +} +`, rInt, location, rStr, rInt) +} diff --git a/website/docs/r/iothub.html.markdown b/website/docs/r/iothub.html.markdown index 12c6a345fcc8..b41347637c4c 100644 --- a/website/docs/r/iothub.html.markdown +++ b/website/docs/r/iothub.html.markdown @@ -67,6 +67,16 @@ resource "azurerm_iothub" "test" { enabled = true } + file_upload { + connection_string = "${azurerm_storage_account.test.primary_blob_connection_string}" + container_name = "${azurerm_storage_container.test.name}" + sas_ttl = "PT1H" + notifications = true + lock_duration = "PT1M" + default_ttl = "PT1H" + max_delivery_count = 10 + } + tags = { purpose = "testing" } @@ -93,6 +103,8 @@ The following arguments are supported: * `fallback_route` - (Optional) A `fallback_route` block as defined below. If the fallback route is enabled, messages that don't match any of the supplied routes are automatically sent to this route. Defaults to messages/events. +* `file_upload` - (Optional) A `file_upload` block as defined below. + * `tags` - (Optional) A mapping of tags to assign to the resource. --- @@ -163,6 +175,25 @@ A `fallback_route` block supports the following: * `enabled` - (Optional) Used to specify whether the fallback route is enabled. +--- + +A `file_upload` block supports the following: + +* `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. + +* `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. + +* `notifications` - (Optional) Used to specify whether file notifications are sent to IoT Hub on upload. It evaluates to false by default. + +* `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. + +* `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. + +* `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. + + ## Attributes Reference The following attributes are exported: