diff --git a/.changelog/6131.txt b/.changelog/6131.txt new file mode 100644 index 0000000000..a3e245e1cf --- /dev/null +++ b/.changelog/6131.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +sql: added `maintenance_version` and `available_maintenance_versions` fields to `google_sql_database_instance` resource +``` diff --git a/google-beta/resource_sql_database_instance.go b/google-beta/resource_sql_database_instance.go index bd807bbad2..2bc93601db 100644 --- a/google-beta/resource_sql_database_instance.go +++ b/google-beta/resource_sql_database_instance.go @@ -515,7 +515,21 @@ is set to true. Defaults to ZONAL.`, Computed: true, Description: `The connection name of the instance to be used in connection strings. For example, when connecting with Cloud SQL Proxy.`, }, - + "maintenance_version": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: `Maintenance version.`, + DiffSuppressFunc: maintenanceVersionDiffSuppress, + }, + "available_maintenance_versions": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: `Available Maintenance versions.`, + }, "database_version": { Type: schema.TypeString, Required: true, @@ -536,7 +550,6 @@ is set to true. Defaults to ZONAL.`, Sensitive: true, Description: `Initial root password. Required for MS SQL Server.`, }, - "ip_address": { Type: schema.TypeList, Computed: true, @@ -871,6 +884,10 @@ func resourceSqlDatabaseInstanceCreate(d *schema.ResourceData, meta interface{}) instance.Settings = desiredSettings } + if _, ok := d.GetOk("maintenance_version"); ok { + instance.MaintenanceVersion = d.Get("maintenance_version").(string) + } + instance.RootPassword = d.Get("root_password").(string) // Modifying a replica during Create can cause problems if the master is @@ -1280,6 +1297,12 @@ func resourceSqlDatabaseInstanceRead(d *schema.ResourceData, meta interface{}) e if err := d.Set("connection_name", instance.ConnectionName); err != nil { return fmt.Errorf("Error setting connection_name: %s", err) } + if err := d.Set("maintenance_version", instance.MaintenanceVersion); err != nil { + return fmt.Errorf("Error setting maintenance_version: %s", err) + } + if err := d.Set("available_maintenance_versions", instance.AvailableMaintenanceVersions); err != nil { + return fmt.Errorf("Error setting available_maintenance_version: %s", err) + } if err := d.Set("service_account_email_address", instance.ServiceAccountEmailAddress); err != nil { return fmt.Errorf("Error setting service_account_email_address: %s", err) } @@ -1356,10 +1379,15 @@ func resourceSqlDatabaseInstanceUpdate(d *schema.ResourceData, meta interface{}) if err != nil { return err } + var maintenance_version string + if v, ok := d.GetOk("maintenance_version"); ok { + maintenance_version = v.(string) + } desiredSetting := d.Get("settings") var op *sqladmin.Operation var instance *sqladmin.DatabaseInstance + // Check if the database version is being updated, because patching database version is an atomic operation and can not be // performed with other fields, we first patch database version before updating the rest of the fields. if v, ok := d.GetOk("database_version"); ok { @@ -1381,6 +1409,27 @@ func resourceSqlDatabaseInstanceUpdate(d *schema.ResourceData, meta interface{}) } } + // Check if the maintenance version is being updated, because patching maintenance version is an atomic operation and can not be + // performed with other fields, we first patch maintenance version before updating the rest of the fields. + if d.HasChange("maintenance_version") { + instance = &sqladmin.DatabaseInstance{MaintenanceVersion: maintenance_version} + err = retryTimeDuration(func() (rerr error) { + op, rerr = config.NewSqlAdminClient(userAgent).Instances.Patch(project, d.Get("name").(string), instance).Do() + return rerr + }, d.Timeout(schema.TimeoutUpdate), isSqlOperationInProgressError) + if err != nil { + return fmt.Errorf("Error, failed to patch instance settings for %s: %s", instance.Name, err) + } + err = sqlAdminOperationWaitTime(config, op, project, "Patch Instance", userAgent, d.Timeout(schema.TimeoutUpdate)) + if err != nil { + return err + } + err = resourceSqlDatabaseInstanceRead(d, meta) + if err != nil { + return err + } + } + s := d.Get("settings") instance = &sqladmin.DatabaseInstance{ Settings: expandSqlDatabaseInstanceSettings(desiredSetting.([]interface{})), @@ -1426,6 +1475,16 @@ func resourceSqlDatabaseInstanceUpdate(d *schema.ResourceData, meta interface{}) return resourceSqlDatabaseInstanceRead(d, meta) } +func maintenanceVersionDiffSuppress(_, old, new string, _ *schema.ResourceData) bool { + // Ignore the database version part and only compare the last part of the maintenance version which represents the release date of the version. + if len(old) > 14 && len(new) > 14 && old[len(old)-14:] >= new[len(new)-14:] { + log.Printf("[DEBUG] Maintenance version in configuration [%s] is older than current maintenance version [%s] on instance. Suppressing diff", new, old) + return true + } else { + return false + } +} + func resourceSqlDatabaseInstanceDelete(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) userAgent, err := generateUserAgentString(d, config.userAgent) diff --git a/google-beta/resource_sql_database_instance_test.go b/google-beta/resource_sql_database_instance_test.go index 205cf45f4d..a6486ddc1a 100644 --- a/google-beta/resource_sql_database_instance_test.go +++ b/google-beta/resource_sql_database_instance_test.go @@ -37,6 +37,39 @@ func init() { }) } +func TestMaintenanceVersionDiffSuppress(t *testing.T) { + cases := map[string]struct { + Old, New string + ShouldSuppress bool + }{ + "older configuration maintenance version than current version should suppress diff": { + Old: "MYSQL_8_0_26.R20220508.01_09", + New: "MYSQL_5_7_37.R20210508.01_03", + ShouldSuppress: true, + }, + "older configuration maintenance version than current version should suppress diff with lexicographically smaller database version": { + Old: "MYSQL_5_8_10.R20220508.01_09", + New: "MYSQL_5_8_7.R20210508.01_03", + ShouldSuppress: true, + }, + "newer configuration maintenance version than current version should not suppress diff": { + Old: "MYSQL_5_7_37.R20210508.01_03", + New: "MYSQL_8_0_26.R20220508.01_09", + ShouldSuppress: false, + }, + } + + for tn, tc := range cases { + tc := tc + t.Run(tn, func(t *testing.T) { + t.Parallel() + if maintenanceVersionDiffSuppress("version", tc.Old, tc.New, nil) != tc.ShouldSuppress { + t.Fatalf("%q => %q expect DiffSuppress to return %t", tc.Old, tc.New, tc.ShouldSuppress) + } + }) + } +} + func testSweepDatabases(region string) error { config, err := sharedConfigForRegion(region) if err != nil { @@ -367,6 +400,46 @@ func TestAccSqlDatabaseInstance_settings_deletionProtection(t *testing.T) { }) } +func TestAccSqlDatabaseInstance_maintenanceVersion(t *testing.T) { + t.Parallel() + + databaseName := "tf-test-" + randString(t, 10) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccSqlDatabaseInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf( + testGoogleSqlDatabaseInstance_maintenanceVersionWithOldVersion, databaseName), + ExpectError: regexp.MustCompile( + `.*Maintenance version \(MYSQL_5_7_37.R20210508.01_03\) must not be set.*`), + }, + { + Config: fmt.Sprintf( + testGoogleSqlDatabaseInstance_basic3, databaseName), + }, + { + ResourceName: "google_sql_database_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: fmt.Sprintf( + testGoogleSqlDatabaseInstance_maintenanceVersionWithOldVersion, databaseName), + }, + { + ResourceName: "google_sql_database_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + }, + }) +} + func TestAccSqlDatabaseInstance_settings_checkServiceNetworking(t *testing.T) { t.Parallel() @@ -1779,6 +1852,18 @@ resource "google_sql_database_instance" "instance" { } } ` +var testGoogleSqlDatabaseInstance_maintenanceVersionWithOldVersion = ` +resource "google_sql_database_instance" "instance" { + name = "%s" + region = "us-central1" + database_version = "MYSQL_5_7" + deletion_protection = false + maintenance_version = "MYSQL_5_7_37.R20210508.01_03" + settings { + tier = "db-f1-micro" + } +} +` var testGoogleSqlDatabaseInstance_settings_checkServiceNetworking = ` resource "google_compute_network" "servicenet" { diff --git a/website/docs/r/sql_database_instance.html.markdown b/website/docs/r/sql_database_instance.html.markdown index 1191e1c640..91ff9e2de5 100644 --- a/website/docs/r/sql_database_instance.html.markdown +++ b/website/docs/r/sql_database_instance.html.markdown @@ -184,6 +184,8 @@ includes an up-to-date reference of supported versions. created. This is done because after a name is used, it cannot be reused for up to [one week](https://cloud.google.com/sql/docs/delete-instance). +* `maintenance_version` - (Optional) The current software version on the instance. This attribute can not be set during creation. Refer to `available_maintenance_versions` attribute to see what `maintenance_version` are available for upgrade. When this attribute gets updated, it will cause an instance restart. Setting a `maintenance_version` value that is older than the current one on the instance will be ignored. + * `master_instance_name` - (Optional) The name of the existing instance that will act as the master in the replication setup. Note, this requires the master to have `binary_log_enabled` set, as well as existing backups. @@ -449,6 +451,8 @@ instance. support accessing the [first address in the list in a terraform output](https://github.com/hashicorp/terraform-provider-google/issues/912) when the resource is configured with a `count`. +* `available_maintenance_versions` - The list of all maintenance versions applicable on the instance. + * `public_ip_address` - The first public (`PRIMARY`) IPv4 address assigned. This is a workaround for an [issue fixed in Terraform 0.12](https://github.com/hashicorp/terraform/issues/17048) but also provides a convenient way to access an IP of a specific type without