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
+
+
+
+
+
+
+
+
+
+
+ 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