diff --git a/aws-logs-metricfilter/.gitignore b/aws-logs-metricfilter/.gitignore new file mode 100644 index 0000000..2fd9f21 --- /dev/null +++ b/aws-logs-metricfilter/.gitignore @@ -0,0 +1,20 @@ +# macOS +.DS_Store +._* + +# Maven outputs +.classpath + +# IntelliJ +*.iml +.idea/ +out.java +out/ +.settings +.project + +# auto-generated files +target/ + +# our logs +rpdk.log diff --git a/aws-logs-metricfilter/.rpdk-config b/aws-logs-metricfilter/.rpdk-config new file mode 100644 index 0000000..c3249d4 --- /dev/null +++ b/aws-logs-metricfilter/.rpdk-config @@ -0,0 +1,16 @@ +{ + "typeName": "AWS::Logs::MetricFilter", + "language": "java", + "runtime": "java8", + "entrypoint": "software.amazon.logs.metricfilter.HandlerWrapper::handleRequest", + "testEntrypoint": "software.amazon.logs.metricfilter.HandlerWrapper::testEntrypoint", + "settings": { + "namespace": [ + "software", + "amazon", + "logs", + "metricfilter" + ], + "codegen_template_path": "guided_aws" + } +} diff --git a/aws-logs-metricfilter/README.md b/aws-logs-metricfilter/README.md new file mode 100644 index 0000000..ca5e3dc --- /dev/null +++ b/aws-logs-metricfilter/README.md @@ -0,0 +1,12 @@ +# AWS::Logs::MetricFilter + +Congratulations on starting development! Next steps: + +1. Write the JSON schema describing your resource, `aws-logs-metricfilter.json` +1. Implement your resource handlers. + +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 generate`. + +> Please don't modify files under `target/generated-sources/rpdk`, as they will be automatically overwritten. + +The code uses [Lombok](https://projectlombok.org/), and [you may have to install IDE integrations](https://projectlombok.org/setup/overview) to enable auto-complete for Lombok-annotated classes. diff --git a/aws-logs-metricfilter/aws-logs-metricfilter.json b/aws-logs-metricfilter/aws-logs-metricfilter.json new file mode 100644 index 0000000..0522593 --- /dev/null +++ b/aws-logs-metricfilter/aws-logs-metricfilter.json @@ -0,0 +1,123 @@ +{ + "typeName": "AWS::Logs::MetricFilter", + "resourceLink": { + "templateUri": "/cloudwatch/home?region=${awsRegion}#logsV2:log-groups/log-group/${LogGroupName}/edit-metric-filter/${MetricName}", + "mappings": { + "MetricName": "/MetricName", + "LogGroupName": "/LogGroupName" + } + }, + "description": "Specifies a metric filter that describes how CloudWatch Logs extracts information from logs and transforms it into Amazon CloudWatch metrics.", + "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-logs.git", + "definitions": { + "MetricTransformation": { + "type": "object", + "properties": { + "DefaultValue": { + "description": "The value to emit when a filter pattern does not match a log event. This value can be null.", + "type": "number" + }, + "MetricName": { + "description": "The name of the CloudWatch metric. Metric name must be in ASCII format.", + "type": "string", + "minLength": 1, + "maxLength": 255, + "pattern": "^((?![:*$])[\\x00-\\x7F]){1,255}" + }, + "MetricNamespace": { + "$comment": "Namespaces can be up to 256 characters long; valid characters include 0-9A-Za-z.-_/#", + "description": "The namespace of the CloudWatch metric.", + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^[0-9a-zA-Z\\.\\-_\\/#]{1,256}" + }, + "MetricValue": { + "description": "The value to publish to the CloudWatch metric when a filter pattern matches a log event.", + "type": "string", + "minLength": 1, + "maxLength": 100 + } + }, + "required": [ + "MetricName", + "MetricNamespace", + "MetricValue" + ], + "additionalProperties": false + } + }, + "properties": { + "FilterName": { + "description": "A name for the metric filter.", + "type": "string", + "minLength": 1, + "maxLength": 512, + "pattern": "^[^:*]{1,512}" + }, + "FilterPattern": { + "description": "Pattern that Logs follows to interpret each entry in a log.", + "type": "string", + "maxLength": 1024 + }, + "LogGroupName": { + "description": "Existing log group that you want to associate with this filter.", + "type": "string", + "minLength": 1, + "maxLength": 512, + "pattern": "^[.\\-_/#A-Za-z0-9]{1,512}" + }, + "MetricTransformations": { + "description": "A collection of information that defines how metric data gets emitted.", + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "$ref": "#/definitions/MetricTransformation" + } + } + }, + "handlers": { + "create": { + "permissions": [ + "logs:PutMetricFilter", + "logs:DescribeMetricFilters" + ] + }, + "read": { + "permissions": [ + "logs:DescribeMetricFilters" + ] + }, + "update": { + "permissions": [ + "logs:PutMetricFilter", + "logs:DescribeMetricFilters" + ] + }, + "delete": { + "permissions": [ + "logs:DeleteMetricFilter" + ] + }, + "list": { + "permissions": [ + "logs:DescribeMetricFilters" + ] + } + }, + "required": [ + "FilterPattern", + "LogGroupName", + "MetricTransformations" + ], + "createOnlyProperties": [ + "/properties/FilterName", + "/properties/LogGroupName" + ], + "primaryIdentifier": [ + "/properties/LogGroupName", + "/properties/FilterName" + ], + "additionalProperties": false +} diff --git a/aws-logs-metricfilter/lombok.config b/aws-logs-metricfilter/lombok.config new file mode 100644 index 0000000..7a21e88 --- /dev/null +++ b/aws-logs-metricfilter/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true diff --git a/aws-logs-metricfilter/overrides.json b/aws-logs-metricfilter/overrides.json new file mode 100644 index 0000000..15778d3 --- /dev/null +++ b/aws-logs-metricfilter/overrides.json @@ -0,0 +1,14 @@ +{ + "CREATE": { + "/LogGroupName": "this-is-a-random-loggroup", + "/FilterName": "my-metric-filter", + "/FilterPattern": "[size]", + "/MetricTransformations": [ + { + "MetricValue": 10, + "MetricNamespace": "my-namespace", + "MetricName": "metric-name" + } + ] + } +} diff --git a/aws-logs-metricfilter/pom.xml b/aws-logs-metricfilter/pom.xml new file mode 100644 index 0000000..68ca2d9 --- /dev/null +++ b/aws-logs-metricfilter/pom.xml @@ -0,0 +1,209 @@ + + + 4.0.0 + + software.amazon.logs.metricfilter + aws-logs-metricfilter-handler + aws-logs-metricfilter-handler + 1.0-SNAPSHOT + jar + + + 1.8 + 1.8 + UTF-8 + UTF-8 + + + + + + software.amazon.cloudformation + aws-cloudformation-rpdk-java-plugin + 1.0.4 + + + + org.projectlombok + lombok + 1.18.4 + provided + + + + org.assertj + assertj-core + 3.12.2 + test + + + + org.junit.jupiter + junit-jupiter + 5.5.0-M1 + test + + + + org.mockito + mockito-core + 2.26.0 + test + + + + org.mockito + mockito-junit-jupiter + 2.26.0 + test + + + + software.amazon.awssdk + cloudwatchlogs + 2.10.49 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + -Xlint:all,-options,-processing + -Werror + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + false + + + + package + + shade + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + + generate + generate-sources + + exec + + + cfn + generate + ${project.basedir} + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + add-source + generate-sources + + add-source + + + + ${project.basedir}/target/generated-sources/rpdk + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 2.4 + + + maven-surefire-plugin + 3.0.0-M3 + + + org.jacoco + jacoco-maven-plugin + 0.8.4 + + + **/BaseConfiguration* + **/BaseHandler* + **/HandlerWrapper* + **/ResourceModel* + + + + + + prepare-agent + + + + report + test + + report + + + + jacoco-check + + check + + + + + PACKAGE + + + BRANCH + COVEREDRATIO + 0.8 + + + INSTRUCTION + COVEREDRATIO + 0.8 + + + + + + + + + + + + ${project.basedir} + + aws-logs-metricfilter.json + + + + + diff --git a/aws-logs-metricfilter/resource-role.yaml b/aws-logs-metricfilter/resource-role.yaml new file mode 100644 index 0000000..49ed490 --- /dev/null +++ b/aws-logs-metricfilter/resource-role.yaml @@ -0,0 +1,33 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: > + This CloudFormation template creates a role assumed by CloudFormation + during CRUDL operations to mutate resources on behalf of the customer. + +Resources: + ExecutionRole: + Type: AWS::IAM::Role + Properties: + MaxSessionDuration: 8400 + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: resources.cloudformation.amazonaws.com + Action: sts:AssumeRole + Path: "/" + Policies: + - PolicyName: ResourceTypePolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - "logs:DeleteMetricFilter" + - "logs:DescribeMetricFilters" + - "logs:PutMetricFilter" + Resource: "*" +Outputs: + ExecutionRoleArn: + Value: + Fn::GetAtt: ExecutionRole.Arn diff --git a/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/BaseHandlerStd.java b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/BaseHandlerStd.java new file mode 100644 index 0000000..93e2dac --- /dev/null +++ b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/BaseHandlerStd.java @@ -0,0 +1,58 @@ +package software.amazon.logs.metricfilter; + +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeMetricFiltersRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeMetricFiltersResponse; +import software.amazon.awssdk.services.cloudwatchlogs.model.InvalidParameterException; +import software.amazon.awssdk.services.cloudwatchlogs.model.ServiceUnavailableException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.CallChain; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +public abstract class BaseHandlerStd extends BaseHandler { + @Override + public final ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final Logger logger) { + return handleRequest( + proxy, + request, + callbackContext != null ? callbackContext : new CallbackContext(), + proxy.newProxy(ClientBuilder::getClient), + logger + ); + } + + protected abstract ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger); + + protected CallChain.Completed + preCreateCheck(final AmazonWebServicesClientProxy proxy, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final ResourceModel model) { + + return proxy.initiate("AWS-Logs-MetricFilter::PreExistenceCheck", proxyClient, model, callbackContext) + .translateToServiceRequest(Translator::translateToReadRequest) + .makeServiceCall((awsRequest, sdkProxyClient) -> sdkProxyClient.injectCredentialsAndInvokeV2(awsRequest, sdkProxyClient.client()::describeMetricFilters)) + .handleError((request, exception, client, model1, context1) -> { + if (exception instanceof InvalidParameterException) { + return ProgressEvent.failed(model, callbackContext, HandlerErrorCode.InvalidRequest, exception.getMessage()); + } + else if (exception instanceof ServiceUnavailableException) { + return ProgressEvent.failed(model, callbackContext, HandlerErrorCode.ServiceInternalError, exception.getMessage()); + } + return ProgressEvent.progress(model, callbackContext); + }); + } +} diff --git a/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/CallbackContext.java b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/CallbackContext.java new file mode 100644 index 0000000..67767e9 --- /dev/null +++ b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/CallbackContext.java @@ -0,0 +1,10 @@ +package software.amazon.logs.metricfilter; + +import software.amazon.cloudformation.proxy.StdCallbackContext; + +@lombok.Getter +@lombok.Setter +@lombok.ToString +@lombok.EqualsAndHashCode(callSuper = true) +public class CallbackContext extends StdCallbackContext { +} diff --git a/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/ClientBuilder.java b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/ClientBuilder.java new file mode 100644 index 0000000..9d4107f --- /dev/null +++ b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/ClientBuilder.java @@ -0,0 +1,13 @@ +package software.amazon.logs.metricfilter; + +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; +import software.amazon.cloudformation.LambdaWrapper; + +public class ClientBuilder { + + public static CloudWatchLogsClient getClient() { + return CloudWatchLogsClient.builder() + .httpClient(LambdaWrapper.HTTP_CLIENT) + .build(); + } +} diff --git a/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/Configuration.java b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/Configuration.java new file mode 100644 index 0000000..4295eea --- /dev/null +++ b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/Configuration.java @@ -0,0 +1,8 @@ +package software.amazon.logs.metricfilter; + +class Configuration extends BaseConfiguration { + + public Configuration() { + super("aws-logs-metricfilter.json"); + } +} diff --git a/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/CreateHandler.java b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/CreateHandler.java new file mode 100644 index 0000000..2b08747 --- /dev/null +++ b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/CreateHandler.java @@ -0,0 +1,98 @@ +package software.amazon.logs.metricfilter; + +import com.amazonaws.util.StringUtils; +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; +import software.amazon.awssdk.services.cloudwatchlogs.model.InvalidParameterException; +import software.amazon.awssdk.services.cloudwatchlogs.model.LimitExceededException; +import software.amazon.awssdk.services.cloudwatchlogs.model.OperationAbortedException; +import software.amazon.awssdk.services.cloudwatchlogs.model.PutMetricFilterRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.PutMetricFilterResponse; +import software.amazon.awssdk.services.cloudwatchlogs.model.ServiceUnavailableException; +import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnResourceConflictException; +import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; +import software.amazon.cloudformation.exceptions.CfnServiceLimitExceededException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.cloudformation.resource.IdentifierUtils; + +public class CreateHandler extends BaseHandlerStd { + private Logger logger; + // if you change the value in the line below, please also update the resource schema + private static final int MAX_LENGTH_METRIC_FILTER_NAME = 512; + + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { + + this.logger = logger; + + final ResourceModel model = request.getDesiredResourceState(); + + // resource can auto-generate a name if not supplied by caller. + // this logic should move up into the CloudFormation engine, but + // currently exists here for backwards-compatibility with existing models + if (StringUtils.isNullOrEmpty(model.getFilterName())) { + model.setFilterName( + IdentifierUtils.generateResourceIdentifier( + request.getLogicalResourceIdentifier(), + request.getClientRequestToken(), + MAX_LENGTH_METRIC_FILTER_NAME + ) + ); + } + + return ProgressEvent.progress(model, callbackContext) + .then(progress -> + preCreateCheck(proxy, callbackContext, proxyClient, model) + .done((response) -> { + if (response.metricFilters().isEmpty()) { + return ProgressEvent.progress(model, callbackContext); + } + return ProgressEvent.defaultFailureHandler(new CfnAlreadyExistsException(null), HandlerErrorCode.AlreadyExists); + }) + ) + .then(progress -> + proxy.initiate("AWS-Logs-MetricFilter::Create", proxyClient, model, callbackContext) + .translateToServiceRequest(Translator::translateToCreateRequest) + .makeServiceCall(this::createResource) + .progress()) + .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); + } + + + /** + * Implement client invocation of the create request through the proxyClient, which is already initialised with + * caller credentials, correct region and retry settings + * @param awsRequest the aws service request to create a resource + * @param proxyClient the aws service client to make the call + * @return awsResponse create resource response + */ + private PutMetricFilterResponse createResource( + final PutMetricFilterRequest awsRequest, + final ProxyClient proxyClient) { + PutMetricFilterResponse awsResponse; + try { + awsResponse = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::putMetricFilter); + } catch (final InvalidParameterException e) { + throw new CfnInvalidRequestException(ResourceModel.TYPE_NAME, e); + } catch (final LimitExceededException e) { + throw new CfnServiceLimitExceededException(e); + } catch (final OperationAbortedException e) { + throw new CfnResourceConflictException(e); + } catch (final ServiceUnavailableException e) { + throw new CfnServiceInternalErrorException(e); + } + + logger.log(String.format("%s successfully created.", ResourceModel.TYPE_NAME)); + return awsResponse; + } +} diff --git a/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/DeleteHandler.java b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/DeleteHandler.java new file mode 100644 index 0000000..2a35e72 --- /dev/null +++ b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/DeleteHandler.java @@ -0,0 +1,66 @@ +package software.amazon.logs.metricfilter; + +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; +import software.amazon.awssdk.services.cloudwatchlogs.model.DeleteMetricFilterRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DeleteMetricFilterResponse; +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 software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +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.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +public class DeleteHandler extends BaseHandlerStd { + private Logger logger; + + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { + + this.logger = logger; + + final ResourceModel model = request.getDesiredResourceState(); + + this.logger.log(String.format("Trying to delete model %s", model.getPrimaryIdentifier())); + + return proxy.initiate("AWS-Logs-MetricFilter::Delete", proxyClient, model, callbackContext) + .translateToServiceRequest(Translator::translateToDeleteRequest) + .makeServiceCall(this::deleteResource) + .done(awsResponse -> ProgressEvent.builder() + .status(OperationStatus.SUCCESS) + .resourceModel(model) + .build()); + } + + private DeleteMetricFilterResponse deleteResource( + final DeleteMetricFilterRequest awsRequest, + final ProxyClient proxyClient) { + DeleteMetricFilterResponse awsResponse; + try { + awsResponse = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::deleteMetricFilter); + } catch (ResourceNotFoundException e) { + logger.log("Resource does not exist and could not be deleted."); + throw new CfnNotFoundException(e); + } catch (InvalidParameterException e) { + throw new CfnInvalidRequestException(e); + } catch (OperationAbortedException e) { + throw new CfnResourceConflictException(e); + } catch (ServiceUnavailableException e) { + throw new CfnServiceInternalErrorException(e); + } + + logger.log(String.format("%s successfully deleted.", ResourceModel.TYPE_NAME)); + return awsResponse; + } +} diff --git a/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/ListHandler.java b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/ListHandler.java new file mode 100644 index 0000000..c15c340 --- /dev/null +++ b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/ListHandler.java @@ -0,0 +1,49 @@ +package software.amazon.logs.metricfilter; + +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeMetricFiltersRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeMetricFiltersResponse; +import software.amazon.awssdk.services.cloudwatchlogs.model.InvalidParameterException; +import software.amazon.awssdk.services.cloudwatchlogs.model.ResourceNotFoundException; +import software.amazon.awssdk.services.cloudwatchlogs.model.ServiceUnavailableException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +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.OperationStatus; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +import java.util.List; + +public class ListHandler extends BaseHandler { + + @Override + public ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final Logger logger) { + + final DescribeMetricFiltersRequest awsRequest = Translator.translateToListRequest(request.getNextToken()); + DescribeMetricFiltersResponse awsResponse; + + try { + awsResponse = proxy.injectCredentialsAndInvokeV2(awsRequest, ClientBuilder.getClient()::describeMetricFilters); + } catch (InvalidParameterException e) { + throw new CfnInvalidRequestException(e); + } catch (ResourceNotFoundException e) { + throw new CfnNotFoundException(e); + } catch (ServiceUnavailableException e) { + throw new CfnServiceInternalErrorException(e); + } + + final List models = Translator.translateFromListResponse(awsResponse); + + return ProgressEvent.builder() + .resourceModels(models) + .nextToken(awsResponse.nextToken()) + .status(OperationStatus.SUCCESS) + .build(); + } +} diff --git a/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/ReadHandler.java b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/ReadHandler.java new file mode 100644 index 0000000..170b24e --- /dev/null +++ b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/ReadHandler.java @@ -0,0 +1,75 @@ +package software.amazon.logs.metricfilter; + +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeMetricFiltersRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeMetricFiltersResponse; +import software.amazon.awssdk.services.cloudwatchlogs.model.InvalidParameterException; +import software.amazon.awssdk.services.cloudwatchlogs.model.ResourceNotFoundException; +import software.amazon.awssdk.services.cloudwatchlogs.model.ServiceUnavailableException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +import java.util.Objects; + +public class ReadHandler extends BaseHandlerStd { + private Logger logger; + + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { + + this.logger = logger; + + final ResourceModel model = request.getDesiredResourceState(); + + logger.log("Trying to read resource..."); + + return proxy.initiate("AWS-Logs-MetricFilter::Read", proxyClient, model, callbackContext) + .translateToServiceRequest(Translator::translateToReadRequest) + .makeServiceCall((awsRequest, sdkProxyClient) -> readResource(awsRequest, sdkProxyClient , model)) + .done(awsResponse -> ProgressEvent.builder() + .status(OperationStatus.SUCCESS) + .resourceModel(Translator.translateFromReadResponse(awsResponse)) + .build()); + } + + private DescribeMetricFiltersResponse readResource( + final DescribeMetricFiltersRequest awsRequest, + final ProxyClient proxyClient, + final ResourceModel model) { + DescribeMetricFiltersResponse awsResponse; + try { + awsResponse = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::describeMetricFilters); + } catch (InvalidParameterException e) { + throw new CfnInvalidRequestException(e); + } catch (ResourceNotFoundException e) { + throw new CfnNotFoundException(e); + } catch (ServiceUnavailableException e) { + throw new CfnServiceInternalErrorException(e); + } + + if (awsResponse.metricFilters().isEmpty()) { + logger.log("Resource does not exist."); + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, + Objects.toString(model.getPrimaryIdentifier())); + } + + logger.log(String.format("%s has successfully been read." , ResourceModel.TYPE_NAME)); + return awsResponse; + } + + private ProgressEvent constructResourceModelFromResponse(final DescribeMetricFiltersResponse awsResponse) { + return ProgressEvent.defaultSuccessHandler(Translator.translateFromReadResponse(awsResponse)); + } + +} diff --git a/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/Translator.java b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/Translator.java new file mode 100644 index 0000000..fdf9fdc --- /dev/null +++ b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/Translator.java @@ -0,0 +1,151 @@ +package software.amazon.logs.metricfilter; + +import software.amazon.awssdk.services.cloudwatchlogs.model.DeleteMetricFilterRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeMetricFiltersRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeMetricFiltersResponse; +import software.amazon.awssdk.services.cloudwatchlogs.model.PutMetricFilterRequest; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class Translator { + + static software.amazon.awssdk.services.cloudwatchlogs.model.MetricTransformation translateMetricTransformationToSdk + (final software.amazon.logs.metricfilter.MetricTransformation metricTransformation) { + if (metricTransformation == null) { + return null; + } + return software.amazon.awssdk.services.cloudwatchlogs.model.MetricTransformation.builder() + .metricName(metricTransformation.getMetricName()) + .metricValue(metricTransformation.getMetricValue()) + .metricNamespace(metricTransformation.getMetricNamespace()) + .defaultValue(metricTransformation.getDefaultValue()) + .build(); + } + + static software.amazon.logs.metricfilter.MetricTransformation translateMetricTransformationFromSdk + (final software.amazon.awssdk.services.cloudwatchlogs.model.MetricTransformation metricTransformation) { + if (metricTransformation == null) { + return null; + } + return software.amazon.logs.metricfilter.MetricTransformation.builder() + .metricName(metricTransformation.metricName()) + .metricValue(metricTransformation.metricValue()) + .metricNamespace(metricTransformation.metricNamespace()) + .defaultValue(metricTransformation.defaultValue()) + .build(); + } + + static List translateMetricTransformationFromSdk + (final List metricTransformations) { + if (metricTransformations.isEmpty()) { + return null; + } + return metricTransformations.stream() + .map(Translator::translateMetricTransformationFromSdk) + .collect(Collectors.toList()); + } + + static ResourceModel translateMetricFilter + (final software.amazon.awssdk.services.cloudwatchlogs.model.MetricFilter metricFilter) { + List mts = metricFilter.metricTransformations() + .stream() + .map(Translator::translateMetricTransformationFromSdk) + .collect(Collectors.toList()); + return ResourceModel.builder() + .filterName(metricFilter.filterName()) + .logGroupName(metricFilter.logGroupName()) + // When a filter pattern is "" the API sets it to null, but this is a meaningful pattern and the + // contract should be identical to what our caller provided + .filterPattern(metricFilter.filterPattern() == null ? "" : metricFilter.filterPattern()) + .metricTransformations(mts) + .build(); + } + + static software.amazon.awssdk.services.cloudwatchlogs.model.MetricFilter translateToSDK + (final ResourceModel model) { + List mts = model.getMetricTransformations() + .stream() + .map(Translator::translateMetricTransformationToSdk) + .collect(Collectors.toList()); + return software.amazon.awssdk.services.cloudwatchlogs.model.MetricFilter.builder() + .filterName(model.getFilterName()) + .logGroupName(model.getLogGroupName()) + .filterPattern(model.getFilterPattern()) + .metricTransformations(mts) + .build(); + } + + static software.amazon.awssdk.services.cloudwatchlogs.model.MetricTransformation translateToSDK + (final MetricTransformation metricTransformation) { + return translateMetricTransformationToSdk(metricTransformation); + } + + static List translateMetricTransformationToSDK + (final List metricTransformations) { + return metricTransformations.stream() + .map(Translator::translateToSDK) + .collect(Collectors.toList()); + } + + static PutMetricFilterRequest translateToCreateRequest(final ResourceModel model) { + return PutMetricFilterRequest.builder() + .logGroupName(model.getLogGroupName()) + .filterName(model.getFilterName()) + .filterPattern(model.getFilterPattern()) + .metricTransformations(model.getMetricTransformations() + .stream() + .map(Translator::translateMetricTransformationToSdk) + .collect(Collectors.toSet())) + .build(); + } + + static DescribeMetricFiltersRequest translateToReadRequest(final ResourceModel model) { + return DescribeMetricFiltersRequest.builder() + .filterNamePrefix(model.getFilterName()) + .logGroupName(model.getLogGroupName()) + .limit(1) + .build(); + } + + static ResourceModel translateFromReadResponse(final DescribeMetricFiltersResponse awsResponse) { + return awsResponse.metricFilters() + .stream() + .map(Translator::translateMetricFilter) + .findFirst() + .get(); + } + + static DeleteMetricFilterRequest translateToDeleteRequest(final ResourceModel model) { + return DeleteMetricFilterRequest.builder() + .filterName(model.getFilterName()) + .logGroupName(model.getLogGroupName()) + .build(); + } + + static PutMetricFilterRequest translateToUpdateRequest(final ResourceModel model) { + return translateToCreateRequest(model); + } + + static DescribeMetricFiltersRequest translateToListRequest(final String nextToken) { + return DescribeMetricFiltersRequest.builder() + .nextToken(nextToken) + .limit(50) + .build(); + } + + static List translateFromListResponse(final DescribeMetricFiltersResponse awsResponse) { + return streamOfOrEmpty(awsResponse.metricFilters()) + .map(Translator::translateMetricFilter) + .collect(Collectors.toList()); + } + + private static Stream streamOfOrEmpty(final Collection collection) { + return Optional.ofNullable(collection) + .map(Collection::stream) + .orElseGet(Stream::empty); + } +} diff --git a/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/UpdateHandler.java b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/UpdateHandler.java new file mode 100644 index 0000000..9233873 --- /dev/null +++ b/aws-logs-metricfilter/src/main/java/software/amazon/logs/metricfilter/UpdateHandler.java @@ -0,0 +1,101 @@ +package software.amazon.logs.metricfilter; + +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; +import software.amazon.awssdk.services.cloudwatchlogs.model.InvalidParameterException; +import software.amazon.awssdk.services.cloudwatchlogs.model.LimitExceededException; +import software.amazon.awssdk.services.cloudwatchlogs.model.OperationAbortedException; +import software.amazon.awssdk.services.cloudwatchlogs.model.PutMetricFilterRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.PutMetricFilterResponse; +import software.amazon.awssdk.services.cloudwatchlogs.model.ResourceNotFoundException; +import software.amazon.awssdk.services.cloudwatchlogs.model.ServiceUnavailableException; +import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.exceptions.CfnResourceConflictException; +import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; +import software.amazon.cloudformation.exceptions.CfnServiceLimitExceededException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +public class UpdateHandler extends BaseHandlerStd { + private Logger logger; + + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { + + this.logger = logger; + + final ResourceModel model = request.getDesiredResourceState(); + final ResourceModel previousModel = request.getPreviousResourceState(); + + this.logger.log(String.format("Trying to update model %s", model.getPrimaryIdentifier())); + + return ProgressEvent.progress(model, callbackContext) + .then(progress -> { + if (!isUpdatable(model, previousModel)) { + return ProgressEvent.builder() + .errorCode(HandlerErrorCode.NotUpdatable) + .status(OperationStatus.FAILED) + .build(); + } + return progress; + }) + .then(progress -> + preCreateCheck(proxy, callbackContext, proxyClient, model) + .done((response) -> { + if (response.metricFilters().isEmpty()) { + return ProgressEvent.defaultFailureHandler(new CfnNotFoundException(null), HandlerErrorCode.NotFound); + } + return ProgressEvent.progress(model, callbackContext); + }) + ) + .then(progress -> proxy.initiate("AWS-Logs-MetricFilter::Update", proxyClient, model, callbackContext) + .translateToServiceRequest(Translator::translateToUpdateRequest) + .makeServiceCall(this::updateResource) + .progress()) + .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); + } + + private boolean isUpdatable(final ResourceModel model, final ResourceModel previousModel) { + // An update request MUST return a NotUpdatable error if the user attempts to change a property + // that is defined as create-only in the resource provider schema. + if (previousModel != null) { + return previousModel.getFilterName().equals(model.getFilterName()) + && previousModel.getLogGroupName().equals(model.getLogGroupName()); + + } + return true; + } + + private PutMetricFilterResponse updateResource( + final PutMetricFilterRequest awsRequest, + final ProxyClient proxyClient) { + PutMetricFilterResponse awsResponse; + try { + awsResponse = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::putMetricFilter); + } catch (final ResourceNotFoundException e) { + logger.log("Resource not found. " + e.getMessage()); + throw new CfnNotFoundException(e); + } catch (final InvalidParameterException e) { + throw new CfnInvalidRequestException(ResourceModel.TYPE_NAME, e); + } catch (final LimitExceededException e) { + throw new CfnServiceLimitExceededException(e); + } catch (final ServiceUnavailableException e) { + throw new CfnServiceInternalErrorException(e); + } catch (final OperationAbortedException e) { + throw new CfnResourceConflictException(e); + } + + logger.log(String.format("%s has successfully been updated.", ResourceModel.TYPE_NAME)); + return awsResponse; + } +} diff --git a/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/AbstractTestBase.java b/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/AbstractTestBase.java new file mode 100644 index 0000000..4685f00 --- /dev/null +++ b/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/AbstractTestBase.java @@ -0,0 +1,66 @@ +package software.amazon.logs.metricfilter; + +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import software.amazon.awssdk.awscore.AwsRequest; +import software.amazon.awssdk.awscore.AwsResponse; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Credentials; +import software.amazon.cloudformation.proxy.LoggerProxy; +import software.amazon.cloudformation.proxy.ProxyClient; + +public class AbstractTestBase { + protected static final Credentials MOCK_CREDENTIALS; + protected static final LoggerProxy logger; + + static { + MOCK_CREDENTIALS = new Credentials("accessKey", "secretKey", "token"); + logger = new LoggerProxy(); + } + static ProxyClient MOCK_PROXY( + final AmazonWebServicesClientProxy proxy, + final CloudWatchLogsClient sdkClient) { + return new ProxyClient() { + @Override + public ResponseT + injectCredentialsAndInvokeV2(RequestT request, Function requestFunction) { + return proxy.injectCredentialsAndInvokeV2(request, requestFunction); + } + + @Override + public + CompletableFuture + injectCredentialsAndInvokeV2Async(RequestT request, Function> requestFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public > + IterableT + injectCredentialsAndInvokeIterableV2(RequestT request, Function requestFunction) { + return proxy.injectCredentialsAndInvokeIterableV2(request, requestFunction); + } + + @Override + public CloudWatchLogsClient client() { + return sdkClient; + } + }; + } + + static ResourceModel buildDefaultModel() { + return ResourceModel.builder() + .filterName("filter-name") + .logGroupName("log-group-name") + .filterPattern("[pattern]") + .metricTransformations(Arrays.asList(MetricTransformation.builder() + .metricName("metric-name") + .metricValue("0") + .metricNamespace("namespace") + .build())) + .build(); + } +} diff --git a/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/CreateHandlerTest.java b/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/CreateHandlerTest.java new file mode 100644 index 0000000..4b7e836 --- /dev/null +++ b/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/CreateHandlerTest.java @@ -0,0 +1,247 @@ +package software.amazon.logs.metricfilter; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; + +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeMetricFiltersRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeMetricFiltersResponse; +import software.amazon.awssdk.services.cloudwatchlogs.model.OperationAbortedException; +import software.amazon.awssdk.services.cloudwatchlogs.model.PutMetricFilterRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.PutMetricFilterResponse; +import software.amazon.awssdk.services.cloudwatchlogs.model.ResourceNotFoundException; +import software.amazon.awssdk.services.cloudwatchlogs.model.ServiceUnavailableException; +import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.exceptions.CfnResourceConflictException; +import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class CreateHandlerTest extends AbstractTestBase { + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private ProxyClient proxyClient; + + @Mock + CloudWatchLogsClient sdkClient; + + final CreateHandler handler = new CreateHandler(); + + @BeforeEach + public void setup() { + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + sdkClient = mock(CloudWatchLogsClient.class); + proxyClient = MOCK_PROXY(proxy, sdkClient); + } + + @AfterEach + public void tear_down() { + verify(sdkClient, atLeastOnce()).serviceName(); + verifyNoMoreInteractions(sdkClient); + } + + @Test + public void handleRequest_Success() { + final ResourceModel model = buildDefaultModel(); + + final DescribeMetricFiltersResponse describeResponse = DescribeMetricFiltersResponse.builder() + .metricFilters(Translator.translateToSDK(model)) + .build(); + + final PutMetricFilterResponse createResponse = PutMetricFilterResponse.builder() + .build(); + + // return no existing metrics for pre-create and then success response for create + when(proxyClient.client().describeMetricFilters(any(DescribeMetricFiltersRequest.class))) + .thenThrow(ResourceNotFoundException.class) + .thenReturn(describeResponse); + + when(proxyClient.client().putMetricFilter(any(PutMetricFilterRequest.class))) + .thenReturn(createResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + verify(proxyClient.client(), times(2)).describeMetricFilters(any(DescribeMetricFiltersRequest.class)); + verify(proxyClient.client(), times(1)).putMetricFilter(any(PutMetricFilterRequest.class)); + } + + @Test + public void handleRequest_Success2() { + final ResourceModel model = buildDefaultModel(); + + final DescribeMetricFiltersResponse preCreateResponse = DescribeMetricFiltersResponse.builder() + .metricFilters(Collections.emptyList()) + .build(); + + final DescribeMetricFiltersResponse postCreateResponse = DescribeMetricFiltersResponse.builder() + .metricFilters(Translator.translateToSDK(model)) + .build(); + + final PutMetricFilterResponse createResponse = PutMetricFilterResponse.builder() + .build(); + + // return no existing metrics for pre-create and then success response for create + when(proxyClient.client().describeMetricFilters(any(DescribeMetricFiltersRequest.class))) + .thenReturn(preCreateResponse) + .thenReturn(postCreateResponse); + + when(proxyClient.client().putMetricFilter(any(PutMetricFilterRequest.class))) + .thenReturn(createResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + verify(proxyClient.client(), times(2)).describeMetricFilters(any(DescribeMetricFiltersRequest.class)); + verify(proxyClient.client(), times(1)).putMetricFilter(any(PutMetricFilterRequest.class)); + } + + @Test + public void handleRequest_FailedCreate_InternalReadThrowsException() { + final ResourceModel model = buildDefaultModel(); + + // throw arbitrary error which should propagate to be handled by wrapper + when(proxyClient.client().describeMetricFilters(any(DescribeMetricFiltersRequest.class))) + .thenThrow(ServiceUnavailableException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.ServiceInternalError); + } + + @Test + public void handleRequest_FailedCreate_AlreadyExists() { + final ResourceModel model = buildDefaultModel(); + + final DescribeMetricFiltersResponse describeResponse = DescribeMetricFiltersResponse.builder() + .metricFilters(Translator.translateToSDK(model)) + .build(); + + when(proxyClient.client().describeMetricFilters(any(DescribeMetricFiltersRequest.class))) + .thenReturn(describeResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.AlreadyExists); + } + + @Test + public void handleRequest_FailedCreate_PutFailed() { + final ResourceModel model = buildDefaultModel(); + + final DescribeMetricFiltersResponse describeResponse = DescribeMetricFiltersResponse.builder() + .metricFilters(Translator.translateToSDK(model)) + .build(); + + // return no existing metrics for pre-create and then success response for create + when(proxyClient.client().describeMetricFilters(any(DescribeMetricFiltersRequest.class))) + .thenThrow(ResourceNotFoundException.class) + .thenReturn(describeResponse); + + when(proxyClient.client().putMetricFilter(any(PutMetricFilterRequest.class))) + .thenThrow(OperationAbortedException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + assertThatThrownBy(() -> handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)) + .isInstanceOf(CfnResourceConflictException.class); + } + + @Test + public void handleRequest_Success_WithGeneratedName() { + // no filter name supplied; should be generated + final ResourceModel model = ResourceModel.builder() + .logGroupName("test-log-group") + .filterPattern("some pattern") + .metricTransformations(Arrays.asList(MetricTransformation.builder() + .metricName("metric-name") + .metricValue("0") + .metricNamespace("namespace") + .build())) + .build(); + + // return no existing metrics for pre-create and then success response for create + when(proxyClient.client().describeMetricFilters(any(DescribeMetricFiltersRequest.class))) + .thenThrow(ResourceNotFoundException.class) + .thenReturn(DescribeMetricFiltersResponse.builder() + .metricFilters(Translator.translateToSDK(model)) + .build()); + + when(proxyClient.client().putMetricFilter(any(PutMetricFilterRequest.class))) + .thenReturn(PutMetricFilterResponse.builder().build()); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .logicalResourceIdentifier("logicalResourceIdentifier") + .clientRequestToken("requestToken") + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isNotNull(); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } +} diff --git a/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/DeleteHandlerTest.java b/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/DeleteHandlerTest.java new file mode 100644 index 0000000..9cefa01 --- /dev/null +++ b/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/DeleteHandlerTest.java @@ -0,0 +1,114 @@ +package software.amazon.logs.metricfilter; + +import java.time.Duration; + +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; +import software.amazon.awssdk.services.cloudwatchlogs.model.DeleteMetricFilterRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DeleteMetricFilterResponse; +import software.amazon.awssdk.services.cloudwatchlogs.model.InvalidParameterException; +import software.amazon.awssdk.services.cloudwatchlogs.model.ResourceNotFoundException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class DeleteHandlerTest extends AbstractTestBase { + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private ProxyClient proxyClient; + + @Mock + CloudWatchLogsClient sdkClient; + + final DeleteHandler handler = new DeleteHandler(); + + @BeforeEach + public void setup() { + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + sdkClient = mock(CloudWatchLogsClient.class); + proxyClient = MOCK_PROXY(proxy, sdkClient); + } + + @AfterEach + public void tear_down() { + verify(sdkClient, atLeastOnce()).serviceName(); + verifyNoMoreInteractions(sdkClient); + } + + @Test + public void handleRequest_Success() { + final ResourceModel model = buildDefaultModel(); + + when(proxyClient.client().deleteMetricFilter(ArgumentMatchers.any(DeleteMetricFilterRequest.class))) + .thenReturn(DeleteMetricFilterResponse.builder().build()); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + verify(proxyClient.client(), times(1)).deleteMetricFilter(any(DeleteMetricFilterRequest.class)); + } + + @Test + public void handleRequest_ResourceNotFound() { + final ResourceModel model = buildDefaultModel(); + + when(proxyClient.client().deleteMetricFilter(ArgumentMatchers.any(DeleteMetricFilterRequest.class))) + .thenThrow(ResourceNotFoundException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + assertThatThrownBy(() -> handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)) + .isInstanceOf(CfnNotFoundException.class); + } + + @Test + public void handleRequest_DeleteFailed() { + final ResourceModel model = buildDefaultModel(); + + when(proxyClient.client().deleteMetricFilter(ArgumentMatchers.any(DeleteMetricFilterRequest.class))) + .thenThrow(InvalidParameterException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + assertThatThrownBy(() -> handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)) + .isInstanceOf(CfnInvalidRequestException.class); + } +} diff --git a/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/ListHandlerTest.java b/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/ListHandlerTest.java new file mode 100644 index 0000000..34d79cf --- /dev/null +++ b/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/ListHandlerTest.java @@ -0,0 +1,77 @@ +package software.amazon.logs.metricfilter; + +import org.mockito.ArgumentMatchers; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeMetricFiltersRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeMetricFiltersResponse; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ListHandlerTest { + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + final ListHandler handler = new ListHandler(); + + @BeforeEach + public void setup() { + proxy = mock(AmazonWebServicesClientProxy.class); + logger = mock(Logger.class); + } + + @Test + public void handleRequest_SimpleSuccess() { + final ResourceModel model = ResourceModel.builder() + .filterName("filter-name") + .logGroupName("log-group-name") + .filterPattern("[pattern]") + .metricTransformations(Arrays.asList(MetricTransformation.builder() + .metricName("metric-name") + .metricValue("0") + .metricNamespace("namespace") + .build())) + .build(); + + final DescribeMetricFiltersResponse describeResponse = DescribeMetricFiltersResponse.builder() + .metricFilters(Translator.translateToSDK(model)) + .build(); + + when(proxy.injectCredentialsAndInvokeV2(ArgumentMatchers.any(DescribeMetricFiltersRequest.class), any())) + .thenReturn(describeResponse); + + 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.getResourceModel()).isNull(); + assertThat(response.getResourceModels()).isNotNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } +} diff --git a/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/Matchers.java b/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/Matchers.java new file mode 100644 index 0000000..b52c961 --- /dev/null +++ b/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/Matchers.java @@ -0,0 +1,26 @@ +package software.amazon.logs.metricfilter; + +import software.amazon.awssdk.services.cloudwatchlogs.model.MetricFilter; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +public class Matchers { + + public static void assertThatModelsAreEqual(final Object rawModel, + final MetricFilter sdkModel) { + assertThat(rawModel).isInstanceOf(ResourceModel.class); + ResourceModel model = (ResourceModel)rawModel; + assertThat(model.getFilterName()).isEqualTo(sdkModel.filterName()); + assertThat(model.getFilterPattern()).isEqualTo(sdkModel.filterPattern()); + assertThat(model.getLogGroupName()).isEqualTo(sdkModel.logGroupName()); + + List mts = sdkModel.metricTransformations() + .stream() + .map(Translator::translateMetricTransformationFromSdk) + .collect(Collectors.toList()); + assertThat(model.getMetricTransformations()).isEqualTo(mts); + } +} diff --git a/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/ReadHandlerTest.java b/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/ReadHandlerTest.java new file mode 100644 index 0000000..b134e7d --- /dev/null +++ b/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/ReadHandlerTest.java @@ -0,0 +1,138 @@ +package software.amazon.logs.metricfilter; + +import java.time.Duration; +import java.util.Collections; + +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeMetricFiltersRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeMetricFiltersResponse; +import software.amazon.awssdk.services.cloudwatchlogs.model.InvalidParameterException; +import software.amazon.awssdk.services.cloudwatchlogs.model.ResourceNotFoundException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ReadHandlerTest extends AbstractTestBase { + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private ProxyClient proxyClient; + + @Mock + CloudWatchLogsClient sdkClient; + + final ReadHandler handler = new ReadHandler(); + + @BeforeEach + public void setup() { + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + sdkClient = mock(CloudWatchLogsClient.class); + proxyClient = MOCK_PROXY(proxy, sdkClient); + } + + @AfterEach + public void tear_down() { + verify(sdkClient, atLeastOnce()).serviceName(); + verifyNoMoreInteractions(sdkClient); + } + + @Test + public void handleRequest_Success() { + final ResourceModel model = buildDefaultModel(); + + final DescribeMetricFiltersResponse describeResponse = DescribeMetricFiltersResponse.builder() + .metricFilters(Translator.translateToSDK(model)) + .build(); + + when(proxyClient.client().describeMetricFilters(ArgumentMatchers.any(DescribeMetricFiltersRequest.class))) + .thenReturn(describeResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + verify(proxyClient.client(), times(1)).describeMetricFilters(any(DescribeMetricFiltersRequest.class)); + } + + @Test + public void handleRequest_ResponseIsEmpty() { + final ResourceModel model = buildDefaultModel(); + + final DescribeMetricFiltersResponse describeResponse = DescribeMetricFiltersResponse.builder() + .metricFilters(Collections.emptyList()) + .build(); + + when(proxyClient.client().describeMetricFilters(ArgumentMatchers.any(DescribeMetricFiltersRequest.class))) + .thenReturn(describeResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + assertThatThrownBy(() -> handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)) + .isInstanceOf(CfnNotFoundException.class); + } + + @Test + public void handleRequest_ResourceNotFound() { + final ResourceModel model = buildDefaultModel(); + + when(proxyClient.client().describeMetricFilters(ArgumentMatchers.any(DescribeMetricFiltersRequest.class))) + .thenThrow(ResourceNotFoundException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + assertThatThrownBy(() -> handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)) + .isInstanceOf(CfnNotFoundException.class); + } + + @Test + public void handleRequest_ExceptionThrown() { + final ResourceModel model = buildDefaultModel(); + + when(proxyClient.client().describeMetricFilters(ArgumentMatchers.any(DescribeMetricFiltersRequest.class))) + .thenThrow(InvalidParameterException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + assertThatThrownBy(() -> handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)) + .isInstanceOf(CfnInvalidRequestException.class); + } +} diff --git a/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/TranslatorTest.java b/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/TranslatorTest.java new file mode 100644 index 0000000..3032e0a --- /dev/null +++ b/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/TranslatorTest.java @@ -0,0 +1,183 @@ +package software.amazon.logs.metricfilter; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.awssdk.services.cloudwatchlogs.model.DeleteMetricFilterRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeMetricFiltersRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeMetricFiltersResponse; +import software.amazon.awssdk.services.cloudwatchlogs.model.MetricFilter; +import software.amazon.awssdk.services.cloudwatchlogs.model.PutMetricFilterRequest; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +public class TranslatorTest { + private static final software.amazon.awssdk.services.cloudwatchlogs.model.MetricTransformation METRIC_TRANSFORMATION = + software.amazon.awssdk.services.cloudwatchlogs.model.MetricTransformation.builder() + .defaultValue(1.0) + .metricName("MetricName") + .metricNamespace("MyNamespace") + .metricValue("Value") + .build(); + + private static final MetricTransformation RPDK_METRIC_TRANSFORMATION = + MetricTransformation.builder() + .defaultValue(1.0) + .metricName("MetricName") + .metricNamespace("MyNamespace") + .metricValue("Value") + .build(); + + private static final MetricFilter METRIC_FILTER = MetricFilter.builder() + .filterName("Filter") + .logGroupName("LogGroup") + .filterPattern("Pattern") + .metricTransformations(Collections.singletonList(METRIC_TRANSFORMATION)) + .build(); + + private static final ResourceModel RESOURCE_MODEL = ResourceModel.builder() + .logGroupName("LogGroup") + .metricTransformations(Collections.singletonList(RPDK_METRIC_TRANSFORMATION)) + .filterPattern("Pattern") + .filterName("FilterName") + .build(); + + @Test + public void translate_packageModel() { + assertThat(Translator.translateMetricTransformationToSdk(RPDK_METRIC_TRANSFORMATION)) + .isEqualToComparingFieldByField(METRIC_TRANSFORMATION); + } + + @Test + public void translate_nullPackageModel_returnsNull() { + assertThat(Translator.translateMetricTransformationToSdk((MetricTransformation)null)).isNull(); + } + + @Test + public void translate_nullSDKModel_returnsNull() { + assertThat(Translator.translateMetricTransformationFromSdk((software.amazon.awssdk.services.cloudwatchlogs.model.MetricTransformation)null)).isNull(); + } + + @Test + public void translate_SDKModel() { + assertThat(Translator.translateMetricTransformationFromSdk(METRIC_TRANSFORMATION)) + .isEqualToComparingFieldByField(RPDK_METRIC_TRANSFORMATION); + } + + @Test + public void translateToSDK() { + assertThat(Translator.translateMetricTransformationToSDK(Collections.singletonList(RPDK_METRIC_TRANSFORMATION))) + .containsExactly(METRIC_TRANSFORMATION); + } + + @Test + public void translateFromSDK() { + assertThat(Translator.translateMetricTransformationFromSdk(Collections.singletonList(METRIC_TRANSFORMATION))) + .containsExactly(RPDK_METRIC_TRANSFORMATION); + } + + @Test + public void translateFromSDK_emptyList_returnsNull() { + assertThat(Translator.translateMetricTransformationFromSdk(Collections.emptyList())).isNull(); + } + + @Test + public void extractMetricFilters_success() { + final DescribeMetricFiltersResponse response = DescribeMetricFiltersResponse.builder() + .metricFilters(Collections.singletonList(METRIC_FILTER)) + .build(); + + final List expectedModels = Arrays.asList(ResourceModel.builder() + .filterName("Filter") + .logGroupName("LogGroup") + .filterPattern("Pattern") + .metricTransformations(Collections.singletonList(RPDK_METRIC_TRANSFORMATION)) + .build()); + + assertThat(Translator.translateFromListResponse(response)).isEqualTo(expectedModels); + } + + @Test + public void extractMetricFilters_API_removesEmptyFilterPattern() { + final DescribeMetricFiltersResponse response = DescribeMetricFiltersResponse.builder() + .metricFilters(Collections.singletonList(METRIC_FILTER.toBuilder() + .filterPattern(null) + .build())) + .build(); + final List expectedModels = Arrays.asList(ResourceModel.builder() + .filterName("Filter") + .logGroupName("LogGroup") + .filterPattern("") + .metricTransformations(Collections.singletonList(RPDK_METRIC_TRANSFORMATION)) + .build()); + + assertThat(Translator.translateFromListResponse(response)).isEqualTo(expectedModels); + } + + @Test + public void extractMetricFilters_noFilters() { + final DescribeMetricFiltersResponse response = DescribeMetricFiltersResponse.builder() + .metricFilters(Collections.emptyList()) + .build(); + final List expectedModels = Collections.emptyList(); + + assertThat(Translator.translateFromListResponse(response)).isEqualTo(expectedModels); + } + + @Test + public void translateToDeleteRequest() { + final DeleteMetricFilterRequest expectedRequest = DeleteMetricFilterRequest.builder() + .filterName("FilterName") + .logGroupName("LogGroup") + .build(); + + final DeleteMetricFilterRequest actualRequest = Translator.translateToDeleteRequest(RESOURCE_MODEL); + + assertThat(actualRequest).isEqualToComparingFieldByField(expectedRequest); + } + + @Test + public void translateToPutRequest() { + final PutMetricFilterRequest expectedRequest = PutMetricFilterRequest.builder() + .logGroupName("LogGroup") + .metricTransformations(Collections.singletonList(METRIC_TRANSFORMATION)) + .filterPattern("Pattern") + .filterName("FilterName") + .build(); + + final DeleteMetricFilterRequest actualRequest = Translator.translateToDeleteRequest(RESOURCE_MODEL); + + assertThat(actualRequest).isEqualToComparingFieldByField(expectedRequest); + } + + @Test + public void translateToReadRequest() { + final DescribeMetricFiltersRequest expectedRequest = DescribeMetricFiltersRequest.builder() + .logGroupName("LogGroup") + .filterNamePrefix("FilterName") + .limit(1) + .build(); + + final DescribeMetricFiltersRequest actualRequest = Translator.translateToReadRequest(RESOURCE_MODEL); + + assertThat(actualRequest).isEqualToComparingFieldByField(expectedRequest); + } + + @Test + public void translateToListRequest() { + final DescribeMetricFiltersRequest expectedRequest = DescribeMetricFiltersRequest.builder() + .limit(50) + .nextToken("token") + .build(); + + final DescribeMetricFiltersRequest actualRequest = Translator.translateToListRequest( "token"); + + assertThat(actualRequest).isEqualToComparingFieldByField(expectedRequest); + } + +} diff --git a/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/UpdateHandlerTest.java b/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/UpdateHandlerTest.java new file mode 100644 index 0000000..1143fce --- /dev/null +++ b/aws-logs-metricfilter/src/test/java/software/amazon/logs/metricfilter/UpdateHandlerTest.java @@ -0,0 +1,177 @@ +package software.amazon.logs.metricfilter; + +import java.time.Duration; +import java.util.Collections; + +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeMetricFiltersRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeMetricFiltersResponse; +import software.amazon.awssdk.services.cloudwatchlogs.model.InvalidParameterException; +import software.amazon.awssdk.services.cloudwatchlogs.model.PutMetricFilterRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.PutMetricFilterResponse; +import software.amazon.awssdk.services.cloudwatchlogs.model.ResourceNotFoundException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class UpdateHandlerTest extends AbstractTestBase { + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private ProxyClient proxyClient; + + @Mock + CloudWatchLogsClient sdkClient; + + final UpdateHandler handler = new UpdateHandler(); + + @BeforeEach + public void setup() { + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + sdkClient = mock(CloudWatchLogsClient.class); + proxyClient = MOCK_PROXY(proxy, sdkClient); + } + + @Test + public void handleRequest_Success() { + final PutMetricFilterResponse updateResponse = PutMetricFilterResponse.builder() + .build(); + + final ResourceModel model = buildDefaultModel(); + + final DescribeMetricFiltersResponse describeResponse = DescribeMetricFiltersResponse.builder() + .metricFilters(Translator.translateToSDK(model)) + .build(); + + when(proxyClient.client().putMetricFilter(ArgumentMatchers.any(PutMetricFilterRequest.class))) + .thenReturn(updateResponse); + when(proxyClient.client().describeMetricFilters(ArgumentMatchers.any(DescribeMetricFiltersRequest.class))) + .thenReturn(describeResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + verify(proxyClient.client(), times(2)).describeMetricFilters(any(DescribeMetricFiltersRequest.class)); + verify(proxyClient.client(), times(1)).putMetricFilter(any(PutMetricFilterRequest.class)); + verify(sdkClient, atLeastOnce()).serviceName(); + verifyNoMoreInteractions(sdkClient); + } + + @Test + public void handleRequest_FilterNameDoesNotMatch_NotUpdatable() { + final ResourceModel model = buildDefaultModel(); + final ResourceModel previousModel = buildDefaultModel(); + previousModel.setFilterName(previousModel.getFilterName() + "a"); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .previousResourceState(previousModel) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.NotUpdatable); + } + + @Test + public void handleRequest_LogGroupNameDoesNotMatch_NotUpdatable() { + final ResourceModel model = buildDefaultModel(); + final ResourceModel previousModel = buildDefaultModel(); + previousModel.setLogGroupName(previousModel.getLogGroupName() + "a"); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .previousResourceState(previousModel) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.NotUpdatable); + } + + @Test + public void handleRequest_ResourceNotFound() { + final ResourceModel model = buildDefaultModel(); + + final DescribeMetricFiltersResponse describeResponse = DescribeMetricFiltersResponse.builder() + .metricFilters(Collections.emptyList()) + .build(); + + when(proxyClient.client().describeMetricFilters(ArgumentMatchers.any(DescribeMetricFiltersRequest.class))) + .thenReturn(describeResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.NotFound); + } + + @Test + public void handleRequest_InternalException() { + final ResourceModel model = buildDefaultModel(); + + final DescribeMetricFiltersResponse describeResponse = DescribeMetricFiltersResponse.builder() + .metricFilters(Translator.translateToSDK(model)) + .build(); + + when(proxyClient.client().describeMetricFilters(ArgumentMatchers.any(DescribeMetricFiltersRequest.class))) + .thenReturn(describeResponse); + + when(proxyClient.client().putMetricFilter(ArgumentMatchers.any(PutMetricFilterRequest.class))) + .thenThrow(InvalidParameterException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + assertThatThrownBy(() -> handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)) + .isInstanceOf(CfnInvalidRequestException.class); + verify(proxyClient.client(), times(1)).describeMetricFilters(any(DescribeMetricFiltersRequest.class)); + verify(proxyClient.client(), times(1)).putMetricFilter(any(PutMetricFilterRequest.class)); + verify(sdkClient, atLeastOnce()).serviceName(); + verifyNoMoreInteractions(sdkClient); + } +} diff --git a/aws-logs-metricfilter/template.yml b/aws-logs-metricfilter/template.yml new file mode 100644 index 0000000..1363636 --- /dev/null +++ b/aws-logs-metricfilter/template.yml @@ -0,0 +1,22 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: AWS SAM template for the AWS::Logs::MetricFilter resource type + +Globals: + Function: + Timeout: 60 # docker start-up times can be long for SAM CLI + +Resources: + TypeFunction: + Type: AWS::Serverless::Function + Properties: + Handler: software.amazon.logs.metricfilter.HandlerWrapper::handleRequest + Runtime: java8 + CodeUri: ./target/aws-logs-metricfilter-handler-1.0-SNAPSHOT.jar + + TestEntrypoint: + Type: AWS::Serverless::Function + Properties: + Handler: software.amazon.logs.metricfilter.HandlerWrapper::testEntrypoint + Runtime: java8 + CodeUri: ./target/aws-logs-metricfilter-handler-1.0-SNAPSHOT.jar