Repository: getmoto/moto
Pull request #6825 (2023-09-18 12:31:11)

ECS: add pidMode validation for FARGATE
ECS on FARGATE only allows `task` for pidMode
https://docs.aws.amazon.com/AmazonECS/latest/userguide/task_definition_parameters.html


Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5954fa91f..4d929cd65 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -116,22 +116,22 @@ jobs:
       # Required to get the correct Digest
       # See https://github.com/docker/build-push-action/issues/461
       - name: Set up QEMU
-        uses: docker/setup-qemu-action@v2
+        uses: docker/setup-qemu-action@v3
       - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v2
+        uses: docker/setup-buildx-action@v3
       - name: Login to DockerHub
-        uses: docker/login-action@v2
+        uses: docker/login-action@v3
         with:
           username: ${{ secrets.DOCKERHUB_USERNAME }}
           password: ${{ secrets.DOCKERHUB_TOKEN }}
       - name: Login to GHCR
-        uses: docker/login-action@v2
+        uses: docker/login-action@v3
         with:
           registry: ghcr.io
           username: ${{ github.actor }}
           password: ${{ secrets.GITHUB_TOKEN }}
       - name: Push to DockerHub and GHCR
-        uses: docker/build-push-action@v4
+        uses: docker/build-push-action@v5
         with:
           push: true
           platforms: linux/amd64,linux/arm64
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 203a1bc67..744765d3c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -67,23 +67,23 @@ jobs:
     # Required to get the correct Digest
     # See https://github.com/docker/build-push-action/issues/461
     - name: Set up QEMU
-      uses: docker/setup-qemu-action@v2
+      uses: docker/setup-qemu-action@v3
     - name: Set up Docker Buildx
-      uses: docker/setup-buildx-action@v2
+      uses: docker/setup-buildx-action@v3
     - name: Login to DockerHub
-      uses: docker/login-action@v2
+      uses: docker/login-action@v3
       with:
         username: ${{ secrets.DOCKERHUB_USERNAME }}
         password: ${{ secrets.DOCKERHUB_TOKEN }}
     - name: Login to GHCR
-      uses: docker/login-action@v2
+      uses: docker/login-action@v3
       with:
         registry: ghcr.io
         username: ${{ github.actor }}
         password: ${{ secrets.GITHUB_TOKEN }}
     - name: Push to DockerHub and GHCR
       id: build_and_push
-      uses: docker/build-push-action@v4
+      uses: docker/build-push-action@v5
       with:
         platforms: linux/amd64,linux/arm64
         push: true
diff --git a/docs/docs/services/opensearch.rst b/docs/docs/services/opensearch.rst
index a6728af5d..c7196e50d 100644
--- a/docs/docs/services/opensearch.rst
+++ b/docs/docs/services/opensearch.rst
@@ -61,7 +61,7 @@ opensearch
 - [ ] get_package_version_history
 - [ ] get_upgrade_history
 - [ ] get_upgrade_status
-- [ ] list_domain_names
+- [x] list_domain_names
 - [ ] list_domains_for_package
 - [ ] list_instance_type_details
 - [ ] list_packages_for_domain
diff --git a/moto/dynamodb/models/dynamo_type.py b/moto/dynamodb/models/dynamo_type.py
index d6554a25c..fe44ab17f 100644
--- a/moto/dynamodb/models/dynamo_type.py
+++ b/moto/dynamodb/models/dynamo_type.py
@@ -196,7 +196,7 @@ class DynamoType(object):
 
     def to_json(self) -> Dict[str, Any]:
         # Returns a regular JSON object where the value can still be/contain a DynamoType
-        if self.is_binary():
+        if self.is_binary() and isinstance(self.value, bytes):
             # Binary data cannot be represented in JSON
             # AWS returns a base64-encoded value - the SDK's then convert that back
             return {self.type: base64.b64encode(self.value).decode("utf-8")}
diff --git a/moto/ecs/models.py b/moto/ecs/models.py
index 5689ccbdb..b05c3749f 100644
--- a/moto/ecs/models.py
+++ b/moto/ecs/models.py
@@ -1138,6 +1138,14 @@ class EC2ContainerServiceBackend(BaseBackend):
         pid_mode: Optional[str] = None,
         ephemeral_storage: Optional[Dict[str, int]] = None,
     ) -> TaskDefinition:
+
+        if requires_compatibilities and "FARGATE" in requires_compatibilities:
+            # TODO need more validation for Fargate
+            if pid_mode and pid_mode != "task":
+                raise EcsClientException(
+                    f"Tasks using the Fargate launch type do not support pidMode '{pid_mode}'. The supported value for pidMode is 'task'."
+                )
+
         if family in self.task_definitions:
             last_id = self._get_last_task_definition_revision_id(family)
             revision = (last_id or 0) + 1
diff --git a/moto/es/urls.py b/moto/es/urls.py
index 9c8322d6b..b26bfd9ca 100644
--- a/moto/es/urls.py
+++ b/moto/es/urls.py
@@ -10,6 +10,7 @@ url_paths = {
     "{0}/2015-01-01/domain$": ElasticsearchServiceResponse.list_domains,
     "{0}/2015-01-01/es/domain$": ElasticsearchServiceResponse.domains,
     "{0}/2015-01-01/es/domain/(?P<domainname>[^/]+)": ElasticsearchServiceResponse.domain,
+    "{0}/2021-01-01/domain$": OpenSearchServiceResponse.dispatch,
     "{0}/2021-01-01/opensearch/compatibleVersions": OpenSearchServiceResponse.dispatch,
     "{0}/2021-01-01/opensearch/domain": OpenSearchServiceResponse.dispatch,
     "{0}/2021-01-01/opensearch/domain/(?P<domainname>[^/]+)": OpenSearchServiceResponse.dispatch,
diff --git a/moto/opensearch/exceptions.py b/moto/opensearch/exceptions.py
index e0c3a9a33..b5b3f9fe9 100644
--- a/moto/opensearch/exceptions.py
+++ b/moto/opensearch/exceptions.py
@@ -5,3 +5,10 @@ from moto.core.exceptions import JsonRESTError
 class ResourceNotFoundException(JsonRESTError):
     def __init__(self, name: str):
         super().__init__("ResourceNotFoundException", f"Domain not found: {name}")
+
+
+class EngineTypeNotFoundException(JsonRESTError):
+    def __init__(self, domain_name: str):
+        super().__init__(
+            "EngineTypeNotFoundException", f"Engine Type not found: {domain_name}"
+        )
diff --git a/moto/opensearch/models.py b/moto/opensearch/models.py
index 4ef254e49..d2b724021 100644
--- a/moto/opensearch/models.py
+++ b/moto/opensearch/models.py
@@ -2,9 +2,9 @@ from typing import Any, Dict, List, Optional
 
 from moto.core import BaseBackend, BackendDict, BaseModel
 from moto.utilities.tagging_service import TaggingService
-from .data import compatible_versions
-from .exceptions import ResourceNotFoundException
 
+from .data import compatible_versions
+from .exceptions import ResourceNotFoundException, EngineTypeNotFoundException
 
 default_cluster_config = {
     "InstanceType": "t3.small.search",
@@ -327,5 +327,27 @@ class OpenSearchServiceBackend(BaseBackend):
     def remove_tags(self, arn: str, tag_keys: List[str]) -> None:
         self.tagger.untag_resource_using_names(arn, tag_keys)
 
+    def list_domain_names(self, engine_type: str) -> List[Dict[str, str]]:
+        domains = []
+        for domain in self.domains.values():
+            if engine_type:
+                if engine_type in domain.engine_version:
+                    domains.append(
+                        {
+                            "DomainName": domain.domain_name,
+                            "EngineType": engine_type.split("_")[0],
+                        }
+                    )
+                else:
+                    raise EngineTypeNotFoundException(domain.domain_name)
+            else:
+                domains.append(
+                    {
+                        "DomainName": domain.domain_name,
+                        "EngineType": domain.engine_version.split("_")[0],
+                    }
+                )
+        return domains
+
 
 opensearch_backends = BackendDict(OpenSearchServiceBackend, "opensearch")
diff --git a/moto/opensearch/responses.py b/moto/opensearch/responses.py
index 2d189ffac..2d3bbf088 100644
--- a/moto/opensearch/responses.py
+++ b/moto/opensearch/responses.py
@@ -2,6 +2,7 @@
 import json
 
 from moto.core.responses import BaseResponse
+
 from .models import opensearch_backends, OpenSearchServiceBackend
 
 
@@ -138,3 +139,10 @@ class OpenSearchServiceResponse(BaseResponse):
         tag_keys = self._get_param("TagKeys")
         self.opensearch_backend.remove_tags(arn, tag_keys)
         return "{}"
+
+    def list_domain_names(self) -> str:
+        engine_type = self._get_param("engineType")
+        domain_names = self.opensearch_backend.list_domain_names(
+            engine_type=engine_type,
+        )
+        return json.dumps(dict(DomainNames=domain_names))


Test patch:
diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py
index 667c0dfa4..ec5f1982f 100644
--- a/tests/test_dynamodb/test_dynamodb.py
+++ b/tests/test_dynamodb/test_dynamodb.py
@@ -5745,11 +5745,19 @@ def test_projection_expression_with_binary_attr():
     )
     table = dynamo_resource.Table("test")
     table.put_item(Item={"pk": "pk", "sk": "sk", "key": b"value\xbf"})
-    assert table.get_item(
+
+    item = table.get_item(
         Key={"pk": "pk", "sk": "sk"},
         ExpressionAttributeNames={"#key": "key"},
         ProjectionExpression="#key",
-    )["Item"] == {"key": Binary(b"value\xbf")}
+    )["Item"]
+    assert item == {"key": Binary(b"value\xbf")}
+
+    item = table.scan()["Items"][0]
+    assert item["key"] == Binary(b"value\xbf")
+
+    item = table.query(KeyConditionExpression=Key("pk").eq("pk"))["Items"][0]
+    assert item["key"] == Binary(b"value\xbf")
 
 
 @mock_dynamodb
diff --git a/tests/test_dynamodb/test_dynamodb_batch_get_item.py b/tests/test_dynamodb/test_dynamodb_batch_get_item.py
index 0bb678ba5..fa75e92ac 100644
--- a/tests/test_dynamodb/test_dynamodb_batch_get_item.py
+++ b/tests/test_dynamodb/test_dynamodb_batch_get_item.py
@@ -14,7 +14,7 @@ def _create_user_table():
         ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
     )
     client.put_item(
-        TableName="users", Item={"username": {"S": "user1"}, "foo": {"S": "bar"}}
+        TableName="users", Item={"username": {"S": "user1"}, "binaryfoo": {"B": b"bar"}}
     )
     client.put_item(
         TableName="users", Item={"username": {"S": "user2"}, "foo": {"S": "bar"}}
@@ -42,11 +42,9 @@ def test_batch_items_returns_all():
         }
     )["Responses"]["users"]
     assert len(returned_items) == 3
-    assert [item["username"]["S"] for item in returned_items] == [
-        "user1",
-        "user2",
-        "user3",
-    ]
+    assert {"username": {"S": "user1"}, "binaryfoo": {"B": b"bar"}} in returned_items
+    assert {"username": {"S": "user2"}, "foo": {"S": "bar"}} in returned_items
+    assert {"username": {"S": "user3"}, "foo": {"S": "bar"}} in returned_items
 
 
 @mock_dynamodb
@@ -137,12 +135,10 @@ def test_batch_items_with_basic_projection_expression():
         }
     )["Responses"]["users"]
 
-    assert [item["username"]["S"] for item in returned_items] == [
-        "user1",
-        "user2",
-        "user3",
-    ]
-    assert [item["foo"]["S"] for item in returned_items] == ["bar", "bar", "bar"]
+    assert len(returned_items) == 3
+    assert {"username": {"S": "user1"}, "binaryfoo": {"B": b"bar"}} in returned_items
+    assert {"username": {"S": "user2"}, "foo": {"S": "bar"}} in returned_items
+    assert {"username": {"S": "user3"}, "foo": {"S": "bar"}} in returned_items
 
 
 @mock_dynamodb
@@ -165,12 +161,9 @@ def test_batch_items_with_basic_projection_expression_and_attr_expression_names(
     )["Responses"]["users"]
 
     assert len(returned_items) == 3
-    assert [item["username"]["S"] for item in returned_items] == [
-        "user1",
-        "user2",
-        "user3",
-    ]
-    assert [item.get("foo") for item in returned_items] == [None, None, None]
+    assert {"username": {"S": "user1"}} in returned_items
+    assert {"username": {"S": "user2"}} in returned_items
+    assert {"username": {"S": "user3"}} in returned_items
 
 
 @mock_dynamodb
diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py
index 51b74be22..912b7be58 100644
--- a/tests/test_ecs/test_ecs_boto3.py
+++ b/tests/test_ecs/test_ecs_boto3.py
@@ -293,6 +293,33 @@ def test_register_task_definition():
     )
 
 
+@mock_ecs
+def test_register_task_definition_fargate_with_pid_mode():
+    client = boto3.client("ecs", region_name="us-east-1")
+    definition = dict(
+        family="test_ecs_task",
+        containerDefinitions=[
+            {"name": "hello_world", "image": "hello-world:latest", "memory": 400}
+        ],
+        requiresCompatibilities=["FARGATE"],
+        pidMode="host",
+        networkMode="awsvpc",
+        cpu="256",
+        memory="512",
+    )
+
+    with pytest.raises(ClientError) as exc:
+        client.register_task_definition(**definition)
+    ex = exc.value
+    assert ex.operation_name == "RegisterTaskDefinition"
+    assert ex.response["ResponseMetadata"]["HTTPStatusCode"] == 400
+    assert ex.response["Error"]["Code"] == "ClientException"
+    assert (
+        ex.response["Error"]["Message"]
+        == "Tasks using the Fargate launch type do not support pidMode 'host'. The supported value for pidMode is 'task'."
+    )
+
+
 @mock_ecs
 def test_list_task_definitions():
     client = boto3.client("ecs", region_name="us-east-1")
diff --git a/tests/test_opensearch/test_opensearch.py b/tests/test_opensearch/test_opensearch.py
index 38609040c..380e018bd 100644
--- a/tests/test_opensearch/test_opensearch.py
+++ b/tests/test_opensearch/test_opensearch.py
@@ -1,9 +1,10 @@
 import boto3
 import pytest
-
 from botocore.exceptions import ClientError
+
 from moto import mock_opensearch
 
+
 # See our Development Tips on writing tests for hints on how to write good tests:
 # http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html
 
@@ -139,3 +140,67 @@ def test_update_domain_config():
             "CustomEndpointEnabled": False,
         }
     }
+
+
+@mock_opensearch
+def test_list_domain_names():
+    client = boto3.client("opensearch", region_name="ap-southeast-1")
+
+    test_domain_names_list_exist = False
+
+    opensearch_domain_name = "testdn"
+    opensearch_engine_version = "OpenSearch_1.0"
+    client.create_domain(
+        DomainName=opensearch_domain_name, EngineVersion=opensearch_engine_version
+    )
+
+    resp = client.list_domain_names()
+    domain_names = resp["DomainNames"]
+
+    for domain_name in domain_names:
+        if domain_name["DomainName"] == opensearch_domain_name:
+            test_domain_names_list_exist = True
+
+    assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+    assert test_domain_names_list_exist
+
+
+@mock_opensearch
+def test_list_filtered_domain_names():
+    client = boto3.client("opensearch", region_name="ap-southeast-1")
+
+    test_domain_names_list_exist = False
+
+    opensearch_domain_name = "testdn"
+    opensearch_engine_version = "OpenSearch_1.0"
+    client.create_domain(
+        DomainName=opensearch_domain_name, EngineVersion=opensearch_engine_version
+    )
+
+    resp = client.list_domain_names(EngineType="OpenSearch")
+    domain_names = resp["DomainNames"]
+
+    for domain_name in domain_names:
+        if domain_name["DomainName"] == opensearch_domain_name:
+            if domain_name["EngineType"] == opensearch_engine_version.split("_")[0]:
+                test_domain_names_list_exist = True
+
+    assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+    assert test_domain_names_list_exist
+
+
+@mock_opensearch
+def test_list_unknown_domain_names_engine_type():
+    client = boto3.client("opensearch", region_name="ap-southeast-1")
+
+    opensearch_domain_name = "testdn"
+    opensearch_engine_version = "OpenSearch_1.0"
+    client.create_domain(
+        DomainName=opensearch_domain_name, EngineVersion=opensearch_engine_version
+    )
+
+    with pytest.raises(ClientError) as exc:
+        client.list_domain_names(EngineType="unknown")
+    err = exc.value.response["Error"]
+    assert err["Code"] == "EngineTypeNotFoundException"
+    assert err["Message"] == "Engine Type not found: testdn"