Skip to content

Commit 824121c

Browse files
committed
feat: implement RDB engine upgrade using MajorUpgradeWorkflow to minimize downtime and preserve endpoints
1 parent 6fc9bba commit 824121c

File tree

5 files changed

+2127
-4
lines changed

5 files changed

+2127
-4
lines changed

docs/resources/rdb_instance.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,37 @@ resource "scaleway_rdb_instance" "main" {
2525
}
2626
```
2727

28+
### Example Engine Upgrade
29+
30+
```terraform
31+
resource "scaleway_rdb_instance" "main" {
32+
name = "test-rdb-upgrade"
33+
node_type = "DB-DEV-S"
34+
engine = "PostgreSQL-14" # Start with older version
35+
is_ha_cluster = false
36+
disable_backup = true
37+
user_name = "my_user"
38+
password = "thiZ_is_v&ry_s3cret"
39+
}
40+
41+
# Upgrade to newer version by changing the engine value
42+
# Available versions can be found in the upgradable_versions attribute
43+
resource "scaleway_rdb_instance" "upgraded" {
44+
name = "test-rdb-upgrade"
45+
node_type = "DB-DEV-S"
46+
engine = "PostgreSQL-15" # Upgrade to newer version
47+
is_ha_cluster = false
48+
disable_backup = true
49+
user_name = "my_user"
50+
password = "thiZ_is_v&ry_s3cret"
51+
}
52+
53+
# Check available upgrade versions
54+
output "upgradable_versions" {
55+
value = scaleway_rdb_instance.main.upgradable_versions
56+
}
57+
```
58+
2859
### Example Block Storage Low Latency
2960

3061
```terraform
@@ -143,7 +174,7 @@ interruption.
143174

144175
- `engine` - (Required) Database Instance's engine version (e.g. `PostgreSQL-11`).
145176

146-
~> **Important** Updates to `engine` will recreate the Database Instance.
177+
~> **Important** Updates to `engine` will perform a blue/green upgrade using a snapshot and endpoint migration. This ensures minimal downtime but any writes between the snapshot and the switch will be lost. Available upgrade versions can be found in the `upgradable_versions` computed attribute.
147178

148179
- `volume_type` - (Optional, default to `lssd`) Type of volume where data are stored (`lssd`, `sbs_5k` or `sbs_15k`).
149180

@@ -244,6 +275,11 @@ are of the form `{region}/{id}`, e.g. `fr-par/11111111-1111-1111-1111-1111111111
244275
- `id` - The ID of the IPv4 address resource.
245276
- `address` - The private IPv4 address.
246277
- `certificate` - Certificate of the Database Instance.
278+
- `upgradable_versions` - List of available engine versions for upgrade.
279+
- `id` - Version ID for upgrade requests.
280+
- `name` - Engine name.
281+
- `version` - Version string.
282+
- `minor_version` - Minor version string.
247283
- `organization_id` - The organization ID the Database Instance is associated with.
248284

249285
## Limitations

internal/services/rdb/instance.go

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,7 @@ func ResourceInstance() *schema.Resource {
5959
Type: schema.TypeString,
6060
Optional: true,
6161
Computed: true,
62-
ForceNew: true,
63-
Description: "Database's engine version id",
62+
Description: "Database's engine version id. Changing this value triggers a blue/green upgrade using MajorUpgradeWorkflow with automatic endpoint migration",
6463
DiffSuppressFunc: dsf.IgnoreCase,
6564
ConflictsWith: []string{
6665
"snapshot_id",
@@ -327,6 +326,35 @@ func ResourceInstance() *schema.Resource {
327326
Optional: true,
328327
Description: "Enable or disable encryption at rest for the database instance",
329328
},
329+
"upgradable_versions": {
330+
Type: schema.TypeList,
331+
Computed: true,
332+
Description: "List of available engine versions for upgrade",
333+
Elem: &schema.Resource{
334+
Schema: map[string]*schema.Schema{
335+
"id": {
336+
Type: schema.TypeString,
337+
Computed: true,
338+
Description: "Version ID for upgrade requests",
339+
},
340+
"name": {
341+
Type: schema.TypeString,
342+
Computed: true,
343+
Description: "Engine name",
344+
},
345+
"version": {
346+
Type: schema.TypeString,
347+
Computed: true,
348+
Description: "Version string",
349+
},
350+
"minor_version": {
351+
Type: schema.TypeString,
352+
Computed: true,
353+
Description: "Minor version string",
354+
},
355+
},
356+
},
357+
},
330358
"private_ip": {
331359
Type: schema.TypeList,
332360
Computed: true,
@@ -671,6 +699,17 @@ func ResourceRdbInstanceRead(ctx context.Context, d *schema.ResourceData, m any)
671699
_ = d.Set("encryption_at_rest", res.Encryption.Enabled)
672700
}
673701

702+
upgradableVersions := make([]map[string]interface{}, len(res.UpgradableVersion))
703+
for i, version := range res.UpgradableVersion {
704+
upgradableVersions[i] = map[string]interface{}{
705+
"id": version.ID,
706+
"name": version.Name,
707+
"version": version.Version,
708+
"minor_version": version.MinorVersion,
709+
}
710+
}
711+
_ = d.Set("upgradable_versions", upgradableVersions)
712+
674713
// set user and password
675714
if user, ok := d.GetOk("user_name"); ok {
676715
_ = d.Set("user_name", user.(string))
@@ -911,18 +950,54 @@ func ResourceRdbInstanceUpdate(ctx context.Context, d *schema.ResourceData, m an
911950
})
912951
}
913952

953+
if d.HasChange("engine") {
954+
oldEngine, newEngine := d.GetChange("engine")
955+
newEngineStr := newEngine.(string)
956+
957+
targetVersionID := ""
958+
var availableVersions []string
959+
for _, version := range rdbInstance.UpgradableVersion {
960+
availableVersions = append(availableVersions, version.Name)
961+
if version.ID == newEngineStr || version.Version == newEngineStr || version.Name == newEngineStr {
962+
targetVersionID = version.ID
963+
break
964+
}
965+
}
966+
967+
if targetVersionID == "" {
968+
return diag.FromErr(fmt.Errorf("engine version %s is not available for upgrade from %s. Available versions: %v",
969+
newEngineStr, oldEngine.(string), availableVersions))
970+
}
971+
972+
upgradeInstanceRequests = append(upgradeInstanceRequests,
973+
rdb.UpgradeInstanceRequest{
974+
Region: region,
975+
InstanceID: ID,
976+
MajorUpgradeWorkflow: &rdb.UpgradeInstanceRequestMajorUpgradeWorkflow{
977+
UpgradableVersionID: targetVersionID,
978+
WithEndpoints: true,
979+
},
980+
})
981+
}
982+
914983
// Carry out the upgrades
915984
for i := range upgradeInstanceRequests {
916985
_, err = waitForRDBInstance(ctx, rdbAPI, region, ID, d.Timeout(schema.TimeoutUpdate))
917986
if err != nil && !httperrors.Is404(err) {
918987
return diag.FromErr(err)
919988
}
920989

921-
_, err = rdbAPI.UpgradeInstance(&upgradeInstanceRequests[i], scw.WithContext(ctx))
990+
upgradedInstance, err := rdbAPI.UpgradeInstance(&upgradeInstanceRequests[i], scw.WithContext(ctx))
922991
if err != nil {
923992
return diag.FromErr(err)
924993
}
925994

995+
if upgradeInstanceRequests[i].MajorUpgradeWorkflow != nil && upgradedInstance.ID != ID {
996+
tflog.Info(ctx, fmt.Sprintf("Engine upgrade created new instance, updating ID from %s to %s", ID, upgradedInstance.ID))
997+
ID = upgradedInstance.ID
998+
d.SetId(regional.NewIDString(region, ID))
999+
}
1000+
9261001
_, err = waitForRDBInstance(ctx, rdbAPI, region, ID, d.Timeout(schema.TimeoutUpdate))
9271002
if err != nil && !httperrors.Is404(err) {
9281003
return diag.FromErr(err)

internal/services/rdb/instance_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,6 +1542,71 @@ func TestAccInstance_EndpointErrorHandling(t *testing.T) {
15421542
})
15431543
}
15441544

1545+
func TestAccInstance_EngineUpgrade(t *testing.T) {
1546+
tt := acctest.NewTestTools(t)
1547+
defer tt.Cleanup()
1548+
1549+
// Get two different PostgreSQL versions for upgrade testing
1550+
oldVersion, newVersion := rdbchecks.GetEngineVersionsForUpgrade(tt, postgreSQLEngineName)
1551+
if oldVersion == newVersion {
1552+
t.Skip("Need at least 2 different PostgreSQL versions for upgrade testing")
1553+
}
1554+
1555+
resource.ParallelTest(t, resource.TestCase{
1556+
PreCheck: func() { acctest.PreCheck(t) },
1557+
ProtoV6ProviderFactories: tt.ProviderFactories,
1558+
CheckDestroy: rdbchecks.IsInstanceDestroyed(tt),
1559+
Steps: []resource.TestStep{
1560+
// Step 1: Create instance with older engine version
1561+
{
1562+
Config: fmt.Sprintf(`
1563+
resource "scaleway_rdb_instance" "main" {
1564+
name = "test-rdb-engine-upgrade"
1565+
node_type = "db-dev-s"
1566+
engine = %q
1567+
is_ha_cluster = false
1568+
disable_backup = true
1569+
user_name = "test_user"
1570+
password = "thiZ_is_v&ry_s3cret"
1571+
tags = ["terraform-test", "engine-upgrade"]
1572+
volume_type = "sbs_5k"
1573+
volume_size_in_gb = 10
1574+
}
1575+
`, oldVersion),
1576+
Check: resource.ComposeTestCheckFunc(
1577+
isInstancePresent(tt, "scaleway_rdb_instance.main"),
1578+
resource.TestCheckResourceAttr("scaleway_rdb_instance.main", "engine", oldVersion),
1579+
resource.TestCheckResourceAttrSet("scaleway_rdb_instance.main", "upgradable_versions.#"),
1580+
),
1581+
},
1582+
// Step 2: Upgrade to newer engine version
1583+
{
1584+
Config: fmt.Sprintf(`
1585+
resource "scaleway_rdb_instance" "main" {
1586+
name = "test-rdb-engine-upgrade"
1587+
node_type = "db-dev-s"
1588+
engine = %q
1589+
is_ha_cluster = false
1590+
disable_backup = true
1591+
user_name = "test_user"
1592+
password = "thiZ_is_v&ry_s3cret"
1593+
tags = ["terraform-test", "engine-upgrade"]
1594+
volume_type = "sbs_5k"
1595+
volume_size_in_gb = 10
1596+
}
1597+
`, newVersion),
1598+
Check: resource.ComposeTestCheckFunc(
1599+
isInstancePresent(tt, "scaleway_rdb_instance.main"),
1600+
resource.TestCheckResourceAttr("scaleway_rdb_instance.main", "engine", newVersion),
1601+
resource.TestCheckResourceAttr("scaleway_rdb_instance.main", "name", "test-rdb-engine-upgrade"),
1602+
// Verify endpoints are preserved
1603+
resource.TestCheckResourceAttrSet("scaleway_rdb_instance.main", "load_balancer.0.ip"),
1604+
),
1605+
},
1606+
},
1607+
})
1608+
}
1609+
15451610
func isInstancePresent(tt *acctest.TestTools, n string) resource.TestCheckFunc {
15461611
return func(s *terraform.State) error {
15471612
rs, ok := s.RootModule().Resources[n]

internal/services/rdb/testdata/instance-engine-upgrade.cassette.yaml

Lines changed: 1920 additions & 0 deletions
Large diffs are not rendered by default.

internal/services/rdb/testfuncs/checks.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,30 @@ func GetLatestEngineVersion(tt *acctest.TestTools, engineName string) string {
7373

7474
return latestEngineVersion
7575
}
76+
77+
func GetEngineVersionsForUpgrade(tt *acctest.TestTools, engineName string) (string, string) {
78+
api := rdbSDK.NewAPI(tt.Meta.ScwClient())
79+
80+
engines, err := api.ListDatabaseEngines(&rdbSDK.ListDatabaseEnginesRequest{})
81+
if err != nil {
82+
tt.T.Fatalf("Could not get engine versions: %s", err)
83+
}
84+
85+
for _, engine := range engines.Engines {
86+
if engine.Name == engineName {
87+
var availableVersions []string
88+
for _, version := range engine.Versions {
89+
if !version.Disabled {
90+
availableVersions = append(availableVersions, version.Name)
91+
}
92+
}
93+
94+
if len(availableVersions) >= 2 {
95+
return availableVersions[1], availableVersions[0]
96+
}
97+
}
98+
}
99+
100+
tt.T.Fatalf("Could not find two different versions for engine %s", engineName)
101+
return "", ""
102+
}

0 commit comments

Comments
 (0)