|
13 | 13 | from samtranslator.translator.translator import Translator |
14 | 14 |
|
15 | 15 | from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException |
| 16 | +from samcli.lib.utils.foreach_handler import filter_foreach_constructs |
16 | 17 | from samcli.lib.utils.packagetype import IMAGE, ZIP |
17 | 18 | from samcli.lib.utils.resources import AWS_SERVERLESS_FUNCTION |
18 | 19 | from samcli.yamlhelper import yaml_dump |
@@ -82,19 +83,33 @@ def get_translated_template_if_valid(self): |
82 | 83 | self._replace_local_codeuri() |
83 | 84 | self._replace_local_image() |
84 | 85 |
|
| 86 | + # Filter out Fn::ForEach constructs before translation |
| 87 | + # CloudFormation will handle these server-side |
| 88 | + template_to_translate, foreach_constructs = filter_foreach_constructs(self.sam_template) |
| 89 | + |
85 | 90 | try: |
86 | 91 | template = sam_translator.translate( |
87 | | - sam_template=self.sam_template, |
| 92 | + sam_template=template_to_translate, |
88 | 93 | parameter_values=self.parameter_overrides, |
89 | 94 | get_managed_policy_map=self._get_managed_policy_map, |
90 | 95 | ) |
| 96 | + |
| 97 | + # Add back Fn::ForEach constructs after translation |
| 98 | + if foreach_constructs: |
| 99 | + if "Resources" not in template: |
| 100 | + template["Resources"] = {} |
| 101 | + template["Resources"].update(foreach_constructs) |
| 102 | + LOG.debug("Preserved %d Fn::ForEach construct(s) in template", len(foreach_constructs)) |
| 103 | + |
91 | 104 | LOG.debug("Translated template is:\n%s", yaml_dump(template)) |
92 | 105 | return yaml_dump(template) |
93 | 106 | except InvalidDocumentException as e: |
94 | 107 | raise InvalidSamDocumentException( |
95 | 108 | functools.reduce(lambda message, error: message + " " + str(error), e.causes, str(e)) |
96 | 109 | ) from e |
97 | 110 |
|
| 111 | + # Removed: _filter_foreach_constructs() now uses shared utility from samcli.lib.utils.foreach_handler |
| 112 | + |
98 | 113 | @functools.lru_cache(maxsize=None) |
99 | 114 | def _get_managed_policy_map(self) -> Dict[str, str]: |
100 | 115 | """ |
@@ -130,7 +145,11 @@ def _replace_local_codeuri(self): |
130 | 145 | ): |
131 | 146 | SamTemplateValidator._update_to_s3_uri("CodeUri", properties) |
132 | 147 |
|
133 | | - for _, resource in all_resources.items(): |
| 148 | + for resource_id, resource in all_resources.items(): |
| 149 | + # Skip Fn::ForEach constructs which are lists, not dicts |
| 150 | + if resource_id.startswith("Fn::ForEach::") or not isinstance(resource, dict): |
| 151 | + continue |
| 152 | + |
134 | 153 | resource_type = resource.get("Type") |
135 | 154 | resource_dict = resource.get("Properties", {}) |
136 | 155 |
|
@@ -158,7 +177,11 @@ def _replace_local_image(self): |
158 | 177 | This ensures sam validate works without having to package the app or use ImageUri. |
159 | 178 | """ |
160 | 179 | resources = self.sam_template.get("Resources", {}) |
161 | | - for _, resource in resources.items(): |
| 180 | + for resource_id, resource in resources.items(): |
| 181 | + # Skip Fn::ForEach constructs which are lists, not dicts |
| 182 | + if resource_id.startswith("Fn::ForEach::") or not isinstance(resource, dict): |
| 183 | + continue |
| 184 | + |
162 | 185 | resource_type = resource.get("Type") |
163 | 186 | properties = resource.get("Properties", {}) |
164 | 187 |
|
|
0 commit comments