Repository: localstack/localstack

Issue:
question: Are there any plans to add AWS::OpenSearchService::Domain resource?
### Is there an existing issue for this?

- [X] I have searched the existing issues and read the documentation

### Question

I am using serverless framework with localstack and I am unable to define an `AWS::OpenSearchService::Domain` resource. I guess I'll have to use `AWS::Elasticsearch::Domain` instead but it is deprecated. Are there any plans to add `AWS::OpenSearchService::Domain` resource soon? 

### Anything else?

_No response_


Discussion:

Resolving patch:
diff --git a/localstack/services/cloudformation/models/__init__.py b/localstack/services/cloudformation/models/__init__.py
--- a/localstack/services/cloudformation/models/__init__.py
+++ b/localstack/services/cloudformation/models/__init__.py
@@ -8,6 +8,7 @@
     "ec2",
     "ecr",
     "elasticsearch",
+    "opensearch",
     "events",
     "iam",
     "kinesis",
diff --git a/localstack/services/cloudformation/models/opensearch.py b/localstack/services/cloudformation/models/opensearch.py
new file mode 100644
--- /dev/null
+++ b/localstack/services/cloudformation/models/opensearch.py
@@ -0,0 +1,75 @@
+from localstack.aws.api.opensearch import (
+    CreateDomainRequest,
+    OpenSearchPartitionInstanceType,
+    OpenSearchWarmPartitionInstanceType,
+)
+from localstack.services.cloudformation.deployment_utils import remove_none_values
+from localstack.services.cloudformation.service_models import GenericBaseModel
+from localstack.utils.aws import aws_stack
+from localstack.utils.collections import select_attributes
+
+
+# OpenSearch still uses "es" ARNs
+# See examples in:
+# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html
+def opensearch_add_tags_params(params, **kwargs):
+    es_arn = aws_stack.es_domain_arn(params.get("DomainName"))
+    tags = params.get("Tags", [])
+    return {"ARN": es_arn, "TagList": tags}
+
+
+class OpenSearchDomain(GenericBaseModel):
+    @staticmethod
+    def cloudformation_type():
+        return "AWS::OpenSearchService::Domain"
+
+    def get_physical_resource_id(self, attribute=None, **kwargs):
+        domain_name = self._domain_name()
+        if attribute == "Arn":
+            # As mentioned above, OpenSearch still uses "es" ARNs
+            return aws_stack.elasticsearch_domain_arn(domain_name)
+        return domain_name
+
+    def fetch_state(self, stack_name, resources):
+        domain_name = self._domain_name()
+        domain_name = self.resolve_refs_recursively(stack_name, domain_name, resources)
+        return aws_stack.connect_to_service("opensearch").describe_domain(DomainName=domain_name)
+
+    def _domain_name(self):
+        return self.props.get("DomainName") or self.resource_id
+
+    @staticmethod
+    def get_deploy_templates():
+        def _create_params(params, **kwargs):
+            # Tags handled outside of creation
+            attributes = [
+                attribute
+                for attribute in CreateDomainRequest.__annotations__.keys()
+                if "Tag" not in attribute
+            ]
+            result = select_attributes(params, attributes)
+            result = remove_none_values(result)
+            cluster_config = result.get("ClusterConfig")
+            if isinstance(cluster_config, dict):
+                # set defaults required for boto3 calls
+                cluster_config.setdefault(
+                    "DedicatedMasterType", OpenSearchPartitionInstanceType.m3_medium_search
+                )
+                cluster_config.setdefault(
+                    "WarmType", OpenSearchWarmPartitionInstanceType.ultrawarm1_medium_search
+                )
+            return result
+
+        return {
+            "create": [
+                {
+                    "function": "create_domain",
+                    "parameters": _create_params,
+                },
+                {"function": "add_tags", "parameters": opensearch_add_tags_params},
+            ],
+            "delete": {
+                "function": "delete_domain",
+                "parameters": {"DomainName": "DomainName"},
+            },
+        }
diff --git a/localstack/utils/cloudformation/template_deployer.py b/localstack/utils/cloudformation/template_deployer.py
--- a/localstack/utils/cloudformation/template_deployer.py
+++ b/localstack/utils/cloudformation/template_deployer.py
@@ -123,6 +123,8 @@ def get_service_name(resource):
         return "cognito-idp"
     if parts[-2] == "Elasticsearch":
         return "es"
+    if parts[-2] == "OpenSearchService":
+        return "opensearch"
     if parts[-2] == "KinesisFirehose":
         return "firehose"
     if parts[-2] == "ResourceGroups":


Test patch:
diff --git a/tests/integration/templates/opensearch_domain.yaml b/tests/integration/templates/opensearch_domain.yaml
new file mode 100644
--- /dev/null
+++ b/tests/integration/templates/opensearch_domain.yaml
@@ -0,0 +1,10 @@
+AWSTemplateFormatVersion: 2010-09-09
+Parameters:
+  OpenSearchDomainName:
+    Type: String
+Resources:
+  OpenSearchServiceDomain:
+    Type: AWS::OpenSearchService::Domain
+    Properties:
+      DomainName:
+        Ref: OpenSearchDomainName
\ No newline at end of file
diff --git a/tests/integration/test_opensearch.py b/tests/integration/test_opensearch.py
--- a/tests/integration/test_opensearch.py
+++ b/tests/integration/test_opensearch.py
@@ -1,5 +1,6 @@
 import json
 import logging
+import os
 import threading
 
 import botocore.exceptions
@@ -385,6 +386,20 @@ def test_endpoint_strategy_port(self, monkeypatch, opensearch_create_domain, ope
             config.EXTERNAL_SERVICE_PORTS_START, config.EXTERNAL_SERVICE_PORTS_END
         )
 
+    # testing CloudFormation deployment here to make sure OpenSearch is installed
+    def test_cloudformation_deployment(self, deploy_cfn_template, opensearch_client):
+        domain_name = f"domain-{short_uid()}"
+        deploy_cfn_template(
+            template_path=os.path.join(
+                os.path.dirname(__file__), "templates/opensearch_domain.yaml"
+            ),
+            parameters={"OpenSearchDomainName": domain_name},
+        )
+
+        response = opensearch_client.list_domain_names(EngineType="OpenSearch")
+        domain_names = [domain["DomainName"] for domain in response["DomainNames"]]
+        assert domain_name in domain_names
+
 
 @pytest.mark.skip_offline
 class TestEdgeProxiedOpensearchCluster: