diff --git a/azurerm/internal/services/postgres/client/client.go b/azurerm/internal/services/postgres/client/client.go index 39e56844d646..5ec30cf626ae 100644 --- a/azurerm/internal/services/postgres/client/client.go +++ b/azurerm/internal/services/postgres/client/client.go @@ -14,6 +14,7 @@ type Client struct { ServerSecurityAlertPoliciesClient *postgresql.ServerSecurityAlertPoliciesClient VirtualNetworkRulesClient *postgresql.VirtualNetworkRulesClient ServerAdministratorsClient *postgresql.ServerAdministratorsClient + ReplicasClient *postgresql.ReplicasClient } func NewClient(o *common.ClientOptions) *Client { @@ -41,6 +42,9 @@ func NewClient(o *common.ClientOptions) *Client { serverAdministratorsClient := postgresql.NewServerAdministratorsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&serverAdministratorsClient.Client, o.ResourceManagerAuthorizer) + replicasClient := postgresql.NewReplicasClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&replicasClient.Client, o.ResourceManagerAuthorizer) + return &Client{ ConfigurationsClient: &configurationsClient, DatabasesClient: &databasesClient, @@ -50,5 +54,6 @@ func NewClient(o *common.ClientOptions) *Client { ServerSecurityAlertPoliciesClient: &serverSecurityAlertPoliciesClient, VirtualNetworkRulesClient: &virtualNetworkRulesClient, ServerAdministratorsClient: &serverAdministratorsClient, + ReplicasClient: &replicasClient, } } diff --git a/azurerm/internal/services/postgres/postgresql_server_resource.go b/azurerm/internal/services/postgres/postgresql_server_resource.go index 22c60f3e20d8..a70bec134846 100644 --- a/azurerm/internal/services/postgres/postgresql_server_resource.go +++ b/azurerm/internal/services/postgres/postgresql_server_resource.go @@ -1,6 +1,7 @@ package postgres import ( + "context" "fmt" "log" "strconv" @@ -11,11 +12,13 @@ import ( "github.com/Azure/go-autorest/autorest/date" "github.com/hashicorp/go-azure-helpers/response" "github.com/hashicorp/terraform-plugin-sdk/helper/customdiff" + "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/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/locks" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/postgres/parse" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/postgres/validate" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" @@ -28,6 +31,29 @@ const ( postgreSQLServerResourceName = "azurerm_postgresql_server" ) +var skuList = []string{ + "B_Gen4_1", + "B_Gen4_2", + "B_Gen5_1", + "B_Gen5_2", + "GP_Gen4_2", + "GP_Gen4_4", + "GP_Gen4_8", + "GP_Gen4_16", + "GP_Gen4_32", + "GP_Gen5_2", + "GP_Gen5_4", + "GP_Gen5_8", + "GP_Gen5_16", + "GP_Gen5_32", + "GP_Gen5_64", + "MO_Gen5_2", + "MO_Gen5_4", + "MO_Gen5_8", + "MO_Gen5_16", + "MO_Gen5_32", +} + func resourcePostgreSQLServer() *schema.Resource { return &schema.Resource{ Create: resourcePostgreSQLServerCreate, @@ -70,30 +96,9 @@ func resourcePostgreSQLServer() *schema.Resource { "resource_group_name": azure.SchemaResourceGroupName(), "sku_name": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice([]string{ - "B_Gen4_1", - "B_Gen4_2", - "B_Gen5_1", - "B_Gen5_2", - "GP_Gen4_2", - "GP_Gen4_4", - "GP_Gen4_8", - "GP_Gen4_16", - "GP_Gen4_32", - "GP_Gen5_2", - "GP_Gen5_4", - "GP_Gen5_8", - "GP_Gen5_16", - "GP_Gen5_32", - "GP_Gen5_64", - "MO_Gen5_2", - "MO_Gen5_4", - "MO_Gen5_8", - "MO_Gen5_16", - "MO_Gen5_32", - }, false), + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(skuList, false), }, "version": { @@ -538,6 +543,19 @@ func resourcePostgreSQLServerCreate(d *schema.ResourceData, meta interface{}) er return fmt.Errorf("waiting for creation of PostgreSQL Server %q (Resource Group %q): %+v", name, resourceGroup, err) } + log.Printf("[DEBUG] Waiting for PostgreSQL Server %q (Resource Group %q) to become available", name, resourceGroup) + stateConf := &resource.StateChangeConf{ + Pending: []string{string(postgresql.ServerStateInaccessible)}, + Target: []string{string(postgresql.ServerStateReady)}, + Refresh: postgreSqlStateRefreshFunc(ctx, client, resourceGroup, name), + MinTimeout: 15 * time.Second, + Timeout: d.Timeout(schema.TimeoutCreate), + } + + if _, err = stateConf.WaitForState(); err != nil { + return fmt.Errorf("waiting for PostgreSQL Server %q (Resource Group %q)to become available: %+v", name, resourceGroup, err) + } + read, err := client.Get(ctx, resourceGroup, name) if err != nil { return fmt.Errorf("retrieving PostgreSQL Server %q (Resource Group %q): %+v", name, resourceGroup, err) @@ -569,6 +587,7 @@ func resourcePostgreSQLServerCreate(d *schema.ResourceData, meta interface{}) er func resourcePostgreSQLServerUpdate(d *schema.ResourceData, meta interface{}) error { client := meta.(*clients.Client).Postgres.ServersClient securityClient := meta.(*clients.Client).Postgres.ServerSecurityAlertPoliciesClient + replicasClient := meta.(*clients.Client).Postgres.ReplicasClient ctx, cancel := timeouts.ForUpdate(meta.(*clients.Client).StopContext, d) defer cancel() @@ -581,11 +600,47 @@ func resourcePostgreSQLServerUpdate(d *schema.ResourceData, meta interface{}) er return fmt.Errorf("parsing Postgres Server ID : %v", err) } + // Locks for upscaling of replicas + mode := postgresql.CreateMode(d.Get("create_mode").(string)) + primaryID := id.String() + if mode == postgresql.CreateModeReplica { + primaryID = d.Get("creation_source_server_id").(string) + } + locks.ByID(primaryID) + defer locks.UnlockByID(primaryID) + sku, err := expandServerSkuName(d.Get("sku_name").(string)) if err != nil { return fmt.Errorf("expanding `sku_name` for PostgreSQL Server %s (Resource Group %q): %v", id.Name, id.ResourceGroup, err) } + if d.HasChange("sku_name") && mode != postgresql.CreateModeReplica { + oldRaw, newRaw := d.GetChange("sku_name") + old := oldRaw.(string) + new := newRaw.(string) + + if indexOfSku(old) < indexOfSku(new) { + listReplicas, err := replicasClient.ListByServer(ctx, id.ResourceGroup, id.Name) + if err != nil { + return fmt.Errorf("listing replicas for PostgreSQL Server %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + } + + propertiesReplica := postgresql.ServerUpdateParameters{ + Sku: sku, + } + for _, replica := range *listReplicas.Value { + future, err := client.Update(ctx, id.ResourceGroup, *replica.Name, propertiesReplica) + if err != nil { + return fmt.Errorf("upscaling PostgreSQL Server Replica %q (Resource Group %q): %+v", *replica.Name, id.ResourceGroup, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for update of PostgreSQL Server Replica %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + } + } + } + } + publicAccess := postgresql.PublicNetworkAccessEnumEnabled if v := d.Get("public_network_access_enabled"); !v.(bool) { publicAccess = postgresql.PublicNetworkAccessEnumDisabled @@ -750,6 +805,15 @@ func resourcePostgreSQLServerDelete(d *schema.ResourceData, meta interface{}) er return nil } +func indexOfSku(skuName string) int { + for k, v := range skuList { + if skuName == v { + return k + } + } + return -1 // not found. +} + func expandServerSkuName(skuName string) (*postgresql.Sku, error) { parts := strings.Split(skuName, "_") if len(parts) != 3 { @@ -970,3 +1034,20 @@ func flattenSecurityAlertPolicySet(input *[]string) []interface{} { return utils.FlattenStringSlice(input) } + +func postgreSqlStateRefreshFunc(ctx context.Context, client *postgresql.ServersClient, resourceGroup string, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + res, err := client.Get(ctx, resourceGroup, name) + if !utils.ResponseWasNotFound(res.Response) && err != nil { + return nil, "", fmt.Errorf("retrieving status of PostgreSQL Server %s (Resource Group %q): %+v", name, resourceGroup, err) + } + + // This is an issue with the RP, there is a 10 to 15 second lag before the + // service will actually return the server + if utils.ResponseWasNotFound(res.Response) { + return res, string(postgresql.ServerStateInaccessible), nil + } + + return res, string(res.ServerProperties.UserVisibleState), nil + } +} diff --git a/azurerm/internal/services/postgres/postgresql_server_resource_test.go b/azurerm/internal/services/postgres/postgresql_server_resource_test.go index ac95b9b5eff0..ad89225cc804 100644 --- a/azurerm/internal/services/postgres/postgresql_server_resource_test.go +++ b/azurerm/internal/services/postgres/postgresql_server_resource_test.go @@ -312,7 +312,7 @@ func TestAccPostgreSQLServer_createReplica(t *testing.T) { }, data.ImportStep("administrator_login_password"), { - Config: r.createReplica(data, "11"), + Config: r.createReplica(data, "GP_Gen5_2"), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), @@ -321,6 +321,49 @@ func TestAccPostgreSQLServer_createReplica(t *testing.T) { }) } +func TestAccPostgreSQLServer_scaleReplicas(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_postgresql_server", "test") + r := PostgreSQLServerResource{} + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.createReplicas(data, "GP_Gen5_2"), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("sku_name").HasValue("GP_Gen5_2"), + check.That("azurerm_postgresql_server.replica1").ExistsInAzure(r), + check.That("azurerm_postgresql_server.replica1").Key("sku_name").HasValue("GP_Gen5_2"), + check.That("azurerm_postgresql_server.replica2").ExistsInAzure(r), + check.That("azurerm_postgresql_server.replica2").Key("sku_name").HasValue("GP_Gen5_2"), + ), + }, + data.ImportStep("administrator_login_password"), + { + Config: r.createReplicas(data, "GP_Gen5_4"), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("sku_name").HasValue("GP_Gen5_4"), + check.That("azurerm_postgresql_server.replica1").ExistsInAzure(r), + check.That("azurerm_postgresql_server.replica1").Key("sku_name").HasValue("GP_Gen5_4"), + check.That("azurerm_postgresql_server.replica2").ExistsInAzure(r), + check.That("azurerm_postgresql_server.replica2").Key("sku_name").HasValue("GP_Gen5_4"), + ), + }, + data.ImportStep("administrator_login_password"), + { + Config: r.createReplicas(data, "GP_Gen5_2"), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("sku_name").HasValue("GP_Gen5_2"), + check.That("azurerm_postgresql_server.replica1").ExistsInAzure(r), + check.That("azurerm_postgresql_server.replica1").Key("sku_name").HasValue("GP_Gen5_2"), + check.That("azurerm_postgresql_server.replica2").ExistsInAzure(r), + check.That("azurerm_postgresql_server.replica2").Key("sku_name").HasValue("GP_Gen5_2"), + ), + }, + data.ImportStep("administrator_login_password"), + }) +} + func TestAccPostgreSQLServer_createPointInTimeRestore(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_postgresql_server", "test") r := PostgreSQLServerResource{} @@ -709,7 +752,7 @@ resource "azurerm_postgresql_server" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomInteger, sku, version) } -func (r PostgreSQLServerResource) createReplica(data acceptance.TestData, version string) string { +func (r PostgreSQLServerResource) createReplica(data acceptance.TestData, sku string) string { return fmt.Sprintf(` %[1]s @@ -718,15 +761,49 @@ resource "azurerm_postgresql_server" "replica" { location = azurerm_resource_group.test.location resource_group_name = azurerm_resource_group.test.name - sku_name = "GP_Gen5_2" - version = "%[3]s" + sku_name = "%[3]s" + version = "11" + + create_mode = "Replica" + creation_source_server_id = azurerm_postgresql_server.test.id + + ssl_enforcement_enabled = true +} +`, r.template(data, sku, "11"), data.RandomInteger, sku) +} + +func (r PostgreSQLServerResource) createReplicas(data acceptance.TestData, sku string) string { + return fmt.Sprintf(` +%[1]s + +resource "azurerm_postgresql_server" "replica1" { + name = "acctest-psql-server-%[2]d-replica1" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + sku_name = "%[3]s" + version = "11" + + create_mode = "Replica" + creation_source_server_id = azurerm_postgresql_server.test.id + + ssl_enforcement_enabled = true +} + +resource "azurerm_postgresql_server" "replica2" { + name = "acctest-psql-server-%[2]d-replica2" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + sku_name = "%[3]s" + version = "11" create_mode = "Replica" creation_source_server_id = azurerm_postgresql_server.test.id ssl_enforcement_enabled = true } -`, r.basic(data, version), data.RandomInteger, version) +`, r.template(data, sku, "11"), data.RandomInteger, sku) } func (r PostgreSQLServerResource) createPointInTimeRestore(data acceptance.TestData, version, restoreTime string) string { diff --git a/website/docs/r/postgresql_server.html.markdown b/website/docs/r/postgresql_server.html.markdown index c5461c476bb5..83605ec5ad92 100644 --- a/website/docs/r/postgresql_server.html.markdown +++ b/website/docs/r/postgresql_server.html.markdown @@ -52,6 +52,8 @@ The following arguments are supported: * `sku_name` - (Required) Specifies the SKU Name for this PostgreSQL Server. The name of the SKU, follows the `tier` + `family` + `cores` pattern (e.g. `B_Gen4_1`, `GP_Gen5_8`). For more information see the [product documentation](https://docs.microsoft.com/en-us/rest/api/postgresql/servers/create#sku). +~> **NOTE:** When replication is set up and `sku_name` is changed to a higher tier or more capacity for the primary, all replicas are scaled up to the same tier/capacity. This is an Azure requirement, for more information see the [replica scaling documentation](https://docs.microsoft.com/en-us/azure/postgresql/concepts-read-replicas#scaling) + * `version` - (Required) Specifies the version of PostgreSQL to use. Valid values are `9.5`, `9.6`, `10`, `10.0`, and `11`. Changing this forces a new resource to be created. * `administrator_login` - (Optional) The Administrator Login for the PostgreSQL Server. Required when `create_mode` is `Default`. Changing this forces a new resource to be created.