Repository: getmoto/moto
Pull request #8987 (2025-06-15 23:13:38)

RAM - Implementing ListResourceTypes
Greetings.

Recently, I'm working with the RAM methods and I noticed that we don't have some API methods for this service implemented here. So, I commited some changes in the lib:
- I'm adding the AWS Glue `database`, `table` and `catalog` shared resource types to enable these resources for the currently available API implementations;
- I'm implementing the `list_resource_types()` API, just to pavimentate the next implementations I'm willing to code.

I've been thinking about implementing the permissions methods in another PR, such as:
- `create_permission()`;
- `get_permission()`;
- `list_permissions()`;
- `list_permission_associations()`;
- `list_resource_share_permissions()`

I'm new here, so, feel free to recomend any design pattern for the implementations I added and to guide me with any recomendations for the code and the contributions :)


Patch:
diff --git a/docs/docs/services/ram.rst b/docs/docs/services/ram.rst
index 546f9704d..1a3b86074 100644
--- a/docs/docs/services/ram.rst
+++ b/docs/docs/services/ram.rst
@@ -38,7 +38,7 @@ ram
 - [ ] list_principals
 - [ ] list_replace_permission_associations_work
 - [ ] list_resource_share_permissions
-- [ ] list_resource_types
+- [X] list_resource_types
 - [ ] list_resources
 - [ ] promote_permission_created_from_policy
 - [ ] promote_resource_share_created_from_policy
diff --git a/moto/athena/models.py b/moto/athena/models.py
index a8fcdbcdc..152e692e4 100644
--- a/moto/athena/models.py
+++ b/moto/athena/models.py
@@ -117,6 +117,7 @@ class Execution(BaseModel):
         self.workgroup = workgroup
         self.execution_parameters = execution_parameters
         self.start_time = time.time()
+        self.end_time = time.time()
         self.status = "SUCCEEDED"
 
         if self.config is not None and "OutputLocation" in self.config:
diff --git a/moto/athena/responses.py b/moto/athena/responses.py
index 60cd55526..0b58b21b2 100644
--- a/moto/athena/responses.py
+++ b/moto/athena/responses.py
@@ -76,18 +76,24 @@ class AthenaResponse(BaseResponse):
                 "Query": execution.query,
                 "StatementType": statement_type,
                 "ResultConfiguration": execution.config,
+                "ResultReuseConfiguration": {
+                    "ResultReuseByAgeConfiguration": {"Enabled": False}
+                },
                 "QueryExecutionContext": execution.context,
                 "Status": {
                     "State": execution.status,
                     "SubmissionDateTime": execution.start_time,
+                    "CompletionDateTime": execution.end_time,
                 },
                 "Statistics": {
                     "EngineExecutionTimeInMillis": 0,
                     "DataScannedInBytes": 0,
                     "TotalExecutionTimeInMillis": 0,
                     "QueryQueueTimeInMillis": 0,
+                    "ServicePreProcessingTimeInMillis": 0,
                     "QueryPlanningTimeInMillis": 0,
                     "ServiceProcessingTimeInMillis": 0,
+                    "ResultReuseInformation": {"ReusedPreviousResult": False},
                 },
                 "WorkGroup": execution.workgroup.name if execution.workgroup else None,
             }
diff --git a/moto/ram/models.py b/moto/ram/models.py
index 69fb55375..6f19c4c2c 100644
--- a/moto/ram/models.py
+++ b/moto/ram/models.py
@@ -37,7 +37,10 @@ class ResourceShare(BaseModel):
         "report-group",  # AWS CodeBuild report group
         "resolver-rule",  # Amazon Route 53 forwarding rule
         "subnet",  # Amazon EC2 subnet
-        "transit-gateway",  # Amazon EC2 transit gateway
+        "transit-gateway",  # Amazon EC2 transit gateway,
+        "database",  # Amazon Glue database
+        "table",  # Amazon Glue table
+        "catalog",  # Amazon Glue catalog
     ]
 
     def __init__(self, account_id: str, region: str, **kwargs: Any):
@@ -153,6 +156,94 @@ class ResourceShare(BaseModel):
 
 
 class ResourceAccessManagerBackend(BaseBackend):
+    RESOURCE_TYPES = [  # List of resource types based on SHAREABLE_RESOURCES
+        {
+            "resourceType": "rds:Cluster",
+            "serviceName": "rds",
+            "resourceRegionScope": "REGIONAL",
+        },
+        {
+            "resourceType": "imagebuilder:Component",
+            "serviceName": "imagebuilder",
+            "resourceRegionScope": "REGIONAL",
+        },
+        {
+            "resourceType": "networkmanager:CoreNetwork",
+            "serviceName": "networkmanager",
+            "resourceRegionScope": "GLOBAL",
+        },
+        {
+            "resourceType": "resource-groups:Group",
+            "serviceName": "resource-groups",
+            "resourceRegionScope": "REGIONAL",
+        },
+        {
+            "resourceType": "imagebuilder:Image",
+            "serviceName": "imagebuilder",
+            "resourceRegionScope": "REGIONAL",
+        },
+        {
+            "resourceType": "imagebuilder:ImageRecipe",
+            "serviceName": "imagebuilder",
+            "resourceRegionScope": "REGIONAL",
+        },
+        {
+            "resourceType": "license-manager:LicenseConfiguration",
+            "serviceName": "license-manager",
+            "resourceRegionScope": "REGIONAL",
+        },
+        {
+            "resourceType": "appmesh:Mesh",
+            "serviceName": "appmesh",
+            "resourceRegionScope": "REGIONAL",
+        },
+        {
+            "resourceType": "ec2:PrefixList",
+            "serviceName": "ec2",
+            "resourceRegionScope": "REGIONAL",
+        },
+        {
+            "resourceType": "codebuild:Project",
+            "serviceName": "codebuild",
+            "resourceRegionScope": "REGIONAL",
+        },
+        {
+            "resourceType": "codebuild:ReportGroup",
+            "serviceName": "codebuild",
+            "resourceRegionScope": "REGIONAL",
+        },
+        {
+            "resourceType": "route53resolver:ResolverRule",
+            "serviceName": "route53resolver",
+            "resourceRegionScope": "REGIONAL",
+        },
+        {
+            "resourceType": "ec2:Subnet",
+            "serviceName": "ec2",
+            "resourceRegionScope": "REGIONAL",
+        },
+        {
+            "resourceType": "ec2:TransitGatewayMulticastDomain",
+            "serviceName": "ec2",
+            "resourceRegionScope": "REGIONAL",
+        },
+        {
+            "resourceType": "glue:Database",
+            "serviceName": "glue",
+            "resourceRegionScope": "REGIONAL",
+        },
+        {
+            "resourceType": "glue:Table",
+            "serviceName": "glue",
+            "resourceRegionScope": "REGIONAL",
+        },
+        {
+            "resourceType": "glue:Catalog",
+            "serviceName": "glue",
+            "resourceRegionScope": "REGIONAL",
+        },
+    ]
+
     def __init__(self, region_name: str, account_id: str):
         super().__init__(region_name, account_id)
         self.resource_shares: List[ResourceShare] = []
@@ -302,5 +393,25 @@ class ResourceAccessManagerBackend(BaseBackend):
 
         return dict(resourceShareAssociations=associations)
 
+    def list_resource_types(self, **kwargs: Any) -> Dict[str, Any]:
+        resource_region_scope = kwargs.get("resourceRegionScope", "ALL")
+
+        if resource_region_scope not in ["ALL", "REGIONAL", "GLOBAL"]:
+            raise InvalidParameterException(
+                f"{resource_region_scope} is not a valid resource region "
+                "scope value. Specify a valid value and try again."
+            )
+
+        if resource_region_scope == "ALL":
+            resource_types = self.RESOURCE_TYPES
+        else:
+            resource_types = [
+                resource_type
+                for resource_type in self.RESOURCE_TYPES
+                if resource_type["resourceRegionScope"] == resource_region_scope
+            ]
+
+        return dict(resourceTypes=resource_types)
+
 
 ram_backends = BackendDict(ResourceAccessManagerBackend, "ram")
diff --git a/moto/ram/responses.py b/moto/ram/responses.py
index 93ea41153..7b36f71f1 100644
--- a/moto/ram/responses.py
+++ b/moto/ram/responses.py
@@ -41,3 +41,6 @@ class ResourceAccessManagerResponse(BaseResponse):
         return json.dumps(
             self.ram_backend.get_resource_share_associations(**self.request_params)
         )
+
+    def list_resource_types(self) -> str:
+        return json.dumps(self.ram_backend.list_resource_types(**self.request_params))
diff --git a/moto/ram/urls.py b/moto/ram/urls.py
index 1b5775bb0..e973085d1 100644
--- a/moto/ram/urls.py
+++ b/moto/ram/urls.py
@@ -9,4 +9,5 @@ url_paths = {
     "{0}/getresourceshares$": ResourceAccessManagerResponse.dispatch,
     "{0}/getresourceshareassociations$": ResourceAccessManagerResponse.dispatch,
     "{0}/updateresourceshare$": ResourceAccessManagerResponse.dispatch,
+    "{0}/listresourcetypes$": ResourceAccessManagerResponse.dispatch,
 }
diff --git a/moto/rds/models.py b/moto/rds/models.py
index 49304d78e..cb4f97375 100644
--- a/moto/rds/models.py
+++ b/moto/rds/models.py
@@ -492,6 +492,7 @@ class DBCluster(RDSBaseModel):
         port: Optional[int] = None,
         preferred_backup_window: str = "01:37-02:07",
         preferred_maintenance_window: str = "wed:02:40-wed:03:10",
+        publicly_accessible: bool = False,
         storage_encrypted: bool = False,
         tags: Optional[List[Dict[str, str]]] = None,
         vpc_security_group_ids: Optional[List[str]] = None,
@@ -582,6 +583,7 @@ class DBCluster(RDSBaseModel):
         self.port = port or DBCluster.default_port(self.engine)
         self.preferred_backup_window = preferred_backup_window
         self.preferred_maintenance_window = preferred_maintenance_window
+        self.publicly_accessible = publicly_accessible
         # This should default to the default security group
         self._vpc_security_group_ids = vpc_security_group_ids or []
         self.hosted_zone_id = "".join(


Test patch:
diff --git a/.github/workflows/tests_sdk_ruby.yml b/.github/workflows/tests_sdk_ruby.yml
index bc5e97bfe..51232c8c9 100644
--- a/.github/workflows/tests_sdk_ruby.yml
+++ b/.github/workflows/tests_sdk_ruby.yml
@@ -13,7 +13,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - name: Set up Ruby ${{ matrix.ruby-version }}
-        uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb
+        uses: ruby/setup-ruby@a4effe49ee8ee5b8b5091268c473a4628afb5651
         with:
           ruby-version: ${{ matrix.ruby-version }}
       - name: Set up Python 3.12
diff --git a/tests/test_athena/test_athena.py b/tests/test_athena/test_athena.py
index 6229dec8a..0121a7a87 100644
--- a/tests/test_athena/test_athena.py
+++ b/tests/test_athena/test_athena.py
@@ -1,3 +1,5 @@
+from datetime import datetime
+
 import boto3
 import pytest
 from botocore.exceptions import ClientError
@@ -172,7 +174,7 @@ def test_get_query_execution(location):
     # Start Query
     exex_id = client.start_query_execution(
         QueryString=query,
-        QueryExecutionContext={"Database": database},
+        QueryExecutionContext={"Database": database, "Catalog": "awsdatacatalog"},
         ResultConfiguration={"OutputLocation": location},
     )["QueryExecutionId"]
     #
@@ -186,15 +188,24 @@ def test_get_query_execution(location):
         assert result_config["OutputLocation"] == f"{location}{exex_id}.csv"
     else:
         assert result_config["OutputLocation"] == f"{location}/{exex_id}.csv"
+    assert (
+        details["ResultReuseConfiguration"]["ResultReuseByAgeConfiguration"]["Enabled"]
+        is False
+    )
     assert details["QueryExecutionContext"]["Database"] == database
+    assert details["QueryExecutionContext"]["Catalog"] == "awsdatacatalog"
     assert details["Status"]["State"] == "SUCCEEDED"
+    assert isinstance(details["Status"]["SubmissionDateTime"], datetime)
+    assert isinstance(details["Status"]["CompletionDateTime"], datetime)
     assert details["Statistics"] == {
         "EngineExecutionTimeInMillis": 0,
         "DataScannedInBytes": 0,
         "TotalExecutionTimeInMillis": 0,
         "QueryQueueTimeInMillis": 0,
+        "ServicePreProcessingTimeInMillis": 0,
         "QueryPlanningTimeInMillis": 0,
         "ServiceProcessingTimeInMillis": 0,
+        "ResultReuseInformation": {"ReusedPreviousResult": False},
     }
     assert "WorkGroup" not in details
 
diff --git a/tests/test_ram/test_ram.py b/tests/test_ram/test_ram.py
index da782ad8d..5a3b96c9d 100644
--- a/tests/test_ram/test_ram.py
+++ b/tests/test_ram/test_ram.py
@@ -8,6 +8,7 @@ from botocore.exceptions import ClientError
 
 from moto import mock_aws
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
+from moto.ram.models import ResourceAccessManagerBackend
 
 
 @mock_aws
@@ -530,3 +531,52 @@ def test_get_resource_share_associations_errors():
         "You cannot specify a principal when the association type is RESOURCE"
         in ex.response["Error"]["Message"]
     )
+
+
+@mock_aws
+@pytest.mark.parametrize(
+    "resource_region_scope, expect_error, error_message",
+    [
+        ({}, False, None),  # default value is "ALL"
+        ({"resourceRegionScope": "ALL"}, False, None),
+        ({"resourceRegionScope": "GLOBAL"}, False, None),
+        ({"resourceRegionScope": "REGIONAL"}, False, None),
+        (
+            {"resourceRegionScope": "INVALID"},
+            True,
+            "INVALID is not a valid resource region scope value. Specify a valid value and try again.",
+        ),
+    ],
+    ids=[
+        "default_region_scope",
+        "all_region_scope",
+        "global_region_scope",
+        "regional_region_scope",
+        "invalid_region_scope",
+    ],
+)
+def test_list_resource_types(resource_region_scope, expect_error, error_message):
+    client = boto3.client("ram", region_name="us-east-1")
+    region_scope = resource_region_scope.get("resourceRegionScope")
+
+    if expect_error:
+        with pytest.raises(ClientError) as e:
+            client.list_resource_types(**resource_region_scope)
+        ex = e.value
+        assert ex.response["ResponseMetadata"]["HTTPStatusCode"] == 400
+        assert "InvalidParameterException" in ex.response["Error"]["Code"]
+        assert ex.response["Error"]["Message"] == error_message
+    else:
+        response = client.list_resource_types(**resource_region_scope)
+        expected_types = ResourceAccessManagerBackend.RESOURCE_TYPES
+        if region_scope == "GLOBAL":
+            expected_types = [
+                rt for rt in expected_types if rt["resourceRegionScope"] == "GLOBAL"
+            ]
+        elif region_scope == "REGIONAL":
+            expected_types = [
+                rt for rt in expected_types if rt["resourceRegionScope"] == "REGIONAL"
+            ]
+
+        assert "resourceTypes" in response
+        assert response["resourceTypes"] == expected_types
diff --git a/tests/test_rds/test_rds_clusters.py b/tests/test_rds/test_rds_clusters.py
index ec857c7c1..7fc5b9a57 100644
--- a/tests/test_rds/test_rds_clusters.py
+++ b/tests/test_rds/test_rds_clusters.py
@@ -2006,3 +2006,17 @@ def test_db_cluster_identifier_is_case_insensitive(client):
 
     response = client.delete_db_cluster(DBClusterIdentifier="xXyY")
     assert response["DBCluster"]["DBClusterIdentifier"] == "xxyy"
+
+
+@mock_aws
+def test_db_cluster_attributes(client):
+    cluster = client.create_db_cluster(
+        DBClusterIdentifier="test-cluster",
+        Engine="aurora-postgresql",
+        MasterUsername="root",
+        MasterUserPassword="password",
+        PubliclyAccessible=True,
+    )["DBCluster"]
+    assert cluster["DBClusterIdentifier"] == "test-cluster"
+    assert cluster["Engine"] == "aurora-postgresql"
+    assert cluster["PubliclyAccessible"] is True