diff --git a/azurerm/internal/services/iothub/iothub_enrichment_resource.go b/azurerm/internal/services/iothub/iothub_enrichment_resource.go new file mode 100644 index 000000000000..7680132f9786 --- /dev/null +++ b/azurerm/internal/services/iothub/iothub_enrichment_resource.go @@ -0,0 +1,251 @@ +package iothub + +import ( + "fmt" + "regexp" + "strings" + "time" + + "github.com/Azure/azure-sdk-for-go/services/iothub/mgmt/2020-03-01/devices" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/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/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/locks" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/iothub/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceIotHubEnrichment() *schema.Resource { + return &schema.Resource{ + Create: resourceArmIotHubEnrichmentCreateUpdate, + Read: resourceArmIotHubEnrichmentRead, + Update: resourceArmIotHubEnrichmentCreateUpdate, + Delete: resourceArmIotHubEnrichmentDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(30 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringMatch( + regexp.MustCompile("^[-_.a-zA-Z0-9]{1,64}$"), + "Enrichment Key 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, + }, + + "value": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "endpoint_names": { + Type: schema.TypeList, + MaxItems: 100, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + Required: true, + }, + }, + } +} + +func resourceArmIotHubEnrichmentCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).IoTHub.ResourceClient + ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) + defer cancel() + + iothubName := d.Get("iothub_name").(string) + resourceGroup := d.Get("resource_group_name").(string) + + locks.ByName(iothubName, IothubResourceName) + defer locks.UnlockByName(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) + } + + enrichmentKey := d.Get("key").(string) + enrichmentValue := d.Get("value").(string) + resourceId := fmt.Sprintf("%s/Enrichments/%s", *iothub.ID, enrichmentKey) + endpointNamesRaw := d.Get("endpoint_names").([]interface{}) + + enrichment := devices.EnrichmentProperties{ + Key: &enrichmentKey, + Value: &enrichmentValue, + EndpointNames: utils.ExpandStringSlice(endpointNamesRaw), + } + + routing := iothub.Properties.Routing + if routing == nil { + routing = &devices.RoutingProperties{} + } + + if routing.Enrichments == nil { + enrichments := make([]devices.EnrichmentProperties, 0) + routing.Enrichments = &enrichments + } + + enrichments := make([]devices.EnrichmentProperties, 0) + + alreadyExists := false + for _, existingEnrichment := range *routing.Enrichments { + if existingEnrichment.Key != nil { + if strings.EqualFold(*existingEnrichment.Key, enrichmentKey) { + if d.IsNewResource() { + return tf.ImportAsExistsError("azurerm_iothub_enrichment", resourceId) + } + enrichments = append(enrichments, enrichment) + alreadyExists = true + } else { + enrichments = append(enrichments, existingEnrichment) + } + } + } + + if d.IsNewResource() { + enrichments = append(enrichments, enrichment) + } else if !alreadyExists { + return fmt.Errorf("Unable to find Enrichment %q defined for IotHub %q (Resource Group %q)", enrichmentKey, iothubName, resourceGroup) + } + routing.Enrichments = &enrichments + + 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 resourceArmIotHubEnrichmentRead(d, meta) +} + +func resourceArmIotHubEnrichmentRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).IoTHub.ResourceClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + parsedEnrichmentId, err := azure.ParseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resourceGroup := parsedEnrichmentId.ResourceGroup + iothubName := parsedEnrichmentId.Path["IotHubs"] + enrichmentKey := parsedEnrichmentId.Path["Enrichments"] + + 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("key", enrichmentKey) + d.Set("iothub_name", iothubName) + d.Set("resource_group_name", resourceGroup) + + if iothub.Properties == nil || iothub.Properties.Routing == nil { + return nil + } + + if enrichments := iothub.Properties.Routing.Enrichments; enrichments != nil { + for _, enrichment := range *enrichments { + if enrichment.Key != nil { + if strings.EqualFold(*enrichment.Key, enrichmentKey) { + d.Set("value", enrichment.Value) + d.Set("endpoint_names", enrichment.EndpointNames) + } + } + } + } + + return nil +} + +func resourceArmIotHubEnrichmentDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).IoTHub.ResourceClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + parsedEnrichmentId, err := azure.ParseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resourceGroup := parsedEnrichmentId.ResourceGroup + iothubName := parsedEnrichmentId.Path["IotHubs"] + enrichmentKey := parsedEnrichmentId.Path["Enrichments"] + + locks.ByName(iothubName, IothubResourceName) + defer locks.UnlockByName(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 + } + enrichments := iothub.Properties.Routing.Enrichments + + if enrichments == nil { + return nil + } + + updatedEnrichments := make([]devices.EnrichmentProperties, 0) + for _, enrichment := range *enrichments { + if enrichment.Key != nil { + if !strings.EqualFold(*enrichment.Key, enrichmentKey) { + updatedEnrichments = append(updatedEnrichments, enrichment) + } + } + } + iothub.Properties.Routing.Enrichments = &updatedEnrichments + + future, err := client.CreateOrUpdate(ctx, resourceGroup, iothubName, iothub, "") + if err != nil { + return fmt.Errorf("Error updating IotHub %q (Resource Group %q) with Enrichment %q: %+v", iothubName, resourceGroup, enrichmentKey, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("Error waiting for IotHub %q (Resource Group %q) to finish updating Enrichment %q: %+v", iothubName, resourceGroup, enrichmentKey, err) + } + + return nil +} diff --git a/azurerm/internal/services/iothub/iothub_enrichment_resource_test.go b/azurerm/internal/services/iothub/iothub_enrichment_resource_test.go new file mode 100644 index 000000000000..cd31f7d1b717 --- /dev/null +++ b/azurerm/internal/services/iothub/iothub_enrichment_resource_test.go @@ -0,0 +1,309 @@ +package iothub_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccIotHubEnrichment_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub_enrichment", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckIotHubEnrichmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccIotHubEnrichment_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckIotHubEnrichmentExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccIotHubEnrichment_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub_enrichment", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckIotHubEnrichmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccIotHubEnrichment_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckIotHubEnrichmentExists(data.ResourceName), + ), + }, + { + Config: testAccIotHubEnrichment_requiresImport(data), + ExpectError: acceptance.RequiresImportError("azurerm_iothub_enrichment"), + }, + }, + }) +} + +func TestAccIotHubEnrichment_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub_enrichment", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckIotHubEnrichmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccIotHubEnrichment_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckIotHubEnrichmentExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccIotHubEnrichment_update(data), + Check: resource.ComposeTestCheckFunc( + testCheckIotHubEnrichmentExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func testCheckIotHubEnrichmentDestroy(s *terraform.State) error { + client := acceptance.AzureProvider.Meta().(*clients.Client).IoTHub.ResourceClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_iothub_enrichment" { + continue + } + + enrichmentKey := rs.Primary.Attributes["key"] + 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 + } + enrichments := iothub.Properties.Routing.Enrichments + + if enrichments == nil { + return nil + } + + for _, enrichment := range *enrichments { + if strings.EqualFold(*enrichment.Key, enrichmentKey) { + return fmt.Errorf("Bad: enrichment %s still exists on IoTHb %s", enrichmentKey, iothubName) + } + } + } + return nil +} + +func testCheckIotHubEnrichmentExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := acceptance.AzureProvider.Meta().(*clients.Client).IoTHub.ResourceClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).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"] + enrichmentKey := parsedIothubId.Path["Enrichments"] + resourceGroup := parsedIothubId.ResourceGroup + + 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 Enrichment %s defined for IotHub %s", enrichmentKey, iothubName) + } + enrichments := iothub.Properties.Routing.Enrichments + + if enrichments == nil { + return fmt.Errorf("Bad: No enrichment %s defined for IotHub %s", enrichmentKey, iothubName) + } + + for _, enrichment := range *enrichments { + if strings.EqualFold(*enrichment.Key, enrichmentKey) { + return nil + } + } + + return fmt.Errorf("Bad: No enrichment %s defined for IotHub %s", enrichmentKey, iothubName) + } +} + +func testAccIotHubEnrichment_requiresImport(data acceptance.TestData) string { + template := testAccIotHubEnrichment_basic(data) + return fmt.Sprintf(` +%s + +resource "azurerm_iothub_enrichment" "import" { + resource_group_name = azurerm_resource_group.test.name + iothub_name = azurerm_iothub.test.name + key = "acctest" + + value = "$twin.tags.DeviceType" + endpoint_names = [azurerm_iothub_endpoint_storage_container.test.name] +} +`, template) +} + +func testAccIotHubEnrichment_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_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_enrichment" "test" { + resource_group_name = azurerm_resource_group.test.name + iothub_name = azurerm_iothub.test.name + key = "acctest" + + value = "$twin.tags.DeviceType" + endpoint_names = [azurerm_iothub_endpoint_storage_container.test.name] +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString) +} + +func testAccIotHubEnrichment_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_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_enrichment" "test" { + resource_group_name = azurerm_resource_group.test.name + iothub_name = azurerm_iothub.test.name + key = "acctest" + + value = "$twin.tags.Tenant" + endpoint_names = [azurerm_iothub_endpoint_storage_container.test.name] +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString) +} diff --git a/azurerm/internal/services/iothub/iothub_resource.go b/azurerm/internal/services/iothub/iothub_resource.go index 445c016ad7b6..3971dd487e06 100644 --- a/azurerm/internal/services/iothub/iothub_resource.go +++ b/azurerm/internal/services/iothub/iothub_resource.go @@ -346,6 +346,39 @@ func resourceIotHub() *schema.Resource { }, }, + "enrichment": { + Type: schema.TypeList, + // Currently only 10 enrichments is allowed for standard or basic tier, 2 for Free tier. + MaxItems: 10, + Optional: true, + Computed: true, + ConfigMode: schema.SchemaConfigModeAttr, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringMatch( + regexp.MustCompile("^[-_.a-zA-Z0-9]{1,64}$"), + "Enrichment Key name can only include alphanumeric characters, periods, underscores, hyphens, has a maximum length of 64 characters, and must be unique.", + ), + }, + "value": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "endpoint_names": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Required: true, + }, + }, + }, + }, + "fallback_route": { Type: schema.TypeList, MaxItems: 1, @@ -509,6 +542,10 @@ func resourceIotHubCreateUpdate(d *schema.ResourceData, meta interface{}) error routingProperties.Routes = expandIoTHubRoutes(d) } + if _, ok := d.GetOk("enrichment"); ok { + routingProperties.Enrichments = expandIoTHubEnrichments(d) + } + if _, ok := d.GetOk("fallback_route"); ok { routingProperties.FallbackRoute = expandIoTHubFallbackRoute(d) } @@ -646,6 +683,11 @@ func resourceIotHubRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("setting `route` in IoTHub %q: %+v", id.Name, err) } + enrichments := flattenIoTHubEnrichment(properties.Routing) + if err := d.Set("enrichment", enrichments); err != nil { + return fmt.Errorf("setting `enrichment` in IoTHub %q: %+v", id.Name, err) + } + fallbackRoute := flattenIoTHubFallbackRoute(properties.Routing) if err := d.Set("fallback_route", fallbackRoute); err != nil { return fmt.Errorf("setting `fallbackRoute` in IoTHub %q: %+v", id.Name, err) @@ -767,6 +809,29 @@ func expandIoTHubRoutes(d *schema.ResourceData) *[]devices.RouteProperties { return &routeProperties } +func expandIoTHubEnrichments(d *schema.ResourceData) *[]devices.EnrichmentProperties { + enrichmentList := d.Get("enrichment").([]interface{}) + + enrichmentProperties := make([]devices.EnrichmentProperties, 0) + + for _, enrichmentRaw := range enrichmentList { + enrichment := enrichmentRaw.(map[string]interface{}) + + key := enrichment["key"].(string) + value := enrichment["value"].(string) + + endpointNamesRaw := enrichment["endpoint_names"].([]interface{}) + + enrichmentProperties = append(enrichmentProperties, devices.EnrichmentProperties{ + Key: &key, + Value: &value, + EndpointNames: utils.ExpandStringSlice(endpointNamesRaw), + }) + } + + return &enrichmentProperties +} + func expandIoTHubFileUpload(d *schema.ResourceData) (map[string]*devices.StorageEndpointProperties, map[string]*devices.MessagingEndpointProperties, bool) { fileUploadList := d.Get("file_upload").([]interface{}) @@ -1110,6 +1175,30 @@ func flattenIoTHubRoute(input *devices.RoutingProperties) []interface{} { return results } +func flattenIoTHubEnrichment(input *devices.RoutingProperties) []interface{} { + results := make([]interface{}, 0) + + if input != nil && input.Enrichments != nil { + for _, enrichment := range *input.Enrichments { + output := make(map[string]interface{}) + + if key := enrichment.Key; key != nil { + output["key"] = *key + } + if value := enrichment.Value; value != nil { + output["value"] = *value + } + if endpointNames := enrichment.EndpointNames; endpointNames != nil { + output["endpoint_names"] = *endpointNames + } + + results = append(results, output) + } + } + + return results +} + func flattenIoTHubFallbackRoute(input *devices.RoutingProperties) []interface{} { if input.FallbackRoute == nil { return []interface{}{} diff --git a/azurerm/internal/services/iothub/iothub_resource_test.go b/azurerm/internal/services/iothub/iothub_resource_test.go index a29fccc3b6d3..00c1587fce92 100644 --- a/azurerm/internal/services/iothub/iothub_resource_test.go +++ b/azurerm/internal/services/iothub/iothub_resource_test.go @@ -99,6 +99,25 @@ func TestAccIotHub_customRoutes(t *testing.T) { }) } +func TestAccIotHub_enrichments(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub", "test") + r := IotHubResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.enrichments(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("endpoint.#").HasValue("2"), + check.That(data.ResourceName).Key("endpoint.0.type").HasValue("AzureIotHub.StorageContainer"), + check.That(data.ResourceName).Key("endpoint.1.type").HasValue("AzureIotHub.EventHub"), + check.That(data.ResourceName).Key("route.#").HasValue("1"), + check.That(data.ResourceName).Key("enrichment.#").HasValue("2"), + ), + }, + data.ImportStep(), + }) +} + func TestAccIotHub_removeEndpointsAndRoutes(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_iothub", "test") r := IotHubResource{} @@ -450,6 +469,113 @@ resource "azurerm_iothub" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomString, data.RandomInteger, data.RandomInteger) } +func (IotHubResource) enrichments(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_eventhub_namespace" "test" { + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + name = "acctest-%d" + sku = "Basic" +} + +resource "azurerm_eventhub" "test" { + name = "acctest" + resource_group_name = azurerm_resource_group.test.name + namespace_name = azurerm_eventhub_namespace.test.name + partition_count = 2 + message_retention = 1 +} + +resource "azurerm_eventhub_authorization_rule" "test" { + resource_group_name = azurerm_resource_group.test.name + namespace_name = azurerm_eventhub_namespace.test.name + eventhub_name = azurerm_eventhub.test.name + name = "acctest" + send = true +} + +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" + } + + event_hub_retention_in_days = 7 + event_hub_partition_count = 77 + + 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 = azurerm_storage_container.test.name + encoding = "Avro" + file_name_format = "{iothub}/{partition}_{YYYY}_{MM}_{DD}_{HH}_{mm}" + resource_group_name = azurerm_resource_group.test.name + } + + endpoint { + type = "AzureIotHub.EventHub" + connection_string = azurerm_eventhub_authorization_rule.test.primary_connection_string + name = "export2" + resource_group_name = azurerm_resource_group.test.name + } + + route { + name = "export" + source = "DeviceMessages" + condition = "true" + endpoint_names = ["export"] + enabled = true + } + + enrichment { + key = "enrichment" + value = "$twin.tags.Tenant" + endpoint_names = ["export2"] + } + + enrichment { + key = "enrichment2" + value = "Multiple endpoint" + endpoint_names = ["export", "export2"] + } + + tags = { + purpose = "testing" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString, data.RandomInteger, data.RandomInteger) +} + func (IotHubResource) removeEndpointsAndRoutes(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" { diff --git a/azurerm/internal/services/iothub/registration.go b/azurerm/internal/services/iothub/registration.go index 035ffb60f51f..9277578e3741 100644 --- a/azurerm/internal/services/iothub/registration.go +++ b/azurerm/internal/services/iothub/registration.go @@ -36,6 +36,7 @@ func (r Registration) SupportedResources() map[string]*schema.Resource { "azurerm_iothub_consumer_group": resourceIotHubConsumerGroup(), "azurerm_iothub": resourceIotHub(), "azurerm_iothub_fallback_route": resourceIotHubFallbackRoute(), + "azurerm_iothub_enrichment": resourceIotHubEnrichment(), "azurerm_iothub_route": resourceIotHubRoute(), "azurerm_iothub_endpoint_eventhub": resourceIotHubEndpointEventHub(), "azurerm_iothub_endpoint_servicebus_queue": resourceIotHubEndpointServiceBusQueue(), diff --git a/website/docs/r/iothub.html.markdown b/website/docs/r/iothub.html.markdown index 9decf6b3ab60..6583615556d4 100644 --- a/website/docs/r/iothub.html.markdown +++ b/website/docs/r/iothub.html.markdown @@ -14,6 +14,8 @@ Manages an IotHub ~> **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 IoTHub, spurious changes will occur. +~> **NOTE:** Enrichments can be defined either directly on the `azurerm_iothub` resource, or using the `azurerm_iothub_enrichment` resource - but the two cannot be used together. If both are used against the same IoTHub, spurious changes will occur. + ~> **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. ## Example Usage @@ -104,6 +106,12 @@ resource "azurerm_iothub" "example" { enabled = true } + enrichment { + key = "tenant" + value = "$twin.tags.Tenant" + endpoint_names = ["export", "export2"] + } + tags = { purpose = "testing" } @@ -118,7 +126,7 @@ The following arguments are supported: * `resource_group_name` - (Required) The name of the resource group under which the IotHub resource has to be created. Changing this forces a new resource to be created. -* `location` - (Required) Specifies the supported Azure location where the resource has to be createc. Changing this forces a new resource to be created. +* `location` - (Required) Specifies the supported Azure location where the resource has to be created. Changing this forces a new resource to be created. * `sku` - (Required) A `sku` block as defined below. @@ -138,7 +146,9 @@ The following arguments are supported: * `route` - (Optional) A `route` block as defined below. -* `public_network_access_enabled` - (Optional) Is the IotHub resource accessible from a public network? +* `enrichment` - (Optional) A `enrichment` block as defined below. + +* `public_network_access_enabled` - (Optional) Is the IotHub resource accessible from a public network? * `min_tls_version` - (Optional) Specifies the minimum TLS version to support for this hub. The only valid value is `1.2`. Changing this forces a new resource to be created. @@ -202,6 +212,16 @@ A `route` block supports the following: --- +An `enrichment` block supports the following: + +* `key` - (Required) The key of the enrichment. + +* `value` - (Required) The value of the enrichment. Value can be any static string, the name of the IoT hub sending the message (use `$iothubname`) or information from the device twin (ex: `$twin.tags.latitude`) + +* `endpoint_names` - (Required) The list of endpoints which will be enriched. + +--- + A `fallback_route` block supports the following: * `source` - (Optional) The source that the routing rule is to be applied to, such as `DeviceMessages`. Possible values include: `RoutingSourceInvalid`, `RoutingSourceDeviceMessages`, `RoutingSourceTwinChangeEvents`, `RoutingSourceDeviceLifecycleEvents`, `RoutingSourceDeviceJobLifecycleEvents`. @@ -230,7 +250,6 @@ A `file_upload` block supports the following: * `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: @@ -262,8 +281,6 @@ A `shared access policy` block contains the following: ## 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. diff --git a/website/docs/r/iothub_enrichment.html.markdown b/website/docs/r/iothub_enrichment.html.markdown new file mode 100644 index 000000000000..9379ec2f4ee3 --- /dev/null +++ b/website/docs/r/iothub_enrichment.html.markdown @@ -0,0 +1,117 @@ +--- +subcategory: "Messaging" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_iothub_enrichment" +description: |- + Manages an IotHub Enrichment +--- + +# azurerm_iothub_enrichment + +Manages an IotHub Enrichment + +~> **NOTE:** Enrichment can be defined either directly on the `azurerm_iothub` resource, or using the `azurerm_iothub_enrichment` resources - 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-resources" + 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" + 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" + capacity = "1" + } + + tags = { + purpose = "testing" + } +} + +resource "azurerm_iothub_endpoint_storage_container" "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 +} + +resource "azurerm_iothub_enrichment" "example" { + resource_group_name = azurerm_resource_group.example.name + iothub_name = azurerm_iothub.example.name + key = "example" + + value = "my value" + endpoint_names = [azurerm_iothub_endpoint_storage_container.example.name] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `key` - (Required) The key of the enrichment. + +* `value` - (Required) The value of the enrichment. Value can be any static string, the name of the IoT hub sending the message (use `$iothubname`) or information from the device twin (ex: `$twin.tags.latitude`) + +* `endpoint_names` - (Required) The list of endpoints which will be enriched. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the IoTHub Enrichment. + +## 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 Enrichment. +* `update` - (Defaults to 30 minutes) Used when updating the IotHub Enrichment. +* `read` - (Defaults to 5 minutes) Used when retrieving the IotHub Enrichment. +* `delete` - (Defaults to 30 minutes) Used when deleting the IotHub Enrichment. + +## Import + +IoTHub Enrichment can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_iothub_enrichment.enrichment1 /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.Devices/IotHubs/hub1/Enrichments/enrichment1 +```