diff --git a/azurerm/config.go b/azurerm/config.go index 0664504fad9b..ff0a23d50b13 100644 --- a/azurerm/config.go +++ b/azurerm/config.go @@ -198,6 +198,7 @@ type ArmClient struct { msSqlElasticPoolsClient MsSql.ElasticPoolsClient sqlFirewallRulesClient sql.FirewallRulesClient sqlServersClient sql.ServersClient + sqlMiServersClient sql.ManagedInstancesClient sqlServerAzureADAdministratorsClient sql.ServerAzureADAdministratorsClient sqlVirtualNetworkRulesClient sql.VirtualNetworkRulesClient @@ -755,6 +756,10 @@ func (c *ArmClient) registerDatabases(endpoint, subscriptionId string, auth auto c.configureClient(&sqlSrvClient.Client, auth) c.sqlServersClient = sqlSrvClient + sqlMiSrvClient := sql.NewManagedInstancesClientWithBaseURI(endpoint, subscriptionId) + c.configureClient(&sqlMiSrvClient.Client, auth) + c.sqlMiServersClient = sqlMiSrvClient + sqlADClient := sql.NewServerAzureADAdministratorsClientWithBaseURI(endpoint, subscriptionId) c.configureClient(&sqlADClient.Client, auth) c.sqlServerAzureADAdministratorsClient = sqlADClient diff --git a/azurerm/provider.go b/azurerm/provider.go index a39fb930abc0..2134b664260e 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -323,6 +323,7 @@ func Provider() terraform.ResourceProvider { "azurerm_sql_database": resourceArmSqlDatabase(), "azurerm_sql_elasticpool": resourceArmSqlElasticPool(), "azurerm_sql_firewall_rule": resourceArmSqlFirewallRule(), + "azurerm_sql_managed_instance": resourceArmSqlMiServer(), "azurerm_sql_server": resourceArmSqlServer(), "azurerm_sql_virtual_network_rule": resourceArmSqlVirtualNetworkRule(), "azurerm_storage_account": resourceArmStorageAccount(), diff --git a/azurerm/resource_arm_sql_managed_instance.go b/azurerm/resource_arm_sql_managed_instance.go new file mode 100644 index 000000000000..b9028f732653 --- /dev/null +++ b/azurerm/resource_arm_sql_managed_instance.go @@ -0,0 +1,236 @@ +package azurerm + +import ( + "fmt" + "log" + + "github.com/Azure/azure-sdk-for-go/services/preview/sql/mgmt/2015-05-01-preview/sql" + "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/response" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/suppress" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmSqlMiServer() *schema.Resource { + return &schema.Resource{ + Create: resourceArmSqlMiServerCreateUpdate, + Read: resourceArmSqlMiServerRead, + Update: resourceArmSqlMiServerCreateUpdate, + Delete: resourceArmSqlMiServerDelete, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: azure.ValidateMsSqlServerName, + }, + + "location": locationSchema(), + + "resource_group_name": resourceGroupNameSchema(), + + "sku": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "GP_Gen4", + "GP_Gen5", + }, true), + DiffSuppressFunc: suppress.CaseDifference, + }, + + "capacity": { + Type: schema.TypeInt, + Computed: true, + }, + + "tier": { + Type: schema.TypeString, + Computed: true, + }, + + "family": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + + "administrator_login": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "administrator_login_password": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + }, + + "vcores": { + Type: schema.TypeInt, + Optional: true, + Default: 8, + ValidateFunc: validate.IntInSlice([]int{ + 8, + 16, + 24, + 32, + 40, + 64, + 80, + }), + }, + + "storage_size_in_gb": { + Type: schema.TypeInt, + Optional: true, + Default: 32, + ValidateFunc: validate.IntBetweenAndDivisibleBy(32, 8000, 32), + }, + + "license_type": { + Type: schema.TypeString, + Optional: true, + Default: "LicenseIncluded", + ValidateFunc: validation.StringInSlice([]string{ + "LicenseIncluded", + "BasePrice", + }, true), + }, + + "subnet_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: azure.ValidateResourceID, + }, + + "tags": tagsSchema(), + }, + } +} + +func resourceArmSqlMiServerCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).sqlMiServersClient + ctx := meta.(*ArmClient).StopContext + + name := d.Get("name").(string) + resGroup := d.Get("resource_group_name").(string) + location := azureRMNormalizeLocation(d.Get("location").(string)) + adminUsername := d.Get("administrator_login").(string) + licenseType := d.Get("license_type").(string) + subnetId := d.Get("subnet_id").(string) + + tags := d.Get("tags").(map[string]interface{}) + metadata := expandTags(tags) + + parameters := sql.ManagedInstance{ + Location: utils.String(location), + Tags: metadata, + ManagedInstanceProperties: &sql.ManagedInstanceProperties{ + LicenseType: utils.String(licenseType), + AdministratorLogin: utils.String(adminUsername), + SubnetID: utils.String(subnetId), + }, + } + + if d.HasChange("administrator_login_password") { + adminPassword := d.Get("administrator_login_password").(string) + parameters.ManagedInstanceProperties.AdministratorLoginPassword = utils.String(adminPassword) + } + + future, err := client.CreateOrUpdate(ctx, resGroup, name, parameters) + if err != nil { + return err + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + + if response.WasConflict(future.Response()) { + return fmt.Errorf("SQL Server names need to be globally unique and %q is already in use.", name) + } + + return err + } + + resp, err := client.Get(ctx, resGroup, name) + if err != nil { + return err + } + + d.SetId(*resp.ID) + + return resourceArmSqlServerRead(d, meta) +} + +func resourceArmSqlMiServerRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).sqlMiServersClient + ctx := meta.(*ArmClient).StopContext + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resGroup := id.ResourceGroup + name := id.Path["managedInstances"] + + resp, err := client.Get(ctx, resGroup, name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[INFO] Error reading SQL Server %q - removing from state", d.Id()) + d.SetId("") + return nil + } + + return fmt.Errorf("Error reading SQL Server %s: %v", name, err) + } + + d.Set("name", name) + d.Set("resource_group_name", resGroup) + if location := resp.Location; location != nil { + d.Set("location", azureRMNormalizeLocation(*location)) + } + + if miServerProperties := resp.ManagedInstanceProperties; miServerProperties != nil { + d.Set("license_type", miServerProperties.LicenseType) + d.Set("administrator_login", miServerProperties.AdministratorLogin) + d.Set("fully_qualified_domain_name", miServerProperties.FullyQualifiedDomainName) + } + + flattenAndSetTags(d, resp.Tags) + + return nil +} + +func resourceArmSqlMiServerDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).sqlMiServersClient + ctx := meta.(*ArmClient).StopContext + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resGroup := id.ResourceGroup + name := id.Path["managedInstances"] + + future, err := client.Delete(ctx, resGroup, name) + if err != nil { + return fmt.Errorf("Error deleting SQL Server %s: %+v", name, err) + } + + return future.WaitForCompletionRef(ctx, client.Client) +} diff --git a/azurerm/resource_arm_sql_managed_instance_test.go b/azurerm/resource_arm_sql_managed_instance_test.go new file mode 100644 index 000000000000..8b20089e009e --- /dev/null +++ b/azurerm/resource_arm_sql_managed_instance_test.go @@ -0,0 +1,197 @@ +package azurerm + +import ( + "fmt" + "testing" + + "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 TestAccAzureRMSqlMiServer_basic(t *testing.T) { + resourceName := "azurerm_sql_managed_instance.test" + ri := tf.AccRandTimeInt() + config := testAccAzureRMSqlMiServer_basic(ri, testLocation()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMSqlMiServerDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMSqlMiServerExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"administrator_login_password"}, + }, + }, + }) +} + +func TestAccAzureRMSqlMiServer_disappears(t *testing.T) { + resourceName := "azurerm_sql_managed_instance.test" + ri := tf.AccRandTimeInt() + config := testAccAzureRMSqlMiServer_basic(ri, testLocation()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMSqlMiServerDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMSqlMiServerExists(resourceName), + testCheckAzureRMSqlMiServerDisappears(resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testCheckAzureRMSqlMiServerExists(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) + } + + sqlServerName := 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 SQL Managed Instance: %s", sqlServerName) + } + + conn := testAccProvider.Meta().(*ArmClient).sqlMiServersClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + resp, err := conn.Get(ctx, resourceGroup, sqlServerName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: SQL Managed Instance %s (resource group: %s) does not exist", sqlServerName, resourceGroup) + } + return fmt.Errorf("Bad: Get SQL Managed Instance: %v", err) + } + + return nil + } +} + +func testCheckAzureRMSqlMiServerDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*ArmClient).sqlMiServersClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_sql_managed_instance" { + continue + } + + sqlServerName := rs.Primary.Attributes["name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + resp, err := conn.Get(ctx, resourceGroup, sqlServerName) + + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return nil + } + + return fmt.Errorf("Bad: Get SQL Managed Instance: %+v", err) + } + + return fmt.Errorf("SQL Managed Instance %s still exists", sqlServerName) + + } + + return nil +} + +func testCheckAzureRMSqlMiServerDisappears(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) + } + + resourceGroup := rs.Primary.Attributes["resource_group_name"] + serverName := rs.Primary.Attributes["name"] + + client := testAccProvider.Meta().(*ArmClient).sqlMiServersClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + future, err := client.Delete(ctx, resourceGroup, serverName) + if err != nil { + return err + } + + return future.WaitForCompletionRef(ctx, client.Client) + } +} + +func testAccAzureRMSqlMiServer_basic(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_virtual_network" "test" { + name = "acctest-vnet-%d" + resource_group_name = "${azurerm_resource_group.test.name}" + address_space = ["10.0.0.0/16"] + location = "${azurerm_resource_group.test.location}" +} + +resource "azurerm_subnet" "test" { + name = "subnet-%d" + resource_group_name = "${azurerm_resource_group.test.name}" + virtual_network_name = "${azurerm_virtual_network.test.name}" + address_prefix = "10.0.0.0/24" + route_table_id = "${azurerm_route_table.test.id}" +} + +resource "azurerm_route_table" "test" { + name = "routetable-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + disable_bgp_route_propagation = false + + route { + name = "RouteToAzureSqlMiMngSvc" + address_prefix = "0.0.0.0/0" + next_hop_type = "Internet" + } +} + +resource "azurerm_subnet_route_table_association" "test" { + subnet_id = "${azurerm_subnet.test.id}" + route_table_id = "${azurerm_route_table.test.id}" +} + +resource "azurerm_sql_managed_instance" "test" { + name = "acctestsqlserver%d" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" + administrator_login = "mradministrator" + administrator_login_password = "thisIsDog11" + license_type = "BasePrice" + subnet_id = "${azurerm_subnet.test.id}" + + tags { + environment = "staging" + database = "test" + } +} +`, rInt, location, rInt, rInt, rInt, rInt) +} diff --git a/website/docs/r/sql_managed_instance.html.markdown b/website/docs/r/sql_managed_instance.html.markdown new file mode 100644 index 000000000000..f4b5e21ff2ea --- /dev/null +++ b/website/docs/r/sql_managed_instance.html.markdown @@ -0,0 +1,82 @@ +--- +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_sql_managed_instance" +sidebar_current: "docs-azurerm-resource-database-sql-managed-instance" +description: |- + Manages a SQL Azure Managed Instance. + +--- + +# azurerm_sql_managed_instance + +Manages a SQL Azure Managed Instance. + +~> **Note:** All arguments including the administrator login and password will be stored in the raw state as plain-text. +[Read more about sensitive data in state](/docs/state/sensitive-data.html). + +## Example Usage + +```hcl +resource "azurerm_resource_group" "test" { + name = "database-rg" + location = "West Europe" +} + +resource "azurerm_sql_managed_instance" "test" { + name = "misqlserver" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" + license_type = "BasePrice" + administrator_login = "mradministrator" + administrator_login_password = "thisIsJpm81" + subnet_id = "${azurerm_subnet.test.id}" + + tags { + environment = "production" + } +} +``` +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the SQL Managed Instance. This needs to be globally unique within Azure. + +* `resource_group_name` - (Required) The name of the resource group in which to create the SQL Server. + +* `location` - (Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created. + +* `sku` - (Required) One `sku` blocks as defined below. + +* `vcores` - (Optional) Number of cores that should be assigned to your instance. Values can be 8, 16, or 24 if you select GP_Gen4 sku name, or 8, 16, 24, 32, or 40 if you select GP_Gen5. + +* `storage_size_in_gb` - (Optional) Maximum storage space for your instance. It should be multiple of 32GB. + +* `license_type` - License of the Managed Instance. Values can be PriceIncluded or BasePrice. + +* `administrator_login` - (Required) The administrator login name for the new server. Changing this forces a new resource to be created. + +* `administrator_login_password` - (Required) The password associated with the `administrator_login` user. Needs to comply with Azure's [Password Policy](https://msdn.microsoft.com/library/ms161959.aspx) + +* `tags` - (Optional) A mapping of tags to assign to the resource. + +--- + +A `sku` block supports the following: + +* `name` - (Required) Sku of the managed instance. Values can be GP_Gen4 or GP_Gen5. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The SQL Managed Instance ID. +* `fully_qualified_domain_name` - The fully qualified domain name of the Azure SQL Server (e.g. myServerName.database.windows.net) + +## Import + +SQL Servers can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_sql_managed_instance.test /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroup/providers/Microsoft.Sql/managedInstances/myserver +```