Skip to content

Commit

Permalink
r/aws_redshift_cluster: support managed master passwords
Browse files Browse the repository at this point in the history
This change adds the manage_master_password and master_password_secret_kms_key_id arguments
allowing the configuration of AWS managed credentials via SecretsManager. The manage_master_password
argument conflicts with the master_password argument, and at least one must be set when
creating a new cluster not restored from a snapshot. The master_password_secret_arn attribute
was added to track the ARN of the SecretsManager secret which stores cluster credentials.
  • Loading branch information
jar-b committed Nov 1, 2023
1 parent a2c5769 commit fc2fa19
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 6 deletions.
3 changes: 3 additions & 0 deletions .changelog/34182.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_redshift_cluster: Add the `manage_master_password` and `master_password_secret_kms_key_id` arguments to support managed admin credentials
```
45 changes: 43 additions & 2 deletions internal/service/redshift/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,11 @@ func ResourceCluster() *schema.Resource {
Optional: true,
Default: "current",
},
"manage_master_password": {
Type: schema.TypeBool,
Optional: true,
ConflictsWith: []string{"master_password"},
},
"manual_snapshot_retention_period": {
Type: schema.TypeInt,
Optional: true,
Expand All @@ -272,6 +277,17 @@ func ResourceCluster() *schema.Resource {
validation.StringMatch(regexache.MustCompile(`^.*[0-9].*`), "must contain at least one number"),
validation.StringMatch(regexache.MustCompile(`^[^\@\/'" ]*$`), "cannot contain [/@\"' ]"),
),
ConflictsWith: []string{"manage_master_password"},
},
"master_password_secret_arn": {
Type: schema.TypeString,
Computed: true,
},
"master_password_secret_kms_key_id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateFunc: verify.ValidKMSKeyID,
},
"master_username": {
Type: schema.TypeString,
Expand Down Expand Up @@ -415,7 +431,6 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta int
ClusterVersion: aws.String(d.Get("cluster_version").(string)),
DBName: aws.String(d.Get("database_name").(string)),
MasterUsername: aws.String(d.Get("master_username").(string)),
MasterUserPassword: aws.String(d.Get("master_password").(string)),
NodeType: aws.String(d.Get("node_type").(string)),
Port: aws.Int64(int64(d.Get("port").(int))),
PubliclyAccessible: aws.Bool(d.Get("publicly_accessible").(bool)),
Expand Down Expand Up @@ -477,11 +492,25 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta int
input.MaintenanceTrackName = aws.String(v.(string))
}

if v, ok := d.GetOk("manage_master_password"); ok {
backupInput.ManageMasterPassword = aws.Bool(v.(bool))
input.ManageMasterPassword = aws.Bool(v.(bool))
}

if v, ok := d.GetOk("manual_snapshot_retention_period"); ok {
backupInput.ManualSnapshotRetentionPeriod = aws.Int64(int64(v.(int)))
input.ManualSnapshotRetentionPeriod = aws.Int64(int64(v.(int)))
}

if v, ok := d.GetOk("master_password"); ok {
input.MasterUserPassword = aws.String(v.(string))
}

if v, ok := d.GetOk("master_password_secret_kms_key_id"); ok {
backupInput.MasterPasswordSecretKmsKeyId = aws.String(v.(string))
input.MasterPasswordSecretKmsKeyId = aws.String(v.(string))
}

if v, ok := d.GetOk("number_of_nodes"); ok {
backupInput.NumberOfNodes = aws.Int64(int64(v.(int)))
// NumberOfNodes set below for CreateCluster.
Expand Down Expand Up @@ -524,7 +553,9 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta int
d.SetId(aws.StringValue(output.Cluster.ClusterIdentifier))
} else {
if _, ok := d.GetOk("master_password"); !ok {
return sdkdiag.AppendErrorf(diags, `provider.aws: aws_redshift_cluster: %s: "master_password": required field is not set`, d.Get("cluster_identifier").(string))
if _, ok := d.GetOk("manage_master_password"); !ok {
return sdkdiag.AppendErrorf(diags, `provider.aws: aws_redshift_cluster: %s: one of "manage_master_password" or "master_password" is required`, d.Get("cluster_identifier").(string))
}
}

if _, ok := d.GetOk("master_username"); !ok {
Expand Down Expand Up @@ -647,6 +678,8 @@ func resourceClusterRead(ctx context.Context, d *schema.ResourceData, meta inter
d.Set("maintenance_track_name", rsc.MaintenanceTrackName)
d.Set("manual_snapshot_retention_period", rsc.ManualSnapshotRetentionPeriod)
d.Set("master_username", rsc.MasterUsername)
d.Set("master_password_secret_arn", rsc.MasterPasswordSecretArn)
d.Set("master_password_secret_kms_key_id", rsc.MasterPasswordSecretKmsKeyId)
d.Set("node_type", rsc.NodeType)
d.Set("number_of_nodes", rsc.NumberOfNodes)
d.Set("preferred_maintenance_window", rsc.PreferredMaintenanceWindow)
Expand Down Expand Up @@ -755,6 +788,14 @@ func resourceClusterUpdate(ctx context.Context, d *schema.ResourceData, meta int
input.MasterUserPassword = aws.String(d.Get("master_password").(string))
}

if d.HasChange("master_password_secret_kms_key_id") {
input.MasterPasswordSecretKmsKeyId = aws.String(d.Get("master_password_secret_kms_key_id").(string))
}

if d.HasChange("manage_master_password") {
input.ManageMasterPassword = aws.Bool(d.Get("manage_master_password").(bool))
}

if d.HasChange("preferred_maintenance_window") {
input.PreferredMaintenanceWindow = aws.String(d.Get("preferred_maintenance_window").(string))
}
Expand Down
53 changes: 53 additions & 0 deletions internal/service/redshift/cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,42 @@ func TestAccRedshiftCluster_restoreFromSnapshotARN(t *testing.T) {
})
}

func TestAccRedshiftCluster_manageMasterPassword(t *testing.T) {
ctx := acctest.Context(t)
var v redshift.Cluster
resourceName := "aws_redshift_cluster.test"
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, redshift.EndpointsID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckClusterDestroy(ctx),
Steps: []resource.TestStep{
{
Config: testAccClusterConfig_manageMasterPassword(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckClusterExists(ctx, resourceName, &v),
resource.TestCheckResourceAttrPair(resourceName, "availability_zone", "data.aws_availability_zones.available", "names.0"),
resource.TestCheckResourceAttr(resourceName, "manage_master_password", "true"),
resource.TestCheckResourceAttrSet(resourceName, "master_password_secret_arn"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"final_snapshot_identifier",
"manage_master_password",
"skip_final_snapshot",
"apply_immediately",
},
},
},
})
}

func testAccCheckClusterDestroy(ctx context.Context) resource.TestCheckFunc {
return func(s *terraform.State) error {
conn := acctest.Provider.Meta().(*conns.AWSClient).RedshiftConn(ctx)
Expand Down Expand Up @@ -1734,3 +1770,20 @@ resource "aws_redshift_cluster" "test2" {
}
`, rName))
}

func testAccClusterConfig_manageMasterPassword(rName string) string {
// "InvalidVPCNetworkStateFault: The requested AZ us-west-2a is not a valid AZ."
return acctest.ConfigCompose(acctest.ConfigAvailableAZsNoOptInExclude("usw2-az2"), fmt.Sprintf(`
resource "aws_redshift_cluster" "test" {
cluster_identifier = %[1]q
availability_zone = data.aws_availability_zones.available.names[0]
database_name = "mydb"
master_username = "foo_test"
manage_master_password = true
node_type = "dc2.large"
automated_snapshot_retention_period = 0
allow_version_upgrade = false
skip_final_snapshot = true
}
`, rName))
}
31 changes: 27 additions & 4 deletions website/docs/r/redshift_cluster.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Provides a Redshift Cluster Resource.

## Example Usage

### Basic Usage

```terraform
resource "aws_redshift_cluster" "example" {
cluster_identifier = "tf-redshift-cluster"
Expand All @@ -28,22 +30,42 @@ resource "aws_redshift_cluster" "example" {
}
```

### With Managed Credentials

```terraform
resource "aws_redshift_cluster" "example" {
cluster_identifier = "tf-redshift-cluster"
database_name = "mydb"
master_username = "exampleuser"
node_type = "dc1.large"
cluster_type = "single-node"
manage_master_password = true
}
```

## Argument Reference

For more detailed documentation about each argument, refer to
the [AWS official documentation](http://docs.aws.amazon.com/cli/latest/reference/redshift/index.html#cli-aws-redshift).

This argument supports the following arguments:
This resource supports the following arguments:

* `cluster_identifier` - (Required) The Cluster Identifier. Must be a lower case string.
* `database_name` - (Optional) The name of the first database to be created when the cluster is created.
If you do not provide a name, Amazon Redshift will create a default database called `dev`.
* `default_iam_role_arn` - (Optional) The Amazon Resource Name (ARN) for the IAM role that was set as default for the cluster when the cluster was created.
* `node_type` - (Required) The node type to be provisioned for the cluster.
* `cluster_type` - (Optional) The cluster type to use. Either `single-node` or `multi-node`.
* `master_password` - (Required unless a `snapshot_identifier` is provided) Password for the master DB user.
Note that this may show up in logs, and it will be stored in the state file. Password must contain at least 8 chars and
contain at least one uppercase letter, one lowercase letter, and one number.
* `manage_master_password` - (Optional) Whether to use AWS SecretsManager to manage the cluster admin credentials.
Conflicts with `master_password`.
One of `master_password` or `manage_master_password` is required unless `snapshot_identifier` is provided.
* `master_password` - (Optional) Password for the master DB user.
Conflicts with `manage_master_password`.
One of `master_password` or `manage_master_password` is required unless `snapshot_identifier` is provided.
Note that this may show up in logs, and it will be stored in the state file.
Password must contain at least 8 characters and contain at least one uppercase letter, one lowercase letter, and one number.
* `master_password_secret_kms_key_id` - (Optional) ID of the KMS key used to encrypt the cluster admin credentials secret.
* `master_username` - (Required unless a `snapshot_identifier` is provided) Username for the master DB user.
* `vpc_security_group_ids` - (Optional) A list of Virtual Private Cloud (VPC) security groups to be associated with the cluster.
* `cluster_subnet_group_name` - (Optional) The name of a cluster subnet group to be associated with this cluster. If this parameter is not provided the resulting cluster will be deployed outside virtual private cloud (VPC).
Expand Down Expand Up @@ -117,6 +139,7 @@ This resource exports the following attributes in addition to the arguments abov
* `encrypted` - Whether the data in the cluster is encrypted
* `vpc_security_group_ids` - The VPC security group Ids associated with the cluster
* `dns_name` - The DNS name of the cluster
* `master_password_secret_arn` - ARN of the cluster admin credentials secret
* `port` - The Port the cluster responds on
* `cluster_version` - The version of Redshift engine software
* `cluster_parameter_group_name` - The name of the parameter group to be associated with this cluster
Expand Down

0 comments on commit fc2fa19

Please sign in to comment.