Repository: aws-cloudformation/cfn-lint

Issue:
W1001 incorrectly checking logic
`cfn-lint 0.13.0`

I'm receiving the following error:
```
W1001 Ref to resource "NatA" that many not be available when when condition "DeployNatGateway" is True and when condition "Az1Nat" is False at Resources/PrivateDefaultRouteA/Properties/NatGatewayId/Ref
```

W1001 is attempting to understand the logic of my Conditions but it's getting it wrong. For example, I have a VPC template that creates a NAT with the option of making it a single NAT or an HA NAT. So I have a parameter options that are 
```
["None", "Single NAT", "High Availability"]
```

I then have the following conditions:
```
t.add_condition(
    "DeployNatGateway",
    Not(Equals(Ref(NatType), "None"))
)

t.add_condition(
    "Az1Nat",
    Or( 
        Equals(Ref(NatType), "Single NAT"),
        Equals(Ref(NatType), "High Availability")
    )
)
```
I then have `NatA` being deployed with the condition `Az1Nat`. There is another resource, `PrivateDefaultRouteA` that has a `Condition` on `DeployNatGateway` and a `Ref` on `NatA`. It appears that since `PrivateDefaultRouteA` implicitly `DependsOn` `NatA` but are using different conditions, it thinks that they can conflict when they're actually two conditions based on the same parameter but we're using mutually exclusive options.

I'm not sure if this is a bug that should be fixed or if this is an example of an edge case that it's not going to handle. In the meantime, I would advocate that this is a good example to implement #113 because we don't want to completely disable W1001 as it did catch some logic errors that are valid.



Discussion:
I will work on a patch for this. 

I have one other feature I want to work on and then I will take a look at #113.

Resolving patch:
diff --git a/src/cfnlint/conditions.py b/src/cfnlint/conditions.py
--- a/src/cfnlint/conditions.py
+++ b/src/cfnlint/conditions.py
@@ -192,14 +192,23 @@ class Conditions(object):
     """ All the conditions """
     Conditions = None
     Equals = None
+    Parameters = None
 
     def __init__(self, cfn):
         self.Conditions = {}
         self.Equals = {}
+        self.Parameters = {}
         try:
             self.Equals = self._get_condition_equals(cfn.search_deep_keys(cfnlint.helpers.FUNCTION_EQUALS))
             for condition_name in cfn.template.get('Conditions', {}):
                 self.Conditions[condition_name] = Condition(cfn.template, condition_name)
+            # Configure parametrs Allowed Values if they have them
+            for parameter_name, parameter_values in cfn.template.get('Parameters', {}).items():
+                # ALlowed Values must be a list so validate they are
+                if isinstance(parameter_values.get('AllowedValues'), list):
+                    # Any parameter in a condition could be used but would have to be done by
+                    # Ref so build a ref to match for getting an equivalent hash
+                    self.Parameters[get_hash({'Ref': parameter_name})] = parameter_values.get('AllowedValues')
         except Exception as err:  # pylint: disable=W0703
             LOGGER.debug('While processing conditions got error: %s', err)
 
@@ -306,28 +315,50 @@ def get_scenarios(self, conditions):
                 for s_v in equal_values:
                     matched_equals[equal_key].add(s_v)
 
-        def multiply_equals(currents, s_hash, sets):
+        def multiply_equals(currents, s_hash, sets, parameter_values):
             """  Multiply Equals when building scenarios """
             results = []
             false_case = ''
             if not currents:
-                for s_set in sets:
+                # If the Parameter being REFed has Allowed Values use those instead
+                if parameter_values:
+                    for p_value in parameter_values:
+                        # the allowed value must be an integer or string
+                        # protecting against really badlyl formatted templates
+                        if isinstance(p_value, (six.integer_types, six.string_types)):
+                            new = {}
+                            # the allowed values could be numbers so force a string
+                            new[s_hash] = str(p_value)
+                            results.append(new)
+                else:
+                    for s_set in sets:
+                        new = {}
+                        new[s_hash] = s_set
+                        false_case += s_set
+                        results.append(new)
                     new = {}
-                    new[s_hash] = s_set
-                    false_case += s_set
+                    new[s_hash] = false_case + '.bad'
                     results.append(new)
-                new = {}
-                new[s_hash] = false_case + '.bad'
-                results.append(new)
             for current in currents:
-                for s_set in sets:
+                # If the Parameter being REFed has Allowed Values use those instead
+                if parameter_values:
+                    for p_value in parameter_values:
+                        # the allowed value must be an integer or string
+                        # protecting against really badlyl formatted templates
+                        if isinstance(p_value, (six.integer_types, six.string_types)):
+                            new = copy(current)
+                            # the allowed values could be numbers so force a string
+                            new[s_hash] = str(p_value)
+                            results.append(new)
+                else:
+                    for s_set in sets:
+                        new = copy(current)
+                        new[s_hash] = s_set
+                        false_case += s_set
+                        results.append(new)
                     new = copy(current)
-                    new[s_hash] = s_set
-                    false_case += s_set
+                    new[s_hash] = false_case + '.bad'
                     results.append(new)
-                new = copy(current)
-                new[s_hash] = false_case + '.bad'
-                results.append(new)
 
             return results
 
@@ -338,9 +369,9 @@ def multiply_equals(currents, s_hash, sets):
             return results
 
         if matched_conditions:
-            scenarios = set()
+            scenarios = []
             for con_hash, sets in matched_equals.items():
-                scenarios = multiply_equals(scenarios, con_hash, sets)
+                scenarios = multiply_equals(scenarios, con_hash, sets, self.Parameters.get(con_hash))
 
         for scenario in scenarios:
             r_condition = {}


Test patch:
diff --git a/test/module/conditions/test_conditions.py b/test/module/conditions/test_conditions.py
--- a/test/module/conditions/test_conditions.py
+++ b/test/module/conditions/test_conditions.py
@@ -24,6 +24,16 @@ def setUp(self):
         """ setup the cfn object """
         filename = 'test/fixtures/templates/good/core/conditions.yaml'
         template = {
+            'Parameters': {
+                'NatType': {
+                    'Type': 'String',
+                    'AllowedValues': ['None', 'Single NAT', 'High Availability']
+                },
+                'myEnvironment': {
+                    'Type': 'String',
+                    'AllowedValues': ['Dev', 'Stage', 'Prod']
+                }
+            },
             'Conditions': {
                 'isProduction': {'Fn::Equals': [{'Ref': 'myEnvironment'}, 'Prod']},
                 'isPrimary': {'Fn::Equals': ['True', {'Fn::FindInMap': ['location', {'Ref': 'AWS::Region'}, 'primary']}]},
@@ -31,7 +41,14 @@ def setUp(self):
                 'isProductionOrStaging': {'Fn::Or': [{'Condition': 'isProduction'}, {'Fn::Equals': [{'Ref': 'myEnvironment'}, 'Stage']}]},
                 'isNotProduction': {'Fn::Not': [{'Condition': 'isProduction'}]},
                 'isDevelopment': {'Fn::Equals': ['Dev', {'Ref': 'myEnvironment'}]},
-                'isPrimaryAndProdOrStage': {'Fn::And': [{'Condition': 'isProductionOrStaging'}, {'Fn::Equals': ['True', {'Fn::FindInMap': ['location', {'Ref': 'AWS::Region'}, 'primary']}]}]}
+                'isPrimaryAndProdOrStage': {'Fn::And': [{'Condition': 'isProductionOrStaging'}, {'Fn::Equals': ['True', {'Fn::FindInMap': ['location', {'Ref': 'AWS::Region'}, 'primary']}]}]},
+                'DeployNatGateway': {'Fn::Not': [{'Fn::Equals': [{'Ref': 'NatType'}, 'None']}]},
+                'Az1Nat': {
+                    'Fn::Or': [
+                        {'Fn::Equals': [{'Ref': 'NatType'}, 'Single NAT']},
+                        {'Fn::Equals': [{'Ref': 'NatType'}, 'High Availability']}
+                    ]
+                }
             },
             'Resources': {}
         }
@@ -40,7 +57,10 @@ def setUp(self):
 
     def test_success_size_of_conditions(self):
         """Test success run"""
-        self.assertEqual(len(self.conditions.Conditions), 7)
+        self.assertEqual(len(self.conditions.Conditions), 9)
+        self.assertEqual(len(self.conditions.Parameters), 2)
+        print(self.conditions.Parameters)
+        self.assertListEqual(self.conditions.Parameters['55caa18684cddafa866bdb947fb31ea563b2ea73'], ['None', 'Single NAT', 'High Availability'])
 
     def test_success_is_production(self):
         """ test isProduction """
@@ -243,6 +263,14 @@ def test_condition_relationship(self):
                 {'isProduction': False, 'isPrimary': False},
             ]
         )
+        # Multiple conditions with parameters specified
+        self.assertEqualListOfDicts(
+            self.conditions.get_scenarios(['DeployNatGateway', 'Az1Nat']),
+            [
+                {'DeployNatGateway': True, 'Az1Nat': True},
+                {'DeployNatGateway': False, 'Az1Nat': False}
+            ]
+        )
 
 
 class TestBadConditions(BaseTestCase):