diff --git a/.changelog/23745.txt b/.changelog/23745.txt new file mode 100644 index 00000000000..9993b30dc29 --- /dev/null +++ b/.changelog/23745.txt @@ -0,0 +1,15 @@ +```release-note:enhancement +resource/aws_athena_database: Add `acl_configuration` and `expected_bucket_owner` arguments +``` + +```release-note:bug +resource/aws_athena_database: Remove from state on resource Read if deleted outside of Terraform +``` + +```release-note:enhancement +resource/aws_athena_database: Do not recreate the resource if `bucket` changes +``` + +```release-note:enhancement +resource/aws_athena_database: Add `comment` argument to support database descriptions +``` \ No newline at end of file diff --git a/internal/service/athena/database.go b/internal/service/athena/database.go index 4827d58866a..079a03ce2ae 100644 --- a/internal/service/athena/database.go +++ b/internal/service/athena/database.go @@ -2,12 +2,14 @@ package athena import ( "fmt" + "log" "regexp" "strings" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/athena" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -18,95 +20,105 @@ func ResourceDatabase() *schema.Resource { return &schema.Resource{ Create: resourceDatabaseCreate, Read: resourceDatabaseRead, - Update: resourceDatabaseUpdate, + Update: schema.Noop, Delete: resourceDatabaseDelete, Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validation.StringMatch(regexp.MustCompile("^[_a-z0-9]+$"), "must be lowercase letters, numbers, or underscore ('_')"), + "acl_configuration": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "s3_acl_option": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(athena.S3AclOption_Values(), false), + ForceNew: true, + }, + }, + }, }, "bucket": { Type: schema.TypeString, - Required: true, - ForceNew: true, + Optional: true, }, - "force_destroy": { - Type: schema.TypeBool, + "comment": { + Type: schema.TypeString, Optional: true, - Default: false, + ForceNew: true, }, "encryption_configuration": { Type: schema.TypeList, Optional: true, + ForceNew: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + "encryption_option": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(athena.EncryptionOption_Values(), false), + ForceNew: true, + }, "kms_key": { Type: schema.TypeString, Optional: true, - }, - "encryption_option": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice([]string{ - athena.EncryptionOptionCseKms, - athena.EncryptionOptionSseKms, - athena.EncryptionOptionSseS3, - }, false), + ForceNew: true, }, }, }, }, + "expected_bucket_owner": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "force_destroy": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile("^[_a-z0-9]+$"), "must be lowercase letters, numbers, or underscore ('_')"), + }, }, } } -func expandAthenaResultConfiguration(bucket string, encryptionConfigurationList []interface{}) *athena.ResultConfiguration { - resultConfig := athena.ResultConfiguration{ - OutputLocation: aws.String("s3://" + bucket), - } - - if len(encryptionConfigurationList) <= 0 { - return &resultConfig - } - - data := encryptionConfigurationList[0].(map[string]interface{}) - keyType := data["encryption_option"].(string) - keyID := data["kms_key"].(string) +func resourceDatabaseCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).AthenaConn - encryptionConfig := athena.EncryptionConfiguration{ - EncryptionOption: aws.String(keyType), - } + name := d.Get("name").(string) + var queryString string - if len(keyID) > 0 { - encryptionConfig.KmsKey = aws.String(keyID) + if v, ok := d.GetOk("comment"); ok { + queryString = fmt.Sprintf("create database `%[1]s` comment '%[2]s';", name, strings.Replace(v.(string), "'", "\\'", -1)) + } else { + queryString = fmt.Sprintf("create database `%[1]s`;", name) } - resultConfig.EncryptionConfiguration = &encryptionConfig - - return &resultConfig -} - -func resourceDatabaseCreate(d *schema.ResourceData, meta interface{}) error { - conn := meta.(*conns.AWSClient).AthenaConn - input := &athena.StartQueryExecutionInput{ - QueryString: aws.String(fmt.Sprintf("create database `%s`;", d.Get("name").(string))), - ResultConfiguration: expandAthenaResultConfiguration(d.Get("bucket").(string), d.Get("encryption_configuration").([]interface{})), + QueryString: aws.String(queryString), + ResultConfiguration: expandAthenaResultConfiguration(d), } resp, err := conn.StartQueryExecution(input) + if err != nil { - return err + return fmt.Errorf("error starting Athena Database (%s) query execution: %w", name, err) } - if err := executeAndExpectNoRowsWhenCreate(*resp.QueryExecutionId, conn); err != nil { + if err := executeAndExpectNoRows(*resp.QueryExecutionId, "create", conn); err != nil { return err } - d.SetId(d.Get("name").(string)) + + d.SetId(name) + return resourceDatabaseRead(d, meta) } @@ -114,26 +126,32 @@ func resourceDatabaseRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).AthenaConn input := &athena.GetDatabaseInput{ - DatabaseName: aws.String(d.Get("name").(string)), + DatabaseName: aws.String(d.Id()), CatalogName: aws.String("AwsDataCatalog"), } - _, err := conn.GetDatabase(input) + res, err := conn.GetDatabase(input) + + if tfawserr.ErrMessageContains(err, athena.ErrCodeMetadataException, "not found") && !d.IsNewResource() { + log.Printf("[WARN] Athena Database (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + if err != nil { - return err + return fmt.Errorf("error reading Athena Database (%s): %w", d.Id(), err) } - return nil -} -func resourceDatabaseUpdate(d *schema.ResourceData, meta interface{}) error { - return resourceDatabaseRead(d, meta) + db := res.Database + + d.Set("name", db.Name) + + return nil } func resourceDatabaseDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).AthenaConn - name := d.Get("name").(string) - - queryString := fmt.Sprintf("drop database `%s`", name) + queryString := fmt.Sprintf("drop database `%s`", d.Id()) if d.Get("force_destroy").(bool) { queryString += " cascade" } @@ -141,7 +159,7 @@ func resourceDatabaseDelete(d *schema.ResourceData, meta interface{}) error { input := &athena.StartQueryExecutionInput{ QueryString: aws.String(queryString), - ResultConfiguration: expandAthenaResultConfiguration(d.Get("bucket").(string), d.Get("encryption_configuration").([]interface{})), + ResultConfiguration: expandAthenaResultConfiguration(d), } resp, err := conn.StartQueryExecution(input) @@ -149,30 +167,70 @@ func resourceDatabaseDelete(d *schema.ResourceData, meta interface{}) error { return err } - if err := executeAndExpectNoRowsWhenDrop(*resp.QueryExecutionId, conn); err != nil { + if err := executeAndExpectNoRows(*resp.QueryExecutionId, "delete", conn); err != nil { return err } + return nil } -func executeAndExpectNoRowsWhenCreate(qeid string, conn *athena.Athena) error { - rs, err := QueryExecutionResult(qeid, conn) - if err != nil { - return err +func expandAthenaResultConfiguration(d *schema.ResourceData) *athena.ResultConfiguration { + + resultConfig := &athena.ResultConfiguration{ + OutputLocation: aws.String("s3://" + d.Get("bucket").(string)), + EncryptionConfiguration: expandAthenaResultConfigurationEncryptionConfig(d.Get("encryption_configuration").([]interface{})), } - if len(rs.Rows) != 0 { - return fmt.Errorf("Athena create database, unexpected query result: %s", flattenAthenaResultSet(rs)) + + if v, ok := d.GetOk("expected_bucket_owner"); ok { + resultConfig.ExpectedBucketOwner = aws.String(v.(string)) } - return nil + + if v, ok := d.GetOk("acl_configuration"); ok && len(v.([]interface{})) > 0 { + resultConfig.AclConfiguration = expandAthenaResultConfigurationAclConfig(v.([]interface{})) + } + + return resultConfig +} + +func expandAthenaResultConfigurationEncryptionConfig(config []interface{}) *athena.EncryptionConfiguration { + if len(config) <= 0 { + return nil + } + + data := config[0].(map[string]interface{}) + + encryptionConfig := &athena.EncryptionConfiguration{ + EncryptionOption: aws.String(data["encryption_option"].(string)), + } + + if v, ok := data["kms_key"].(string); ok && v != "" { + encryptionConfig.KmsKey = aws.String(v) + } + + return encryptionConfig +} + +func expandAthenaResultConfigurationAclConfig(config []interface{}) *athena.AclConfiguration { + if len(config) <= 0 { + return nil + } + + data := config[0].(map[string]interface{}) + + encryptionConfig := &athena.AclConfiguration{ + S3AclOption: aws.String(data["s3_acl_option"].(string)), + } + + return encryptionConfig } -func executeAndExpectNoRowsWhenDrop(qeid string, conn *athena.Athena) error { +func executeAndExpectNoRows(qeid, action string, conn *athena.Athena) error { rs, err := QueryExecutionResult(qeid, conn) if err != nil { return err } if len(rs.Rows) != 0 { - return fmt.Errorf("Athena drop database, unexpected query result: %s", flattenAthenaResultSet(rs)) + return fmt.Errorf("Athena %s database, unexpected query result: %s", action, flattenAthenaResultSet(rs)) } return nil } diff --git a/internal/service/athena/database_test.go b/internal/service/athena/database_test.go index 3c4646d046a..639534f7372 100644 --- a/internal/service/athena/database_test.go +++ b/internal/service/athena/database_test.go @@ -7,7 +7,6 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/athena" - "github.com/aws/aws-sdk-go/service/s3" sdkacctest "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" @@ -17,8 +16,10 @@ import ( ) func TestAccAthenaDatabase_basic(t *testing.T) { - rInt := sdkacctest.RandInt() + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) dbName := sdkacctest.RandString(8) + resourceName := "aws_athena_database.test" + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ErrorCheck: acctest.ErrorCheck(t, athena.EndpointsID), @@ -26,9 +27,37 @@ func TestAccAthenaDatabase_basic(t *testing.T) { CheckDestroy: testAccCheckDatabaseDestroy, Steps: []resource.TestStep{ { - Config: testAccAthenaDatabaseConfig(rInt, dbName, false), + Config: testAccAthenaDatabaseConfig(rName, dbName, false), Check: resource.ComposeTestCheckFunc( - testAccCheckDatabaseExists("aws_athena_database.hoge"), + testAccCheckDatabaseExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", dbName), + resource.TestCheckResourceAttrPair(resourceName, "bucket", "aws_s3_bucket.test", "bucket"), + resource.TestCheckResourceAttr(resourceName, "acl_configuration.#", "0"), + resource.TestCheckResourceAttr(resourceName, "encryption_configuration.#", "0"), + ), + }, + }, + }) +} + +func TestAccAthenaDatabase_acl(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + dbName := sdkacctest.RandString(8) + resourceName := "aws_athena_database.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, athena.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAthenaDatabaseAclConfig(rName, dbName, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckDatabaseExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", dbName), + resource.TestCheckResourceAttr(resourceName, "acl_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "acl_configuration.0.s3_acl_option", "BUCKET_OWNER_FULL_CONTROL"), ), }, }, @@ -36,8 +65,10 @@ func TestAccAthenaDatabase_basic(t *testing.T) { } func TestAccAthenaDatabase_encryption(t *testing.T) { - rInt := sdkacctest.RandInt() + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) dbName := sdkacctest.RandString(8) + resourceName := "aws_athena_database.test" + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ErrorCheck: acctest.ErrorCheck(t, athena.EndpointsID), @@ -45,10 +76,12 @@ func TestAccAthenaDatabase_encryption(t *testing.T) { CheckDestroy: testAccCheckDatabaseDestroy, Steps: []resource.TestStep{ { - Config: testAccAthenaDatabaseWithKMSConfig(rInt, dbName, false), + Config: testAccAthenaDatabaseWithKMSConfig(rName, dbName, false), Check: resource.ComposeTestCheckFunc( - testAccCheckDatabaseExists("aws_athena_database.hoge"), - resource.TestCheckResourceAttr("aws_athena_database.hoge", "encryption_configuration.0.encryption_option", "SSE_KMS"), + testAccCheckDatabaseExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "encryption_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "encryption_configuration.0.encryption_option", "SSE_KMS"), + resource.TestCheckResourceAttrPair(resourceName, "encryption_configuration.0.kms_key", "aws_kms_key.test", "arn"), ), }, }, @@ -56,8 +89,10 @@ func TestAccAthenaDatabase_encryption(t *testing.T) { } func TestAccAthenaDatabase_nameStartsWithUnderscore(t *testing.T) { - rInt := sdkacctest.RandInt() + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) dbName := "_" + sdkacctest.RandString(8) + resourceName := "aws_athena_database.test" + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ErrorCheck: acctest.ErrorCheck(t, athena.EndpointsID), @@ -65,10 +100,10 @@ func TestAccAthenaDatabase_nameStartsWithUnderscore(t *testing.T) { CheckDestroy: testAccCheckDatabaseDestroy, Steps: []resource.TestStep{ { - Config: testAccAthenaDatabaseConfig(rInt, dbName, false), + Config: testAccAthenaDatabaseConfig(rName, dbName, false), Check: resource.ComposeTestCheckFunc( - testAccCheckDatabaseExists("aws_athena_database.hoge"), - resource.TestCheckResourceAttr("aws_athena_database.hoge", "name", dbName), + testAccCheckDatabaseExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", dbName), ), }, }, @@ -76,8 +111,9 @@ func TestAccAthenaDatabase_nameStartsWithUnderscore(t *testing.T) { } func TestAccAthenaDatabase_nameCantHaveUppercase(t *testing.T) { - rInt := sdkacctest.RandInt() + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) dbName := "A" + sdkacctest.RandString(8) + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ErrorCheck: acctest.ErrorCheck(t, athena.EndpointsID), @@ -85,7 +121,7 @@ func TestAccAthenaDatabase_nameCantHaveUppercase(t *testing.T) { CheckDestroy: testAccCheckDatabaseDestroy, Steps: []resource.TestStep{ { - Config: testAccAthenaDatabaseConfig(rInt, dbName, false), + Config: testAccAthenaDatabaseConfig(rName, dbName, false), ExpectError: regexp.MustCompile(`must be lowercase letters, numbers, or underscore \('_'\)`), }, }, @@ -93,8 +129,9 @@ func TestAccAthenaDatabase_nameCantHaveUppercase(t *testing.T) { } func TestAccAthenaDatabase_destroyFailsIfTablesExist(t *testing.T) { - rInt := sdkacctest.RandInt() + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) dbName := sdkacctest.RandString(8) + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ErrorCheck: acctest.ErrorCheck(t, athena.EndpointsID), @@ -102,9 +139,9 @@ func TestAccAthenaDatabase_destroyFailsIfTablesExist(t *testing.T) { CheckDestroy: testAccCheckDatabaseDestroy, Steps: []resource.TestStep{ { - Config: testAccAthenaDatabaseConfig(rInt, dbName, false), + Config: testAccAthenaDatabaseConfig(rName, dbName, false), Check: resource.ComposeTestCheckFunc( - testAccCheckDatabaseExists("aws_athena_database.hoge"), + testAccCheckDatabaseExists("aws_athena_database.test"), testAccDatabaseCreateTables(dbName), testAccCheckDatabaseDropFails(dbName), testAccDatabaseDestroyTables(dbName), @@ -115,8 +152,9 @@ func TestAccAthenaDatabase_destroyFailsIfTablesExist(t *testing.T) { } func TestAccAthenaDatabase_forceDestroyAlwaysSucceeds(t *testing.T) { - rInt := sdkacctest.RandInt() + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) dbName := sdkacctest.RandString(8) + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ErrorCheck: acctest.ErrorCheck(t, athena.EndpointsID), @@ -124,9 +162,9 @@ func TestAccAthenaDatabase_forceDestroyAlwaysSucceeds(t *testing.T) { CheckDestroy: testAccCheckDatabaseDestroy, Steps: []resource.TestStep{ { - Config: testAccAthenaDatabaseConfig(rInt, dbName, true), + Config: testAccAthenaDatabaseConfig(rName, dbName, true), Check: resource.ComposeTestCheckFunc( - testAccCheckDatabaseExists("aws_athena_database.hoge"), + testAccCheckDatabaseExists("aws_athena_database.test"), testAccDatabaseCreateTables(dbName), ), }, @@ -134,88 +172,99 @@ func TestAccAthenaDatabase_forceDestroyAlwaysSucceeds(t *testing.T) { }) } -// StartQueryExecution requires OutputLocation but terraform destroy deleted S3 bucket as well. -// So temporary S3 bucket as OutputLocation is created to confirm whether the database is actually deleted. +func TestAccAthenaDatabase_description(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + dbName := sdkacctest.RandString(8) + resourceName := "aws_athena_database.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, athena.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAthenaDatabaseCommentConfig(rName, dbName, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckDatabaseExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", dbName), + ), + }, + }, + }) +} + +func TestAccAthenaDatabase_unescaped_description(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + dbName := sdkacctest.RandString(8) + resourceName := "aws_athena_database.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, athena.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAthenaDatabaseUnescapedCommentConfig(rName, dbName, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckDatabaseExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", dbName), + ), + }, + }, + }) +} + +func TestAccAthenaDatabase_disppears(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + dbName := sdkacctest.RandString(8) + + resourceName := "aws_athena_database.test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, athena.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAthenaDatabaseConfig(rName, dbName, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckDatabaseExists(resourceName), + acctest.CheckResourceDisappears(acctest.Provider, tfathena.ResourceDatabase(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + func testAccCheckDatabaseDestroy(s *terraform.State) error { - athenaconn := acctest.Provider.Meta().(*conns.AWSClient).AthenaConn - s3conn := acctest.Provider.Meta().(*conns.AWSClient).S3Conn + conn := acctest.Provider.Meta().(*conns.AWSClient).AthenaConn for _, rs := range s.RootModule().Resources { if rs.Type != "aws_athena_database" { continue } - rInt := sdkacctest.RandInt() - bucketName := fmt.Sprintf("tf-test-athena-db-%d", rInt) - _, err := s3conn.CreateBucket(&s3.CreateBucketInput{ - Bucket: aws.String(bucketName), - }) - if err != nil { - return err + input := &athena.ListDatabasesInput{ + CatalogName: aws.String("AwsDataCatalog"), } - input := &athena.StartQueryExecutionInput{ - QueryString: aws.String("show databases;"), - ResultConfiguration: &athena.ResultConfiguration{ - OutputLocation: aws.String("s3://" + bucketName), - }, - } - - resp, err := athenaconn.StartQueryExecution(input) + res, err := conn.ListDatabases(input) if err != nil { return err } - ers, err := tfathena.QueryExecutionResult(*resp.QueryExecutionId, athenaconn) - if err != nil { - return err - } - found := false - dbName := rs.Primary.Attributes["name"] - for _, row := range ers.Rows { - for _, datum := range row.Data { - if *datum.VarCharValue == dbName { - found = true - } + var database *athena.Database + for _, db := range res.DatabaseList { + if aws.StringValue(db.Name) == rs.Primary.ID { + database = db + break } } - if found { - return fmt.Errorf("[DELETE ERROR] Athena failed to drop database: %s", dbName) - } - loresp, err := s3conn.ListObjectsV2( - &s3.ListObjectsV2Input{ - Bucket: aws.String(bucketName), - }, - ) - if err != nil { - return fmt.Errorf("[DELETE ERROR] S3 Bucket list Objects err: %s", err) - } - - objectsToDelete := make([]*s3.ObjectIdentifier, 0) - - if len(loresp.Contents) != 0 { - for _, v := range loresp.Contents { - objectsToDelete = append(objectsToDelete, &s3.ObjectIdentifier{ - Key: v.Key, - }) - } - } - - _, err = s3conn.DeleteObjects(&s3.DeleteObjectsInput{ - Bucket: aws.String(bucketName), - Delete: &s3.Delete{ - Objects: objectsToDelete, - }, - }) - if err != nil { - return fmt.Errorf("[DELETE ERROR] S3 Bucket delete Objects err: %s", err) - } - - _, err = s3conn.DeleteBucket(&s3.DeleteBucketInput{ - Bucket: aws.String(bucketName), - }) - if err != nil { - return fmt.Errorf("[DELETE ERROR] S3 Bucket delete Bucket err: %s", err) + if database != nil { + return fmt.Errorf("Athena database (%s) still exists", rs.Primary.ID) } } @@ -339,55 +388,107 @@ func testAccAthenaDatabaseFindBucketName(s *terraform.State, dbName string) (buc return bucket, err } -func testAccAthenaDatabaseConfig(randInt int, dbName string, forceDestroy bool) string { +func testAccAthenaDatabaseConfig(rName string, dbName string, forceDestroy bool) string { return fmt.Sprintf(` -resource "aws_s3_bucket" "hoge" { - bucket = "tf-test-athena-db-%[1]d" +resource "aws_s3_bucket" "test" { + bucket = %[1]q force_destroy = true } -resource "aws_athena_database" "hoge" { - name = "%[2]s" - bucket = aws_s3_bucket.hoge.bucket +resource "aws_athena_database" "test" { + name = %[2]q + bucket = aws_s3_bucket.test.bucket force_destroy = %[3]t } -`, randInt, dbName, forceDestroy) +`, rName, dbName, forceDestroy) } -func testAccAthenaDatabaseWithKMSConfig(randInt int, dbName string, forceDestroy bool) string { +func testAccAthenaDatabaseAclConfig(rName string, dbName string, forceDestroy bool) string { return fmt.Sprintf(` -resource "aws_kms_key" "hoge" { +resource "aws_s3_bucket" "test" { + bucket = %[1]q + force_destroy = true +} + +resource "aws_athena_database" "test" { + name = %[2]q + bucket = aws_s3_bucket.test.bucket + force_destroy = %[3]t + + acl_configuration { + s3_acl_option = "BUCKET_OWNER_FULL_CONTROL" + } +} +`, rName, dbName, forceDestroy) +} + +func testAccAthenaDatabaseWithKMSConfig(rName string, dbName string, forceDestroy bool) string { + return fmt.Sprintf(` +resource "aws_kms_key" "test" { deletion_window_in_days = 10 + description = %[1]q } -resource "aws_s3_bucket" "hoge" { - bucket = "tf-test-athena-db-%[1]d" +resource "aws_s3_bucket" "test" { + bucket = %[1]q force_destroy = true } resource "aws_s3_bucket_server_side_encryption_configuration" "test" { - bucket = aws_s3_bucket.hoge.id + bucket = aws_s3_bucket.test.id rule { apply_server_side_encryption_by_default { - kms_master_key_id = aws_kms_key.hoge.arn + kms_master_key_id = aws_kms_key.test.arn sse_algorithm = "aws:kms" } } } -resource "aws_athena_database" "hoge" { +resource "aws_athena_database" "test" { # Must have bucket SSE enabled first depends_on = [aws_s3_bucket_server_side_encryption_configuration.test] - name = "%[2]s" - bucket = aws_s3_bucket.hoge.bucket + name = %[2]q + bucket = aws_s3_bucket.test.bucket force_destroy = %[3]t encryption_configuration { encryption_option = "SSE_KMS" - kms_key = aws_kms_key.hoge.arn + kms_key = aws_kms_key.test.arn } } -`, randInt, dbName, forceDestroy) +`, rName, dbName, forceDestroy) +} + +func testAccAthenaDatabaseCommentConfig(rName string, dbName string, forceDestroy bool) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q + force_destroy = true +} + +resource "aws_athena_database" "test" { + name = %[2]q + bucket = aws_s3_bucket.test.bucket + comment = "athena is a goddess" + force_destroy = %[3]t +} +`, rName, dbName, forceDestroy) +} + +func testAccAthenaDatabaseUnescapedCommentConfig(rName string, dbName string, forceDestroy bool) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q + force_destroy = true +} + +resource "aws_athena_database" "test" { + name = %[2]q + bucket = aws_s3_bucket.test.bucket + comment = "athena's a goddess" + force_destroy = %[3]t +} +`, rName, dbName, forceDestroy) } diff --git a/internal/service/athena/sweep.go b/internal/service/athena/sweep.go new file mode 100644 index 00000000000..fd250b80307 --- /dev/null +++ b/internal/service/athena/sweep.go @@ -0,0 +1,78 @@ +//go:build sweep +// +build sweep + +package athena + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/athena" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/sweep" +) + +func init() { + resource.AddTestSweepers("aws_athena_database", &resource.Sweeper{ + Name: "aws_athena_database", + F: sweepDatabases, + }) +} + +func sweepDatabases(region string) error { + client, err := sweep.SharedRegionalSweepClient(region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + conn := client.(*conns.AWSClient).AthenaConn + input := &athena.ListDatabasesInput{ + CatalogName: aws.String("AwsDataCatalog"), + } + var errs *multierror.Error + + sweepResources := make([]*sweep.SweepResource, 0) + for { + output, err := conn.ListDatabases(input) + + for _, v := range output.DatabaseList { + name := aws.StringValue(v.Name) + if name == "default" { + continue + } + r := ResourceDatabase() + d := r.Data(nil) + d.SetId(name) + + if err != nil { + err := fmt.Errorf("error listing Athena Databases (%s): %w", name, err) + log.Printf("[ERROR] %s", err) + errs = multierror.Append(errs, err) + continue + } + + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) + } + + if aws.StringValue(output.NextToken) == "" { + break + } + + input.NextToken = output.NextToken + } + + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping Athena Database sweep for %s: %s", region, err) + return nil + } + + err = sweep.SweepOrchestrator(sweepResources) + + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping Athena Databases (%s): %w", region, err)) + } + + return errs.ErrorOrNil() +} diff --git a/website/docs/r/athena_database.html.markdown b/website/docs/r/athena_database.html.markdown index 53ee0b6d92b..0585ef7066f 100644 --- a/website/docs/r/athena_database.html.markdown +++ b/website/docs/r/athena_database.html.markdown @@ -13,13 +13,13 @@ Provides an Athena database. ## Example Usage ```terraform -resource "aws_s3_bucket" "hoge" { - bucket = "hoge" +resource "aws_s3_bucket" "example" { + bucket = "example" } -resource "aws_athena_database" "hoge" { +resource "aws_athena_database" "example" { name = "database_name" - bucket = aws_s3_bucket.hoge.bucket + bucket = aws_s3_bucket.example.bucket } ``` @@ -27,18 +27,25 @@ resource "aws_athena_database" "hoge" { The following arguments are supported: +* `bucket` - (Required) Name of S3 bucket to save the results of the query execution. * `name` - (Required) Name of the database to create. -* `bucket` - (Required) Name of s3 bucket to save the results of the query execution. -* `encryption_configuration` - (Optional) The encryption key block AWS Athena uses to decrypt the data in S3, such as an AWS Key Management Service (AWS KMS) key. An `encryption_configuration` block is documented below. +* `acl_configuration` - (Optional) Indicates that an Amazon S3 canned ACL should be set to control ownership of stored query results. See [ACL Configuration](#acl-configuration) below. +* `comment` - (Optional) Description of the database. +* `encryption_configuration` - (Optional) The encryption key block AWS Athena uses to decrypt the data in S3, such as an AWS Key Management Service (AWS KMS) key. See [Encryption Configuration](#encryption-configuration) below. +* `expected_bucket_owner` - (Optional) The AWS account ID that you expect to be the owner of the Amazon S3 bucket. * `force_destroy` - (Optional, Default: false) A boolean that indicates all tables should be deleted from the database so that the database can be destroyed without error. The tables are *not* recoverable. -An `encryption_configuration` block supports the following arguments: +### ACL Configuration -* `encryption_option` - (Required) The type of key; one of `SSE_S3`, `SSE_KMS`, `CSE_KMS` -* `kms_key` - (Optional) The KMS key ARN or ID; required for key types `SSE_KMS` and `CSE_KMS`. +* `s3_acl_option` - (Required) The Amazon S3 canned ACL that Athena should specify when storing query results. Valid value is `BUCKET_OWNER_FULL_CONTROL`. ~> **NOTE:** When Athena queries are executed, result files may be created in the specified bucket. Consider using `force_destroy` on the bucket too in order to avoid any problems when destroying the bucket. +### Encryption Configuration + +* `encryption_option` - (Required) The type of key; one of `SSE_S3`, `SSE_KMS`, `CSE_KMS` +* `kms_key` - (Optional) The KMS key ARN or ID; required for key types `SSE_KMS` and `CSE_KMS`. + ## Attributes Reference In addition to all arguments above, the following attributes are exported: