diff --git a/.changelog/28590.txt b/.changelog/28590.txt new file mode 100644 index 000000000000..d445ceabaccf --- /dev/null +++ b/.changelog/28590.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_lightsail_database: The `availability_zone` attribute is now optional/computed to support HA `bundle_id`s +``` \ No newline at end of file diff --git a/internal/service/lightsail/database.go b/internal/service/lightsail/database.go index 03d4feb83f37..5e7df959da6f 100644 --- a/internal/service/lightsail/database.go +++ b/internal/service/lightsail/database.go @@ -1,7 +1,7 @@ package lightsail import ( - "fmt" + "context" "log" "regexp" "time" @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/lightsail" "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" @@ -22,12 +23,13 @@ const ( func ResourceDatabase() *schema.Resource { return &schema.Resource{ - Create: resourceDatabaseCreate, - Read: resourceDatabaseRead, - Update: resourceDatabaseUpdate, - Delete: resourceDatabaseDelete, + CreateWithoutTimeout: resourceDatabaseCreate, + ReadWithoutTimeout: resourceDatabaseRead, + UpdateWithoutTimeout: resourceDatabaseUpdate, + DeleteWithoutTimeout: resourceDatabaseDelete, + Importer: &schema.ResourceImporter{ - State: ResourceDatabaseImport, + StateContext: resourceDatabaseImport, }, Schema: map[string]*schema.Schema{ @@ -42,7 +44,8 @@ func ResourceDatabase() *schema.Resource { }, "availability_zone": { Type: schema.TypeString, - Required: true, + Optional: true, + Computed: true, ForceNew: true, }, "backup_retention_enabled": { @@ -179,109 +182,96 @@ func ResourceDatabase() *schema.Resource { } } -func resourceDatabaseCreate(d *schema.ResourceData, meta interface{}) error { +func resourceDatabaseCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).LightsailConn() defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) - req := lightsail.CreateRelationalDatabaseInput{ + relationalDatabaseName := d.Get("relational_database_name").(string) + input := &lightsail.CreateRelationalDatabaseInput{ MasterDatabaseName: aws.String(d.Get("master_database_name").(string)), MasterUsername: aws.String(d.Get("master_username").(string)), RelationalDatabaseBlueprintId: aws.String(d.Get("blueprint_id").(string)), RelationalDatabaseBundleId: aws.String(d.Get("bundle_id").(string)), - RelationalDatabaseName: aws.String(d.Get("relational_database_name").(string)), + RelationalDatabaseName: aws.String(relationalDatabaseName), } if v, ok := d.GetOk("availability_zone"); ok { - req.AvailabilityZone = aws.String(v.(string)) + input.AvailabilityZone = aws.String(v.(string)) } if v, ok := d.GetOk("master_password"); ok { - req.MasterUserPassword = aws.String(v.(string)) + input.MasterUserPassword = aws.String(v.(string)) } if v, ok := d.GetOk("preferred_backup_window"); ok { - req.PreferredBackupWindow = aws.String(v.(string)) + input.PreferredBackupWindow = aws.String(v.(string)) } if v, ok := d.GetOk("preferred_maintenance_window"); ok { - req.PreferredMaintenanceWindow = aws.String(v.(string)) + input.PreferredMaintenanceWindow = aws.String(v.(string)) } if v, ok := d.GetOk("publicly_accessible"); ok { - req.PubliclyAccessible = aws.Bool(v.(bool)) + input.PubliclyAccessible = aws.Bool(v.(bool)) } if len(tags) > 0 { - req.Tags = Tags(tags.IgnoreAWS()) + input.Tags = Tags(tags.IgnoreAWS()) } - resp, err := conn.CreateRelationalDatabase(&req) - if err != nil { - return err - } + output, err := conn.CreateRelationalDatabaseWithContext(ctx, input) - if len(resp.Operations) == 0 { - return fmt.Errorf("No operations found for Create Relational Database request") + if err != nil { + return diag.Errorf("creating Lightsail Relational Database (%s): %s", relationalDatabaseName, err) } - op := resp.Operations[0] - d.SetId(d.Get("relational_database_name").(string)) + d.SetId(relationalDatabaseName) - err = waitOperation(conn, op.Id) - if err != nil { - return fmt.Errorf("Error waiting for Relational Database (%s) to become ready: %s", d.Id(), err) + if err := waitOperationWithContext(ctx, conn, output.Operations[0].Id); err != nil { + return diag.Errorf("waiting for Lightsail Relational Database (%s) create: %s", d.Id(), err) } // Backup Retention is not a value you can pass on creation and defaults to true. // Forcing an update of the value after creation if the backup_retention_enabled value is false. - if v := d.Get("backup_retention_enabled"); v == false { - log.Printf("[DEBUG] Lightsail Database (%s) backup_retention_enabled setting is false. Updating value.", d.Id()) - req := lightsail.UpdateRelationalDatabaseInput{ + if !d.Get("backup_retention_enabled").(bool) { + input := &lightsail.UpdateRelationalDatabaseInput{ ApplyImmediately: aws.Bool(true), - RelationalDatabaseName: aws.String(d.Id()), DisableBackupRetention: aws.Bool(true), + RelationalDatabaseName: aws.String(d.Id()), } - resp, err := conn.UpdateRelationalDatabase(&req) + output, err := conn.UpdateRelationalDatabaseWithContext(ctx, input) + if err != nil { - return err + return diag.Errorf("updating Lightsail Relational Database (%s) backup retention: %s", d.Id(), err) } - if len(resp.Operations) == 0 { - return fmt.Errorf("No operations found for Update Relational Database request") + if err := waitOperationWithContext(ctx, conn, output.Operations[0].Id); err != nil { + return diag.Errorf("waiting for Lightsail Relational Database (%s) update: %s", d.Id(), err) } - op := resp.Operations[0] - - err = waitOperation(conn, op.Id) - if err != nil { - return fmt.Errorf("Error waiting for Relational Database (%s) to become ready: %s", d.Id(), err) - } - - err = waitDatabaseBackupRetentionModified(conn, aws.String(d.Id()), aws.Bool(v.(bool))) - if err != nil { - return fmt.Errorf("Error waiting for Relational Database (%s) Backup Retention to be updated: %s", d.Id(), err) + if err := waitDatabaseBackupRetentionModified(ctx, conn, aws.String(d.Id()), false); err != nil { + return diag.Errorf("waiting for Lightsail Relational Database (%s) backup retention update: %s", d.Id(), err) } } // Some Operations can complete before the Database enters the Available state. Added a waiter to make sure the Database is available before continuing. - _, err = waitDatabaseModified(conn, aws.String(d.Id())) - if err != nil { - return fmt.Errorf("Error waiting for Relational Database (%s) to become available: %s", d.Id(), err) + if _, err = waitDatabaseModified(ctx, conn, aws.String(d.Id())); err != nil { + return diag.Errorf("waiting for Lightsail Relational Database (%s) to become available: %s", d.Id(), err) } - return resourceDatabaseRead(d, meta) + return resourceDatabaseRead(ctx, d, meta) } -func resourceDatabaseRead(d *schema.ResourceData, meta interface{}) error { +func resourceDatabaseRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).LightsailConn() defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig // Some Operations can complete before the Database enters the Available state. Added a waiter to make sure the Database is available before continuing. // This is to support importing a resource that is not in a ready state. - database, err := waitDatabaseModified(conn, aws.String(d.Id())) + database, err := waitDatabaseModified(ctx, conn, aws.String(d.Id())) if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, lightsail.ErrCodeNotFoundException) { log.Printf("[WARN] Lightsail Relational Database (%s) not found, removing from state", d.Id()) @@ -290,7 +280,7 @@ func resourceDatabaseRead(d *schema.ResourceData, meta interface{}) error { } if err != nil { - return fmt.Errorf("error reading LightSail Relational Database (%s): %w", d.Id(), err) + return diag.Errorf("reading Lightsail Relational Database (%s): %s", d.Id(), err) } rd := database.RelationalDatabase @@ -322,153 +312,132 @@ func resourceDatabaseRead(d *schema.ResourceData, meta interface{}) error { //lintignore:AWSR002 if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %w", err) + return diag.Errorf("setting tags: %s", err) } if err := d.Set("tags_all", tags.Map()); err != nil { - return fmt.Errorf("error setting tags_all: %w", err) + return diag.Errorf("setting tags_all: %s", err) } return nil } -func resourceDatabaseDelete(d *schema.ResourceData, meta interface{}) error { +func resourceDatabaseUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).LightsailConn() - // Some Operations can complete before the Database enters the Available state. Added a waiter to make sure the Database is available before continuing. - _, err := waitDatabaseModified(conn, aws.String(d.Id())) - if err != nil { - return fmt.Errorf("Error waiting for Relational Database (%s) to become available: %s", d.Id(), err) - } - - skipFinalSnapshot := d.Get("skip_final_snapshot").(bool) + if d.HasChangesExcept("apply_immediately", "final_snapshot_name", "skip_final_snapshot", "tags", "tags_all") { + input := &lightsail.UpdateRelationalDatabaseInput{ + ApplyImmediately: aws.Bool(d.Get("apply_immediately").(bool)), + RelationalDatabaseName: aws.String(d.Id()), + } - req := lightsail.DeleteRelationalDatabaseInput{ - RelationalDatabaseName: aws.String(d.Id()), - SkipFinalSnapshot: aws.Bool(skipFinalSnapshot), - } + if d.HasChange("backup_retention_enabled") { + if d.Get("backup_retention_enabled").(bool) { + input.EnableBackupRetention = aws.Bool(d.Get("backup_retention_enabled").(bool)) + } else { + input.DisableBackupRetention = aws.Bool(true) + } + } - if !skipFinalSnapshot { - if name, present := d.GetOk("final_snapshot_name"); present { - req.FinalRelationalDatabaseSnapshotName = aws.String(name.(string)) - } else { - return fmt.Errorf("Lightsail Database FinalRelationalDatabaseSnapshotName is required when a final snapshot is required") + if d.HasChange("ca_certificate_identifier") { + input.CaCertificateIdentifier = aws.String(d.Get("ca_certificate_identifier").(string)) } - } - resp, err := conn.DeleteRelationalDatabase(&req) + if d.HasChange("master_password") { + input.MasterUserPassword = aws.String(d.Get("master_password").(string)) + } - if err != nil { - return err - } + if d.HasChange("preferred_backup_window") { + input.PreferredBackupWindow = aws.String(d.Get("preferred_backup_window").(string)) + } - op := resp.Operations[0] + if d.HasChange("preferred_maintenance_window") { + input.PreferredMaintenanceWindow = aws.String(d.Get("preferred_maintenance_window").(string)) + } - err = waitOperation(conn, op.Id) - if err != nil { - return fmt.Errorf("Error waiting for Relational Database (%s) to Delete: %s", d.Id(), err) - } + if d.HasChange("publicly_accessible") { + input.PubliclyAccessible = aws.Bool(d.Get("publicly_accessible").(bool)) + } - return nil -} + output, err := conn.UpdateRelationalDatabaseWithContext(ctx, input) -func ResourceDatabaseImport( - d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - // Neither skip_final_snapshot nor final_snapshot_identifier can be fetched - // from any API call, so we need to default skip_final_snapshot to true so - // that final_snapshot_identifier is not required - d.Set("skip_final_snapshot", true) - return []*schema.ResourceData{d}, nil -} + if err != nil { + return diag.Errorf("updating Lightsail Relational Database (%s): %s", d.Id(), err) + } -func resourceDatabaseUpdate(d *schema.ResourceData, meta interface{}) error { - conn := meta.(*conns.AWSClient).LightsailConn() - requestUpdate := false + if err := waitOperationWithContext(ctx, conn, output.Operations[0].Id); err != nil { + return diag.Errorf("waiting for Lightsail Relational Database (%s) update: %s", d.Id(), err) + } - req := lightsail.UpdateRelationalDatabaseInput{ - ApplyImmediately: aws.Bool(d.Get("apply_immediately").(bool)), - RelationalDatabaseName: aws.String(d.Id()), - } + if d.HasChange("backup_retention_enabled") { + if err := waitDatabaseBackupRetentionModified(ctx, conn, aws.String(d.Id()), d.Get("backup_retention_enabled").(bool)); err != nil { + return diag.Errorf("waiting for Lightsail Relational Database (%s) backup retention update: %s", d.Id(), err) + } + } - if d.HasChange("ca_certificate_identifier") { - req.CaCertificateIdentifier = aws.String(d.Get("ca_certificate_identifier").(string)) - requestUpdate = true - } + if d.HasChange("publicly_accessible") { + if err := waitDatabasePubliclyAccessibleModified(ctx, conn, aws.String(d.Id()), d.Get("publicly_accessible").(bool)); err != nil { + return diag.Errorf("waiting for Lightsail Relational Database (%s) publicly accessible update: %s", d.Id(), err) + } + } - if d.HasChange("backup_retention_enabled") { - if d.Get("backup_retention_enabled").(bool) { - req.EnableBackupRetention = aws.Bool(d.Get("backup_retention_enabled").(bool)) - } else { - req.DisableBackupRetention = aws.Bool(true) + // Some Operations can complete before the Database enters the Available state. Added a waiter to make sure the Database is available before continuing. + if _, err = waitDatabaseModified(ctx, conn, aws.String(d.Id())); err != nil { + return diag.Errorf("waiting for Lightsail Relational Database (%s) to become available: %s", d.Id(), err) } - requestUpdate = true } - if d.HasChange("master_password") { - req.MasterUserPassword = aws.String(d.Get("master_password").(string)) - requestUpdate = true - } + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") - if d.HasChange("preferred_backup_window") { - req.PreferredBackupWindow = aws.String(d.Get("preferred_backup_window").(string)) - requestUpdate = true + if err := UpdateTagsWithContext(ctx, conn, d.Id(), o, n); err != nil { + return diag.Errorf("updating Lightsail Relational Database (%s) tags: %s", d.Id(), err) + } } - if d.HasChange("preferred_maintenance_window") { - req.PreferredMaintenanceWindow = aws.String(d.Get("preferred_maintenance_window").(string)) - requestUpdate = true - } + return resourceDatabaseRead(ctx, d, meta) +} - if d.HasChange("publicly_accessible") { - req.PubliclyAccessible = aws.Bool(d.Get("publicly_accessible").(bool)) - requestUpdate = true +func resourceDatabaseDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).LightsailConn() + + // Some Operations can complete before the Database enters the Available state. Added a waiter to make sure the Database is available before continuing. + if _, err := waitDatabaseModified(ctx, conn, aws.String(d.Id())); err != nil { + return diag.Errorf("waiting for Lightsail Relational Database (%s) to become available: %s", d.Id(), err) } - if d.HasChange("tags") { - o, n := d.GetChange("tags") + skipFinalSnapshot := d.Get("skip_final_snapshot").(bool) - if err := UpdateTags(conn, d.Id(), o, n); err != nil { - return fmt.Errorf("error updating Lightsail Database (%s) tags: %s", d.Id(), err) - } + input := &lightsail.DeleteRelationalDatabaseInput{ + RelationalDatabaseName: aws.String(d.Id()), + SkipFinalSnapshot: aws.Bool(skipFinalSnapshot), } - if d.HasChange("tags_all") { - o, n := d.GetChange("tags_all") - if err := UpdateTags(conn, d.Id(), o, n); err != nil { - return fmt.Errorf("error updating Lightsail Database (%s) tags: %s", d.Id(), err) + if !skipFinalSnapshot { + if name, present := d.GetOk("final_snapshot_name"); present { + input.FinalRelationalDatabaseSnapshotName = aws.String(name.(string)) + } else { + return diag.Errorf("Lightsail Database FinalRelationalDatabaseSnapshotName is required when a final snapshot is required") } } - if requestUpdate { - resp, err := conn.UpdateRelationalDatabase(&req) - if err != nil { - return err - } - - if len(resp.Operations) == 0 { - return fmt.Errorf("No operations found for Update Relational Database request") - } - - op := resp.Operations[0] + output, err := conn.DeleteRelationalDatabaseWithContext(ctx, input) - err = waitOperation(conn, op.Id) - if err != nil { - return fmt.Errorf("Error waiting for Relational Database (%s) to become ready: %s", d.Id(), err) - } - - if d.HasChange("backup_retention_enabled") { - err = waitDatabaseBackupRetentionModified(conn, aws.String(d.Id()), aws.Bool(d.Get("backup_retention_enabled").(bool))) - if err != nil { - return fmt.Errorf("Error waiting for Relational Database (%s) Backup Retention to be updated: %s", d.Id(), err) - } - } + if err != nil { + return diag.Errorf("deleting Lightsail Relational Database (%s): %s", d.Id(), err) + } - // Some Operations can complete before the Database enters the Available state. Added a waiter to make sure the Database is available before continuing. - _, err = waitDatabaseModified(conn, aws.String(d.Id())) - if err != nil { - return fmt.Errorf("Error waiting for Relational Database (%s) to become available: %s", d.Id(), err) - } + if err := waitOperationWithContext(ctx, conn, output.Operations[0].Id); err != nil { + return diag.Errorf("waiting for Lightsail Relational Database (%s) delete: %s", d.Id(), err) } - return resourceDatabaseRead(d, meta) + return nil +} + +func resourceDatabaseImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + // Neither skip_final_snapshot nor final_snapshot_identifier can be fetched + // from any API call, so we need to default skip_final_snapshot to true so + // that final_snapshot_identifier is not required + d.Set("skip_final_snapshot", true) + return []*schema.ResourceData{d}, nil } diff --git a/internal/service/lightsail/database_test.go b/internal/service/lightsail/database_test.go index 32dbc821ecc6..bfd9a7e1bbb3 100644 --- a/internal/service/lightsail/database_test.go +++ b/internal/service/lightsail/database_test.go @@ -24,7 +24,7 @@ import ( func TestAccLightsailDatabase_basic(t *testing.T) { var db lightsail.RelationalDatabase resourceName := "aws_lightsail_database.test" - rName := fmt.Sprintf("tf-test-lightsail-%d", sdkacctest.RandInt()) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { @@ -73,10 +73,10 @@ func TestAccLightsailDatabase_basic(t *testing.T) { }) } -func TestAccLightsailDatabase_RelationalDatabaseName(t *testing.T) { +func TestAccLightsailDatabase_relationalDatabaseName(t *testing.T) { var db lightsail.RelationalDatabase resourceName := "aws_lightsail_database.test" - rName := fmt.Sprintf("tf-test-lightsail-%d", sdkacctest.RandInt()) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) rNameTooShort := "s" rNameTooLong := fmt.Sprintf("%s-%s", rName, sdkacctest.RandString(255)) rNameContainsUnderscore := fmt.Sprintf("%s-%s", rName, "_test") @@ -135,9 +135,9 @@ func TestAccLightsailDatabase_RelationalDatabaseName(t *testing.T) { }) } -func TestAccLightsailDatabase_MasterDatabaseName(t *testing.T) { +func TestAccLightsailDatabase_masterDatabaseName(t *testing.T) { var db lightsail.RelationalDatabase - rName := fmt.Sprintf("tf-test-lightsail-%d", sdkacctest.RandInt()) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_lightsail_database.test" dbName := "randomdatabasename" dbNameTooShort := "" @@ -201,9 +201,9 @@ func TestAccLightsailDatabase_MasterDatabaseName(t *testing.T) { }) } -func TestAccLightsailDatabase_MasterUsername(t *testing.T) { +func TestAccLightsailDatabase_masterUsername(t *testing.T) { var db lightsail.RelationalDatabase - rName := fmt.Sprintf("tf-test-lightsail-%d", sdkacctest.RandInt()) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_lightsail_database.test" username := "username1" usernameTooShort := "" @@ -272,8 +272,8 @@ func TestAccLightsailDatabase_MasterUsername(t *testing.T) { }) } -func TestAccLightsailDatabase_MasterPassword(t *testing.T) { - rName := fmt.Sprintf("tf-test-lightsail-%d", sdkacctest.RandInt()) +func TestAccLightsailDatabase_masterPassword(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) password := "testpassword" passwordTooShort := "short" passwordTooLong := fmt.Sprintf("%s-%s", password, sdkacctest.RandString(128)) @@ -320,9 +320,9 @@ func TestAccLightsailDatabase_MasterPassword(t *testing.T) { }) } -func TestAccLightsailDatabase_PreferredBackupWindow(t *testing.T) { +func TestAccLightsailDatabase_preferredBackupWindow(t *testing.T) { var db lightsail.RelationalDatabase - rName := fmt.Sprintf("tf-test-lightsail-%d", sdkacctest.RandInt()) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_lightsail_database.test" backupWindowInvalidHour := "25:30-10:00" backupWindowInvalidMinute := "10:00-10:70" @@ -374,9 +374,9 @@ func TestAccLightsailDatabase_PreferredBackupWindow(t *testing.T) { }) } -func TestAccLightsailDatabase_PreferredMaintenanceWindow(t *testing.T) { +func TestAccLightsailDatabase_preferredMaintenanceWindow(t *testing.T) { var db lightsail.RelationalDatabase - rName := fmt.Sprintf("tf-test-lightsail-%d", sdkacctest.RandInt()) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_lightsail_database.test" maintenanceWindowInvalidDay := "tuesday:04:30-tue:05:00" maintenanceWindowInvalidHour := "tue:04:30-tue:30:00" @@ -433,9 +433,9 @@ func TestAccLightsailDatabase_PreferredMaintenanceWindow(t *testing.T) { }) } -func TestAccLightsailDatabase_PubliclyAccessible(t *testing.T) { +func TestAccLightsailDatabase_publiclyAccessible(t *testing.T) { var db lightsail.RelationalDatabase - rName := fmt.Sprintf("tf-test-lightsail-%d", sdkacctest.RandInt()) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_lightsail_database.test" resource.ParallelTest(t, resource.TestCase{ @@ -449,7 +449,7 @@ func TestAccLightsailDatabase_PubliclyAccessible(t *testing.T) { CheckDestroy: testAccCheckDatabaseDestroy, Steps: []resource.TestStep{ { - Config: testAccDatabaseConfig_publiclyAccessible(rName, "true"), + Config: testAccDatabaseConfig_publiclyAccessible(rName, true), Check: resource.ComposeTestCheckFunc( testAccCheckDatabaseExists(resourceName, &db), resource.TestCheckResourceAttr(resourceName, "publicly_accessible", "true"), @@ -467,7 +467,7 @@ func TestAccLightsailDatabase_PubliclyAccessible(t *testing.T) { }, }, { - Config: testAccDatabaseConfig_publiclyAccessible(rName, "false"), + Config: testAccDatabaseConfig_publiclyAccessible(rName, false), Check: resource.ComposeTestCheckFunc( testAccCheckDatabaseExists(resourceName, &db), resource.TestCheckResourceAttr(resourceName, "publicly_accessible", "false"), @@ -477,9 +477,9 @@ func TestAccLightsailDatabase_PubliclyAccessible(t *testing.T) { }) } -func TestAccLightsailDatabase_BackupRetentionEnabled(t *testing.T) { +func TestAccLightsailDatabase_backupRetentionEnabled(t *testing.T) { var db lightsail.RelationalDatabase - rName := fmt.Sprintf("tf-test-lightsail-%d", sdkacctest.RandInt()) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_lightsail_database.test" resource.ParallelTest(t, resource.TestCase{ @@ -493,7 +493,7 @@ func TestAccLightsailDatabase_BackupRetentionEnabled(t *testing.T) { CheckDestroy: testAccCheckDatabaseDestroy, Steps: []resource.TestStep{ { - Config: testAccDatabaseConfig_backupRetentionEnabled(rName, "true"), + Config: testAccDatabaseConfig_backupRetentionEnabled(rName, true), Check: resource.ComposeTestCheckFunc( testAccCheckDatabaseExists(resourceName, &db), resource.TestCheckResourceAttr(resourceName, "backup_retention_enabled", "true"), @@ -511,7 +511,7 @@ func TestAccLightsailDatabase_BackupRetentionEnabled(t *testing.T) { }, }, { - Config: testAccDatabaseConfig_backupRetentionEnabled(rName, "false"), + Config: testAccDatabaseConfig_backupRetentionEnabled(rName, false), Check: resource.ComposeTestCheckFunc( testAccCheckDatabaseExists(resourceName, &db), resource.TestCheckResourceAttr(resourceName, "backup_retention_enabled", "false"), @@ -521,9 +521,9 @@ func TestAccLightsailDatabase_BackupRetentionEnabled(t *testing.T) { }) } -func TestAccLightsailDatabase_FinalSnapshotName(t *testing.T) { +func TestAccLightsailDatabase_finalSnapshotName(t *testing.T) { var db lightsail.RelationalDatabase - rName := fmt.Sprintf("tf-test-lightsail-%d", sdkacctest.RandInt()) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_lightsail_database.test" sName := fmt.Sprintf("%s-snapshot", rName) sNameTooShort := "s" @@ -578,9 +578,9 @@ func TestAccLightsailDatabase_FinalSnapshotName(t *testing.T) { }) } -func TestAccLightsailDatabase_Tags(t *testing.T) { +func TestAccLightsailDatabase_tags(t *testing.T) { var db1, db2, db3 lightsail.RelationalDatabase - rName := fmt.Sprintf("tf-test-lightsail-%d", sdkacctest.RandInt()) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_lightsail_database.test" resource.ParallelTest(t, resource.TestCase{ @@ -633,9 +633,48 @@ func TestAccLightsailDatabase_Tags(t *testing.T) { }) } +func TestAccLightsailDatabase_ha(t *testing.T) { + var db lightsail.RelationalDatabase + resourceName := "aws_lightsail_database.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(lightsail.EndpointsID, t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, lightsail.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDatabaseConfig_ha(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDatabaseExists(resourceName, &db), + resource.TestCheckResourceAttr(resourceName, "relational_database_name", rName), + resource.TestCheckResourceAttr(resourceName, "bundle_id", "micro_ha_1_0"), + resource.TestCheckResourceAttrSet(resourceName, "availability_zone"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "apply_immediately", + "master_password", + "skip_final_snapshot", + "final_snapshot_name", + }, + }, + }, + }) +} + func TestAccLightsailDatabase_disappears(t *testing.T) { var db lightsail.RelationalDatabase - rName := fmt.Sprintf("tf-test-lightsail-%d", sdkacctest.RandInt()) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_lightsail_database.test" testDestroy := func(*terraform.State) error { @@ -782,22 +821,13 @@ func testAccCheckDatabaseSnapshotDestroy(s *terraform.State) error { return nil } -func testAccDatabaseConfigBase() string { - return ` -data "aws_availability_zones" "available" { - state = "available" - - filter { - name = "opt-in-status" - values = ["opt-in-not-required"] - } -} -` +func testAccDatabaseConfig_base() string { + return acctest.ConfigAvailableAZsNoOptIn() } func testAccDatabaseConfig_basic(rName string) string { return acctest.ConfigCompose( - testAccDatabaseConfigBase(), + testAccDatabaseConfig_base(), fmt.Sprintf(` resource "aws_lightsail_database" "test" { relational_database_name = %[1]q @@ -814,7 +844,7 @@ resource "aws_lightsail_database" "test" { func testAccDatabaseConfig_masterDatabaseName(rName string, masterDatabaseName string) string { return acctest.ConfigCompose( - testAccDatabaseConfigBase(), + testAccDatabaseConfig_base(), fmt.Sprintf(` resource "aws_lightsail_database" "test" { relational_database_name = %[1]q @@ -831,7 +861,7 @@ resource "aws_lightsail_database" "test" { func testAccDatabaseConfig_masterUsername(rName string, masterUsername string) string { return acctest.ConfigCompose( - testAccDatabaseConfigBase(), + testAccDatabaseConfig_base(), fmt.Sprintf(` resource "aws_lightsail_database" "test" { relational_database_name = %[1]q @@ -848,7 +878,7 @@ resource "aws_lightsail_database" "test" { func testAccDatabaseConfig_masterPassword(rName string, masterPassword string) string { return acctest.ConfigCompose( - testAccDatabaseConfigBase(), + testAccDatabaseConfig_base(), fmt.Sprintf(` resource "aws_lightsail_database" "test" { relational_database_name = %[1]q @@ -865,7 +895,7 @@ resource "aws_lightsail_database" "test" { func testAccDatabaseConfig_preferredBackupWindow(rName string, preferredBackupWindow string) string { return acctest.ConfigCompose( - testAccDatabaseConfigBase(), + testAccDatabaseConfig_base(), fmt.Sprintf(` resource "aws_lightsail_database" "test" { relational_database_name = %[1]q @@ -884,7 +914,7 @@ resource "aws_lightsail_database" "test" { func testAccDatabaseConfig_preferredMaintenanceWindow(rName string, preferredMaintenanceWindow string) string { return acctest.ConfigCompose( - testAccDatabaseConfigBase(), + testAccDatabaseConfig_base(), fmt.Sprintf(` resource "aws_lightsail_database" "test" { relational_database_name = %[1]q @@ -901,9 +931,9 @@ resource "aws_lightsail_database" "test" { `, rName, preferredMaintenanceWindow)) } -func testAccDatabaseConfig_publiclyAccessible(rName string, publiclyAccessible string) string { +func testAccDatabaseConfig_publiclyAccessible(rName string, publiclyAccessible bool) string { return acctest.ConfigCompose( - testAccDatabaseConfigBase(), + testAccDatabaseConfig_base(), fmt.Sprintf(` resource "aws_lightsail_database" "test" { relational_database_name = %[1]q @@ -913,16 +943,16 @@ resource "aws_lightsail_database" "test" { master_username = "test" blueprint_id = "mysql_8_0" bundle_id = "micro_1_0" - publicly_accessible = %[2]q + publicly_accessible = %[2]t apply_immediately = true skip_final_snapshot = true } `, rName, publiclyAccessible)) } -func testAccDatabaseConfig_backupRetentionEnabled(rName string, backupRetentionEnabled string) string { +func testAccDatabaseConfig_backupRetentionEnabled(rName string, backupRetentionEnabled bool) string { return acctest.ConfigCompose( - testAccDatabaseConfigBase(), + testAccDatabaseConfig_base(), fmt.Sprintf(` resource "aws_lightsail_database" "test" { relational_database_name = %[1]q @@ -932,7 +962,7 @@ resource "aws_lightsail_database" "test" { master_username = "test" blueprint_id = "mysql_8_0" bundle_id = "micro_1_0" - backup_retention_enabled = %[2]q + backup_retention_enabled = %[2]t apply_immediately = true skip_final_snapshot = true } @@ -941,7 +971,7 @@ resource "aws_lightsail_database" "test" { func testAccDatabaseConfig_finalSnapshotName(rName string, sName string) string { return acctest.ConfigCompose( - testAccDatabaseConfigBase(), + testAccDatabaseConfig_base(), fmt.Sprintf(` resource "aws_lightsail_database" "test" { relational_database_name = %[1]q @@ -958,7 +988,7 @@ resource "aws_lightsail_database" "test" { func testAccDatabaseConfig_tags1(rName string, tagKey1, tagValue1 string) string { return acctest.ConfigCompose( - testAccDatabaseConfigBase(), + testAccDatabaseConfig_base(), fmt.Sprintf(` resource "aws_lightsail_database" "test" { relational_database_name = %[1]q @@ -978,7 +1008,7 @@ resource "aws_lightsail_database" "test" { func testAccDatabaseConfig_tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { return acctest.ConfigCompose( - testAccDatabaseConfigBase(), + testAccDatabaseConfig_base(), fmt.Sprintf(` resource "aws_lightsail_database" "test" { relational_database_name = %[1]q @@ -996,3 +1026,19 @@ resource "aws_lightsail_database" "test" { } `, rName, tagKey1, tagValue1, tagKey2, tagValue2)) } + +func testAccDatabaseConfig_ha(rName string) string { + return acctest.ConfigCompose( + testAccDatabaseConfig_base(), + fmt.Sprintf(` +resource "aws_lightsail_database" "test" { + relational_database_name = %[1]q + master_database_name = "testdatabasename" + master_password = "testdatabasepassword" + master_username = "test" + blueprint_id = "mysql_8_0" + bundle_id = "micro_ha_1_0" + skip_final_snapshot = true +} +`, rName)) +} diff --git a/internal/service/lightsail/status.go b/internal/service/lightsail/status.go index 7d4ae66447d3..82d6c7ff0f59 100644 --- a/internal/service/lightsail/status.go +++ b/internal/service/lightsail/status.go @@ -114,8 +114,30 @@ func statusDatabaseBackupRetention(conn *lightsail.Lightsail, db *string) resour return nil, "Failed", fmt.Errorf("Error retrieving Database info for (%s)", dbValue) } - log.Printf("[DEBUG] Lightsail Database (%s) Backup Retention setting is currently %t", dbValue, *output.RelationalDatabase.BackupRetentionEnabled) - return output, strconv.FormatBool(*output.RelationalDatabase.BackupRetentionEnabled), nil + return output, strconv.FormatBool(aws.BoolValue(output.RelationalDatabase.BackupRetentionEnabled)), nil + } +} + +func statusDatabasePubliclyAccessible(conn *lightsail.Lightsail, db *string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &lightsail.GetRelationalDatabaseInput{ + RelationalDatabaseName: db, + } + + dbValue := aws.StringValue(db) + log.Printf("[DEBUG] Checking if Lightsail Database (%s) Backup Retention setting has been updated.", dbValue) + + output, err := conn.GetRelationalDatabase(input) + + if err != nil { + return output, "FAILED", err + } + + if output.RelationalDatabase == nil { + return nil, "Failed", fmt.Errorf("Error retrieving Database info for (%s)", dbValue) + } + + return output, strconv.FormatBool(aws.BoolValue(output.RelationalDatabase.PubliclyAccessible)), nil } } diff --git a/internal/service/lightsail/wait.go b/internal/service/lightsail/wait.go index 9617b3c36ff9..fb3c0d26e801 100644 --- a/internal/service/lightsail/wait.go +++ b/internal/service/lightsail/wait.go @@ -14,11 +14,11 @@ import ( ) const ( - // OperationTimeout is the Timout Value for Operations - OperationTimeout = 20 * time.Minute + // OperationTimeout is the Timeout Value for Operations + OperationTimeout = 30 * time.Minute // OperationDelay is the Delay Value for Operations OperationDelay = 5 * time.Second - // OperationMinTimeout is the MinTimout Value for Operations + // OperationMinTimeout is the MinTimeout Value for Operations OperationMinTimeout = 3 * time.Second // DatabaseStateModifying is a state value for a Relational Database undergoing a modification @@ -26,15 +26,15 @@ const ( // DatabaseStateAvailable is a state value for a Relational Database available for modification DatabaseStateAvailable = "available" - // DatabaseTimeout is the Timout Value for Relational Database Modifications - DatabaseTimeout = 20 * time.Minute + // DatabaseTimeout is the Timeout Value for Relational Database Modifications + DatabaseTimeout = 30 * time.Minute // DatabaseDelay is the Delay Value for Relational Database Modifications DatabaseDelay = 5 * time.Second - // DatabaseMinTimeout is the MinTimout Value for Relational Database Modifications + // DatabaseMinTimeout is the MinTimeout Value for Relational Database Modifications DatabaseMinTimeout = 3 * time.Second ) -// waitOperation waits for an Operation to return Succeeded or Compleated +// waitOperation waits for an Operation to return Succeeded or Completed func waitOperation(conn *lightsail.Lightsail, oid *string) error { stateConf := &resource.StateChangeConf{ Pending: []string{lightsail.OperationStatusStarted}, @@ -55,7 +55,7 @@ func waitOperation(conn *lightsail.Lightsail, oid *string) error { } // waitDatabaseModified waits for a Modified Database return available -func waitDatabaseModified(conn *lightsail.Lightsail, db *string) (*lightsail.GetRelationalDatabaseOutput, error) { +func waitDatabaseModified(ctx context.Context, conn *lightsail.Lightsail, db *string) (*lightsail.GetRelationalDatabaseOutput, error) { stateConf := &resource.StateChangeConf{ Pending: []string{DatabaseStateModifying}, Target: []string{DatabaseStateAvailable}, @@ -65,7 +65,7 @@ func waitDatabaseModified(conn *lightsail.Lightsail, db *string) (*lightsail.Get MinTimeout: DatabaseMinTimeout, } - outputRaw, err := stateConf.WaitForState() + outputRaw, err := stateConf.WaitForStateContext(ctx) if output, ok := outputRaw.(*lightsail.GetRelationalDatabaseOutput); ok { return output, err @@ -76,17 +76,36 @@ func waitDatabaseModified(conn *lightsail.Lightsail, db *string) (*lightsail.Get // waitDatabaseBackupRetentionModified waits for a Modified BackupRetention on Database return available -func waitDatabaseBackupRetentionModified(conn *lightsail.Lightsail, db *string, status *bool) error { +func waitDatabaseBackupRetentionModified(ctx context.Context, conn *lightsail.Lightsail, db *string, target bool) error { stateConf := &resource.StateChangeConf{ - Pending: []string{strconv.FormatBool(!aws.BoolValue(status))}, - Target: []string{strconv.FormatBool(aws.BoolValue(status))}, + Pending: []string{strconv.FormatBool(!target)}, + Target: []string{strconv.FormatBool(target)}, Refresh: statusDatabaseBackupRetention(conn, db), Timeout: DatabaseTimeout, Delay: DatabaseDelay, MinTimeout: DatabaseMinTimeout, } - outputRaw, err := stateConf.WaitForState() + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if _, ok := outputRaw.(*lightsail.GetRelationalDatabaseOutput); ok { + return err + } + + return err +} + +func waitDatabasePubliclyAccessibleModified(ctx context.Context, conn *lightsail.Lightsail, db *string, target bool) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{strconv.FormatBool(!target)}, + Target: []string{strconv.FormatBool(target)}, + Refresh: statusDatabasePubliclyAccessible(conn, db), + Timeout: DatabaseTimeout, + Delay: DatabaseDelay, + MinTimeout: DatabaseMinTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) if _, ok := outputRaw.(*lightsail.GetRelationalDatabaseOutput); ok { return err @@ -228,3 +247,23 @@ func waitInstanceStateWithContext(ctx context.Context, conn *lightsail.Lightsail return nil, err } + +// waitOperation waits for an Operation to return Succeeded or Completed with context +func waitOperationWithContext(ctx context.Context, conn *lightsail.Lightsail, oid *string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{lightsail.OperationStatusStarted}, + Target: []string{lightsail.OperationStatusCompleted, lightsail.OperationStatusSucceeded}, + Refresh: statusOperation(conn, oid), + Timeout: OperationTimeout, + Delay: OperationDelay, + MinTimeout: OperationMinTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if _, ok := outputRaw.(*lightsail.GetOperationOutput); ok { + return err + } + + return err +} diff --git a/website/docs/r/lightsail_database.html.markdown b/website/docs/r/lightsail_database.html.markdown index 508ed9288614..6ea016b7ec2a 100644 --- a/website/docs/r/lightsail_database.html.markdown +++ b/website/docs/r/lightsail_database.html.markdown @@ -103,7 +103,7 @@ resource "aws_lightsail_database" "test" { The following arguments are supported: * `name` - (Required) The name to use for your new Lightsail database resource. Names be unique within each AWS Region in your Lightsail account. -* `availability_zone` - (Required) The Availability Zone in which to create your new database. Use the us-east-2a case-sensitive format. +* `availability_zone` - The Availability Zone in which to create your new database. Use the us-east-2a case-sensitive format. * `master_database_name` - (Required) The name of the master database created when the Lightsail database resource is created. * `master_password` - (Sensitive) The password for the master user of your new database. The password can include any printable ASCII character except "/", """, or "@". * `master_username` - The master user name for your new database.