Repository: cloud-custodian/cloud-custodian

Issue:
aws - opensearch serverless support
### Describe the feature

Add support for aws opensearch serverless

### Extra information or context

_No response_


Discussion:

Resolving patch:
diff --git a/c7n/resources/opensearch.py b/c7n/resources/opensearch.py
new file mode 100644
--- /dev/null
+++ b/c7n/resources/opensearch.py
@@ -0,0 +1,114 @@
+# Copyright The Cloud Custodian Authors.
+# SPDX-License-Identifier: Apache-2.0
+
+from c7n.manager import resources
+from c7n.query import QueryResourceManager, TypeInfo
+from c7n.tags import RemoveTag, Tag, TagActionFilter, TagDelayedAction, universal_augment
+from c7n.utils import local_session, type_schema
+from c7n.actions import BaseAction
+
+
+@resources.register('opensearch-serverless')
+class OpensearchServerless(QueryResourceManager):
+    class resource_type(TypeInfo):
+        service = 'opensearchserverless'
+        arn_type = 'arn'
+        enum_spec = ('list_collections', 'collectionSummaries[]', None)
+        name = "name"
+        id = "id"
+        cfn_type = 'AWS::OpenSearchServerless::Collection'
+        arn = "arn"
+        permission_prefix = 'aoss'
+        augment = universal_augment
+
+@OpensearchServerless.action_registry.register('tag')
+class TagOpensearchServerlessResource(Tag):
+    """Create tags on an OpenSearch Serverless resource
+
+    :example:
+
+    .. code-block:: yaml
+
+        policies:
+            - name: tag-opensearch-serverless
+              resource: opensearch-serverless
+              actions:
+                - type: tag
+                  key: test-key
+                  value: test-value
+    """
+    permissions = ('aoss:TagResource',)
+
+    def process_resource_set(self, client, resources, new_tags):
+        tags = [{'key': item['Key'], 'value': item['Value']} for item in new_tags]
+        for r in resources:
+            client.tag_resource(resourceArn=r["arn"], tags=tags)
+
+
+@OpensearchServerless.action_registry.register('remove-tag')
+class RemoveTagOpensearchServerlessResource(RemoveTag):
+    """Remove tags from an OpenSearch serverless resource
+
+    :example:
+
+    .. code-block:: yaml
+
+        policies:
+            - name: remove-tag-opensearch-serverless
+              resource: opensearch-serverless
+              actions:
+                - type: remove-tag
+                  tags: ["tag-key"]
+    """
+    permissions = ('aoss:UntagResource',)
+
+    def process_resource_set(self, client, resources, tags):
+        for r in resources:
+            client.untag_resource(resourceArn=r['arn'], tagKeys=tags)
+
+OpensearchServerless.filter_registry.register('marked-for-op', TagActionFilter)
+@OpensearchServerless.action_registry.register('mark-for-op')
+class MarkOpensearchServerlessForOp(TagDelayedAction):
+    """Mark OpenSearch Serverless for deferred action
+
+    :example:
+
+    .. code-block:: yaml
+
+        policies:
+          - name: opensearch-serverless-invalid-tag-mark
+            resource: opensearch-serverless
+            filters:
+              - "tag:InvalidTag": present
+            actions:
+              - type: mark-for-op
+                op: delete
+                days: 1
+    """
+
+@OpensearchServerless.action_registry.register('delete')
+class DeleteOpensearchServerless(BaseAction):
+    """Delete an OpenSearch Serverless
+
+    :example:
+
+    .. code-block:: yaml
+
+        policies:
+          - name: delete-opensearch-serverless
+            resource: opensearch-serverless
+            actions:
+              - type: delete
+    """
+    schema = type_schema('delete')
+    permissions = ('aoss:DeleteCollection',)
+    valid_delete_states = ('ACTIVE', 'FAILED')
+
+    def process(self, resources):
+        resources = self.filter_resources(resources, "status", self.valid_delete_states)
+        client = local_session(self.manager.session_factory).client('opensearchserverless')
+        for r in resources:
+            try:
+              client.delete_collection(id=r['id'])
+            except client.exceptions.ResourceNotFoundException:
+              continue
diff --git a/c7n/resources/resource_map.py b/c7n/resources/resource_map.py
--- a/c7n/resources/resource_map.py
+++ b/c7n/resources/resource_map.py
@@ -166,6 +166,7 @@
   "aws.nat-gateway": "c7n.resources.vpc.NATGateway",
   "aws.network-acl": "c7n.resources.vpc.NetworkAcl",
   "aws.network-addr": "c7n.resources.vpc.NetworkAddress",
+  "aws.opensearch-serverless": "c7n.resources.opensearch.OpensearchServerless",
   "aws.ops-item": "c7n.resources.ssm.OpsItem",
   "aws.opswork-cm": "c7n.resources.opsworks.OpsworksCM",
   "aws.opswork-stack": "c7n.resources.opsworks.OpsworkStack",


Test patch:
diff --git a/tests/data/placebo/test_opensearch_serverless_delete/aoss.DeleteCollection_1.json b/tests/data/placebo/test_opensearch_serverless_delete/aoss.DeleteCollection_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_opensearch_serverless_delete/aoss.DeleteCollection_1.json
@@ -0,0 +1,11 @@
+{
+    "status_code": 200,
+    "data": {
+        "deleteCollectionDetail": {
+            "id": "6ot2t518av2khgxr73sf",
+            "name": "c7n-test",
+            "status": "DELETING"
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_opensearch_serverless_delete/aoss.ListCollections_1.json b/tests/data/placebo/test_opensearch_serverless_delete/aoss.ListCollections_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_opensearch_serverless_delete/aoss.ListCollections_1.json
@@ -0,0 +1,14 @@
+{
+    "status_code": 200,
+    "data": {
+        "collectionSummaries": [
+            {
+                "arn": "arn:aws:aoss:us-east-1:644160558196:collection/6ot2t518av2khgxr73sf",
+                "id": "6ot2t518av2khgxr73sf",
+                "name": "c7n-test",
+                "status": "ACTIVE"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_opensearch_serverless_delete/aoss.ListCollections_2.json b/tests/data/placebo/test_opensearch_serverless_delete/aoss.ListCollections_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_opensearch_serverless_delete/aoss.ListCollections_2.json
@@ -0,0 +1,14 @@
+{
+    "status_code": 200,
+    "data": {
+        "collectionSummaries": [
+            {
+                "arn": "arn:aws:aoss:us-east-1:644160558196:collection/6ot2t518av2khgxr73sf",
+                "id": "6ot2t518av2khgxr73sf",
+                "name": "c7n-test",
+                "status": "DELETING"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_opensearch_serverless_remove_tag/aoss.ListCollections_1.json b/tests/data/placebo/test_opensearch_serverless_remove_tag/aoss.ListCollections_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_opensearch_serverless_remove_tag/aoss.ListCollections_1.json
@@ -0,0 +1,14 @@
+{
+    "status_code": 200,
+    "data": {
+        "collectionSummaries": [
+            {
+                "arn": "arn:aws:aoss:us-east-1:644160558196:collection/6ot2t518av2khgxr73sf",
+                "id": "6ot2t518av2khgxr73sf",
+                "name": "c7n-test",
+                "status": "ACTIVE"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_opensearch_serverless_remove_tag/aoss.ListTagsForResource_1.json b/tests/data/placebo/test_opensearch_serverless_remove_tag/aoss.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_opensearch_serverless_remove_tag/aoss.ListTagsForResource_1.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "tags": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_opensearch_serverless_remove_tag/aoss.UntagResource_1.json b/tests/data/placebo/test_opensearch_serverless_remove_tag/aoss.UntagResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_opensearch_serverless_remove_tag/aoss.UntagResource_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_opensearch_serverless_tag/aoss.ListCollections_1.json b/tests/data/placebo/test_opensearch_serverless_tag/aoss.ListCollections_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_opensearch_serverless_tag/aoss.ListCollections_1.json
@@ -0,0 +1,14 @@
+{
+    "status_code": 200,
+    "data": {
+        "collectionSummaries": [
+            {
+                "arn": "arn:aws:aoss:us-east-1:644160558196:collection/6ot2t518av2khgxr73sf",
+                "id": "6ot2t518av2khgxr73sf",
+                "name": "c7n-test",
+                "status": "ACTIVE"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_opensearch_serverless_tag/aoss.ListTagsForResource_1.json b/tests/data/placebo/test_opensearch_serverless_tag/aoss.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_opensearch_serverless_tag/aoss.ListTagsForResource_1.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "tags": [
+            {
+                "key": "foo",
+                "value": "bar"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_opensearch_serverless_tag/aoss.TagResource_1.json b/tests/data/placebo/test_opensearch_serverless_tag/aoss.TagResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_opensearch_serverless_tag/aoss.TagResource_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/test_opensearch.py b/tests/test_opensearch.py
new file mode 100644
--- /dev/null
+++ b/tests/test_opensearch.py
@@ -0,0 +1,70 @@
+# Copyright The Cloud Custodian Authors.
+# SPDX-License-Identifier: Apache-2.0
+from .common import BaseTest
+
+
+class OpensearchServerless(BaseTest):
+
+    def test_opensearch_serverless_tag(self):
+        session_factory = self.replay_flight_data('test_opensearch_serverless_tag')
+        p = self.load_policy(
+            {
+                'name': 'test-opensearch-serverless-tag',
+                'resource': 'opensearch-serverless',
+                'filters': [
+                    {
+                        'tag:foo': 'absent',
+                    }
+                ],
+                'actions': [
+                    {
+                        'type': 'tag',
+                        'tags': {'foo': 'bar'}
+                    }
+                ]
+            }, session_factory=session_factory
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        client = session_factory().client('opensearchserverless')
+        tags = client.list_tags_for_resource(resourceArn=resources[0]["arn"])['tags']
+        self.assertEqual(len(tags), 1)
+        self.assertEqual(tags, [{'key': 'foo', 'value': 'bar'}])
+
+
+    def test_opensearch_serverless_remove_tag(self):
+        session_factory = self.replay_flight_data('test_opensearch_serverless_remove_tag')
+        p = self.load_policy(
+            {
+                'name': 'test-opensearch-serverless-remove-tag',
+                'resource': 'opensearch-serverless',
+                'actions': [
+                    {
+                        'type': 'remove-tag',
+                        'tags': ['foo']
+                    }
+                ]
+            }, session_factory=session_factory
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        client = session_factory().client('opensearchserverless')
+        tags = client.list_tags_for_resource(resourceArn=resources[0]['arn'])['tags']
+        self.assertEqual(len(tags), 0)
+
+    def test_opensearch_serverless_delete(self):
+        session_factory = self.replay_flight_data('test_opensearch_serverless_delete')
+        p = self.load_policy(
+            {
+                'name': 'test-opensearch-serverless-delete',
+                'resource': 'opensearch-serverless',
+                'filters': [{'name': 'c7n-test'}],
+                'actions': [{'type': 'delete'}]
+            },
+            session_factory=session_factory
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        client = session_factory().client('opensearchserverless')
+        collections = client.list_collections()['collectionSummaries']
+        self.assertEqual(collections[0]["status"], "DELETING")