From 152df05070b6644196ad6acc5a92897c05114692 Mon Sep 17 00:00:00 2001 From: Atsushi Ishibashi Date: Tue, 17 Oct 2017 13:37:08 +0900 Subject: [PATCH 1/6] Make files --- aws/config.go | 3 +++ aws/provider.go | 1 + aws/resource_aws_athena_database.go | 30 ++++++++++++++++++++++++ aws/resource_aws_athena_database_test.go | 7 ++++++ 4 files changed, 41 insertions(+) create mode 100644 aws/resource_aws_athena_database.go create mode 100644 aws/resource_aws_athena_database_test.go diff --git a/aws/config.go b/aws/config.go index 3e1aa9f3f0d5..d6d7d266830c 100644 --- a/aws/config.go +++ b/aws/config.go @@ -17,6 +17,7 @@ import ( "github.com/aws/aws-sdk-go/service/acm" "github.com/aws/aws-sdk-go/service/apigateway" "github.com/aws/aws-sdk-go/service/applicationautoscaling" + "github.com/aws/aws-sdk-go/service/athena" "github.com/aws/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/service/batch" "github.com/aws/aws-sdk-go/service/cloudformation" @@ -178,6 +179,7 @@ type AWSClient struct { wafregionalconn *wafregional.WAFRegional iotconn *iot.IoT batchconn *batch.Batch + athenaconn *athena.Athena } func (c *AWSClient) S3() *s3.S3 { @@ -387,6 +389,7 @@ func (c *Config) Client() (interface{}, error) { client.wafconn = waf.New(sess) client.wafregionalconn = wafregional.New(sess) client.batchconn = batch.New(sess) + client.athenaconn = athena.New(sess) // Workaround for https://github.com/aws/aws-sdk-go/issues/1376 client.kinesisconn.Handlers.Retry.PushBack(func(r *request.Request) { diff --git a/aws/provider.go b/aws/provider.go index fea7bceb456d..44f8dd2ca1b4 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -256,6 +256,7 @@ func Provider() terraform.ResourceProvider { "aws_app_cookie_stickiness_policy": resourceAwsAppCookieStickinessPolicy(), "aws_appautoscaling_target": resourceAwsAppautoscalingTarget(), "aws_appautoscaling_policy": resourceAwsAppautoscalingPolicy(), + "aws_athena_database": resourceAwsAthenaDatabase(), "aws_autoscaling_attachment": resourceAwsAutoscalingAttachment(), "aws_autoscaling_group": resourceAwsAutoscalingGroup(), "aws_autoscaling_notification": resourceAwsAutoscalingNotification(), diff --git a/aws/resource_aws_athena_database.go b/aws/resource_aws_athena_database.go new file mode 100644 index 000000000000..60e3bf235a69 --- /dev/null +++ b/aws/resource_aws_athena_database.go @@ -0,0 +1,30 @@ +package aws + +import "github.com/hashicorp/terraform/helper/schema" + +func resourceAwsAthenaDatabase() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAthenaDatabaseCreate, + Read: resourceAwsAthenaDatabaseRead, + Update: resourceAwsAthenaDatabaseUpdate, + Delete: resourceAwsAthenaDatabaseDelete, + + Schema: map[string]*schema.Schema{}, + } +} + +func resourceAwsAthenaDatabaseCreate(d *schema.ResourceData, meta interface{}) error { + return nil +} + +func resourceAwsAthenaDatabaseRead(d *schema.ResourceData, meta interface{}) error { + return nil +} + +func resourceAwsAthenaDatabaseUpdate(d *schema.ResourceData, meta interface{}) error { + return nil +} + +func resourceAwsAthenaDatabaseDelete(d *schema.ResourceData, meta interface{}) error { + return nil +} diff --git a/aws/resource_aws_athena_database_test.go b/aws/resource_aws_athena_database_test.go new file mode 100644 index 000000000000..3bae2459067a --- /dev/null +++ b/aws/resource_aws_athena_database_test.go @@ -0,0 +1,7 @@ +package aws + +import "testing" + +func TestAccAWSAthenaDatabase(t *testing.T) { + +} From 0c513f3958aab8201f5cb717cc7d4a5b59de1be1 Mon Sep 17 00:00:00 2001 From: Atsushi Ishibashi Date: Sun, 22 Oct 2017 06:27:16 +0900 Subject: [PATCH 2/6] wip --- aws/resource_aws_athena_database.go | 104 +++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 2 deletions(-) diff --git a/aws/resource_aws_athena_database.go b/aws/resource_aws_athena_database.go index 60e3bf235a69..c7545dafdd0f 100644 --- a/aws/resource_aws_athena_database.go +++ b/aws/resource_aws_athena_database.go @@ -1,6 +1,15 @@ package aws -import "github.com/hashicorp/terraform/helper/schema" +import ( + "fmt" + "io/ioutil" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/athena" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) func resourceAwsAthenaDatabase() *schema.Resource { return &schema.Resource{ @@ -9,11 +18,67 @@ func resourceAwsAthenaDatabase() *schema.Resource { Update: resourceAwsAthenaDatabaseUpdate, Delete: resourceAwsAthenaDatabaseDelete, - Schema: map[string]*schema.Schema{}, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "bucket": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "region": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, } } func resourceAwsAthenaDatabaseCreate(d *schema.ResourceData, meta interface{}) error { + athenaconn := meta.(*AWSClient).athenaconn + s3conn := meta.(*AWSClient).s3conn + + var bucket string + if val, ok := d.GetOk("bucket"); ok { + bucket = val.(string) + } else { + bucket = resource.UniqueId() + } + d.Set("bucket", bucket) + var awsRegion string + if val, ok := d.GetOk("region"); ok { + awsRegion = val.(string) + } else { + awsRegion = meta.(*AWSClient).region + } + + s3input := &s3.CreateBucketInput{ + Bucket: aws.String(bucket), + CreateBucketConfiguration: &s3.CreateBucketConfiguration{ + LocationConstraint: aws.String(awsRegion), + }, + } + + s3resp, err := s3conn.CreateBucket(s3input) + if err != nil { + return err + } + + athenainput := &athena.StartQueryExecutionInput{ + QueryString: aws.String(createDatabaseQueryString(d.Get("name").(string))), + ResultConfiguration: &athena.ResultConfiguration{ + OutputLocation: aws.String("s3://" + bucket), + }, + } + + athenaresp, err := athenaconn.StartQueryExecution(athenainput) + if err != nil { + return err + } return nil } @@ -28,3 +93,38 @@ func resourceAwsAthenaDatabaseUpdate(d *schema.ResourceData, meta interface{}) e func resourceAwsAthenaDatabaseDelete(d *schema.ResourceData, meta interface{}) error { return nil } + +func createDatabaseQueryString(databaseName string) string { + return fmt.Sprintf("create database %s;", databaseName) +} + +func checkCreateDatabaseQueryExecution(qeid string) error { + + return nil +} + +func showDatabaseQueryString(databaseName string) string { + return fmt.Sprint("show databases;") +} + +func checkShowDatabaseQueryExecution(qeid string) error { + return nil +} + +func dropDatabaseQueryString(databaseName string) string { + return fmt.Sprintf("drop database %s;", databaseName) +} + +func queryExecutionBody(qeid, bucket string, meta interface{}) ([]byte, error) { + s3conn := meta.(*AWSClient).s3conn + + input := &s3.GetObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(qeid + ".txt"), + } + resp, err := s3conn.GetObject(input) + if err != nil { + return nil, err + } + return ioutil.ReadAll(resp.Body) +} From 2cd4c28f1b600597e7d655a2182de35748f7202a Mon Sep 17 00:00:00 2001 From: Atsushi Ishibashi Date: Mon, 23 Oct 2017 15:51:25 +0900 Subject: [PATCH 3/6] implement CRD methods --- aws/resource_aws_athena_database.go | 174 ++++++++++++++++------- aws/resource_aws_athena_database_test.go | 141 +++++++++++++++++- 2 files changed, 266 insertions(+), 49 deletions(-) diff --git a/aws/resource_aws_athena_database.go b/aws/resource_aws_athena_database.go index c7545dafdd0f..1b30c4b16bc7 100644 --- a/aws/resource_aws_athena_database.go +++ b/aws/resource_aws_athena_database.go @@ -2,11 +2,12 @@ package aws import ( "fmt" - "io/ioutil" + "log" + "strings" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/athena" - "github.com/aws/aws-sdk-go/service/s3" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" ) @@ -15,74 +16,65 @@ func resourceAwsAthenaDatabase() *schema.Resource { return &schema.Resource{ Create: resourceAwsAthenaDatabaseCreate, Read: resourceAwsAthenaDatabaseRead, - Update: resourceAwsAthenaDatabaseUpdate, + //Update: resourceAwsAthenaDatabaseUpdate, Delete: resourceAwsAthenaDatabaseDelete, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, + ForceNew: true, }, "bucket": { Type: schema.TypeString, - Optional: true, - Computed: true, + Required: true, ForceNew: true, }, - "region": { - Type: schema.TypeString, - Optional: true, - Computed: true, - }, }, } } func resourceAwsAthenaDatabaseCreate(d *schema.ResourceData, meta interface{}) error { athenaconn := meta.(*AWSClient).athenaconn - s3conn := meta.(*AWSClient).s3conn - - var bucket string - if val, ok := d.GetOk("bucket"); ok { - bucket = val.(string) - } else { - bucket = resource.UniqueId() - } - d.Set("bucket", bucket) - var awsRegion string - if val, ok := d.GetOk("region"); ok { - awsRegion = val.(string) - } else { - awsRegion = meta.(*AWSClient).region - } - - s3input := &s3.CreateBucketInput{ - Bucket: aws.String(bucket), - CreateBucketConfiguration: &s3.CreateBucketConfiguration{ - LocationConstraint: aws.String(awsRegion), + + athenainput := &athena.StartQueryExecutionInput{ + QueryString: aws.String(createDatabaseQueryString(d.Get("name").(string))), + ResultConfiguration: &athena.ResultConfiguration{ + OutputLocation: aws.String("s3://" + d.Get("bucket").(string)), }, } - s3resp, err := s3conn.CreateBucket(s3input) + athenaresp, err := athenaconn.StartQueryExecution(athenainput) if err != nil { return err } - athenainput := &athena.StartQueryExecutionInput{ - QueryString: aws.String(createDatabaseQueryString(d.Get("name").(string))), + if err := checkCreateDatabaseQueryExecution(*athenaresp.QueryExecutionId, d, meta); err != nil { + return err + } + d.SetId(d.Get("name").(string)) + return resourceAwsAthenaDatabaseRead(d, meta) +} + +func resourceAwsAthenaDatabaseRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).athenaconn + + bucket := d.Get("bucket").(string) + input := &athena.StartQueryExecutionInput{ + QueryString: aws.String(showDatabaseQueryString()), ResultConfiguration: &athena.ResultConfiguration{ OutputLocation: aws.String("s3://" + bucket), }, } - athenaresp, err := athenaconn.StartQueryExecution(athenainput) + resp, err := conn.StartQueryExecution(input) if err != nil { return err } - return nil -} -func resourceAwsAthenaDatabaseRead(d *schema.ResourceData, meta interface{}) error { + if err := checkShowDatabaseQueryExecution(*resp.QueryExecutionId, d, meta); err != nil { + return err + } return nil } @@ -91,6 +83,24 @@ func resourceAwsAthenaDatabaseUpdate(d *schema.ResourceData, meta interface{}) e } func resourceAwsAthenaDatabaseDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).athenaconn + + bucket := d.Get("bucket").(string) + input := &athena.StartQueryExecutionInput{ + QueryString: aws.String(dropDatabaseQueryString(d.Get("name").(string))), + ResultConfiguration: &athena.ResultConfiguration{ + OutputLocation: aws.String("s3://" + bucket), + }, + } + + resp, err := conn.StartQueryExecution(input) + if err != nil { + return err + } + + if err := checkDropDatabaseQueryExecution(*resp.QueryExecutionId, d, meta); err != nil { + return err + } return nil } @@ -98,16 +108,38 @@ func createDatabaseQueryString(databaseName string) string { return fmt.Sprintf("create database %s;", databaseName) } -func checkCreateDatabaseQueryExecution(qeid string) error { - +func checkCreateDatabaseQueryExecution(qeid string, d *schema.ResourceData, meta interface{}) error { + rs, err := queryExecutionResult(qeid, meta) + if err != nil { + return err + } + if len(rs.Rows) != 0 { + return fmt.Errorf("[ERROR] Athena create database, unexpected query result: %s", flattenAthenaResultSet(rs)) + } return nil } -func showDatabaseQueryString(databaseName string) string { +func showDatabaseQueryString() string { return fmt.Sprint("show databases;") } -func checkShowDatabaseQueryExecution(qeid string) error { +func checkShowDatabaseQueryExecution(qeid string, d *schema.ResourceData, meta interface{}) error { + rs, err := queryExecutionResult(qeid, meta) + if err != nil { + return err + } + found := false + dbName := d.Get("name").(string) + for _, row := range rs.Rows { + for _, datum := range row.Data { + if *datum.VarCharValue == dbName { + found = true + } + } + } + if !found { + return fmt.Errorf("[ERROR] Athena not found database: %s, query result: %s", dbName, flattenAthenaResultSet(rs)) + } return nil } @@ -115,16 +147,62 @@ func dropDatabaseQueryString(databaseName string) string { return fmt.Sprintf("drop database %s;", databaseName) } -func queryExecutionBody(qeid, bucket string, meta interface{}) ([]byte, error) { - s3conn := meta.(*AWSClient).s3conn +func checkDropDatabaseQueryExecution(qeid string, d *schema.ResourceData, meta interface{}) error { + rs, err := queryExecutionResult(qeid, meta) + if err != nil { + return err + } + if len(rs.Rows) != 0 { + return fmt.Errorf("[ERROR] Athena drop database, unexpected query result: %s", flattenAthenaResultSet(rs)) + } + return nil +} - input := &s3.GetObjectInput{ - Bucket: aws.String(bucket), - Key: aws.String(qeid + ".txt"), +func queryExecutionResult(qeid string, meta interface{}) (*athena.ResultSet, error) { + conn := meta.(*AWSClient).athenaconn + + input := &athena.GetQueryExecutionInput{ + QueryExecutionId: aws.String(qeid), } - resp, err := s3conn.GetObject(input) + err := resource.Retry(5*time.Minute, func() *resource.RetryError { + out, err := conn.GetQueryExecution(input) + if err != nil { + return resource.NonRetryableError(err) + } + switch *out.QueryExecution.Status.State { + case athena.QueryExecutionStateQueued, athena.QueryExecutionStateRunning: + log.Printf("[DEBUG] Executing Athena Query...") + return resource.RetryableError(nil) + case athena.QueryExecutionStateSucceeded: + return nil + case athena.QueryExecutionStateFailed: + return resource.NonRetryableError(fmt.Errorf("[Error] QueryExecution Failed")) + case athena.QueryExecutionStateCancelled: + return resource.NonRetryableError(fmt.Errorf("[Error] QueryExecution Canceled")) + default: + return resource.NonRetryableError(fmt.Errorf("[Error] Unexpected QueryExecution State: %s", *out.QueryExecution.Status.State)) + } + }) if err != nil { return nil, err } - return ioutil.ReadAll(resp.Body) + + qrinput := &athena.GetQueryResultsInput{ + QueryExecutionId: aws.String(qeid), + } + resp, err := conn.GetQueryResults(qrinput) + if err != nil { + return nil, err + } + return resp.ResultSet, nil +} + +func flattenAthenaResultSet(rs *athena.ResultSet) string { + ss := make([]string, 0) + for _, row := range rs.Rows { + for _, datum := range row.Data { + ss = append(ss, *datum.VarCharValue) + } + } + return strings.Join(ss, "\n") } diff --git a/aws/resource_aws_athena_database_test.go b/aws/resource_aws_athena_database_test.go index 3bae2459067a..1b428ae08028 100644 --- a/aws/resource_aws_athena_database_test.go +++ b/aws/resource_aws_athena_database_test.go @@ -1,7 +1,146 @@ package aws -import "testing" +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/athena" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) func TestAccAWSAthenaDatabase(t *testing.T) { + rInt := acctest.RandInt() + dbName := acctest.RandString(8) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAthenaDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAthenaDatabaseConfig(rInt, dbName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAthenaDatabaseExists("aws_athena_database.hoge"), + ), + }, + }, + }) +} + +func testAccCheckAWSAthenaDatabaseDestroy(s *terraform.State) error { + athenaconn := testAccProvider.Meta().(*AWSClient).athenaconn + s3conn := testAccProvider.Meta().(*AWSClient).s3conn + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_athena_database" { + continue + } + + rInt := acctest.RandInt() + bucketName := fmt.Sprintf("tf-athena-db-%s-%d", rs.Primary.Attributes["name"], rInt) + _, err := s3conn.CreateBucket(&s3.CreateBucketInput{ + Bucket: aws.String(bucketName), + }) + if err != nil { + return err + } + defer func() { + + }() + + input := &athena.StartQueryExecutionInput{ + QueryString: aws.String(showDatabaseQueryString()), + ResultConfiguration: &athena.ResultConfiguration{ + OutputLocation: aws.String("s3://" + bucketName), + }, + } + + resp, err := athenaconn.StartQueryExecution(input) + if err != nil { + return err + } + + ers, err := queryExecutionResult(*resp.QueryExecutionId, testAccProvider.Meta()) + 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 + } + } + } + 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) + } + + } + return nil +} + +func testAccCheckAWSAthenaDatabaseExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s, %v", name, s.RootModule().Resources) + } + return nil + } +} + +func testAccAthenaDatabaseConfig(randInt int, dbName string) string { + return fmt.Sprintf(` + resource "aws_s3_bucket" "hoge" { + bucket = "tf-athena-db-%s-%d" + force_destroy = true + } + + resource "aws_athena_database" "hoge" { + name = "%s" + bucket = "${aws_s3_bucket.hoge.bucket}" + depends_on = ["aws_s3_bucket.hoge"] + } + `, dbName, randInt, dbName) } From d8c71cb69ae11b335d0bcd19a8d7df90deafab26 Mon Sep 17 00:00:00 2001 From: Atsushi Ishibashi Date: Mon, 23 Oct 2017 16:20:04 +0900 Subject: [PATCH 4/6] make docs --- website/aws.erb | 9 +++++ website/docs/r/athena_database.html.markdown | 39 ++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 website/docs/r/athena_database.html.markdown diff --git a/website/aws.erb b/website/aws.erb index d2cbf0efe199..9366a5c7f855 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -285,6 +285,15 @@ + > + Athena Resources + + + > Batch Resources