Repository: aws-cloudformation/cfn-lint

Issue:
SAM Function Global Environment Variables Interact Oddly with `!If` and Conditional Values
*cfn-lint version: (`cfn-lint --version`)*

cfn-lint 0.46.0

*Description of issue.*

When the `!If` intrinsic function is used with conditional values in SAM's Global Environment Variables, cfn-lint warns that the value on the `true` side of the condition may not be available when the condition is not true. For example, given an [`AWS::AppConfig::Environment`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appconfig-environment.html) in the current template, an importable instance of same, and an environment variable meant to pass the ID of one of them to a Function:

```yaml
---
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Parameters:
  DeploymentMode:
    Type: String
    AllowedValues:
    - sandbox
    - live
    Default: sandbox
    ConstraintDescription: Value must be a known deployment mode.
Conditions:
  IsLive: !Equals [ !Ref DeploymentMode, live ]
Globals:
  Function:
    Environment:
      Variables:
        AppConfig__Environment: !If
        - IsLive
        - !Ref ConfigEnvironment
        - !ImportValue ConfigEnvironment-blah
Resources:
  ConfigApplication:
    Type: AWS::AppConfig::Application
    Properties:
      Name: Application-A
  ConfigEnvironment:
    Type: AWS::AppConfig::Environment
    Condition: IsLive
    Properties:
      ApplicationId: !Ref ConfigApplication
      Name: Environment-B
  FunctionC:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: provided.al2
      Handler: provided
```

…cfn-lint reports this:

> [cfn-lint] W1001: Ref to resource "ConfigEnvironment" that may not be available when condition "IsLive" is False at Function/Environment/Variables/AppConfig__Environment/Fn::If/1/Ref

Oddly, the error is reported at a location in `Function` (under `Globals`, presumably), which I think isn't a location which exists in the transformed template. More oddly, this error is reported _only_ there. If the `!If` is removed and `ConfigEnvironment` is `!Ref`ed directly, the following are reported:

> [cfn-lint] W1001: Ref to resource "ConfigEnvironment" that may not be available when condition "IsLive" is False at Function/Environment/Variables/AppConfig__Environment/Ref
>
> [cfn-lint] W1001: Ref to resource "ConfigEnvironment" that may not be available when condition "IsLive" is False at Resources/FunctionC/Properties/Environment/Variables/AppConfig__Environment/Ref

That is, the error is reported both in the Globals section and in the `Environment.Variables` of `FunctionC` itself. But when the `!If` is present (as it should be), the error remains in `Globals`. If the environment variables are moved from `Globals` to directly within `FunctionC`, no error is reported. Unfortunately, this does not scale. Suppressing W1001 globally for deployments is our current workaround. 

Nothing in cfn-lint jumps out at me as responsible for this issue, so I don't have any leads for you to track down. The thing that looked closest was how this section is considered to have "non-strict" values, but that appears to be related to string vs. number, so it didn't seem right to me.



Discussion:
Mapping from the violation in the transformed template back to the line in the original source template is a bit best effort

Still looking into the violation itself though
I think that it's because [Line 933 in template.py][key] is key-not-found'ing on `text[path[0]]`. At that time, `path[0]` is "Function", which isn't a location which exists in the transformed template. This is causing the path conditions to be empty. When [the comparison is made][comparison] between the scenario count (0) and the number of path conditions (0), the scenario is appended to the results.

Does that sound reasonable? I'm afraid I don't know what the best fix to apply is, if so. ~~Maybe early-return `results` if `len(path_conditions) == 0`? It works in my testing, at least. I could whip up that PR.~~ ha ha disregard that fails a bunch of unit tests

[key]: https://github.com/aws-cloudformation/cfn-python-lint/blob/c8f6c3e42dff492e167b3df10c39cbbe4d58ca82/src/cfnlint/template.py#L933
[comparison]: https://github.com/aws-cloudformation/cfn-python-lint/blob/c8f6c3e42dff492e167b3df10c39cbbe4d58ca82/src/cfnlint/template.py#L619
Translated template for reference

```
{
  "AWSTemplateFormatVersion": null,
  "Conditions": {
    "IsLive": {
      "Fn::Equals": [
        {
          "Ref": "DeploymentMode"
        },
        "live"
      ]
    }
  },
  "Parameters": {
    "DeploymentMode": {
      "AllowedValues": [
        "sandbox",
        "live"
      ],
      "ConstraintDescription": "Value must be a known deployment mode.",
      "Default": "sandbox",
      "Type": "String"
    }
  },
  "Resources": {
    "ConfigApplication": {
      "Properties": {
        "Name": "Application-A"
      },
      "Type": "AWS::AppConfig::Application"
    },
    "ConfigEnvironment": {
      "Condition": "IsLive",
      "Properties": {
        "ApplicationId": {
          "Ref": "ConfigApplication"
        },
        "Name": "Environment-B"
      },
      "Type": "AWS::AppConfig::Environment"
    },
    "FunctionC": {
      "Properties": {
        "Code": {
          "S3Bucket": "bucket",
          "S3Key": "value"
        },
        "Environment": {
          "Variables": {
            "AppConfig__Environment": {
              "Fn::If": [
                "IsLive",
                {
                  "Ref": "ConfigEnvironment"
                },
                {
                  "Fn::ImportValue": "ConfigEnvironment-blah"
                }
              ]
            }
          }
        },
        "Handler": "provided",
        "Role": {
          "Fn::GetAtt": [
            "FunctionCRole",
            "Arn"
          ]
        },
        "Runtime": "provided.al2",
        "Tags": [
          {
            "Key": "lambda:createdBy",
            "Value": "SAM"
          }
        ]
      },
      "Type": "AWS::Lambda::Function"
    },
    "FunctionCRole": {
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Statement": [
            {
              "Action": [
                "sts:AssumeRole"
              ],
              "Effect": "Allow",
              "Principal": {
                "Service": [
                  "lambda.amazonaws.com"
                ]
              }
            }
          ],
          "Version": "2012-10-17"
        },
        "ManagedPolicyArns": [
          "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
        ],
        "Tags": [
          {
            "Key": "lambda:createdBy",
            "Value": "SAM"
          }
        ]
      },
      "Type": "AWS::IAM::Role"
    }
  }
}
```

A first glance this looks okay.  I need to look into this a little more.
Seeing what @chrisoverzero is seeing.  Trying to figure out why now.  That path is for sure jacked and need to figure out why.
Figured out the fix.  Working on a resolution.

Resolving patch:
diff --git a/src/cfnlint/rules/functions/RelationshipConditions.py b/src/cfnlint/rules/functions/RelationshipConditions.py
--- a/src/cfnlint/rules/functions/RelationshipConditions.py
+++ b/src/cfnlint/rules/functions/RelationshipConditions.py
@@ -24,7 +24,7 @@ def match(self, cfn):
         matches = []
 
         # Start with Ref checks
-        ref_objs = cfn.search_deep_keys('Ref')
+        ref_objs = cfn.search_deep_keys(searchText='Ref', includeGlobals=False)
         for ref_obj in ref_objs:
             value = ref_obj[-1]
             if value not in PSEUDOPARAMS:
@@ -42,7 +42,7 @@ def match(self, cfn):
                                 '/'.join(map(str, ref_obj[:-1])))))
 
         # The do GetAtt
-        getatt_objs = cfn.search_deep_keys('Fn::GetAtt')
+        getatt_objs = cfn.search_deep_keys(searchText='Fn::GetAtt', includeGlobals=False)
         for getatt_obj in getatt_objs:
             value_obj = getatt_obj[-1]
             value = None
diff --git a/src/cfnlint/template.py b/src/cfnlint/template.py
--- a/src/cfnlint/template.py
+++ b/src/cfnlint/template.py
@@ -309,7 +309,7 @@ def _search_deep_keys(self, searchText, cfndict, path):
 
         return keys
 
-    def search_deep_keys(self, searchText):
+    def search_deep_keys(self, searchText, includeGlobals=True):
         """
             Search for a key in all parts of the template.
             :return if searchText is "Ref", an array like ['Resources', 'myInstance', 'Properties', 'ImageId', 'Ref', 'Ec2ImageId']
@@ -318,7 +318,10 @@ def search_deep_keys(self, searchText):
         results = []
         results.extend(self._search_deep_keys(searchText, self.template, []))
         # Globals are removed during a transform.  They need to be checked manually
-        results.extend(self._search_deep_keys(searchText, self.transform_pre.get('Globals'), []))
+        if includeGlobals:
+            pre_results = self._search_deep_keys(searchText, self.transform_pre.get('Globals'), [])
+            for pre_result in pre_results:
+                results.append(['Globals'] + pre_result)
         return results
 
     def get_condition_values(self, template, path=[]):


Test patch:
diff --git a/test/fixtures/templates/good/functions/relationship_conditions_sam.yaml b/test/fixtures/templates/good/functions/relationship_conditions_sam.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/good/functions/relationship_conditions_sam.yaml
@@ -0,0 +1,37 @@
+---
+AWSTemplateFormatVersion: 2010-09-09
+Transform: AWS::Serverless-2016-10-31
+Parameters:
+  DeploymentMode:
+    Type: String
+    AllowedValues:
+    - sandbox
+    - live
+    Default: sandbox
+    ConstraintDescription: Value must be a known deployment mode.
+Conditions:
+  IsLive: !Equals [ !Ref DeploymentMode, live ]
+Globals:
+  Function:
+    Environment:
+      Variables:
+        AppConfig__Environment: !If
+        - IsLive
+        - !Ref ConfigEnvironment
+        - !ImportValue ConfigEnvironment-blah
+Resources:
+  ConfigApplication:
+    Type: AWS::AppConfig::Application
+    Properties:
+      Name: Application-A
+  ConfigEnvironment:
+    Type: AWS::AppConfig::Environment
+    Condition: IsLive
+    Properties:
+      ApplicationId: !Ref ConfigApplication
+      Name: Environment-B
+  FunctionC:
+    Type: AWS::Serverless::Function
+    Properties:
+      Runtime: provided.al2
+      Handler: provided
\ No newline at end of file
diff --git a/test/unit/rules/functions/test_relationship_conditions.py b/test/unit/rules/functions/test_relationship_conditions.py
--- a/test/unit/rules/functions/test_relationship_conditions.py
+++ b/test/unit/rules/functions/test_relationship_conditions.py
@@ -15,6 +15,7 @@ def setUp(self):
         self.collection.register(RelationshipConditions())
         self.success_templates = [
             'test/fixtures/templates/good/functions/relationship_conditions.yaml',
+            'test/fixtures/templates/good/functions/relationship_conditions_sam.yaml'
         ]
 
     def test_file_positive(self):