diff --git a/aws-redshift-cluster/aws-redshift-cluster.json b/aws-redshift-cluster/aws-redshift-cluster.json index 57a826a..0c71b24 100644 --- a/aws-redshift-cluster/aws-redshift-cluster.json +++ b/aws-redshift-cluster/aws-redshift-cluster.json @@ -67,7 +67,7 @@ "maxLength": 128 }, "MasterUserPassword": { - "description": "The password associated with the master user account for the cluster that is being created. Password must be between 8 and 64 characters in length, should have at least one uppercase letter.Must contain at least one lowercase letter.Must contain one number.Can be any printable ASCII character.", + "description": "The password associated with the master user account for the cluster that is being created. You can't use MasterUserPassword if ManageMasterPassword is true. Password must be between 8 and 64 characters in length, should have at least one uppercase letter.Must contain at least one lowercase letter.Must contain one number.Can be any printable ASCII character.", "type": "string", "maxLength": 64 }, @@ -281,6 +281,18 @@ "NamespaceResourcePolicy": { "description": "The namespace resource policy document that will be attached to a Redshift cluster.", "type": "object" + }, + "ManageMasterPassword": { + "description": "A boolean indicating if the redshift cluster's admin user credentials is managed by Redshift or not. You can't use MasterUserPassword if ManageMasterPassword is true. If ManageMasterPassword is false or not set, Amazon Redshift uses MasterUserPassword for the admin user account's password.", + "type": "boolean" + }, + "MasterPasswordSecretKmsKeyId": { + "description": "The ID of the Key Management Service (KMS) key used to encrypt and store the cluster's admin user credentials secret.", + "type": "string" + }, + "MasterPasswordSecretArn": { + "description": "The Amazon Resource Name (ARN) for the cluster's admin user credentials secret.", + "type": "string" } }, "additionalProperties": false, @@ -297,7 +309,8 @@ "/properties/DeferMaintenanceIdentifier", "/properties/Endpoint/Port", "/properties/Endpoint/Address", - "/properties/ClusterNamespaceArn" + "/properties/ClusterNamespaceArn", + "/properties/MasterPasswordSecretArn" ], "createOnlyProperties": [ "/properties/ClusterIdentifier", @@ -313,7 +326,8 @@ "/properties/Classic", "/properties/SnapshotIdentifier", "/properties/DeferMaintenance", - "/properties/DeferMaintenanceDuration" + "/properties/DeferMaintenanceDuration", + "/properties/ManageMasterPassword" ], "tagging": { "taggable": true diff --git a/aws-redshift-cluster/docs/README.md b/aws-redshift-cluster/docs/README.md index cba95fd..d1dfafe 100644 --- a/aws-redshift-cluster/docs/README.md +++ b/aws-redshift-cluster/docs/README.md @@ -61,7 +61,9 @@ To declare this entity in your AWS CloudFormation template, use the following sy "ResourceAction" : String, "RotateEncryptionKey" : Boolean, "MultiAZ" : Boolean, - "NamespaceResourcePolicy" : Map + "NamespaceResourcePolicy" : Map, + "ManageMasterPassword" : Boolean, + "MasterPasswordSecretKmsKeyId" : String, } } @@ -125,6 +127,8 @@ Properties: RotateEncryptionKey: Boolean MultiAZ: Boolean NamespaceResourcePolicy: Map + ManageMasterPassword: Boolean + MasterPasswordSecretKmsKeyId: String ## Properties @@ -155,7 +159,7 @@ _Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/l #### MasterUserPassword -The password associated with the master user account for the cluster that is being created. Password must be between 8 and 64 characters in length, should have at least one uppercase letter.Must contain at least one lowercase letter.Must contain one number.Can be any printable ASCII character. +The password associated with the master user account for the cluster that is being created. You can't use MasterUserPassword if ManageMasterPassword is true. Password must be between 8 and 64 characters in length, should have at least one uppercase letter.Must contain at least one lowercase letter.Must contain one number.Can be any printable ASCII character. _Required_: No @@ -646,6 +650,26 @@ _Type_: Map _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) +#### ManageMasterPassword + +A boolean indicating if the redshift cluster's admin user credentials is managed by Redshift or not. You can't use MasterUserPassword if ManageMasterPassword is true. If ManageMasterPassword is false or not set, Amazon Redshift uses MasterUserPassword for the admin user account's password. + +_Required_: No + +_Type_: Boolean + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### MasterPasswordSecretKmsKeyId + +The ID of the Key Management Service (KMS) key used to encrypt and store the cluster's admin user credentials secret. + +_Required_: No + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + ## Return Values ### Ref @@ -673,3 +697,7 @@ Returns the Address value. #### ClusterNamespaceArn The Amazon Resource Name (ARN) of the cluster namespace. + +#### MasterPasswordSecretArn + +The Amazon Resource Name (ARN) for the cluster's admin user credentials secret. diff --git a/aws-redshift-cluster/pom.xml b/aws-redshift-cluster/pom.xml index bb74832..af36bf7 100644 --- a/aws-redshift-cluster/pom.xml +++ b/aws-redshift-cluster/pom.xml @@ -23,7 +23,7 @@ software.amazon.awssdk redshift - 2.21.37 + 2.21.44 diff --git a/aws-redshift-cluster/src/main/java/software/amazon/redshift/cluster/BaseHandlerStd.java b/aws-redshift-cluster/src/main/java/software/amazon/redshift/cluster/BaseHandlerStd.java index 3c050ba..1643f14 100644 --- a/aws-redshift-cluster/src/main/java/software/amazon/redshift/cluster/BaseHandlerStd.java +++ b/aws-redshift-cluster/src/main/java/software/amazon/redshift/cluster/BaseHandlerStd.java @@ -3,6 +3,8 @@ import com.amazonaws.util.CollectionUtils; import com.amazonaws.util.StringUtils; import com.google.common.collect.Sets; + +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import software.amazon.awssdk.services.redshift.RedshiftClient; import software.amazon.awssdk.services.redshift.model.AquaConfiguration; @@ -204,10 +206,16 @@ protected boolean issueModifyClusterMaintenanceRequest(ResourceModel model) { ObjectUtils.allNotNull(model.getDeferMaintenanceIdentifier()); } - // check for required parameters to not have null values protected boolean invalidCreateClusterRequest(ResourceModel model) { - return model.getClusterIdentifier() == null || model.getNodeType() == null - || model.getMasterUsername() == null || model.getMasterUserPassword() == null; + // check for required parameters to not have null values + boolean isInvalid = model.getClusterIdentifier() == null || model.getNodeType() == null + || model.getMasterUsername() == null; + + // check if either MasterUserPassword is provided or ManageMasterPassword is true + if (model.getMasterUserPassword() == null) { + return !BooleanUtils.isTrue(model.getManageMasterPassword()) || isInvalid; + } + return isInvalid; } protected boolean issueModifyClusterParameterGroupRequest(ResourceModel prevModel, ResourceModel model) { diff --git a/aws-redshift-cluster/src/main/java/software/amazon/redshift/cluster/Translator.java b/aws-redshift-cluster/src/main/java/software/amazon/redshift/cluster/Translator.java index 920bd5f..6b2538b 100644 --- a/aws-redshift-cluster/src/main/java/software/amazon/redshift/cluster/Translator.java +++ b/aws-redshift-cluster/src/main/java/software/amazon/redshift/cluster/Translator.java @@ -114,6 +114,8 @@ static CreateClusterRequest translateToCreateRequest(final ResourceModel model, .enhancedVpcRouting(model.getEnhancedVpcRouting()) .maintenanceTrackName(model.getMaintenanceTrackName()) .multiAZ(model.getMultiAZ()) + .manageMasterPassword(model.getManageMasterPassword()) + .masterPasswordSecretKmsKeyId(model.getMasterPasswordSecretKmsKeyId()) .build(); } @@ -593,6 +595,17 @@ static ResourceModel translateFromReadResponse(final DescribeClustersResponse aw .findAny() .orElse(null); + final String masterPasswordSecretArn = streamOfOrEmpty(awsResponse.clusters()) + .map(software.amazon.awssdk.services.redshift.model.Cluster::masterPasswordSecretArn) + .filter(Objects::nonNull) + .findAny() + .orElse(null); + + final String masterPasswordSecretKmsKeyId = streamOfOrEmpty(awsResponse.clusters()) + .map(software.amazon.awssdk.services.redshift.model.Cluster::masterPasswordSecretKmsKeyId) + .filter(Objects::nonNull) + .findAny() + .orElse(null); return ResourceModel.builder() .clusterIdentifier(clusterIdentifier) @@ -634,6 +647,8 @@ static ResourceModel translateFromReadResponse(final DescribeClustersResponse aw .deferMaintenanceIdentifier(translateDeferMaintenanceIdentifierFromSdk(deferMaintenanceWindows)) .deferMaintenanceStartTime(translateDeferMaintenanceStartTimeFromSdk(deferMaintenanceWindows)) .deferMaintenanceEndTime(translateDeferMaintenanceEndTimeFromSdk(deferMaintenanceWindows)) + .masterPasswordSecretArn(masterPasswordSecretArn) + .masterPasswordSecretKmsKeyId(masterPasswordSecretKmsKeyId) .build(); } @@ -683,7 +698,7 @@ static String finalClusterSnapshotIdentifierBuilder(String clusterIdentifier, bo static ModifyClusterRequest translateToUpdateRequest(final ResourceModel model, final ResourceModel prevModel) { ModifyClusterRequest modifyClusterRequest = ModifyClusterRequest.builder() .clusterIdentifier(model.getClusterIdentifier()) - .masterUserPassword(model.getMasterUserPassword().equals(prevModel.getMasterUserPassword()) ? null : model.getMasterUserPassword()) + .masterUserPassword(model.getMasterUserPassword() == null || model.getMasterUserPassword().equals(prevModel.getMasterUserPassword()) ? null : model.getMasterUserPassword()) .allowVersionUpgrade(model.getAllowVersionUpgrade() == null || model.getAllowVersionUpgrade().equals(prevModel.getAllowVersionUpgrade()) ? null : model.getAllowVersionUpgrade()) .automatedSnapshotRetentionPeriod(model.getAutomatedSnapshotRetentionPeriod() == null || model.getAutomatedSnapshotRetentionPeriod().equals(prevModel.getAutomatedSnapshotRetentionPeriod()) ? null : model.getAutomatedSnapshotRetentionPeriod()) //.clusterParameterGroupName(model.getClusterParameterGroupName() == null || model.getClusterParameterGroupName().equals(prevModel.getClusterParameterGroupName()) ? null : model.getClusterParameterGroupName()) @@ -706,6 +721,8 @@ static ModifyClusterRequest translateToUpdateRequest(final ResourceModel model, .maintenanceTrackName(model.getMaintenanceTrackName() == null || model.getMaintenanceTrackName().equals(prevModel.getMaintenanceTrackName()) ? null : model.getMaintenanceTrackName()) .enhancedVpcRouting(model.getEnhancedVpcRouting() == null || model.getEnhancedVpcRouting().equals(prevModel.getEnhancedVpcRouting()) ? null : model.getEnhancedVpcRouting()) .multiAZ(model.getMultiAZ() == null || model.getMultiAZ().equals(prevModel.getMultiAZ()) ? null : model.getMultiAZ()) + .manageMasterPassword(model.getManageMasterPassword()) + .masterPasswordSecretKmsKeyId(model.getMasterPasswordSecretKmsKeyId() == null || model.getMasterPasswordSecretKmsKeyId().equals(prevModel.getMasterPasswordSecretKmsKeyId()) ? null : model.getMasterPasswordSecretKmsKeyId()) .build(); return modifyClusterRequest; @@ -877,6 +894,8 @@ static RestoreFromClusterSnapshotRequest translateToRestoreFromClusterSnapshotRe .numberOfNodes(model.getNumberOfNodes()) .encrypted(model.getEncrypted()) .multiAZ(model.getMultiAZ()) + .manageMasterPassword(model.getManageMasterPassword()) + .masterPasswordSecretKmsKeyId(model.getMasterPasswordSecretKmsKeyId()) .build(); } diff --git a/aws-redshift-cluster/src/main/java/software/amazon/redshift/cluster/UpdateHandler.java b/aws-redshift-cluster/src/main/java/software/amazon/redshift/cluster/UpdateHandler.java index 2df27a3..6a57c3f 100644 --- a/aws-redshift-cluster/src/main/java/software/amazon/redshift/cluster/UpdateHandler.java +++ b/aws-redshift-cluster/src/main/java/software/amazon/redshift/cluster/UpdateHandler.java @@ -137,7 +137,9 @@ public class UpdateHandler extends BaseHandlerStd { "PreferredMaintenanceWindow", "PubliclyAccessible", "VpcSecurityGroupIds", - "MultiAZ" + "MultiAZ", + "ManageMasterPassword", + "MasterPasswordSecretKmsKeyId" }; public static final String[] DETECTABLE_MODIFY_CLUSTER_ATTRIBUTES_SENSITIVE = new String[] { "MasterUserPassword" diff --git a/aws-redshift-cluster/src/test/java/software/amazon/redshift/cluster/AbstractTestBase.java b/aws-redshift-cluster/src/test/java/software/amazon/redshift/cluster/AbstractTestBase.java index 238aceb..1aa2f43 100644 --- a/aws-redshift-cluster/src/test/java/software/amazon/redshift/cluster/AbstractTestBase.java +++ b/aws-redshift-cluster/src/test/java/software/amazon/redshift/cluster/AbstractTestBase.java @@ -24,6 +24,7 @@ public class AbstractTestBase { protected static final String AWS_PARTITION; protected static final String AWS_ACCOUNT_ID; protected static final String CLUSTER_IDENTIFIER; + protected static final String SNAPSHOT_IDENTIFIER; protected static final String CLUSTER_NAMESPACE_UUID; protected static final String MASTER_USERNAME; protected static final String MASTER_USERPASSWORD; @@ -65,6 +66,7 @@ public class AbstractTestBase { DEFER_MAINTENANCE_IDENTIFIER = "cfn-defer-maintenance-identifier"; DEFER_MAINTENANCE_START_TIME = "2023-12-10T00:00:00Z"; DEFER_MAINTENANCE_END_TIME = "2024-01-19T00:00:00Z"; + SNAPSHOT_IDENTIFIER = "redshift-cluster-1-snapshot"; RESOURCE_POLICY = ResourcePolicy.builder() .resourceArn(CLUSTER_NAMESPACE_ARN) @@ -181,6 +183,12 @@ public static ResourceModel createClusterRequestModel() { .build(); } + public static ResourceModel restoreClusterRequestModel() { + return createClusterRequestModel().toBuilder() + .snapshotIdentifier(SNAPSHOT_IDENTIFIER) + .build(); + } + public static ResourceModel createClusterResponseModel() { return ResourceModel.builder() .clusterIdentifier(CLUSTER_IDENTIFIER) diff --git a/aws-redshift-cluster/src/test/java/software/amazon/redshift/cluster/CreateHandlerTest.java b/aws-redshift-cluster/src/test/java/software/amazon/redshift/cluster/CreateHandlerTest.java index bdbd803..38cea63 100644 --- a/aws-redshift-cluster/src/test/java/software/amazon/redshift/cluster/CreateHandlerTest.java +++ b/aws-redshift-cluster/src/test/java/software/amazon/redshift/cluster/CreateHandlerTest.java @@ -19,6 +19,8 @@ import software.amazon.awssdk.services.redshift.model.GetResourcePolicyRequest; import software.amazon.awssdk.services.redshift.model.ModifyClusterMaintenanceRequest; import software.amazon.awssdk.services.redshift.model.PutResourcePolicyRequest; +import software.amazon.awssdk.services.redshift.model.RestoreFromClusterSnapshotRequest; +import software.amazon.awssdk.services.redshift.model.RestoreFromClusterSnapshotResponse; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.OperationStatus; import software.amazon.cloudformation.proxy.ProgressEvent; @@ -41,6 +43,9 @@ import static org.mockito.Mockito.when; import static software.amazon.redshift.cluster.TestUtils.MULTIAZ_CLUSTER; import static software.amazon.redshift.cluster.TestUtils.MULTIAZ_ENABLED; +import static software.amazon.redshift.cluster.TestUtils.MANAGED_ADMIN_PASSWORD_CLUSTER; +import static software.amazon.redshift.cluster.TestUtils.MASTER_PASSWORD_SECRET_ARN; +import static software.amazon.redshift.cluster.TestUtils.MASTER_PASSWORD_SECRET_KMS_KEY_ID; @ExtendWith(MockitoExtension.class) public class CreateHandlerTest extends AbstractTestBase { @@ -353,4 +358,110 @@ public void testCreateClusterModifyClusterMaintenance() { verify(proxyClient.client(), times(4)) .describeClusters(any(DescribeClustersRequest.class)); } + + @Test + public void testCreateCluster_ManagedAdminPassword() { + ResourceModel requestModel = createClusterRequestModel(); + requestModel.setMasterUserPassword(null); + requestModel.setManageMasterPassword(true); + requestModel.setMasterPasswordSecretKmsKeyId(MASTER_PASSWORD_SECRET_KMS_KEY_ID); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .region(AWS_REGION) + .logicalResourceIdentifier("logicalId") + .clientRequestToken("token") + .build(); + + when(proxyClient.client().createCluster(any(CreateClusterRequest.class))) + .thenReturn(CreateClusterResponse.builder() + .cluster(MANAGED_ADMIN_PASSWORD_CLUSTER) + .build()); + when(proxyClient.client().describeClusters(any(DescribeClustersRequest.class))) + .thenReturn(DescribeClustersResponse.builder() + .clusters(MANAGED_ADMIN_PASSWORD_CLUSTER) + .build()); + when(proxyClient.client().describeLoggingStatus(any(DescribeLoggingStatusRequest.class))) + .thenReturn(DescribeLoggingStatusResponse.builder().loggingEnabled(false).build()); + when(proxyClient.client().getResourcePolicy(any(GetResourcePolicyRequest.class))).thenReturn(getEmptyResourcePolicyResponseSdk()); + + ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(30); + + response = handler.handleRequest(proxy, request, response.getCallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + assertThat(response.getResourceModel().getClusterIdentifier()). + isEqualTo(request.getDesiredResourceState().getClusterIdentifier()); + assertThat(response.getResourceModel().getMasterPasswordSecretArn()). + isEqualTo(MASTER_PASSWORD_SECRET_ARN); + assertThat(response.getResourceModel().getMasterPasswordSecretKmsKeyId()). + isEqualTo(request.getDesiredResourceState().getMasterPasswordSecretKmsKeyId()); + assertThat(response.getResourceModel().getMasterUserPassword()).isNull(); + + verify(proxyClient.client()).createCluster(any(CreateClusterRequest.class)); + verify(proxyClient.client(), times(3)) + .describeClusters(any(DescribeClustersRequest.class)); + } + + @Test + public void testRestoreCluster_ManagedAdminPassword() { + ResourceModel requestModel = restoreClusterRequestModel(); + requestModel.setMasterUserPassword(null); + requestModel.setManageMasterPassword(true); + requestModel.setMasterPasswordSecretKmsKeyId(MASTER_PASSWORD_SECRET_KMS_KEY_ID); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .region(AWS_REGION) + .logicalResourceIdentifier("logicalId") + .clientRequestToken("token") + .build(); + + when(proxyClient.client().restoreFromClusterSnapshot(any(RestoreFromClusterSnapshotRequest.class))).thenReturn(RestoreFromClusterSnapshotResponse.builder() + .cluster(MANAGED_ADMIN_PASSWORD_CLUSTER) + .build()); + when(proxyClient.client().describeClusters(any(DescribeClustersRequest.class))) + .thenReturn(DescribeClustersResponse.builder() + .clusters(MANAGED_ADMIN_PASSWORD_CLUSTER) + .build()); + when(proxyClient.client().describeLoggingStatus(any(DescribeLoggingStatusRequest.class))) + .thenReturn(DescribeLoggingStatusResponse.builder().loggingEnabled(false).build()); + when(proxyClient.client().getResourcePolicy(any(GetResourcePolicyRequest.class))).thenReturn(getEmptyResourcePolicyResponseSdk()); + + ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(30); + + response = handler.handleRequest(proxy, request, response.getCallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + assertThat(response.getResourceModel().getClusterIdentifier()). + isEqualTo(request.getDesiredResourceState().getClusterIdentifier()); + assertThat(response.getResourceModel().getMasterPasswordSecretArn()). + isEqualTo(MASTER_PASSWORD_SECRET_ARN); + assertThat(response.getResourceModel().getMasterPasswordSecretKmsKeyId()). + isEqualTo(request.getDesiredResourceState().getMasterPasswordSecretKmsKeyId()); + assertThat(response.getResourceModel().getMasterUserPassword()).isNull(); + + verify(proxyClient.client()).restoreFromClusterSnapshot(any(RestoreFromClusterSnapshotRequest.class)); + verify(proxyClient.client(), times(3)) + .describeClusters(any(DescribeClustersRequest.class)); + } + } diff --git a/aws-redshift-cluster/src/test/java/software/amazon/redshift/cluster/TestUtils.java b/aws-redshift-cluster/src/test/java/software/amazon/redshift/cluster/TestUtils.java index 7e4f05d..ca08501 100644 --- a/aws-redshift-cluster/src/test/java/software/amazon/redshift/cluster/TestUtils.java +++ b/aws-redshift-cluster/src/test/java/software/amazon/redshift/cluster/TestUtils.java @@ -25,6 +25,8 @@ public class TestUtils { final static String OWNER_ACCOUNT_NO = "1111"; final static String MULTIAZ_ENABLED = "Enabled"; final static String MULTIAZ_DISABLED = "Disabled"; + final static String MASTER_PASSWORD_SECRET_KMS_KEY_ID = "master-password-secret-kms-key-id"; + final static String MASTER_PASSWORD_SECRET_ARN = "secret-arn"; final static Cluster BASIC_CLUSTER = Cluster.builder() .clusterStatus("available") @@ -63,6 +65,26 @@ public class TestUtils { .vpcSecurityGroups(Collections.emptyList()) .build(); + final static Cluster MANAGED_ADMIN_PASSWORD_CLUSTER = Cluster.builder() + .clusterStatus("available") + .clusterAvailabilityStatus("Available") + .clusterIdentifier(CLUSTER_IDENTIFIER) + .masterUsername(MASTER_USERNAME) + .nodeType(NODETYPE) + .numberOfNodes(NUMBER_OF_NODES) + .allowVersionUpgrade(true) + .automatedSnapshotRetentionPeriod(0) + .encrypted(false) + .enhancedVpcRouting(false) + .manualSnapshotRetentionPeriod(1) + .publiclyAccessible(false) + .clusterSecurityGroups(Collections.emptyList()) + .iamRoles(Collections.emptyList()) + .vpcSecurityGroups(Collections.emptyList()) + .masterPasswordSecretArn(MASTER_PASSWORD_SECRET_ARN) + .masterPasswordSecretKmsKeyId(MASTER_PASSWORD_SECRET_KMS_KEY_ID) + .build(); + final static Cluster BASIC_CLUSTER_READ = Cluster.builder() .clusterIdentifier(CLUSTER_IDENTIFIER) .masterUsername(MASTER_USERNAME) diff --git a/aws-redshift-cluster/src/test/java/software/amazon/redshift/cluster/UpdateHandlerTest.java b/aws-redshift-cluster/src/test/java/software/amazon/redshift/cluster/UpdateHandlerTest.java index e3fabb2..052f701 100644 --- a/aws-redshift-cluster/src/test/java/software/amazon/redshift/cluster/UpdateHandlerTest.java +++ b/aws-redshift-cluster/src/test/java/software/amazon/redshift/cluster/UpdateHandlerTest.java @@ -14,7 +14,6 @@ import software.amazon.awssdk.services.redshift.model.ClusterIamRole; import software.amazon.awssdk.services.redshift.model.CreateTagsRequest; import software.amazon.awssdk.services.redshift.model.CreateTagsResponse; -import software.amazon.awssdk.services.redshift.model.DeleteResourcePolicyRequest; import software.amazon.awssdk.services.redshift.model.DeleteTagsRequest; import software.amazon.awssdk.services.redshift.model.DeleteTagsResponse; import software.amazon.awssdk.services.redshift.model.DescribeClustersRequest; @@ -61,7 +60,10 @@ import static software.amazon.redshift.cluster.TestUtils.BASIC_CLUSTER; import static software.amazon.redshift.cluster.TestUtils.BASIC_MODEL; import static software.amazon.redshift.cluster.TestUtils.BASIC_RESOURCE_HANDLER_REQUEST; +import static software.amazon.redshift.cluster.TestUtils.MASTER_PASSWORD_SECRET_ARN; import static software.amazon.redshift.cluster.TestUtils.MULTIAZ_CLUSTER; +import static software.amazon.redshift.cluster.TestUtils.MANAGED_ADMIN_PASSWORD_CLUSTER; +import static software.amazon.redshift.cluster.TestUtils.MASTER_PASSWORD_SECRET_KMS_KEY_ID; import static software.amazon.redshift.cluster.TestUtils.modifyAttribute; @ExtendWith(MockitoExtension.class) @@ -484,10 +486,10 @@ public void testModifyMasterUserPasswordAndPubliclyAccessible() { // todo: make tests more independent so we can add tests like this elsewhere verify(handler).sleep(10); - assertThat(UpdateHandler.DETECTABLE_MODIFY_CLUSTER_ATTRIBUTES_SENSITIVE.length == 1); - assertThat(UpdateHandler.DETECTABLE_MODIFY_CLUSTER_ATTRIBUTES_INSENSITIVE.length == 17); + assertThat(UpdateHandler.DETECTABLE_MODIFY_CLUSTER_ATTRIBUTES_SENSITIVE.length).isEqualTo(1); + assertThat(UpdateHandler.DETECTABLE_MODIFY_CLUSTER_ATTRIBUTES_INSENSITIVE.length).isEqualTo(21); - assertThat(UpdateHandler.DETECTABLE_MODIFY_CLUSTER_ATTRIBUTES_SENSITIVE[0].equals("MasterUserPassword")); + assertThat(UpdateHandler.DETECTABLE_MODIFY_CLUSTER_ATTRIBUTES_SENSITIVE[0]).isEqualTo("MasterUserPassword"); List insensitiveFileds = Arrays.asList( "AllowVersionUpgrade", @@ -511,9 +513,8 @@ public void testModifyMasterUserPasswordAndPubliclyAccessible() { ); for (int i = 0; i < insensitiveFileds.size(); i++) { - assertThat(UpdateHandler.DETECTABLE_MODIFY_CLUSTER_ATTRIBUTES_INSENSITIVE[i].equals( - insensitiveFileds.get(i) - )); + assertThat(UpdateHandler.DETECTABLE_MODIFY_CLUSTER_ATTRIBUTES_INSENSITIVE[i]).isEqualTo( + insensitiveFileds.get(i)); } } @@ -566,11 +567,8 @@ public void testModifyEncrypted_EnableMultiAZ() { // todo: make tests more independent so we can add tests like this elsewhere verify(handler).sleep(10); - assertThat(UpdateHandler.DETECTABLE_MODIFY_CLUSTER_ATTRIBUTES_SENSITIVE.length == 1); - assertThat(UpdateHandler.DETECTABLE_MODIFY_CLUSTER_ATTRIBUTES_INSENSITIVE.length == 18); - - assertThat(UpdateHandler.DETECTABLE_MODIFY_CLUSTER_ATTRIBUTES_SENSITIVE[0].equals("Encrypted")); - assertThat(UpdateHandler.DETECTABLE_MODIFY_CLUSTER_ATTRIBUTES_SENSITIVE[0].equals("MultiAZ")); + assertThat(UpdateHandler.DETECTABLE_MODIFY_CLUSTER_ATTRIBUTES_SENSITIVE.length).isEqualTo(1); + assertThat(UpdateHandler.DETECTABLE_MODIFY_CLUSTER_ATTRIBUTES_INSENSITIVE.length).isEqualTo(21); List insensitiveFileds = Arrays.asList( "AllowVersionUpgrade", @@ -595,12 +593,177 @@ public void testModifyEncrypted_EnableMultiAZ() { ); for (int i = 0; i < insensitiveFileds.size(); i++) { - assertThat(UpdateHandler.DETECTABLE_MODIFY_CLUSTER_ATTRIBUTES_INSENSITIVE[i].equals( + assertThat(UpdateHandler.DETECTABLE_MODIFY_CLUSTER_ATTRIBUTES_INSENSITIVE[i]).isEqualTo( insensitiveFileds.get(i) - )); + ); } } + @Test + public void testModify_OptInManagedMasterPassword() { + ResourceModel previousModel = BASIC_MODEL.toBuilder().build(); + + ResourceModel updateModel = BASIC_MODEL.toBuilder() + .manageMasterPassword(true) + .masterPasswordSecretKmsKeyId(MASTER_PASSWORD_SECRET_KMS_KEY_ID) + .build(); + + final ResourceHandlerRequest request = BASIC_RESOURCE_HANDLER_REQUEST.toBuilder() + .desiredResourceState(updateModel) + .previousResourceState(previousModel) + .build(); + + Cluster existingCluster = BASIC_CLUSTER.toBuilder().build(); + Cluster modifiedCluster = MANAGED_ADMIN_PASSWORD_CLUSTER.toBuilder().build(); + + when(proxyClient.client().describeClusters(any(DescribeClustersRequest.class))) + .thenReturn(DescribeClustersResponse.builder() + .clusters(existingCluster) + .build()) + .thenReturn(DescribeClustersResponse.builder() + .clusters(modifiedCluster) + .build()); + + when(proxyClient.client().describeLoggingStatus(any(DescribeLoggingStatusRequest.class))) + .thenReturn(DescribeLoggingStatusResponse.builder().build()); + + when(proxyClient.client().modifyCluster(any(ModifyClusterRequest.class))) + .thenReturn(ModifyClusterResponse.builder() + .cluster(modifiedCluster) + .build()); + when(proxyClient.client().getResourcePolicy(any(GetResourcePolicyRequest.class))).thenReturn(getEmptyResourcePolicyResponseSdk()); + + ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(30); + + response = handler.handleRequest(proxy, request, response.getCallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + + assertThat(response.getResourceModel().getMasterPasswordSecretArn()). + isEqualTo(MASTER_PASSWORD_SECRET_ARN); + assertThat(response.getResourceModel().getMasterPasswordSecretKmsKeyId()). + isEqualTo(request.getDesiredResourceState().getMasterPasswordSecretKmsKeyId()); + assertThat(response.getResourceModel().getMasterUserPassword()).isNull(); + + + verify(proxyClient.client()).modifyCluster(any(ModifyClusterRequest.class)); + } + + @Test + public void testModify_OptOutManagedMasterPassword() { + ResourceModel previousModel = BASIC_MODEL.toBuilder() + .masterUserPassword(null) + .manageMasterPassword(true) + .masterPasswordSecretKmsKeyId(MASTER_PASSWORD_SECRET_KMS_KEY_ID) + .build(); + + ResourceModel updateModel = BASIC_MODEL.toBuilder().build(); + + final ResourceHandlerRequest request = BASIC_RESOURCE_HANDLER_REQUEST.toBuilder() + .desiredResourceState(updateModel) + .previousResourceState(previousModel) + .build(); + + Cluster existingCluster = MANAGED_ADMIN_PASSWORD_CLUSTER.toBuilder().build(); + Cluster modifiedCluster = BASIC_CLUSTER.toBuilder().build(); + + when(proxyClient.client().describeClusters(any(DescribeClustersRequest.class))) + .thenReturn(DescribeClustersResponse.builder() + .clusters(existingCluster) + .build()) + .thenReturn(DescribeClustersResponse.builder() + .clusters(modifiedCluster) + .build()); + + when(proxyClient.client().describeLoggingStatus(any(DescribeLoggingStatusRequest.class))) + .thenReturn(DescribeLoggingStatusResponse.builder().build()); + + when(proxyClient.client().modifyCluster(any(ModifyClusterRequest.class))) + .thenReturn(ModifyClusterResponse.builder() + .cluster(modifiedCluster) + .build()); + when(proxyClient.client().getResourcePolicy(any(GetResourcePolicyRequest.class))).thenReturn(getEmptyResourcePolicyResponseSdk()); + + ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(30); + + response = handler.handleRequest(proxy, request, response.getCallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + + assertThat(response.getResourceModel().getMasterPasswordSecretArn()).isNull(); + assertThat(response.getResourceModel().getMasterPasswordSecretKmsKeyId()).isNull(); + + verify(proxyClient.client()).modifyCluster(any(ModifyClusterRequest.class)); + } + + @Test + public void testModify_UpdateMasterPasswordSecretKmsKeyId() { + ResourceModel previousModel = BASIC_MODEL.toBuilder() + .manageMasterPassword(true) + .masterPasswordSecretKmsKeyId(MASTER_PASSWORD_SECRET_KMS_KEY_ID) + .build(); + + String newMasterPasswordSecretKmsKeyId = MASTER_PASSWORD_SECRET_KMS_KEY_ID + "2"; + ResourceModel updateModel = BASIC_MODEL.toBuilder() + .manageMasterPassword(true) + .masterPasswordSecretKmsKeyId(newMasterPasswordSecretKmsKeyId) + .build(); + + final ResourceHandlerRequest request = BASIC_RESOURCE_HANDLER_REQUEST.toBuilder() + .desiredResourceState(updateModel) + .previousResourceState(previousModel) + .build(); + + Cluster existingCluster = MANAGED_ADMIN_PASSWORD_CLUSTER.toBuilder().build(); + Cluster modifiedCluster = MANAGED_ADMIN_PASSWORD_CLUSTER.toBuilder().masterPasswordSecretKmsKeyId(newMasterPasswordSecretKmsKeyId).build(); + + when(proxyClient.client().describeClusters(any(DescribeClustersRequest.class))) + .thenReturn(DescribeClustersResponse.builder() + .clusters(existingCluster) + .build()) + .thenReturn(DescribeClustersResponse.builder() + .clusters(modifiedCluster) + .build()); + + when(proxyClient.client().describeLoggingStatus(any(DescribeLoggingStatusRequest.class))) + .thenReturn(DescribeLoggingStatusResponse.builder().build()); + + when(proxyClient.client().modifyCluster(any(ModifyClusterRequest.class))) + .thenReturn(ModifyClusterResponse.builder() + .cluster(modifiedCluster) + .build()); + when(proxyClient.client().getResourcePolicy(any(GetResourcePolicyRequest.class))).thenReturn(getEmptyResourcePolicyResponseSdk()); + + ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(30); + + response = handler.handleRequest(proxy, request, response.getCallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + + assertThat(response.getResourceModel().getMasterPasswordSecretArn()). + isEqualTo(MASTER_PASSWORD_SECRET_ARN); + assertThat(response.getResourceModel().getMasterPasswordSecretKmsKeyId()). + isEqualTo(request.getDesiredResourceState().getMasterPasswordSecretKmsKeyId()); + assertThat(response.getResourceModel().getMasterUserPassword()).isNull(); + + verify(proxyClient.client()).modifyCluster(any(ModifyClusterRequest.class)); + } + private static boolean BOOLEAN_BEFORE = true; private static boolean BOOLEAN_AFTER = false; @@ -628,7 +791,9 @@ private static Stream detectableModifyClusterAttributeTest() { Arguments.of("Port", 100, 200), Arguments.of("PreferredMaintenanceWindow", "before-window", "after-window"), Arguments.of("PubliclyAccessible", BOOLEAN_BEFORE, BOOLEAN_AFTER), - Arguments.of("VpcSecurityGroupIds", Arrays.asList("before-sg-id"), Arrays.asList("after-sg-id")) + Arguments.of("VpcSecurityGroupIds", Arrays.asList("before-sg-id"), Arrays.asList("after-sg-id")), + Arguments.of("ManageMasterPassword", BOOLEAN_BEFORE, BOOLEAN_AFTER), + Arguments.of("MasterPasswordSecretKmsKeyId", "before-kms", "after-kms") ); } @@ -691,7 +856,6 @@ public void testModifyClusterAttributes( .build()); when(proxyClient.client().getResourcePolicy(any(GetResourcePolicyRequest.class))).thenReturn(getEmptyResourcePolicyResponseSdk()); - ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); assertThat(response).isNotNull();