From 5bb96da282edca7e98f8fa083023a00eee1dd0d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maksymilian=20Bogu=C5=84?= Date: Tue, 20 Aug 2019 16:23:59 +0200 Subject: [PATCH] iot_route resource --- azurerm/provider.go | 1 + azurerm/resource_arm_iothub.go | 16 +- azurerm/resource_arm_iothub_route.go | 260 ++++++++++++++++++++++ azurerm/resource_arm_iothub_route_test.go | 238 ++++++++++++++++++++ website/docs/r/iothub.html.markdown | 2 + website/docs/r/iothub_route.html.markdown | 110 +++++++++ 6 files changed, 620 insertions(+), 7 deletions(-) create mode 100644 azurerm/resource_arm_iothub_route.go create mode 100644 azurerm/resource_arm_iothub_route_test.go create mode 100644 website/docs/r/iothub_route.html.markdown diff --git a/azurerm/provider.go b/azurerm/provider.go index f63176c31996..2675968bdcc4 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -248,6 +248,7 @@ func Provider() terraform.ResourceProvider { "azurerm_iot_dps_certificate": resourceArmIotDPSCertificate(), "azurerm_iothub_consumer_group": resourceArmIotHubConsumerGroup(), "azurerm_iothub": resourceArmIotHub(), + "azurerm_iothub_route": resourceArmIotHubRoute(), "azurerm_iothub_endpoint_eventhub": resourceArmIotHubEndpointEventHub(), "azurerm_iothub_endpoint_servicebus_queue": resourceArmIotHubEndpointServiceBusQueue(), "azurerm_iothub_endpoint_servicebus_topic": resourceArmIotHubEndpointServiceBusTopic(), diff --git a/azurerm/resource_arm_iothub.go b/azurerm/resource_arm_iothub.go index 9f01606213c1..580bdb3e013a 100755 --- a/azurerm/resource_arm_iothub.go +++ b/azurerm/resource_arm_iothub.go @@ -299,8 +299,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": { @@ -463,12 +465,12 @@ func resourceArmIotHubCreateUpdate(d *schema.ResourceData, meta interface{}) err skuInfo := expandIoTHubSku(d) t := d.Get("tags").(map[string]interface{}) - fallbackRoute := expandIoTHubFallbackRoute(d) - routes := expandIoTHubRoutes(d) - routingProperties := devices.RoutingProperties{ - Routes: routes, - FallbackRoute: fallbackRoute, + FallbackRoute: expandIoTHubFallbackRoute(d), + } + + if _, ok := d.GetOk("route"); ok { + routingProperties.Routes = expandIoTHubRoutes(d) } if _, ok := d.GetOk("endpoint"); ok { diff --git a/azurerm/resource_arm_iothub_route.go b/azurerm/resource_arm_iothub_route.go new file mode 100644 index 000000000000..f234e5df7853 --- /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 000000000000..d80a26145fec --- /dev/null +++ b/azurerm/resource_arm_iothub_route_test.go @@ -0,0 +1,238 @@ +package azurerm + +import ( + "fmt" + "strings" + "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/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMIotHubRoute_basic(t *testing.T) { + resourceName := "azurerm_iothub_route.test" + rInt := tf.AccRandTimeInt() + rs := acctest.RandString(4) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMIotHubRouteDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMIotHubRoute_basic(rInt, rs, 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() + rs := acctest.RandString(4) + location := testLocation() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMIotHubRouteDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMIotHubRoute_basic(rInt, rs, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMIotHubRouteExists(resourceName), + ), + }, + { + Config: testAccAzureRMIotHubRoute_requiresImport(rInt, rs, 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, rStr string, location string) string { + template := testAccAzureRMIotHubRoute_basic(rInt, rStr, 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%[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" + 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, rStr) +} diff --git a/website/docs/r/iothub.html.markdown b/website/docs/r/iothub.html.markdown index e5f5ac40114d..8313ac1ce315 100644 --- a/website/docs/r/iothub.html.markdown +++ b/website/docs/r/iothub.html.markdown @@ -12,6 +12,8 @@ Manages an IotHub ~> **NOTE:** Endpoints can be defined either directly on the `azurerm_iothub` resource, or using the `azurerm_iothub_endpoint_*` resources - but the two ways of defining the endpoints cannot be used together. If both are used against the same IoTHub, spurious changes will occur. Also, defining a `azurerm_iothub_endpoint_*` resource and another endpoint of a different type directly on the `azurerm_iothub` resource is not supported. +~> **NOTE:** Routes can be defined either directly on the `azurerm_iothub` resource, or using the `azurerm_iothub_route` resource - 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 000000000000..a036840d05b8 --- /dev/null +++ b/website/docs/r/iothub_route.html.markdown @@ -0,0 +1,110 @@ +--- +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 IoTHub, 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 route. + +* `resource_group_name` - (Required) The name of the resource group under which the IotHub Route 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 Route 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 +```