diff --git a/.gitignore b/.gitignore index cad5726..e46bd52 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Files local to the user +/overrides.json + # Compiled class file *.class @@ -29,6 +32,9 @@ hs_err_pid* # Build target/ +# Test +.hypothesis/ + # Generated Files .classpath .factorypath diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b961a3e..efca07a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,3 +16,6 @@ repos: - --no-sort-keys - id: check-merge-conflict - id: check-yaml + args: + # Needed, or !Sub (and other CloudFormation specific tags will not be recognized) + - --unsafe diff --git a/aws-logs-loggroup/.gitignore b/aws-logs-loggroup/.gitignore index 9b36fbc..2409f7c 100644 --- a/aws-logs-loggroup/.gitignore +++ b/aws-logs-loggroup/.gitignore @@ -15,6 +15,7 @@ out/ # auto-generated files target/ +docs/README.md # our logs rpdk.log diff --git a/aws-logs-loggroup/README.md b/aws-logs-loggroup/README.md index 81918c8..a3cc2b7 100644 --- a/aws-logs-loggroup/README.md +++ b/aws-logs-loggroup/README.md @@ -1,12 +1,12 @@ # AWS::Logs::LogGroup -Congratulations on starting development! Next steps: +## Developing -1. Write the JSON schema describing your resource, `aws-logs-loggroup.json` -2. The RPDK will automatically generate the correct resource model from the +- The JSON schema describing this resource is `aws-logs-loggroup.json` +- The RPDK will automatically generate the correct resource model from the schema whenever the project is built via Maven. You can also do this manually with the following command: `cfn-cli generate` -3. Implement your resource handlers +- Resource handlers live in `src/main/java/software/amazon/logs/loggroup` Please don't modify files under `target/generated-sources/rpdk`, as they will be @@ -15,3 +15,44 @@ automatically overwritten. The code use [Lombok](https://projectlombok.org/), and [you may have to install IDE integrations](https://projectlombok.org/) to enable auto-complete for Lombok-annotated classes. + +## Running Contract Tests + +You can execute the following commands to run the tests. +You will need to have docker installed and running. + +```bash +# Create a CloudFormation stack with development dependencies (a KMS CMK) +# NOTE: this has a monthly cost of 1 USD for the CMK +aws cloudformation deploy \ + --stack-name aws-logs-loggroup-dev-resources \ + --template-file dev-resources.yaml +# Write the stack output to overrides.json +aws cloudformation describe-stacks \ + --stack-name aws-logs-loggroup-dev-resources \ + --query "Stacks[0].Outputs[?OutputKey=='OverridesJson'].OutputValue" \ + --output text > overrides.json +# Package the code with Maven +mvn package +# Start the code as a lambda function in the background +# You can also run it in a separate terminal (without the & to run it in the foreground) +sam local start-lambda & +# Test the resource handlers by running them with credentials in your account +cfn test +# Stop the lambda function in the background +kill $(jobs -lp | tail -n1) +# Destroy the CLoudFormation stack +# NOTE: destroying and recreating will increase the monthly cost +aws cloudformation delete-stack \ + --stack-name aws-logs-loggroup-dev-resources +# Wait for the stack to be completly deleted +aws cloudformation wait stack-delete-complete \ + --stack-name aws-logs-loggroup-dev-resources +``` + +Currently the following tests are broken, see issue [\#25](https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-logs/issues/25) + +- contract_create_read_success +- contract_create_list_success +- contract_update_read_success +- contract_update_list_success diff --git a/aws-logs-loggroup/aws-logs-loggroup.json b/aws-logs-loggroup/aws-logs-loggroup.json index c8b1a63..646543b 100644 --- a/aws-logs-loggroup/aws-logs-loggroup.json +++ b/aws-logs-loggroup/aws-logs-loggroup.json @@ -4,12 +4,18 @@ "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-logs.git", "properties": { "LogGroupName": { - "description": "The name of the log group. If you don't specify a name, AWS CloudFormation generates a unique ID for the log group. ", + "description": "The name of the log group. If you don't specify a name, AWS CloudFormation generates a unique ID for the log group.", "type": "string", "minLength": 1, "maxLength": 512, "pattern": "^[.\\-_/#A-Za-z0-9]{1,512}\\Z" }, + "KmsKeyId": { + "description": "The Amazon Resource Name (ARN) of the CMK to use when encrypting log data.", + "type": "string", + "maxLength": 256, + "pattern": "^arn:[a-z0-9-]+:kms:[a-z0-9-]+:\\d{12}:(key|alias)/.+\\Z" + }, "RetentionInDays": { "description": "The number of days to retain the log events in the specified log group. Possible values are: 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, and 3653.", "type": "integer", @@ -42,7 +48,8 @@ "create": { "permissions": [ "logs:DescribeLogGroups", - "logs:CreateLogGroup" + "logs:CreateLogGroup", + "logs:PutRetentionPolicy" ] }, "read": { @@ -53,6 +60,8 @@ "update": { "permissions": [ "logs:DescribeLogGroups", + "logs:AssociateKmsKey", + "logs:DisassociateKmsKey", "logs:PutRetentionPolicy", "logs:DeleteRetentionPolicy" ] diff --git a/aws-logs-loggroup/dev-resources.yaml b/aws-logs-loggroup/dev-resources.yaml new file mode 100644 index 0000000..5fed924 --- /dev/null +++ b/aws-logs-loggroup/dev-resources.yaml @@ -0,0 +1,39 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: Development dependencies for AWS::Logs::LogGroup +Resources: + Cmk: + Type: AWS::KMS::Key + Properties: + Description: KMS CMK for use with the AWS::Logs::LogGroup resource provider + KeyUsage: ENCRYPT_DECRYPT + KeyPolicy: + Version: 2012-10-17 + Id: cmk-policy + Statement: + - Sid: Enable IAM User Permissions + Effect: Allow + Principal: + AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" + Action: kms:* + Resource: "*" + - Sid: CloudWatch Logs Permissions + Effect: Allow + Principal: + Service: !Sub "logs.${AWS::Region}.${AWS::URLSuffix}" + Action: + - "kms:Encrypt*" + - "kms:Decrypt*" + - "kms:ReEncrypt*" + - "kms:GenerateDataKey*" + - "kms:Describe*" + Resource: "*" + # Alias is only added to show the StackName in the KMS Console + CmkAlias: + Type: "AWS::KMS::Alias" + Properties: + AliasName: !Sub "arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:alias/${AWS::StackName}" + TargetKeyId: !Ref Cmk +Outputs: + OverridesJson: + Description: Use this as overrides.json + Value: !Sub "{\"CREATE\": {\"/KmsKeyId\": \"${Cmk.Arn}\"}}" diff --git a/aws-logs-loggroup/resource-role.yaml b/aws-logs-loggroup/resource-role.yaml index df55ed0..57a3f2d 100644 --- a/aws-logs-loggroup/resource-role.yaml +++ b/aws-logs-loggroup/resource-role.yaml @@ -23,10 +23,12 @@ Resources: Statement: - Effect: Allow Action: + - "logs:AssociateKmsKey" - "logs:CreateLogGroup" - "logs:DeleteLogGroup" - "logs:DeleteRetentionPolicy" - "logs:DescribeLogGroups" + - "logs:DisassociateKmsKey" - "logs:PutRetentionPolicy" Resource: "*" Outputs: diff --git a/aws-logs-loggroup/src/main/java/software/amazon/logs/loggroup/Translator.java b/aws-logs-loggroup/src/main/java/software/amazon/logs/loggroup/Translator.java index 3d4421a..66d333d 100644 --- a/aws-logs-loggroup/src/main/java/software/amazon/logs/loggroup/Translator.java +++ b/aws-logs-loggroup/src/main/java/software/amazon/logs/loggroup/Translator.java @@ -6,6 +6,8 @@ import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeLogGroupsRequest; import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeLogGroupsResponse; import software.amazon.awssdk.services.cloudwatchlogs.model.PutRetentionPolicyRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DisassociateKmsKeyRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.AssociateKmsKeyRequest; import java.util.Collection; import java.util.List; @@ -39,6 +41,7 @@ static DeleteLogGroupRequest translateToDeleteRequest(final ResourceModel model) static CreateLogGroupRequest translateToCreateRequest(final ResourceModel model) { return CreateLogGroupRequest.builder() .logGroupName(model.getLogGroupName()) + .kmsKeyId(model.getKmsKeyId()) .build(); } @@ -51,8 +54,21 @@ static PutRetentionPolicyRequest translateToPutRetentionPolicyRequest(final Reso static DeleteRetentionPolicyRequest translateToDeleteRetentionPolicyRequest(final ResourceModel model) { return DeleteRetentionPolicyRequest.builder() - .logGroupName(model.getLogGroupName()) - .build(); + .logGroupName(model.getLogGroupName()) + .build(); + } + + static DisassociateKmsKeyRequest translateToDisassociateKmsKeyRequest(final ResourceModel model) { + return DisassociateKmsKeyRequest.builder() + .logGroupName(model.getLogGroupName()) + .build(); + } + + static AssociateKmsKeyRequest translateToAssociateKmsKeyRequest(final ResourceModel model) { + return AssociateKmsKeyRequest.builder() + .logGroupName(model.getLogGroupName()) + .kmsKeyId(model.getKmsKeyId()) + .build(); } static ResourceModel translateForRead(final DescribeLogGroupsResponse response) { @@ -62,19 +78,25 @@ static ResourceModel translateForRead(final DescribeLogGroupsResponse response) .findAny() .orElse(null); final String logGroupArn = streamOfOrEmpty(response.logGroups()) - .map(software.amazon.awssdk.services.cloudwatchlogs.model.LogGroup::arn) - .filter(Objects::nonNull) - .findAny() - .orElse(null); + .map(software.amazon.awssdk.services.cloudwatchlogs.model.LogGroup::arn) + .filter(Objects::nonNull) + .findAny() + .orElse(null); final Integer retentionInDays = streamOfOrEmpty(response.logGroups()) .map(software.amazon.awssdk.services.cloudwatchlogs.model.LogGroup::retentionInDays) .filter(Objects::nonNull) .findAny() .orElse(null); + final String kmsKeyId = streamOfOrEmpty(response.logGroups()) + .map(software.amazon.awssdk.services.cloudwatchlogs.model.LogGroup::kmsKeyId) + .filter(Objects::nonNull) + .findAny() + .orElse(null); return ResourceModel.builder() .arn(logGroupArn) .logGroupName(logGroupName) .retentionInDays(retentionInDays) + .kmsKeyId(kmsKeyId) .build(); } @@ -84,6 +106,7 @@ static List translateForList(final DescribeLogGroupsResponse resp .arn(logGroup.arn()) .logGroupName(logGroup.logGroupName()) .retentionInDays(logGroup.retentionInDays()) + .kmsKeyId(logGroup.kmsKeyId()) .build()) .collect(Collectors.toList()); } diff --git a/aws-logs-loggroup/src/main/java/software/amazon/logs/loggroup/UpdateHandler.java b/aws-logs-loggroup/src/main/java/software/amazon/logs/loggroup/UpdateHandler.java index 8844b79..8705beb 100644 --- a/aws-logs-loggroup/src/main/java/software/amazon/logs/loggroup/UpdateHandler.java +++ b/aws-logs-loggroup/src/main/java/software/amazon/logs/loggroup/UpdateHandler.java @@ -1,12 +1,20 @@ package software.amazon.logs.loggroup; +import software.amazon.cloudformation.exceptions.CfnInternalFailureException; +import software.amazon.cloudformation.exceptions.CfnResourceConflictException; +import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; import software.amazon.awssdk.services.cloudwatchlogs.model.DeleteRetentionPolicyRequest; import software.amazon.awssdk.services.cloudwatchlogs.model.PutRetentionPolicyRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DisassociateKmsKeyRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.AssociateKmsKeyRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.InvalidParameterException; +import software.amazon.awssdk.services.cloudwatchlogs.model.OperationAbortedException; import software.amazon.awssdk.services.cloudwatchlogs.model.ResourceNotFoundException; +import software.amazon.awssdk.services.cloudwatchlogs.model.ServiceUnavailableException; import java.util.Objects; @@ -19,15 +27,26 @@ public ProgressEvent handleRequest( final CallbackContext callbackContext, final Logger logger) { - // RetentionPolicyInDays is the only attribute that is not createOnly + // Everything except RetentionPolicyInDays and KmsKeyId is createOnly final ResourceModel model = request.getDesiredResourceState(); - - if (model.getRetentionInDays() == null) { + final ResourceModel previousModel = request.getPreviousResourceState(); + final boolean retentionChanged = ! retentionUnchanged(previousModel, model); + final boolean kmsKeyChanged = ! kmsKeyUnchanged(previousModel, model); + if (retentionChanged && model.getRetentionInDays() == null) { deleteRetentionPolicy(proxy, request, logger); - } else { + } else if (retentionChanged){ putRetentionPolicy(proxy, request, logger); } + // It can take up to five minutes for the (dis)associate operation to take effect + // It's unclear from the documentation if that state can be checked via the API. + // https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/encrypt-log-data-kms.html + if (kmsKeyChanged && model.getKmsKeyId() == null) { + disassociateKmsKey(proxy, request, logger); + } else if (kmsKeyChanged) { + associateKmsKey(proxy, request, logger); + } + return ProgressEvent.defaultSuccessHandler(model); } @@ -69,8 +88,77 @@ private void putRetentionPolicy(final AmazonWebServicesClientProxy proxy, logger.log(retentionPolicyMessage); } + private void disassociateKmsKey(final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final Logger logger) { + final ResourceModel model = request.getDesiredResourceState(); + final DisassociateKmsKeyRequest disassociateKmsKeyRequest = + Translator.translateToDisassociateKmsKeyRequest(model); + try { + proxy.injectCredentialsAndInvokeV2(disassociateKmsKeyRequest, + ClientBuilder.getClient()::disassociateKmsKey); + } catch (final ResourceNotFoundException e) { + // The specified resource does not exist. + throwNotFoundException(model); + } catch (final InvalidParameterException e) { + // A parameter is specified incorrectly. We should be passing valid parameters. + throw new CfnInternalFailureException(e); + } catch (final OperationAbortedException e){ + // Multiple requests to update the same resource were in conflict. + throw new CfnResourceConflictException(ResourceModel.TYPE_NAME, + Objects.toString(model.getPrimaryIdentifier()), "OperationAborted", e); + } catch (final ServiceUnavailableException e) { + // The service cannot complete the request. + throw new CfnServiceInternalErrorException(e); + } + + final String kmsKeyMessage = + String.format("%s [%s] successfully disassociated kms key.", + ResourceModel.TYPE_NAME, model.getLogGroupName()); + logger.log(kmsKeyMessage); + } + + private void associateKmsKey(final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final Logger logger) { + final ResourceModel model = request.getDesiredResourceState(); + final AssociateKmsKeyRequest associateKmsKeyRequest = + Translator.translateToAssociateKmsKeyRequest(model); + try { + proxy.injectCredentialsAndInvokeV2(associateKmsKeyRequest, + ClientBuilder.getClient()::associateKmsKey); + } catch (final ResourceNotFoundException e) { + // The specified resource does not exist. + throwNotFoundException(model); + } catch (final InvalidParameterException e) { + // A parameter is specified incorrectly. We should be passing valid parameters. + throw new CfnInternalFailureException(e); + } catch (final OperationAbortedException e){ + // Multiple requests to update the same resource were in conflict. + throw new CfnResourceConflictException(ResourceModel.TYPE_NAME, + Objects.toString(model.getPrimaryIdentifier()), "OperationAborted", e); + } catch (final ServiceUnavailableException e) { + // The service cannot complete the request. + throw new CfnServiceInternalErrorException(e); + } + + final String kmsKeyMessage = + String.format("%s [%s] successfully associated kms key: [%s].", + ResourceModel.TYPE_NAME, model.getLogGroupName(), model.getKmsKeyId()); + logger.log(kmsKeyMessage); + } + private void throwNotFoundException(final ResourceModel model) { throw new software.amazon.cloudformation.exceptions.ResourceNotFoundException(ResourceModel.TYPE_NAME, Objects.toString(model.getPrimaryIdentifier())); } + + + private static boolean retentionUnchanged(final ResourceModel previousModel, final ResourceModel model) { + return (previousModel != null && model.getRetentionInDays().equals(previousModel.getRetentionInDays())); + } + + private static boolean kmsKeyUnchanged(final ResourceModel previousModel, final ResourceModel model) { + return (previousModel != null && model.getKmsKeyId().equals(previousModel.getKmsKeyId())); + } } diff --git a/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/CreateHandlerTest.java b/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/CreateHandlerTest.java index 78bbe1d..b334e70 100644 --- a/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/CreateHandlerTest.java +++ b/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/CreateHandlerTest.java @@ -53,6 +53,7 @@ public void handleRequest_Success() { final LogGroup logGroup = LogGroup.builder() .logGroupName("LogGroup") .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); doReturn(describeResponseInitial, createLogGroupResponse, putRetentionPolicyResponse) @@ -65,6 +66,7 @@ public void handleRequest_Success() { final ResourceModel model = ResourceModel.builder() .logGroupName("LogGroup") .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final ResourceHandlerRequest request = ResourceHandlerRequest.builder() @@ -131,6 +133,7 @@ public void handleRequest_SuccessGeneratedLogGroupName() { final ResourceModel model = ResourceModel.builder() .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final ResourceHandlerRequest request = ResourceHandlerRequest.builder() @@ -171,6 +174,7 @@ public void handleRequest_FailureAlreadyExists() { final ResourceModel model = ResourceModel.builder() .logGroupName("LogGroup") .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final ResourceHandlerRequest request = ResourceHandlerRequest.builder() diff --git a/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/DeleteHandlerTest.java b/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/DeleteHandlerTest.java index cd931c4..95b348f 100644 --- a/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/DeleteHandlerTest.java +++ b/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/DeleteHandlerTest.java @@ -48,6 +48,7 @@ public void handleRequest_Success() { final LogGroup logGroup = LogGroup.builder() .logGroupName("LogGroup") .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final DescribeLogGroupsResponse describeResponse = DescribeLogGroupsResponse.builder() .logGroups(Collections.singletonList(logGroup)) @@ -63,6 +64,7 @@ public void handleRequest_Success() { final ResourceModel model = ResourceModel.builder() .logGroupName("LogGroup") .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final ResourceHandlerRequest request = ResourceHandlerRequest.builder() @@ -93,6 +95,7 @@ public void handleRequest_FailureNotFound() { final ResourceModel model = ResourceModel.builder() .logGroupName("LogGroup") .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final ResourceHandlerRequest request = ResourceHandlerRequest.builder() diff --git a/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/ListHandlerTest.java b/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/ListHandlerTest.java index cb9a405..22256ee 100644 --- a/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/ListHandlerTest.java +++ b/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/ListHandlerTest.java @@ -42,10 +42,12 @@ public void handleRequest_Success() { final LogGroup logGroup = LogGroup.builder() .logGroupName("LogGroup") .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final LogGroup logGroup2 = LogGroup.builder() .logGroupName("LogGroup2") .retentionInDays(2) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb") .build(); final DescribeLogGroupsResponse describeResponse = DescribeLogGroupsResponse.builder() .logGroups(Arrays.asList(logGroup, logGroup2)) @@ -62,11 +64,13 @@ public void handleRequest_Success() { final ResourceModel model1 = ResourceModel.builder() .logGroupName("LogGroup") .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final ResourceModel model2 = ResourceModel.builder() .logGroupName("LogGroup2") .retentionInDays(2) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb") .build(); final ResourceHandlerRequest request = ResourceHandlerRequest.builder() diff --git a/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/ReadHandlerTest.java b/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/ReadHandlerTest.java index 7c6b45b..93453e6 100644 --- a/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/ReadHandlerTest.java +++ b/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/ReadHandlerTest.java @@ -45,6 +45,7 @@ public void handleRequest_Success() { final LogGroup logGroup = LogGroup.builder() .logGroupName("LogGroup") .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final DescribeLogGroupsResponse describeResponse = DescribeLogGroupsResponse.builder() .logGroups(Collections.singletonList(logGroup)) @@ -60,6 +61,7 @@ public void handleRequest_Success() { final ResourceModel model = ResourceModel.builder() .logGroupName("LogGroup") .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final ResourceHandlerRequest request = ResourceHandlerRequest.builder() @@ -94,6 +96,7 @@ public void handleRequest_FailureNotFound_EmptyLogGroupResponse() { final ResourceModel model = ResourceModel.builder() .logGroupName("LogGroup") .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final ResourceHandlerRequest request = ResourceHandlerRequest.builder() @@ -116,6 +119,7 @@ public void handleRequest_FailureNotFound_WithException() { final ResourceModel model = ResourceModel.builder() .logGroupName("LogGroup") .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final ResourceHandlerRequest request = ResourceHandlerRequest.builder() diff --git a/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/TranslatorTest.java b/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/TranslatorTest.java index a022635..63f4083 100644 --- a/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/TranslatorTest.java +++ b/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/TranslatorTest.java @@ -10,6 +10,8 @@ import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeLogGroupsResponse; import software.amazon.awssdk.services.cloudwatchlogs.model.LogGroup; import software.amazon.awssdk.services.cloudwatchlogs.model.PutRetentionPolicyRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DisassociateKmsKeyRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.AssociateKmsKeyRequest; import java.util.Collections; @@ -19,6 +21,7 @@ public class TranslatorTest { private static final ResourceModel RESOURCE_MODEL = ResourceModel.builder() .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .logGroupName("LogGroup") .build(); @@ -51,6 +54,7 @@ public void testTranslateToDelete() { public void testTranslateToCreate() { final CreateLogGroupRequest request = CreateLogGroupRequest.builder() .logGroupName(RESOURCE_MODEL.getLogGroupName()) + .kmsKeyId(RESOURCE_MODEL.getKmsKeyId()) .build(); assertThat(Translator.translateToCreateRequest(RESOURCE_MODEL)).isEqualToComparingFieldByField(request); } @@ -76,11 +80,33 @@ public void testTranslateToDeleteRetentionPolicyRequest() { .isEqualToComparingFieldByField(request); } + @Test + public void testTranslateToDisassociateKmsKeyRequest() { + final DisassociateKmsKeyRequest request = DisassociateKmsKeyRequest.builder() + .logGroupName(RESOURCE_MODEL.getLogGroupName()) + .build(); + + assertThat(Translator.translateToDisassociateKmsKeyRequest(RESOURCE_MODEL)) + .isEqualToComparingFieldByField(request); + } + + @Test + public void testTranslateToAssociateKmsKeyRequest() { + final AssociateKmsKeyRequest request = AssociateKmsKeyRequest.builder() + .logGroupName(RESOURCE_MODEL.getLogGroupName()) + .kmsKeyId(RESOURCE_MODEL.getKmsKeyId()) + .build(); + + assertThat(Translator.translateToAssociateKmsKeyRequest(RESOURCE_MODEL)) + .isEqualToComparingFieldByField(request); + } + @Test public void testTranslateForRead() { final LogGroup logGroup = LogGroup.builder() .logGroupName("LogGroup") .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final DescribeLogGroupsResponse response = DescribeLogGroupsResponse.builder() diff --git a/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/UpdateHandlerTest.java b/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/UpdateHandlerTest.java index c588537..a23aee2 100644 --- a/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/UpdateHandlerTest.java +++ b/aws-logs-loggroup/src/test/java/software/amazon/logs/loggroup/UpdateHandlerTest.java @@ -15,6 +15,11 @@ import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeLogGroupsResponse; import software.amazon.awssdk.services.cloudwatchlogs.model.LogGroup; import software.amazon.awssdk.services.cloudwatchlogs.model.PutRetentionPolicyResponse; +import software.amazon.awssdk.services.cloudwatchlogs.model.DisassociateKmsKeyResponse; +import software.amazon.awssdk.services.cloudwatchlogs.model.PutRetentionPolicyRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DeleteRetentionPolicyRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.AssociateKmsKeyRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DisassociateKmsKeyRequest; import java.util.Arrays; import java.util.Collections; @@ -48,6 +53,7 @@ public void handleRequest_Success() { final LogGroup logGroup = LogGroup.builder() .logGroupName("LogGroup") .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final DescribeLogGroupsResponse describeResponse = DescribeLogGroupsResponse.builder() .logGroups(Collections.singletonList(logGroup)) @@ -63,6 +69,7 @@ public void handleRequest_Success() { final ResourceModel model = ResourceModel.builder() .logGroupName("LogGroup") .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final ResourceHandlerRequest request = ResourceHandlerRequest.builder() @@ -86,6 +93,7 @@ public void handleRequest_Success_RetentionPolicyDeleted() { final DeleteRetentionPolicyResponse deleteRetentionPolicyResponse = DeleteRetentionPolicyResponse.builder().build(); final LogGroup logGroup = LogGroup.builder() .logGroupName("LogGroup") + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final DescribeLogGroupsResponse describeResponse = DescribeLogGroupsResponse.builder() .logGroups(Collections.singletonList(logGroup)) @@ -100,6 +108,7 @@ public void handleRequest_Success_RetentionPolicyDeleted() { final ResourceModel model = ResourceModel.builder() .logGroupName("LogGroup") + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final ResourceHandlerRequest request = ResourceHandlerRequest.builder() @@ -118,11 +127,51 @@ public void handleRequest_Success_RetentionPolicyDeleted() { assertThat(response.getErrorCode()).isNull(); } + @Test + public void handleRequest_Success_KmsKeyIdDeleted() { + final DisassociateKmsKeyResponse disassociateKmsKeyResponse = DisassociateKmsKeyResponse.builder().build(); + final LogGroup logGroup = LogGroup.builder() + .logGroupName("LogGroup") + .retentionInDays(1) + .build(); + final DescribeLogGroupsResponse describeResponse = DescribeLogGroupsResponse.builder() + .logGroups(Collections.singletonList(logGroup)) + .build(); + + doReturn(disassociateKmsKeyResponse, describeResponse) + .when(proxy) + .injectCredentialsAndInvokeV2( + ArgumentMatchers.any(), + ArgumentMatchers.any() + ); + + final ResourceModel model = ResourceModel.builder() + .logGroupName("LogGroup") + .retentionInDays(1) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, null, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getResourceModel()).isEqualToComparingFieldByField(logGroup); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + @Test public void handleRequest_SuccessNoChange() { final LogGroup initialLogGroup = LogGroup.builder() .logGroupName("LogGroup") .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final DescribeLogGroupsResponse initialDescribeResponse = DescribeLogGroupsResponse.builder() .logGroups(Collections.singletonList(initialLogGroup)) @@ -130,6 +179,7 @@ public void handleRequest_SuccessNoChange() { final LogGroup logGroup = LogGroup.builder() .logGroupName("LogGroup") .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final DescribeLogGroupsResponse describeResponse = DescribeLogGroupsResponse.builder() .logGroups(Collections.singletonList(logGroup)) @@ -145,6 +195,7 @@ public void handleRequest_SuccessNoChange() { final ResourceModel model = ResourceModel.builder() .logGroupName("LogGroup") .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final ResourceHandlerRequest request = ResourceHandlerRequest.builder() @@ -163,6 +214,42 @@ public void handleRequest_SuccessNoChange() { assertThat(response.getErrorCode()).isNull(); } + @Test + public void handleRequest_SuccessNoChange_NoAction_WithPreviousModel() { + final LogGroup logGroup = LogGroup.builder() + .logGroupName("LogGroup") + .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") + .build(); + final ResourceModel previousModel = ResourceModel.builder() + .logGroupName("LogGroup") + .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") + .build(); + + final ResourceModel model = ResourceModel.builder() + .logGroupName("LogGroup") + .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .previousResourceState(previousModel) + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, null, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getResourceModel()).isEqualToComparingFieldByField(logGroup); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + @Test public void handleRequest_FailureNotFound_ServiceException() { doThrow(software.amazon.awssdk.services.cloudwatchlogs.model.ResourceNotFoundException.class) @@ -175,6 +262,7 @@ public void handleRequest_FailureNotFound_ServiceException() { final ResourceModel model = ResourceModel.builder() .logGroupName("LogGroup") .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") .build(); final ResourceHandlerRequest request = ResourceHandlerRequest.builder() @@ -184,4 +272,284 @@ public void handleRequest_FailureNotFound_ServiceException() { assertThrows(software.amazon.cloudformation.exceptions.ResourceNotFoundException.class, () -> handler.handleRequest(proxy, request, null, logger)); } + + @Test + public void handleRequest_PutRetention_FailureNotFound_ServiceException() { + doThrow(software.amazon.awssdk.services.cloudwatchlogs.model.ResourceNotFoundException.class) + .when(proxy) + .injectCredentialsAndInvokeV2( + ArgumentMatchers.isA(PutRetentionPolicyRequest.class), + ArgumentMatchers.any() + ); + + final ResourceModel model = ResourceModel.builder() + .logGroupName("LogGroup") + .retentionInDays(1) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + assertThrows(software.amazon.cloudformation.exceptions.ResourceNotFoundException.class, + () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_DeleteRetention_FailureNotFound_ServiceException() { + doThrow(software.amazon.awssdk.services.cloudwatchlogs.model.ResourceNotFoundException.class) + .when(proxy) + .injectCredentialsAndInvokeV2( + ArgumentMatchers.isA(DeleteRetentionPolicyRequest.class), + ArgumentMatchers.any() + ); + + final ResourceModel model = ResourceModel.builder() + .logGroupName("LogGroup") + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + assertThrows(software.amazon.cloudformation.exceptions.ResourceNotFoundException.class, + () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_AssociateKms_FailureNotFound_ServiceException() { + final PutRetentionPolicyResponse putRetentionPolicyResponse = PutRetentionPolicyResponse.builder().build(); + doReturn(putRetentionPolicyResponse) + .when(proxy) + .injectCredentialsAndInvokeV2( + ArgumentMatchers.isA(PutRetentionPolicyRequest.class), + ArgumentMatchers.any() + ); + doThrow(software.amazon.awssdk.services.cloudwatchlogs.model.ResourceNotFoundException.class) + .when(proxy) + .injectCredentialsAndInvokeV2( + ArgumentMatchers.isA(AssociateKmsKeyRequest.class), + ArgumentMatchers.any() + ); + + final ResourceModel model = ResourceModel.builder() + .logGroupName("LogGroup") + .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + assertThrows(software.amazon.cloudformation.exceptions.ResourceNotFoundException.class, + () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_AssociateKms_InvalidParameter_ServiceException() { + final PutRetentionPolicyResponse putRetentionPolicyResponse = PutRetentionPolicyResponse.builder().build(); + doReturn(putRetentionPolicyResponse) + .when(proxy) + .injectCredentialsAndInvokeV2( + ArgumentMatchers.isA(PutRetentionPolicyRequest.class), + ArgumentMatchers.any() + ); + doThrow(software.amazon.awssdk.services.cloudwatchlogs.model.InvalidParameterException.class) + .when(proxy) + .injectCredentialsAndInvokeV2( + ArgumentMatchers.isA(AssociateKmsKeyRequest.class), + ArgumentMatchers.any() + ); + + final ResourceModel model = ResourceModel.builder() + .logGroupName("LogGroup") + .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + assertThrows(software.amazon.cloudformation.exceptions.CfnInternalFailureException.class, + () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_AssociateKms_OperationAborted_ServiceException() { + final PutRetentionPolicyResponse putRetentionPolicyResponse = PutRetentionPolicyResponse.builder().build(); + doReturn(putRetentionPolicyResponse) + .when(proxy) + .injectCredentialsAndInvokeV2( + ArgumentMatchers.isA(PutRetentionPolicyRequest.class), + ArgumentMatchers.any() + ); + doThrow(software.amazon.awssdk.services.cloudwatchlogs.model.OperationAbortedException.class) + .when(proxy) + .injectCredentialsAndInvokeV2( + ArgumentMatchers.isA(AssociateKmsKeyRequest.class), + ArgumentMatchers.any() + ); + + final ResourceModel model = ResourceModel.builder() + .logGroupName("LogGroup") + .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + assertThrows(software.amazon.cloudformation.exceptions.CfnResourceConflictException.class, + () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_AssociateKms_ServiceUnavailable_ServiceException() { + final PutRetentionPolicyResponse putRetentionPolicyResponse = PutRetentionPolicyResponse.builder().build(); + doReturn(putRetentionPolicyResponse) + .when(proxy) + .injectCredentialsAndInvokeV2( + ArgumentMatchers.isA(PutRetentionPolicyRequest.class), + ArgumentMatchers.any() + ); + doThrow(software.amazon.awssdk.services.cloudwatchlogs.model.ServiceUnavailableException.class) + .when(proxy) + .injectCredentialsAndInvokeV2( + ArgumentMatchers.isA(AssociateKmsKeyRequest.class), + ArgumentMatchers.any() + ); + + final ResourceModel model = ResourceModel.builder() + .logGroupName("LogGroup") + .retentionInDays(1) + .kmsKeyId("arn:aws:kms:us-east-1:$123456789012:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + assertThrows(software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException.class, + () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_DisassociateKms_FailureNotFound_ServiceException() { + final PutRetentionPolicyResponse putRetentionPolicyResponse = PutRetentionPolicyResponse.builder().build(); + doReturn(putRetentionPolicyResponse) + .when(proxy) + .injectCredentialsAndInvokeV2( + ArgumentMatchers.isA(PutRetentionPolicyRequest.class), + ArgumentMatchers.any() + ); + doThrow(software.amazon.awssdk.services.cloudwatchlogs.model.ResourceNotFoundException.class) + .when(proxy) + .injectCredentialsAndInvokeV2( + ArgumentMatchers.isA(DisassociateKmsKeyRequest.class), + ArgumentMatchers.any() + ); + + final ResourceModel model = ResourceModel.builder() + .logGroupName("LogGroup") + .retentionInDays(1) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + assertThrows(software.amazon.cloudformation.exceptions.ResourceNotFoundException.class, + () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_DisassociateKms_InvalidParameter_ServiceException() { + final PutRetentionPolicyResponse putRetentionPolicyResponse = PutRetentionPolicyResponse.builder().build(); + doReturn(putRetentionPolicyResponse) + .when(proxy) + .injectCredentialsAndInvokeV2( + ArgumentMatchers.isA(PutRetentionPolicyRequest.class), + ArgumentMatchers.any() + ); + doThrow(software.amazon.awssdk.services.cloudwatchlogs.model.InvalidParameterException.class) + .when(proxy) + .injectCredentialsAndInvokeV2( + ArgumentMatchers.isA(DisassociateKmsKeyRequest.class), + ArgumentMatchers.any() + ); + + final ResourceModel model = ResourceModel.builder() + .logGroupName("LogGroup") + .retentionInDays(1) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + assertThrows(software.amazon.cloudformation.exceptions.CfnInternalFailureException.class, + () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_DisassociateKms_OperationAborted_ServiceException() { + final PutRetentionPolicyResponse putRetentionPolicyResponse = PutRetentionPolicyResponse.builder().build(); + doReturn(putRetentionPolicyResponse) + .when(proxy) + .injectCredentialsAndInvokeV2( + ArgumentMatchers.isA(PutRetentionPolicyRequest.class), + ArgumentMatchers.any() + ); + doThrow(software.amazon.awssdk.services.cloudwatchlogs.model.OperationAbortedException.class) + .when(proxy) + .injectCredentialsAndInvokeV2( + ArgumentMatchers.isA(DisassociateKmsKeyRequest.class), + ArgumentMatchers.any() + ); + + final ResourceModel model = ResourceModel.builder() + .logGroupName("LogGroup") + .retentionInDays(1) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + assertThrows(software.amazon.cloudformation.exceptions.CfnResourceConflictException.class, + () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_DisassociateKms_ServiceUnavailable_ServiceException() { + final PutRetentionPolicyResponse putRetentionPolicyResponse = PutRetentionPolicyResponse.builder().build(); + doReturn(putRetentionPolicyResponse) + .when(proxy) + .injectCredentialsAndInvokeV2( + ArgumentMatchers.isA(PutRetentionPolicyRequest.class), + ArgumentMatchers.any() + ); + doThrow(software.amazon.awssdk.services.cloudwatchlogs.model.ServiceUnavailableException.class) + .when(proxy) + .injectCredentialsAndInvokeV2( + ArgumentMatchers.isA(DisassociateKmsKeyRequest.class), + ArgumentMatchers.any() + ); + + final ResourceModel model = ResourceModel.builder() + .logGroupName("LogGroup") + .retentionInDays(1) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + assertThrows(software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException.class, + () -> handler.handleRequest(proxy, request, null, logger)); + } }