Skip to content

Commit 86c818d

Browse files
authored
Add rule E1032 to validate ForEach with transform (#2809)
1 parent 5d610cf commit 86c818d

File tree

9 files changed

+143
-10
lines changed

9 files changed

+143
-10
lines changed

src/cfnlint/rules/functions/ForEach.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55
import logging
66

7+
from cfnlint.languageExtensions import LanguageExtensions
78
from cfnlint.rules import CloudFormationLintRule
89

910
LOGGER = logging.getLogger("cfnlint")
@@ -18,4 +19,19 @@ class ForEach(CloudFormationLintRule):
1819

1920
# pylint: disable=unused-argument
2021
def match(self, cfn):
21-
return []
22+
matches = []
23+
intrinsic_function = "Fn::ForEach"
24+
for_eaches = cfn.transform_pre["Fn::ForEach"]
25+
26+
for for_each in for_eaches:
27+
has_language_extensions_transform = cfn.has_language_extensions_transform()
28+
29+
LanguageExtensions.validate_transform_is_declared(
30+
self,
31+
has_language_extensions_transform,
32+
matches,
33+
for_each[:-1],
34+
intrinsic_function,
35+
)
36+
37+
return matches

src/cfnlint/rules/functions/GetAtt.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ def match(self, cfn):
9898
# only check resname if its set. if it isn't it is because of bad structure
9999
# and an error is already provided
100100
if resname:
101+
if not isinstance(resname, str):
102+
message = "Invalid GetAtt structure {0} at {1}"
103+
matches.append(
104+
RuleMatch(
105+
getatt,
106+
message.format(getatt[-1], "/".join(map(str, getatt[:-1]))),
107+
)
108+
)
109+
continue
101110
if resname in valid_getatts:
102111
if restype is not None:
103112
# Check for maps

src/cfnlint/template/template.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ def __init__(self, filename, template, regions=["us-east-1"]):
4646
self.transform_pre["Fn::Sub"] = self.search_deep_keys("Fn::Sub")
4747
self.transform_pre["Fn::FindInMap"] = self.search_deep_keys("Fn::FindInMap")
4848
self.transform_pre["Transform"] = self.template.get("Transform", [])
49+
self.transform_pre["Fn::ForEach"] = self.search_deep_keys(
50+
cfnlint.helpers.FUNCTION_FOR_EACH
51+
)
4952

5053
self.conditions = cfnlint.conditions.Conditions(self)
5154
self.__cache_search_deep_class = {}
@@ -460,12 +463,13 @@ def _search_deep_keys(self, searchText: str | re.Pattern, cfndict, path):
460463
# dict and list checks
461464
pathprop = pathprop[:-1]
462465
elif isinstance(searchText, re.Pattern):
463-
if re.match(searchText, key):
464-
pathprop.append(cfndict[key])
465-
keys.append(pathprop)
466-
# pop the last element off for nesting of found elements for
467-
# dict and list checks
468-
pathprop = pathprop[:-1]
466+
if isinstance(key, str):
467+
if re.match(searchText, key):
468+
pathprop.append(cfndict[key])
469+
keys.append(pathprop)
470+
# pop the last element off for nesting of found elements for
471+
# dict and list checks
472+
pathprop = pathprop[:-1]
469473
if isinstance(cfndict[key], dict):
470474
keys.extend(
471475
self._search_deep_keys(searchText, cfndict[key], pathprop)

src/cfnlint/template/transforms/_language_extensions.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ def __init__(self, message: str, key: str) -> None:
4343
def language_extension(cfn: Any) -> TransformResult:
4444
transform = _Transform()
4545
try:
46-
cfn.transform_pre["Fn::ForEach"] = []
4746
return transform.transform(cfn)
4847
except (_ValueError, _TypeError, _ResolveError) as e:
4948
# pylint: disable=import-outside-toplevel
@@ -115,7 +114,6 @@ def _walk(self, item: Any, params: MutableMapping[str, Any], cfn: Any):
115114
for k, v in deepcopy(obj).items():
116115
# see if key matches Fn::ForEach
117116
if re.match(FUNCTION_FOR_EACH, k):
118-
cfn.transform_pre["Fn::ForEach"].append({k: v})
119117
# only translate the foreach if its valid
120118
foreach = _ForEach(k, v, self._collections)
121119
# get the values will flatten the foreach
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
AWSTemplateFormatVersion: 2010-09-09
2+
3+
Parameters:
4+
Environment:
5+
Type: String
6+
Mappings:
7+
Buckets:
8+
Production:
9+
Identifiers: [A, B, C]
10+
Resources:
11+
"Fn::ForEach::Buckets":
12+
- Identifier
13+
- !FindInMap [Buckets, !Ref Environment, Identifiers]
14+
- "S3Bucket${Identifier}":
15+
Type: "AWS::S3::Bucket"
16+
Properties:
17+
AccessControl: PublicRead
18+
MetricsConfigurations:
19+
- Id: !Sub "EntireBucket${Identifier}"
20+
WebsiteConfiguration:
21+
IndexDocument: index.html
22+
ErrorDocument: error.html
23+
RoutingRules:
24+
- RoutingRuleCondition:
25+
HttpErrorCodeReturnedEquals: "404"
26+
KeyPrefixEquals: out1/
27+
RedirectRule:
28+
HostName: ec2-11-22-333-44.compute-1.amazonaws.com
29+
ReplaceKeyPrefixWith: report-404/
30+
DeletionPolicy: Retain
31+
UpdateReplacePolicy: Retain
32+
Outputs:
33+
"Fn::ForEach::BucketOutputs":
34+
- Identifier
35+
- !FindInMap [Buckets, !Ref Environment, Identifiers]
36+
- "Fn::ForEach::GetAttLoop":
37+
- Property
38+
- [Arn, DomainName, WebsiteURL]
39+
- "S3Bucket${Identifier}${Property}":
40+
Value: !GetAtt [!Sub "S3Bucket${Identifier}", {"Ref": "Property"}]

test/fixtures/templates/bad/functions/getatt.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,5 @@ Outputs:
5858
Value: !GetAtt ProvisionedProduct.Output.Example
5959
OutputBadName:
6060
Value: !GetAtt myElasticSearch.DomainEndpoint.Example
61+
BadStructure:
62+
Value: !GetAtt [!Sub "${ProjectS3Root}", !Sub "${SSMLookup}"]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
AWSTemplateFormatVersion: 2010-09-09
2+
Transform: "AWS::LanguageExtensions"
3+
Parameters:
4+
Environment:
5+
Type: String
6+
Mappings:
7+
Buckets:
8+
Production:
9+
Identifiers: [A, B, C]
10+
Resources:
11+
"Fn::ForEach::Buckets":
12+
- Identifier
13+
- !FindInMap [Buckets, !Ref Environment, Identifiers]
14+
- "S3Bucket${Identifier}":
15+
Type: "AWS::S3::Bucket"
16+
Properties:
17+
AccessControl: PublicRead
18+
MetricsConfigurations:
19+
- Id: !Sub "EntireBucket${Identifier}"
20+
WebsiteConfiguration:
21+
IndexDocument: index.html
22+
ErrorDocument: error.html
23+
RoutingRules:
24+
- RoutingRuleCondition:
25+
HttpErrorCodeReturnedEquals: "404"
26+
KeyPrefixEquals: out1/
27+
RedirectRule:
28+
HostName: ec2-11-22-333-44.compute-1.amazonaws.com
29+
ReplaceKeyPrefixWith: report-404/
30+
DeletionPolicy: Retain
31+
UpdateReplacePolicy: Retain
32+
Outputs:
33+
"Fn::ForEach::BucketOutputs":
34+
- Identifier
35+
- !FindInMap [Buckets, !Ref Environment, Identifiers]
36+
- "Fn::ForEach::GetAttLoop":
37+
- Property
38+
- [Arn, DomainName, WebsiteURL]
39+
- "S3Bucket${Identifier}${Property}":
40+
Value: !GetAtt [!Sub "S3Bucket${Identifier}", {"Ref": "Property"}]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: MIT-0
4+
"""
5+
from test.unit.rules import BaseRuleTestCase
6+
7+
from cfnlint.rules.functions.ForEach import ForEach
8+
9+
10+
class TestForEach(BaseRuleTestCase):
11+
def setUp(self):
12+
super(TestForEach, self).setUp()
13+
self.collection.register(ForEach())
14+
self.success_templates = [
15+
"test/fixtures/templates/good/functions/foreach.yaml",
16+
]
17+
18+
def test_file_positive(self):
19+
self.helper_file_positive()
20+
21+
def test_file_negative_missing_transform(self):
22+
self.helper_file_negative(
23+
"test/fixtures/templates/bad/functions/foreach_no_transform.yaml", 3
24+
)

test/unit/rules/functions/test_get_att.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,5 @@ def test_file_negative(self):
3030
def test_file_negative_getatt(self):
3131
"""Test failure"""
3232
self.helper_file_negative(
33-
"test/fixtures/templates/bad/functions/getatt.yaml", 5
33+
"test/fixtures/templates/bad/functions/getatt.yaml", 7
3434
)

0 commit comments

Comments
 (0)