diff --git a/azurerm/helpers/validate/float.go b/azurerm/helpers/validate/float.go new file mode 100644 index 000000000000..24ec42bc741f --- /dev/null +++ b/azurerm/helpers/validate/float.go @@ -0,0 +1,28 @@ +package validate + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +// FloatInSlice returns a SchemaValidateFunc which tests if the provided value +// is of type float64 and matches the value of an element in the valid slice +func FloatInSlice(valid []float64) schema.SchemaValidateFunc { + return func(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(float64) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %s to be float", i)) + return warnings, errors + } + + for _, validFloat := range valid { + if v == validFloat { + return warnings, errors + } + } + + errors = append(errors, fmt.Errorf("expected %s to be one of %v, got %f", k, valid, v)) + return warnings, errors + } +} diff --git a/azurerm/helpers/validate/float_test.go b/azurerm/helpers/validate/float_test.go new file mode 100644 index 000000000000..02e0d43923d2 --- /dev/null +++ b/azurerm/helpers/validate/float_test.go @@ -0,0 +1,50 @@ +package validate + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func TestValidateFloatInSlice(t *testing.T) { + cases := map[string]struct { + Value interface{} + ValidateFunc schema.SchemaValidateFunc + ExpectValidationErrors bool + }{ + "accept valid value": { + Value: 1.5, + ValidateFunc: FloatInSlice([]float64{1.0, 1.5, 2.0}), + ExpectValidationErrors: false, + }, + "accept valid negative value ": { + Value: -1.0, + ValidateFunc: FloatInSlice([]float64{-1.0, 2.0}), + ExpectValidationErrors: false, + }, + "accept zero": { + Value: 0.0, + ValidateFunc: FloatInSlice([]float64{0.0, 2.0}), + ExpectValidationErrors: false, + }, + "reject out of range value": { + Value: -1.0, + ValidateFunc: FloatInSlice([]float64{0.0, 2.0}), + ExpectValidationErrors: true, + }, + "reject incorrectly typed value": { + Value: 1, + ValidateFunc: FloatInSlice([]float64{0, 1, 2}), + ExpectValidationErrors: true, + }, + } + + for tn, tc := range cases { + _, errors := tc.ValidateFunc(tc.Value, tn) + if len(errors) > 0 && !tc.ExpectValidationErrors { + t.Errorf("%s: unexpected errors %s", tn, errors) + } else if len(errors) == 0 && tc.ExpectValidationErrors { + t.Errorf("%s: expected errors but got none", tn) + } + } +} diff --git a/azurerm/internal/services/mssql/client/client.go b/azurerm/internal/services/mssql/client/client.go index 40d5ee615d74..9f504ef8db51 100644 --- a/azurerm/internal/services/mssql/client/client.go +++ b/azurerm/internal/services/mssql/client/client.go @@ -6,13 +6,18 @@ import ( ) type Client struct { + DatabasesClient *sql.DatabasesClient ElasticPoolsClient *sql.ElasticPoolsClient DatabaseVulnerabilityAssessmentRuleBaselinesClient *sql.DatabaseVulnerabilityAssessmentRuleBaselinesClient + ServersClient *sql.ServersClient ServerSecurityAlertPoliciesClient *sql.ServerSecurityAlertPoliciesClient ServerVulnerabilityAssessmentsClient *sql.ServerVulnerabilityAssessmentsClient } func NewClient(o *common.ClientOptions) *Client { + DatabasesClient := sql.NewDatabasesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&DatabasesClient.Client, o.ResourceManagerAuthorizer) + ElasticPoolsClient := sql.NewElasticPoolsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&ElasticPoolsClient.Client, o.ResourceManagerAuthorizer) @@ -22,13 +27,18 @@ func NewClient(o *common.ClientOptions) *Client { ServerSecurityAlertPoliciesClient := sql.NewServerSecurityAlertPoliciesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&ServerSecurityAlertPoliciesClient.Client, o.ResourceManagerAuthorizer) + ServersClient := sql.NewServersClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&ServersClient.Client, o.ResourceManagerAuthorizer) + ServerVulnerabilityAssessmentsClient := sql.NewServerVulnerabilityAssessmentsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&ServerVulnerabilityAssessmentsClient.Client, o.ResourceManagerAuthorizer) return &Client{ + DatabasesClient: &DatabasesClient, ElasticPoolsClient: &ElasticPoolsClient, DatabaseVulnerabilityAssessmentRuleBaselinesClient: &DatabaseVulnerabilityAssessmentRuleBaselinesClient, - ServerSecurityAlertPoliciesClient: &ServerSecurityAlertPoliciesClient, - ServerVulnerabilityAssessmentsClient: &ServerVulnerabilityAssessmentsClient, + ServersClient: &ServersClient, + ServerSecurityAlertPoliciesClient: &ServerSecurityAlertPoliciesClient, + ServerVulnerabilityAssessmentsClient: &ServerVulnerabilityAssessmentsClient, } } diff --git a/azurerm/internal/services/mssql/data_source_mssql_database.go b/azurerm/internal/services/mssql/data_source_mssql_database.go new file mode 100644 index 000000000000..b3d57ae76b4f --- /dev/null +++ b/azurerm/internal/services/mssql/data_source_mssql_database.go @@ -0,0 +1,129 @@ +package mssql + +import ( + "fmt" + "time" + + "github.com/Azure/azure-sdk-for-go/services/preview/sql/mgmt/v3.0/sql" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/mssql/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/mssql/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func dataSourceArmMsSqlDatabase() *schema.Resource { + return &schema.Resource{ + Read: dataSourceArmMsSqlDatabaseRead, + + Timeouts: &schema.ResourceTimeout{ + Read: schema.DefaultTimeout(5 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: azure.ValidateMsSqlDatabaseName, + }, + + "server_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validate.MsSqlServerID, + }, + + "collation": { + Type: schema.TypeString, + Computed: true, + }, + + "elastic_pool_id": { + Type: schema.TypeString, + Computed: true, + }, + + "license_type": { + Type: schema.TypeString, + Computed: true, + }, + + "max_size_gb": { + Type: schema.TypeInt, + Computed: true, + }, + + "read_replica_count": { + Type: schema.TypeInt, + Computed: true, + }, + + "read_scale": { + Type: schema.TypeBool, + Computed: true, + }, + + "sku_name": { + Type: schema.TypeString, + Computed: true, + }, + + "zone_redundant": { + Type: schema.TypeBool, + Computed: true, + }, + + "tags": tags.SchemaDataSource(), + }, + } +} + +func dataSourceArmMsSqlDatabaseRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).MSSQL.DatabasesClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + name := d.Get("name").(string) + mssqlServerId := d.Get("server_id").(string) + serverId, err := parse.MsSqlServerID(mssqlServerId) + if err != nil { + return err + } + + resp, err := client.Get(ctx, serverId.ResourceGroup, serverId.Name, name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Database %q (Resource Group %q, SQL Server %q) was not found", name, serverId.ResourceGroup, serverId.Name) + } + + return fmt.Errorf("Failure in making Read request on AzureRM Database %s (Resource Group %q, SQL Server %q): %+v", name, serverId.ResourceGroup, serverId.Name, err) + } + + if id := resp.ID; id != nil { + d.SetId(*resp.ID) + } + d.Set("name", name) + d.Set("server_id", mssqlServerId) + + if props := resp.DatabaseProperties; props != nil { + d.Set("collation", props.Collation) + d.Set("elastic_pool_id", props.ElasticPoolID) + d.Set("license_type", props.LicenseType) + if props.MaxSizeBytes != nil { + d.Set("max_size_gb", int32((*props.MaxSizeBytes)/int64(1073741824))) + } + d.Set("read_replica_count", props.ReadReplicaCount) + if props.ReadScale == sql.DatabaseReadScaleEnabled { + d.Set("read_scale", true) + } else if props.ReadScale == sql.DatabaseReadScaleDisabled { + d.Set("read_scale", false) + } + d.Set("sku_name", props.CurrentServiceObjectiveName) + d.Set("zone_redundant", props.ZoneRedundant) + } + + return tags.FlattenAndSet(d, resp.Tags) +} diff --git a/azurerm/internal/services/mssql/parse/mssql.go b/azurerm/internal/services/mssql/parse/mssql.go new file mode 100644 index 000000000000..c30862557d7b --- /dev/null +++ b/azurerm/internal/services/mssql/parse/mssql.go @@ -0,0 +1,95 @@ +package parse + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type MsSqlDatabaseId struct { + Name string + MsSqlServer string + ResourceGroup string +} + +type MsSqlServerId struct { + Name string + ResourceGroup string +} + +type MsSqlElasticPoolId struct { + Name string + MsSqlServer string + ResourceGroup string +} + +func MsSqlDatabaseID(input string) (*MsSqlDatabaseId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("Unable to parse MsSql Database ID %q: %+v", input, err) + } + + database := MsSqlDatabaseId{ + ResourceGroup: id.ResourceGroup, + } + + if database.MsSqlServer, err = id.PopSegment("servers"); err != nil { + return nil, err + } + + if database.Name, err = id.PopSegment("databases"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &database, nil +} + +func MsSqlServerID(input string) (*MsSqlServerId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("Unable to parse MsSql Server ID %q: %+v", input, err) + } + + server := MsSqlServerId{ + ResourceGroup: id.ResourceGroup, + } + + if server.Name, err = id.PopSegment("servers"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &server, nil +} + +func MSSqlElasticPoolID(input string) (*MsSqlElasticPoolId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("Unable to parse MsSql Elastic Pool ID %q: %+v", input, err) + } + + elasticPool := MsSqlElasticPoolId{ + ResourceGroup: id.ResourceGroup, + } + + if elasticPool.MsSqlServer, err = id.PopSegment("servers"); err != nil { + return nil, err + } + + if elasticPool.Name, err = id.PopSegment("elasticPools"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &elasticPool, nil +} diff --git a/azurerm/internal/services/mssql/parse/mssql_test.go b/azurerm/internal/services/mssql/parse/mssql_test.go new file mode 100644 index 000000000000..4e304333f26f --- /dev/null +++ b/azurerm/internal/services/mssql/parse/mssql_test.go @@ -0,0 +1,156 @@ +package parse + +import ( + "testing" +) + +func TestMsSqlDatabaseID(t *testing.T) { + testData := []struct { + Name string + Input string + Expected *MsSqlDatabaseId + }{ + { + Name: "Empty", + Input: "", + Expected: nil, + }, + { + Name: "No Resource Groups Segment", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Expected: nil, + }, + { + Name: "No Resource Groups Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/", + Expected: nil, + }, + { + Name: "Resource Group ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/foo/", + Expected: nil, + }, + { + Name: "Missing Sql Server Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Sql/servers/", + Expected: nil, + }, + { + Name: "Missing Sql Database", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Sql/servers/sqlServer1", + Expected: nil, + }, + { + Name: "Missing Sql Database Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Sql/servers/sqlServer1/databases", + Expected: nil, + }, + { + Name: "Sql Database ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Sql/servers/sqlServer1/databases/sqlDB1", + Expected: &MsSqlDatabaseId{ + Name: "sqlDB1", + MsSqlServer: "sqlServer1", + ResourceGroup: "resGroup1", + }, + }, + { + Name: "Wrong Casing", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Sql/servers/sqlServer1/Databases/sqlDB1", + Expected: nil, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Name) + + actual, err := MsSqlDatabaseID(v.Input) + if err != nil { + if v.Expected == nil { + continue + } + + t.Fatalf("Expected a value but got an error: %s", err) + } + + if actual.Name != v.Expected.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expected.Name, actual.Name) + } + + if actual.MsSqlServer != v.Expected.MsSqlServer { + t.Fatalf("Expected %q but got %q for Sql Server", v.Expected.Name, actual.Name) + } + + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for Resource Group", v.Expected.ResourceGroup, actual.ResourceGroup) + } + } +} + +func TestMsSqlServerID(t *testing.T) { + testData := []struct { + Name string + Input string + Expected *MsSqlServerId + }{ + { + Name: "Empty", + Input: "", + Expected: nil, + }, + { + Name: "No Resource Groups Segment", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Expected: nil, + }, + { + Name: "No Resource Groups Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/", + Expected: nil, + }, + { + Name: "Resource Group ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/foo/", + Expected: nil, + }, + { + Name: "Missing Sql Server Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Sql/servers/", + Expected: nil, + }, + { + Name: "Sql Server", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Sql/servers/sqlServer1", + Expected: &MsSqlServerId{ + Name: "sqlServer1", + ResourceGroup: "resGroup1", + }, + }, + { + Name: "Wrong Casing", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Sql/Servers/sqlServer1", + Expected: nil, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Name) + + actual, err := MsSqlServerID(v.Input) + if err != nil { + if v.Expected == nil { + continue + } + + t.Fatalf("Expected a value but got an error: %s", err) + } + + if actual.Name != v.Expected.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expected.Name, actual.Name) + } + + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for Resource Group", v.Expected.ResourceGroup, actual.ResourceGroup) + } + } +} diff --git a/azurerm/internal/services/mssql/registration.go b/azurerm/internal/services/mssql/registration.go index d2f13db37bc5..9b14b0f795bc 100644 --- a/azurerm/internal/services/mssql/registration.go +++ b/azurerm/internal/services/mssql/registration.go @@ -21,6 +21,7 @@ func (r Registration) WebsiteCategories() []string { // SupportedDataSources returns the supported Data Sources supported by this Service func (r Registration) SupportedDataSources() map[string]*schema.Resource { return map[string]*schema.Resource{ + "azurerm_mssql_database": dataSourceArmMsSqlDatabase(), "azurerm_mssql_elasticpool": dataSourceArmMsSqlElasticpool(), } } @@ -28,8 +29,9 @@ func (r Registration) SupportedDataSources() map[string]*schema.Resource { // SupportedResources returns the supported Resources supported by this Service func (r Registration) SupportedResources() map[string]*schema.Resource { return map[string]*schema.Resource{ - "azurerm_mssql_elasticpool": resourceArmMsSqlElasticPool(), + "azurerm_mssql_database": resourceArmMsSqlDatabase(), "azurerm_mssql_database_vulnerability_assessment_rule_baseline": resourceArmMssqlDatabaseVulnerabilityAssessmentRuleBaseline(), + "azurerm_mssql_elasticpool": resourceArmMsSqlElasticPool(), "azurerm_mssql_server_security_alert_policy": resourceArmMssqlServerSecurityAlertPolicy(), "azurerm_mssql_server_vulnerability_assessment": resourceArmMssqlServerVulnerabilityAssessment(), } diff --git a/azurerm/internal/services/mssql/resource_arm_mssql_database.go b/azurerm/internal/services/mssql/resource_arm_mssql_database.go new file mode 100644 index 000000000000..ca2219dd4612 --- /dev/null +++ b/azurerm/internal/services/mssql/resource_arm_mssql_database.go @@ -0,0 +1,370 @@ +package mssql + +import ( + "fmt" + "log" + "time" + + "github.com/Azure/azure-sdk-for-go/services/preview/sql/mgmt/v3.0/sql" + "github.com/Azure/go-autorest/autorest/date" + "github.com/hashicorp/go-azure-helpers/response" + "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/suppress" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + azValidate "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/mssql/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/mssql/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" + azSchema "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmMsSqlDatabase() *schema.Resource { + return &schema.Resource{ + Create: resourceArmMsSqlDatabaseCreateUpdate, + Read: resourceArmMsSqlDatabaseRead, + Update: resourceArmMsSqlDatabaseCreateUpdate, + Delete: resourceArmMsSqlDatabaseDelete, + Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error { + _, err := parse.MsSqlDatabaseID(id) + return err + }), + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(60 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(60 * time.Minute), + Delete: schema.DefaultTimeout(60 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: azure.ValidateMsSqlDatabaseName, + }, + + "server_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.MsSqlServerID, + }, + + "auto_pause_delay_in_minutes": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ValidateFunc: validate.MsSqlDatabaseAutoPauseDelay, + }, + + //recovery is not support in version 2017-10-01-preview + "create_mode": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{ + string(sql.CreateModeCopy), + string(sql.CreateModeDefault), + string(sql.CreateModeOnlineSecondary), + string(sql.CreateModePointInTimeRestore), + string(sql.CreateModeRestore), + string(sql.CreateModeRestoreExternalBackup), + string(sql.CreateModeRestoreExternalBackupSecondary), + string(sql.CreateModeRestoreLongTermRetentionBackup), + string(sql.CreateModeSecondary), + }, false), + }, + + "collation": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validate.MsSqlDBCollation(), + }, + + "elastic_pool_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validate.MsSqlElasticPoolID, + }, + + "license_type": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{ + string(sql.BasePrice), + string(sql.LicenseIncluded), + }, false), + }, + + "max_size_gb": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ValidateFunc: validation.IntBetween(1, 4096), + }, + + "min_capacity": { + Type: schema.TypeFloat, + Optional: true, + Computed: true, + ValidateFunc: azValidate.FloatInSlice([]float64{0.5, 0.75, 1, 1.25, 1.5, 1.75, 2}), + }, + + "restore_point_in_time": { + Type: schema.TypeString, + Optional: true, + Computed: true, + DiffSuppressFunc: suppress.RFC3339Time, + ValidateFunc: validation.IsRFC3339Time, + }, + + "read_replica_count": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ValidateFunc: validation.IntBetween(0, 4), + }, + + "read_scale": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + + "sample_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{ + string(sql.AdventureWorksLT), + }, false), + }, + + // hyper_scale can not be changed into other sku + "sku_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + ValidateFunc: validate.MsSqlDBSkuName(), + DiffSuppressFunc: suppress.CaseDifference, + }, + + "creation_source_database_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + ValidateFunc: validate.MsSqlDatabaseID, + }, + + "zone_redundant": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + + "tags": tags.Schema(), + }, + } +} + +func resourceArmMsSqlDatabaseCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).MSSQL.DatabasesClient + serverClient := meta.(*clients.Client).MSSQL.ServersClient + ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) + defer cancel() + + log.Printf("[INFO] preparing arguments for MsSql Database creation.") + + name := d.Get("name").(string) + sqlServerId := d.Get("server_id").(string) + serverId, _ := parse.MsSqlServerID(sqlServerId) + + if features.ShouldResourcesBeImported() && d.IsNewResource() { + existing, err := client.Get(ctx, serverId.ResourceGroup, serverId.Name, name) + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("Failed to check for presence of existing Database %q (MsSql Server %q / Resource Group %q): %s", name, serverId.Name, serverId.ResourceGroup, err) + } + } + + if existing.ID != nil && *existing.ID != "" { + return tf.ImportAsExistsError("azurerm_mssql_database", *existing.ID) + } + } + + serverResp, err := serverClient.Get(ctx, serverId.ResourceGroup, serverId.Name) + if err != nil { + return fmt.Errorf("Failure in making Read request on MsSql Server %q (Resource Group %q): %s", serverId.Name, serverId.ResourceGroup, err) + } + + location := *serverResp.Location + if location == "" { + return fmt.Errorf("Location is empty from making Read request on MsSql Server %q", serverId.Name) + } + + params := sql.Database{ + Name: &name, + Location: &location, + DatabaseProperties: &sql.DatabaseProperties{ + AutoPauseDelay: utils.Int32(int32(d.Get("auto_pause_delay_in_minutes").(int))), + Collation: utils.String(d.Get("collation").(string)), + ElasticPoolID: utils.String(d.Get("elastic_pool_id").(string)), + LicenseType: sql.DatabaseLicenseType(d.Get("license_type").(string)), + MinCapacity: utils.Float(d.Get("min_capacity").(float64)), + ReadReplicaCount: utils.Int32(int32(d.Get("read_replica_count").(int))), + SampleName: sql.SampleName(d.Get("sample_name").(string)), + ZoneRedundant: utils.Bool(d.Get("zone_redundant").(bool)), + }, + + Tags: tags.Expand(d.Get("tags").(map[string]interface{})), + } + + if v, ok := d.GetOk("create_mode"); ok { + if _, ok := d.GetOk("creation_source_database_id"); (v.(string) == string(sql.CreateModeCopy) || v.(string) == string(sql.CreateModePointInTimeRestore) || v.(string) == string(sql.CreateModeRestore) || v.(string) == string(sql.CreateModeSecondary)) && !ok { + return fmt.Errorf("'creation_source_database_id' is required for create_mode %s", v.(string)) + } + params.DatabaseProperties.CreateMode = sql.CreateMode(v.(string)) + } + + if v, ok := d.GetOk("max_size_gb"); ok { + params.DatabaseProperties.MaxSizeBytes = utils.Int64(int64(v.(int) * 1073741824)) + } + + if v, ok := d.GetOkExists("read_scale"); ok { + if v.(bool) { + params.DatabaseProperties.ReadScale = sql.DatabaseReadScaleEnabled + } else { + params.DatabaseProperties.ReadScale = sql.DatabaseReadScaleDisabled + } + } + + if v, ok := d.GetOk("restore_point_in_time"); ok { + if cm, ok := d.GetOk("create_mode"); ok && cm.(string) != string(sql.CreateModePointInTimeRestore) { + return fmt.Errorf("'restore_point_in_time' is supported only for create_mode %s", string(sql.CreateModePointInTimeRestore)) + } + restorePointInTime, _ := time.Parse(time.RFC3339, v.(string)) + params.DatabaseProperties.RestorePointInTime = &date.Time{Time: restorePointInTime} + } + + if v, ok := d.GetOk("sku_name"); ok { + params.Sku = &sql.Sku{ + Name: utils.String(v.(string)), + } + } + + if v, ok := d.GetOk("creation_source_database_id"); ok { + params.DatabaseProperties.SourceDatabaseID = utils.String(v.(string)) + } + + future, err := client.CreateOrUpdate(ctx, serverId.ResourceGroup, serverId.Name, name, params) + if err != nil { + return fmt.Errorf("Failure in creating MsSql Database %q (Sql Server %q / Resource Group %q): %+v", name, serverId.Name, serverId.ResourceGroup, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("Failure in waiting for creation of MsSql Database %q (MsSql Server Name %q / Resource Group %q): %+v", name, serverId.Name, serverId.ResourceGroup, err) + } + + read, err := client.Get(ctx, serverId.ResourceGroup, serverId.Name, name) + if err != nil { + return fmt.Errorf("Failure in retrieving MsSql Database %q (MsSql Server Name %q / Resource Group %q): %+v", name, serverId.Name, serverId.ResourceGroup, err) + } + + if read.ID == nil || *read.ID == "" { + return fmt.Errorf("Cannot read MsSql Database %q (MsSql Server Name %q / Resource Group %q) ID", name, serverId.Name, serverId.ResourceGroup) + } + + d.SetId(*read.ID) + + return resourceArmMsSqlDatabaseRead(d, meta) +} + +func resourceArmMsSqlDatabaseRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).MSSQL.DatabasesClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + databaseId, err := parse.MsSqlDatabaseID(d.Id()) + if err != nil { + return err + } + + resp, err := client.Get(ctx, databaseId.ResourceGroup, databaseId.MsSqlServer, databaseId.Name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + d.SetId("") + return nil + } + return fmt.Errorf("Failure in reading MsSql Database %s (MsSql Server Name %q / Resource Group %q): %s", databaseId.Name, databaseId.MsSqlServer, databaseId.ResourceGroup, err) + } + + d.Set("name", resp.Name) + + serverClient := meta.(*clients.Client).MSSQL.ServersClient + + serverResp, err := serverClient.Get(ctx, databaseId.ResourceGroup, databaseId.MsSqlServer) + if err != nil || *serverResp.ID == "" { + return fmt.Errorf("Failure in making Read request on MsSql Server %q (Resource Group %q): %s", databaseId.MsSqlServer, databaseId.ResourceGroup, err) + } + d.Set("server_id", serverResp.ID) + + if props := resp.DatabaseProperties; props != nil { + d.Set("auto_pause_delay_in_minutes", props.AutoPauseDelay) + d.Set("collation", props.Collation) + d.Set("elastic_pool_id", props.ElasticPoolID) + d.Set("license_type", props.LicenseType) + if props.MaxSizeBytes != nil { + d.Set("max_size_gb", int32((*props.MaxSizeBytes)/int64(1073741824))) + } + d.Set("min_capacity", props.MinCapacity) + d.Set("read_replica_count", props.ReadReplicaCount) + if props.ReadScale == sql.DatabaseReadScaleEnabled { + d.Set("read_scale", true) + } else if props.ReadScale == sql.DatabaseReadScaleDisabled { + d.Set("read_scale", false) + } + d.Set("sku_name", props.CurrentServiceObjectiveName) + d.Set("zone_redundant", props.ZoneRedundant) + } + + return tags.FlattenAndSet(d, resp.Tags) +} + +func resourceArmMsSqlDatabaseDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).MSSQL.DatabasesClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.MsSqlDatabaseID(d.Id()) + if err != nil { + return err + } + + future, err := client.Delete(ctx, id.ResourceGroup, id.MsSqlServer, id.Name) + if err != nil { + return fmt.Errorf("Failure in deleting MsSql Database %q ( MsSql Server %q / Resource Group %q): %+v", id.Name, id.MsSqlServer, id.ResourceGroup, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + if response.WasNotFound(future.Response()) { + return nil + } + return fmt.Errorf("Failure in waiting for MsSql Database %q ( MsSql Server %q / Resource Group %q) to be deleted: %+v", id.Name, id.MsSqlServer, id.ResourceGroup, err) + } + + return nil +} diff --git a/azurerm/internal/services/mssql/resource_arm_mssql_elasticpool.go b/azurerm/internal/services/mssql/resource_arm_mssql_elasticpool.go index ab4603acac80..cfb027789c00 100644 --- a/azurerm/internal/services/mssql/resource_arm_mssql_elasticpool.go +++ b/azurerm/internal/services/mssql/resource_arm_mssql_elasticpool.go @@ -14,6 +14,7 @@ import ( "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/features" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/mssql/parse" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" @@ -243,28 +244,28 @@ func resourceArmMsSqlElasticPoolRead(d *schema.ResourceData, meta interface{}) e ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) defer cancel() - resGroup, serverName, name, err := parseArmMsSqlElasticPoolId(d.Id()) + elasticPool, err := parse.MSSqlElasticPoolID(d.Id()) if err != nil { return err } - resp, err := client.Get(ctx, resGroup, serverName, name) + resp, err := client.Get(ctx, elasticPool.ResourceGroup, elasticPool.MsSqlServer, elasticPool.Name) if err != nil { if utils.ResponseWasNotFound(resp.Response) { d.SetId("") return nil } - return fmt.Errorf("Error making Read request on MsSql Elastic Pool %s: %s", name, err) + return fmt.Errorf("Error making Read request on MsSql Elastic Pool %s: %s", elasticPool.Name, err) } d.Set("name", resp.Name) - d.Set("resource_group_name", resGroup) + d.Set("resource_group_name", elasticPool.ResourceGroup) if location := resp.Location; location != nil { d.Set("location", azure.NormalizeLocation(*location)) } - d.Set("server_name", serverName) + d.Set("server_name", elasticPool.MsSqlServer) if err := d.Set("sku", flattenAzureRmMsSqlElasticPoolSku(resp.Sku)); err != nil { return fmt.Errorf("Error setting `sku`: %+v", err) @@ -294,24 +295,15 @@ func resourceArmMsSqlElasticPoolDelete(d *schema.ResourceData, meta interface{}) ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) defer cancel() - resGroup, serverName, name, err := parseArmMSSqlElasticPoolId(d.Id()) + elasticPool, err := parse.MSSqlElasticPoolID(d.Id()) if err != nil { return err } - _, err = client.Delete(ctx, resGroup, serverName, name) + _, err = client.Delete(ctx, elasticPool.ResourceGroup, elasticPool.MsSqlServer, elasticPool.Name) return err } -func parseArmMsSqlElasticPoolId(sqlElasticPoolId string) (string, string, string, error) { - id, err := azure.ParseAzureResourceID(sqlElasticPoolId) - if err != nil { - return "", "", "", fmt.Errorf("[ERROR] Unable to parse MsSQL ElasticPool ID %q: %+v", sqlElasticPoolId, err) - } - - return id.ResourceGroup, id.Path["servers"], id.Path["elasticPools"], nil -} - func expandAzureRmMsSqlElasticPoolPerDatabaseSettings(d *schema.ResourceData) *sql.ElasticPoolPerDatabaseSettings { perDatabaseSettings := d.Get("per_database_settings").([]interface{}) perDatabaseSetting := perDatabaseSettings[0].(map[string]interface{}) @@ -381,12 +373,3 @@ func flattenAzureRmMsSqlElasticPoolPerDatabaseSettings(resp *sql.ElasticPoolPerD return []interface{}{perDatabaseSettings} } - -func parseArmMSSqlElasticPoolId(sqlElasticPoolId string) (string, string, string, error) { - id, err := azure.ParseAzureResourceID(sqlElasticPoolId) - if err != nil { - return "", "", "", fmt.Errorf("[ERROR] Unable to parse SQL ElasticPool ID %q: %+v", sqlElasticPoolId, err) - } - - return id.ResourceGroup, id.Path["servers"], id.Path["elasticPools"], nil -} diff --git a/azurerm/internal/services/mssql/tests/data_source_mssql_database_test.go b/azurerm/internal/services/mssql/tests/data_source_mssql_database_test.go new file mode 100644 index 000000000000..df8e8299e7c6 --- /dev/null +++ b/azurerm/internal/services/mssql/tests/data_source_mssql_database_test.go @@ -0,0 +1,77 @@ +package tests + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" +) + +func TestAccDataSourceAzureRMMsSqlDatabase_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_mssql_database", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMsSqlDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAzureRMMsSqlDatabase_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMsSqlDatabaseExists(data.ResourceName), + ), + }, + }, + }) +} + +func TestAccDataSourceAzureRMMsSqlDatabase_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_mssql_database", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMsSqlDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAzureRMMsSqlDatabase_complete(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMsSqlDatabaseExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "collation", "SQL_AltDiction_CP850_CI_AI"), + resource.TestCheckResourceAttr(data.ResourceName, "license_type", "BasePrice"), + resource.TestCheckResourceAttr(data.ResourceName, "max_size_gb", "1"), + resource.TestCheckResourceAttr(data.ResourceName, "sku_name", "GP_Gen4_2"), + resource.TestCheckResourceAttr(data.ResourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(data.ResourceName, "tags.ENV", "Test"), + ), + }, + }, + }) +} + +func testAccDataSourceAzureRMMsSqlDatabase_basic(data acceptance.TestData) string { + template := testAccAzureRMMsSqlDatabase_basic(data) + return fmt.Sprintf(` +%s + +data "azurerm_mssql_database" "test" { + name = azurerm_mssql_database.test.name + server_id = azurerm_sql_server.test.id +} + +`, template) +} + +func testAccDataSourceAzureRMMsSqlDatabase_complete(data acceptance.TestData) string { + template := testAccAzureRMMsSqlDatabase_complete(data) + return fmt.Sprintf(` +%s + +data "azurerm_mssql_database" "test" { + name = azurerm_mssql_database.test.name + server_id = azurerm_sql_server.test.id +} + +`, template) +} diff --git a/azurerm/internal/services/mssql/tests/resource_arm_mssql_database_test.go b/azurerm/internal/services/mssql/tests/resource_arm_mssql_database_test.go new file mode 100644 index 000000000000..7b14ffe64a9c --- /dev/null +++ b/azurerm/internal/services/mssql/tests/resource_arm_mssql_database_test.go @@ -0,0 +1,634 @@ +package tests + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "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/internal/features" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/mssql/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMMsSqlDatabase_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_mssql_database", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMsSqlDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMMsSqlDatabase_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMsSqlDatabaseExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMMsSqlDatabase_requiresImport(t *testing.T) { + if !features.ShouldResourcesBeImported() { + t.Skip("Skipping since resources aren't required to be imported") + return + } + + data := acceptance.BuildTestData(t, "azurerm_mssql_database", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMsSqlDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMMsSqlDatabase_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMsSqlDatabaseExists(data.ResourceName), + ), + }, + data.RequiresImportErrorStep(testAccAzureRMMsSqlDatabase_requiresImport), + }, + }) +} + +func TestAccAzureRMMsSqlDatabase_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_mssql_database", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMsSqlDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMMsSqlDatabase_complete(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMsSqlDatabaseExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "collation", "SQL_AltDiction_CP850_CI_AI"), + resource.TestCheckResourceAttr(data.ResourceName, "license_type", "BasePrice"), + resource.TestCheckResourceAttr(data.ResourceName, "max_size_gb", "1"), + resource.TestCheckResourceAttr(data.ResourceName, "sku_name", "GP_Gen4_2"), + resource.TestCheckResourceAttr(data.ResourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(data.ResourceName, "tags.ENV", "Test"), + ), + }, + data.ImportStep("sample_name"), + { + Config: testAccAzureRMMsSqlDatabase_update(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMsSqlDatabaseExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "license_type", "LicenseIncluded"), + resource.TestCheckResourceAttr(data.ResourceName, "max_size_gb", "2"), + resource.TestCheckResourceAttr(data.ResourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(data.ResourceName, "tags.ENV", "Staging"), + ), + }, + data.ImportStep("sample_name"), + }, + }) +} + +func TestAccAzureRMMsSqlDatabase_elasticPool(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_mssql_database", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMsSqlDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMMsSqlDatabase_elasticPool(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMsSqlDatabaseExists(data.ResourceName), + resource.TestCheckResourceAttrSet(data.ResourceName, "elastic_pool_id"), + resource.TestCheckResourceAttr(data.ResourceName, "sku_name", "ElasticPool"), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMMsSqlDatabase_GP(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_mssql_database", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMsSqlDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMMsSqlDatabase_GP(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMsSqlDatabaseExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "sku_name", "GP_Gen5_2"), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMMsSqlDatabase_GP_Serverless(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_mssql_database", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMsSqlDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMMsSqlDatabase_GPServerless(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMsSqlDatabaseExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "auto_pause_delay_in_minutes", "70"), + resource.TestCheckResourceAttr(data.ResourceName, "min_capacity", "0.75"), + resource.TestCheckResourceAttr(data.ResourceName, "sku_name", "GP_S_Gen5_2"), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMMsSqlDatabase_GPServerlessUpdate(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMsSqlDatabaseExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "auto_pause_delay_in_minutes", "90"), + resource.TestCheckResourceAttr(data.ResourceName, "min_capacity", "1.25"), + resource.TestCheckResourceAttr(data.ResourceName, "sku_name", "GP_S_Gen5_2"), + ), + }, + data.ImportStep(), + }, + }) +} +func TestAccAzureRMMsSqlDatabase_BC(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_mssql_database", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMsSqlDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMMsSqlDatabase_BC(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMsSqlDatabaseExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "read_scale", "true"), + resource.TestCheckResourceAttr(data.ResourceName, "sku_name", "BC_Gen5_2"), + resource.TestCheckResourceAttr(data.ResourceName, "zone_redundant", "true"), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMMsSqlDatabase_BCUpdate(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMsSqlDatabaseExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "read_scale", "false"), + resource.TestCheckResourceAttr(data.ResourceName, "sku_name", "BC_Gen5_2"), + resource.TestCheckResourceAttr(data.ResourceName, "zone_redundant", "false"), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMMsSqlDatabase_HS(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_mssql_database", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMsSqlDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMMsSqlDatabase_HS(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMsSqlDatabaseExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "read_replica_count", "2"), + resource.TestCheckResourceAttr(data.ResourceName, "sku_name", "HS_Gen4_1"), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMMsSqlDatabase_HSUpdate(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMsSqlDatabaseExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "read_replica_count", "4"), + resource.TestCheckResourceAttr(data.ResourceName, "sku_name", "HS_Gen4_1"), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMMsSqlDatabase_createCopyMode(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_mssql_database", "copy") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMsSqlDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMMsSqlDatabase_createCopyMode(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMsSqlDatabaseExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "collation", "SQL_AltDiction_CP850_CI_AI"), + resource.TestCheckResourceAttr(data.ResourceName, "license_type", "BasePrice"), + resource.TestCheckResourceAttr(data.ResourceName, "sku_name", "GP_Gen4_2"), + ), + }, + data.ImportStep("create_mode", "creation_source_database_id"), + }, + }) +} + +func TestAccAzureRMMsSqlDatabase_createPITRMode(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_mssql_database", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMsSqlDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMMsSqlDatabase_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMsSqlDatabaseExists(data.ResourceName), + ), + }, + data.ImportStep(), + + { + PreConfig: func() { time.Sleep(7 * time.Minute) }, + Config: testAccAzureRMMsSqlDatabase_createPITRMode(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMsSqlDatabaseExists("azurerm_mssql_database.pitr"), + ), + }, + + data.ImportStep("create_mode", "creation_source_database_id", "restore_point_in_time"), + }, + }) +} + +func TestAccAzureRMMsSqlDatabase_createSecondaryMode(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_mssql_database", "secondary") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMsSqlDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMMsSqlDatabase_createSecondaryMode(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMsSqlDatabaseExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "collation", "SQL_AltDiction_CP850_CI_AI"), + resource.TestCheckResourceAttr(data.ResourceName, "license_type", "BasePrice"), + resource.TestCheckResourceAttr(data.ResourceName, "sku_name", "GP_Gen4_2"), + ), + }, + data.ImportStep("create_mode", "creation_source_database_id", "sample_name"), + }, + }) +} + +func testCheckAzureRMMsSqlDatabaseExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := acceptance.AzureProvider.Meta().(*clients.Client).MSSQL.DatabasesClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + id, err := parse.MsSqlDatabaseID(rs.Primary.ID) + if err != nil { + return err + } + + resp, err := client.Get(ctx, id.ResourceGroup, id.MsSqlServer, id.Name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("MsSql Database %q (resource group: %q) does not exist", id.Name, id.ResourceGroup) + } + + return fmt.Errorf("Get on MsSql Database Client: %+v", err) + } + + return nil + } +} + +func testCheckAzureRMMsSqlDatabaseDestroy(s *terraform.State) error { + client := acceptance.AzureProvider.Meta().(*clients.Client).MSSQL.DatabasesClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_mssql_database" { + continue + } + + id, err := parse.MsSqlDatabaseID(rs.Primary.ID) + if err != nil { + return err + } + + if resp, err := client.Get(ctx, id.ResourceGroup, id.MsSqlServer, id.Name); err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Get on MsSql Database Client: %+v", err) + } + } + return nil + } + + return nil +} + +func testAccAzureRMMsSqlDatabase_template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-mssql-%[1]d" + location = "%[2]s" +} + +resource "azurerm_sql_server" "test" { + name = "acctest-sqlserver-%[1]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + version = "12.0" + administrator_login = "mradministrator" + administrator_login_password = "thisIsDog11" +} +`, data.RandomInteger, data.Locations.Primary) +} + +func testAccAzureRMMsSqlDatabase_basic(data acceptance.TestData) string { + template := testAccAzureRMMsSqlDatabase_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_mssql_database" "test" { + name = "acctest-db-%d" + server_id = azurerm_sql_server.test.id +} +`, template, data.RandomInteger) +} + +func testAccAzureRMMsSqlDatabase_requiresImport(data acceptance.TestData) string { + template := testAccAzureRMMsSqlDatabase_basic(data) + return fmt.Sprintf(` +%s + +resource "azurerm_mssql_database" "import" { + name = azurerm_mssql_database.test.name + server_id = azurerm_sql_server.test.id +} +`, template) +} + +func testAccAzureRMMsSqlDatabase_complete(data acceptance.TestData) string { + template := testAccAzureRMMsSqlDatabase_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_mssql_database" "test" { + name = "acctest-db-%[2]d" + server_id = azurerm_sql_server.test.id + collation = "SQL_AltDiction_CP850_CI_AI" + license_type = "BasePrice" + max_size_gb = 1 + sample_name = "AdventureWorksLT" + sku_name = "GP_Gen4_2" + + tags = { + ENV = "Test" + } +} +`, template, data.RandomInteger) +} + +func testAccAzureRMMsSqlDatabase_update(data acceptance.TestData) string { + template := testAccAzureRMMsSqlDatabase_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_mssql_database" "test" { + name = "acctest-db-%[2]d" + server_id = azurerm_sql_server.test.id + collation = "SQL_AltDiction_CP850_CI_AI" + license_type = "LicenseIncluded" + max_size_gb = 2 + sku_name = "GP_Gen4_2" + + tags = { + ENV = "Staging" + } +} +`, template, data.RandomInteger) +} + +func testAccAzureRMMsSqlDatabase_elasticPool(data acceptance.TestData) string { + template := testAccAzureRMMsSqlDatabase_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_mssql_elasticpool" "test" { + name = "acctest-pool-%[2]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + server_name = azurerm_sql_server.test.name + max_size_gb = 4.8828125 + zone_redundant = false + + sku { + name = "BasicPool" + tier = "Basic" + capacity = 50 + } + + per_database_settings { + min_capacity = 0 + max_capacity = 5 + } +} + +resource "azurerm_mssql_database" "test" { + name = "acctest-db-%[2]d" + server_id = azurerm_sql_server.test.id + elastic_pool_id = azurerm_mssql_elasticpool.test.id +} +`, template, data.RandomInteger) +} + +func testAccAzureRMMsSqlDatabase_GP(data acceptance.TestData) string { + template := testAccAzureRMMsSqlDatabase_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_mssql_database" "test" { + name = "acctest-db-%d" + server_id = azurerm_sql_server.test.id + sku_name = "GP_Gen5_2" +} +`, template, data.RandomInteger) +} + +func testAccAzureRMMsSqlDatabase_GPServerless(data acceptance.TestData) string { + template := testAccAzureRMMsSqlDatabase_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_mssql_database" "test" { + name = "acctest-db-%d" + server_id = azurerm_sql_server.test.id + auto_pause_delay_in_minutes = 70 + min_capacity = 0.75 + sku_name = "GP_S_Gen5_2" +} +`, template, data.RandomInteger) +} + +func testAccAzureRMMsSqlDatabase_GPServerlessUpdate(data acceptance.TestData) string { + template := testAccAzureRMMsSqlDatabase_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_mssql_database" "test" { + name = "acctest-db-%d" + server_id = azurerm_sql_server.test.id + auto_pause_delay_in_minutes = 90 + min_capacity = 1.25 + sku_name = "GP_S_Gen5_2" +} +`, template, data.RandomInteger) +} + +func testAccAzureRMMsSqlDatabase_HS(data acceptance.TestData) string { + template := testAccAzureRMMsSqlDatabase_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_mssql_database" "test" { + name = "acctest-db-%d" + server_id = azurerm_sql_server.test.id + read_replica_count = 2 + sku_name = "HS_Gen4_1" +} +`, template, data.RandomInteger) +} + +func testAccAzureRMMsSqlDatabase_HSUpdate(data acceptance.TestData) string { + template := testAccAzureRMMsSqlDatabase_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_mssql_database" "test" { + name = "acctest-db-%d" + server_id = azurerm_sql_server.test.id + read_replica_count = 4 + sku_name = "HS_Gen4_1" +} +`, template, data.RandomInteger) +} + +func testAccAzureRMMsSqlDatabase_BC(data acceptance.TestData) string { + template := testAccAzureRMMsSqlDatabase_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_mssql_database" "test" { + name = "acctest-db-%d" + server_id = azurerm_sql_server.test.id + read_scale = true + sku_name = "BC_Gen5_2" + zone_redundant = true +} +`, template, data.RandomInteger) +} + +func testAccAzureRMMsSqlDatabase_BCUpdate(data acceptance.TestData) string { + template := testAccAzureRMMsSqlDatabase_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_mssql_database" "test" { + name = "acctest-db-%d" + server_id = azurerm_sql_server.test.id + read_scale = false + sku_name = "BC_Gen5_2" + zone_redundant = false +} +`, template, data.RandomInteger) +} + +func testAccAzureRMMsSqlDatabase_createCopyMode(data acceptance.TestData) string { + template := testAccAzureRMMsSqlDatabase_complete(data) + return fmt.Sprintf(` +%s + +resource "azurerm_mssql_database" "copy" { + name = "acctest-dbc-%d" + server_id = azurerm_sql_server.test.id + create_mode = "Copy" + creation_source_database_id = azurerm_mssql_database.test.id +} +`, template, data.RandomInteger) +} + +func testAccAzureRMMsSqlDatabase_createPITRMode(data acceptance.TestData) string { + template := testAccAzureRMMsSqlDatabase_basic(data) + return fmt.Sprintf(` +%s + +resource "azurerm_mssql_database" "pitr" { + name = "acctest-dbp-%d" + server_id = azurerm_sql_server.test.id + create_mode = "PointInTimeRestore" + restore_point_in_time = "%s" + creation_source_database_id = azurerm_mssql_database.test.id + +} +`, template, data.RandomInteger, time.Now().Add(time.Duration(7)*time.Minute).UTC().Format(time.RFC3339)) +} + +func testAccAzureRMMsSqlDatabase_createSecondaryMode(data acceptance.TestData) string { + template := testAccAzureRMMsSqlDatabase_complete(data) + return fmt.Sprintf(` +%s + +resource "azurerm_resource_group" "second" { + name = "acctestRG-mssql2-%[2]d" + location = "%[3]s" +} + +resource "azurerm_sql_server" "second" { + name = "acctest-sqlserver2-%[2]d" + resource_group_name = azurerm_resource_group.second.name + location = azurerm_resource_group.second.location + version = "12.0" + administrator_login = "mradministrator" + administrator_login_password = "thisIsDog11" +} + +resource "azurerm_mssql_database" "secondary" { + name = "acctest-dbs-%[2]d" + server_id = azurerm_sql_server.second.id + create_mode = "Secondary" + creation_source_database_id = azurerm_mssql_database.test.id + +} +`, template, data.RandomInteger, data.Locations.Secondary) +} diff --git a/azurerm/internal/services/mssql/validate/mssql_database.go b/azurerm/internal/services/mssql/validate/mssql_database.go new file mode 100644 index 000000000000..252c71d65db4 --- /dev/null +++ b/azurerm/internal/services/mssql/validate/mssql_database.go @@ -0,0 +1,56 @@ +package validate + +import ( + "fmt" + "regexp" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/mssql/parse" +) + +func MsSqlDatabaseID(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return warnings, errors + } + + if _, err := parse.MsSqlDatabaseID(v); err != nil { + errors = append(errors, fmt.Errorf("Can not parse %q as a MsSql Database resource id: %v", k, err)) + } + + return warnings, errors +} + +func MsSqlDatabaseAutoPauseDelay(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(int) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %s to be integer", k)) + return warnings, errors + } + min := 60 + max := 10080 + if (v < min || v > max) && v%10 != 0 && v != -1 { + errors = append(errors, fmt.Errorf("expected %s to be in the range (%d - %d) and divisible by 10 or -1, got %d", k, min, max, v)) + return warnings, errors + } + + return warnings, errors +} + +func MsSqlDBSkuName() schema.SchemaValidateFunc { + return validation.StringMatch( + regexp.MustCompile(`(?i)(^(GP_S_Gen5_(1|2|4|6|8|10|12|14|16))$|^((GP|HS|BC)_Gen4_(1|2|3|4|5|6|7|8|9|10|16|24))$|^((GP|HS|BC)_Gen5_(2|4|6|8|10|12|14|16|18|20|24|32|40|80))$|^(BC_M_(8|10|12|14|16|18|20|24|32|64|128))$|^(Basic)$|^(ElasticPool)$|^(S(0|1|2|3|4|6|7|9|12))$|^(P(1|2|4|6|11|15))$|^(DW(1|2|3|4|5|10|15|20)00c)$|^(DS(1|2|3|4|5|6|10|12|15|20)00)$)`), + + `This is not a valid sku name. For example, a valid sku name is 'GP_S_Gen5_1','HS_Gen4_1','BC_Gen5_2', 'ElasticPool', 'Basic', 'S0', 'P1'.`, + ) +} + +func MsSqlDBCollation() schema.SchemaValidateFunc { + return validation.StringMatch( + regexp.MustCompile(`(^[A-Z]+)([A-Za-z0-9]+_)+((BIN|BIN2|CI_AI|CI_AI_KS|CI_AI_KS_WS|CI_AI_WS|CI_AS|CI_AS_KS|CI_AS_KS_WS|CI_AI_WS|CS_AI|CS_AI_KS|CS_AI_KS_WS|CS_AI_WS|CS_AS|CS_AS_KS|CS_AS_KS_WS|CS_AS_WS)+)((_[A-Za-z0-9]+)+$)*`), + + `This is not a valid collation.`, + ) +} diff --git a/azurerm/internal/services/mssql/validate/mssql_database_test.go b/azurerm/internal/services/mssql/validate/mssql_database_test.go new file mode 100644 index 000000000000..96d71079e235 --- /dev/null +++ b/azurerm/internal/services/mssql/validate/mssql_database_test.go @@ -0,0 +1,216 @@ +package validate + +import ( + "testing" +) + +func TestMsSqlDatabaseAutoPauseDelay(t *testing.T) { + testCases := []struct { + input string + shouldError bool + }{ + {"-1", false}, + {"-2", true}, + {"30", true}, + {"60", false}, + {"65", true}, + {"360", false}, + {"19900", true}, + } + + for _, test := range testCases { + _, es := MsSqlDatabaseAutoPauseDelay(test.input, "name") + + if test.shouldError && len(es) == 0 { + t.Fatalf("Expected validating name %q to fail", test.input) + } + } +} + +func TestMsSqlDBSkuName(t *testing.T) { + tests := []struct { + name string + input string + valid bool + }{ + { + name: "DataWarehouse", + input: "DW100c", + valid: true, + }, + { + name: "DataWarehouse", + input: "DW102c", + valid: false, + }, + { + name: "Stretch", + input: "DS100", + valid: true, + }, + { + name: "Stretch", + input: "DS1001", + valid: false, + }, + { + name: "Valid GP", + input: "GP_Gen4_3", + valid: true, + }, + { + name: "Valid Serverless GP", + input: "GP_S_Gen5_2", + valid: true, + }, + { + name: "Valid HS", + input: "HS_Gen5_2", + valid: true, + }, + { + name: "Valid BC", + input: "BC_Gen4_5", + valid: true, + }, + { + name: "Valid BC", + input: "BC_M_12", + valid: true, + }, + { + name: "Valid BC", + input: "BC_Gen5_14", + valid: true, + }, + { + name: "Valid Standard", + input: "S3", + valid: true, + }, + { + name: "Valid Basic", + input: "Basic", + valid: true, + }, + { + name: "Valid Premium", + input: "P15", + valid: true, + }, + { + name: "empty", + input: "", + valid: false, + }, + { + name: "Extra dot", + input: "BC_Gen5_3.", + valid: false, + }, + { + name: "Wrong capacity", + input: "BC_Gen5_3", + valid: false, + }, + { + name: "Wrong Family", + input: "BC_Inv_2", + valid: false, + }, + { + name: "Wrong Serverless", + input: "GP_S_Gen4_2", + valid: false, + }, + { + name: "Wrong Serverless", + input: "BC_S_Gen5_2", + valid: false, + }, + { + name: "Lower case", + input: "bc_gen5_2", + valid: true, + }, + } + var validationFunction = MsSqlDBSkuName() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := validationFunction(tt.input, "") + valid := err == nil + if valid != tt.valid { + t.Errorf("Expected valid status %t but got %t for input %s", tt.valid, valid, tt.input) + } + }) + } +} + +func TestMsSqlDBCollation(t *testing.T) { + tests := []struct { + name string + input string + valid bool + }{ + { + name: "SQL Collation", + input: "SQL_Latin1_General_CP1_CI_AS", + valid: true, + }, + { + name: "Windows Collation", + input: "Latin1_General_100_CI_AS_SC", + valid: true, + }, + { + name: "SQL Collation", + input: "SQL_AltDiction_CP850_CI_AI", + valid: true, + }, + { + name: "SQL Collation", + input: "SQL_Croatian_CP1250_CI_AS", + valid: true, + }, + { + name: "Windows Collation", + input: "Chinese_Hong_Kong_Stroke_90_CI_AI", + valid: true, + }, + { + name: "Windows Collation", + input: "Japanese_BIN", + valid: true, + }, + { + name: "lowercase", + input: "sql_croatian_cp1250_ci_as", + valid: false, + }, + { + name: "extra dot", + input: "SQL_Croatian_CP1250.", + valid: false, + }, + { + name: "Invalid collation", + input: "CDD", + valid: false, + }, + { + name: "Double definition", + input: "Latin1_General_100_CI_CS", + valid: false, + }, + } + var validationFunction = MsSqlDBCollation() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := validationFunction(tt.input, "") + valid := err == nil + if valid != tt.valid { + t.Errorf("Expected valid status %t but got %t for input %s", tt.valid, valid, tt.input) + } + }) + } +} diff --git a/azurerm/internal/services/mssql/validate/mssql_elastic_pool.go b/azurerm/internal/services/mssql/validate/mssql_elastic_pool.go new file mode 100644 index 000000000000..74033fc29af3 --- /dev/null +++ b/azurerm/internal/services/mssql/validate/mssql_elastic_pool.go @@ -0,0 +1,24 @@ +package validate + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/mssql/parse" +) + +func MsSqlElasticPoolID(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return warnings, errors + } + if v == "" { + return warnings, errors + } + + if _, err := parse.MSSqlElasticPoolID(v); err != nil { + errors = append(errors, fmt.Errorf("Can not parse %q as a MsSql Elastic Pool resource id: %v", k, err)) + } + + return warnings, errors +} diff --git a/azurerm/internal/services/mssql/validate/mssql_server.go b/azurerm/internal/services/mssql/validate/mssql_server.go new file mode 100644 index 000000000000..0ab924cf6acd --- /dev/null +++ b/azurerm/internal/services/mssql/validate/mssql_server.go @@ -0,0 +1,21 @@ +package validate + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/mssql/parse" +) + +func MsSqlServerID(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return warnings, errors + } + + if _, err := parse.MsSqlServerID(v); err != nil { + errors = append(errors, fmt.Errorf("Can not parse %q as a MsSql Server resource id: %v", k, err)) + } + + return warnings, errors +} diff --git a/website/azurerm.erb b/website/azurerm.erb index 0721f294857b..f0f0d094730b 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -342,6 +342,10 @@ azurerm_monitor_scheduled_query_rules_log +
  • + azurerm_mssql_database +
  • +
  • azurerm_mssql_elasticpool
  • @@ -1169,6 +1173,10 @@ azurerm_sql_database +
  • + azurerm_mssql_database +
  • +
  • azurerm_sql_active_directory_administrator
  • diff --git a/website/docs/d/mssql_database.html.markdown b/website/docs/d/mssql_database.html.markdown new file mode 100644 index 000000000000..fff8353e37d1 --- /dev/null +++ b/website/docs/d/mssql_database.html.markdown @@ -0,0 +1,56 @@ +--- +subcategory: "Database" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_mssql_database" +description: |- + Gets information about an existing SQL database. +--- + +# Data Source: azurerm_mssql_database + +Use this data source to access information about an existing SQL database. + +## Example Usage + +```hcl +data "azurerm_mssql_database" "example" { + name = "example-mssql-db" + server_id = "example-mssql-server-id" +} + +output "database_id" { + value = data.azurerm_mssql_database.example.id +} +``` + +## Argument Reference + +* `name` - The name of the Ms SQL Database. + +* `server_id` - The id of the Ms SQL Server on which to create the database. + +## Attribute Reference + +* `collation` - The collation of the database. + +* `elastic_pool_id` - The id of the elastic pool containing this database. + +* `license_type` - The license type to apply for this database. + +* `max_size_gb` - The max size of the database in gigabytes. + +* `read_replica_count` - The number of readonly secondary replicas associated with the database to which readonly application intent connections may be routed. + +* `read_scale` - If enabled, connections that have application intent set to readonly in their connection string may be routed to a readonly secondary replica. + +* `sku_name` - The name of the sku of the database. + +* `zone_redundant` - Whether or not this database is zone redundant, which means the replicas of this database will be spread across multiple availability zones. + +* `tags` - A mapping of tags to assign to the resource. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `read` - (Defaults to 5 minutes) Used when retrieving the SQL database. diff --git a/website/docs/r/mssql_database.html.markdown b/website/docs/r/mssql_database.html.markdown new file mode 100644 index 000000000000..8e5dc1567ec5 --- /dev/null +++ b/website/docs/r/mssql_database.html.markdown @@ -0,0 +1,114 @@ +--- +subcategory: "Database" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_mssql_database" +description: |- + Manages a MS SQL Database. +--- + +# azurerm_mssql_database + +Manages a MS SQL Database. + +## Example Usage + +```hcl +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "example" { + name = "example-resources" + location = "West Europe" +} + +resource "azurerm_sql_server" "example" { + name = "example-sqlserver" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + version = "12.0" + administrator_login = "4dm1n157r470r" + administrator_login_password = "4-v3ry-53cr37-p455w0rd" +} +resource "azurerm_mssql_database" "test" { + name = "acctest-db-%d" + server_id = azurerm_sql_server.test.id + collation = "SQL_Latin1_General_CP1_CI_AS" + license_type = "LicenseIncluded" + max_size_gb = 4 + read_scale = true + sku_name = "BC_Gen5_2" + zone_redundant = true + + tags = { + foo = "bar" + } + +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the Ms SQL Database. Changing this forces a new resource to be created. + +* `server_id` - (Required) The id of the Ms SQL Server on which to create the database. Changing this forces a new resource to be created. + +~> **NOTE:** This setting is still required for "Serverless" SKU's + +* `auto_pause_delay_in_minutes` - (Optional) Time in minutes after which database is automatically paused. A value of `-1` means that automatic pause is disabled. This property is only settable for General Purpose Serverless databases. + +* `create_mode` - (Optional) The create mode of the database. Possible values are `Copy`, `Default`, `OnlineSecondary`, `PointInTimeRestore`, `Restore`, `RestoreExternalBackup`, `RestoreExternalBackupSecondary`, `RestoreLongTermRetentionBackup` and `Secondary`. + +* `collation` - (Optional) Specifies the collation of the database. Changing this forces a new resource to be created. + +* `elastic_pool_id` - (Optional) Specifies the ID of the elastic pool containing this database. Changing this forces a new resource to be created. + +* `license_type` - (Optional) Specifies the license type applied to this database. Possible values are `LicenseIncluded` and `BasePrice`. + +* `max_size_gb` - (Optional) The max size of the database in gigabytes. + +* `min_capacity` - (Optional) Minimal capacity that database will always have allocated, if not paused. This property is only settable for General Purpose Serverless databases. + +* `restore_point_in_time` - (Required) Specifies the point in time (ISO8601 format) of the source database that will be restored to create the new database. This property is only settable for `create_mode`= `PointInTimeRestore` databases. + +* `read_replica_count` - (Optional) The number of readonly secondary replicas associated with the database to which readonly application intent connections may be routed. This property is only settable for Hyperscale edition databases. + +* `read_scale` - (Optional) If enabled, connections that have application intent set to readonly in their connection string may be routed to a readonly secondary replica. This property is only settable for Premium and Business Critical databases. + +* `sample_name` - (Optional) Specifies the name of the sample schema to apply when creating this database. Possible value is `AdventureWorksLT`. + +* `sku_name` - (Optional) Specifies the name of the sku used by the database. Changing this forces a new resource to be created. For example, `GP_S_Gen5_2`,`HS_Gen4_1`,`BC_Gen5_2`, `ElasticPool`, `Basic`,`S0`, `P2` ,`DW100c`, `DS100`. + +~> **NOTE** The default sku_name value may differ between Azure locations depending on local availability of Gen4/Gen5 capacity. + +* `creation_source_database_id` - (Optional) The id of the source database to be referred to create the new database. This should only be used for databases with `create_mode` values that use another database as reference. Changing this forces a new resource to be created. + +* `zone_redundant` - (Optional) Whether or not this database is zone redundant, which means the replicas of this database will be spread across multiple availability zones. This property is only settable for Premium and Business Critical databases. + +* `tags` - (Optional) A mapping of tags to assign to the resource. + + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the MS SQL Database. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `create` - (Defaults to 60 minutes) Used when creating the MS SQL Database. +* `update` - (Defaults to 60 minutes) Used when updating the MS SQL Database. +* `read` - (Defaults to 5 minutes) Used when retrieving the MS SQL Database. +* `delete` - (Defaults to 60 minutes) Used when deleting the MS SQL Database. + +## Import + +SQL Database can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_mssql_database.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Sql/servers/server1/databases/example1 +```