From ef335c39def8ee37c49a29de154fc66807303084 Mon Sep 17 00:00:00 2001 From: drfaust92 Date: Sat, 18 Jul 2026 22:23:27 -0400 Subject: [PATCH] sql: support in-place storage shrink for google_sql_database_instance Reducing `disk_size` (with `disk_autoresize` disabled) previously forced the instance to be recreated. Use the dedicated Instances.PerformDiskShrink operation to shrink storage in place instead, with a pre-flight GetDiskShrinkConfig check that returns a clear error when the requested size is below the instance's minimum shrinkable size. --- .../resource_sql_database_instance.go.tmpl | 74 +++++++++++++---- ...esource_sql_database_instance_test.go.tmpl | 83 ++++++++++++++++++- 2 files changed, 138 insertions(+), 19 deletions(-) diff --git a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance.go.tmpl b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance.go.tmpl index a105bb48b9c0..aba90f7f55da 100644 --- a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance.go.tmpl +++ b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance.go.tmpl @@ -205,12 +205,9 @@ func diskSizeCutomizeDiff(ctx context.Context, d *schema.ResourceDiff, meta inte return nil } - // If we are here, we are trying to shrink the disk with auto resize disabled and no ignore changes on disk size. - // This will force a new resource. - if err := d.ForceNew(key); err != nil { - return err - } - + // If we are here, we are shrinking the disk with auto resize disabled. Keep the diff so + // the update performs an in-place storage shrink (Instances.PerformDiskShrink) rather than + // recreating the instance. return nil } @@ -576,7 +573,7 @@ API (for read pools, effective_availability_type may differ from availability_ty Type: schema.TypeInt, Optional: true, Computed: true, - Description: `The size of data disk, in GB. Size of a running instance cannot be reduced but can be increased. The minimum value is 10GB for PD_SSD, PD_HDD and 20GB for HYPERDISK_BALANCED.`, + Description: `The size of data disk, in GB. The size of a running instance can be increased, or reduced when disk_autoresize is disabled (this triggers an in-place storage shrink, which restarts the instance). The minimum value is 10GB for PD_SSD, PD_HDD and 20GB for HYPERDISK_BALANCED.`, }, "disk_type": { Type: schema.TypeString, @@ -2973,17 +2970,17 @@ func resourceSqlDatabaseInstanceUpdate(d *schema.ResourceData, meta interface{}) Settings: expandSqlDatabaseInstanceSettings(desiredSetting.([]interface{}), databaseVersion), } + // A disk_size decrease is a storage shrink, which is carried out by the dedicated + // Instances.PerformDiskShrink operation below rather than by the settings update. For a + // shrink, keep the current (larger) size on the update call so disk size is unchanged + // there, and record that a shrink must be performed. Increases are already applied via + // the expanded settings (DataDiskSizeGb) on the update call. + diskShrink := false if d.HasChange("settings.0.disk_size") { - autoResize := true - _, autoResizeI := d.GetChange("settings.0.disk_autoresize") - if autoResizeI != nil { - autoResize = autoResizeI.(bool) - } oldDiskSizeI, newDiskSizeI := d.GetChange("settings.0.disk_size") - if !autoResize || newDiskSizeI.(int) > oldDiskSizeI.(int) { - // If auto resize is not enabled - set the disk size as requested, even if it's a decrease - let it fail. - // Otherwise, allow increasing even if auto resize is enabled. - instance.Settings.DataDiskSizeGb = int64(newDiskSizeI.(int)) + if newDiskSizeI.(int) < oldDiskSizeI.(int) { + diskShrink = true + instance.Settings.DataDiskSizeGb = int64(oldDiskSizeI.(int)) } } @@ -3065,6 +3062,51 @@ func resourceSqlDatabaseInstanceUpdate(d *schema.ResourceData, meta interface{}) return err } + // Perform an in-place storage shrink if disk_size was reduced. This is a dedicated, + // disruptive operation (the instance restarts) and is run after the settings update. + if diskShrink { + name := d.Get("name").(string) + targetSizeGb := int64(d.Get("settings.0.disk_size").(int)) + + // Pre-flight: the smallest size an instance can shrink to depends on its current + // storage usage. Fetch that minimum and surface a clear error up front instead of + // letting the shrink operation start and then fail. + var shrinkConfig *sqladmin.SqlInstancesGetDiskShrinkConfigResponse + err = transport_tpg.Retry(transport_tpg.RetryOptions{ + RetryFunc: func() (rerr error) { + shrinkConfig, rerr = NewClient(config, userAgent).Projects.Instances.GetDiskShrinkConfig(project, name).Do() + return rerr + }, + Timeout: d.Timeout(schema.TimeoutUpdate), + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.IsSqlOperationInProgressError}, + }) + if err != nil { + return fmt.Errorf("Error, failed to get storage shrink config for %s: %s", name, err) + } + if targetSizeGb < shrinkConfig.MinimalTargetSizeGb { + return fmt.Errorf("Error, cannot shrink storage of %s to %d GB: the minimum size this instance can currently shrink to is %d GB", name, targetSizeGb, shrinkConfig.MinimalTargetSizeGb) + } + + shrinkContext := &sqladmin.PerformDiskShrinkContext{ + TargetSizeGb: targetSizeGb, + } + err = transport_tpg.Retry(transport_tpg.RetryOptions{ + RetryFunc: func() (rerr error) { + op, rerr = NewClient(config, userAgent).Projects.Instances.PerformDiskShrink(project, name, shrinkContext).Do() + return rerr + }, + Timeout: d.Timeout(schema.TimeoutUpdate), + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.IsSqlOperationInProgressError}, + }) + if err != nil { + return fmt.Errorf("Error, failed to perform storage shrink for %s: %s", name, err) + } + err = SqlAdminOperationWaitTime(config, op, project, "Perform Disk Shrink", userAgent, d.Timeout(schema.TimeoutUpdate)) + if err != nil { + return err + } + } + // Perform a backup restore if the backup context exists and has changed if r, ok := d.GetOk("restore_backup_context"); ok { if d.HasChange("restore_backup_context") { diff --git a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_test.go.tmpl b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_test.go.tmpl index b449b573bcd2..54bb061cb2b9 100644 --- a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_test.go.tmpl +++ b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_test.go.tmpl @@ -4860,9 +4860,12 @@ func TestAccSqlDatabaseInstance_DiskSizeAutoResizeWithDiskSize(t *testing.T) { Check: testGoogleSqlDatabaseInstanceCheckDiskSize(t, databaseName, 15), }, { - // Disk size is now 15gb - requested 14gb, but disabled auto resize - should error because it can't be deleted for replacement. - Config: testGoogleSqlDatabaseInstance_diskSizeAutoResize(project, databaseName, 14, 104, &falseVar, false, false), - ExpectError: regexp.MustCompile("Instance cannot be destroyed"), + // Disk size is now 15gb - requested 14gb with auto resize disabled. A shrink is now an + // in-place storage shrink (an update), not a destroy/recreate, so prevent_destroy no + // longer blocks the plan and the plan is a non-empty (update) diff. + Config: testGoogleSqlDatabaseInstance_diskSizeAutoResize(project, databaseName, 14, 104, &falseVar, false, false), + PlanOnly: true, + ExpectNonEmptyPlan: true, }, { // Disk size is now 15gb - requested 14gb, but ignore changes is set - so should ignore the configuration change. @@ -4877,6 +4880,39 @@ func TestAccSqlDatabaseInstance_DiskSizeAutoResizeWithDiskSize(t *testing.T) { }) } +func TestAccSqlDatabaseInstance_storageShrink(t *testing.T) { + t.Parallel() + + databaseName := "tf-test-" + acctest.RandString(t, 10) + var instanceID string + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccSqlDatabaseInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + // Create a non-shared-core instance with a 100GB disk and auto resize disabled + // (storage shrink is not supported on shared-core machine types). The starting + // size is well above the instance's minimum shrinkable size. + Config: testGoogleSqlDatabaseInstance_storageShrink(databaseName, 100), + Check: resource.ComposeTestCheckFunc( + testGoogleSqlDatabaseInstanceCheckDiskSize(t, databaseName, 100), + testAccSqlDatabaseInstanceCaptureID(&instanceID), + ), + }, + { + // Reduce disk_size to 90GB. This must be an in-place storage shrink (no recreate). + Config: testGoogleSqlDatabaseInstance_storageShrink(databaseName, 90), + Check: resource.ComposeTestCheckFunc( + testGoogleSqlDatabaseInstanceCheckDiskSize(t, databaseName, 90), + testAccSqlDatabaseInstanceCheckSameID(&instanceID), + ), + }, + }, + }) +} + func TestEnhancedBackupManagerDiffSuppressFunc(t *testing.T) { cases := map[string]struct { key string @@ -10343,6 +10379,47 @@ func testGoogleSqlDatabaseInstanceResizeDisk(t *testing.T, instance string, addG } } +func testGoogleSqlDatabaseInstance_storageShrink(dbName string, diskSize int) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "instance" { + name = "%s" + region = "us-central1" + database_version = "POSTGRES_15" + deletion_protection = false + settings { + tier = "db-custom-2-8192" + disk_type = "PD_SSD" + disk_size = %d + disk_autoresize = false + } +} +`, dbName, diskSize) +} + +func testAccSqlDatabaseInstanceCaptureID(id *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources["google_sql_database_instance.instance"] + if !ok { + return fmt.Errorf("resource google_sql_database_instance.instance not found in state") + } + *id = rs.Primary.ID + return nil + } +} + +func testAccSqlDatabaseInstanceCheckSameID(id *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources["google_sql_database_instance.instance"] + if !ok { + return fmt.Errorf("resource google_sql_database_instance.instance not found in state") + } + if rs.Primary.ID != *id { + return fmt.Errorf("instance was recreated during storage shrink: id changed from %q to %q", *id, rs.Primary.ID) + } + return nil + } +} + func testGoogleSqlDatabaseInstanceCheckDiskSize(t *testing.T, instance string, size int64) resource.TestCheckFunc { return func(s *terraform.State) error { config := acctest.GoogleProviderConfig(t)