diff --git a/aws/provider.go b/aws/provider.go index d685093a706..794a278c8dd 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -466,6 +466,7 @@ func Provider() terraform.ResourceProvider { "aws_db_instance_role_association": resourceAwsDbInstanceRoleAssociation(), "aws_db_option_group": resourceAwsDbOptionGroup(), "aws_db_parameter_group": resourceAwsDbParameterGroup(), + "aws_db_proxy": resourceAwsDbProxy(), "aws_db_security_group": resourceAwsDbSecurityGroup(), "aws_db_snapshot": resourceAwsDbSnapshot(), "aws_db_subnet_group": resourceAwsDbSubnetGroup(), diff --git a/aws/resource_aws_db_proxy.go b/aws/resource_aws_db_proxy.go new file mode 100644 index 00000000000..4884a9a6ce0 --- /dev/null +++ b/aws/resource_aws_db_proxy.go @@ -0,0 +1,408 @@ +package aws + +import ( + "bytes" + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/rds" + "github.com/hashicorp/terraform-plugin-sdk/helper/hashcode" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" +) + +func resourceAwsDbProxy() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsDbProxyCreate, + Read: resourceAwsDbProxyRead, + Update: resourceAwsDbProxyUpdate, + Delete: resourceAwsDbProxyDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Update: schema.DefaultTimeout(30 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateRdsIdentifier, + }, + "debug_logging": { + Type: schema.TypeBool, + Optional: true, + }, + "engine_family": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + rds.EngineFamilyMysql, + "POSTGRESQL", + }, false), + }, + "idle_client_timeout": { + Type: schema.TypeInt, + Optional: true, + }, + "require_tls": { + Type: schema.TypeBool, + Optional: true, + }, + "role_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + "vpc_security_group_ids": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "vpc_subnet_ids": { + Type: schema.TypeSet, + Required: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "auth": { + Type: schema.TypeSet, + Required: true, + ForceNew: false, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "auth_scheme": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + rds.AuthSchemeSecrets, + }, false), + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "iam_auth": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + rds.IAMAuthModeDisabled, + rds.IAMAuthModeRequired, + }, false), + }, + "secret_arn": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateArn, + }, + "username": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + Set: resourceAwsDbProxyAuthHash, + }, + + "tags": tagsSchema(), + }, + } +} + +func resourceAwsDbProxyCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).rdsconn + tags := keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().RdsTags() + + params := rds.CreateDBProxyInput{ + Auth: expandDbProxyAuth(d.Get("auth").(*schema.Set).List()), + DBProxyName: aws.String(d.Get("name").(string)), + DebugLogging: aws.Bool(d.Get("debug_logging").(bool)), + EngineFamily: aws.String(d.Get("engine_family").(string)), + RequireTLS: aws.Bool(d.Get("require_tls").(bool)), + RoleArn: aws.String(d.Get("role_arn").(string)), + Tags: tags, + VpcSubnetIds: expandStringSet(d.Get("vpc_subnet_ids").(*schema.Set)), + } + + if v, ok := d.GetOk("debug_logging"); ok { + params.DebugLogging = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("idle_client_timeout"); ok { + params.IdleClientTimeout = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("require_tls"); ok { + params.RequireTLS = aws.Bool(v.(bool)) + } + + if v := d.Get("vpc_security_group_ids").(*schema.Set); v.Len() > 0 { + params.VpcSecurityGroupIds = expandStringSet(v) + } + + log.Printf("[DEBUG] Create DB Proxy: %#v", params) + resp, err := conn.CreateDBProxy(¶ms) + if err != nil { + return fmt.Errorf("Error creating DB Proxy: %s", err) + } + + d.SetId(aws.StringValue(resp.DBProxy.DBProxyName)) + d.Set("arn", resp.DBProxy.DBProxyArn) + log.Printf("[INFO] DB Proxy ID: %s", d.Id()) + + stateChangeConf := &resource.StateChangeConf{ + Pending: []string{rds.DBProxyStatusCreating}, + Target: []string{rds.DBProxyStatusAvailable}, + Refresh: resourceAwsDbProxyRefreshFunc(conn, d.Id()), + Timeout: d.Timeout(schema.TimeoutCreate), + } + + _, err = stateChangeConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for DB Proxy creation: %s", err) + } + + return resourceAwsDbProxyRead(d, meta) +} + +func resourceAwsDbProxyRefreshFunc(conn *rds.RDS, proxyName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + resp, err := conn.DescribeDBProxies(&rds.DescribeDBProxiesInput{ + DBProxyName: aws.String(proxyName), + }) + + if err != nil { + if isAWSErr(err, rds.ErrCodeDBProxyNotFoundFault, "") { + return 42, "", nil + } + return 42, "", err + } + + dbProxy := resp.DBProxies[0] + return dbProxy, *dbProxy.Status, nil + } +} + +func expandDbProxyAuth(l []interface{}) []*rds.UserAuthConfig { + if len(l) == 0 { + return nil + } + + userAuthConfigs := make([]*rds.UserAuthConfig, 0, len(l)) + + for _, mRaw := range l { + m, ok := mRaw.(map[string]interface{}) + + if !ok { + continue + } + + userAuthConfig := &rds.UserAuthConfig{} + + if v, ok := m["auth_scheme"].(string); ok && v != "" { + userAuthConfig.AuthScheme = aws.String(v) + } + + if v, ok := m["description"].(string); ok && v != "" { + userAuthConfig.Description = aws.String(v) + } + + if v, ok := m["iam_auth"].(string); ok && v != "" { + userAuthConfig.IAMAuth = aws.String(v) + } + + if v, ok := m["secret_arn"].(string); ok && v != "" { + userAuthConfig.SecretArn = aws.String(v) + } + + if v, ok := m["username"].(string); ok && v != "" { + userAuthConfig.UserName = aws.String(v) + } + + userAuthConfigs = append(userAuthConfigs, userAuthConfig) + } + + return userAuthConfigs +} + +func flattenDbProxyAuth(userAuthConfig *rds.UserAuthConfigInfo) map[string]interface{} { + m := make(map[string]interface{}) + + m["auth_scheme"] = aws.StringValue(userAuthConfig.AuthScheme) + m["description"] = aws.StringValue(userAuthConfig.Description) + m["iam_auth"] = aws.StringValue(userAuthConfig.IAMAuth) + m["secret_arn"] = aws.StringValue(userAuthConfig.SecretArn) + m["username"] = aws.StringValue(userAuthConfig.UserName) + + return m +} + +func flattenDbProxyAuths(userAuthConfigs []*rds.UserAuthConfigInfo) *schema.Set { + s := []interface{}{} + for _, v := range userAuthConfigs { + s = append(s, flattenDbProxyAuth(v)) + } + return schema.NewSet(resourceAwsDbProxyAuthHash, s) +} + +func resourceAwsDbProxyRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).rdsconn + + params := rds.DescribeDBProxiesInput{ + DBProxyName: aws.String(d.Id()), + } + + resp, err := conn.DescribeDBProxies(¶ms) + if err != nil { + if isAWSErr(err, rds.ErrCodeDBProxyNotFoundFault, "") { + log.Printf("[WARN] DB Proxy (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + return err + } + + if len(resp.DBProxies) != 1 || + *resp.DBProxies[0].DBProxyName != d.Id() { + return fmt.Errorf("Unable to find DB Proxy: %#v", resp.DBProxies) + } + + dbProxy := resp.DBProxies[0] + + d.Set("arn", aws.StringValue(dbProxy.DBProxyArn)) + d.Set("auth", flattenDbProxyAuths(dbProxy.Auth)) + d.Set("name", dbProxy.DBProxyName) + d.Set("debug_logging", dbProxy.DebugLogging) + d.Set("engine_family", dbProxy.EngineFamily) + d.Set("idle_client_timeout", dbProxy.IdleClientTimeout) + d.Set("require_tls", dbProxy.RequireTLS) + d.Set("role_arn", dbProxy.RoleArn) + d.Set("vpc_subnet_ids", flattenStringSet(dbProxy.VpcSubnetIds)) + d.Set("security_group_ids", flattenStringSet(dbProxy.VpcSecurityGroupIds)) + + tags, err := keyvaluetags.RdsListTags(conn, d.Get("arn").(string)) + + if err != nil { + return fmt.Errorf("Error listing tags for RDS DB Proxy (%s): %s", d.Get("arn").(string), err) + } + + if err := d.Set("tags", tags.IgnoreAws().Map()); err != nil { + return fmt.Errorf("Error setting tags: %s", err) + } + + return nil +} + +func resourceAwsDbProxyUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).rdsconn + + oName, nName := d.GetChange("name") + + params := rds.ModifyDBProxyInput{ + Auth: expandDbProxyAuth(d.Get("auth").(*schema.Set).List()), + DBProxyName: aws.String(oName.(string)), + NewDBProxyName: aws.String(nName.(string)), + DebugLogging: aws.Bool(d.Get("debug_logging").(bool)), + RequireTLS: aws.Bool(d.Get("require_tls").(bool)), + RoleArn: aws.String(d.Get("role_arn").(string)), + } + + if v, ok := d.GetOk("idle_client_timeout"); ok { + params.IdleClientTimeout = aws.Int64(int64(v.(int))) + } + + if v := d.Get("vpc_security_group_ids").(*schema.Set); v.Len() > 0 { + params.SecurityGroups = expandStringSet(v) + } + + log.Printf("[DEBUG] Update DB Proxy: %#v", params) + _, err := conn.ModifyDBProxy(¶ms) + if err != nil { + return fmt.Errorf("Error updating DB Proxy: %s", err) + } + + stateChangeConf := &resource.StateChangeConf{ + Pending: []string{rds.DBProxyStatusModifying}, + Target: []string{rds.DBProxyStatusAvailable}, + Refresh: resourceAwsDbProxyRefreshFunc(conn, d.Id()), + Timeout: d.Timeout(schema.TimeoutCreate), + } + + _, err = stateChangeConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for DB Proxy update: %s", err) + } + + if d.HasChange("tags") { + o, n := d.GetChange("tags") + + if err := keyvaluetags.RdsUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return fmt.Errorf("Error updating RDS DB Proxy (%s) tags: %s", d.Get("arn").(string), err) + } + } + + return resourceAwsDbProxyRead(d, meta) +} + +func resourceAwsDbProxyDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).rdsconn + + params := rds.DeleteDBProxyInput{ + DBProxyName: aws.String(d.Id()), + } + _, err := conn.DeleteDBProxy(¶ms) + if err != nil { + return fmt.Errorf("Error deleting DB Proxy: %s", err) + } + + stateChangeConf := &resource.StateChangeConf{ + Pending: []string{rds.DBProxyStatusDeleting}, + Target: []string{""}, + Refresh: resourceAwsDbProxyRefreshFunc(conn, d.Id()), + Timeout: d.Timeout(schema.TimeoutDelete), + } + + _, err = stateChangeConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for DB Proxy deletion: %s", err) + } + + return nil +} + +func resourceAwsDbProxyAuthHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + if v, ok := m["auth_scheme"].(string); ok { + buf.WriteString(fmt.Sprintf("%s-", v)) + } + if v, ok := m["description"].(string); ok { + buf.WriteString(fmt.Sprintf("%s-", v)) + } + if v, ok := m["iam_auth"].(string); ok { + buf.WriteString(fmt.Sprintf("%s-", v)) + } + if v, ok := m["secret_arn"].(string); ok { + buf.WriteString(fmt.Sprintf("%s-", v)) + } + if v, ok := m["username"].(string); ok { + buf.WriteString(fmt.Sprintf("%s-", v)) + } + return hashcode.String(buf.String()) +} diff --git a/aws/resource_aws_db_proxy_test.go b/aws/resource_aws_db_proxy_test.go new file mode 100644 index 00000000000..862a8275575 --- /dev/null +++ b/aws/resource_aws_db_proxy_test.go @@ -0,0 +1,275 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/rds" + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func init() { + resource.AddTestSweepers("aws_db_proxy", &resource.Sweeper{ + Name: "aws_db_proxy", + F: testSweepRdsDbProxies, + }) +} + +func testSweepRdsDbProxies(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("Error getting client: %s", err) + } + conn := client.(*AWSClient).rdsconn + + err = conn.DescribeDBProxiesPages(&rds.DescribeDBProxiesInput{}, func(out *rds.DescribeDBProxiesOutput, lastPage bool) bool { + for _, dbpg := range out.DBProxies { + if dbpg == nil { + continue + } + + input := &rds.DeleteDBProxyInput{ + DBProxyName: dbpg.DBProxyName, + } + name := aws.StringValue(dbpg.DBProxyName) + + log.Printf("[INFO] Deleting DB Proxy: %s", name) + + _, err := conn.DeleteDBProxy(input) + + if err != nil { + log.Printf("[ERROR] Failed to delete DB Proxy %s: %s", name, err) + continue + } + } + + return !lastPage + }) + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping RDS DB Proxy sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("Error retrieving DB Proxies: %s", err) + } + + return nil +} + +func TestAccAWSDBProxy_basic(t *testing.T) { + var v rds.DBProxy + resourceName := "aws_db_proxy.test" + name := fmt.Sprintf("tf-acc-db-proxy-%d", acctest.RandInt()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSDBProxyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSDBProxyConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSDBProxyExists(resourceName, &v), + resource.TestCheckResourceAttr( + resourceName, "name", name), + resource.TestCheckResourceAttr( + resourceName, "engine_family", "MYSQL"), + resource.TestMatchResourceAttr( + resourceName, "arn", regexp.MustCompile(`^arn:[^:]+:rds:[^:]+:\d{12}:db-proxy:.+`)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAWSDBProxyDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).rdsconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_db_proxy" { + continue + } + + // Try to find the Group + resp, err := conn.DescribeDBProxies( + &rds.DescribeDBProxiesInput{ + DBProxyName: aws.String(rs.Primary.ID), + }) + + if err == nil { + if len(resp.DBProxies) != 0 && + *resp.DBProxies[0].DBProxyName == rs.Primary.ID { + return fmt.Errorf("DB Proxy still exists") + } + } + + // Verify the error + newerr, ok := err.(awserr.Error) + if !ok { + return err + } + if newerr.Code() != rds.ErrCodeDBProxyNotFoundFault { + return err + } + } + + return nil +} + +func testAccCheckAWSDBProxyExists(n string, v *rds.DBProxy) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No DB Proxy ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).rdsconn + + opts := rds.DescribeDBProxiesInput{ + DBProxyName: aws.String(rs.Primary.ID), + } + + resp, err := conn.DescribeDBProxies(&opts) + + if err != nil { + return err + } + + if len(resp.DBProxies) != 1 || + *resp.DBProxies[0].DBProxyName != rs.Primary.ID { + return fmt.Errorf("DB Proxy not found") + } + + *v = *resp.DBProxies[0] + + return nil + } +} + +func testAccAWSDBProxyConfig(n string) string { + return fmt.Sprintf(` +resource "aws_db_proxy" "test" { + depends_on = [ + aws_secretsmanager_secret_version.test, + aws_iam_role_policy.test + ] + + name = "%s" + debug_logging = false + engine_family = "MYSQL" + idle_client_timeout = 1800 + require_tls = true + role_arn = aws_iam_role.test.arn + vpc_security_group_ids = [] + vpc_subnet_ids = aws_subnet.test.*.id + + auth { + auth_scheme = "SECRETS" + description = "test" + iam_auth = "DISABLED" + secret_arn = aws_secretsmanager_secret.test.arn + } + + tags = { + Name = "%s" + } +} + +# Secrets Manager setup + +resource "aws_secretsmanager_secret" "test" { + name = "%s" + recovery_window_in_days = 0 +} + +resource "aws_secretsmanager_secret_version" "test" { + secret_id = aws_secretsmanager_secret.test.id + secret_string = "{\"username\":\"db_user\",\"password\":\"db_user_password\"}" +} + +# IAM setup + +resource "aws_iam_role" "test" { + name = "%s" + assume_role_policy = data.aws_iam_policy_document.assume.json +} + +data "aws_iam_policy_document" "assume" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["rds.amazonaws.com"] + } + } +} + +resource "aws_iam_role_policy" "test" { + role = aws_iam_role.test.id + policy = data.aws_iam_policy_document.test.json +} + +data "aws_iam_policy_document" "test" { + statement { + actions = [ + "secretsmanager:GetRandomPassword", + "secretsmanager:CreateSecret", + "secretsmanager:ListSecrets", + ] + resources = ["*"] + } + + statement { + actions = ["secretsmanager:*"] + resources = [aws_secretsmanager_secret.test.arn] + } +} + +# VPC setup + +data "aws_availability_zones" "available" { + state = "available" + + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } +} + +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + + tags = { + Name = "%s" + } +} + +resource "aws_subnet" "test" { + count = 2 + cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 8, count.index) + availability_zone = data.aws_availability_zones.available.names[count.index] + vpc_id = aws_vpc.test.id + + tags = { + Name = "%s-${count.index}" + } +} +`, n, n, n, n, n, n) +} diff --git a/website/aws.erb b/website/aws.erb index 0ba22b5d000..14c88c389ba 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -2526,6 +2526,9 @@