From c82b217c9d9fdb5c200b91862517a37eb8e1f07e Mon Sep 17 00:00:00 2001 From: Paul Stack Date: Thu, 29 Jun 2017 20:31:12 +0300 Subject: [PATCH] Add support for DynamoDb as an AWS DMS target (#1002) * Added support for dynamodb as dms endpoint (with tests). For issue: (https://github.com/hashicorp/terraform/issues/14752) Signed-off-by: stack72 * resource/aws_dms_endpoint: Add retry to allow IAM to propagate Allows the IAM Role for DynamoDB to propagate for use in DMS Endpoint --- aws/resource_aws_dms_endpoint.go | 84 ++++++++--- aws/resource_aws_dms_endpoint_test.go | 165 +++++++++++++++++++++- website/docs/r/dms_endpoint.html.markdown | 10 +- 3 files changed, 230 insertions(+), 29 deletions(-) diff --git a/aws/resource_aws_dms_endpoint.go b/aws/resource_aws_dms_endpoint.go index 586ed9f7c5c..2acaec6257e 100644 --- a/aws/resource_aws_dms_endpoint.go +++ b/aws/resource_aws_dms_endpoint.go @@ -3,10 +3,12 @@ package aws import ( "log" "strings" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" dms "github.com/aws/aws-sdk-go/service/databasemigrationservice" + "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/validation" ) @@ -43,6 +45,10 @@ func resourceAwsDmsEndpoint() *schema.Resource { ForceNew: true, ValidateFunc: validateDmsEndpointId, }, + "service_access_role": { + Type: schema.TypeString, + Optional: true, + }, "endpoint_type": { Type: schema.TypeString, Required: true, @@ -58,6 +64,7 @@ func resourceAwsDmsEndpoint() *schema.Resource { "mysql", "oracle", "postgres", + "dynamodb", "mariadb", "aurora", "redshift", @@ -79,16 +86,16 @@ func resourceAwsDmsEndpoint() *schema.Resource { }, "password": { Type: schema.TypeString, - Required: true, + Optional: true, Sensitive: true, }, "port": { Type: schema.TypeInt, - Required: true, + Optional: true, }, "server_name": { Type: schema.TypeString, - Required: true, + Optional: true, }, "ssl_mode": { Type: schema.TypeString, @@ -107,7 +114,7 @@ func resourceAwsDmsEndpoint() *schema.Resource { }, "username": { Type: schema.TypeString, - Required: true, + Optional: true, }, }, } @@ -120,22 +127,31 @@ func resourceAwsDmsEndpointCreate(d *schema.ResourceData, meta interface{}) erro EndpointIdentifier: aws.String(d.Get("endpoint_id").(string)), EndpointType: aws.String(d.Get("endpoint_type").(string)), EngineName: aws.String(d.Get("engine_name").(string)), - Password: aws.String(d.Get("password").(string)), - Port: aws.Int64(int64(d.Get("port").(int))), - ServerName: aws.String(d.Get("server_name").(string)), Tags: dmsTagsFromMap(d.Get("tags").(map[string]interface{})), - Username: aws.String(d.Get("username").(string)), } - if v, ok := d.GetOk("database_name"); ok { - request.DatabaseName = aws.String(v.(string)) + // if dynamodb then add required params + if d.Get("engine_name").(string) == "dynamodb" { + request.DynamoDbSettings = &dms.DynamoDbSettings{ + ServiceAccessRoleArn: aws.String(d.Get("service_access_role").(string)), + } + } else { + request.Password = aws.String(d.Get("password").(string)) + request.Port = aws.Int64(int64(d.Get("port").(int))) + request.ServerName = aws.String(d.Get("server_name").(string)) + request.Username = aws.String(d.Get("username").(string)) + + if v, ok := d.GetOk("database_name"); ok { + request.DatabaseName = aws.String(v.(string)) + } + if v, ok := d.GetOk("extra_connection_attributes"); ok { + request.ExtraConnectionAttributes = aws.String(v.(string)) + } } + if v, ok := d.GetOk("certificate_arn"); ok { request.CertificateArn = aws.String(v.(string)) } - if v, ok := d.GetOk("extra_connection_attributes"); ok { - request.ExtraConnectionAttributes = aws.String(v.(string)) - } if v, ok := d.GetOk("kms_key_arn"); ok { request.KmsKeyId = aws.String(v.(string)) } @@ -145,7 +161,20 @@ func resourceAwsDmsEndpointCreate(d *schema.ResourceData, meta interface{}) erro log.Println("[DEBUG] DMS create endpoint:", request) - _, err := conn.CreateEndpoint(request) + err := resource.Retry(5*time.Minute, func() *resource.RetryError { + if _, err := conn.CreateEndpoint(request); err != nil { + if awserr, ok := err.(awserr.Error); ok { + switch awserr.Code() { + case "AccessDeniedFault": + return resource.RetryableError(awserr) + } + } + // Didn't recognize the error, so shouldn't retry. + return resource.NonRetryableError(err) + } + // Successful delete + return nil + }) if err != nil { return err } @@ -208,6 +237,13 @@ func resourceAwsDmsEndpointUpdate(d *schema.ResourceData, meta interface{}) erro hasChanges = true } + if d.HasChange("service_access_role") { + request.DynamoDbSettings = &dms.DynamoDbSettings{ + ServiceAccessRoleArn: aws.String(d.Get("service_access_role").(string)), + } + hasChanges = true + } + if d.HasChange("endpoint_type") { request.EndpointType = aws.String(d.Get("endpoint_type").(string)) hasChanges = true @@ -290,18 +326,28 @@ func resourceAwsDmsEndpointSetState(d *schema.ResourceData, endpoint *dms.Endpoi d.SetId(*endpoint.EndpointIdentifier) d.Set("certificate_arn", endpoint.CertificateArn) - d.Set("database_name", endpoint.DatabaseName) d.Set("endpoint_arn", endpoint.EndpointArn) d.Set("endpoint_id", endpoint.EndpointIdentifier) // For some reason the AWS API only accepts lowercase type but returns it as uppercase d.Set("endpoint_type", strings.ToLower(*endpoint.EndpointType)) d.Set("engine_name", endpoint.EngineName) - d.Set("extra_connection_attributes", endpoint.ExtraConnectionAttributes) + + if *endpoint.EngineName == "dynamodb" { + if endpoint.DynamoDbSettings != nil { + d.Set("service_access_role", endpoint.DynamoDbSettings.ServiceAccessRoleArn) + } else { + d.Set("service_access_role", "") + } + } else { + d.Set("database_name", endpoint.DatabaseName) + d.Set("extra_connection_attributes", endpoint.ExtraConnectionAttributes) + d.Set("port", endpoint.Port) + d.Set("server_name", endpoint.ServerName) + d.Set("username", endpoint.Username) + } + d.Set("kms_key_arn", endpoint.KmsKeyId) - d.Set("port", endpoint.Port) - d.Set("server_name", endpoint.ServerName) d.Set("ssl_mode", endpoint.SslMode) - d.Set("username", endpoint.Username) return nil } diff --git a/aws/resource_aws_dms_endpoint_test.go b/aws/resource_aws_dms_endpoint_test.go index 59c3d87c77b..4fc5d02fb59 100644 --- a/aws/resource_aws_dms_endpoint_test.go +++ b/aws/resource_aws_dms_endpoint_test.go @@ -13,7 +13,7 @@ import ( func TestAccAwsDmsEndpointBasic(t *testing.T) { resourceName := "aws_dms_endpoint.dms_endpoint" - randId := acctest.RandString(8) + randId := acctest.RandString(8) + "-basic" resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -21,7 +21,7 @@ func TestAccAwsDmsEndpointBasic(t *testing.T) { CheckDestroy: dmsEndpointDestroy, Steps: []resource.TestStep{ { - Config: dmsEndpointConfig(randId), + Config: dmsEndpointBasicConfig(randId), Check: resource.ComposeTestCheckFunc( checkDmsEndpointExists(resourceName), resource.TestCheckResourceAttrSet(resourceName, "endpoint_arn"), @@ -34,7 +34,7 @@ func TestAccAwsDmsEndpointBasic(t *testing.T) { ImportStateVerifyIgnore: []string{"password"}, }, { - Config: dmsEndpointConfigUpdate(randId), + Config: dmsEndpointBasicConfigUpdate(randId), Check: resource.ComposeTestCheckFunc( checkDmsEndpointExists(resourceName), resource.TestCheckResourceAttr(resourceName, "database_name", "tf-test-dms-db-updated"), @@ -50,6 +50,38 @@ func TestAccAwsDmsEndpointBasic(t *testing.T) { }) } +func TestAccAwsDmsEndpointDynamoDb(t *testing.T) { + resourceName := "aws_dms_endpoint.dms_endpoint" + randId := acctest.RandString(8) + "-dynamodb" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: dmsEndpointDestroy, + Steps: []resource.TestStep{ + { + Config: dmsEndpointDynamoDbConfig(randId), + Check: resource.ComposeTestCheckFunc( + checkDmsEndpointExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "endpoint_arn"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"password"}, + }, + { + Config: dmsEndpointDynamoDbConfigUpdate(randId), + Check: resource.ComposeTestCheckFunc( + checkDmsEndpointExists(resourceName), + ), + }, + }, + }) +} + func dmsEndpointDestroy(s *terraform.State) error { for _, rs := range s.RootModule().Resources { if rs.Type != "aws_dms_endpoint" { @@ -98,7 +130,7 @@ func checkDmsEndpointExists(n string) resource.TestCheckFunc { } } -func dmsEndpointConfig(randId string) string { +func dmsEndpointBasicConfig(randId string) string { return fmt.Sprintf(` resource "aws_dms_endpoint" "dms_endpoint" { database_name = "tf-test-dms-db" @@ -120,7 +152,7 @@ resource "aws_dms_endpoint" "dms_endpoint" { `, randId) } -func dmsEndpointConfigUpdate(randId string) string { +func dmsEndpointBasicConfigUpdate(randId string) string { return fmt.Sprintf(` resource "aws_dms_endpoint" "dms_endpoint" { database_name = "tf-test-dms-db-updated" @@ -141,3 +173,126 @@ resource "aws_dms_endpoint" "dms_endpoint" { } `, randId) } + +func dmsEndpointDynamoDbConfig(randId string) string { + return fmt.Sprintf(` +resource "aws_dms_endpoint" "dms_endpoint" { + endpoint_id = "tf-test-dms-endpoint-%[1]s" + endpoint_type = "target" + engine_name = "dynamodb" + service_access_role = "${aws_iam_role.iam_role.arn}" + ssl_mode = "none" + tags { + Name = "tf-test-dynamodb-endpoint-%[1]s" + Update = "to-update" + Remove = "to-remove" + } + + depends_on = ["aws_iam_role_policy.dms_dynamodb_access"] +} +resource "aws_iam_role" "iam_role" { + name = "tf-test-iam-dynamodb-role-%[1]s" + + assume_role_policy = <