Repository: aws-cloudformation/cfn-lint

Issue:
E3025 RDS Instance Type db.t3 Support
*cfn-lint version: (`0.18.1 w/ latest Spec files`)*

*Description of issue:*
When attempting to specify an RDS Instance Type of the new db.t3 instances cfn-lint throws a E3025 error stating that a db.t3 is incompatible with engine type mysql in region us-east-1.
Looking at [RdsProperties.json](../blob/master/src/cfnlint/data/AdditionalSpecs/RdsProperties.json) it appears that the new db.t3 instance types have not been added yet.
Looking at the reference page [Amazon RDS for MySQL Pricing](https://aws.amazon.com/rds/mysql/pricing/) the db.t3 instances are supported when using mysql as the engine type.

*Example Code:*
```yaml
AppDB
  Type: AWS::RDS::DBInstance
  Properties:
    DBInstanceClass: db.t3.large
    Engine: mysql
```


Discussion:
looking into it.  

Resolving patch:
diff --git a/scripts/update_specs_from_pricing.py b/scripts/update_specs_from_pricing.py
--- a/scripts/update_specs_from_pricing.py
+++ b/scripts/update_specs_from_pricing.py
@@ -191,6 +191,34 @@ def get_rds_pricing():
         FormatVersion='aws_v1',
     )
 
+    product_map = {
+        '2': ['mysql'],
+        '3': ['oracle-se1'],
+        '4': ['oracle-se'],
+        '5': ['oracle-ee'],
+        '6': ['oracle-se1'],
+        '8': ['sqlserver-se'],
+        '9': ['sqlserver-ee'],
+        '10': ['sqlserver-ex'],
+        '11': ['sqlserver-web'],
+        '12': ['sqlserver-se'],
+        '14': ['postgres'],
+        '15': ['sqlserver-ee'],
+        '16': ['aurora-mysql', 'aurora'],
+        '18': ['mariadb'],
+        '19': ['oracle-se2'],
+        '20': ['oracle-se2'],
+        '21': ['aurora-postgresql'],
+    }
+
+    license_map = {
+        'License included': 'license-included',
+        'Bring your own license': 'bring-your-own-license',
+        'No license required': 'general-public-license'
+    }
+
+    rds_specs = {}
+
     results = {}
     for page in page_iterator:
         for price_item in page.get('PriceList', []):
@@ -198,11 +226,36 @@ def get_rds_pricing():
             product = products.get('product', {})
             if product:
                 if product.get('productFamily') == 'Database Instance':
+                    # Get overall instance types
                     if not results.get(region_map[product.get('attributes').get('location')]):
                         results[region_map[product.get('attributes').get('location')]] = set()
                     results[region_map[product.get('attributes').get('location')]].add(
                         product.get('attributes').get('instanceType')
                     )
+                    # Rds Instance Size spec
+                    product_names = product_map.get(product.get('attributes').get('engineCode'), [])
+                    product_region = region_map.get(product.get('attributes').get('location'))
+                    license_name = license_map.get(product.get('attributes').get('licenseModel'))
+                    instance_type = product.get('attributes').get('instanceType')
+                    for product_name in product_names:
+                        if not rds_specs.get(license_name):
+                            rds_specs[license_name] = {}
+                        if not rds_specs.get(license_name).get(product_name):
+                            rds_specs[license_name][product_name] = {}
+                        if not rds_specs.get(license_name).get(product_name).get(product_region):
+                            rds_specs[license_name][product_name][product_region] = set()
+
+                        rds_specs[license_name][product_name][product_region].add(instance_type)
+
+    for license_name, license_values in rds_specs.items():
+        for product_name, product_values in license_values.items():
+            for product_region, instance_types in product_values.items():
+                rds_specs[license_name][product_name][product_region] = list(sorted(instance_types))
+
+    LOGGER.info('Updating RDS Spec files')
+    filename = 'src/cfnlint/data/AdditionalSpecs/RdsProperties.json'
+    with open(filename, 'w+') as f:
+        json.dump(rds_specs, f, indent=2, sort_keys=True, separators=(',', ': '))
     return results
 
 def get_neptune_pricing():
diff --git a/src/cfnlint/rules/resources/rds/InstanceSize.py b/src/cfnlint/rules/resources/rds/InstanceSize.py
--- a/src/cfnlint/rules/resources/rds/InstanceSize.py
+++ b/src/cfnlint/rules/resources/rds/InstanceSize.py
@@ -31,24 +31,48 @@ class InstanceSize(CloudFormationLintRule):
 
     valid_instance_types = cfnlint.helpers.load_resources('data/AdditionalSpecs/RdsProperties.json')
 
+    def _get_license_model(self, engine, license_model):
+        """ Logic to get the correct license model"""
+        if not license_model:
+            if engine in self.valid_instance_types.get('license-included'):
+                license_model = 'license-included'
+            elif engine in self.valid_instance_types.get('bring-your-own-license'):
+                license_model = 'bring-your-own-license'
+            else:
+                license_model = 'general-public-license'
+            self.logger.debug('Based on Engine: %s we determined the default license will be %s', engine, license_model)
+
+        return license_model
+
     def get_resources(self, cfn):
         """ Get resources that can be checked """
         results = []
         for resource_name, resource_values in cfn.get_resources('AWS::RDS::DBInstance').items():
             path = ['Resources', resource_name, 'Properties']
             properties = resource_values.get('Properties')
+            # Properties items_safe heps remove conditions and focusing on the actual values and scenarios
             for prop_safe, prop_path_safe in properties.items_safe(path):
                 engine = prop_safe.get('Engine')
                 inst_class = prop_safe.get('DBInstanceClass')
                 license_model = prop_safe.get('LicenseModel')
+
+                # Need to get a default license model if none provided
+                # Also need to validate all these values are strings otherwise we cannot
+                # do validation
                 if isinstance(engine, six.string_types) and isinstance(inst_class, six.string_types):
-                    results.append(
-                        {
-                            'Engine': engine,
-                            'DBInstanceClass': inst_class,
-                            'Path': prop_path_safe,
-                            'LicenseModel': license_model
-                        })
+                    license_model = self._get_license_model(engine, license_model)
+                    if isinstance(license_model, six.string_types):
+                        results.append(
+                            {
+                                'Engine': engine,
+                                'DBInstanceClass': inst_class,
+                                'Path': prop_path_safe,
+                                'LicenseModel': license_model
+                            })
+                    else:
+                        self.logger.debug('Skip evaluation based on [LicenseModel] not being a string.')
+                else:
+                    self.logger.debug('Skip evaluation based on [Engine] or [DBInstanceClass] not being strings.')
 
         return results
 
@@ -56,31 +80,20 @@ def check_db_config(self, properties, region):
         """ Check db properties """
         matches = []
 
-        for valid_instance_type in self.valid_instance_types:
-            engine = properties.get('Engine')
-            if engine in valid_instance_type.get('engines'):
-                if region in valid_instance_type.get('regions'):
-                    db_license = properties.get('LicenseModel')
-                    valid_licenses = valid_instance_type.get('license')
-                    db_instance_class = properties.get('DBInstanceClass')
-                    valid_instance_types = valid_instance_type.get('instance_types')
-                    if db_license and valid_licenses:
-                        if db_license not in valid_licenses:
-                            self.logger.debug('Skip evaluation based on license not matching.')
-                            continue
-                    if db_instance_class not in valid_instance_types:
-                        if db_license is None:
-                            message = 'DBInstanceClass "{0}" is not compatible with engine type "{1}" in region "{2}". Use instance types [{3}]'
-                            matches.append(
-                                RuleMatch(
-                                    properties.get('Path') + ['DBInstanceClass'], message.format(
-                                        db_instance_class, engine, region, ', '.join(map(str, valid_instance_types)))))
-                        else:
-                            message = 'DBInstanceClass "{0}" is not compatible with engine type "{1}" and LicenseModel "{2}" in region "{3}". Use instance types [{4}]'
-                            matches.append(
-                                RuleMatch(
-                                    properties.get('Path') + ['DBInstanceClass'], message.format(
-                                        db_instance_class, engine, db_license, region, ', '.join(map(str, valid_instance_types)))))
+        db_engine = properties.get('Engine')
+        db_license = properties.get('LicenseModel')
+        db_instance_class = properties.get('DBInstanceClass')
+        if db_license in self.valid_instance_types:
+            if db_engine in self.valid_instance_types[db_license]:
+                if region in self.valid_instance_types[db_license][db_engine]:
+                    if db_instance_class not in self.valid_instance_types[db_license][db_engine][region]:
+                        message = 'DBInstanceClass "{0}" is not compatible with engine type "{1}" and LicenseModel "{2}" in region "{3}". Use instance types [{4}]'
+                        matches.append(
+                            RuleMatch(
+                                properties.get('Path') + ['DBInstanceClass'], message.format(
+                                    db_instance_class, db_engine, db_license, region, ', '.join(map(str, self.valid_instance_types[db_license][db_engine][region])))))
+        else:
+            self.logger.debug('Skip evaluation based on license [%s] not matching.', db_license)
         return matches
 
     def match(self, cfn):


Test patch:
diff --git a/test/fixtures/templates/bad/resources/rds/instance_sizes.yaml b/test/fixtures/templates/bad/resources/rds/instance_sizes.yaml
--- a/test/fixtures/templates/bad/resources/rds/instance_sizes.yaml
+++ b/test/fixtures/templates/bad/resources/rds/instance_sizes.yaml
@@ -1,5 +1,10 @@
 ---
 AWSTemplateFormatVersion: '2010-09-09'
+Parameters:
+  DatabaseEngine:
+    Type: String
+  License:
+    Type: String
 Resources:
   # Fails on invalid instance type
   DBInstance1:
@@ -27,3 +32,30 @@ Resources:
     Properties:
       DBInstanceClass: db.m4.xlarge
       Engine: mysql
+  # Doesn't fail when InstanceClass is wrong but the Engine can't be determined
+  DBInstance5:
+    Type: AWS::RDS::DBInstance
+    Properties:
+      DBInstanceClass: notanappropriatetype
+      Engine: !Ref DatabaseEngine
+  # Doesn't fail when InstanceClass is wrong but the LicenseModel can't be determined
+  DBInstance7:
+    Type: AWS::RDS::DBInstance
+    Properties:
+      DBInstanceClass: notanappropriatetype
+      Engine: sqlserver-se
+      LicenseModel: !Ref License
+  # Doesn't fail when LicenseModel isn't a valid value
+  DBInstance8:
+    Type: AWS::RDS::DBInstance
+    Properties:
+      DBInstanceClass: notanappropriatetype
+      Engine: sqlserver-se
+      LicenseModel: bad-license-model
+  # Doesn't fail when Egnine isn't a valid value
+  DBInstance9:
+    Type: AWS::RDS::DBInstance
+    Properties:
+      DBInstanceClass: notanappropriatetype
+      Engine: bad-ee
+      LicenseModel: license-included
diff --git a/test/fixtures/templates/good/resources/rds/instance_sizes.yaml b/test/fixtures/templates/good/resources/rds/instance_sizes.yaml
--- a/test/fixtures/templates/good/resources/rds/instance_sizes.yaml
+++ b/test/fixtures/templates/good/resources/rds/instance_sizes.yaml
@@ -32,3 +32,15 @@ Resources:
       DBInstanceClass: db.r4.large
       Engine: oracle-se2
       LicenseModel: license-included
+  # Doesnt fail on no license when default is license-included
+  DBInstance5:
+    Type: AWS::RDS::DBInstance
+    Properties:
+      DBInstanceClass: db.m1.small
+      Engine: sqlserver-ex
+  # Doesnt fail on no license when default is bring-your-own-license
+  DBInstance6:
+    Type: AWS::RDS::DBInstance
+    Properties:
+      DBInstanceClass: db.m1.small
+      Engine: oracle-ee