diff --git a/azurerm/import_arm_eventhub_test.go b/azurerm/import_arm_eventhub_test.go index 72340efc35b0..c420d7034fd7 100644 --- a/azurerm/import_arm_eventhub_test.go +++ b/azurerm/import_arm_eventhub_test.go @@ -30,3 +30,28 @@ func TestAccAzureRMEventHub_importBasic(t *testing.T) { }, }) } + +func TestAccAzureRMEventHub_importCaptureDescription(t *testing.T) { + resourceName := "azurerm_eventhub.test" + + ri := acctest.RandInt() + rs := acctest.RandString(5) + config := testAccAzureRMEventHub_captureDescription(ri, rs, testLocation(), true) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMEventHubDestroy, + Steps: []resource.TestStep{ + { + Config: config, + }, + + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} diff --git a/azurerm/resource_arm_eventhub.go b/azurerm/resource_arm_eventhub.go index b5320ea0328f..148e66105f9f 100644 --- a/azurerm/resource_arm_eventhub.go +++ b/azurerm/resource_arm_eventhub.go @@ -3,10 +3,12 @@ package azurerm import ( "fmt" "log" - "net/http" + + "strings" "github.com/Azure/azure-sdk-for-go/arm/eventhub" "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" ) @@ -50,6 +52,73 @@ func resourceArmEventHub() *schema.Resource { ValidateFunc: validateEventHubMessageRetentionCount, }, + "capture_description": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Required: true, + }, + "encoding": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: ignoreCaseDiffSuppressFunc, + ValidateFunc: validation.StringInSlice([]string{ + string(eventhub.Avro), + string(eventhub.AvroDeflate), + }, true), + }, + "interval_in_seconds": { + Type: schema.TypeInt, + Optional: true, + Default: 300, + ValidateFunc: validation.IntBetween(60, 900), + }, + "size_limit_in_bytes": { + Type: schema.TypeInt, + Optional: true, + Default: 314572800, + ValidateFunc: validation.IntBetween(10485760, 524288000), + }, + "destination": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "EventHubArchive.AzureBlockBlob", + // TODO: support `EventHubArchive.AzureDataLake` once supported in the Swagger / SDK + // https://github.com/Azure/azure-rest-api-specs/issues/2255 + // BlobContainerName & StorageAccountID can then become Optional + }, false), + }, + "archive_name_format": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateEventHubArchiveNameFormat, + }, + "blob_container_name": { + Type: schema.TypeString, + Required: true, + }, + "storage_account_id": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + }, + }, + }, + "partition_ids": { Type: schema.TypeSet, Elem: &schema.Schema{Type: schema.TypeString}, @@ -61,13 +130,12 @@ func resourceArmEventHub() *schema.Resource { } func resourceArmEventHubCreate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*ArmClient) - eventhubClient := client.eventHubClient + client := meta.(*ArmClient).eventHubClient log.Printf("[INFO] preparing arguments for Azure ARM EventHub creation.") name := d.Get("name").(string) namespaceName := d.Get("namespace_name").(string) - resGroup := d.Get("resource_group_name").(string) + resourceGroup := d.Get("resource_group_name").(string) partitionCount := int64(d.Get("partition_count").(int)) messageRetention := int64(d.Get("message_retention").(int)) @@ -78,18 +146,27 @@ func resourceArmEventHubCreate(d *schema.ResourceData, meta interface{}) error { }, } - _, err := eventhubClient.CreateOrUpdate(resGroup, namespaceName, name, parameters) + if _, ok := d.GetOk("capture_description"); ok { + captureDescription, err := expandEventHubCaptureDescription(d) + if err != nil { + return fmt.Errorf("Error expanding EventHub Capture Description: %s", err) + } + + parameters.Properties.CaptureDescription = captureDescription + } + + _, err := client.CreateOrUpdate(resourceGroup, namespaceName, name, parameters) if err != nil { return err } - read, err := eventhubClient.Get(resGroup, namespaceName, name) + read, err := client.Get(resourceGroup, namespaceName, name) if err != nil { return err } if read.ID == nil { - return fmt.Errorf("Cannot read EventHub %s (resource group %s) ID", name, resGroup) + return fmt.Errorf("Cannot read EventHub %s (resource group %s) ID", name, resourceGroup) } d.SetId(*read.ID) @@ -98,51 +175,60 @@ func resourceArmEventHubCreate(d *schema.ResourceData, meta interface{}) error { } func resourceArmEventHubRead(d *schema.ResourceData, meta interface{}) error { - eventhubClient := meta.(*ArmClient).eventHubClient - + client := meta.(*ArmClient).eventHubClient id, err := parseAzureResourceID(d.Id()) if err != nil { return err } - resGroup := id.ResourceGroup + + resourceGroup := id.ResourceGroup namespaceName := id.Path["namespaces"] name := id.Path["eventhubs"] - - resp, err := eventhubClient.Get(resGroup, namespaceName, name) + resp, err := client.Get(resourceGroup, namespaceName, name) if err != nil { if utils.ResponseWasNotFound(resp.Response) { d.SetId("") return nil } - return fmt.Errorf("Error making Read request on Azure EventHub %s: %s", name, err) + return fmt.Errorf("Error making Read request on Azure EventHub %q (resource group %q): %+v", name, resourceGroup, err) } d.Set("name", resp.Name) d.Set("namespace_name", namespaceName) - d.Set("resource_group_name", resGroup) + d.Set("resource_group_name", resourceGroup) - d.Set("partition_count", resp.Properties.PartitionCount) - d.Set("message_retention", resp.Properties.MessageRetentionInDays) - d.Set("partition_ids", resp.Properties.PartitionIds) + if props := resp.Properties; props != nil { + d.Set("partition_count", props.PartitionCount) + d.Set("message_retention", props.MessageRetentionInDays) + d.Set("partition_ids", props.PartitionIds) + + captureDescription := flattenEventHubCaptureDescription(props.CaptureDescription) + if err := d.Set("capture_description", captureDescription); err != nil { + return err + } + } return nil } func resourceArmEventHubDelete(d *schema.ResourceData, meta interface{}) error { - eventhubClient := meta.(*ArmClient).eventHubClient - + client := meta.(*ArmClient).eventHubClient id, err := parseAzureResourceID(d.Id()) if err != nil { return err } - resGroup := id.ResourceGroup + + resourceGroup := id.ResourceGroup namespaceName := id.Path["namespaces"] name := id.Path["eventhubs"] + resp, err := client.Delete(resourceGroup, namespaceName, name) - resp, err := eventhubClient.Delete(resGroup, namespaceName, name) + if err != nil { + if utils.ResponseWasNotFound(resp) { + return nil + } - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("Error issuing Azure ARM delete request of EventHub'%s': %s", name, err) + return fmt.Errorf("Error issuing delete request for EventHub %q (resource group %q): %+v", name, resourceGroup, err) } return nil @@ -165,3 +251,115 @@ func validateEventHubMessageRetentionCount(v interface{}, k string) (ws []string } return } + +func validateEventHubArchiveNameFormat(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + requiredComponents := []string{ + "{Namespace}", + "{EventHub}", + "{PartitionId}", + "{Year}", + "{Month}", + "{Day}", + "{Hour}", + "{Minute}", + "{Second}", + } + + for _, component := range requiredComponents { + if !strings.Contains(value, component) { + errors = append(errors, fmt.Errorf("%s needs to contain %q", k, component)) + } + } + + return +} + +func expandEventHubCaptureDescription(d *schema.ResourceData) (*eventhub.CaptureDescription, error) { + inputs := d.Get("capture_description").([]interface{}) + input := inputs[0].(map[string]interface{}) + + enabled := input["enabled"].(bool) + encoding := input["encoding"].(string) + intervalInSeconds := input["interval_in_seconds"].(int) + sizeLimitInBytes := input["size_limit_in_bytes"].(int) + + captureDescription := eventhub.CaptureDescription{ + Enabled: utils.Bool(enabled), + Encoding: eventhub.EncodingCaptureDescription(encoding), + IntervalInSeconds: utils.Int32(int32(intervalInSeconds)), + SizeLimitInBytes: utils.Int32(int32(sizeLimitInBytes)), + } + + if v, ok := input["destination"]; ok { + destinations := v.([]interface{}) + if len(destinations) > 0 { + destination := destinations[0].(map[string]interface{}) + + destinationName := destination["name"].(string) + archiveNameFormat := destination["archive_name_format"].(string) + blobContainerName := destination["blob_container_name"].(string) + storageAccountId := destination["storage_account_id"].(string) + + captureDescription.Destination = &eventhub.Destination{ + Name: utils.String(destinationName), + DestinationProperties: &eventhub.DestinationProperties{ + ArchiveNameFormat: utils.String(archiveNameFormat), + BlobContainer: utils.String(blobContainerName), + StorageAccountResourceID: utils.String(storageAccountId), + }, + } + } + } + + return &captureDescription, nil +} + +func flattenEventHubCaptureDescription(description *eventhub.CaptureDescription) []interface{} { + results := make([]interface{}, 0) + + if description != nil { + output := make(map[string]interface{}, 0) + + if enabled := description.Enabled; enabled != nil { + output["enabled"] = *enabled + } + + output["encoding"] = string(description.Encoding) + + if interval := description.IntervalInSeconds; interval != nil { + output["interval_in_seconds"] = *interval + } + + if size := description.SizeLimitInBytes; size != nil { + output["size_limit_in_bytes"] = *size + } + + if destination := description.Destination; destination != nil { + destinationOutput := make(map[string]interface{}, 0) + + if name := destination.Name; name != nil { + destinationOutput["name"] = *name + } + + if props := destination.DestinationProperties; props != nil { + if archiveNameFormat := props.ArchiveNameFormat; archiveNameFormat != nil { + destinationOutput["archive_name_format"] = *archiveNameFormat + } + if blobContainerName := props.BlobContainer; blobContainerName != nil { + destinationOutput["blob_container_name"] = *blobContainerName + } + if storageAccountId := props.StorageAccountResourceID; storageAccountId != nil { + destinationOutput["storage_account_id"] = *storageAccountId + } + } + + output["destination"] = []interface{}{destinationOutput} + } + + results = append(results, output) + } + + return results +} diff --git a/azurerm/resource_arm_eventhub_test.go b/azurerm/resource_arm_eventhub_test.go index e92bb617ad76..43d67e4d3fea 100644 --- a/azurerm/resource_arm_eventhub_test.go +++ b/azurerm/resource_arm_eventhub_test.go @@ -5,6 +5,8 @@ import ( "net/http" "testing" + "strconv" + "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" @@ -94,6 +96,74 @@ func TestAccAzureRMEventHubMessageRetentionCount_validation(t *testing.T) { } } +func TestAccAzureRMEventHubArchiveNameFormat_validation(t *testing.T) { + cases := []struct { + Value string + ErrCount int + }{ + { + Value: "", + ErrCount: 9, + }, + { + Value: "Prod_{EventHub}/{Namespace}\\{PartitionId}_{Year}_{Month}/{Day}/{Hour}/{Minute}/{Second}", + ErrCount: 0, + }, + { + Value: "Prod_{Eventub}/{Namespace}\\{PartitionId}_{Year}_{Month}/{Day}/{Hour}/{Minute}/{Second}", + ErrCount: 1, + }, + { + Value: "{Namespace}\\{PartitionId}_{Year}_{Month}/{Day}/{Hour}/{Minute}/{Second}", + ErrCount: 1, + }, + { + Value: "{Namespace}\\{PartitionId}_{Year}_{Month}/{Day}/{Hour}/{Minute}/{Second}", + ErrCount: 1, + }, + { + Value: "Prod_{EventHub}/{PartitionId}_{Year}_{Month}/{Day}/{Hour}/{Minute}/{Second}", + ErrCount: 1, + }, + { + Value: "Prod_{EventHub}/{Namespace}\\{Year}_{Month}/{Day}/{Hour}/{Minute}/{Second}", + ErrCount: 1, + }, + { + Value: "Prod_{EventHub}/{Namespace}\\{PartitionId}_{Month}/{Day}/{Hour}/{Minute}/{Second}", + ErrCount: 1, + }, + { + Value: "Prod_{EventHub}/{Namespace}\\{PartitionId}_{Year}/{Day}/{Hour}/{Minute}/{Second}", + ErrCount: 1, + }, + { + Value: "Prod_{EventHub}/{Namespace}\\{PartitionId}_{Year}_{Month}/{Hour}/{Minute}/{Second}", + ErrCount: 1, + }, + { + Value: "Prod_{EventHub}/{Namespace}\\{PartitionId}_{Year}_{Month}/{Day}/{Minute}/{Second}", + ErrCount: 1, + }, + { + Value: "Prod_{EventHub}/{Namespace}\\{PartitionId}_{Year}_{Month}/{Day}/{Hour}/{Second}", + ErrCount: 1, + }, + { + Value: "Prod_{EventHub}/{Namespace}\\{PartitionId}_{Year}_{Month}/{Day}/{Hour}/{Minute}", + ErrCount: 1, + }, + } + + for _, tc := range cases { + _, errors := validateEventHubArchiveNameFormat(tc.Value, "azurerm_eventhub") + + if len(errors) != tc.ErrCount { + t.Fatalf("Expected %q to trigger a validation error", tc.Value) + } + } +} + func TestAccAzureRMEventHub_basic(t *testing.T) { ri := acctest.RandInt() @@ -134,6 +204,60 @@ func TestAccAzureRMEventHub_standard(t *testing.T) { }) } +func TestAccAzureRMEventHub_captureDescription(t *testing.T) { + resourceName := "azurerm_eventhub.test" + ri := acctest.RandInt() + rs := acctest.RandString(5) + config := testAccAzureRMEventHub_captureDescription(ri, rs, testLocation(), true) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMEventHubDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMEventHubExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "capture_description.0.enabled", "true"), + ), + }, + }, + }) +} + +func TestAccAzureRMEventHub_captureDescriptionDisabled(t *testing.T) { + resourceName := "azurerm_eventhub.test" + ri := acctest.RandInt() + rs := acctest.RandString(5) + location := testLocation() + + config := testAccAzureRMEventHub_captureDescription(ri, rs, location, true) + updatedConfig := testAccAzureRMEventHub_captureDescription(ri, rs, location, false) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMEventHubDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMEventHubExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "capture_description.0.enabled", "true"), + ), + }, + { + Config: updatedConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMEventHubExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "capture_description.0.enabled", "false"), + ), + }, + }, + }) +} + func testCheckAzureRMEventHubDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*ArmClient).eventHubClient @@ -237,3 +361,57 @@ resource "azurerm_eventhub" "test" { } `, rInt, location, rInt, rInt) } + +func testAccAzureRMEventHub_captureDescription(rInt int, rString string, location string, enabled bool) string { + enabledString := strconv.FormatBool(enabled) + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestrg-%d" + location = "%s" +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%s" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_storage_container" "test" { + name = "example" + resource_group_name = "${azurerm_resource_group.test.name}" + storage_account_name = "${azurerm_storage_account.test.name}" + container_access_type = "private" +} + +resource "azurerm_eventhub_namespace" "test" { + name = "acctestehn%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + sku = "Standard" +} + +resource "azurerm_eventhub" "test" { + name = "acctesteh%d" + namespace_name = "${azurerm_eventhub_namespace.test.name}" + resource_group_name = "${azurerm_resource_group.test.name}" + partition_count = 2 + message_retention = 7 + + capture_description { + enabled = %s + encoding = "Avro" + interval_in_seconds = 60 + size_limit_in_bytes = 10485760 + + destination { + name = "EventHubArchive.AzureBlockBlob" + archive_name_format = "Prod_{EventHub}/{Namespace}\\{PartitionId}_{Year}_{Month}/{Day}/{Hour}/{Minute}/{Second}" + blob_container_name = "${azurerm_storage_container.test.name}" + storage_account_id = "${azurerm_storage_account.test.id}" + } + } +} +`, rInt, location, rString, rInt, rInt, enabledString) +} diff --git a/website/docs/r/eventhub.html.markdown b/website/docs/r/eventhub.html.markdown index bb8b6589aa31..d231eccce357 100644 --- a/website/docs/r/eventhub.html.markdown +++ b/website/docs/r/eventhub.html.markdown @@ -6,7 +6,7 @@ description: |- Creates a new Event Hubs as a nested resource within an Event Hubs namespace. --- -# azurerm\_eventhub +# azurerm_eventhub Creates a new Event Hubs as a nested resource within a Event Hubs namespace. @@ -20,7 +20,7 @@ resource "azurerm_resource_group" "test" { resource "azurerm_eventhub_namespace" "test" { name = "acceptanceTestEventHubNamespace" - location = "West US" + location = "${azurerm_resource_group.test.location}" resource_group_name = "${azurerm_resource_group.test.name}" sku = "Standard" capacity = 1 @@ -53,6 +53,34 @@ The following arguments are supported: * `message_retention` - (Required) Specifies the number of days to retain the events for this Event Hub. Needs to be between 1 and 7 days; or 1 day when using a Basic SKU for the parent EventHub Namespace. +* `capture_description` - (Optional) A `capture_description` block as defined below. + +--- + +A `capture_description` block supports the following: + +* `enabled` - (Required) Specifies if the Capture Description is Enabled. + +* `encoding` - (Required) Specifies the Encoding used for the Capture Description. Possible values are `Avro` and `AvroDeflate`. + +* `interval_in_seconds` - (Optional) Specifies the time interval in seconds at which the capture will happen. Values can be between `60` and `900` seconds. Defaults to `300` seconds. + +* `size_limit_in_bytes` - (Optional) Specifies the amount of data built up in your EventHub before a Capture Operation occurs. Value should be between `10485760` and `524288000` bytes. Defaults to `314572800` bytes. + +* `destination` - (Required) A `destination` block as defined below. + +A `destination` block supports the following: + +* `name` - (Required) The Name of the Destination where the capture should take place. At this time the only supported value is `EventHubArchive.AzureBlockBlob`. + +-> At this time it's only possible to Capture EventHub messages to Blob Storage. There's [a Feature Request for the Azure SDK to add support for Capturing messages to Azure Data Lake here](https://github.com/Azure/azure-rest-api-specs/issues/2255). + +* `archive_name_format` - The Blob naming convention for archiving. e.g. `{Namespace}/{EventHub}/{PartitionId}/{Year}/{Month}/{Day}/{Hour}/{Minute}/{Second}`. Here all the parameters (Namespace,EventHub .. etc) are mandatory irrespective of order + +* `blob_container_name` - (Required) The name of the Container within the Blob Storage Account where messages should be archived. + +* `storage_account_id` - (Required) The ID of the Blob Storage Account where messages should be archived. + ## Attributes Reference The following attributes are exported: