diff --git a/aws/resource_aws_timestreamwrite_database.go b/aws/resource_aws_timestreamwrite_database.go index 42b8282015c2..cd12ca4407d7 100644 --- a/aws/resource_aws_timestreamwrite_database.go +++ b/aws/resource_aws_timestreamwrite_database.go @@ -1,24 +1,29 @@ package aws import ( + "context" "fmt" "log" + "regexp" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/timestreamwrite" + "github.com/hashicorp/aws-sdk-go-base/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/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" ) func resourceAwsTimestreamWriteDatabase() *schema.Resource { return &schema.Resource{ - Create: resourceAwsTimestreamWriteDatabaseCreate, - Read: resourceAwsTimestreamWriteDatabaseRead, - Update: resourceAwsTimestreamWriteDatabaseUpdate, - Delete: resourceAwsTimestreamWriteDatabaseDelete, + CreateWithoutTimeout: resourceAwsTimestreamWriteDatabaseCreate, + ReadWithoutTimeout: resourceAwsTimestreamWriteDatabaseRead, + UpdateWithoutTimeout: resourceAwsTimestreamWriteDatabaseUpdate, + DeleteWithoutTimeout: resourceAwsTimestreamWriteDatabaseDelete, Importer: &schema.ResourceImporter{ - State: schema.ImportStatePassthrough, + StateContext: schema.ImportStatePassthroughContext, }, Schema: map[string]*schema.Schema{ @@ -31,83 +36,121 @@ func resourceAwsTimestreamWriteDatabase() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(3, 64), + validation.StringMatch(regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`), "must only include alphanumeric, underscore, period, or hyphen characters"), + ), }, "kms_key_id": { Type: schema.TypeString, - Computed: true, Optional: true, + Computed: true, ValidateFunc: validateArn, }, + "table_count": { + Type: schema.TypeInt, + Computed: true, + }, + "tags": tagsSchema(), + + "tags_all": tagsSchemaComputed(), }, + + CustomizeDiff: SetTagsDiff, } } -func resourceAwsTimestreamWriteDatabaseCreate(d *schema.ResourceData, meta interface{}) error { +func resourceAwsTimestreamWriteDatabaseCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*AWSClient).timestreamwriteconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) + + dbName := d.Get("database_name").(string) input := ×treamwrite.CreateDatabaseInput{ - DatabaseName: aws.String(d.Get("database_name").(string)), + DatabaseName: aws.String(dbName), } + if v, ok := d.GetOk("kms_key_id"); ok { input.KmsKeyId = aws.String(v.(string)) } - if attr, ok := d.GetOk("tags"); ok { - input.Tags = keyvaluetags.New(attr.(map[string]interface{})).IgnoreAws().TimestreamwriteTags() + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().TimestreamwriteTags() } - resp, err := conn.CreateDatabase(input) + resp, err := conn.CreateDatabaseWithContext(ctx, input) if err != nil { - return err + return diag.FromErr(fmt.Errorf("error creating Timestream Database (%s): %w", dbName, err)) } - name := aws.StringValue(resp.Database.DatabaseName) + if resp == nil || resp.Database == nil { + return diag.FromErr(fmt.Errorf("error creating Timestream Database (%s): empty output", dbName)) + } - d.SetId(name) + d.SetId(aws.StringValue(resp.Database.DatabaseName)) - return resourceAwsTimestreamWriteDatabaseRead(d, meta) + return resourceAwsTimestreamWriteDatabaseRead(ctx, d, meta) } -func resourceAwsTimestreamWriteDatabaseRead(d *schema.ResourceData, meta interface{}) error { +func resourceAwsTimestreamWriteDatabaseRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*AWSClient).timestreamwriteconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig - resp, err := conn.DescribeDatabase(×treamwrite.DescribeDatabaseInput{ + input := ×treamwrite.DescribeDatabaseInput{ DatabaseName: aws.String(d.Id()), - }) + } + + resp, err := conn.DescribeDatabaseWithContext(ctx, input) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, timestreamwrite.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] Timestream Database %s not found, removing from state", d.Id()) + d.SetId("") + return nil + } + if err != nil { - if isAWSErr(err, timestreamwrite.ErrCodeResourceNotFoundException, "") { - log.Printf("[WARN] Timestream Database %q not found, removing from state", d.Id()) - d.SetId("") - return nil - } - return err + return diag.FromErr(fmt.Errorf("error reading Timestream Database (%s): %w", d.Id(), err)) } - d.Set("database_name", resp.Database.DatabaseName) - d.Set("kms_key_id", resp.Database.KmsKeyId) - d.Set("arn", resp.Database.Arn) + if resp == nil || resp.Database == nil { + return diag.FromErr(fmt.Errorf("error reading Timestream Database (%s): empty output", d.Id())) + } - arn := aws.StringValue(resp.Database.Arn) + db := resp.Database + arn := aws.StringValue(db.Arn) + + d.Set("arn", arn) + d.Set("database_name", db.DatabaseName) + d.Set("kms_key_id", db.KmsKeyId) + d.Set("table_count", db.TableCount) tags, err := keyvaluetags.TimestreamwriteListTags(conn, arn) if err != nil { - return fmt.Errorf("error listing tags for Timestream Database (%s): %s", arn, err) + return diag.FromErr(fmt.Errorf("error listing tags for Timestream Database (%s): %w", arn, err)) } - if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting tags: %w", err)) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting tags_all: %w", err)) } return nil } -func resourceAwsTimestreamWriteDatabaseUpdate(d *schema.ResourceData, meta interface{}) error { +func resourceAwsTimestreamWriteDatabaseUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*AWSClient).timestreamwriteconn if d.HasChange("kms_key_id") { @@ -116,36 +159,39 @@ func resourceAwsTimestreamWriteDatabaseUpdate(d *schema.ResourceData, meta inter KmsKeyId: aws.String(d.Get("kms_key_id").(string)), } - _, err := conn.UpdateDatabase(input) + _, err := conn.UpdateDatabaseWithContext(ctx, input) + if err != nil { - return err + return diag.FromErr(fmt.Errorf("error updating Timestream Database (%s): %w", d.Id(), err)) } } - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.TimestreamwriteUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { - return fmt.Errorf("error updating Timesteram Database (%s) tags: %s", d.Get("arn").(string), err) + return diag.FromErr(fmt.Errorf("error updating Timestream Database (%s) tags: %w", d.Get("arn").(string), err)) } } - return resourceAwsTimestreamWriteDatabaseRead(d, meta) + return resourceAwsTimestreamWriteDatabaseRead(ctx, d, meta) } -func resourceAwsTimestreamWriteDatabaseDelete(d *schema.ResourceData, meta interface{}) error { +func resourceAwsTimestreamWriteDatabaseDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*AWSClient).timestreamwriteconn input := ×treamwrite.DeleteDatabaseInput{ DatabaseName: aws.String(d.Id()), } - _, err := conn.DeleteDatabase(input) + _, err := conn.DeleteDatabaseWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, timestreamwrite.ErrCodeResourceNotFoundException) { + return nil + } + if err != nil { - if isAWSErr(err, timestreamwrite.ErrCodeResourceNotFoundException, "") { - return nil - } - return err + return diag.FromErr(fmt.Errorf("error deleting Timestream Database (%s): %w", d.Id(), err)) } return nil diff --git a/aws/resource_aws_timestreamwrite_database_test.go b/aws/resource_aws_timestreamwrite_database_test.go index 8e6dda9af93c..7419d34f171a 100644 --- a/aws/resource_aws_timestreamwrite_database_test.go +++ b/aws/resource_aws_timestreamwrite_database_test.go @@ -2,28 +2,90 @@ package aws import ( "fmt" + "log" "regexp" "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/timestreamwrite" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) +func init() { + resource.AddTestSweepers("aws_timestreamwrite_database", &resource.Sweeper{ + Name: "aws_timestreamwrite_database", + F: testSweepTimestreamWriteDatabases, + }) +} + +func testSweepTimestreamWriteDatabases(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + conn := client.(*AWSClient).timestreamwriteconn + + var sweeperErrs *multierror.Error + + input := ×treamwrite.ListDatabasesInput{} + + err = conn.ListDatabasesPages(input, func(page *timestreamwrite.ListDatabasesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, database := range page.Databases { + if database == nil { + continue + } + + dbName := aws.StringValue(database.DatabaseName) + + r := resourceAwsTimestreamWriteDatabase() + d := r.Data(nil) + d.SetId(dbName) + + err := r.Delete(d, client) + + if err != nil { + sweeperErr := fmt.Errorf("error deleting Timestream Database (%s): %w", dbName, err) + log.Printf("[ERROR] %s", sweeperErr) + sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) + continue + } + } + + return !lastPage + }) + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping Timestream Database sweep for %s: %s", region, err) + return sweeperErrs.ErrorOrNil() // In case we have completed some pages, but had errors + } + + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error listing Timestream Databases: %w", err)) + } + + return sweeperErrs.ErrorOrNil() +} + func TestAccAWSTimestreamWriteDatabase_basic(t *testing.T) { resourceName := "aws_timestreamwrite_database.test" rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTimestreamWrite(t) }, ErrorCheck: testAccErrorCheck(t, timestreamwrite.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSTimestreamWriteDatabaseDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSTimestreamWriteDatabaseConfigNoTags(rName), + Config: testAccAWSTimestreamWriteDatabaseConfigBasic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSTimestreamWriteDatabaseExists(resourceName), testAccCheckResourceAttrRegionalARN(resourceName, "arn", "timestream", fmt.Sprintf("database/%s", rName)), @@ -47,7 +109,7 @@ func TestAccAWSTimestreamWriteDatabase_kmsKey(t *testing.T) { kmsResourceName := "aws_kms_key.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTimestreamWrite(t) }, ErrorCheck: testAccErrorCheck(t, timestreamwrite.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSTimestreamWriteDatabaseDestroy, @@ -69,12 +131,53 @@ func TestAccAWSTimestreamWriteDatabase_kmsKey(t *testing.T) { }) } +func TestAccAWSTimestreamWriteDatabase_updateKmsKey(t *testing.T) { + resourceName := "aws_timestreamwrite_database.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + kmsResourceName := "aws_kms_key.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTimestreamWrite(t) }, + ErrorCheck: testAccErrorCheck(t, timestreamwrite.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSTimestreamWriteDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSTimestreamWriteDatabaseConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSTimestreamWriteDatabaseExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "kms_key_id", "kms", regexp.MustCompile(`key/.+`)), + ), + }, + { + Config: testAccAWSTimestreamWriteDatabaseConfigKmsKey(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSTimestreamWriteDatabaseExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "kms_key_id", kmsResourceName, "arn"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSTimestreamWriteDatabaseConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSTimestreamWriteDatabaseExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "kms_key_id", "kms", regexp.MustCompile(`key/.+`)), + ), + }, + }, + }) +} + func TestAccAWSTimestreamWriteDatabase_Tags(t *testing.T) { resourceName := "aws_timestreamwrite_database.test" rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTimestreamWrite(t) }, ErrorCheck: testAccErrorCheck(t, timestreamwrite.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSTimestreamWriteDatabaseDestroy, @@ -85,6 +188,8 @@ func TestAccAWSTimestreamWriteDatabase_Tags(t *testing.T) { testAccCheckAWSTimestreamWriteDatabaseExists(resourceName), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key1", "value1"), ), }, { @@ -94,6 +199,9 @@ func TestAccAWSTimestreamWriteDatabase_Tags(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key2", "value2"), ), }, { @@ -102,6 +210,8 @@ func TestAccAWSTimestreamWriteDatabase_Tags(t *testing.T) { testAccCheckAWSTimestreamWriteDatabaseExists(resourceName), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key2", "value2"), ), }, { @@ -121,15 +231,19 @@ func testAccCheckAWSTimestreamWriteDatabaseDestroy(s *terraform.State) error { continue } - _, err := conn.DescribeDatabase(×treamwrite.DescribeDatabaseInput{ + output, err := conn.DescribeDatabase(×treamwrite.DescribeDatabaseInput{ DatabaseName: aws.String(rs.Primary.ID), }) - if isAWSErr(err, timestreamwrite.ErrCodeResourceNotFoundException, "") { + if tfawserr.ErrCodeEquals(err, timestreamwrite.ErrCodeResourceNotFoundException) { continue } - if err == nil { + if err != nil { + return err + } + + if output != nil && output.Database != nil { return fmt.Errorf("Timestream Database (%s) still exists", rs.Primary.ID) } } @@ -145,12 +259,12 @@ func testAccCheckAWSTimestreamWriteDatabaseExists(n string) resource.TestCheckFu } if rs.Primary.ID == "" { - return fmt.Errorf("No Timestream Database ID is set") + return fmt.Errorf("no resource ID is set") } conn := testAccProvider.Meta().(*AWSClient).timestreamwriteconn - _, err := conn.DescribeDatabase(×treamwrite.DescribeDatabaseInput{ + output, err := conn.DescribeDatabase(×treamwrite.DescribeDatabaseInput{ DatabaseName: aws.String(rs.Primary.ID), }) @@ -158,11 +272,31 @@ func testAccCheckAWSTimestreamWriteDatabaseExists(n string) resource.TestCheckFu return err } + if output == nil || output.Database == nil { + return fmt.Errorf("Timestream Database (%s) not found", rs.Primary.ID) + } + return nil } } -func testAccAWSTimestreamWriteDatabaseConfigNoTags(rName string) string { +func testAccPreCheckAWSTimestreamWrite(t *testing.T) { + conn := testAccProvider.Meta().(*AWSClient).timestreamwriteconn + + input := ×treamwrite.ListDatabasesInput{} + + _, err := conn.ListDatabases(input) + + if testAccPreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccAWSTimestreamWriteDatabaseConfigBasic(rName string) string { return fmt.Sprintf(` resource "aws_timestreamwrite_database" "test" { database_name = %[1]q @@ -198,21 +332,21 @@ resource "aws_timestreamwrite_database" "test" { func testAccAWSTimestreamWriteDatabaseConfigKmsKey(rName string) string { return fmt.Sprintf(` resource "aws_kms_key" "test" { - description = "Terraform acc test" + description = %[1]q policy = <