Skip to content

Commit f156dbd

Browse files
authored
fix(rdb): maintain ACL during RDB instance engine upgrade (#3832)
* Maintain ACL during RDB instance engine upgrade (fix #3627) * fix(rdb): document and warn about ACL reconciliation after blue-green engine upgrades. * fix(rdb): format ACL upgrade helpers and compress the engine upgrade ACL cassette.
1 parent 02ec3cc commit f156dbd

6 files changed

Lines changed: 3368 additions & 0 deletions

File tree

docs/resources/rdb_instance.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,8 @@ interruption.
219219

220220
~> **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.
221221

222+
~> **Note** The provider copies instance-level data managed outside `scaleway_rdb_instance`, such as ACL rules, to the upgraded instance during the engine upgrade. However, Terraform plans dependent resources before the blue/green upgrade returns the new instance ID. As a result, resources that reference the previous instance ID, such as `scaleway_rdb_acl`, may require a second `terraform apply` to fully reconcile their Terraform state with the upgraded instance.
223+
222224
- `volume_type` - (Optional, default to `lssd`) Type of volume where data are stored (`lssd`, `sbs_5k` or `sbs_15k`).
223225

224226
- `volume_size_in_gb` - (Optional) Volume size (in GB). Cannot be used when `volume_type` is set to `lssd`.

internal/services/rdb/acl.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,49 @@ func mergeDiffToSchema(rulesFromSchema map[string]struct{}, ruleMap map[string]*
356356
return res
357357
}
358358

359+
func maintainACLDuringUpgrade(ctx context.Context, api *rdb.API, region scw.Region, oldInstanceID, newInstanceID string) error {
360+
res, err := api.ListInstanceACLRules(&rdb.ListInstanceACLRulesRequest{
361+
Region: region,
362+
InstanceID: locality.ExpandID(oldInstanceID),
363+
}, scw.WithAllPages(), scw.WithContext(ctx))
364+
if err != nil {
365+
if httperrors.Is404(err) {
366+
return nil
367+
}
368+
369+
return err
370+
}
371+
372+
if len(res.Rules) == 0 {
373+
return nil
374+
}
375+
376+
rules := rdbACLRulesToRequests(res.Rules)
377+
_, err = api.SetInstanceACLRules(&rdb.SetInstanceACLRulesRequest{
378+
Region: region,
379+
InstanceID: locality.ExpandID(newInstanceID),
380+
Rules: rules,
381+
}, scw.WithContext(ctx))
382+
383+
return err
384+
}
385+
386+
func rdbACLRulesToRequests(rules []*rdb.ACLRule) []*rdb.ACLRuleRequest {
387+
if len(rules) == 0 {
388+
return nil
389+
}
390+
391+
reqs := make([]*rdb.ACLRuleRequest, len(rules))
392+
for i, r := range rules {
393+
reqs[i] = &rdb.ACLRuleRequest{
394+
IP: r.IP,
395+
Description: r.Description,
396+
}
397+
}
398+
399+
return reqs
400+
}
401+
359402
func rdbACLRulesFlatten(rules []*rdb.ACLRule) []map[string]any {
360403
res := make([]map[string]any, 0, len(rules))
361404

internal/services/rdb/instance.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,6 +1097,12 @@ func ResourceRdbInstanceUpdate(ctx context.Context, d *schema.ResourceData, m an
10971097
}
10981098
}
10991099

1100+
if err := maintainACLDuringUpgrade(ctx, rdbAPI, region, oldInstanceID, ID); err != nil {
1101+
tflog.Warn(ctx, fmt.Sprintf("Failed to maintain ACL during upgrade: %v", err))
1102+
} else {
1103+
tflog.Warn(ctx, "ACL rules were copied to the upgraded instance. Because the instance ID changed during the blue/green upgrade, dependent resources such as scaleway_rdb_acl may require a second terraform apply to reconcile their state.")
1104+
}
1105+
11001106
_, err = waitForRDBInstance(ctx, rdbAPI, region, oldInstanceID, d.Timeout(schema.TimeoutUpdate))
11011107
if err != nil && !httperrors.Is404(err) {
11021108
tflog.Warn(ctx, fmt.Sprintf("Old instance %s not ready for deletion: %v", oldInstanceID, err))

internal/services/rdb/instance_test.go

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
1010
"github.com/hashicorp/terraform-plugin-testing/terraform"
1111
rdbSDK "github.com/scaleway/scaleway-sdk-go/api/rdb/v1"
12+
"github.com/scaleway/scaleway-sdk-go/scw"
1213
"github.com/scaleway/terraform-provider-scaleway/v2/internal/acctest"
1314
"github.com/scaleway/terraform-provider-scaleway/v2/internal/httperrors"
1415
"github.com/scaleway/terraform-provider-scaleway/v2/internal/services/rdb"
@@ -1860,6 +1861,167 @@ func TestAccInstance_EngineUpgradeKeepsHA(t *testing.T) {
18601861
})
18611862
}
18621863

1864+
func TestAccInstance_EngineUpgrade_WithACL(t *testing.T) {
1865+
tt := acctest.NewTestTools(t)
1866+
defer tt.Cleanup()
1867+
1868+
oldVersion, newVersion := rdbchecks.GetEngineVersionsForUpgrade(tt, postgreSQLEngineName)
1869+
if oldVersion == newVersion {
1870+
t.Skip("Need at least 2 different PostgreSQL versions for upgrade testing")
1871+
}
1872+
1873+
aclRule1 := "1.2.3.4/32"
1874+
aclRule2 := "9.0.0.0/16"
1875+
1876+
var oldInstanceID string
1877+
1878+
configForVersion := func(version string) string {
1879+
return fmt.Sprintf(`
1880+
resource "scaleway_rdb_instance" "main" {
1881+
name = "test-rdb-engine-upgrade-acl"
1882+
node_type = "db-dev-s"
1883+
engine = %q
1884+
is_ha_cluster = false
1885+
disable_backup = true
1886+
user_name = "test_user"
1887+
password = "thiZ_is_v&ry_s3cret"
1888+
tags = ["terraform-test", "engine-upgrade-acl"]
1889+
volume_type = "sbs_5k"
1890+
volume_size_in_gb = 10
1891+
}
1892+
1893+
resource "scaleway_rdb_acl" "main" {
1894+
instance_id = scaleway_rdb_instance.main.id
1895+
acl_rules {
1896+
ip = %q
1897+
description = "rule1"
1898+
}
1899+
acl_rules {
1900+
ip = %q
1901+
description = "rule2"
1902+
}
1903+
}
1904+
`, version, aclRule1, aclRule2)
1905+
}
1906+
1907+
resource.ParallelTest(t, resource.TestCase{
1908+
ProtoV6ProviderFactories: tt.ProviderFactories,
1909+
CheckDestroy: rdbchecks.IsInstanceDestroyed(tt),
1910+
Steps: []resource.TestStep{
1911+
{
1912+
Config: configForVersion(oldVersion),
1913+
Check: resource.ComposeTestCheckFunc(
1914+
isInstancePresent(tt, "scaleway_rdb_instance.main"),
1915+
resource.TestCheckResourceAttr("scaleway_rdb_instance.main", "engine", oldVersion),
1916+
resource.TestCheckResourceAttr("scaleway_rdb_acl.main", "acl_rules.0.ip", aclRule1),
1917+
resource.TestCheckResourceAttr("scaleway_rdb_acl.main", "acl_rules.1.ip", aclRule2),
1918+
func(s *terraform.State) error {
1919+
rs, ok := s.RootModule().Resources["scaleway_rdb_instance.main"]
1920+
if !ok {
1921+
return errors.New("resource not found: scaleway_rdb_instance.main")
1922+
}
1923+
1924+
_, _, instanceID, err := rdb.NewAPIWithRegionAndID(tt.Meta, rs.Primary.ID)
1925+
if err != nil {
1926+
return err
1927+
}
1928+
1929+
oldInstanceID = instanceID
1930+
1931+
return nil
1932+
},
1933+
),
1934+
},
1935+
{
1936+
Config: configForVersion(newVersion),
1937+
ExpectNonEmptyPlan: true,
1938+
Check: resource.ComposeTestCheckFunc(
1939+
isInstancePresent(tt, "scaleway_rdb_instance.main"),
1940+
resource.TestCheckResourceAttr("scaleway_rdb_instance.main", "engine", newVersion),
1941+
resource.TestCheckResourceAttr("scaleway_rdb_acl.main", "acl_rules.0.ip", aclRule1),
1942+
resource.TestCheckResourceAttr("scaleway_rdb_acl.main", "acl_rules.1.ip", aclRule2),
1943+
resource.TestCheckResourceAttr("scaleway_rdb_acl.main", "acl_rules.0.description", "rule1"),
1944+
resource.TestCheckResourceAttr("scaleway_rdb_acl.main", "acl_rules.1.description", "rule2"),
1945+
checkInstanceACLRules(tt, "scaleway_rdb_instance.main", []string{aclRule1, aclRule2}),
1946+
),
1947+
},
1948+
{
1949+
Config: configForVersion(newVersion),
1950+
Check: resource.ComposeTestCheckFunc(
1951+
isInstancePresent(tt, "scaleway_rdb_instance.main"),
1952+
resource.TestCheckResourceAttr("scaleway_rdb_instance.main", "engine", newVersion),
1953+
resource.TestCheckResourceAttr("scaleway_rdb_acl.main", "acl_rules.0.ip", aclRule1),
1954+
resource.TestCheckResourceAttr("scaleway_rdb_acl.main", "acl_rules.1.ip", aclRule2),
1955+
resource.TestCheckResourceAttr("scaleway_rdb_acl.main", "acl_rules.0.description", "rule1"),
1956+
resource.TestCheckResourceAttr("scaleway_rdb_acl.main", "acl_rules.1.description", "rule2"),
1957+
checkInstanceACLRules(tt, "scaleway_rdb_instance.main", []string{aclRule1, aclRule2}),
1958+
func(s *terraform.State) error {
1959+
instanceRS, ok := s.RootModule().Resources["scaleway_rdb_instance.main"]
1960+
if !ok {
1961+
return errors.New("resource not found: scaleway_rdb_instance.main")
1962+
}
1963+
1964+
aclRS, ok := s.RootModule().Resources["scaleway_rdb_acl.main"]
1965+
if !ok {
1966+
return errors.New("resource not found: scaleway_rdb_acl.main")
1967+
}
1968+
1969+
_, _, newInstanceID, err := rdb.NewAPIWithRegionAndID(tt.Meta, instanceRS.Primary.ID)
1970+
if err != nil {
1971+
return err
1972+
}
1973+
1974+
if newInstanceID == oldInstanceID {
1975+
return fmt.Errorf("expected new instance ID after upgrade, but got same ID: %s", newInstanceID)
1976+
}
1977+
1978+
if aclRS.Primary.ID != instanceRS.Primary.ID {
1979+
return fmt.Errorf("expected ACL resource to track upgraded instance ID %s after second apply, got %s", instanceRS.Primary.ID, aclRS.Primary.ID)
1980+
}
1981+
1982+
return nil
1983+
},
1984+
),
1985+
},
1986+
},
1987+
})
1988+
}
1989+
1990+
func checkInstanceACLRules(tt *acctest.TestTools, instanceResource string, expectedIPs []string) resource.TestCheckFunc {
1991+
return func(s *terraform.State) error {
1992+
rs, ok := s.RootModule().Resources[instanceResource]
1993+
if !ok {
1994+
return fmt.Errorf("resource not found: %s", instanceResource)
1995+
}
1996+
1997+
rdbAPI, region, instanceID, err := rdb.NewAPIWithRegionAndID(tt.Meta, rs.Primary.ID)
1998+
if err != nil {
1999+
return err
2000+
}
2001+
2002+
res, err := rdbAPI.ListInstanceACLRules(&rdbSDK.ListInstanceACLRulesRequest{
2003+
Region: region,
2004+
InstanceID: instanceID,
2005+
}, scw.WithAllPages())
2006+
if err != nil {
2007+
return fmt.Errorf("listing ACL rules: %w", err)
2008+
}
2009+
2010+
actualIPs := make(map[string]bool)
2011+
for _, r := range res.Rules {
2012+
actualIPs[r.IP.String()] = true
2013+
}
2014+
2015+
for _, expected := range expectedIPs {
2016+
if !actualIPs[expected] {
2017+
return fmt.Errorf("expected ACL rule %q not found on instance, got: %v", expected, actualIPs)
2018+
}
2019+
}
2020+
2021+
return nil
2022+
}
2023+
}
2024+
18632025
func isInstancePresent(tt *acctest.TestTools, n string) resource.TestCheckFunc {
18642026
return func(s *terraform.State) error {
18652027
rs, ok := s.RootModule().Resources[n]

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

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

templates/resources/rdb_instance.md.tmpl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ interruption.
4040

4141
~> **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.
4242

43+
~> **Note** The provider copies instance-level data managed outside `scaleway_rdb_instance`, such as ACL rules, to the upgraded instance during the engine upgrade. However, Terraform plans dependent resources before the blue/green upgrade returns the new instance ID. As a result, resources that reference the previous instance ID, such as `scaleway_rdb_acl`, may require a second `terraform apply` to fully reconcile their Terraform state with the upgraded instance.
44+
4345
- `volume_type` - (Optional, default to `lssd`) Type of volume where data are stored (`lssd`, `sbs_5k` or `sbs_15k`).
4446

4547
- `volume_size_in_gb` - (Optional) Volume size (in GB). Cannot be used when `volume_type` is set to `lssd`.

0 commit comments

Comments
 (0)