diff --git a/azurerm/helpers/azure/mysql.go b/azurerm/helpers/azure/mysql.go deleted file mode 100644 index 7d133e333be3..000000000000 --- a/azurerm/helpers/azure/mysql.go +++ /dev/null @@ -1,15 +0,0 @@ -package azure - -import ( - "fmt" - - "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate" -) - -func ValidateMySqlServerName(i interface{}, k string) (_ []string, errors []error) { - if m, regexErrs := validate.RegExHelper(i, k, `^[0-9a-z]([-0-9a-z]{0,61}[0-9a-z])?$`); !m { - return nil, append(regexErrs, fmt.Errorf("%q can contain only lowercase letters, numbers, and '-', but can't start or end with '-' or have more than 63 characters.", k)) - } - - return nil, nil -} diff --git a/azurerm/internal/services/mysql/mysql_configuration_resource.go b/azurerm/internal/services/mysql/mysql_configuration_resource.go index ea1cd6e80ad2..eeb16de1b1ed 100644 --- a/azurerm/internal/services/mysql/mysql_configuration_resource.go +++ b/azurerm/internal/services/mysql/mysql_configuration_resource.go @@ -2,6 +2,7 @@ package mysql import ( "fmt" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/mysql/validate" "log" "time" @@ -43,7 +44,7 @@ func resourceArmMySQLConfiguration() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, - ValidateFunc: azure.ValidateMySqlServerName, + ValidateFunc: validate.MysqlServerServerName, }, "value": { diff --git a/azurerm/internal/services/mysql/mysql_database_resource.go b/azurerm/internal/services/mysql/mysql_database_resource.go index f1f96e5ad6b8..a9b5f9ea9786 100644 --- a/azurerm/internal/services/mysql/mysql_database_resource.go +++ b/azurerm/internal/services/mysql/mysql_database_resource.go @@ -2,6 +2,7 @@ package mysql import ( "fmt" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/mysql/validate" "log" "time" @@ -46,7 +47,7 @@ func resourceArmMySqlDatabase() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, - ValidateFunc: azure.ValidateMySqlServerName, + ValidateFunc: validate.MysqlServerServerName, }, "charset": { diff --git a/azurerm/internal/services/mysql/mysql_firewall_rule_resource.go b/azurerm/internal/services/mysql/mysql_firewall_rule_resource.go index cc21cf9c0f48..6a872bc21c81 100644 --- a/azurerm/internal/services/mysql/mysql_firewall_rule_resource.go +++ b/azurerm/internal/services/mysql/mysql_firewall_rule_resource.go @@ -2,6 +2,7 @@ package mysql import ( "fmt" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/mysql/validate" "log" "time" @@ -46,7 +47,7 @@ func resourceArmMySqlFirewallRule() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, - ValidateFunc: azure.ValidateMySqlServerName, + ValidateFunc: validate.MysqlServerServerName, }, "start_ip_address": { diff --git a/azurerm/internal/services/mysql/mysql_server_resource.go b/azurerm/internal/services/mysql/mysql_server_resource.go index 54bab650e0f6..0040123514ad 100644 --- a/azurerm/internal/services/mysql/mysql_server_resource.go +++ b/azurerm/internal/services/mysql/mysql_server_resource.go @@ -2,6 +2,10 @@ package mysql import ( "fmt" + "github.com/Azure/go-autorest/autorest/date" + "github.com/hashicorp/go-azure-helpers/response" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/mysql/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/mysql/validate" "log" "strconv" "strings" @@ -28,7 +32,18 @@ func resourceArmMySqlServer() *schema.Resource { Delete: resourceArmMySqlServerDelete, Importer: &schema.ResourceImporter{ - State: schema.ImportStatePassthrough, + State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + if _, err := parse.MysqlServerServerID(d.Id()); err != nil { + return []*schema.ResourceData{d}, err + } + + d.Set("create_mode", "Default") + if v, ok := d.GetOk("create_mode"); ok && v.(string) != "" { + d.Set("create_mode", v) + } + + return []*schema.ResourceData{d}, nil + }, }, Timeouts: &schema.ResourceTimeout{ @@ -43,13 +58,89 @@ func resourceArmMySqlServer() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, - ValidateFunc: azure.ValidateMySqlServerName, + ValidateFunc: validate.MysqlServerServerName, + }, + + "administrator_login": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "administrator_login_password": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + }, + + "auto_grow_enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, // TODO: remove in 3.0 and default to true + ConflictsWith: []string{"storage_profile", "storage_profile.0.auto_grow"}, + }, + + "backup_retention_days": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ConflictsWith: []string{"storage_profile", "storage_profile.0.backup_retention_days"}, + ValidateFunc: validation.IntBetween(7, 35), + }, + + "create_mode": { + Type: schema.TypeString, + Optional: true, + Default: string(mysql.CreateModeDefault), + ValidateFunc: validation.StringInSlice([]string{ + string(mysql.CreateModeDefault), + string(mysql.CreateModeGeoRestore), + string(mysql.CreateModePointInTimeRestore), + string(mysql.CreateModeReplica), + }, false), + }, + + "creation_source_server_id": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validate.MysqlServerServerID, + }, + + "fqdn": { + Type: schema.TypeString, + Computed: true, + }, + + "geo_redundant_backup_enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + ConflictsWith: []string{"storage_profile", "storage_profile.0.geo_redundant_backup"}, + }, + + "infrastructure_encryption_enabled": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, }, "location": azure.SchemaLocation(), + "public_network_access_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "resource_group_name": azure.SchemaResourceGroupName(), + "restore_point_in_time": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.IsRFC3339Time, + }, + "sku_name": { Type: schema.TypeString, Required: true, @@ -77,100 +168,129 @@ func resourceArmMySqlServer() *schema.Resource { }, false), }, - "administrator_login": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + "ssl_enforcement": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Deprecated: "this has been moved to the boolean attribute `ssl_enforcement_enabled` and will be removed in version 3.0 of the provider.", + ExactlyOneOf: []string{"ssl_enforcement", "ssl_enforcement_enabled"}, + ValidateFunc: validation.StringInSlice([]string{ + string(mysql.SslEnforcementEnumDisabled), + string(mysql.SslEnforcementEnumEnabled), + }, true), + DiffSuppressFunc: suppress.CaseDifference, }, - "administrator_login_password": { - Type: schema.TypeString, - Required: true, - Sensitive: true, + "ssl_enforcement_enabled": { + Type: schema.TypeBool, + Optional: true, // required in 3.0 + Computed: true, // remove computed in 3.0 + ExactlyOneOf: []string{"ssl_enforcement", "ssl_enforcement_enabled"}, }, - "version": { + "ssl_minimal_tls_version_enforced": { Type: schema.TypeString, - Required: true, + Optional: true, + Default: string(mysql.TLSEnforcementDisabled), ValidateFunc: validation.StringInSlice([]string{ - string(mysql.FiveFullStopSix), - string(mysql.FiveFullStopSeven), - string(mysql.EightFullStopZero), - }, true), - DiffSuppressFunc: suppress.CaseDifference, - ForceNew: true, + string(mysql.TLSEnforcementDisabled), + string(mysql.TLS10), + string(mysql.TLS11), + string(mysql.TLS12), + }, false), + }, + + "storage_mb": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ConflictsWith: []string{"storage_profile", "storage_profile.0.storage_mb"}, + ValidateFunc: validation.All( + validation.IntBetween(5120, 4194304), + validation.IntDivisibleBy(1024), + ), }, "storage_profile": { - Type: schema.TypeList, - Required: true, - MaxItems: 1, + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Deprecated: "all storage_profile properties have been moved to the top level. This block will be removed in version 3.0 of the provider.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "storage_mb": { - Type: schema.TypeInt, - Required: true, - ValidateFunc: validation.All( - validation.IntBetween(5120, 4194304), - validation.IntDivisibleBy(1024), - ), + "auto_grow": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ConflictsWith: []string{"auto_grow_enabled"}, + Deprecated: "this has been moved to the top level boolean attribute `auto_grow_enabled` and will be removed in version 3.0 of the provider.", + DiffSuppressFunc: suppress.CaseDifference, + ValidateFunc: validation.StringInSlice([]string{ + string(mysql.StorageAutogrowEnabled), + string(mysql.StorageAutogrowDisabled), + }, false), }, "backup_retention_days": { - Type: schema.TypeInt, - Optional: true, + Type: schema.TypeInt, + Optional: true, + Computed: true, + ConflictsWith: []string{"backup_retention_days"}, + Deprecated: "this has been moved to the top level and will be removed in version 3.0 of the provider.", ValidateFunc: validation.IntBetween(7, 35), }, "geo_redundant_backup": { - Type: schema.TypeString, - Optional: true, - ValidateFunc: validation.StringInSlice([]string{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ConflictsWith: []string{"geo_redundant_backup_enabled"}, + Deprecated: "this has been moved to the top level boolean attribute `geo_redundant_backup_enabled` and will be removed in version 3.0 of the provider.", + DiffSuppressFunc: suppress.CaseDifference, + ValidateFunc: validation.StringInSlice([]string{ "Enabled", "Disabled", }, true), - DiffSuppressFunc: suppress.CaseDifference, }, - "auto_grow": { - Type: schema.TypeString, - Optional: true, - Default: string(mysql.StorageAutogrowEnabled), - ValidateFunc: validation.StringInSlice([]string{ - string(mysql.StorageAutogrowEnabled), - string(mysql.StorageAutogrowDisabled), - }, false), + "storage_mb": { + Type: schema.TypeInt, + Optional: true, + ConflictsWith: []string{"storage_mb"}, + Deprecated: "this has been moved to the top level and will be removed in version 3.0 of the provider.", + ValidateFunc: validation.All( + validation.IntBetween(5120, 4194304), + validation.IntDivisibleBy(1024), + ), }, }, }, }, - "ssl_enforcement": { + "tags": tags.Schema(), + + "version": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringInSlice([]string{ - string(mysql.SslEnforcementEnumDisabled), - string(mysql.SslEnforcementEnumEnabled), + string(mysql.FiveFullStopSix), + string(mysql.FiveFullStopSeven), + string(mysql.EightFullStopZero), }, true), DiffSuppressFunc: suppress.CaseDifference, + ForceNew: true, }, - - "public_network_access_enabled": { - Type: schema.TypeBool, - Optional: true, - Default: true, - }, - - "fqdn": { - Type: schema.TypeString, - Computed: true, - }, - - "tags": tags.Schema(), }, CustomizeDiff: func(diff *schema.ResourceDiff, v interface{}) error { tier, _ := diff.GetOk("sku_name") - storageMB, _ := diff.GetOk("storage_profile.0.storage_mb") - if strings.HasPrefix(tier.(string), "B_") && storageMB.(int) > 1048576 { + var storageMB int + if v, ok := diff.GetOk("storage_mb"); ok { + storageMB = v.(int) + } else if v, ok := diff.GetOk("storage_profile.0.storage_mb"); ok { + storageMB = v.(int) + } + + if strings.HasPrefix(tier.(string), "B_") && storageMB > 1048576 { return fmt.Errorf("basic pricing tier only supports upto 1,048,576 MB (1TB) of storage") } @@ -190,16 +310,11 @@ func resourceArmMySqlServerCreate(d *schema.ResourceData, meta interface{}) erro location := azure.NormalizeLocation(d.Get("location").(string)) resourceGroup := d.Get("resource_group_name").(string) - publicAccess := mysql.PublicNetworkAccessEnumEnabled - if v := d.Get("public_network_access_enabled").(bool); !v { - publicAccess = mysql.PublicNetworkAccessEnumDisabled - } - if features.ShouldResourcesBeImported() && d.IsNewResource() { existing, err := client.Get(ctx, resourceGroup, name) if err != nil { if !utils.ResponseWasNotFound(existing.Response) { - return fmt.Errorf("Error checking for presence of existing MySQL Server %q (Resource Group %q): %+v", name, resourceGroup, err) + return fmt.Errorf("checking for presence of existing MySQL Server %q (Resource Group %q): %+v", name, resourceGroup, err) } } @@ -208,42 +323,131 @@ func resourceArmMySqlServerCreate(d *schema.ResourceData, meta interface{}) erro } } + mode := mysql.CreateMode(d.Get("create_mode").(string)) + tlsMin := mysql.MinimalTLSVersionEnum(d.Get("ssl_minimal_tls_version_enforced").(string)) + source := d.Get("creation_source_server_id").(string) + version := mysql.ServerVersion(d.Get("version").(string)) + sku, err := expandServerSkuName(d.Get("sku_name").(string)) if err != nil { - return fmt.Errorf("error expanding sku_name for MySQL Server %q (Resource Group %q): %v", name, resourceGroup, err) + return fmt.Errorf("expanding sku_name for MySQL Server %q (Resource Group %q): %v", name, resourceGroup, err) } - properties := mysql.ServerForCreate{ - Location: &location, - Properties: &mysql.ServerPropertiesForDefaultCreate{ - AdministratorLogin: utils.String(d.Get("administrator_login").(string)), - AdministratorLoginPassword: utils.String(d.Get("administrator_login_password").(string)), - Version: mysql.ServerVersion(d.Get("version").(string)), - SslEnforcement: mysql.SslEnforcementEnum(d.Get("ssl_enforcement").(string)), - StorageProfile: expandMySQLStorageProfile(d), - CreateMode: mysql.CreateMode("Default"), + infraEncrypt := mysql.InfrastructureEncryptionEnabled + if v := d.Get("infrastructure_encryption_enabled"); !v.(bool) { + infraEncrypt = mysql.InfrastructureEncryptionDisabled + } + + publicAccess := mysql.PublicNetworkAccessEnumEnabled + if v := d.Get("public_network_access_enabled"); !v.(bool) { + publicAccess = mysql.PublicNetworkAccessEnumDisabled + } + + ssl := mysql.SslEnforcementEnumEnabled + if v, ok := d.GetOk("ssl_enforcement"); ok && strings.EqualFold(v.(string), string(mysql.SslEnforcementEnumDisabled)) { + ssl = mysql.SslEnforcementEnumDisabled + } + if v, ok := d.GetOkExists("ssl_enforcement_enabled"); ok && !v.(bool) { + ssl = mysql.SslEnforcementEnumDisabled + } + + storage := expandMySQLStorageProfile(d) + + var props mysql.BasicServerPropertiesForCreate + switch mode { + case mysql.CreateModeDefault: + admin := d.Get("administrator_login").(string) + pass := d.Get("administrator_login_password").(string) + + if admin == "" { + return fmt.Errorf("`administrator_login` must not be empty when `create_mode` is `default`") + } + if pass == "" { + return fmt.Errorf("`administrator_login_password` must not be empty when `create_mode` is `default`") + } + + if _, ok := d.GetOk("restore_point_in_time"); ok { + return fmt.Errorf("`restore_point_in_time` cannot be set when `create_mode` is `default`") + } + + // check admin + props = &mysql.ServerPropertiesForDefaultCreate{ + AdministratorLogin: &admin, + AdministratorLoginPassword: &pass, + CreateMode: mode, + InfrastructureEncryption: infraEncrypt, PublicNetworkAccess: publicAccess, - }, + MinimalTLSVersion: tlsMin, + SslEnforcement: ssl, + StorageProfile: storage, + Version: version, + } + case mysql.CreateModePointInTimeRestore: + v, ok := d.GetOk("restore_point_in_time") + if !ok || v.(string) == "" { + return fmt.Errorf("restore_point_in_time must be set when create_mode is PointInTimeRestore") + } + time, _ := time.Parse(time.RFC3339, v.(string)) // should be validated by the schema + + props = &mysql.ServerPropertiesForRestore{ + CreateMode: mode, + SourceServerID: &source, + RestorePointInTime: &date.Time{ + Time: time, + }, + InfrastructureEncryption: infraEncrypt, + PublicNetworkAccess: publicAccess, + MinimalTLSVersion: tlsMin, + SslEnforcement: ssl, + StorageProfile: storage, + Version: version, + } + case mysql.CreateModeGeoRestore: + props = &mysql.ServerPropertiesForGeoRestore{ + CreateMode: mode, + SourceServerID: &source, + InfrastructureEncryption: infraEncrypt, + PublicNetworkAccess: publicAccess, + MinimalTLSVersion: tlsMin, + SslEnforcement: ssl, + StorageProfile: storage, + Version: version, + } + case mysql.CreateModeReplica: + props = &mysql.ServerPropertiesForReplica{ + CreateMode: mode, + SourceServerID: &source, + InfrastructureEncryption: infraEncrypt, + PublicNetworkAccess: publicAccess, + MinimalTLSVersion: tlsMin, + SslEnforcement: ssl, + Version: version, + } + } + + server := mysql.ServerForCreate{ + Location: &location, + Properties: props, Sku: sku, Tags: tags.Expand(d.Get("tags").(map[string]interface{})), } - future, err := client.Create(ctx, resourceGroup, name, properties) + future, err := client.Create(ctx, resourceGroup, name, server) if err != nil { - return fmt.Errorf("Error creating MySQL Server %q (Resource Group %q): %+v", name, resourceGroup, err) + return fmt.Errorf("creating MySQL Server %q (Resource Group %q): %+v", name, resourceGroup, err) } if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { - return fmt.Errorf("Error waiting for creation of MySQL Server %q (Resource Group %q): %+v", name, resourceGroup, err) + return fmt.Errorf("waiting for creation of MySQL Server %q (Resource Group %q): %+v", name, resourceGroup, err) } read, err := client.Get(ctx, resourceGroup, name) if err != nil { - return fmt.Errorf("Error retrieving MySQL Server %q (Resource Group %q): %+v", name, resourceGroup, err) + return fmt.Errorf("retrieving MySQL Server %q (Resource Group %q): %+v", name, resourceGroup, err) } if read.ID == nil { - return fmt.Errorf("Cannot read MySQL Server %q (Resource Group %q) ID", name, resourceGroup) + return fmt.Errorf("cannot read MySQL Server %q (Resource Group %q) ID", name, resourceGroup) } d.SetId(*read.ID) @@ -258,12 +462,14 @@ func resourceArmMySqlServerUpdate(d *schema.ResourceData, meta interface{}) erro log.Printf("[INFO] preparing arguments for AzureRM MySQL Server update.") - name := d.Get("name").(string) - resourceGroup := d.Get("resource_group_name").(string) + id, err := parse.MysqlServerServerID(d.Id()) + if err != nil { + return fmt.Errorf("parsing MySQL Server ID : %v", err) + } sku, err := expandServerSkuName(d.Get("sku_name").(string)) if err != nil { - return fmt.Errorf("error expanding sku_name for MySQL Server %q (Resource Group %q): %v", name, resourceGroup, err) + return fmt.Errorf("expanding sku_name for MySQL Server %q (Resource Group %q): %v", id.Name, id.ResourceGroup, err) } publicAccess := mysql.PublicNetworkAccessEnumEnabled @@ -271,34 +477,44 @@ func resourceArmMySqlServerUpdate(d *schema.ResourceData, meta interface{}) erro publicAccess = mysql.PublicNetworkAccessEnumDisabled } + ssl := mysql.SslEnforcementEnumEnabled + if v := d.Get("ssl_enforcement"); strings.EqualFold(v.(string), string(mysql.SslEnforcementEnumDisabled)) { + ssl = mysql.SslEnforcementEnumDisabled + } + if v := d.Get("ssl_enforcement_enabled").(bool); !v { + ssl = mysql.SslEnforcementEnumDisabled + } + + storageProfile := expandMySQLStorageProfile(d) + properties := mysql.ServerUpdateParameters{ ServerUpdateParametersProperties: &mysql.ServerUpdateParametersProperties{ - StorageProfile: expandMySQLStorageProfile(d), AdministratorLoginPassword: utils.String(d.Get("administrator_login_password").(string)), - Version: mysql.ServerVersion(d.Get("version").(string)), - SslEnforcement: mysql.SslEnforcementEnum(d.Get("ssl_enforcement").(string)), PublicNetworkAccess: publicAccess, + SslEnforcement: ssl, + StorageProfile: storageProfile, + Version: mysql.ServerVersion(d.Get("version").(string)), }, Sku: sku, Tags: tags.Expand(d.Get("tags").(map[string]interface{})), } - future, err := client.Update(ctx, resourceGroup, name, properties) + future, err := client.Update(ctx, id.ResourceGroup, id.Name, properties) if err != nil { - return fmt.Errorf("Error updating MySQL Server %q (Resource Group %q): %+v", name, resourceGroup, err) + return fmt.Errorf("updating MySQL Server %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) } if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { - return fmt.Errorf("Error waiting for MySQL Server %q (Resource Group %q) to finish updating: %+v", name, resourceGroup, err) + return fmt.Errorf("waiting for MySQL Server %q (Resource Group %q) to finish updating: %+v", id.Name, id.ResourceGroup, err) } - read, err := client.Get(ctx, resourceGroup, name) + read, err := client.Get(ctx, id.ResourceGroup, id.Name) if err != nil { - return fmt.Errorf("Error retrieving MySQL Server %q (Resource Group %q): %+v", name, resourceGroup, err) + return fmt.Errorf("retrieving MySQL Server %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) } if read.ID == nil { - return fmt.Errorf("Cannot read MySQL Server %q (Resource Group %q) ID", name, resourceGroup) + return fmt.Errorf("cannot read MySQL Server %q (Resource Group %q) ID", id.Name, id.ResourceGroup) } d.SetId(*read.ID) @@ -311,25 +527,24 @@ func resourceArmMySqlServerRead(d *schema.ResourceData, meta interface{}) error ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) defer cancel() - id, err := azure.ParseAzureResourceID(d.Id()) + id, err := parse.MysqlServerServerID(d.Id()) if err != nil { - return err + return fmt.Errorf("parsing MySQL Server ID : %v", err) } - resourceGroup := id.ResourceGroup - name := id.Path["servers"] - resp, err := client.Get(ctx, resourceGroup, name) + resp, err := client.Get(ctx, id.ResourceGroup, id.Name) if err != nil { if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[WARN] MySQL Server %q was not found (Resource Group %q)", id.Name, id.ResourceGroup) d.SetId("") return nil } - return fmt.Errorf("Error making Read request on Azure MySQL Server %q (Resource Group %q): %+v", name, resourceGroup, err) + return fmt.Errorf("making Read request on Azure MySQL Server %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) } d.Set("name", resp.Name) - d.Set("resource_group_name", resourceGroup) + d.Set("resource_group_name", id.ResourceGroup) if location := resp.Location; location != nil { d.Set("location", azure.NormalizeLocation(*location)) @@ -339,17 +554,29 @@ func resourceArmMySqlServerRead(d *schema.ResourceData, meta interface{}) error d.Set("sku_name", sku.Name) } - d.Set("administrator_login", resp.AdministratorLogin) - d.Set("version", string(resp.Version)) - d.Set("ssl_enforcement", string(resp.SslEnforcement)) - d.Set("public_network_access_enabled", resp.PublicNetworkAccess != mysql.PublicNetworkAccessEnumDisabled) + if props := resp.ServerProperties; props != nil { + d.Set("administrator_login", props.AdministratorLogin) + d.Set("infrastructure_encryption_enabled", props.InfrastructureEncryption == mysql.InfrastructureEncryptionEnabled) + d.Set("public_network_access_enabled", props.PublicNetworkAccess == mysql.PublicNetworkAccessEnumEnabled) + d.Set("ssl_enforcement", string(props.SslEnforcement)) + d.Set("ssl_enforcement_enabled", props.SslEnforcement == mysql.SslEnforcementEnumEnabled) + d.Set("ssl_minimal_tls_version_enforced", props.MinimalTLSVersion) + d.Set("version", string(props.Version)) + + if err := d.Set("storage_profile", flattenMySQLStorageProfile(resp.StorageProfile)); err != nil { + return fmt.Errorf("setting `storage_profile`: %+v", err) + } - if err := d.Set("storage_profile", flattenMySQLStorageProfile(resp.StorageProfile)); err != nil { - return fmt.Errorf("Error setting `storage_profile`: %+v", err) - } + if storage := props.StorageProfile; storage != nil { + d.Set("auto_grow_enabled", storage.StorageAutogrow == mysql.StorageAutogrowEnabled) + d.Set("backup_retention_days", storage.BackupRetentionDays) + d.Set("geo_redundant_backup_enabled", storage.GeoRedundantBackup == mysql.Enabled) + d.Set("storage_mb", storage.StorageMB) + } - // Computed - d.Set("fqdn", resp.FullyQualifiedDomainName) + // Computed + d.Set("fqdn", props.FullyQualifiedDomainName) + } return tags.FlattenAndSet(d, resp.Tags) } @@ -359,20 +586,24 @@ func resourceArmMySqlServerDelete(d *schema.ResourceData, meta interface{}) erro ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) defer cancel() - id, err := azure.ParseAzureResourceID(d.Id()) + id, err := parse.MysqlServerServerID(d.Id()) if err != nil { - return err + return fmt.Errorf("parsing MySQL Server ID : %v", err) } - resourceGroup := id.ResourceGroup - name := id.Path["servers"] - future, err := client.Delete(ctx, resourceGroup, name) + future, err := client.Delete(ctx, id.ResourceGroup, id.Name) if err != nil { - return fmt.Errorf("Error deleting MySQL Server %q (Resource Group %q): %+v", name, resourceGroup, err) + if response.WasNotFound(future.Response()) { + return nil + } + return fmt.Errorf("deleting MySQL Server %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) } if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { - return fmt.Errorf("Error waiting for deletion of MySQL Server %q (Resource Group %q): %+v", name, resourceGroup, err) + if response.WasNotFound(future.Response()) { + return nil + } + return fmt.Errorf("waiting for deletion of MySQL Server %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) } return nil @@ -410,36 +641,59 @@ func expandServerSkuName(skuName string) (*mysql.Sku, error) { } func expandMySQLStorageProfile(d *schema.ResourceData) *mysql.StorageProfile { - storageprofiles := d.Get("storage_profile").([]interface{}) - storageprofile := storageprofiles[0].(map[string]interface{}) + storage := mysql.StorageProfile{} + if v, ok := d.GetOk("storage_profile"); ok { + storageprofile := v.([]interface{})[0].(map[string]interface{}) + + storage.BackupRetentionDays = utils.Int32(int32(storageprofile["backup_retention_days"].(int))) + storage.GeoRedundantBackup = mysql.GeoRedundantBackup(storageprofile["geo_redundant_backup"].(string)) + storage.StorageAutogrow = mysql.StorageAutogrow(storageprofile["auto_grow"].(string)) + storage.StorageMB = utils.Int32(int32(storageprofile["storage_mb"].(int))) + } + + + // now override whatever we may have from the block with the top level properties + if v, ok := d.GetOk("auto_grow_enabled"); ok { + storage.StorageAutogrow = mysql.StorageAutogrowDisabled + if v.(bool) { + storage.StorageAutogrow = mysql.StorageAutogrowEnabled + } + } + + if v, ok := d.GetOk("backup_retention_days"); ok { + storage.BackupRetentionDays = utils.Int32(int32(v.(int))) + } - backupRetentionDays := storageprofile["backup_retention_days"].(int) - geoRedundantBackup := storageprofile["geo_redundant_backup"].(string) - storageMB := storageprofile["storage_mb"].(int) - autoGrow := storageprofile["auto_grow"].(string) + if v, ok := d.GetOk("geo_redundant_backup_enabled"); ok { + storage.GeoRedundantBackup = mysql.Disabled + if v.(bool) { + storage.GeoRedundantBackup = mysql.Enabled + } + } - return &mysql.StorageProfile{ - BackupRetentionDays: utils.Int32(int32(backupRetentionDays)), - GeoRedundantBackup: mysql.GeoRedundantBackup(geoRedundantBackup), - StorageMB: utils.Int32(int32(storageMB)), - StorageAutogrow: mysql.StorageAutogrow(autoGrow), + if v, ok := d.GetOk("storage_mb"); ok { + storage.StorageMB = utils.Int32(int32(v.(int))) } + + return &storage } func flattenMySQLStorageProfile(resp *mysql.StorageProfile) []interface{} { values := map[string]interface{}{} - if storageMB := resp.StorageMB; storageMB != nil { - values["storage_mb"] = *storageMB - } + values["auto_grow"] = string(resp.StorageAutogrow) + values["backup_retention_days"] = nil if backupRetentionDays := resp.BackupRetentionDays; backupRetentionDays != nil { values["backup_retention_days"] = *backupRetentionDays } values["geo_redundant_backup"] = string(resp.GeoRedundantBackup) - values["auto_grow"] = string(resp.StorageAutogrow) + values["storage_mb"] = nil + if storageMB := resp.StorageMB; storageMB != nil { + values["storage_mb"] = *storageMB + } return []interface{}{values} } diff --git a/azurerm/internal/services/mysql/mysql_virtual_network_rule_resource.go b/azurerm/internal/services/mysql/mysql_virtual_network_rule_resource.go index 21a520afca60..95c0362d61e8 100644 --- a/azurerm/internal/services/mysql/mysql_virtual_network_rule_resource.go +++ b/azurerm/internal/services/mysql/mysql_virtual_network_rule_resource.go @@ -3,6 +3,7 @@ package mysql import ( "context" "fmt" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/mysql/validate" "log" "time" @@ -10,10 +11,9 @@ import ( "github.com/hashicorp/go-azure-helpers/response" "github.com/hashicorp/terraform-plugin-sdk/helper/resource" "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/tf" - "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate" + 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/timeouts" @@ -42,7 +42,7 @@ func resourceArmMySqlVirtualNetworkRule() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, - ValidateFunc: validate.VirtualNetworkRuleName, + ValidateFunc: azValidate.VirtualNetworkRuleName, }, "resource_group_name": azure.SchemaResourceGroupName(), @@ -51,7 +51,7 @@ func resourceArmMySqlVirtualNetworkRule() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, - ValidateFunc: validation.StringIsNotEmpty, + ValidateFunc: validate.MysqlServerServerName, }, "subnet_id": { diff --git a/azurerm/internal/services/mysql/parse/mysql.go b/azurerm/internal/services/mysql/parse/mysql.go new file mode 100644 index 000000000000..474fa49bf666 --- /dev/null +++ b/azurerm/internal/services/mysql/parse/mysql.go @@ -0,0 +1,32 @@ +package parse + +import ( + "fmt" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type MysqlServerServerId struct { + ResourceGroup string + Name string +} + +func MysqlServerServerID(input string) (*MysqlServerServerId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("[ERROR] Unable to parse MySQL Server ID %q: %+v", input, err) + } + + server := MysqlServerServerId{ + 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 +} diff --git a/azurerm/internal/services/mysql/parse/mysql_test.go b/azurerm/internal/services/mysql/parse/mysql_test.go new file mode 100644 index 000000000000..bf3bb278ef38 --- /dev/null +++ b/azurerm/internal/services/mysql/parse/mysql_test.go @@ -0,0 +1,69 @@ +package parse + +import ( + "testing" +) + +func TestValidateMysqlServerServerID(t *testing.T) { + testData := []struct { + Name string + Input string + Expected *MysqlServerServerId + }{ + { + Name: "Empty resource ID", + Input: "", + Expected: nil, + }, + { + Name: "No resourceGroups segment", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Expected: nil, + }, + { + Name: "No resource group name", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/", + Expected: nil, + }, + { + Name: "Resource group", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/", + Expected: nil, + }, + { + Name: "Missing server name", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforMySQL/servers/", + Expected: nil, + }, + { + Name: "Valid", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforMySQL/servers/test-mysql", + Expected: &MysqlServerServerId{ + Name: "test-mysql", + ResourceGroup: "test-rg", + }, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Name) + + actual, err := MysqlServerServerID(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/mysql/tests/mysql_server_resource_test.go b/azurerm/internal/services/mysql/tests/mysql_server_resource_test.go index 833e6aa6ea71..d08433c9f220 100644 --- a/azurerm/internal/services/mysql/tests/mysql_server_resource_test.go +++ b/azurerm/internal/services/mysql/tests/mysql_server_resource_test.go @@ -3,6 +3,7 @@ package tests import ( "fmt" "testing" + "time" "github.com/hashicorp/terraform-plugin-sdk/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/terraform" @@ -20,7 +21,7 @@ func TestAccAzureRMMySQLServer_basicFiveSix(t *testing.T) { CheckDestroy: testCheckAzureRMMySQLServerDestroy, Steps: []resource.TestStep{ { - Config: testAccAzureRMMySQLServer_basicFiveSix(data), + Config: testAccAzureRMMySQLServer_basic(data, "5.6"), Check: resource.ComposeTestCheckFunc( testCheckAzureRMMySQLServerExists(data.ResourceName), ), @@ -30,7 +31,7 @@ func TestAccAzureRMMySQLServer_basicFiveSix(t *testing.T) { }) } -func TestAccAzureRMMySQLServer_disablePublicNetworkAccess(t *testing.T) { +func TestAccAzureRMMySQLServer_basicFiveSixDeprecated(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_mysql_server", "test") resource.ParallelTest(t, resource.TestCase{ @@ -39,7 +40,7 @@ func TestAccAzureRMMySQLServer_disablePublicNetworkAccess(t *testing.T) { CheckDestroy: testCheckAzureRMMySQLServerDestroy, Steps: []resource.TestStep{ { - Config: testAccAzureRMMySQLServer_disablePublicNetworkAccess(data), + Config: testAccAzureRMMySQLServer_basicDeprecated(data, "5.6"), Check: resource.ComposeTestCheckFunc( testCheckAzureRMMySQLServerExists(data.ResourceName), ), @@ -49,7 +50,7 @@ func TestAccAzureRMMySQLServer_disablePublicNetworkAccess(t *testing.T) { }) } -func TestAccAzureRMMySQLServer_requiresImport(t *testing.T) { +func TestAccAzureRMMySQLServer_basicFiveSeven(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_mysql_server", "test") resource.ParallelTest(t, resource.TestCase{ @@ -58,21 +59,38 @@ func TestAccAzureRMMySQLServer_requiresImport(t *testing.T) { CheckDestroy: testCheckAzureRMMySQLServerDestroy, Steps: []resource.TestStep{ { - Config: testAccAzureRMMySQLServer_basicFiveSevenUpdated(data), + Config: testAccAzureRMMySQLServer_basic(data, "5.7"), Check: resource.ComposeTestCheckFunc( testCheckAzureRMMySQLServerExists(data.ResourceName), ), }, + data.ImportStep("administrator_login_password"), // not returned as sensitive + }, + }) +} + +func TestAccAzureRMMySQLServer_basicEightZero(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_mysql_server", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMySQLServerDestroy, + Steps: []resource.TestStep{ { - Config: testAccAzureRMMySQLServer_requiresImport(data), - ExpectError: acceptance.RequiresImportError("azurerm_mysql_server"), + Config: testAccAzureRMMySQLServer_basic(data, "8.0"), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMySQLServerExists(data.ResourceName), + ), }, + data.ImportStep("administrator_login_password"), // not returned as sensitive }, }) } -func TestAccAzureRMMySQLServer_basicFiveSeven(t *testing.T) { +func TestAccAzureRMMySQLServer_autogrowOnly(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_mysql_server", "test") + mysqlVersion := "5.7" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acceptance.PreCheck(t) }, @@ -80,7 +98,14 @@ func TestAccAzureRMMySQLServer_basicFiveSeven(t *testing.T) { CheckDestroy: testCheckAzureRMMySQLServerDestroy, Steps: []resource.TestStep{ { - Config: testAccAzureRMMySQLServer_basicFiveSeven(data), + Config: testAccAzureRMMySQLServer_autogrow(data, mysqlVersion), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMySQLServerExists(data.ResourceName), + ), + }, + data.ImportStep("administrator_login_password"), // not returned as sensitive + { + Config: testAccAzureRMMySQLServer_basic(data, mysqlVersion), Check: resource.ComposeTestCheckFunc( testCheckAzureRMMySQLServerExists(data.ResourceName), ), @@ -90,7 +115,26 @@ func TestAccAzureRMMySQLServer_basicFiveSeven(t *testing.T) { }) } -func TestAccAzureRMMySQLServer_basicEightZero(t *testing.T) { +func TestAccAzureRMMySQLServer_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_mysql_server", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMySQLServerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMMySQLServer_basic(data, "5.7"), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMySQLServerExists(data.ResourceName), + ), + }, + data.RequiresImportErrorStep(testAccAzureRMMySQLServer_requiresImport), + }, + }) +} + +func TestAccAzureRMMySQLServer_complete(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_mysql_server", "test") resource.ParallelTest(t, resource.TestCase{ @@ -99,7 +143,7 @@ func TestAccAzureRMMySQLServer_basicEightZero(t *testing.T) { CheckDestroy: testCheckAzureRMMySQLServerDestroy, Steps: []resource.TestStep{ { - Config: testAccAzureRMMySQLServer_basicEightZero(data), + Config: testAccAzureRMMySQLServer_complete(data, "8.0"), Check: resource.ComposeTestCheckFunc( testCheckAzureRMMySQLServerExists(data.ResourceName), ), @@ -109,8 +153,9 @@ func TestAccAzureRMMySQLServer_basicEightZero(t *testing.T) { }) } -func TestAccAzureRMMySqlServer_memoryOptimized(t *testing.T) { +func TestAccAzureRMMySQLServer_update(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_mysql_server", "test") + mysqlVersion := "8.0" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acceptance.PreCheck(t) }, @@ -118,17 +163,60 @@ func TestAccAzureRMMySqlServer_memoryOptimized(t *testing.T) { CheckDestroy: testCheckAzureRMMySQLServerDestroy, Steps: []resource.TestStep{ { - Config: testAccAzureRMMySQLServer_memoryOptimizedGeoRedundant(data), + Config: testAccAzureRMMySQLServer_basic(data, mysqlVersion), Check: resource.ComposeTestCheckFunc( testCheckAzureRMMySQLServerExists(data.ResourceName), ), - }, data.ImportStep("administrator_login_password"), // not returned as sensitive + }, + data.ImportStep("administrator_login_password"), // not returned as sensitive + { + Config: testAccAzureRMMySQLServer_complete(data, mysqlVersion), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMySQLServerExists(data.ResourceName), + ), + }, + data.ImportStep("administrator_login_password"), // not returned as sensitive + { + Config: testAccAzureRMMySQLServer_basic(data, mysqlVersion), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMySQLServerExists(data.ResourceName), + ), + }, + data.ImportStep("administrator_login_password"), // not returned as sensitive + }, + }) +} + +func TestAccAzureRMMySQLServer_completeDeprecatedMigrate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_mysql_server", "test") + mysqlVersion := "5.6" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMySQLServerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMMySQLServer_completeDeprecated(data, mysqlVersion), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMySQLServerExists(data.ResourceName), + ), + }, + data.ImportStep("administrator_login_password"), // not returned as sensitive + { + Config: testAccAzureRMMySQLServer_complete(data, mysqlVersion), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMySQLServerExists(data.ResourceName), + ), + }, + data.ImportStep("administrator_login_password"), // not returned as sensitive }, }) } -func TestAccAzureRMMySQLServer_basicFiveSevenUpdated(t *testing.T) { +func TestAccAzureRMMySQLServer_updateDeprecated(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_mysql_server", "test") + mysqlVersion := "5.6" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acceptance.PreCheck(t) }, @@ -136,23 +224,23 @@ func TestAccAzureRMMySQLServer_basicFiveSevenUpdated(t *testing.T) { CheckDestroy: testCheckAzureRMMySQLServerDestroy, Steps: []resource.TestStep{ { - Config: testAccAzureRMMySQLServer_basicFiveSeven(data), + Config: testAccAzureRMMySQLServer_basicDeprecated(data, mysqlVersion), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMySQLServerExists(data.ResourceName), + ), + }, + data.ImportStep("administrator_login_password"), // not returned as sensitive + { + Config: testAccAzureRMMySQLServer_completeDeprecated(data, mysqlVersion), Check: resource.ComposeTestCheckFunc( testCheckAzureRMMySQLServerExists(data.ResourceName), - resource.TestCheckResourceAttr(data.ResourceName, "sku_name", "GP_Gen5_2"), - resource.TestCheckResourceAttr(data.ResourceName, "version", "5.7"), - resource.TestCheckResourceAttr(data.ResourceName, "storage_profile.0.storage_mb", "51200"), - resource.TestCheckResourceAttr(data.ResourceName, "administrator_login", "acctestun"), ), }, + data.ImportStep("administrator_login_password"), // not returned as sensitive { - Config: testAccAzureRMMySQLServer_basicFiveSevenUpdated(data), + Config: testAccAzureRMMySQLServer_basicDeprecated(data, mysqlVersion), Check: resource.ComposeTestCheckFunc( testCheckAzureRMMySQLServerExists(data.ResourceName), - resource.TestCheckResourceAttr(data.ResourceName, "sku_name", "GP_Gen5_4"), - resource.TestCheckResourceAttr(data.ResourceName, "version", "5.7"), - resource.TestCheckResourceAttr(data.ResourceName, "storage_profile.0.storage_mb", "640000"), - resource.TestCheckResourceAttr(data.ResourceName, "administrator_login", "acctestun"), ), }, data.ImportStep("administrator_login_password"), // not returned as sensitive @@ -169,29 +257,26 @@ func TestAccAzureRMMySQLServer_updateSKU(t *testing.T) { CheckDestroy: testCheckAzureRMMySQLServerDestroy, Steps: []resource.TestStep{ { - Config: testAccAzureRMMySQLServer_basicEightZero(data), + Config: testAccAzureRMMySQLServer_sku(data, "GP_Gen5_2"), Check: resource.ComposeTestCheckFunc( testCheckAzureRMMySQLServerExists(data.ResourceName), - resource.TestCheckResourceAttr(data.ResourceName, "sku_name", "GP_Gen5_2"), - resource.TestCheckResourceAttr(data.ResourceName, "storage_profile.0.storage_mb", "51200"), - resource.TestCheckResourceAttr(data.ResourceName, "administrator_login", "acctestun"), ), }, + data.ImportStep("administrator_login_password"), // not returned as sensitive { - Config: testAccAzureRMMySQLServer_basicFiveSevenUpdated(data), + Config: testAccAzureRMMySQLServer_sku(data, "MO_Gen5_16"), Check: resource.ComposeTestCheckFunc( testCheckAzureRMMySQLServerExists(data.ResourceName), - resource.TestCheckResourceAttr(data.ResourceName, "sku_name", "GP_Gen5_4"), - resource.TestCheckResourceAttr(data.ResourceName, "storage_profile.0.storage_mb", "4194304"), - resource.TestCheckResourceAttr(data.ResourceName, "administrator_login", "acctestun"), ), }, + data.ImportStep("administrator_login_password"), // not returned as sensitive }, }) } -func TestAccAzureRMMySQLServer_storageAutogrow(t *testing.T) { +func TestAccAzureRMMySQLServer_createReplica(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_mysql_server", "test") + mysqlVersion := "8.0" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acceptance.PreCheck(t) }, @@ -199,24 +284,52 @@ func TestAccAzureRMMySQLServer_storageAutogrow(t *testing.T) { CheckDestroy: testCheckAzureRMMySQLServerDestroy, Steps: []resource.TestStep{ { - Config: testAccAzureRMMySQLServer_basicFiveSeven(data), + Config: testAccAzureRMMySQLServer_basic(data, mysqlVersion), Check: resource.ComposeTestCheckFunc( testCheckAzureRMMySQLServerExists(data.ResourceName), - resource.TestCheckResourceAttr(data.ResourceName, "storage_profile.0.auto_grow", "Enabled"), ), }, + data.ImportStep("administrator_login_password"), { - Config: testAccAzureRMMySQLServer_autogrow(data), + Config: testAccAzureRMMySQLServer_createReplica(data, mysqlVersion), Check: resource.ComposeTestCheckFunc( testCheckAzureRMMySQLServerExists(data.ResourceName), - resource.TestCheckResourceAttr(data.ResourceName, "storage_profile.0.auto_grow", "Disabled"), + testCheckAzureRMMySQLServerExists("azurerm_mysql_server.replica"), ), }, + data.ImportStep("administrator_login_password"), }, }) } -// +func TestAccAzureRMMySQLServer_createPointInTimeRestore(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_mysql_server", "test") + restoreTime := time.Now().Add(11 * time.Minute) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMySQLServerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMMySQLServer_basic(data, "8.0"), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMySQLServerExists(data.ResourceName), + ), + }, + data.ImportStep("administrator_login_password"), + { + PreConfig: func() { time.Sleep(restoreTime.Sub(time.Now().Add(-7 * time.Minute))) }, + Config: testAccAzureRMMySQLServer_createPointInTimeRestore(data, "8.0", restoreTime.Format(time.RFC3339)), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMySQLServerExists(data.ResourceName), + testCheckAzureRMMySQLServerExists("azurerm_mysql_server.restore"), + ), + }, + data.ImportStep("administrator_login_password"), + }, + }) +} func testCheckAzureRMMySQLServerExists(resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -273,7 +386,7 @@ func testCheckAzureRMMySQLServerDestroy(s *terraform.State) error { return nil } -func testAccAzureRMMySQLServer_basicFiveSix(data acceptance.TestData) string { +func testAccAzureRMMySQLServer_basic(data acceptance.TestData, version string) string { return fmt.Sprintf(` provider "azurerm" { features {} @@ -285,27 +398,20 @@ resource "azurerm_resource_group" "test" { } resource "azurerm_mysql_server" "test" { - name = "acctestmysqlsvr-%d" - location = azurerm_resource_group.test.location - resource_group_name = azurerm_resource_group.test.name - - sku_name = "GP_Gen5_2" - - storage_profile { - storage_mb = 51200 - backup_retention_days = 7 - geo_redundant_backup = "Disabled" - } - + name = "acctestmysqlsvr-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "GP_Gen5_2" administrator_login = "acctestun" administrator_login_password = "H@Sh1CoR3!" - version = "5.6" - ssl_enforcement = "Enabled" + ssl_enforcement_enabled = true + storage_mb = 51200 + version = "%s" } -`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, version) } -func testAccAzureRMMySQLServer_disablePublicNetworkAccess(data acceptance.TestData) string { +func testAccAzureRMMySQLServer_basicDeprecated(data acceptance.TestData, version string) string { return fmt.Sprintf(` provider "azurerm" { features {} @@ -325,20 +431,17 @@ resource "azurerm_mysql_server" "test" { storage_profile { storage_mb = 51200 - backup_retention_days = 7 - geo_redundant_backup = "Disabled" } - administrator_login = "acctestun" - administrator_login_password = "H@Sh1CoR3!" - version = "5.6" - ssl_enforcement = "Enabled" - public_network_access_enabled = false + administrator_login = "acctestun" + administrator_login_password = "H@Sh1CoR3!" + version = "%s" + ssl_enforcement = "Enabled" } -`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, version) } -func testAccAzureRMMySQLServer_basicFiveSeven(data acceptance.TestData) string { +func testAccAzureRMMySQLServer_complete(data acceptance.TestData, version string) string { return fmt.Sprintf(` provider "azurerm" { features {} @@ -350,27 +453,24 @@ resource "azurerm_resource_group" "test" { } resource "azurerm_mysql_server" "test" { - name = "acctestmysqlsvr-%d" - location = azurerm_resource_group.test.location - resource_group_name = azurerm_resource_group.test.name - - sku_name = "GP_Gen5_2" - - storage_profile { - storage_mb = 51200 - backup_retention_days = 7 - geo_redundant_backup = "Disabled" - } - + name = "acctestmysqlsvr-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "GP_Gen5_2" administrator_login = "acctestun" administrator_login_password = "H@Sh1CoR3!" - version = "5.7" - ssl_enforcement = "Enabled" + auto_grow_enabled = true + backup_retention_days = 7 + create_mode = "Default" + geo_redundant_backup_enabled = false + ssl_enforcement_enabled = true + storage_mb = 51200 + version = "%s" } -`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, version) } -func testAccAzureRMMySQLServer_basicEightZero(data acceptance.TestData) string { +func testAccAzureRMMySQLServer_completeDeprecated(data acceptance.TestData, version string) string { return fmt.Sprintf(` provider "azurerm" { features {} @@ -392,14 +492,15 @@ resource "azurerm_mysql_server" "test" { storage_mb = 51200 backup_retention_days = 7 geo_redundant_backup = "Disabled" + auto_grow = "Enabled" } administrator_login = "acctestun" administrator_login_password = "H@Sh1CoR3!" - version = "8.0" + version = "%s" ssl_enforcement = "Enabled" } -`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, version) } func testAccAzureRMMySQLServer_requiresImport(data acceptance.TestData) string { @@ -424,10 +525,10 @@ resource "azurerm_mysql_server" "import" { version = "5.7" ssl_enforcement = "Enabled" } -`, testAccAzureRMMySQLServer_basicFiveSevenUpdated(data)) +`, testAccAzureRMMySQLServer_basic(data, "5.7")) } -func testAccAzureRMMySQLServer_basicFiveSevenUpdated(data acceptance.TestData) string { +func testAccAzureRMMySQLServer_sku(data acceptance.TestData, sku string) string { return fmt.Sprintf(` provider "azurerm" { features {} @@ -439,27 +540,20 @@ resource "azurerm_resource_group" "test" { } resource "azurerm_mysql_server" "test" { - name = "acctestmysqlsvr-%d" - location = azurerm_resource_group.test.location - resource_group_name = azurerm_resource_group.test.name - - sku_name = "GP_Gen5_4" - - storage_profile { - storage_mb = 640000 - backup_retention_days = 7 - geo_redundant_backup = "Disabled" - } - + name = "acctestmysqlsvr-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "%s" + storage_mb = 4194304 administrator_login = "acctestun" administrator_login_password = "H@Sh1CoR3!" version = "5.7" ssl_enforcement = "Enabled" } -`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, sku) } -func testAccAzureRMMySQLServer_memoryOptimizedGeoRedundant(data acceptance.TestData) string { +func testAccAzureRMMySQLServer_autogrow(data acceptance.TestData, version string) string { return fmt.Sprintf(` provider "azurerm" { features {} @@ -471,55 +565,54 @@ resource "azurerm_resource_group" "test" { } resource "azurerm_mysql_server" "test" { - name = "acctestmysqlsvr-%d" - location = azurerm_resource_group.test.location - resource_group_name = azurerm_resource_group.test.name - - sku_name = "MO_Gen5_16" - - storage_profile { - storage_mb = 4194304 - backup_retention_days = 7 - geo_redundant_backup = "Enabled" - } + name = "acctestmysqlsvr-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "GP_Gen5_2" + administrator_login = "acctestun" + administrator_login_password = "H@Sh1CoR3!" + auto_grow_enabled = true + backup_retention_days = 7 + geo_redundant_backup_enabled = false + ssl_enforcement_enabled = true + storage_mb = 51200 + version = "%s" +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, version) +} + +func testAccAzureRMMySQLServer_createReplica(data acceptance.TestData, version string) string { + return fmt.Sprintf(` +%s - administrator_login = "acctestun" - administrator_login_password = "H@Sh1CoR3!" - version = "5.7" - ssl_enforcement = "Enabled" +resource "azurerm_mysql_server" "replica" { + name = "acctestmysqlsvr-%d-replica" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "GP_Gen5_2" + create_mode = "Replica" + creation_source_server_id = azurerm_mysql_server.test.id + ssl_enforcement_enabled = true + version = "%s" } -`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +`, testAccAzureRMMySQLServer_basic(data, version), data.RandomInteger, version) } -func testAccAzureRMMySQLServer_autogrow(data acceptance.TestData) string { +func testAccAzureRMMySQLServer_createPointInTimeRestore(data acceptance.TestData, version, restoreTime string) string { return fmt.Sprintf(` -provider "azurerm" { - features {} -} - -resource "azurerm_resource_group" "test" { - name = "acctestRG-%d" - location = "%s" -} - -resource "azurerm_mysql_server" "test" { - name = "acctestmysqlsvr-%d" - location = azurerm_resource_group.test.location - resource_group_name = azurerm_resource_group.test.name - - sku_name = "GP_Gen5_2" - - storage_profile { - storage_mb = 51200 - backup_retention_days = 7 - geo_redundant_backup = "Disabled" - auto_grow = "Disabled" - } +%s - administrator_login = "acctestun" - administrator_login_password = "H@Sh1CoR3!" - version = "5.7" - ssl_enforcement = "Enabled" -} -`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +resource "azurerm_mysql_server" "restore" { + name = "acctestmysqlsvr-%d-restore" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "GP_Gen5_2" + create_mode = "PointInTimeRestore" + creation_source_server_id = azurerm_mysql_server.test.id + restore_point_in_time = "%s" + ssl_enforcement_enabled = true + storage_mb = 51200 + version = "%s" +} +`, testAccAzureRMMySQLServer_basic(data, version), data.RandomInteger, restoreTime, version) } diff --git a/azurerm/internal/services/mysql/validate/mysql.go b/azurerm/internal/services/mysql/validate/mysql.go new file mode 100644 index 000000000000..8ddfcfe7831a --- /dev/null +++ b/azurerm/internal/services/mysql/validate/mysql.go @@ -0,0 +1,29 @@ +package validate + +import ( + "fmt" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/mysql/parse" +) + +func MysqlServerServerID(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.MysqlServerServerID(v); err != nil { + errors = append(errors, fmt.Errorf("Can not parse %q as a MySQL Server resource id: %v", k, err)) + } + + return warnings, errors +} + +func MysqlServerServerName(i interface{}, k string) (_ []string, errors []error) { + if m, regexErrs := validate.RegExHelper(i, k, `^[0-9a-z][-0-9a-z]{1,61}[0-9a-z]$`); !m { + return nil, append(regexErrs, fmt.Errorf("%q can contain only lowercase letters, numbers, and '-', but can't start or end with '-', and must be at least 3 characters and no more than 63 characters long.", k)) + } + + return nil, nil +} diff --git a/azurerm/internal/services/mysql/validate/mysql_test.go b/azurerm/internal/services/mysql/validate/mysql_test.go new file mode 100644 index 000000000000..3e919164fb37 --- /dev/null +++ b/azurerm/internal/services/mysql/validate/mysql_test.go @@ -0,0 +1,106 @@ +package validate + +import ( + "testing" +) + +func TestValidateMysqlServerServerID(t *testing.T) { + testData := []struct { + input string + expected bool + }{ + { + // empty + input: "", + expected: false, + }, + { + // invalid + input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg", + expected: false, + }, + { + // valid + input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforMySQL/servers/test-mysql", + expected: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q..", v.input) + + _, errors := MysqlServerServerID(v.input, "name") + actual := len(errors) == 0 + if v.expected != actual { + t.Fatalf("Expected %t but got %t", v.expected, actual) + } + } +} + +func TestValidateMysqlServerServerName(t *testing.T) { + testData := []struct { + input string + expected bool + }{ + { + // empty + input: "", + expected: false, + }, + { + // basic example + input: "ab-c", + expected: true, + }, + { + // can't contain upper case letter + input: "AbcD", + expected: false, + }, + { + // can't start with a hyphen + input: "-abc", + expected: false, + }, + { + // can't contain underscore + input: "ab_c", + expected: false, + }, + { + // can't end with hyphen + input: "abc-", + expected: false, + }, + { + // can not be shorter than 3 characters + input: "ab", + expected: false, + }, + { + // can not be shorter than 3 characters (catching bad regex) + input: "a", + expected: false, + }, + { + // 63 chars + input: "abcdefghijklmnopqrstuvwxyzabcdefabcdefghijklmnopqrstuvwxyzabcde", + expected: true, + }, + { + // 64 chars + input: "abcdefghijklmnopqrstuvwxyzabcdefabcdefghijklmnopqrstuvwxyzabcdef", + expected: false, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q..", v.input) + + _, errors := MysqlServerServerName(v.input, "name") + actual := len(errors) == 0 + if v.expected != actual { + t.Fatalf("Expected %t but got %t", v.expected, actual) + } + } +} diff --git a/azurerm/internal/services/postgres/parse/postgres_test.go b/azurerm/internal/services/postgres/parse/postgres_test.go index b3b2dcc1fb6b..ae2dfa439205 100644 --- a/azurerm/internal/services/postgres/parse/postgres_test.go +++ b/azurerm/internal/services/postgres/parse/postgres_test.go @@ -32,7 +32,7 @@ func TestAnalysisServicesServerId(t *testing.T) { }, { Name: "Missing Servers Value", - Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Web/servers/", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.DBforPostgreSQL/servers/", Expected: nil, }, { diff --git a/azurerm/internal/services/postgres/postgresql_server_resource.go b/azurerm/internal/services/postgres/postgresql_server_resource.go index 7f8cc2c45208..f9d1eeb3e24f 100644 --- a/azurerm/internal/services/postgres/postgresql_server_resource.go +++ b/azurerm/internal/services/postgres/postgresql_server_resource.go @@ -531,15 +531,15 @@ func resourceArmPostgreSQLServerRead(d *schema.ResourceData, meta interface{}) e d.Set("name", resp.Name) d.Set("resource_group_name", id.ResourceGroup) + if location := resp.Location; location != nil { + d.Set("location", azure.NormalizeLocation(*location)) + } + if sku := resp.Sku; sku != nil { d.Set("sku_name", sku.Name) } if props := resp.ServerProperties; props != nil { - if location := resp.Location; location != nil { - d.Set("location", azure.NormalizeLocation(*location)) - } - d.Set("administrator_login", props.AdministratorLogin) d.Set("ssl_enforcement", string(props.SslEnforcement)) d.Set("ssl_minimal_tls_version_enforced", props.MinimalTLSVersion)