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 +