Skip to content

Commit d6a46c5

Browse files
authored
Resolve Fn::FindInMap when a string is returned as part of a foreach (#2814)
* Resolve Fn::FindInMap when a string is returned as part of a foreach * Update rule to handle ForEach false positives
1 parent 34b7c60 commit d6a46c5

File tree

9 files changed

+192
-21
lines changed

9 files changed

+192
-21
lines changed

src/cfnlint/conditions/conditions.py

+2
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,8 @@ def build_scenerios_on_region(
271271
return
272272
cnf_region = self._cnf.copy()
273273
found_region = False
274+
if condition_name not in self._conditions:
275+
return
274276
for eql in self._conditions[condition_name].equals:
275277
is_region, equal_region = eql.is_region
276278
if is_region:

src/cfnlint/rules/functions/FindInMap.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ def validate_enhanced_find_in_map(self, find_in_map, cfn):
259259

260260
def _match_with_language_extensions(self, cfn):
261261
matches = []
262-
find_in_maps = cfn.search_deep_keys("Fn::FindInMap")
262+
find_in_maps = cfn.transform_pre["Fn::FindInMap"]
263263
for in_map in find_in_maps:
264264
matches.extend(
265265
self.validate_enhanced_find_in_map(find_in_map=in_map, cfn=cfn)

src/cfnlint/rules/parameters/Used.py

+8
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,19 @@ def isparaminref(self, subs, parameter):
3535
def match(self, cfn):
3636
matches = []
3737

38+
le_refs = None
39+
if cfn.has_language_extensions_transform():
40+
le_refs = cfn.search_deep_keys("Ref")
41+
3842
reftrees = cfn.transform_pre.get("Ref")
3943
subtrees = cfn.transform_pre.get("Fn::Sub")
4044
refs = []
4145
for reftree in reftrees:
4246
refs.append(reftree[-1])
47+
if le_refs:
48+
for le_ref in le_refs:
49+
if le_ref[-1] not in refs:
50+
refs.append(le_ref[-1])
4351
subs = []
4452
for subtree in subtrees:
4553
if isinstance(subtree[-1], list):

src/cfnlint/template/template.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -844,7 +844,7 @@ def is_resource_available(self, path, resource):
844844
resource_condition = (
845845
self.template.get("Resources", {}).get(resource, {}).get("Condition")
846846
)
847-
if resource_condition:
847+
if isinstance(resource_condition, str):
848848
# if path conditions are empty that means its always true
849849
if not path_conditions:
850850
return [{resource_condition: False}]
@@ -1133,7 +1133,7 @@ def get_conditions_from_path(
11331133
if len(path) >= 2:
11341134
if path[0] in ["Resources", "Outputs"]:
11351135
condition = text.get(path[0], {}).get(path[1], {}).get("Condition")
1136-
if condition:
1136+
if isinstance(condition, str):
11371137
if not results.get(condition):
11381138
results[condition] = set()
11391139
results[condition].add(True)

src/cfnlint/template/transforms/_language_extensions.py

+102-18
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from __future__ import annotations
77

8+
import logging
89
import random
910
import string
1011
from copy import deepcopy
@@ -16,6 +17,8 @@
1617
from cfnlint.helpers import FUNCTION_FOR_EACH
1718
from cfnlint.template.transforms._types import TransformResult
1819

20+
LOGGER = logging.getLogger("cfnlint")
21+
1922
# initializing size of string
2023
_N = 7
2124

@@ -51,14 +54,14 @@ def language_extension(cfn: Any) -> TransformResult:
5154

5255
message = "Error transforming template: {0}"
5356
if hasattr(e.key, "start_mark"):
54-
sm_line = e.key.start_mark.line + 1
55-
sm_column = e.key.start_mark.column + 1
57+
sm_line = e.key.start_mark[0] + 1
58+
sm_column = e.key.start_mark[1] + 1
5659
else:
5760
sm_line = 1
5861
sm_column = 1
5962
if hasattr(e.key, "end_mark"):
60-
em_line = e.key.end_mark.line + 1
61-
em_column = e.key.end_mark.column + 1
63+
em_line = e.key.end_mark[0] + 1
64+
em_column = e.key.end_mark[1] + 1
6265
else:
6366
em_line = 1
6467
em_column = 1
@@ -76,6 +79,7 @@ def language_extension(cfn: Any) -> TransformResult:
7679
], None
7780
except Exception as e: # pylint: disable=broad-exception-caught
7881
# pylint: disable=import-outside-toplevel
82+
from cfnlint.match import Match # pylint: disable=cyclic-import
7983
from cfnlint.rules import TransformError # pylint: disable=cyclic-import
8084

8185
message = "Error transforming template: {0}"
@@ -141,6 +145,17 @@ def _walk(self, item: Any, params: MutableMapping[str, Any], cfn: Any):
141145
)
142146
if only_string:
143147
return obj[k][0]
148+
elif k == "Fn::FindInMap":
149+
try:
150+
mapping = _ForEachValueFnFindInMap(get_hash(v), v)
151+
map_value = mapping.value(cfn, params)
152+
if map_value is None:
153+
del obj[k]
154+
continue
155+
if isinstance(map_value, str):
156+
return map_value
157+
except Exception as e: # pylint: disable=broad-exception-caught
158+
LOGGER.debug("Transform and Fn::FindInMap error: %s", {str(e)})
144159
elif k == "Ref":
145160
if isinstance(v, str):
146161
if v in params:
@@ -152,7 +167,11 @@ def _walk(self, item: Any, params: MutableMapping[str, Any], cfn: Any):
152167
return params[r]
153168
obj[k] = r
154169
else:
155-
obj[k] = self._walk(v, params, cfn)
170+
sub_value = self._walk(v, params, cfn)
171+
if sub_value is None:
172+
del obj[k]
173+
else:
174+
obj[k] = self._walk(v, params, cfn)
156175
elif isinstance(obj, list):
157176
for i, v in enumerate(obj):
158177
obj[i] = self._walk(v, params, cfn)
@@ -195,45 +214,89 @@ def create(obj: Any) -> _ForEachValue:
195214
raise _TypeError(f"Unsupported value {obj!r}", obj)
196215

197216
# pylint: disable=unused-argument
198-
def value(self, cfn):
217+
def value(
218+
self, cfn, params: Optional[Mapping[str, Any]] = None, only_params: bool = False
219+
):
199220
return self._value
200221

201222
@property
202223
def hash(self):
203224
return self._hash
204225

205226

227+
class _FnFindInMapDefaultValue(_ForEachValue):
228+
def __init__(self, _hash: str, value: Any = None) -> None:
229+
super().__init__(_hash, value)
230+
if not isinstance(value, dict):
231+
raise _TypeError(
232+
"Fn::FindInMap parameter must be an object with key 'DefaultValue'",
233+
value,
234+
)
235+
if len(value) != 1:
236+
raise _ValueError(
237+
"Fn::FindInMap parameter only supports 'DefaultValue'", value
238+
)
239+
240+
for k, v in value.items():
241+
if k != "DefaultValue":
242+
raise _ValueError(
243+
"Fn::FindInMap parameter only supports 'DefaultValue'", value
244+
)
245+
self._value = _ForEachValue.create(v)
246+
247+
def value(
248+
self, cfn, params: Optional[Mapping[str, Any]] = None, only_params: bool = False
249+
):
250+
if params is None:
251+
params = {}
252+
253+
return self._value.value(cfn, params, only_params)
254+
255+
206256
class _ForEachValueFnFindInMap(_ForEachValue):
207257
def __init__(self, _hash: str, obj: Any) -> None:
208258
super().__init__(_hash)
209259
if not isinstance(obj, list):
210260
raise _TypeError("Fn::FindInMap should be a list", obj)
211261

212-
if len(obj) != 3:
213-
raise _ValueError("Fn::FindInMap requires a list of 3 values", obj)
262+
if len(obj) not in [3, 4]:
263+
raise _ValueError("Fn::FindInMap requires a list of 3 or 4 values", obj)
214264

215265
self._map = [
216266
_ForEachValue.create(obj[0]),
217267
_ForEachValue.create(obj[1]),
218268
_ForEachValue.create(obj[2]),
219269
]
270+
if len(obj) == 4:
271+
self._map.append(_FnFindInMapDefaultValue(get_hash(obj[3]), obj[3]))
220272

221273
self._obj = obj
222274

223-
def value(self, cfn: Any) -> Any:
275+
def value(
276+
self,
277+
cfn: Any,
278+
params: Optional[Mapping[str, Any]] = None,
279+
only_params: bool = False,
280+
) -> Any:
281+
if params is None:
282+
params = {}
224283
t_map = deepcopy(self._map)
225284
mapping = None
226285

227286
try:
228-
mapping = cfn.template.get("Mappings", {}).get(t_map[0].value(cfn))
287+
mapping = cfn.template.get("Mappings", {}).get(
288+
t_map[0].value(cfn, params, only_params)
289+
)
229290
except Exception: # pylint: disable=broad-exception-caught
230291
if len(cfn.template.get("Mappings", {}).keys()) == 1:
231292
mapping = cfn.template.get("Mappings", {}).get(
232293
list(cfn.template.get("Mappings", {}).keys())[0]
233294
)
234295

235296
try:
236-
if mapping is None and isinstance(t_map[1].value(cfn), str):
297+
if mapping is None and isinstance(
298+
t_map[1].value(cfn, params, only_params), str
299+
):
237300
for k, v in cfn.template.get("Mappings", {}).items():
238301
if isinstance(v, dict):
239302
if t_map[1].value(cfn) in v:
@@ -244,12 +307,14 @@ def value(self, cfn: Any) -> Any:
244307
pass
245308

246309
try:
247-
if mapping is None and isinstance(t_map[2].value(cfn), str):
310+
if mapping is None and isinstance(
311+
t_map[2].value(cfn, params, only_params), str
312+
):
248313
for m1, mv1 in cfn.template.get("Mappings", {}).items():
249314
if isinstance(mv1, dict):
250315
for k1, kv1 in mv1.items():
251316
if isinstance(kv1, dict):
252-
if t_map[2].value(cfn) in kv1:
317+
if t_map[2].value(cfn, params, only_params) in kv1:
253318
t_map[1] = _ForEachValue.create(k1)
254319
t_map[0] = _ForEachValue.create(m1)
255320
mapping = mv1
@@ -265,17 +330,23 @@ def value(self, cfn: Any) -> Any:
265330
t_map[2].value(cfn)
266331
for k, v in mapping.items():
267332
if isinstance(v, dict):
268-
if t_map[2].value(cfn) in v:
333+
if t_map[2].value(cfn, params, only_params) in v:
269334
t_map[1] = _ForEachValue.create(k)
270335
except _ResolveError:
271336
pass
272337

273338
if mapping:
274339
try:
275-
return mapping.get(t_map[1].value(cfn)).get(t_map[2].value(cfn))
340+
return mapping.get(t_map[1].value(cfn, params, only_params), {}).get(
341+
t_map[2].value(cfn, params, only_params)
342+
)
276343
except _ResolveError as e:
344+
if len(self._map) == 4:
345+
return self._map[3].value(cfn, params, only_params)
277346
raise _ResolveError("Can't resolve Fn::FindInMap", self._obj) from e
278347

348+
if len(self._map) == 4:
349+
return self._map[3].value(cfn, params, only_params)
279350
raise _ResolveError("Can't resolve Fn::FindInMap", self._obj)
280351

281352

@@ -289,12 +360,25 @@ def __init__(self, _hash: str, obj: Any) -> None:
289360
self._obj = obj
290361

291362
# pylint: disable=too-many-return-statements
292-
def value(self, cfn: Any) -> Any:
363+
def value(
364+
self,
365+
cfn: Any,
366+
params: Optional[Mapping[str, Any]] = None,
367+
only_params: bool = False,
368+
) -> Any:
369+
if params is None:
370+
params = {}
293371
v = self._ref.value(cfn)
294372

295373
if not isinstance(v, str):
296374
raise _ResolveError("Can't resolve Fn::Ref", self._obj)
297375

376+
if v in params:
377+
return params[v]
378+
379+
if only_params:
380+
raise _ResolveError("Can't resolve Fn::Ref", self._obj)
381+
298382
region = cfn.regions[0]
299383
account_id = "123456789012"
300384
partition = "aws"
@@ -373,15 +457,15 @@ def values(
373457
if self._collection:
374458
for item in self._collection:
375459
try:
376-
yield item.value(cfn)
460+
yield item.value(cfn, {}, False)
377461
except _ResolveError:
378462
v = "".join(random.choices(string.ascii_letters, k=_N)) # nosec
379463
collection_cache[item.hash] = v
380464
yield v
381465
return
382466
if self._fn:
383467
try:
384-
values = self._fn.value(cfn)
468+
values = self._fn.value(cfn, {}, False)
385469
if values:
386470
if isinstance(values, list):
387471
for value in values:

src/cfnlint/template/transforms/transform.py

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55
from __future__ import annotations
66

7+
import logging
78
from typing import Any, Mapping
89

910
from cfnlint.conditions import Conditions
@@ -12,6 +13,8 @@
1213
from cfnlint.template.transforms._protocols import Transformer
1314
from cfnlint.template.transforms._sam import sam
1415

16+
LOGGER = logging.getLogger("cfnlint")
17+
1518

1619
class Transform:
1720
def __init__(self) -> None:
@@ -48,4 +51,5 @@ def transform(self, cfn: Any):
4851

4952
cfn.graph = Graph(cfn)
5053
cfn.conditions = Conditions(cfn)
54+
LOGGER.info("Transformed template: %s", cfn.template)
5155
return matches

0 commit comments

Comments
 (0)