Skip to content

Commit 29d1a17

Browse files
committed
Module resource foreach
1 parent 4e1ada8 commit 29d1a17

File tree

4 files changed

+112
-46
lines changed

4 files changed

+112
-46
lines changed

awscli/customizations/cloudformation/modules/flatten.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from awscli.customizations.cloudformation import exceptions
2323
from awscli.customizations.cloudformation.modules.visitor import Visitor
2424
from awscli.customizations.cloudformation.modules.merge import isdict
25-
from awscli.customizations.cloudformation.yamlhelper import yaml_dump
2625

2726
LOG = logging.getLogger(__name__)
2827

@@ -567,8 +566,6 @@ def _process_flatten(flatten_config):
567566
InvalidModuleError: If the configuration is invalid
568567
"""
569568

570-
print("_process_flatten:", yaml_dump(flatten_config))
571-
572569
# Handle simple source case (just a list or scalar)
573570
if not isdict(flatten_config):
574571
if isinstance(flatten_config, list):
@@ -647,8 +644,6 @@ def vf(v):
647644
flatten_config = v.d[FLATTEN]
648645
result = _process_flatten(flatten_config)
649646

650-
print("flatten result:", yaml_dump(result))
651-
652647
# Replace the Fn::Flatten with its result
653648
v.p[v.k] = result
654649
except Exception as e:

awscli/customizations/cloudformation/modules/foreach.py

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
from awscli.customizations.cloudformation.modules.util import (
3434
isdict,
3535
)
36-
from awscli.customizations.cloudformation.yamlhelper import yaml_dump
3736

3837
IDENTIFIER_PLACEHOLDER = "$Identifier"
3938
INDEX_PLACEHOLDER = "$Index"
@@ -187,9 +186,6 @@ def process_foreach_item(
187186
4. A Map where keys serve as identifiers
188187
"""
189188

190-
print(f"Processing ForEach for {name}")
191-
print(config)
192-
193189
# Modules or Resources
194190
items = template[section]
195191

@@ -213,8 +209,6 @@ def _resolve_foreach_ref(config, k, parent_module):
213209

214210
fe = config[FOREACH]
215211

216-
print("_resolve_foreach_ref fe:", yaml_dump(fe))
217-
218212
if isdict(fe):
219213
if REF in fe:
220214
resolved = parent_module.find_ref(fe[REF])
@@ -226,16 +220,10 @@ def _resolve_foreach_ref(config, k, parent_module):
226220

227221
if FLATTEN in fe:
228222

229-
print("fe before resolve:", yaml_dump(fe))
230-
231223
parent_module.resolve(fe)
232224

233-
print("fe after resolve:", yaml_dump(fe))
234-
235225
fn_flatten(config)
236226

237-
print("config after flatten:", yaml_dump(config))
238-
239227
return
240228

241229
# Validate map keys
@@ -252,7 +240,6 @@ def _parse_foreach_value(m, k):
252240

253241
# Handle empty list case
254242
if isinstance(m, list) and not m:
255-
print(f"Warning: ForEach for {k} is an empty list")
256243
return tokens, values
257244

258245
# List of objects with Identifier property
@@ -336,6 +323,30 @@ def _create_foreach_copies(name, config, tokens, values, items):
336323
del items[name]
337324

338325

326+
def _get_value_from_getatt_reference(node_dict, value_obj):
327+
"""
328+
Extract a value from a GetAtt reference to $Value.X
329+
330+
node_dict: The dictionary that might contain a GetAtt reference
331+
value_obj: The value object containing properties to reference
332+
333+
Returns: The referenced value if found, None otherwise
334+
"""
335+
if not isinstance(node_dict, dict) or GETATT not in node_dict:
336+
return None
337+
338+
getatt = node_dict[GETATT]
339+
if not (isinstance(getatt, list) and len(getatt) >= 2):
340+
return None
341+
342+
if getatt[0] == "$Value" and isinstance(value_obj, dict):
343+
prop_name = getatt[1]
344+
if prop_name in value_obj:
345+
return value_obj[prop_name]
346+
347+
return None
348+
349+
339350
def _process_properties(module, i, token):
340351
"""
341352
Process properties in a copied module, replacing placeholders.
@@ -365,14 +376,12 @@ def resolve_identifier_refs(vis):
365376
else:
366377
vis.p[vis.k] = new_val
367378
# Handle GetAtt to $Value.X
368-
elif isinstance(vis.d, dict) and GETATT in vis.d:
369-
getatt = vis.d[GETATT]
370-
if isinstance(getatt, list) and len(getatt) >= 2:
371-
if getatt[0] == "$Value" and FOREACH_VALUE in module:
372-
value_obj = module[FOREACH_VALUE]
373-
prop_name = getatt[1]
374-
if prop_name in value_obj:
375-
vis.p[vis.k] = value_obj[prop_name]
379+
elif FOREACH_VALUE in module:
380+
value = _get_value_from_getatt_reference(
381+
vis.d, module[FOREACH_VALUE]
382+
)
383+
if value is not None:
384+
vis.p[vis.k] = value
376385

377386
# Process ${Identifier} references
378387
Visitor(module[PROPERTIES]).visit(resolve_identifier_refs)
@@ -619,14 +628,15 @@ def resolve_value_refs(v):
619628
modified = False
620629

621630
# Replace ${Value.X} with actual values
622-
for prop_name, prop_value in value_obj.items():
623-
if isinstance(prop_value, (str, int, float, bool)):
624-
placeholder = f"${{Value.{prop_name}}}"
625-
if placeholder in sub_val:
626-
sub_val = sub_val.replace(
627-
placeholder, str(prop_value)
628-
)
629-
modified = True
631+
if isdict(value_obj):
632+
for prop_name, prop_value in value_obj.items():
633+
if isinstance(prop_value, (str, int, float, bool)):
634+
placeholder = f"${{Value.{prop_name}}}"
635+
if placeholder in sub_val:
636+
sub_val = sub_val.replace(
637+
placeholder, str(prop_value)
638+
)
639+
modified = True
630640

631641
if modified:
632642
if not is_sub_needed(sub_val):
@@ -635,13 +645,10 @@ def resolve_value_refs(v):
635645
v.d[SUB] = sub_val
636646

637647
# Handle GetAtt to $Value.X
638-
elif isinstance(v.d, dict) and GETATT in v.d:
639-
getatt = v.d[GETATT]
640-
if isinstance(getatt, list) and len(getatt) >= 2:
641-
if getatt[0] == "$Value" and isinstance(value_obj, dict):
642-
prop_name = getatt[1]
643-
if prop_name in value_obj:
644-
v.p[v.k] = value_obj[prop_name]
648+
else:
649+
value = _get_value_from_getatt_reference(v.d, value_obj)
650+
if value is not None:
651+
v.p[v.k] = value
645652

646653
Visitor(copied_module[PROPERTIES]).visit(resolve_value_refs)
647654

awscli/customizations/cloudformation/modules/process.py

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@
6464
is_sub_needed,
6565
)
6666
from awscli.customizations.cloudformation.modules.visitor import Visitor
67-
from awscli.customizations.cloudformation import yamlhelper
67+
from awscli.customizations.cloudformation.yamlhelper import (
68+
yaml_parse,
69+
)
6870
from awscli.customizations.cloudformation.modules.names import (
6971
RESOURCES,
7072
METADATA,
@@ -368,6 +370,9 @@ def __init__(self, template, module_config, parent_module, s3_client=None):
368370
# {"Name": Length}
369371
self.foreach_modules = {}
370372

373+
# Similar to foreeach_modules, for Resources with a ForEach
374+
self.resource_identifiers = {}
375+
371376
def __str__(self):
372377
"Print out a string with module details for logs"
373378
return (
@@ -385,7 +390,7 @@ def process(self):
385390
content, lines = read_source(self.source, self.s3_client)
386391
self.lines = lines
387392

388-
module_dict = yamlhelper.yaml_parse(content)
393+
module_dict = yaml_parse(content)
389394
self.original_module_dict = copy.deepcopy(module_dict)
390395
return self.process_content(module_dict)
391396

@@ -422,6 +427,7 @@ def process_content(self, module_dict):
422427
# The module may only have sub modules in the Modules section
423428
self.resources = {}
424429
else:
430+
# Store the resources first
425431
self.resources = module_dict[RESOURCES]
426432

427433
if PARAMETERS in module_dict:
@@ -467,6 +473,15 @@ def process_content(self, module_dict):
467473
self.validate_overrides()
468474

469475
if not self.invoked:
476+
# Process ForEach in resources before emitting them to the parent template
477+
if RESOURCES in module_dict:
478+
self.resource_identifiers = process_foreach(
479+
module_dict, self.parent_module
480+
)
481+
482+
# Update resources after ForEach processing
483+
self.resources = module_dict[RESOURCES]
484+
470485
# Process resources and put them into the parent template
471486
for logical_id, resource in self.resources.items():
472487
self.process_resource(logical_id, resource)
@@ -690,8 +705,6 @@ def resolve_output_getatt(self, v, d, n):
690705
else:
691706
foreach_modules = self.foreach_modules
692707

693-
print("resolve_output_getatt foreach_modules:", foreach_modules)
694-
695708
name = v[0]
696709
prop_name = v[1]
697710

@@ -750,6 +763,57 @@ def resolve_output_getatt(self, v, d, n):
750763
if reffed_prop is None:
751764
return False
752765

766+
# Handle the case where the output value is a GetAtt to a resource with ForEach
767+
if isinstance(reffed_prop, dict) and GETATT in reffed_prop:
768+
getatt_ref = reffed_prop[GETATT]
769+
if isinstance(getatt_ref, list) and len(getatt_ref) >= 2:
770+
resource_name = getatt_ref[0]
771+
property_path = getatt_ref[1]
772+
773+
# Check if this is a reference to a resource with ForEach using [*] syntax
774+
if "[*]" in resource_name:
775+
base_name = resource_name.split("[")[0]
776+
# Check if we have resource identifiers from ForEach processing
777+
if (
778+
hasattr(self, "resource_identifiers")
779+
and base_name in self.resource_identifiers
780+
):
781+
# Create a list of GetAtt references to each copied resource
782+
resource_list = []
783+
for i, _ in enumerate(
784+
self.resource_identifiers[base_name]
785+
):
786+
resource_list.append(
787+
{
788+
GETATT: [
789+
f"{self.name}{base_name}{i}",
790+
property_path,
791+
]
792+
}
793+
)
794+
d[n] = resource_list
795+
return True
796+
797+
# Check if this is a reference to a specific instance using [identifier] syntax
798+
elif "[" in resource_name and "]" in resource_name:
799+
base_name = resource_name.split("[")[0]
800+
identifier = resource_name.split("[")[1].split("]")[0]
801+
802+
if (
803+
hasattr(self, "resource_identifiers")
804+
and base_name in self.resource_identifiers
805+
):
806+
keys = self.resource_identifiers[base_name]
807+
if identifier in keys:
808+
index = keys.index(identifier)
809+
d[n] = {
810+
GETATT: [
811+
f"{self.name}{base_name}{index}",
812+
property_path,
813+
]
814+
}
815+
return True
816+
753817
if isinstance(reffed_prop, list):
754818
for i, r in enumerate(reffed_prop):
755819
self.replace_reffed_prop(r, reffed_prop, i)

tests/unit/customizations/cloudformation/modules/module-resource-foreach-module.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Resources:
1313
BucketName: !Sub "${BucketPrefix}-bucket-${Identifier}"
1414
Tags:
1515
- Key: Environment
16-
Value: !Ref Identifier
16+
Value: !Sub ${Identifier}
1717

1818
Outputs:
1919
BucketArns:

0 commit comments

Comments
 (0)