Skip to content

Commit c53b104

Browse files
authored
When using a list param in foreach pass back select statements when no allowed value (#3176)
1 parent 6918363 commit c53b104

File tree

3 files changed

+121
-4
lines changed

3 files changed

+121
-4
lines changed

src/cfnlint/decode/node.py

-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ class node_class(cls):
7676
def __init__(
7777
self, x, start_mark: Mark | None = None, end_mark: Mark | None = None
7878
):
79-
LOGGER.debug(type(start_mark))
8079
try:
8180
cls.__init__(self, x)
8281
except TypeError:

src/cfnlint/template/transforms/_language_extensions.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
from __future__ import annotations
77

8+
import hashlib
9+
import json
810
import logging
911
import random
1012
import string
@@ -193,14 +195,18 @@ def _walk(self, item: Any, params: MutableMapping[str, Any], cfn: Any):
193195
return obj
194196

195197
def _replace_string_params(
196-
self, s: str, params: Mapping[str, Any]
198+
self,
199+
s: str,
200+
params: Mapping[str, Any],
197201
) -> Tuple[bool, str]:
198202
pattern = r"(\$|&){[a-zA-Z0-9\.:]+}"
199203
if not re.search(pattern, s):
200204
return (True, s)
201205

202206
new_s = deepcopy(s)
203207
for k, v in params.items():
208+
if isinstance(v, dict):
209+
v = hashlib.md5(json.dumps(v).encode("utf-8")).digest().hex()[0:4]
204210
new_s = re.sub(rf"\$\{{{k}\}}", v, new_s)
205211
new_s = re.sub(rf"\&\{{{k}\}}", re.sub("[^0-9a-zA-Z]+", "", v), new_s)
206212

@@ -453,6 +459,9 @@ def value(
453459
return [x.strip() for x in allowed_values[0].split(",")]
454460
return allowed_values[0]
455461

462+
if "List" in t:
463+
return [{"Fn::Select": [0, {"Ref": v}]}, {"Fn::Select": [1, {"Ref": v}]}]
464+
456465
raise _ResolveError("Can't resolve Fn::Ref", self._obj)
457466

458467

@@ -490,7 +499,7 @@ def values(
490499
if values:
491500
if isinstance(values, list):
492501
for value in values:
493-
if isinstance(value, str):
502+
if isinstance(value, (str, dict)):
494503
yield value
495504
else:
496505
raise _ValueError(

test/unit/module/template/transforms/test_language_extensions.py

+110-1
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
from cfnlint.template import Template
1111
from cfnlint.template.transforms._language_extensions import (
1212
_ForEach,
13+
_ForEachCollection,
1314
_ForEachValue,
1415
_ForEachValueFnFindInMap,
15-
_ForEachValueRef,
1616
_ResolveError,
1717
_Transform,
1818
_TypeError,
@@ -27,6 +27,7 @@ def test_valid(self):
2727
_ForEach(
2828
"key", [{"Ref": "Parameter"}, {"Ref": "AWS::NotificationArns"}, {}], {}
2929
)
30+
_ForEach("key", ["AccountId", {"Ref": "AccountIds"}, {}], {})
3031

3132
def test_wrong_type(self):
3233
with self.assertRaises(_TypeError):
@@ -54,6 +55,32 @@ def test_output_type(self):
5455
_ForEach("key", ["foo", ["bar"], []], {})
5556

5657

58+
class TestForEach(TestCase):
59+
def setUp(self) -> None:
60+
super().setUp()
61+
self.cfn = Template(
62+
"",
63+
{
64+
"Parameters": {
65+
"AccountIds": {
66+
"Type": "CommaDelimitedList",
67+
},
68+
},
69+
},
70+
regions=["us-west-2"],
71+
)
72+
73+
def test_valid(self):
74+
fec = _ForEachCollection({"Ref": "AccountIds"})
75+
self.assertListEqual(
76+
list(fec.values(self.cfn, {})),
77+
[
78+
{"Fn::Select": [0, {"Ref": "AccountIds"}]},
79+
{"Fn::Select": [1, {"Ref": "AccountIds"}]},
80+
],
81+
)
82+
83+
5784
class TestRef(TestCase):
5885
def setUp(self) -> None:
5986
self.template_obj = convert_dict(
@@ -75,6 +102,9 @@ def setUp(self) -> None:
75102
"Type": "List<AWS::EC2::Subnet::Id>",
76103
"AllowedValues": ["sg-12345678, sg-87654321"],
77104
},
105+
"AccountIds": {
106+
"Type": "CommaDelimitedList",
107+
},
78108
},
79109
}
80110
)
@@ -115,6 +145,15 @@ def test_ref(self):
115145
fe = _ForEachValue.create({"Ref": "SecurityGroups"})
116146
self.assertEqual(fe.value(self.cfn), ["sg-12345678", "sg-87654321"])
117147

148+
fe = _ForEachValue.create({"Ref": "AccountIds"})
149+
self.assertEqual(
150+
fe.value(self.cfn),
151+
[
152+
{"Fn::Select": [0, {"Ref": "AccountIds"}]},
153+
{"Fn::Select": [1, {"Ref": "AccountIds"}]},
154+
],
155+
)
156+
118157

119158
class TestFindInMap(TestCase):
120159
def setUp(self) -> None:
@@ -416,6 +455,76 @@ def test_transform_findinmap_function(self):
416455
result,
417456
)
418457

458+
def test_transform_list_parameter(self):
459+
template_obj = deepcopy(self.template_obj)
460+
parameters = {"AccountIds": {"Type": "CommaDelimitedList"}}
461+
template_obj["Parameters"] = parameters
462+
463+
nested_set(
464+
template_obj,
465+
[
466+
"Resources",
467+
"Fn::ForEach::SpecialCharacters",
468+
1,
469+
],
470+
{"Ref": "AccountIds"},
471+
)
472+
nested_set(
473+
template_obj,
474+
[
475+
"Resources",
476+
"Fn::ForEach::SpecialCharacters",
477+
2,
478+
],
479+
{
480+
"S3Bucket&{Identifier}": {
481+
"Type": "AWS::S3::Bucket",
482+
"Properties": {
483+
"BucketName": {"Ref": "Identifier"},
484+
"Tags": [
485+
{"Key": "Name", "Value": {"Fn::Sub": "Name-${Identifier}"}},
486+
],
487+
},
488+
}
489+
},
490+
)
491+
cfn = Template(filename="", template=template_obj, regions=["us-east-1"])
492+
matches, template = language_extension(cfn)
493+
self.assertListEqual(matches, [])
494+
495+
result = deepcopy(self.result)
496+
result["Parameters"] = parameters
497+
result["Resources"]["S3Bucket5096"] = {
498+
"Properties": {
499+
"BucketName": {"Fn::Select": [1, {"Ref": "AccountIds"}]},
500+
"Tags": [
501+
{
502+
"Key": "Name",
503+
"Value": "Name-5096",
504+
},
505+
],
506+
},
507+
"Type": "AWS::S3::Bucket",
508+
}
509+
result["Resources"]["S3Bucketa72a"] = {
510+
"Properties": {
511+
"BucketName": {"Fn::Select": [0, {"Ref": "AccountIds"}]},
512+
"Tags": [
513+
{
514+
"Key": "Name",
515+
"Value": "Name-a72a",
516+
},
517+
],
518+
},
519+
"Type": "AWS::S3::Bucket",
520+
}
521+
del result["Resources"]["S3Bucketab"]
522+
del result["Resources"]["S3Bucketcd"]
523+
self.assertDictEqual(
524+
template,
525+
result,
526+
)
527+
419528
def test_bad_collection_ref(self):
420529
template_obj = deepcopy(self.template_obj)
421530
nested_set(

0 commit comments

Comments
 (0)