Repository: cloud-custodian/cloud-custodian

Issue:
Support for AWS Bedrock
### Describe the feature

Would love to have support for AWS Bedrock

### Extra information or context

_No response_


Discussion:
@PratMis Hi I've just about gotten my head round the extension mechanism... just about.  I'm enquiring over on slack about adding support for something else. Maybe I could help you with Bedrock. Only thing is it's useful in testing to actually create instances of the resources and I'm not sure what the cost implications are of what you'd need. Never looked at Bedrock.
oops, sorry missed your comment. Yes, I usually spin up a test resource and write policies against it to test. Apparently, bedrock is getting rolled out to customers slowly and I was lucky I got to play with it this week. Was able to create a custom model and am planning to use it to test the new custodian support. 
Ok so there is support 

Resolving patch:
diff --git a/c7n/resources/bedrock.py b/c7n/resources/bedrock.py
new file mode 100644
--- /dev/null
+++ b/c7n/resources/bedrock.py
@@ -0,0 +1,123 @@
+# 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
+from c7n.utils import local_session, type_schema
+from c7n.actions import BaseAction
+
+
+@resources.register('bedrock-custom-model')
+class BedrockCustomModel(QueryResourceManager):
+    class resource_type(TypeInfo):
+        service = 'bedrock'
+        enum_spec = ('list_custom_models', 'modelSummaries[]', None)
+        detail_spec = (
+            'get_custom_model', 'modelIdentifier', 'modelArn', None)
+        name = "modelName"
+        id = arn = "modelArn"
+        permission_prefix = 'bedrock'
+
+    def augment(self, resources):
+        client = local_session(self.session_factory).client('bedrock')
+
+        def _augment(r):
+            tags = self.retry(client.list_tags_for_resource,
+                resourceARN=r['modelArn'])['tags']
+            r['Tags'] = [{'Key': t['key'], 'Value':t['value']} for t in tags]
+            return r
+        resources = super(BedrockCustomModel, self).augment(resources)
+        return list(map(_augment, resources))
+
+
+@BedrockCustomModel.action_registry.register('tag')
+class TagBedrockCustomModel(Tag):
+    """Create tags on Bedrock custom models
+
+    :example:
+
+    .. code-block:: yaml
+
+        policies:
+            - name: bedrock-custom-models-tag
+              resource: aws.bedrock-custom-model
+              actions:
+                - type: tag
+                  key: test
+                  value: something
+    """
+    permissions = ('bedrock: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["modelArn"], tags=tags)
+
+
+@BedrockCustomModel.action_registry.register('remove-tag')
+class RemoveTagBedrockCustomModel(RemoveTag):
+    """Remove tags from a bedrock custom model
+    :example:
+
+    .. code-block:: yaml
+
+        policies:
+            - name: bedrock-model-remove-tag
+              resource: aws.bedrock-custom-model
+              actions:
+                - type: remove-tag
+                  tags: ["tag-key"]
+    """
+    permissions = ('bedrock:UntagResource',)
+
+    def process_resource_set(self, client, resources, tags):
+        for r in resources:
+            client.untag_resource(resourceARN=r['modelArn'], tagKeys=tags)
+
+
+BedrockCustomModel.filter_registry.register('marked-for-op', TagActionFilter)
+@BedrockCustomModel.action_registry.register('mark-for-op')
+class MarkBedrockCustomModelForOp(TagDelayedAction):
+    """Mark custom models for future actions
+
+    :example:
+
+    .. code-block:: yaml
+
+        policies:
+          - name: custom-model-tag-mark
+            resource: aws.bedrock-custom-model
+            filters:
+              - "tag:delete": present
+            actions:
+              - type: mark-for-op
+                op: delete
+                days: 1
+    """
+
+
+@BedrockCustomModel.action_registry.register('delete')
+class DeleteBedrockCustomModel(BaseAction):
+    """Delete a bedrock custom model
+
+    :example:
+
+    .. code-block:: yaml
+
+        policies:
+          - name: custom-model-delete
+            resource: aws.bedrock-custom-model
+            actions:
+              - type: delete
+    """
+    schema = type_schema('delete')
+    permissions = ('bedrock:DeleteCustomModel',)
+
+    def process(self, resources):
+        client = local_session(self.manager.session_factory).client('bedrock')
+        for r in resources:
+            try:
+              client.delete_custom_model(modelIdentifier=r['modelArn'])
+            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
@@ -26,6 +26,7 @@
   "aws.batch-compute": "c7n.resources.batch.ComputeEnvironment",
   "aws.batch-definition": "c7n.resources.batch.JobDefinition",
   "aws.batch-queue": "c7n.resources.batch.BatchJobQueue",
+  "aws.bedrock-custom-model": "c7n.resources.bedrock.BedrockCustomModel",
   "aws.budget": "c7n.resources.budgets.Budget",
   "aws.cache-cluster": "c7n.resources.elasticache.ElastiCacheCluster",
   "aws.cache-snapshot": "c7n.resources.elasticache.ElastiCacheSnapshot",


Test patch:
diff --git a/tests/data/placebo/test_bedrock_custom_model/bedrock.GetCustomModel_1.json b/tests/data/placebo/test_bedrock_custom_model/bedrock.GetCustomModel_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_bedrock_custom_model/bedrock.GetCustomModel_1.json
@@ -0,0 +1,40 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "modelArn": "arn:aws:bedrock:us-east-1:644160558196:custom-model/amazon.titan-text-express-v1:0:8k/w4vtut9aq95n",
+        "modelName": "c7n-test3",
+        "jobArn": "arn:aws:bedrock:us-east-1:644160558196:model-customization-job/amazon.titan-text-express-v1:0:8k/3m4q4ew79u8o",
+        "baseModelArn": "arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-text-express-v1:0:8k",
+        "hyperParameters": {
+            "batchSize": "1",
+            "epochCount": "1",
+            "learningRate": "0.005",
+            "learningRateWarmupSteps": "0"
+        },
+        "trainingDataConfig": {
+            "s3Uri": "s3://bedrock-c7n/training.jsonl"
+        },
+        "outputDataConfig": {
+            "s3Uri": "s3://bedrock-c7n"
+        },
+        "trainingMetrics": {
+            "trainingLoss": 6.125
+        },
+        "validationMetrics": [
+            {
+                "validationLoss": 66.83333587646484
+            }
+        ],
+        "creationTime": {
+            "__class__": "datetime",
+            "year": 2023,
+            "month": 11,
+            "day": 17,
+            "hour": 17,
+            "minute": 36,
+            "second": 7,
+            "microsecond": 245000
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_bedrock_custom_model/bedrock.ListCustomModels_1.json b/tests/data/placebo/test_bedrock_custom_model/bedrock.ListCustomModels_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_bedrock_custom_model/bedrock.ListCustomModels_1.json
@@ -0,0 +1,24 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "modelSummaries": [
+            {
+                "modelArn": "arn:aws:bedrock:us-east-1:644160558196:custom-model/amazon.titan-text-express-v1:0:8k/w4vtut9aq95n",
+                "modelName": "c7n-test3",
+                "creationTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 11,
+                    "day": 17,
+                    "hour": 17,
+                    "minute": 36,
+                    "second": 7,
+                    "microsecond": 245000
+                },
+                "baseModelArn": "arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-text-express-v1:0:8k",
+                "baseModelName": ""
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_bedrock_custom_model/bedrock.ListTagsForResource_1.json b/tests/data/placebo/test_bedrock_custom_model/bedrock.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_bedrock_custom_model/bedrock.ListTagsForResource_1.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "tags": [
+            {
+                "key": "Owner",
+                "value": "c7n"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_bedrock_custom_model/bedrock.ListTagsForResource_2.json b/tests/data/placebo/test_bedrock_custom_model/bedrock.ListTagsForResource_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_bedrock_custom_model/bedrock.ListTagsForResource_2.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "tags": [
+            {
+                "key": "foo",
+                "value": "bar"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_bedrock_custom_model/bedrock.TagResource_1.json b/tests/data/placebo/test_bedrock_custom_model/bedrock.TagResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_bedrock_custom_model/bedrock.TagResource_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_bedrock_custom_model/bedrock.UntagResource_1.json b/tests/data/placebo/test_bedrock_custom_model/bedrock.UntagResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_bedrock_custom_model/bedrock.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_bedrock_custom_model_delete/bedrock.DeleteCustomModel_1.json b/tests/data/placebo/test_bedrock_custom_model_delete/bedrock.DeleteCustomModel_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_bedrock_custom_model_delete/bedrock.DeleteCustomModel_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_bedrock_custom_model_delete/bedrock.GetCustomModel_1.json b/tests/data/placebo/test_bedrock_custom_model_delete/bedrock.GetCustomModel_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_bedrock_custom_model_delete/bedrock.GetCustomModel_1.json
@@ -0,0 +1,40 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "modelArn": "arn:aws:bedrock:us-east-1:644160558196:custom-model/amazon.titan-text-express-v1:0:8k/w4vtut9aq95n",
+        "modelName": "c7n-test3",
+        "jobArn": "arn:aws:bedrock:us-east-1:644160558196:model-customization-job/amazon.titan-text-express-v1:0:8k/3m4q4ew79u8o",
+        "baseModelArn": "arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-text-express-v1:0:8k",
+        "hyperParameters": {
+            "batchSize": "1",
+            "epochCount": "1",
+            "learningRate": "0.005",
+            "learningRateWarmupSteps": "0"
+        },
+        "trainingDataConfig": {
+            "s3Uri": "s3://bedrock-c7n/training.jsonl"
+        },
+        "outputDataConfig": {
+            "s3Uri": "s3://bedrock-c7n"
+        },
+        "trainingMetrics": {
+            "trainingLoss": 6.125
+        },
+        "validationMetrics": [
+            {
+                "validationLoss": 66.83333587646484
+            }
+        ],
+        "creationTime": {
+            "__class__": "datetime",
+            "year": 2023,
+            "month": 11,
+            "day": 17,
+            "hour": 17,
+            "minute": 36,
+            "second": 7,
+            "microsecond": 245000
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_bedrock_custom_model_delete/bedrock.ListCustomModels_1.json b/tests/data/placebo/test_bedrock_custom_model_delete/bedrock.ListCustomModels_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_bedrock_custom_model_delete/bedrock.ListCustomModels_1.json
@@ -0,0 +1,24 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "modelSummaries": [
+            {
+                "modelArn": "arn:aws:bedrock:us-east-1:644160558196:custom-model/amazon.titan-text-express-v1:0:8k/w4vtut9aq95n",
+                "modelName": "c7n-test3",
+                "creationTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 11,
+                    "day": 17,
+                    "hour": 17,
+                    "minute": 36,
+                    "second": 7,
+                    "microsecond": 245000
+                },
+                "baseModelArn": "arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-text-express-v1:0:8k",
+                "baseModelName": ""
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_bedrock_custom_model_delete/bedrock.ListCustomModels_2.json b/tests/data/placebo/test_bedrock_custom_model_delete/bedrock.ListCustomModels_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_bedrock_custom_model_delete/bedrock.ListCustomModels_2.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "modelSummaries": []
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_bedrock_custom_model_delete/bedrock.ListTagsForResource_1.json b/tests/data/placebo/test_bedrock_custom_model_delete/bedrock.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_bedrock_custom_model_delete/bedrock.ListTagsForResource_1.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "tags": [
+            {
+                "key": "foo",
+                "value": "bar"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/test_bedrock.py b/tests/test_bedrock.py
new file mode 100644
--- /dev/null
+++ b/tests/test_bedrock.py
@@ -0,0 +1,52 @@
+# Copyright The Cloud Custodian Authors.
+# SPDX-License-Identifier: Apache-2.0
+from .common import BaseTest
+
+
+class BedrockCustomModel(BaseTest):
+
+    def test_bedrock_custom_model(self):
+        session_factory = self.replay_flight_data('test_bedrock_custom_model')
+        p = self.load_policy(
+            {
+                'name': 'bedrock-custom-model-tag',
+                'resource': 'bedrock-custom-model',
+                'filters': [
+                    {'tag:foo': 'absent'},
+                    {'tag:Owner': 'c7n'},
+                ],
+                'actions': [
+                    {
+                        'type': 'tag',
+                        'tags': {'foo': 'bar'}
+                    },
+                    {
+                        'type': 'remove-tag',
+                        'tags': ['Owner']
+                    }
+                ]
+            }, session_factory=session_factory
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        client = session_factory().client('bedrock')
+        tags = client.list_tags_for_resource(resourceARN=resources[0]['modelArn'])['tags']
+        self.assertEqual(len(tags), 1)
+        self.assertEqual(tags, [{'key': 'foo', 'value': 'bar'}])
+
+    def test_bedrock_custom_model_delete(self):
+        session_factory = self.replay_flight_data('test_bedrock_custom_model_delete')
+        p = self.load_policy(
+            {
+                'name': 'custom-model-delete',
+                'resource': 'bedrock-custom-model',
+                'filters': [{'modelName': 'c7n-test3'}],
+                'actions': [{'type': 'delete'}]
+            },
+            session_factory=session_factory
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        client = session_factory().client('bedrock')
+        models = client.list_custom_models().get('modelSummaries')
+        self.assertEqual(len(models), 0)