diff --git a/azurerm/resource_arm_iothub.go b/azurerm/resource_arm_iothub.go index aa8aa71bafb0..414c913379f5 100644 --- a/azurerm/resource_arm_iothub.go +++ b/azurerm/resource_arm_iothub.go @@ -7,12 +7,14 @@ import ( "strconv" "time" + "github.com/Azure/azure-sdk-for-go/services/eventhub/mgmt/2017-04-01/eventhub" "github.com/Azure/azure-sdk-for-go/services/iothub/mgmt/2018-04-01/devices" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/validation" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/response" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" + "strings" ) func resourceArmIotHub() *schema.Resource { @@ -114,6 +116,115 @@ func resourceArmIotHub() *schema.Resource { }, }, + "endpoint": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "AzureIotHub.StorageContainer", + "AzureIotHub.ServiceBusQueue", + "AzureIotHub.ServiceBusTopic", + "AzureIotHub.EventHub", + }, false), + }, + "connection_string": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // As Azure API masks the connection string key suppress diff for this property + if old != "" && strings.HasSuffix(old, "****") { + return true + } + + return false + }, + }, + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateIoTHubEndpointName, + }, + "batch_frequency_in_seconds": { + Type: schema.TypeInt, + Optional: true, + Default: 300, + ValidateFunc: validation.IntBetween(60, 720), + }, + "max_chunk_size_in_bytes": { + Type: schema.TypeInt, + Optional: true, + Default: 314572800, + ValidateFunc: validation.IntBetween(10485760, 524288000), + }, + "container_name": { + Type: schema.TypeString, + Optional: true, + }, + "encoding": { + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: ignoreCaseDiffSuppressFunc, + ValidateFunc: validation.StringInSlice([]string{ + string(eventhub.Avro), + string(eventhub.AvroDeflate), + }, true), + }, + "file_name_format": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateIoTHubFileNameFormat, + }, + }, + }, + }, + + "route": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(0, 64), + }, + "source": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "DeviceJobLifecycleEvents", + "DeviceLifecycleEvents", + "DeviceMessages", + "Invalid", + "TwinChangeEvents", + }, false), + }, + "condition": { + // The condition is a string value representing device-to-cloud message routes query expression + // https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-query-language#device-to-cloud-message-routes-query-expressions + Type: schema.TypeString, + Optional: true, + Default: "true", + }, + "endpoint_names": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Required: true, + }, + "enabled": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "tags": tagsSchema(), }, } @@ -123,6 +234,7 @@ func resourceArmIotHub() *schema.Resource { func resourceArmIotHubCreateAndUpdate(d *schema.ResourceData, meta interface{}) error { client := meta.(*ArmClient).iothubResourceClient ctx := meta.(*ArmClient).StopContext + subscriptionID := meta.(*ArmClient).subscriptionId name := d.Get("name").(string) resourceGroup := d.Get("resource_group_name").(string) @@ -145,11 +257,28 @@ func resourceArmIotHubCreateAndUpdate(d *schema.ResourceData, meta interface{}) skuInfo := expandIoTHubSku(d) tags := d.Get("tags").(map[string]interface{}) + endpoints, err := expandIoTHubEndpoints(d, subscriptionID) + if err != nil { + return fmt.Errorf("Error expanding `endpoint`: %+v", err) + } + + routes := expandIoTHubRoutes(d) + + routingProperties := devices.RoutingProperties{ + Endpoints: endpoints, + Routes: routes, + } + + iotHubProperties := devices.IotHubProperties{ + Routing: &routingProperties, + } + properties := devices.IotHubDescription{ - Name: utils.String(name), - Location: utils.String(location), - Sku: &skuInfo, - Tags: expandTags(tags), + Name: utils.String(name), + Location: utils.String(location), + Sku: &skuInfo, + Tags: expandTags(tags), + Properties: &iotHubProperties, } future, err := client.CreateOrUpdate(ctx, resourceGroup, name, properties, "") @@ -207,6 +336,16 @@ func resourceArmIotHubRead(d *schema.ResourceData, meta interface{}) error { if properties := hub.Properties; properties != nil { d.Set("hostname", properties.HostName) + + endpoints := flattenIoTHubEndpoint(properties.Routing) + if err := d.Set("endpoint", endpoints); err != nil { + return fmt.Errorf("Error flattening `endpoint` in IoTHub %q: %+v", name, err) + } + + routes := flattenIoTHubRoute(properties.Routing) + if err := d.Set("route", routes); err != nil { + return fmt.Errorf("Error flattening `route` in IoTHub %q: %+v", name, err) + } } d.Set("name", name) @@ -280,6 +419,114 @@ func iothubStateStatusCodeRefreshFunc(ctx context.Context, client devices.IotHub } } +func expandIoTHubRoutes(d *schema.ResourceData) *[]devices.RouteProperties { + routeList := d.Get("route").([]interface{}) + + routeProperties := make([]devices.RouteProperties, 0) + + for _, routeRaw := range routeList { + route := routeRaw.(map[string]interface{}) + + name := route["name"].(string) + source := devices.RoutingSource(route["source"].(string)) + condition := route["condition"].(string) + + endpointNamesRaw := route["endpoint_names"].([]interface{}) + endpointsNames := make([]string, 0) + for _, n := range endpointNamesRaw { + endpointsNames = append(endpointsNames, n.(string)) + } + + isEnabled := route["enabled"].(bool) + + routeProperties = append(routeProperties, devices.RouteProperties{ + Name: &name, + Source: source, + Condition: &condition, + EndpointNames: &endpointsNames, + IsEnabled: &isEnabled, + }) + } + + return &routeProperties +} + +func expandIoTHubEndpoints(d *schema.ResourceData, subscriptionId string) (*devices.RoutingEndpoints, error) { + routeEndpointList := d.Get("endpoint").([]interface{}) + + serviceBusQueueEndpointProperties := make([]devices.RoutingServiceBusQueueEndpointProperties, 0) + serviceBusTopicEndpointProperties := make([]devices.RoutingServiceBusTopicEndpointProperties, 0) + eventHubProperties := make([]devices.RoutingEventHubProperties, 0) + storageContainerProperties := make([]devices.RoutingStorageContainerProperties, 0) + + for _, endpointRaw := range routeEndpointList { + endpoint := endpointRaw.(map[string]interface{}) + + t := endpoint["type"] + connectionStr := endpoint["connection_string"].(string) + name := endpoint["name"].(string) + subscriptionID := subscriptionId + resourceGroup := d.Get("resource_group_name").(string) + + switch t { + case "AzureIotHub.StorageContainer": + containerName := endpoint["container_name"].(string) + fileNameFormat := endpoint["file_name_format"].(string) + batchFrequencyInSeconds := int32(endpoint["batch_frequency_in_seconds"].(int)) + maxChunkSizeInBytes := int32(endpoint["max_chunk_size_in_bytes"].(int)) + encoding := endpoint["encoding"].(string) + + storageContainer := devices.RoutingStorageContainerProperties{ + ConnectionString: &connectionStr, + Name: &name, + SubscriptionID: &subscriptionID, + ResourceGroup: &resourceGroup, + ContainerName: &containerName, + FileNameFormat: &fileNameFormat, + BatchFrequencyInSeconds: &batchFrequencyInSeconds, + MaxChunkSizeInBytes: &maxChunkSizeInBytes, + Encoding: &encoding, + } + storageContainerProperties = append(storageContainerProperties, storageContainer) + break + case "AzureIotHub.ServiceBusQueue": + sbQueue := devices.RoutingServiceBusQueueEndpointProperties{ + ConnectionString: &connectionStr, + Name: &name, + SubscriptionID: &subscriptionID, + ResourceGroup: &resourceGroup, + } + serviceBusQueueEndpointProperties = append(serviceBusQueueEndpointProperties, sbQueue) + break + case "AzureIotHub.ServiceBusTopic": + sbTopic := devices.RoutingServiceBusTopicEndpointProperties{ + ConnectionString: &connectionStr, + Name: &name, + SubscriptionID: &subscriptionID, + ResourceGroup: &resourceGroup, + } + serviceBusTopicEndpointProperties = append(serviceBusTopicEndpointProperties, sbTopic) + break + case "AzureIotHub.EventHub": + eventHub := devices.RoutingEventHubProperties{ + ConnectionString: &connectionStr, + Name: &name, + SubscriptionID: &subscriptionID, + ResourceGroup: &resourceGroup, + } + eventHubProperties = append(eventHubProperties, eventHub) + break + } + } + + return &devices.RoutingEndpoints{ + ServiceBusQueues: &serviceBusQueueEndpointProperties, + ServiceBusTopics: &serviceBusTopicEndpointProperties, + EventHubs: &eventHubProperties, + StorageContainers: &storageContainerProperties, + }, nil +} + func expandIoTHubSku(d *schema.ResourceData) devices.IotHubSkuInfo { skuList := d.Get("sku").([]interface{}) skuMap := skuList[0].(map[string]interface{}) @@ -333,3 +580,157 @@ func flattenIoTHubSharedAccessPolicy(input *[]devices.SharedAccessSignatureAutho return results } + +func flattenIoTHubEndpoint(input *devices.RoutingProperties) []interface{} { + results := make([]interface{}, 0) + + if input != nil && input.Endpoints != nil { + + if containers := input.Endpoints.StorageContainers; containers != nil { + for _, container := range *containers { + output := make(map[string]interface{}, 0) + + if connString := container.ConnectionString; connString != nil { + output["connection_string"] = *connString + } + if name := container.Name; name != nil { + output["name"] = *name + } + if containerName := container.ContainerName; containerName != nil { + output["container_name"] = *containerName + } + if fileNameFmt := container.FileNameFormat; fileNameFmt != nil { + output["file_name_format"] = *fileNameFmt + } + if batchFreq := container.BatchFrequencyInSeconds; batchFreq != nil { + output["batch_frequency_in_seconds"] = *batchFreq + } + if chunkSize := container.MaxChunkSizeInBytes; chunkSize != nil { + output["max_chunk_size_in_bytes"] = *chunkSize + } + if encoding := container.Encoding; encoding != nil { + output["encoding"] = *encoding + } + output["type"] = "AzureIotHub.StorageContainer" + + results = append(results, output) + } + } + + if queues := input.Endpoints.ServiceBusQueues; queues != nil { + for _, queue := range *queues { + output := make(map[string]interface{}, 0) + + if connString := queue.ConnectionString; connString != nil { + output["connection_string"] = *connString + } + if name := queue.Name; name != nil { + output["name"] = *name + } + + results = append(results, output) + } + } + + if topics := input.Endpoints.ServiceBusTopics; topics != nil { + for _, topic := range *topics { + output := make(map[string]interface{}, 0) + + if connString := topic.ConnectionString; connString != nil { + output["connection_string"] = *connString + } + if name := topic.Name; name != nil { + output["name"] = *name + } + + results = append(results, output) + } + } + + if eventHubs := input.Endpoints.EventHubs; eventHubs != nil { + for _, eventHub := range *eventHubs { + output := make(map[string]interface{}, 0) + + if connString := eventHub.ConnectionString; connString != nil { + output["connection_string"] = *connString + } + if name := eventHub.Name; name != nil { + output["name"] = *name + } + + results = append(results, output) + } + } + } + + return results +} + +func flattenIoTHubRoute(input *devices.RoutingProperties) []interface{} { + results := make([]interface{}, 0) + + if input != nil && input.Routes != nil { + for _, route := range *input.Routes { + output := make(map[string]interface{}, 0) + + if name := route.Name; name != nil { + output["name"] = *name + } + if condition := route.Condition; condition != nil { + output["condition"] = *condition + } + if endpointNames := route.EndpointNames; endpointNames != nil { + output["endpoint_names"] = *endpointNames + } + if isEnabled := route.IsEnabled; isEnabled != nil { + output["enabled"] = *isEnabled + } + output["source"] = route.Source + + results = append(results, output) + } + } + + return results +} + +func validateIoTHubEndpointName(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + reservedNames := []string{ + "events", + "operationsMonitoringEvents", + "fileNotifications", + "$default", + } + + for _, name := range reservedNames { + if name == value { + errors = append(errors, fmt.Errorf("The reserved endpoint name %s could not be used as a name for a custom endpoint", name)) + } + } + + return +} + +func validateIoTHubFileNameFormat(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + requiredComponents := []string{ + "{iothub}", + "{partition}", + "{YYYY}", + "{MM}", + "{DD}", + "{HH}", + "{mm}", + } + + for _, component := range requiredComponents { + if !strings.Contains(value, component) { + errors = append(errors, fmt.Errorf("%s needs to contain %q", k, component)) + } + } + + return +} diff --git a/azurerm/resource_arm_iothub_test.go b/azurerm/resource_arm_iothub_test.go index 91e394cc6d79..3116bd4494e4 100644 --- a/azurerm/resource_arm_iothub_test.go +++ b/azurerm/resource_arm_iothub_test.go @@ -46,6 +46,25 @@ func TestAccAzureRMIotHub_standard(t *testing.T) { }) } +func TestAccAzureRMIotHub_customRoutes(t *testing.T) { + rInt := acctest.RandInt() + rStr := acctest.RandString(5) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMIotHubDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMIotHub_customRoutes(rInt, rStr, testLocation()), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMIotHubExists("azurerm_iothub.test"), + ), + }, + }, + }) +} + func testCheckAzureRMIotHubDestroy(s *terraform.State) error { client := testAccProvider.Meta().(*ArmClient).iothubResourceClient ctx := testAccProvider.Meta().(*ArmClient).StopContext @@ -147,3 +166,61 @@ resource "azurerm_iothub" "test" { } `, rInt, location, rInt) } + +func testAccAzureRMIotHub_customRoutes(rInt int, rStr string, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "foo" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%s" + resource_group_name = "${azurerm_resource_group.foo.name}" + location = "${azurerm_resource_group.foo.location}" + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_storage_container" "test" { + name = "test" + resource_group_name = "${azurerm_resource_group.foo.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.foo.name}" + location = "${azurerm_resource_group.foo.location}" + sku { + name = "S1" + tier = "Standard" + capacity = "1" + } + + endpoint { + type = "AzureIotHub.StorageContainer" + connection_string = "${azurerm_storage_account.test.primary_blob_connection_string}" + name = "export" + batch_frequency_in_seconds = 60 + max_chunk_size_in_bytes = 10485760 + container_name = "test" + encoding = "Avro" + file_name_format = "{iothub}/{partition}_{YYYY}_{MM}_{DD}_{HH}_{mm}" + } + + route { + name = "export" + source = "DeviceMessages" + condition = "true" + endpoint_names = ["export"] + enabled = true + } + + tags { + "purpose" = "testing" + } +} +`, rInt, location, rStr, rInt) +} diff --git a/website/docs/r/iothub.html.markdown b/website/docs/r/iothub.html.markdown index fa878fa6e762..d22ccb8ef07a 100644 --- a/website/docs/r/iothub.html.markdown +++ b/website/docs/r/iothub.html.markdown @@ -3,7 +3,7 @@ layout: "azurerm" page_title: "Azure Resource Manager: azurerm_iothub" sidebar_current: "docs-azurerm-resource-messaging-iothub" description: |- - Manages a IotHub resource + Manages a IotHub resource --- # azurerm_iothub @@ -18,6 +18,21 @@ resource "azurerm_resource_group" "test" { location = "West US" } +resource "azurerm_storage_account" "test" { + name = "teststa" + 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 = "test" resource_group_name = "${azurerm_resource_group.test.name}" @@ -28,6 +43,25 @@ resource "azurerm_iothub" "test" { capacity = "1" } + endpoint { + type = "AzureIotHub.StorageContainers" + connection_string = "${azurerm_storage_account.test.primary_blob_connection_string}" + name = "export" + batch_frequency_in_seconds = 60 + max_chunk_size_in_bytes = 10485760 + container_name = "test" + encoding = "Avro" + file_name_format = "{iothub}/{partition}_{YYYY}_{MM}_{DD}_{HH}_{mm}" + } + + route { + name = "export" + source = "DeviceMessages" + condition = "true" + endpoint_names = ["export"] + is_enabled = true + } + tags { "purpose" = "testing" } @@ -44,7 +78,11 @@ The following arguments are supported: * `location` - (Required) Specifies the supported Azure location where the resource has to be createc. Changing this forces a new resource to be created. -* `sku` - (Required) A `sku` block as defined below. +* `sku` - (Required) A `sku` block as defined below. + +* `endpoint` - (Optional) An `endpoint` block as defined below. + +* `route` - (Optional) A `route` block as defined below. * `tags` - (Optional) A mapping of tags to assign to the resource. @@ -56,7 +94,41 @@ A `sku` block supports the following: * `tier` - (Required) The billing tier for the IoT Hub. Possible values are `Basic`, `Free` or `Standard`. -* `capacity` - (Required) The number of provisioned IoT Hub units. +* `capacity` - (Required) The number of provisioned IoT Hub units. + +--- + +An `endpoint` block supports the following: + +* `type` - (Required) The type of the endpoint. Possible values are `AzureIotHub.StorageContainer`, `AzureIotHub.ServiceBusQueue`, `AzureIotHub.ServiceBusTopic` or `AzureIotHub.EventHub`. + +* `connection_string` - (Required) The connection string for the endpoint. + +* `name` - (Required) The name of the endpoint. The name must be unique across endpoint types. The following names are reserved: `events`, `operationsMonitoringEvents`, `fileNotifications` and `$default`. + +* `batch_frequency_in_seconds` - (Optional) Time interval at which blobs are written to storage. Value should be between 60 and 720 seconds. Default value is 300 seconds. This attribute is mandatory for endpoint type `AzureIotHub.StorageContainer`. + +* `max_chunk_size_in_bytes` - (Optional) Maximum number of bytes for each blob written to storage. Value should be between 10485760(10MB) and 524288000(500MB). Default value is 314572800(300MB). This attribute is mandatory for endpoint type `AzureIotHub.StorageContainer`. + +* `container_name` - (Optional) The name of storage container in the storage account. This attribute is mandatory for endpoint type `AzureIotHub.StorageContainer`. + +* `encoding` - (Optional) Encoding that is used to serialize messages to blobs. Supported values are 'avro' and 'avrodeflate'. Default value is 'avro'. This attribute is mandatory for endpoint type `AzureIotHub.StorageContainer`. + +* `file_name_format` - (Optional) File name format for the blob. Default format is ``{iothub}/{partition}/{YYYY}/{MM}/{DD}/{HH}/{mm}``. All parameters are mandatory but can be reordered. This attribute is mandatory for endpoint type `AzureIotHub.StorageContainer`. + +--- + +A `route` block supports the following: + +* `name` - (Required) The name of the route. The name can only include alphanumeric characters, periods, underscores, hyphens, has a maximum length of 64 characters, and must be unique. + +* `source` - (Required) The source that the routing rule is to be applied to, such as `DeviceMessages`. Possible values include: `RoutingSourceInvalid`, `RoutingSourceDeviceMessages`, `RoutingSourceTwinChangeEvents`, `RoutingSourceDeviceLifecycleEvents`, `RoutingSourceDeviceJobLifecycleEvents`. + +* `condition` - (Optional) The condition that is evaluated to apply the routing rule. If no condition is provided, it evaluates to true by default. For grammar, see: https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-query-language. + +* `endpoint_names` - (Required) The list of endpoints to which messages that satisfy the condition are routed. + +* `enabled` - (Required) Used to specify whether a route is enabled. ## Attributes Reference