Skip to content

Commit f89b981

Browse files
jremy42remyleone
andauthored
feat(RDB): implement RDB engine upgrade using MajorUpgradeWorkflow (#3408)
* feat: implement RDB engine upgrade using MajorUpgradeWorkflow to minimize downtime and preserve endpoints * feat(rdb): add comprehensive tests and documentation for engine upgrade feature * refactor(rdb): consolidate engine upgrade tests into single comprehensive test * feat: ensure both old and new RDB instances reach stable states during engine upgrade * fix: resolve golangci-lint issues for RDB engine upgrade implementation * test: compress cassettes and remove orphan test data files * docs: regenerate documentation after rebase on master * refactor: replace interface{} with any and remove unnecessary comments * chore: bump sdk (#3411) * chore: bump sdk * fix cassettes * Compress cassettes * chore: fix documentation and include a make docs (#3412) * chore: fix documentation and include a make docs * Fix linter * feat(rdb): clarify engine expects version name not ID * fix(rdb): gofmt formatting * feat(rdb): accept only version.Name for engine (simplify validation) * docs: regenerate after engine description update --------- Co-authored-by: Rémy Léone <rleone@scaleway.com>
1 parent 2cf89e8 commit f89b981

File tree

6 files changed

+2105
-11
lines changed

6 files changed

+2105
-11
lines changed

docs/resources/rdb_instance.md

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,38 @@ resource "scaleway_rdb_instance" "main" {
7777
}
7878
```
7979

80+
### Example Engine Upgrade
81+
82+
```terraform
83+
# Initial creation with PostgreSQL 14
84+
resource "scaleway_rdb_instance" "main" {
85+
name = "my-database"
86+
node_type = "DB-DEV-S"
87+
engine = "PostgreSQL-14"
88+
is_ha_cluster = false
89+
disable_backup = true
90+
user_name = "my_user"
91+
password = "thiZ_is_v&ry_s3cret"
92+
}
93+
94+
# Check available versions for upgrade
95+
output "upgradable_versions" {
96+
value = scaleway_rdb_instance.main.upgradable_versions
97+
}
98+
99+
# To upgrade to PostgreSQL 15, simply change the engine value
100+
# This will trigger a blue/green upgrade with automatic endpoint migration
101+
# resource "scaleway_rdb_instance" "main" {
102+
# name = "my-database"
103+
# node_type = "DB-DEV-S"
104+
# engine = "PostgreSQL-15" # Changed from PostgreSQL-14
105+
# is_ha_cluster = false
106+
# disable_backup = true
107+
# user_name = "my_user"
108+
# password = "thiZ_is_v&ry_s3cret"
109+
# }
110+
```
111+
80112
### Examples of endpoint configuration
81113

82114
Database Instances can have a maximum of 1 public endpoint and 1 private endpoint. They can have both, or none.
@@ -141,9 +173,9 @@ interruption.
141173

142174
~> **Important** Once your Database Instance reaches `disk_full` status, if you are using `lssd` storage, you should upgrade the `node_type`, and if you are using `bssd` storage, you should increase the volume size before making any other changes to your Database Instance.
143175

144-
- `engine` - (Required) Database Instance's engine version (e.g. `PostgreSQL-11`).
176+
- `engine` - (Required) Database Instance's engine version name (e.g. `PostgreSQL-16`, `MySQL-8`).
145177

146-
~> **Important** Updates to `engine` will recreate the Database Instance.
178+
~> **Important** Updates to `engine` will perform a blue/green upgrade using `MajorUpgradeWorkflow`. This creates a new instance from a snapshot, migrates endpoints automatically, and updates the Terraform state with the new instance ID. The upgrade ensures minimal downtime but **any writes between the snapshot and the endpoint migration will be lost**. Use the `upgradable_versions` computed attribute to check available versions for upgrade.
147179

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

@@ -245,6 +277,11 @@ are of the form `{region}/{id}`, e.g. `fr-par/11111111-1111-1111-1111-1111111111
245277
- `address` - The private IPv4 address.
246278
- `certificate` - Certificate of the Database Instance.
247279
- `organization_id` - The organization ID the Database Instance is associated with.
280+
- `upgradable_versions` - List of available engine versions for upgrade. Each version contains:
281+
- `id` - Version ID to use in upgrade requests.
282+
- `name` - Engine version name (e.g., `PostgreSQL-15`).
283+
- `version` - Version string (e.g., `15.5`).
284+
- `minor_version` - Minor version string (e.g., `15.5.0`).
248285

249286
## Limitations
250287

internal/services/rdb/instance.go

Lines changed: 112 additions & 7 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 name (e.g., 'PostgreSQL-16', 'MySQL-8'). 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,18 @@ 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]any, len(res.UpgradableVersion))
703+
for i, version := range res.UpgradableVersion {
704+
upgradableVersions[i] = map[string]any{
705+
"id": version.ID,
706+
"name": version.Name,
707+
"version": version.Version,
708+
"minor_version": version.MinorVersion,
709+
}
710+
}
711+
712+
_ = d.Set("upgradable_versions", upgradableVersions)
713+
674714
// set user and password
675715
if user, ok := d.GetOk("user_name"); ok {
676716
_ = d.Set("user_name", user.(string))
@@ -911,21 +951,86 @@ func ResourceRdbInstanceUpdate(ctx context.Context, d *schema.ResourceData, m an
911951
})
912952
}
913953

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

921-
_, err = rdbAPI.UpgradeInstance(&upgradeInstanceRequests[i], scw.WithContext(ctx))
992+
upgradedInstance, err := rdbAPI.UpgradeInstance(&upgradeInstanceRequests[i], scw.WithContext(ctx))
922993
if err != nil {
923994
return diag.FromErr(err)
924995
}
925996

926-
_, err = waitForRDBInstance(ctx, rdbAPI, region, ID, d.Timeout(schema.TimeoutUpdate))
927-
if err != nil && !httperrors.Is404(err) {
928-
return diag.FromErr(err)
997+
if upgradeInstanceRequests[i].MajorUpgradeWorkflow != nil && upgradedInstance.ID != ID {
998+
tflog.Info(ctx, fmt.Sprintf("Engine upgrade created new instance, updating ID from %s to %s", ID, upgradedInstance.ID))
999+
oldInstanceID := ID
1000+
ID = upgradedInstance.ID
1001+
d.SetId(regional.NewIDString(region, ID))
1002+
1003+
_, err = waitForRDBInstance(ctx, rdbAPI, region, ID, d.Timeout(schema.TimeoutUpdate))
1004+
if err != nil && !httperrors.Is404(err) {
1005+
return diag.FromErr(err)
1006+
}
1007+
1008+
_, err = waitForRDBInstance(ctx, rdbAPI, region, oldInstanceID, d.Timeout(schema.TimeoutUpdate))
1009+
if err != nil && !httperrors.Is404(err) {
1010+
tflog.Warn(ctx, fmt.Sprintf("Old instance %s not ready for deletion: %v", oldInstanceID, err))
1011+
} else {
1012+
_, err = rdbAPI.DeleteInstance(&rdb.DeleteInstanceRequest{
1013+
Region: region,
1014+
InstanceID: oldInstanceID,
1015+
}, scw.WithContext(ctx))
1016+
if err != nil && !httperrors.Is404(err) {
1017+
tflog.Warn(ctx, fmt.Sprintf("Failed to delete old instance %s: %v", oldInstanceID, err))
1018+
} else {
1019+
_, err = rdbAPI.WaitForInstance(&rdb.WaitForInstanceRequest{
1020+
Region: region,
1021+
InstanceID: oldInstanceID,
1022+
Timeout: scw.TimeDurationPtr(d.Timeout(schema.TimeoutUpdate)),
1023+
}, scw.WithContext(ctx))
1024+
if err != nil && !httperrors.Is404(err) {
1025+
tflog.Warn(ctx, fmt.Sprintf("Error waiting for old instance %s deletion: %v", oldInstanceID, err))
1026+
}
1027+
}
1028+
}
1029+
} else {
1030+
_, err = waitForRDBInstance(ctx, rdbAPI, region, ID, d.Timeout(schema.TimeoutUpdate))
1031+
if err != nil && !httperrors.Is404(err) {
1032+
return diag.FromErr(err)
1033+
}
9291034
}
9301035
}
9311036

0 commit comments

Comments
 (0)