diff --git a/azurerm/internal/services/kusto/client.go b/azurerm/internal/services/kusto/client.go index 699620e3e25e..773eef803dfc 100644 --- a/azurerm/internal/services/kusto/client.go +++ b/azurerm/internal/services/kusto/client.go @@ -6,7 +6,8 @@ import ( ) type Client struct { - ClustersClient *kusto.ClustersClient + ClustersClient *kusto.ClustersClient + DatabasesClient *kusto.DatabasesClient } func BuildClient(o *common.ClientOptions) *Client { @@ -14,7 +15,11 @@ func BuildClient(o *common.ClientOptions) *Client { ClustersClient := kusto.NewClustersClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&ClustersClient.Client, o.ResourceManagerAuthorizer) + DatabasesClient := kusto.NewDatabasesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&DatabasesClient.Client, o.ResourceManagerAuthorizer) + return &Client{ - ClustersClient: &ClustersClient, + ClustersClient: &ClustersClient, + DatabasesClient: &DatabasesClient, } } diff --git a/azurerm/provider.go b/azurerm/provider.go index a8d83a8ffe9b..0979998b1b39 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -261,6 +261,7 @@ func Provider() terraform.ResourceProvider { "azurerm_key_vault": resourceArmKeyVault(), "azurerm_kubernetes_cluster": resourceArmKubernetesCluster(), "azurerm_kusto_cluster": resourceArmKustoCluster(), + "azurerm_kusto_database": resourceArmKustoDatabase(), "azurerm_lb_backend_address_pool": resourceArmLoadBalancerBackendAddressPool(), "azurerm_lb_nat_pool": resourceArmLoadBalancerNatPool(), "azurerm_lb_nat_rule": resourceArmLoadBalancerNatRule(), diff --git a/azurerm/resource_arm_kusto_database.go b/azurerm/resource_arm_kusto_database.go new file mode 100644 index 000000000000..b43d6cb19dd7 --- /dev/null +++ b/azurerm/resource_arm_kusto_database.go @@ -0,0 +1,220 @@ +package azurerm + +import ( + "fmt" + "log" + "regexp" + + "github.com/Azure/azure-sdk-for-go/services/kusto/mgmt/2019-01-21/kusto" + "github.com/hashicorp/terraform/helper/schema" + "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 resourceArmKustoDatabase() *schema.Resource { + return &schema.Resource{ + Create: resourceArmKustoDatabaseCreateUpdate, + Read: resourceArmKustoDatabaseRead, + Update: resourceArmKustoDatabaseCreateUpdate, + Delete: resourceArmKustoDatabaseDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateAzureRMKustoDatabaseName, + }, + + "resource_group_name": azure.SchemaResourceGroupName(), + + "location": azure.SchemaLocation(), + + "cluster_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateAzureRMKustoClusterName, + }, + + "soft_delete_period": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validate.ISO8601Duration, + }, + + "hot_cache_period": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validate.ISO8601Duration, + }, + + "size": { + Type: schema.TypeFloat, + Computed: true, + }, + }, + } +} + +func resourceArmKustoDatabaseCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).kusto.DatabasesClient + ctx := meta.(*ArmClient).StopContext + + log.Printf("[INFO] preparing arguments for Azure Kusto Database creation.") + + name := d.Get("name").(string) + resourceGroup := d.Get("resource_group_name").(string) + clusterName := d.Get("cluster_name").(string) + + if requireResourcesToBeImported && d.IsNewResource() { + server, err := client.Get(ctx, resourceGroup, clusterName, name) + if err != nil { + if !utils.ResponseWasNotFound(server.Response) { + return fmt.Errorf("Error checking for presence of existing Kusto Database %q (Resource Group %q, Cluster %q): %s", name, resourceGroup, clusterName, err) + } + } + + if server.ID != nil && *server.ID != "" { + return tf.ImportAsExistsError("azurerm_kusto_database", *server.ID) + } + } + + location := azure.NormalizeLocation(d.Get("location").(string)) + + databaseProperties := expandKustoDatabaseProperties(d) + + kustoDatabase := kusto.Database{ + Name: &name, + Location: &location, + DatabaseProperties: databaseProperties, + } + + future, err := client.CreateOrUpdate(ctx, resourceGroup, clusterName, name, kustoDatabase) + if err != nil { + return fmt.Errorf("Error creating or updating Kusto Cluster %q (Resource Group %q, Cluster %q): %+v", name, resourceGroup, clusterName, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("Error waiting for completion of Kusto Cluster %q (Resource Group %q, Cluster %q): %+v", name, resourceGroup, clusterName, err) + } + + resp, getDetailsErr := client.Get(ctx, resourceGroup, clusterName, name) + if getDetailsErr != nil { + return fmt.Errorf("Error retrieving Kusto Cluster %q (Resource Group %q, Cluster %q): %+v", name, resourceGroup, clusterName, err) + } + + if resp.ID == nil { + return fmt.Errorf("Cannot read ID for Kusto Cluster %q (Resource Group %q, Cluster %q)", name, resourceGroup, clusterName) + } + + d.SetId(*resp.ID) + + return resourceArmKustoDatabaseRead(d, meta) +} + +func resourceArmKustoDatabaseRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).kusto.DatabasesClient + ctx := meta.(*ArmClient).StopContext + + id, err := azure.ParseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resourceGroup := id.ResourceGroup + clusterName := id.Path["Clusters"] + name := id.Path["Databases"] + + databaseResponse, err := client.Get(ctx, resourceGroup, clusterName, name) + + if err != nil { + if utils.ResponseWasNotFound(databaseResponse.Response) { + d.SetId("") + return nil + } + return fmt.Errorf("Error retrieving Kusto Database %q (Resource Group %q, Cluster %q): %+v", name, resourceGroup, clusterName, err) + } + + d.Set("name", name) + d.Set("resource_group_name", resourceGroup) + d.Set("cluster_name", clusterName) + + if location := databaseResponse.Location; location != nil { + d.Set("location", azure.NormalizeLocation(*location)) + } + + if props := databaseResponse.DatabaseProperties; props != nil { + d.Set("hot_cache_period", props.HotCachePeriod) + d.Set("soft_delete_period", props.SoftDeletePeriod) + + if statistics := props.Statistics; statistics != nil { + d.Set("size", statistics.Size) + } + } + + return nil +} + +func resourceArmKustoDatabaseDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).kusto.DatabasesClient + ctx := meta.(*ArmClient).StopContext + + id, err := azure.ParseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resGroup := id.ResourceGroup + clusterName := id.Path["Clusters"] + name := id.Path["Databases"] + + future, err := client.Delete(ctx, resGroup, clusterName, name) + if err != nil { + return fmt.Errorf("Error deleting Kusto Database %q (Resource Group %q, Cluster %q): %+v", name, resGroup, clusterName, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("Error waiting for deletion of Kusto Database %q (Resource Group %q, Cluster %q): %+v", name, resGroup, clusterName, err) + } + + return nil +} + +func validateAzureRMKustoDatabaseName(v interface{}, k string) (warnings []string, errors []error) { + name := v.(string) + + if regexp.MustCompile(`^[\s]+$`).MatchString(name) { + errors = append(errors, fmt.Errorf("%q must not consist of whitespaces only", k)) + } + + if !regexp.MustCompile(`^[a-zA-Z0-9\s.-]+$`).MatchString(name) { + errors = append(errors, fmt.Errorf("%q may only contain alphanumeric characters, whitespaces, dashes and dots: %q", k, name)) + } + + if len(name) > 260 { + errors = append(errors, fmt.Errorf("%q must be (inclusive) between 4 and 22 characters long but is %d", k, len(name))) + } + + return warnings, errors +} + +func expandKustoDatabaseProperties(d *schema.ResourceData) *kusto.DatabaseProperties { + databaseProperties := &kusto.DatabaseProperties{} + + if softDeletePeriod, ok := d.GetOk("soft_delete_period"); ok { + databaseProperties.SoftDeletePeriod = utils.String(softDeletePeriod.(string)) + } + + if hotCachePeriod, ok := d.GetOk("hot_cache_period"); ok { + databaseProperties.HotCachePeriod = utils.String(hotCachePeriod.(string)) + } + + return databaseProperties +} diff --git a/azurerm/resource_arm_kusto_database_test.go b/azurerm/resource_arm_kusto_database_test.go new file mode 100644 index 000000000000..cbe2bc04017e --- /dev/null +++ b/azurerm/resource_arm_kusto_database_test.go @@ -0,0 +1,302 @@ +package azurerm + +import ( + "fmt" + "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" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMKustoDatabase_basic(t *testing.T) { + resourceName := "azurerm_kusto_database.test" + ri := tf.AccRandTimeInt() + rs := acctest.RandString(6) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMKustoDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMKustoDatabase_basic(ri, rs, testLocation()), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMKustoDatabaseExists(resourceName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureRMKustoDatabase_softDeletePeriod(t *testing.T) { + resourceName := "azurerm_kusto_database.test" + ri := tf.AccRandTimeInt() + rs := acctest.RandString(6) + preConfig := testAccAzureRMKustoDatabase_softDeletePeriod(ri, rs, testLocation()) + postConfig := testAccAzureRMKustoDatabase_softDeletePeriodUpdate(ri, rs, testLocation()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMKustoDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: preConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMKustoDatabaseExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "soft_delete_period", "P7D"), + ), + }, + { + Config: postConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMKustoDatabaseExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "soft_delete_period", "P31D"), + ), + }, + }, + }) +} + +func TestAccAzureRMKustoDatabase_hotCachePeriod(t *testing.T) { + resourceName := "azurerm_kusto_database.test" + ri := tf.AccRandTimeInt() + rs := acctest.RandString(6) + preConfig := testAccAzureRMKustoDatabase_hotCachePeriod(ri, rs, testLocation()) + postConfig := testAccAzureRMKustoDatabase_hotCachePeriodUpdate(ri, rs, testLocation()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMKustoDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: preConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMKustoDatabaseExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "hot_cache_period", "P7D"), + ), + }, + { + Config: postConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMKustoDatabaseExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "hot_cache_period", "P14DT12H"), + ), + }, + }, + }) +} + +func testAccAzureRMKustoDatabase_basic(rInt int, rs string, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "rg" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_kusto_cluster" "cluster" { + name = "acctestkc%s" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + + sku { + name = "Dev(No SLA)_Standard_D11_v2" + capacity = 1 + } +} + +resource "azurerm_kusto_database" "test" { + name = "acctestkd-%d" + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + cluster_name = azurerm_kusto_cluster.cluster.name +} +`, rInt, location, rs, rInt) +} + +func testAccAzureRMKustoDatabase_softDeletePeriod(rInt int, rs string, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "rg" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_kusto_cluster" "cluster" { + name = "acctestkc%s" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + + sku { + name = "Dev(No SLA)_Standard_D11_v2" + capacity = 1 + } +} + +resource "azurerm_kusto_database" "test" { + name = "acctestkd-%d" + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + cluster_name = azurerm_kusto_cluster.cluster.name + + soft_delete_period = "P7D" +} +`, rInt, location, rs, rInt) +} + +func testAccAzureRMKustoDatabase_softDeletePeriodUpdate(rInt int, rs string, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "rg" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_kusto_cluster" "cluster" { + name = "acctestkc%s" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + + sku { + name = "Dev(No SLA)_Standard_D11_v2" + capacity = 1 + } +} + +resource "azurerm_kusto_database" "test" { + name = "acctestkd-%d" + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + cluster_name = azurerm_kusto_cluster.cluster.name + + soft_delete_period = "P31D" +} +`, rInt, location, rs, rInt) +} + +func testAccAzureRMKustoDatabase_hotCachePeriod(rInt int, rs string, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "rg" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_kusto_cluster" "cluster" { + name = "acctestkc%s" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + + sku { + name = "Dev(No SLA)_Standard_D11_v2" + capacity = 1 + } +} + +resource "azurerm_kusto_database" "test" { + name = "acctestkd-%d" + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + cluster_name = azurerm_kusto_cluster.cluster.name + + hot_cache_period = "P7D" +} +`, rInt, location, rs, rInt) +} + +func testAccAzureRMKustoDatabase_hotCachePeriodUpdate(rInt int, rs string, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "rg" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_kusto_cluster" "cluster" { + name = "acctestkc%s" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + + sku { + name = "Dev(No SLA)_Standard_D11_v2" + capacity = 1 + } +} + +resource "azurerm_kusto_database" "test" { + name = "acctestkd-%d" + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + cluster_name = azurerm_kusto_cluster.cluster.name + + hot_cache_period = "P14DT12H" +} +`, rInt, location, rs, rInt) +} + +func testCheckAzureRMKustoDatabaseDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*ArmClient).kusto.DatabasesClient + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_kusto_database" { + continue + } + + resourceGroup := rs.Primary.Attributes["resource_group_name"] + clusterName := rs.Primary.Attributes["cluster_name"] + name := rs.Primary.Attributes["name"] + + ctx := testAccProvider.Meta().(*ArmClient).StopContext + resp, err := client.Get(ctx, resourceGroup, clusterName, name) + + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return nil + } + return err + } + + return nil + } + + return nil +} + +func testCheckAzureRMKustoDatabaseExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + // Ensure we have enough information in state to look up in API + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + kustoDatabase := rs.Primary.Attributes["name"] + resourceGroup, hasResourceGroup := rs.Primary.Attributes["resource_group_name"] + if !hasResourceGroup { + return fmt.Errorf("Bad: no resource group found in state for Kusto Database: %s", kustoDatabase) + } + + clusterName, hasClusterName := rs.Primary.Attributes["cluster_name"] + if !hasClusterName { + return fmt.Errorf("Bad: no resource group found in state for Kusto Database: %s", kustoDatabase) + } + + client := testAccProvider.Meta().(*ArmClient).kusto.DatabasesClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + resp, err := client.Get(ctx, resourceGroup, clusterName, kustoDatabase) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: Kusto Database %q (resource group: %q, cluster: %q) does not exist", kustoDatabase, resourceGroup, clusterName) + } + + return fmt.Errorf("Bad: Get on DatabasesClient: %+v", err) + } + + return nil + } +} diff --git a/website/docs/r/kusto_database.html.markdown b/website/docs/r/kusto_database.html.markdown new file mode 100644 index 000000000000..a8131081f71d --- /dev/null +++ b/website/docs/r/kusto_database.html.markdown @@ -0,0 +1,76 @@ +--- +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_kusto_database" +sidebar_current: "docs-azurerm-resource-kusto-database" +description: |- + Manages Kusto / Data Explorer Database +--- + +# azurerm_kusto_database + +Manages a Kusto (also known as Azure Data Explorer) Database + +## Example Usage + +```hcl +resource "azurerm_resource_group" "rg" { + name = "my-kusto-rg" + location = "East US" +} + +resource "azurerm_kusto_cluster" "cluster" { + name = "kustocluster" + location = "${azurerm_resource_group.rg.location}" + resource_group_name = "${azurerm_resource_group.rg.name}" + + sku { + name = "Standard_D13_v2" + capacity = 2 + } +} + +resource "azurerm_kusto_database" "database" { + name = "my-kusto-database" + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + cluster_name = azurerm_kusto_cluster.cluster.name + + hot_cache_period = "P7D" + soft_delete_period = "P31D" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the Kusto Database to create. Changing this forces a new resource to be created. + +* `location` - (Required) The location where the Kusto Database should be created. Changing this forces a new resource to be created. + +* `resource_group_name` - (Required) Specifies the Resource Group where the Kusto Database should exist. Changing this forces a new resource to be created. + +* `cluster_name` - (Required) Specifies the name of the Kusto Cluster this database will be added to. Changing this forces a new resource to be created. + +* `hot_cache_period` - (Optional) The time the data that should be kept in cache for fast queries as ISO 8601 timespan. Default is unlimited. For more information see: [ISO 8601 Timespan](https://en.wikipedia.org/wiki/ISO_8601#Durations) + +* `soft_delete_period` - (Optional) The time the data should be kept before it stops being accessible to queries as ISO 8601 timespan. Default is unlimited. For more information see: [ISO 8601 Timespan](https://en.wikipedia.org/wiki/ISO_8601#Durations) + + +## Attributes Reference + +The following attributes are exported: + +* `id` - The Kusto Cluster ID. + +* `size` - The size of the database in bytes. + +--- + +## Import + +Kusto Clusters can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_kusto_database.test /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Kusto/Clusters/cluster1/Databases/database1 +```