Repository: localstack/localstack

Issue:
Elasticsearch domain managed by Terraform cannot be updated; request for /tags/? returns 404
<!-- Love localstack? Please consider supporting our collective:
👉  https://opencollective.com/localstack/donate -->

We use Terraform to create and update resources in Localstack, which has worked for services like S3 and Dynamo so far.

We hit an issue with Elasticsearch domains, where the domain is created successfully but Terraform fails to apply in subsequent runs, when it makes a request to:

```
logs: ---[ REQUEST POST-SIGN ]-----------------------------
logs: GET /2015-01-01/tags/?arn=arn%3Aaws%3Aes%3Aus-east-1%3A000000000000%3Adomain%2Fepdam-local-amd HTTP/1.1
logs: Host: localhost:4578
logs: User-Agent: aws-sdk-go/1.14.31 (go1.9.2; darwin; amd64) APN/1.0 HashiCorp/1.0 Terraform/0.11.8-dev
logs: Authorization: AWS4-HMAC-SHA256 Credential=mock_access_key/20190221/us-west-2/es/aws4_request, SignedHeaders=host;x-amz-date, Signature=26f42429e2af2240466635ab9202c8888617afe9be7b8ef91a8831d6b4160bd1
logs: X-Amz-Date: 20190221T191447Z
logs: Accept-Encoding: gzip
```

and the response is:

```
logs: ---[ RESPONSE ]--------------------------------------
logs: HTTP/1.0 404 NOT FOUND
logs: Connection: close
logs: Content-Length: 233
logs: Access-Control-Allow-Origin: *
logs: Content-Type: text/html
logs: Date: Thu, 21 Feb 2019 19:14:47 GMT
logs: Server: Werkzeug/0.14.1 Python/2.7.15
```

While a request to `localhost:4578/2015-01-01/tags/?arn=...` gets 404, a request to `localhost:4578/2015-01-01/tags?arn=...`, (without the `/` before the query params), is successful.

The reason we are reporting this against Localstack and not [terraform](https://github.com/hashicorp/terraform) or [terraform-provider-aws](https://github.com/terraform-providers/terraform-provider-aws) is that the AWS REST API apparently supports requests with slashes before query parameters, or else Terraform could not be used to manage Elasticsearch domains in AWS.


Discussion:
Thanks for reporting @davidemurray-nike . Looks like we need to add a second route definition to the endpoint here: https://github.com/localstack/localstack/blob/master/localstack/services/es/es_api.py#L194 (Also, note that we don't actually store the tags currently, we're just returning hardcoded values.)

Could you give that a try and see if it works with Terraform, and then raise a pull request? Much appreciated.

Resolving patch:
diff --git a/localstack/services/es/es_api.py b/localstack/services/es/es_api.py
--- a/localstack/services/es/es_api.py
+++ b/localstack/services/es/es_api.py
@@ -14,6 +14,7 @@
 ES_DOMAINS = {}
 
 app = Flask(APP_NAME)
+app.url_map.strict_slashes = False
 
 
 def error_response(error_type, code=400, message='Unknown error.'):


Test patch:
diff --git a/localstack/ext/java/src/test/java/cloud/localstack/docker/BasicDockerFunctionalityTest.java b/localstack/ext/java/src/test/java/cloud/localstack/docker/BasicDockerFunctionalityTest.java
--- a/localstack/ext/java/src/test/java/cloud/localstack/docker/BasicDockerFunctionalityTest.java
+++ b/localstack/ext/java/src/test/java/cloud/localstack/docker/BasicDockerFunctionalityTest.java
@@ -7,6 +7,8 @@
 import com.amazon.sqs.javamessaging.SQSConnection;
 import com.amazon.sqs.javamessaging.SQSConnectionFactory;
 import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.services.cloudwatch.*;
+import com.amazonaws.services.cloudwatch.model.*;
 import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
 import com.amazonaws.services.dynamodbv2.model.AttributeDefinition;
 import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
@@ -33,6 +35,7 @@
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.runner.RunWith;
+import org.junit.Assert;
 
 import javax.jms.MessageConsumer;
 import javax.jms.MessageProducer;
@@ -171,6 +174,30 @@ public void testSQS() throws Exception {
         Assertions.assertThat(received.getText()).isEqualTo("Hello World!");
     }
 
+    @org.junit.Test
+    @org.junit.jupiter.api.Test
+    public void testCloudWatch() throws Exception {
+        AmazonCloudWatch client = TestUtils.getClientCloudWatch();
+        Dimension dimension = new Dimension()
+            .withName("UNIQUE_PAGES")
+            .withValue("URLS");
+        MetricDatum datum = new MetricDatum()
+            .withMetricName("PAGES_VISITED")
+            .withUnit(StandardUnit.None)
+            .withDimensions(dimension);
+        PutMetricDataRequest request = new PutMetricDataRequest()
+            .withNamespace("SITE/TRAFFIC")
+            .withMetricData(datum);
+        // assert no error gets thrown for null values
+        datum.setValue(null);
+        PutMetricDataResult response = client.putMetricData(request);
+        Assert.assertNotNull(response);
+        // assert success for double values
+        datum.setValue(123.4);
+        response = client.putMetricData(request);
+        Assert.assertNotNull(response);
+    }
+
     private SQSConnection createSQSConnection() throws Exception {
         SQSConnectionFactory connectionFactory = SQSConnectionFactory.builder().withEndpoint(
                 Localstack.INSTANCE.getEndpointSQS()).withAWSCredentialsProvider(
diff --git a/tests/integration/test_cloudwatch.py b/tests/integration/test_cloudwatch.py
new file mode 100644
--- /dev/null
+++ b/tests/integration/test_cloudwatch.py
@@ -0,0 +1,35 @@
+import unittest
+from localstack.utils.aws import aws_stack
+
+
+class CloudWatchTest(unittest.TestCase):
+
+    def test_put_metric_data(self):
+        client = aws_stack.connect_to_service('cloudwatch')
+
+        data = [
+            {
+                'MetricName': 'm1',
+                'Dimensions': [{
+                    'Name': 'foo',
+                    'Value': 'bar'
+                }],
+                'Value': 123.45,
+                'StatisticValues': {
+                    'SampleCount': 123.0,
+                    'Sum': 123.0,
+                    'Minimum': 123.0,
+                    'Maximum': 123.0
+                },
+                'Values': [
+                    123.0,
+                ],
+                'Counts': [
+                    123.0,
+                ],
+                'Unit': 'Seconds',
+                'StorageResolution': 123
+            },
+        ]
+        response = client.put_metric_data(Namespace='string', MetricData=data)
+        self.assertEquals(response['ResponseMetadata']['HTTPStatusCode'], 200)
diff --git a/tests/integration/test_logs.py b/tests/integration/test_logs.py
new file mode 100644
--- /dev/null
+++ b/tests/integration/test_logs.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+
+import unittest
+from localstack.utils.aws import aws_stack
+from localstack.utils.common import short_uid
+
+
+class CloudWatchLogsTest(unittest.TestCase):
+
+    def test_put_events_multibyte_msg(self):
+        client = aws_stack.connect_to_service('logs')
+
+        group = 'g-%s' % short_uid()
+        stream = 's-%s' % short_uid()
+        response = client.create_log_group(logGroupName=group)
+        self.assertEquals(response['ResponseMetadata']['HTTPStatusCode'], 200)
+        response = client.create_log_stream(logGroupName=group, logStreamName=stream)
+        self.assertEquals(response['ResponseMetadata']['HTTPStatusCode'], 200)
+
+        # send message with non-ASCII (multi-byte) chars
+        body_msg = '🙀 - 参よ'
+        events = [{
+            'timestamp': 1234567,
+            'message': body_msg
+        }]
+        response = client.put_log_events(logGroupName=group, logStreamName=stream, logEvents=events)
+        self.assertEquals(response['ResponseMetadata']['HTTPStatusCode'], 200)