Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion samcli/commands/deploy/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,13 @@ def do_cli(
# after we figure out how to enable resolve-images-repos in package
if resolve_image_repos:
image_repositories = sync_ecr_stack(
template_file, stack_name, region, s3_bucket, s3_prefix, image_repositories
template_file,
stack_name,
region,
s3_bucket,
s3_prefix,
image_repositories,
language_extensions_enabled=language_extensions_enabled,
)

with osutils.tempfile_platform_independent() as output_template_file:
Expand Down
8 changes: 7 additions & 1 deletion samcli/commands/deploy/guided_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,13 @@ def guided_prompts(self, parameter_override_keys):

image_repositories = (
sync_ecr_stack(
self.template_file, stack_name, region, managed_s3_bucket, self.s3_prefix, self.image_repositories
self.template_file,
stack_name,
region,
managed_s3_bucket,
self.s3_prefix,
self.image_repositories,
language_extensions_enabled=self._language_extensions_enabled,
)
if self.resolve_image_repositories
else self.prompt_image_repository(
Expand Down
22 changes: 17 additions & 5 deletions samcli/commands/package/package_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,13 @@ def run(self):
stack_name = f"sam-app-{template_basename}"

self.image_repositories = sync_ecr_stack(
self.template_file, stack_name, self.region, self.s3_bucket, self.s3_prefix, self.image_repositories
self.template_file,
stack_name,
self.region,
self.s3_bucket,
self.s3_prefix,
self.image_repositories,
language_extensions_enabled=self._language_extensions_enabled,
)

stacks, _ = SamLocalStackProvider.get_stacks(
Expand Down Expand Up @@ -269,17 +275,23 @@ def _export_with_language_extensions(self, template_path, original_template_dict
return exported_template

LOG.debug("Template uses language extensions, preserving Fn::ForEach structure")
deferred_dynamic: List = []
output_template = merge_language_extensions_s3_uris(
result.original_template, exported_template, result.dynamic_artifact_properties
result.original_template,
exported_template,
result.dynamic_artifact_properties,
parameter_values=parameter_values,
deferred_dynamic=deferred_dynamic,
)
if result.dynamic_artifact_properties:
all_dynamic_properties = list(result.dynamic_artifact_properties or []) + deferred_dynamic
if all_dynamic_properties:
LOG.debug(
"Generating Mappings for %d dynamic artifact properties",
len(result.dynamic_artifact_properties),
len(all_dynamic_properties),
)
output_template = generate_and_apply_artifact_mappings(
output_template,
result.dynamic_artifact_properties,
all_dynamic_properties,
exported_template.get("Resources", {}),
template_dir,
)
Expand Down
12 changes: 10 additions & 2 deletions samcli/lib/bootstrap/companion_stack/companion_stack_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,13 @@ def is_repo_uri(self, repo_uri: Optional[str], function_logical_id: str) -> bool


def sync_ecr_stack(
template_file: str, stack_name: str, region: str, s3_bucket: str, s3_prefix: str, image_repositories: Dict[str, str]
template_file: str,
stack_name: str,
region: str,
s3_bucket: str,
s3_prefix: str,
image_repositories: Dict[str, str],
language_extensions_enabled: bool = False,
) -> Dict[str, str]:
"""Blocking call to sync local functions with ECR Companion Stack

Expand All @@ -297,6 +303,8 @@ def sync_ecr_stack(
S3 prefix for the bucket
image_repositories : Dict[str, str]
Mapping between function logical ID and ECR URI
language_extensions_enabled : bool
Whether AWS::LanguageExtensions transform processing (e.g. Fn::ForEach expansion) is enabled

Returns
-------
Expand All @@ -307,7 +315,7 @@ def sync_ecr_stack(
image_repositories = image_repositories.copy() if image_repositories else {}
manager = CompanionStackManager(stack_name, region, s3_bucket, s3_prefix)

stacks = SamLocalStackProvider.get_stacks(template_file, language_extensions_enabled=False)[0]
stacks = SamLocalStackProvider.get_stacks(template_file, language_extensions_enabled=language_extensions_enabled)[0]
function_provider = SamFunctionProvider(stacks, ignore_code_extraction_warnings=True)
function_logical_ids = [
function.full_path for function in function_provider.get_all() if function.packagetype == IMAGE
Expand Down
10 changes: 7 additions & 3 deletions samcli/lib/package/artifact_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,21 +375,25 @@ def _do_export_with_language_extensions(

exported_template = template.export()

deferred_dynamic: List = []
exported_template_dict = merge_language_extensions_s3_uris(
result.original_template,
exported_template,
result.dynamic_artifact_properties,
parameter_values=parameter_values,
deferred_dynamic=deferred_dynamic,
)

if result.dynamic_artifact_properties:
all_dynamic_properties = list(result.dynamic_artifact_properties or []) + deferred_dynamic
if all_dynamic_properties:
LOG.debug(
"Generating Mappings for %d dynamic artifact properties in child template",
len(result.dynamic_artifact_properties),
len(all_dynamic_properties),
)
exported_resources = exported_template.get("Resources", {})
exported_template_dict = generate_and_apply_artifact_mappings(
exported_template_dict,
result.dynamic_artifact_properties,
all_dynamic_properties,
exported_resources,
child_template_dir,
)
Expand Down
141 changes: 133 additions & 8 deletions samcli/lib/package/language_extensions_packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
)
from samcli.lib.cfn_language_extensions.sam_integration import (
contains_loop_variable,
resolve_collection,
sanitize_resource_key_for_mapping,
substitute_loop_variable,
)
Expand All @@ -44,6 +45,8 @@ def merge_language_extensions_s3_uris(
original_template: Dict[str, Any],
exported_template: Dict[str, Any],
dynamic_properties: Optional[List[DynamicArtifactProperty]] = None,
parameter_values: Optional[Dict[str, Any]] = None,
deferred_dynamic: Optional[List] = None,
) -> Dict[str, Any]:
"""
Update the original template (with Fn::ForEach intact) with S3 URIs from the exported template.
Expand Down Expand Up @@ -81,7 +84,14 @@ def merge_language_extensions_s3_uris(
original_resources = result.get("Resources", {})
exported_resources = exported_template.get("Resources", {})

_update_resources_with_s3_uris(original_resources, exported_resources, dynamic_prop_keys)
_update_resources_with_s3_uris(
original_resources,
exported_resources,
dynamic_prop_keys,
template=original_template,
parameter_values=parameter_values,
deferred_dynamic=deferred_dynamic,
)

_merge_metadata(result.get("Metadata", {}), exported_template.get("Metadata", {}))

Expand Down Expand Up @@ -208,6 +218,9 @@ def _update_resources_with_s3_uris(
original_resources: Dict[str, Any],
exported_resources: Dict[str, Any],
dynamic_prop_keys: Optional[set] = None,
template: Optional[Dict[str, Any]] = None,
parameter_values: Optional[Dict[str, Any]] = None,
deferred_dynamic: Optional[List] = None,
) -> None:
"""
Update resources in the original template with S3 URIs from the exported template.
Expand All @@ -216,7 +229,15 @@ def _update_resources_with_s3_uris(
"""
for resource_key, resource_value in original_resources.items():
if is_foreach_key(resource_key):
_update_foreach_with_s3_uris(resource_key, resource_value, exported_resources, dynamic_prop_keys)
_update_foreach_with_s3_uris(
resource_key,
resource_value,
exported_resources,
dynamic_prop_keys,
template=template,
parameter_values=parameter_values,
deferred_dynamic=deferred_dynamic,
)
elif isinstance(resource_value, dict) and resource_key in exported_resources:
exported_resource = exported_resources.get(resource_key, {})
_copy_artifact_uris(resource_value, exported_resource)
Expand All @@ -228,6 +249,9 @@ def _update_foreach_with_s3_uris(
exported_resources: Dict[str, Any],
dynamic_prop_keys: Optional[set] = None,
outer_context: Optional[List[Tuple[str, List[str]]]] = None,
template: Optional[Dict[str, Any]] = None,
parameter_values: Optional[Dict[str, Any]] = None,
deferred_dynamic: Optional[List] = None,
) -> None:
"""
Update artifact URIs in a Fn::ForEach construct.
Expand All @@ -245,9 +269,18 @@ def _update_foreach_with_s3_uris(
if not isinstance(loop_variable, str) or not isinstance(body, dict):
return

collection_values: List[str] = []
if isinstance(collection, list):
collection_values = [str(item) for item in collection if item is not None]
collection_values = resolve_collection(collection, template or {}, parameter_values)

# Detect whether the collection is a parameter reference so deferred artifacts
# carry the metadata that warn_parameter_based_collections uses to advise
# re-packaging (mirrors detect_foreach_dynamic_properties in sam_integration.py).
collection_is_parameter_ref = False
collection_parameter_name: Optional[str] = None
if isinstance(collection, dict) and "Ref" in collection:
param_name = collection["Ref"]
if param_name in (template or {}).get("Parameters", {}):
collection_is_parameter_ref = True
collection_parameter_name = param_name

if outer_context is None:
outer_context = []
Expand All @@ -261,6 +294,9 @@ def _update_foreach_with_s3_uris(
exported_resources,
dynamic_prop_keys,
outer_context=current_outer_context,
template=template,
parameter_values=parameter_values,
deferred_dynamic=deferred_dynamic,
)
continue

Expand All @@ -281,13 +317,102 @@ def _update_foreach_with_s3_uris(
exported_resource = exported_resources[expanded_key]
if not isinstance(exported_resource, dict):
continue
exported_props = exported_resource.get("Properties", {})

_copy_artifact_uris_for_type(
properties, exported_props, resource_template.get("Type", ""), foreach_key, dynamic_prop_keys
resource_type = resource_template.get("Type", "")
_merge_or_defer_foreach_artifacts(
properties=properties,
resource_type=resource_type,
resource_template_key=resource_template_key,
foreach_key=foreach_key,
loop_variable=loop_variable,
collection_values=collection_values,
outer_context=outer_context,
exported_resources=exported_resources,
dynamic_prop_keys=dynamic_prop_keys,
deferred_dynamic=deferred_dynamic,
collection_is_parameter_ref=collection_is_parameter_ref,
collection_parameter_name=collection_parameter_name,
)


def _merge_or_defer_foreach_artifacts(
properties: Dict[str, Any],
resource_type: str,
resource_template_key: str,
foreach_key: str,
loop_variable: str,
collection_values: List[str],
outer_context: Optional[List[Tuple[str, List[str]]]],
exported_resources: Dict[str, Any],
dynamic_prop_keys: Optional[set],
deferred_dynamic: Optional[List],
collection_is_parameter_ref: bool = False,
collection_parameter_name: Optional[str] = None,
) -> None:
"""Decide, per artifact property, whether all Fn::ForEach iterations resolved
to the same exported URI (static copy) or to distinct URIs (defer to Mappings).

- All iterations identical -> copy the shared raw value onto the ForEach body.
- Iterations differ -> append a synthetic DynamicArtifactProperty to
``deferred_dynamic`` (handled later by generate_and_apply_artifact_mappings)
and leave the body value untouched.
- No accumulator / nested loop -> fall back to copying the first resolved
iteration's value (legacy behavior); nested ForEach with static differing
values is a documented limitation.
"""
prop_names = PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES.get(resource_type)
if not prop_names:
return

for prop_name in _resolve_property_paths(prop_names, properties):
# Loop-variable properties are handled by the existing dynamic path.
if dynamic_prop_keys and (foreach_key, prop_name) in dynamic_prop_keys:
continue

# Track each iteration's normalized URI (for the identical-vs-differing
# comparison) paired with the RAW exported value (for copying). The
# comparison uses the normalized string, but the value written back must
# preserve the original shape (e.g. a {S3Bucket, S3Key} object) so the
# resulting CloudFormation stays valid.
resolved: List[Tuple[str, Any]] = []
for value in collection_values:
expanded_key = _build_expanded_key(resource_template_key, loop_variable, [value], outer_context)
if not expanded_key:
continue
uri = _find_artifact_uri_for_resource(exported_resources, expanded_key, resource_type, prop_name)
if uri is None:
continue
exported_resource = exported_resources.get(expanded_key, {})
raw = _get_prop_value(exported_resource.get("Properties", {}), prop_name)
resolved.append((uri, raw))

if not resolved:
continue

distinct_uris = {uri for uri, _ in resolved}
first_raw = resolved[0][1]
if len(distinct_uris) == 1:
_set_prop_value(properties, prop_name, first_raw)
elif deferred_dynamic is not None and not outer_context:
deferred_dynamic.append(
DynamicArtifactProperty(
foreach_key=foreach_key,
loop_name=foreach_key.replace("Fn::ForEach::", ""),
loop_variable=loop_variable,
collection=collection_values,
resource_key=resource_template_key,
resource_type=resource_type,
property_name=prop_name,
property_value=_get_prop_value(properties, prop_name),
outer_loops=[],
collection_is_parameter_ref=collection_is_parameter_ref,
collection_parameter_name=collection_parameter_name,
)
)
else:
_set_prop_value(properties, prop_name, first_raw)


def _build_expanded_key(
resource_template_key: str,
loop_variable: str,
Expand Down
Loading
Loading