Skip to content

Commit 19fed58

Browse files
committed
When using a list param in foreach pass back select statements when no allowed value
1 parent 6918363 commit 19fed58

File tree

3 files changed

+126
-4
lines changed

3 files changed

+126
-4
lines changed

src/cfnlint/decode/node.py

Lines changed: 0 additions & 1 deletion
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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Tuple
1313

1414
import regex as re
15+
import hashlib
16+
import json
1517

1618
from cfnlint.conditions._utils import get_hash
1719
from cfnlint.decode.node import str_node
@@ -193,14 +195,16 @@ 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, s: str, params: Mapping[str, Any],
197199
) -> Tuple[bool, str]:
198200
pattern = r"(\$|&){[a-zA-Z0-9\.:]+}"
199201
if not re.search(pattern, s):
200202
return (True, s)
201203

202204
new_s = deepcopy(s)
203205
for k, v in params.items():
206+
if isinstance(v, dict):
207+
v = hashlib.md5(json.dumps(v).encode('utf-8')).digest().hex()[0:4]
204208
new_s = re.sub(rf"\$\{{{k}\}}", v, new_s)
205209
new_s = re.sub(rf"\&\{{{k}\}}", re.sub("[^0-9a-zA-Z]+", "", v), new_s)
206210

@@ -453,6 +457,9 @@ def value(
453457
return [x.strip() for x in allowed_values[0].split(",")]
454458
return allowed_values[0]
455459

460+
if "List" in t:
461+
return [{"Fn::Select": [0, {"Ref": v}]}, {"Fn::Select": [1, {"Ref": v}]}]
462+
456463
raise _ResolveError("Can't resolve Fn::Ref", self._obj)
457464

458465

@@ -490,7 +497,7 @@ def values(
490497
if values:
491498
if isinstance(values, list):
492499
for value in values:
493-
if isinstance(value, str):
500+
if isinstance(value, (str, dict)):
494501
yield value
495502
else:
496503
raise _ValueError(

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

Lines changed: 117 additions & 1 deletion
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,83 @@ 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": {
484+
"Ref": "Identifier"
485+
},
486+
"Tags": [
487+
{
488+
"Key": "Name",
489+
"Value": {
490+
"Fn::Sub": "Name-${Identifier}"
491+
}
492+
},
493+
],
494+
},
495+
}
496+
},
497+
)
498+
cfn = Template(filename="", template=template_obj, regions=["us-east-1"])
499+
matches, template = language_extension(cfn)
500+
self.assertListEqual(matches, [])
501+
502+
result = deepcopy(self.result)
503+
result["Parameters"] = parameters
504+
result["Resources"]["S3Bucket5096"] = {
505+
"Properties": {
506+
"BucketName": {"Fn::Select": [1, {"Ref": "AccountIds"}]},
507+
"Tags": [
508+
{
509+
"Key": "Name",
510+
"Value": "Name-5096",
511+
},
512+
],
513+
},
514+
"Type": "AWS::S3::Bucket",
515+
}
516+
result["Resources"]["S3Bucketa72a"] = {
517+
"Properties": {
518+
"BucketName": {"Fn::Select": [0, {"Ref": "AccountIds"}]},
519+
"Tags": [
520+
{
521+
"Key": "Name",
522+
"Value": "Name-a72a",
523+
},
524+
],
525+
},
526+
"Type": "AWS::S3::Bucket",
527+
}
528+
del result["Resources"]["S3Bucketab"]
529+
del result["Resources"]["S3Bucketcd"]
530+
self.assertDictEqual(
531+
template,
532+
result,
533+
)
534+
419535
def test_bad_collection_ref(self):
420536
template_obj = deepcopy(self.template_obj)
421537
nested_set(

0 commit comments

Comments
 (0)