diff --git a/azurerm/provider.go b/azurerm/provider.go index 1638e85d9da59..41666b972c157 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -304,6 +304,7 @@ func Provider() terraform.ResourceProvider { "azurerm_iot_dps": resourceArmIotDPS(), "azurerm_iothub_consumer_group": resourceArmIotHubConsumerGroup(), "azurerm_iothub": resourceArmIotHub(), + "azurerm_iothub_route": resourceArmIotHubRoute(), "azurerm_iothub_shared_access_policy": resourceArmIotHubSharedAccessPolicy(), "azurerm_key_vault_access_policy": resourceArmKeyVaultAccessPolicy(), "azurerm_key_vault_certificate": resourceArmKeyVaultCertificate(), diff --git a/azurerm/resource_arm_iothub.go b/azurerm/resource_arm_iothub.go index 40c5e084cd9cd..3c2b43dadd002 100755 --- a/azurerm/resource_arm_iothub.go +++ b/azurerm/resource_arm_iothub.go @@ -237,8 +237,10 @@ func resourceArmIotHub() *schema.Resource { }, "route": { - Type: schema.TypeList, - Optional: true, + Type: schema.TypeList, + Optional: true, + Computed: true, + Deprecated: "Use the `azurerm_iothub_route` resource instead.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { @@ -401,6 +403,10 @@ func resourceArmIotHubCreateUpdate(d *schema.ResourceData, meta interface{}) err skuInfo := expandIoTHubSku(d) tags := d.Get("tags").(map[string]interface{}) fallbackRoute := expandIoTHubFallbackRoute(d) + } + + if _, ok := d.GetOk("route"); ok { + routingProperties.Routes = expandIoTHubRoutes(d) endpoints, err := expandIoTHubEndpoints(d, subscriptionID) if err != nil { diff --git a/azurerm/resource_arm_iothub_route.go b/azurerm/resource_arm_iothub_route.go new file mode 100644 index 0000000000000..f234e5df78533 --- /dev/null +++ b/azurerm/resource_arm_iothub_route.go @@ -0,0 +1,260 @@ +package azurerm + +import ( + "fmt" + "regexp" + "strings" + + "github.com/Azure/azure-sdk-for-go/services/preview/iothub/mgmt/2018-12-01-preview/devices" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmIotHubRoute() *schema.Resource { + return &schema.Resource{ + Create: resourceArmIotHubRouteCreateUpdate, + Read: resourceArmIotHubRouteRead, + Update: resourceArmIotHubRouteCreateUpdate, + Delete: resourceArmIotHubRouteDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringMatch( + regexp.MustCompile("^[-_.a-zA-Z0-9]{1,64}$"), + "Route Name name can only include alphanumeric characters, periods, underscores, hyphens, has a maximum length of 64 characters, and must be unique.", + ), + }, + + "resource_group_name": azure.SchemaResourceGroupName(), + + "iothub_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.IoTHubName, + }, + + "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, + }, + }, + } +} + +func resourceArmIotHubRouteCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).iothub.ResourceClient + ctx := meta.(*ArmClient).StopContext + + iothubName := d.Get("iothub_name").(string) + resourceGroup := d.Get("resource_group_name").(string) + + azureRMLockByName(iothubName, iothubResourceName) + defer azureRMUnlockByName(iothubName, iothubResourceName) + + iothub, err := client.Get(ctx, resourceGroup, iothubName) + if err != nil { + if utils.ResponseWasNotFound(iothub.Response) { + return fmt.Errorf("IotHub %q (Resource Group %q) was not found", iothubName, resourceGroup) + } + + return fmt.Errorf("Error loading IotHub %q (Resource Group %q): %+v", iothubName, resourceGroup, err) + } + + routeName := d.Get("name").(string) + + resourceId := fmt.Sprintf("%s/Routes/%s", *iothub.ID, routeName) + + source := devices.RoutingSource(d.Get("source").(string)) + condition := d.Get("condition").(string) + endpointNamesRaw := d.Get("endpoint_names").([]interface{}) + isEnabled := d.Get("enabled").(bool) + + route := devices.RouteProperties{ + Name: &routeName, + Source: source, + Condition: &condition, + EndpointNames: utils.ExpandStringSlice(endpointNamesRaw), + IsEnabled: &isEnabled, + } + + routing := iothub.Properties.Routing + + if routing == nil { + routing = &devices.RoutingProperties{} + } + + if routing.Routes == nil { + routes := make([]devices.RouteProperties, 0) + routing.Routes = &routes + } + + routes := make([]devices.RouteProperties, 0) + + alreadyExists := false + for _, existingRoute := range *routing.Routes { + if strings.EqualFold(*existingRoute.Name, routeName) { + if d.IsNewResource() && requireResourcesToBeImported { + return tf.ImportAsExistsError("azurerm_iothub_route", resourceId) + } + routes = append(routes, route) + alreadyExists = true + + } else { + routes = append(routes, existingRoute) + } + } + + if d.IsNewResource() { + routes = append(routes, route) + } else if !alreadyExists { + return fmt.Errorf("Unable to find Route %q defined for IotHub %q (Resource Group %q)", routeName, iothubName, resourceGroup) + } + + routing.Routes = &routes + + future, err := client.CreateOrUpdate(ctx, resourceGroup, iothubName, iothub, "") + if err != nil { + return fmt.Errorf("Error creating/updating IotHub %q (Resource Group %q): %+v", iothubName, resourceGroup, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("Error waiting for the completion of the creating/updating of IotHub %q (Resource Group %q): %+v", iothubName, resourceGroup, err) + } + + d.SetId(resourceId) + + return resourceArmIotHubRouteRead(d, meta) +} + +func resourceArmIotHubRouteRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).iothub.ResourceClient + ctx := meta.(*ArmClient).StopContext + + parsedIothubRouteId, err := parseAzureResourceID(d.Id()) + + if err != nil { + return err + } + + resourceGroup := parsedIothubRouteId.ResourceGroup + iothubName := parsedIothubRouteId.Path["IotHubs"] + routeName := parsedIothubRouteId.Path["Routes"] + + iothub, err := client.Get(ctx, resourceGroup, iothubName) + if err != nil { + return fmt.Errorf("Error loading IotHub %q (Resource Group %q): %+v", iothubName, resourceGroup, err) + } + + d.Set("name", routeName) + d.Set("iothub_name", iothubName) + d.Set("resource_group_name", resourceGroup) + + if iothub.Properties == nil || iothub.Properties.Routing == nil { + return nil + } + + if routes := iothub.Properties.Routing.Routes; routes != nil { + for _, route := range *routes { + if strings.EqualFold(*route.Name, routeName) { + + d.Set("source", route.Source) + d.Set("condition", route.Condition) + d.Set("enabled", route.IsEnabled) + d.Set("endpoint_names", route.EndpointNames) + } + } + } + + return nil +} + +func resourceArmIotHubRouteDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).iothub.ResourceClient + ctx := meta.(*ArmClient).StopContext + + parsedIothubRouteId, err := parseAzureResourceID(d.Id()) + + if err != nil { + return err + } + + resourceGroup := parsedIothubRouteId.ResourceGroup + iothubName := parsedIothubRouteId.Path["IotHubs"] + routeName := parsedIothubRouteId.Path["Routes"] + + azureRMLockByName(iothubName, iothubResourceName) + defer azureRMUnlockByName(iothubName, iothubResourceName) + + iothub, err := client.Get(ctx, resourceGroup, iothubName) + if err != nil { + if utils.ResponseWasNotFound(iothub.Response) { + return fmt.Errorf("IotHub %q (Resource Group %q) was not found", iothubName, resourceGroup) + } + + return fmt.Errorf("Error loading IotHub %q (Resource Group %q): %+v", iothubName, resourceGroup, err) + } + + if iothub.Properties == nil || iothub.Properties.Routing == nil { + return nil + } + routes := iothub.Properties.Routing.Routes + + if routes == nil { + return nil + } + + updatedRoutes := make([]devices.RouteProperties, 0) + for _, route := range *routes { + if !strings.EqualFold(*route.Name, routeName) { + updatedRoutes = append(updatedRoutes, route) + } + } + + iothub.Properties.Routing.Routes = &updatedRoutes + + future, err := client.CreateOrUpdate(ctx, resourceGroup, iothubName, iothub, "") + if err != nil { + return fmt.Errorf("Error updating IotHub %q (Resource Group %q) with Route %q: %+v", iothubName, resourceGroup, routeName, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("Error waiting for IotHub %q (Resource Group %q) to finish updating Route %q: %+v", iothubName, resourceGroup, routeName, err) + } + + return nil +} diff --git a/azurerm/resource_arm_iothub_route_test.go b/azurerm/resource_arm_iothub_route_test.go new file mode 100644 index 0000000000000..489dafcbaa10c --- /dev/null +++ b/azurerm/resource_arm_iothub_route_test.go @@ -0,0 +1,265 @@ +package azurerm + +import ( + "fmt" + "net/http" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" +) + +func TestAccAzureRMIotHubRoute_basic(t *testing.T) { + resourceName := "azurerm_iothub_route.test" + rInt := tf.AccRandTimeInt() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMIotHubDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMIotHubRoute_basic(rInt, testLocation()), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMIotHubRouteExists(resourceName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureRMIotHubRoute_requiresImport(t *testing.T) { + if !requireResourcesToBeImported { + t.Skip("Skipping since resources aren't required to be imported") + return + } + + resourceName := "azurerm_iothub_route.test" + rInt := tf.AccRandTimeInt() + location := testLocation() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMIotHubDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMIotHubRoute_basic(rInt, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMIotHubRouteExists(resourceName), + ), + }, + { + Config: testAccAzureRMIotHubRoute_requiresImport(rInt, location), + ExpectError: testRequiresImportError("azurerm_iothub_route"), + }, + }, + }) +} + +func testCheckAzureRMIotHubRouteDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*ArmClient).iothub.ResourceClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_iothub_route" { + continue + } + + routeName := rs.Primary.Attributes["name"] + iothubName := rs.Primary.Attributes["iothub_name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + iothub, err := client.Get(ctx, resourceGroup, iothubName) + if err != nil { + if utils.ResponseWasNotFound(iothub.Response) { + return nil + } + + return fmt.Errorf("Bad: Get on iothubResourceClient: %+v", err) + } + if iothub.Properties == nil || iothub.Properties.Routing == nil { + return nil + } + routes := iothub.Properties.Routing.Routes + + if routes == nil { + return nil + } + + for _, route := range *routes { + if strings.EqualFold(*route.Name, routeName) { + return fmt.Errorf("Bad: route %s still exists on IoTHb %s", routeName, iothubName) + } + } + } + return nil +} + +func testCheckAzureRMIotHubRouteExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + parsedIothubId, err := azure.ParseAzureResourceID(rs.Primary.ID) + if err != nil { + return err + } + iothubName := parsedIothubId.Path["IotHubs"] + routeName := parsedIothubId.Path["Routes"] + resourceGroup := parsedIothubId.ResourceGroup + + client := testAccProvider.Meta().(*ArmClient).iothub.ResourceClient + + iothub, err := client.Get(ctx, resourceGroup, iothubName) + if err != nil { + if utils.ResponseWasNotFound(iothub.Response) { + return fmt.Errorf("IotHub %q (Resource Group %q) was not found", iothubName, resourceGroup) + } + + return fmt.Errorf("Error loading IotHub %q (Resource Group %q): %+v", iothubName, resourceGroup, err) + } + + if iothub.Properties == nil || iothub.Properties.Routing == nil { + return fmt.Errorf("Bad: No route %s defined for IotHub %s", routeName, iothubName) + } + routes := iothub.Properties.Routing.Routes + + if routes == nil { + return fmt.Errorf("Bad: No route %s defined for IotHub %s", routeName, iothubName) + } + + for _, route := range *routes { + if strings.EqualFold(*route.Name, routeName) { + return nil + } + } + + return fmt.Errorf("Bad: No route %s defined for IotHub %s", routeName, iothubName) + + } +} + +func testAccAzureRMIotHubRoute_requiresImport(rInt int, location string) string { + template := testAccAzureRMIotHub_basic(rInt, location) + return fmt.Sprintf(` +%s + +resource "azurerm_iothub_route" "import" { + resource_group_name = "${azurerm_resource_group.test.name}" + iothub_name = "${azurerm_iothub.test.name}" + name = "acctest" + + source = "DeviceMessages" + condition = "true" + endpoint_names = ["${azurerm_iothub_endpoint_storage_container.test.name}"] + enabled = true +} +`, template) +} + +func testAccAzureRMIotHubRoute_basic(rInt int, rStr string, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]d" + location = "%[2]s" +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%[1]d" + 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-%[1]d" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" + + sku { + name = "S1" + tier = "Standard" + capacity = "1" + } + + tags = { + purpose = "testing" + } +} + +resource "azurerm_iothub_endpoint_storage_container" "test" { + resource_group_name = "${azurerm_resource_group.test.name}" + iothub_name = "${azurerm_iothub.test.name}" + name = "acctest" + + connection_string = "${azurerm_storage_account.test.primary_blob_connection_string}" + batch_frequency_in_seconds = 60 + max_chunk_size_in_bytes = 10485760 + container_name = "${azurerm_storage_container.test.name}" + encoding = "Avro" + file_name_format = "{iothub}/{partition}_{YYYY}_{MM}_{DD}_{HH}_{mm}" +} + +resource "azurerm_iothub_route" "test" { + resource_group_name = "${azurerm_resource_group.test.name}" + iothub_name = "${azurerm_iothub.test.name}" + name = "acctest" + + source = "DeviceMessages" + condition = "true" + endpoint_names = ["${azurerm_iothub_endpoint_storage_container.test.name}"] + enabled = true +} + +`, rInt, location) +} + +func testAccAzureRMIotHub_fallbackRoute(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%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 = "S1" + tier = "Standard" + capacity = "1" + } + + fallback_route { + source = "DeviceMessages" + endpoint_names = ["events"] + enabled = true + } + + tags = { + purpose = "testing" + } +} +`, rInt, location, rInt) +} diff --git a/website/docs/r/iothub.html.markdown b/website/docs/r/iothub.html.markdown index ea088ccf92da1..aaf768fb6667b 100644 --- a/website/docs/r/iothub.html.markdown +++ b/website/docs/r/iothub.html.markdown @@ -10,6 +10,8 @@ description: |- Manages an IotHub +~> **NOTE:** Routes can be defined either directly on the `azurerm_iothub` resource, or using the `azurerm_iothub_route` resourcs - but the two cannot be used together. If both are used against the same Virtual Machine, spurious changes will occur. + ## Example Usage ```hcl diff --git a/website/docs/r/iothub_route.html.markdown b/website/docs/r/iothub_route.html.markdown new file mode 100644 index 0000000000000..2d87269d45ae9 --- /dev/null +++ b/website/docs/r/iothub_route.html.markdown @@ -0,0 +1,111 @@ +--- +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_iothub_route" +sidebar_current: "docs-azurerm-resource-messaging-iothub-route-x" +description: |- + Manages an IotHub Route +--- + +# azurerm_iothub_route + +Manages an IotHub Route + +~> **NOTE:** Routes can be defined either directly on the `azurerm_iothub` resource, or using the `azurerm_iothub_route` resourcs - but the two cannot be used together. If both are used against the same Virtual Machine, spurious changes will occur. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "example" + location = "West US" +} + +resource "azurerm_storage_account" "example" { + name = "examplestorageaccount" + resource_group_name = "${azurerm_resource_group.example.name}" + location = "${azurerm_resource_group.example.location}" + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_storage_container" "example" { + name = "example" + resource_group_name = "${azurerm_resource_group.example.name}" + storage_account_name = "${azurerm_storage_account.example.name}" + container_access_type = "private" +} + +resource "azurerm_iothub" "example" { + name = "exampleIothub" + resource_group_name = "${azurerm_resource_group.example.name}" + location = "${azurerm_resource_group.example.location}" + + sku { + name = "S1" + tier = "Standard" + capacity = "1" + } + + tags = { + purpose = "testing" + } +} + +resource "azurerm_iothub_route" "example" { + resource_group_name = "${azurerm_resource_group.example.name}" + iothub_name = "${azurerm_iothub.example.name}" + name = "example" + + connection_string = "${azurerm_storage_account.example.primary_blob_connection_string}" + batch_frequency_in_seconds = 60 + max_chunk_size_in_bytes = 10485760 + container_name = "${azurerm_storage_container.example.name}" + encoding = "Avro" + file_name_format = "{iothub}/{partition}_{YYYY}_{MM}_{DD}_{HH}_{mm}" +} + +resource "azurerm_iothub_route" "example" { + resource_group_name = "${azurerm_resource_group.example.name}" + iothub_name = "${azurerm_iothub.example.name}" + name = "example" + + source = "DeviceMessages" + condition = "true" + endpoint_names = ["${azurerm_iothub_endpoint_storage_container.example.name}"] + enabled = true +} + +``` + +## Argument Reference + +The following arguments are supported: + +* `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`. + +* `resource_group_name` - (Required) The name of the resource group under which the IotHub Storage Container Endpoint resource has to be created. Changing this forces a new resource to be created. + +* `iothub_name` - (Required) The name of the IoTHub to which this Storage Container Endpoint belongs. Changing this forces a new resource to be created. + + +* `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 + +The following attributes are exported: + +* `id` - The ID of the IoTHub Route. + +## Import + +IoTHub Route can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_iothub_route.route1 /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.Devices/IotHubs/hub1/Routes/route1 +```