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

RDS: add cluster engine validation when creating a rds cluster
1. added Enum for valid cluster engines
    - Ref document: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/rds/client/create_db_cluster.html#:~:text=Valid%20Values%3A%20aurora%2Dmysql%20%7C%20aurora%2Dpostgresql%20%7C%20mysql%20%7C%20postgres
2. add cluster engine validation when creating a rds cluster
3. added unittest for the validation


Patch:
diff --git a/moto/ec2/models/vpcs.py b/moto/ec2/models/vpcs.py
index 73ae96e8b..f0da5bc27 100644
--- a/moto/ec2/models/vpcs.py
+++ b/moto/ec2/models/vpcs.py
@@ -43,7 +43,7 @@ class VPCEndPoint(TaggedEC2Resource, CloudFormationModel):
 
     DEFAULT_POLICY = {
         "Version": "2008-10-17",
-        "Statement ": [
+        "Statement": [
             {"Effect": "Allow", "Principal": "*", "Action": "*", "Resource": "*"}
         ],
     }
diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py
index 4bded43bb..9af62bab8 100644
--- a/moto/ec2/responses/instances.py
+++ b/moto/ec2/responses/instances.py
@@ -823,16 +823,94 @@ EC2_INSTANCE_STATUS = """<?xml version="1.0" encoding="UTF-8"?>
 </DescribeInstanceStatusResponse>"""
 
 EC2_DESCRIBE_INSTANCE_TYPES = """<?xml version="1.0" encoding="UTF-8"?>
-<DescribeInstanceTypesResponse xmlns="http://api.outscale.com/wsdl/fcuext/2014-04-15/">
+<DescribeInstanceTypesResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
     <requestId>f8b86168-d034-4e65-b48d-3b84c78e64af</requestId>
     <instanceTypeSet>
     {% for instance_type in instance_types %}
         <item>
+            <autoRecoverySupported>{{ instance_type.AutoRecoverySupported }}</autoRecoverySupported>
+            <bareMetal>{{ instance_type.BareMetal }}</bareMetal>
+            <burstablePerformanceSupported>{{ instance_type.BurstablePerformanceSupported }}</burstablePerformanceSupported>
+            <currentGeneration>{{ instance_type.CurrentGeneration }}</currentGeneration>
+            <dedicatedHostsSupported>{{ instance_type.DedicatedHostsSupported }}</dedicatedHostsSupported>
+            <ebsInfo>
+                <ebsOptimizedInfo>
+                    <baselineBandwidthInMbps>{{ instance_type.get('EbsInfo', {}).get('EbsOptimizedInfo', {}).get('BaselineBandwidthInMbps', 0) | int }}</baselineBandwidthInMbps>
+                    <baselineIops>{{  instance_type.get('EbsInfo', {}).get('EbsOptimizedInfo', {}).get('BaselineIops', 0) | int }}</baselineIops>
+                    <baselineThroughputInMBps>{{  instance_type.get('EbsInfo', {}).get('EbsOptimizedInfo', {}).get('BaselineThroughputInMBps', 0.0) | float }}</baselineThroughputInMBps>
+                    <maximumBandwidthInMbps>{{  instance_type.get('EbsInfo', {}).get('EbsOptimizedInfo', {}).get('MaximumBandwidthInMbps', 0) | int }}</maximumBandwidthInMbps>
+                    <maximumIops>{{  instance_type.get('EbsInfo', {}).get('EbsOptimizedInfo', {}).get('MaximumIops', 0) | int }}</maximumIops>
+                    <maximumThroughputInMBps>{{ instance_type.get('EbsInfo', {}).get('EbsOptimizedInfo', {}).get('MaximumThroughputInMBps', 0.0) | float }}</maximumThroughputInMBps>
+                </ebsOptimizedInfo>
+                <ebsOptimizedSupport>{{ instance_type.get('EbsInfo', {}).get('EbsOptimizedSupport', 'default') }}</ebsOptimizedSupport>
+                <encryptionSupport>{{ instance_type.get('EbsInfo', {}).get('EncryptionSupport', 'supported') }}</encryptionSupport>
+                <nvmeSupport>{{ instance_type.get('EbsInfo', {}).get('NvmeSupport', 'required') }}</nvmeSupport>
+            </ebsInfo>
+            <networkInfo>
+                <defaultNetworkCardIndex>{{ instance_type.get('NetworkInfo', {}).get('DefaultNetworkCardIndex', 0) | int }}</defaultNetworkCardIndex>
+                <efaSupported>{{ instance_type.get('NetworkInfo', {}).get('EfaSupported', False) }}</efaSupported>
+                <enaSrdSupported>{{ instance_type.get('NetworkInfo', {}).get('EnaSrdSupported', False) }}</enaSrdSupported>
+                <enaSupport>{{ instance_type.get('NetworkInfo', {}).get('EnaSupported', False) }}</enaSupport>
+                <encryptionInTransitSupported>{{ instance_type.get('NetworkInfo', {}).get('EncryptionInTransitSupported', False) }}</encryptionInTransitSupported>
+                <ipv4AddressesPerInterface>{{ instance_type.get('NetworkInfo', {}).get('Ipv4AddressesPerInterface', 0) | int }}</ipv4AddressesPerInterface>
+                <ipv6AddressesPerInterface>{{ instance_type.get('NetworkInfo', {}).get('Ipv6AddressesPerInterface', 0) | int }}</ipv6AddressesPerInterface>
+                <ipv6Supported>{{ instance_type.get('NetworkInfo', {}).get('Ipv6Supported', False) }}</ipv6Supported>
+                <maximumNetworkCards>{{ instance_type.get('NetworkInfo', {}).get('MaximumNetworkCards', 0) | int }}</maximumNetworkCards>
+                <maximumNetworkInterfaces>{{ instance_type.get('NetworkInfo', {}).get('MaximumNetworkInterfaces', 0) | int }}</maximumNetworkInterfaces>
+                <networkCards>
+                  {% for network_card in instance_type.get('NetworkInfo', {}).get('NetworkCards', []) %}
+                    <item>
+                        <baselineBandwidthInGbps>{{ network_card.get('BaselineBandwidthInGbps', 0.0) | float }}</baselineBandwidthInGbps>
+                        <maximumNetworkInterfaces>{{ network_card.get('MaximumNetworkInterfaces', 0) | int }}</maximumNetworkInterfaces>
+                        <networkCardIndex>{{ network_card.get('NetworkCardIndex', 0) | int }}</networkCardIndex>
+                        <networkPerformance>{{ network_card.get('NetworkPerformance', 'Up to 25 Schmeckles') }}</networkPerformance>
+                        <peakBandwidthInGbps>{{ network_card.get('PeakBandwidthInGbps', 0.0) | float }}</peakBandwidthInGbps>
+                    </item>
+                  {% endfor %}
+                </networkCards>
+                <networkPerformance>{{ instance_type.get('NetworkInfo', {}).get('NetworkPerformance', 'Up to 25 Schmeckles') }}</networkPerformance>
+            </networkInfo>
+            <freeTierEligible>{{ instance_type.FreeTierEligible }}</freeTierEligible>
+            <hibernationSupported>{{ instance_type.HibernationSupported }}</hibernationSupported>
+            <hypervisor>{{ instance_type.get('Hypervisor', 'motovisor') }}</hypervisor>
+            <instanceStorageSupported>{{ instance_type.InstanceStorageSupported }}</instanceStorageSupported>
+            <placementGroupInfo>
+                <supportedStrategies>
+                  {% for strategy in instance_type.get('PlacementGroupInfo', {}).get('SupportedStrategies', []) %}
+                    <item>{{ strategy }}</item>
+                  {% endfor %}
+                </supportedStrategies>
+            </placementGroupInfo>
+            <supportedRootDeviceTypes>
+              {% for dev_type in instance_type.get('SupportedRootDeviceTypes', []) %}
+                <item>{{ dev_type }}</item>
+              {% endfor %}
+            </supportedRootDeviceTypes>
+            <supportedUsageClasses>
+              {% for usage_class in instance_type.get('SupportedUsageClasses', []) %}
+                <item>{{ usage_class }}</item>
+              {% endfor %}
+            </supportedUsageClasses>
+            <supportedVirtualizationTypes>
+              {% for supported_vtype in instance_type.get('SupportedVirtualizationTypes', []) %}
+                <item>{{ supported_vtype }}</item>
+              {% endfor %}
+            </supportedVirtualizationTypes>
             <instanceType>{{ instance_type.InstanceType }}</instanceType>
             <vCpuInfo>
                 <defaultVCpus>{{ instance_type.get('VCpuInfo', {}).get('DefaultVCpus', 0)|int }}</defaultVCpus>
                 <defaultCores>{{ instance_type.get('VCpuInfo', {}).get('DefaultCores', 0)|int }}</defaultCores>
                 <defaultThreadsPerCore>{{ instance_type.get('VCpuInfo').get('DefaultThreadsPerCore', 0)|int }}</defaultThreadsPerCore>
+                <validCores>
+                  {% for valid_core in instance_type.get("VCpuInfo", {}).get('ValidCores', []) %}
+                    <item>{{ valid_core }}</item>
+                  {% endfor %}
+                </validCores>
+                <validThreadsPerCore>
+                  {% for threads_per_core in instance_type.get("VCpuInfo", {}).get('ValidThreadsPerCore', []) %}
+                    <item>{{ threads_per_core }}</item>
+                  {% endfor %}
+                </validThreadsPerCore>
             </vCpuInfo>
             <memoryInfo>
                 <sizeInMiB>{{ instance_type.get('MemoryInfo', {}).get('SizeInMiB', 0)|int }}</sizeInMiB>
@@ -848,6 +926,7 @@ EC2_DESCRIBE_INSTANCE_TYPES = """<?xml version="1.0" encoding="UTF-8"?>
                     </item>
                     {% endfor %}
                 </supportedArchitectures>
+                <sustainedClockSpeedInGhz>{{ instance_type.get('ProcessorInfo', {}).get('SustainedClockSpeedInGhz', 0.0) | float }}</sustainedClockSpeedInGhz>
             </processorInfo>
             {% if instance_type.get('GpuInfo', {})|length > 0 %}
             <gpuInfo>
diff --git a/moto/rds/models.py b/moto/rds/models.py
index db0042cac..899ff6456 100644
--- a/moto/rds/models.py
+++ b/moto/rds/models.py
@@ -49,6 +49,7 @@ from .utils import (
     merge_filters,
     validate_filters,
     valid_preferred_maintenance_window,
+    ClusterEngine,
 )
 
 
@@ -129,6 +130,17 @@ class Cluster:
         self.db_cluster_instance_class = kwargs.get("db_cluster_instance_class")
         self.deletion_protection = kwargs.get("deletion_protection")
         self.engine = kwargs.get("engine")
+        if self.engine not in ClusterEngine.list_cluster_engines():
+            raise InvalidParameterValue(
+                (
+                    "Engine '{engine}' is not supported "
+                    "to satisfy constraint: Member must satisfy enum value set: "
+                    "{valid_engines}"
+                ).format(
+                    engine=self.engine,
+                    valid_engines=ClusterEngine.list_cluster_engines(),
+                )
+            )
         self.engine_version = kwargs.get("engine_version") or Cluster.default_engine_version(self.engine)  # type: ignore
         self.engine_mode = kwargs.get("engine_mode") or "provisioned"
         self.iops = kwargs.get("iops")
@@ -1591,7 +1603,10 @@ class RDSBackend(BaseBackend):
         if cluster_id is not None:
             cluster = self.clusters.get(cluster_id)
             if cluster is not None:
-                if cluster.engine == "aurora" and cluster.engine_mode == "serverless":
+                if (
+                    cluster.engine in ClusterEngine.serverless_engines()
+                    and cluster.engine_mode == "serverless"
+                ):
                     raise InvalidParameterValue(
                         "Instances cannot be added to Aurora Serverless clusters."
                     )
diff --git a/moto/rds/utils.py b/moto/rds/utils.py
index 75038d2e5..6c26c96d1 100644
--- a/moto/rds/utils.py
+++ b/moto/rds/utils.py
@@ -1,6 +1,7 @@
 import copy
 from collections import namedtuple
-from typing import Any, Dict, Tuple, Optional
+from typing import Any, Dict, Tuple, Optional, List
+from enum import Enum
 
 from botocore.utils import merge_dicts
 
@@ -22,6 +23,21 @@ FilterDef = namedtuple(
 )
 
 
+class ClusterEngine(str, Enum):
+    AURORA_POSTGRESQL = "aurora-postgresql"
+    AURORA_MYSQL = "aurora-mysql"
+    RDS_POSTGRESQL = "postgres"
+    RDS_MYSQL = "mysql"
+
+    @classmethod
+    def list_cluster_engines(self) -> List[str]:
+        return sorted([item.value for item in ClusterEngine])
+
+    @classmethod
+    def serverless_engines(self) -> List[str]:
+        return [ClusterEngine.AURORA_MYSQL, ClusterEngine.AURORA_POSTGRESQL]
+
+
 def get_object_value(obj: Any, attr: str) -> Any:
     """Retrieves an arbitrary attribute value from an object.
 


Test patch:
diff --git a/tests/test_ec2/test_vpcs.py b/tests/test_ec2/test_vpcs.py
index 0849359f3..bb6c320a8 100644
--- a/tests/test_ec2/test_vpcs.py
+++ b/tests/test_ec2/test_vpcs.py
@@ -993,7 +993,7 @@ def test_create_vpc_endpoint__policy():
     # create without policy --> verify the default policy is created
     default_policy = {
         "Version": "2008-10-17",
-        "Statement ": [
+        "Statement": [
             {"Effect": "Allow", "Principal": "*", "Action": "*", "Resource": "*"}
         ],
     }
diff --git a/tests/test_rds/test_rds_clusters.py b/tests/test_rds/test_rds_clusters.py
index 2c261d498..b099e0f6b 100644
--- a/tests/test_rds/test_rds_clusters.py
+++ b/tests/test_rds/test_rds_clusters.py
@@ -31,12 +31,29 @@ def test_describe_db_cluster_fails_for_non_existent_cluster():
     assert err["Message"] == "DBCluster cluster-id not found."
 
 
+@mock_rds
+def test_create_db_cluster_invalid_engine():
+    client = boto3.client("rds", region_name=RDS_REGION)
+
+    with pytest.raises(ClientError) as ex:
+        client.create_db_cluster(
+            DBClusterIdentifier="cluster-id", Engine="aurora-postgresql"
+        )
+    err = ex.value.response["Error"]
+    assert err["Code"] == "InvalidParameterValue"
+    assert err["Message"] == (
+        "The parameter MasterUsername must be provided and must not be blank."
+    )
+
+
 @mock_rds
 def test_create_db_cluster_needs_master_username():
     client = boto3.client("rds", region_name=RDS_REGION)
 
     with pytest.raises(ClientError) as ex:
-        client.create_db_cluster(DBClusterIdentifier="cluster-id", Engine="aurora")
+        client.create_db_cluster(
+            DBClusterIdentifier="cluster-id", Engine="aurora-postgresql"
+        )
     err = ex.value.response["Error"]
     assert err["Code"] == "InvalidParameterValue"
     assert err["Message"] == (
@@ -50,7 +67,9 @@ def test_create_db_cluster_needs_master_user_password():
 
     with pytest.raises(ClientError) as ex:
         client.create_db_cluster(
-            DBClusterIdentifier="cluster-id", Engine="aurora", MasterUsername="root"
+            DBClusterIdentifier="cluster-id",
+            Engine="aurora-postgresql",
+            MasterUsername="root",
         )
     err = ex.value.response["Error"]
     assert err["Code"] == "InvalidParameterValue"
@@ -66,7 +85,7 @@ def test_create_db_cluster_needs_long_master_user_password():
     with pytest.raises(ClientError) as ex:
         client.create_db_cluster(
             DBClusterIdentifier="cluster-id",
-            Engine="aurora",
+            Engine="aurora-postgresql",
             MasterUsername="root",
             MasterUserPassword="hunter2",
         )
@@ -84,7 +103,7 @@ def test_modify_db_cluster_needs_long_master_user_password():
 
     client.create_db_cluster(
         DBClusterIdentifier="cluster-id",
-        Engine="aurora",
+        Engine="aurora-postgresql",
         MasterUsername="root",
         MasterUserPassword="hunter21",
     )
@@ -110,7 +129,7 @@ def test_modify_db_cluster_new_cluster_identifier():
 
     client.create_db_cluster(
         DBClusterIdentifier=old_id,
-        Engine="aurora",
+        Engine="aurora-postgresql",
         MasterUsername="root",
         MasterUserPassword="hunter21",
     )
@@ -137,7 +156,7 @@ def test_create_db_cluster__verify_default_properties():
 
     resp = client.create_db_cluster(
         DBClusterIdentifier="cluster-id",
-        Engine="aurora",
+        Engine="aurora-mysql",
         MasterUsername="root",
         MasterUserPassword="hunter2_",
     )
@@ -169,8 +188,8 @@ def test_create_db_cluster__verify_default_properties():
     )
     assert cluster["ReaderEndpoint"] == expected_readonly
     assert cluster["MultiAZ"] is False
-    assert cluster["Engine"] == "aurora"
-    assert cluster["EngineVersion"] == "5.6.mysql_aurora.1.22.5"
+    assert cluster["Engine"] == "aurora-mysql"
+    assert cluster["EngineVersion"] == "5.7.mysql_aurora.2.07.2"
     assert cluster["Port"] == 3306
     assert cluster["MasterUsername"] == "root"
     assert cluster["PreferredBackupWindow"] == "01:37-02:07"
@@ -206,7 +225,7 @@ def test_create_db_cluster_additional_parameters():
         AvailabilityZones=["eu-north-1b"],
         DatabaseName="users",
         DBClusterIdentifier="cluster-id",
-        Engine="aurora",
+        Engine="aurora-postgresql",
         EngineVersion="8.0.mysql_aurora.3.01.0",
         EngineMode="serverless",
         MasterUsername="root",
@@ -232,7 +251,7 @@ def test_create_db_cluster_additional_parameters():
 
     assert cluster["AvailabilityZones"] == ["eu-north-1b"]
     assert cluster["DatabaseName"] == "users"
-    assert cluster["Engine"] == "aurora"
+    assert cluster["Engine"] == "aurora-postgresql"
     assert cluster["EngineVersion"] == "8.0.mysql_aurora.3.01.0"
     assert cluster["EngineMode"] == "serverless"
     assert cluster["Port"] == 1234
@@ -259,14 +278,14 @@ def test_describe_db_cluster_after_creation():
 
     client.create_db_cluster(
         DBClusterIdentifier="cluster-id1",
-        Engine="aurora",
+        Engine="aurora-postgresql",
         MasterUsername="root",
         MasterUserPassword="hunter2_",
     )
 
     cluster_arn = client.create_db_cluster(
         DBClusterIdentifier="cluster-id2",
-        Engine="aurora",
+        Engine="aurora-postgresql",
         MasterUsername="root",
         MasterUserPassword="hunter2_",
     )["DBCluster"]["DBClusterArn"]
@@ -292,7 +311,7 @@ def test_delete_db_cluster():
 
     client.create_db_cluster(
         DBClusterIdentifier="cluster-id",
-        Engine="aurora",
+        Engine="aurora-postgresql",
         MasterUsername="root",
         MasterUserPassword="hunter2_",
     )
@@ -308,7 +327,7 @@ def test_delete_db_cluster_do_snapshot():
 
     client.create_db_cluster(
         DBClusterIdentifier="cluster-id",
-        Engine="aurora",
+        Engine="aurora-postgresql",
         MasterUsername="root",
         MasterUserPassword="hunter2_",
     )
@@ -329,7 +348,7 @@ def test_delete_db_cluster_that_is_protected():
 
     client.create_db_cluster(
         DBClusterIdentifier="cluster-id",
-        Engine="aurora",
+        Engine="aurora-postgresql",
         MasterUsername="root",
         MasterUserPassword="hunter2_",
         DeletionProtection=True,
@@ -369,7 +388,7 @@ def test_start_db_cluster_after_stopping():
 
     client.create_db_cluster(
         DBClusterIdentifier="cluster-id",
-        Engine="aurora",
+        Engine="aurora-postgresql",
         MasterUsername="root",
         MasterUserPassword="hunter2_",
     )
@@ -386,7 +405,7 @@ def test_start_db_cluster_without_stopping():
 
     client.create_db_cluster(
         DBClusterIdentifier="cluster-id",
-        Engine="aurora",
+        Engine="aurora-postgresql",
         MasterUsername="root",
         MasterUserPassword="hunter2_",
     )
@@ -404,7 +423,7 @@ def test_stop_db_cluster():
 
     client.create_db_cluster(
         DBClusterIdentifier="cluster-id",
-        Engine="aurora",
+        Engine="aurora-postgresql",
         MasterUsername="root",
         MasterUserPassword="hunter2_",
     )
@@ -425,7 +444,7 @@ def test_stop_db_cluster_already_stopped():
 
     client.create_db_cluster(
         DBClusterIdentifier="cluster-id",
-        Engine="aurora",
+        Engine="aurora-postgresql",
         MasterUsername="root",
         MasterUserPassword="hunter2_",
     )
@@ -838,7 +857,7 @@ def test_create_db_cluster_with_enable_http_endpoint_invalid():
     resp = client.create_db_cluster(
         DBClusterIdentifier="cluster-id",
         DatabaseName="users",
-        Engine="aurora-mysql",
+        Engine="aurora-postgresql",
         EngineMode="serverless",
         EngineVersion="5.7.0",
         MasterUsername="root",
diff --git a/tests/test_rds/test_rds_clusters_with_instances.py b/tests/test_rds/test_rds_clusters_with_instances.py
index 66caeed7f..c7352bad1 100644
--- a/tests/test_rds/test_rds_clusters_with_instances.py
+++ b/tests/test_rds/test_rds_clusters_with_instances.py
@@ -74,7 +74,7 @@ def test_add_instance_to_serverless_cluster():
 
     _ = client.create_db_cluster(
         DBClusterIdentifier="dbci",
-        Engine="aurora",
+        Engine="aurora-postgresql",
         EngineMode="serverless",
         MasterUsername="masterusername",
         MasterUserPassword="hunter2_",