Skip to content

Commit a486922

Browse files
authored
fix(cfn-lang-ext): unify packageable resource properties (aws#9005) (aws#9009)
* feat(cfn-lang-ext): add jmespath-aware property get/set helpers * refactor(cfn-lang-ext): use jmespath for artifact property copy * refactor(cfn-lang-ext): use jmespath for build_context artifact copy * fix(cfn-lang-ext): derive PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES from canonical dicts (aws#9005) Replace the literal PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES dict with a derivation from RESOURCES_WITH_LOCAL_PATHS and RESOURCES_WITH_IMAGE_COMPONENT in samcli.lib.utils.resources, establishing a single source of truth. This fixes issue aws#9005 where eight resource types were missing from the language-extensions packaging path: - AWS::Serverless::Application - AWS::CloudFormation::StackSet - AWS::ElasticBeanstalk::ApplicationVersion - AWS::AppSync::GraphQLSchema - AWS::AppSync::Resolver - AWS::AppSync::FunctionConfiguration - AWS::CloudFormation::ModuleVersion - AWS::CloudFormation::ResourceVersion The derivation also adds AWS::Glue::Job with dotted-path property Command.ScriptLocation, and adds Code.ImageUri to AWS::Lambda::Function. All 14 new tests pass, confirming the derived dict includes the expected entries and excludes design exclusions (ECR::Repository, ServerlessRepo::Application). Cross-task integration anchors for Glue::Job dotted-path handling now pass. * test(cfn-lang-ext): regression tests for aws#9005 and sibling resource types * style: apply black formatting to new tests * chore: fix lint findings (ruff import order, mypy unreachable guard) * fix(cfn-lang-ext): route Fn::ForEach dynamic-property consumers through jmespath PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES now contains dotted paths like Command.ScriptLocation (Glue) and Code.ImageUri (Lambda image), but four call sites in build_context, sam_integration, and language_extensions_packaging were still doing flat-key dict access: - _update_foreach_artifact_paths - _count_dynamic_properties - _get_artifact_value - detect_foreach_dynamic_properties For dotted entries those reads return None, the early skip kicks in, no SAM* mapping is generated, and the dotted artifact value never propagates back to the Fn::ForEach body. CloudFormation Mapping names and FindInMap third-arg keys also can't contain dots. Switch the four sites to _get_prop_value / _set_prop_value, derive the Mapping name and FindInMap third-arg from the leaf segment via a new _leaf_prop_name helper, and add _resolve_property_paths to skip parent paths when a more specific dotted child is present (so Lambda::Function "Code" doesn't shadow "Code.ImageUri" for image functions). Adds 6 regression tests covering Fn::ForEach over Glue::Job with templated Command.ScriptLocation and Lambda::Function with templated Code.ImageUri at both detection and Mapping-generation layers. Addresses review feedback on PR aws#9009. * docs(cfn-lang-ext): expand CloudFormation Language Extensions reference Adds documentation for behaviors that are now supported / verified by this PR's coverage: - Full table of recognized dynamic artifact properties (derived from the canonical packageable-resource list rather than a hand-maintained one), including AWS::Serverless::Application, AWS::CloudFormation::StackSet, AppSync, ElasticBeanstalk, Glue::Job, Lambda::Function image package - Note about how dotted property paths (Command.ScriptLocation, Code.ImageUri) are addressed on the resource vs. used as Mapping identifiers - Multiple resources per ForEach body, Mapping name collision resolution - ForEach in Outputs; Conditions and DependsOn on emitted resources - Validation errors (ForEach element count, nesting limit, missing parameter, empty collection) * docs(cfn-lang-ext): correct validation errors table to match code The previous table paraphrased messages and listed an error that doesn't exist: - Layout error: actual message is "Fn::ForEach::<key> layout is incorrect" and covers more cases than just element count (non-list value, non-string identifier, etc.). - Nesting depth: actual message is the multi-line "Fn::ForEach nesting depth of <N> exceeds the maximum allowed depth of 5. CloudFormation supports up to 5 nested Fn::ForEach loops." - Missing parameter referenced by Fn::ForEach: there is no such error. In partial mode (typical sam build/package), the unresolved Ref is preserved and CFN rejects it at deploy time. - Empty collection: no message; silently skipped (kept as-is). * fix(cfn-lang-ext): derive Mapping-prefix list and artifact-property set from canonical dict Two consumers of PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES were left out of sync when this PR began emitting Mappings for newly-supported resource types (Application, Glue, EB, AppSync, CFN Module/ResourceVersion) and for dotted-leaf entries (Code.ImageUri): - samcli/lib/cfn_language_extensions/utils.py — _SAM_GENERATED_MAPPING_PREFIXES was a hand-maintained tuple covering only the pre-PR set, so is_sam_generated_mapping returned False for SAMLocation*, SAMScriptLocation*, SAMSourceBundle*, SAMModulePackage*, SAMSchemaHandlerPackage*. That made _update_sam_mappings_relative_paths skip these mappings on template move (paths weren't adjusted) and prevented _create_deploy_error from wrapping CFN "key not found" errors as MissingMappingKeyError. - samcli/commands/_utils/template.py — _ARTIFACT_PATH_PROPERTIES was built from the dotted form ("Command.ScriptLocation") but the SAM-generated Mapping value-dicts use the leaf ("ScriptLocation") as the key. So the inner property check rejected leaf names and Glue script paths never got their relative paths adjusted on template move. Both lists now derive from PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES at module import (mirroring the single-source-of-truth approach used for the dict itself in models.py). Adds two sync tests so future drift fails loudly. Addresses second review comment on PR aws#9009. * fix(cfn-lang-ext): key Mapping-name collision detection on the leaf, not the dotted path The Mapping name now uses only the leaf segment (SAM<leaf><Loop>), but the collision counter was still keyed on the dotted property_name. So two resources whose dotted paths share a leaf — e.g. AWS::Serverless::Function "ImageUri" and AWS::Lambda::Function "Code.ImageUri" — would land in different collision-group buckets (each count=1), neither gets the resource-key suffix, and both produce the identical Mapping name. The second resource's Mapping entries silently overwrite the first's at deploy time — the exact silent-corruption shape aws#9005 was meant to eliminate. Fix: re-key the collision counter and the lookup by the leaf in both language_extensions_packaging._generate_artifact_mappings / _compute_mapping_name and build_context._count_dynamic_properties / _update_foreach_artifact_paths. Adds two regression tests that put a Serverless::Function (ImageUri) and a Lambda::Function (Code.ImageUri) in the same Fn::ForEach body and assert that two distinct SAMImageUri<Loop>* Mappings are produced and neither resource's entries are lost. Addresses third review comment on PR aws#9009.
1 parent 9006343 commit a486922

13 files changed

Lines changed: 1044 additions & 71 deletions

File tree

docs/cfn-language-extensions.md

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,37 @@ Running `sam build` expands this into `UsersFunction`, `OrdersFunction`, and `Pr
4141

4242
### Dynamic artifact properties
4343

44-
When a packageable property (like `CodeUri`, `ContentUri`, `ImageUri`) uses a loop variable (e.g., `./services/${Name}`), SAM CLI generates a CloudFormation `Mappings` section that maps each collection value to its S3 URI. The `Fn::ForEach` body is rewritten to use `Fn::FindInMap` so CloudFormation can resolve the correct artifact at deploy time.
44+
When a packageable property uses a loop variable (e.g., `./services/${Name}`), SAM CLI generates a CloudFormation `Mappings` section that maps each collection value to its S3 URI. The `Fn::ForEach` body is rewritten to use `Fn::FindInMap` so CloudFormation can resolve the correct artifact at deploy time.
45+
46+
The set of recognized artifact properties is derived from the same canonical list `sam package` already uses (`RESOURCES_WITH_LOCAL_PATHS` and `RESOURCES_WITH_IMAGE_COMPONENT` in `samcli/lib/utils/resources.py`), so every resource type whose artifact property `sam package` would normally rewrite is supported here too. That includes:
47+
48+
| Resource type | Property |
49+
|---------------|----------|
50+
| `AWS::Serverless::Function` | `CodeUri`, `ImageUri` |
51+
| `AWS::Serverless::LayerVersion` | `ContentUri` |
52+
| `AWS::Serverless::Api` | `DefinitionUri` |
53+
| `AWS::Serverless::HttpApi` | `DefinitionUri` |
54+
| `AWS::Serverless::StateMachine` | `DefinitionUri` |
55+
| `AWS::Serverless::GraphQLApi` | `SchemaUri`, `CodeUri` |
56+
| `AWS::Serverless::Application` | `Location` |
57+
| `AWS::Lambda::Function` | `Code`, `Code.ImageUri` |
58+
| `AWS::Lambda::LayerVersion` | `Content` |
59+
| `AWS::ApiGateway::RestApi` | `BodyS3Location` |
60+
| `AWS::ApiGatewayV2::Api` | `BodyS3Location` |
61+
| `AWS::AppSync::GraphQLSchema` | `DefinitionS3Location` |
62+
| `AWS::AppSync::Resolver` | `RequestMappingTemplateS3Location`, `ResponseMappingTemplateS3Location`, `CodeS3Location` |
63+
| `AWS::AppSync::FunctionConfiguration` | `RequestMappingTemplateS3Location`, `ResponseMappingTemplateS3Location`, `CodeS3Location` |
64+
| `AWS::StepFunctions::StateMachine` | `DefinitionS3Location` |
65+
| `AWS::ElasticBeanstalk::ApplicationVersion` | `SourceBundle` |
66+
| `AWS::Glue::Job` | `Command.ScriptLocation` |
67+
| `AWS::CloudFormation::Stack` | `TemplateURL` |
68+
| `AWS::CloudFormation::StackSet` | `TemplateURL` |
69+
| `AWS::CloudFormation::ModuleVersion` | `ModulePackage` |
70+
| `AWS::CloudFormation::ResourceVersion` | `SchemaHandlerPackage` |
71+
72+
When a property is dotted (e.g. `Command.ScriptLocation` on `AWS::Glue::Job` or `Code.ImageUri` on `AWS::Lambda::Function`), SAM CLI reads and writes the value at the dotted location on the resource — so it lands at `Properties.Command.ScriptLocation` rather than at a literal `Properties["Command.ScriptLocation"]` key — and uses only the leaf segment when it needs to construct an alphanumeric identifier (Mapping name suffix or `Fn::FindInMap` third argument).
73+
74+
When the property is loop-templated, the Mapping name is `SAM<LeafProperty><LoopName>` (e.g., `SAMCodeUriServices`, `SAMScriptLocationJobs`). Customer-authored mappings should not start with these `SAM*` prefixes — they are reserved for SAM CLI (see [Limitations](#limitations) below).
4575

4676
For example, after `sam package`:
4777

@@ -67,6 +97,45 @@ Resources:
6797
CodeUri: !FindInMap [SAMCodeUriServices, !Ref Name, CodeUri]
6898
```
6999

100+
### Multiple resources per ForEach body
101+
102+
A single `Fn::ForEach` body can emit more than one resource per iteration. Each resource is generated for every collection value:
103+
104+
```yaml
105+
Resources:
106+
Fn::ForEach::Tables:
107+
- TableName
108+
- [Users, Orders, Products]
109+
- ${TableName}Table:
110+
Type: AWS::DynamoDB::Table
111+
Properties:
112+
TableName: !Sub "${AWS::StackName}-${TableName}"
113+
# ...
114+
115+
${TableName}StreamProcessor:
116+
Type: AWS::Serverless::Function
117+
Properties:
118+
CodeUri: stream-processors/${TableName}/
119+
Events:
120+
DDBStream:
121+
Type: DynamoDB
122+
Properties:
123+
Stream: !GetAtt
124+
- !Sub "${TableName}Table"
125+
- StreamArn
126+
```
127+
128+
### Mapping name collision resolution
129+
130+
When two resources in the same `Fn::ForEach` body declare the same dynamic artifact property (for example, both an `Api` and a `StateMachine` use `DefinitionUri`), SAM CLI appends a sanitized suffix derived from the resource logical-ID template to keep Mapping names unique:
131+
132+
| Resource template | Property | Mapping name |
133+
|-------------------|----------|--------------|
134+
| `${Svc}Api` | `DefinitionUri` | `SAMDefinitionUriServicesApi` |
135+
| `${Svc}StateMachine` | `DefinitionUri` | `SAMDefinitionUriServicesStateMachine` |
136+
137+
When there is no collision the base name (e.g., `SAMDefinitionUriServices`) is used.
138+
70139
### Parameter-based collections
71140

72141
When the `Fn::ForEach` collection is a parameter reference (`!Ref ServiceNames`), the collection values are resolved at package time from:
@@ -120,6 +189,46 @@ Resources:
120189
STAGE: !Ref Env
121190
```
122191

192+
### ForEach in Outputs
193+
194+
`Fn::ForEach` blocks are also expanded inside the `Outputs` section, so you can emit one output per collection value:
195+
196+
```yaml
197+
Outputs:
198+
Fn::ForEach::FunctionArns:
199+
- Name
200+
- [alpha, beta]
201+
- ${Name}FunctionArn:
202+
Value: !GetAtt
203+
- !Sub "${Name}Function"
204+
- Arn
205+
```
206+
207+
### Conditions and DependsOn
208+
209+
Resources emitted by `Fn::ForEach` can carry `Condition` and `DependsOn` like any other resource. The condition or dependency is replicated onto each generated resource:
210+
211+
```yaml
212+
Conditions:
213+
IsProd: !Equals [!Ref Environment, prod]
214+
215+
Resources:
216+
SharedTable:
217+
Type: AWS::DynamoDB::Table
218+
# ...
219+
220+
Fn::ForEach::Functions:
221+
- Name
222+
- [api, worker]
223+
- ${Name}Function:
224+
Type: AWS::Serverless::Function
225+
Condition: IsProd
226+
DependsOn: SharedTable
227+
Properties:
228+
Handler: main.handler
229+
CodeUri: functions/${Name}/
230+
```
231+
123232
### &{identifier} syntax
124233

125234
The `&{identifier}` syntax strips non-alphanumeric characters from the substituted value, useful for generating valid logical IDs from values like IP addresses:
@@ -154,11 +263,26 @@ The following intrinsic functions are resolved locally during expansion:
154263

155264
Functions that require deployed resources (`Fn::GetAtt`, `Fn::ImportValue`, `Fn::GetAZs`) are preserved for CloudFormation to resolve at deploy time.
156265

266+
## Validation errors
267+
268+
The following template issues are caught locally before the SAM transform runs:
269+
270+
| Cause | Error message |
271+
|-------|---------------|
272+
| The `Fn::ForEach` value is malformed — not a list, doesn't have exactly 3 elements, or has a non-string loop identifier. | `Fn::ForEach::<key> layout is incorrect` (raised as `InvalidTemplateException`; see `samcli/lib/cfn_language_extensions/processors/foreach.py`). |
273+
| More than 5 levels of `Fn::ForEach` are nested. | `Fn::ForEach nesting depth of <N> exceeds the maximum allowed depth of 5. CloudFormation supports up to 5 nested Fn::ForEach loops.` |
274+
| The collection resolves to an empty list (e.g., a `CommaDelimitedList` parameter with `Default: ""`). | No error — the loop is silently skipped and no resources are emitted. |
275+
| The `!Ref` in the collection points at a parameter that is not declared in the template. | No error in the typical `sam build` / `sam package` flow. SAM CLI runs intrinsic resolution in PARTIAL mode and preserves the unresolved `{"Ref": "<name>"}`. CloudFormation will reject the unresolved ref at deploy time. |
276+
157277
## Limitations
158278

159279
- **Collections must be resolvable at build/package time.** `Fn::ForEach` collections that use `Fn::GetAtt`, `Fn::ImportValue`, or SSM/Secrets Manager dynamic references cannot be expanded locally. Use a parameter with `--parameter-overrides` instead.
160280
- **Parameter values are fixed at package time.** If you change `--parameter-overrides` at deploy time without re-packaging, the Mappings won't include entries for new values and deployment will fail.
161281
- **`DeletionPolicy` and `UpdateReplacePolicy`** are validated and resolved during expansion. They support `Ref` to parameters but not other intrinsic functions.
282+
- **Nesting limit.** Up to 5 levels of `Fn::ForEach` may be nested, matching CloudFormation's server-side limit.
283+
- **Reserved Mapping names.** Mapping names starting with any of the following are reserved for SAM CLI — do not author your own mappings with these prefixes:
284+
- `SAMCodeUri`, `SAMImageUri`, `SAMContentUri`, `SAMDefinitionUri`, `SAMSchemaUri`, `SAMBodyS3Location`, `SAMDefinitionS3Location`, `SAMTemplateURL`, `SAMCode`, `SAMContent` — emitted by `sam package` for dynamic artifact properties (see the table above).
285+
- `SAMLayers` — emitted by `sam build` when a `Fn::ForEach`-generated function picks up auto-generated dependency-layer references (Lambda layers SAM CLI builds into a nested stack). This prefix has no corresponding user-authored property; it is added automatically.
162286

163287
## Telemetry
164288

samcli/commands/_utils/template.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,17 @@
3333
# Artifact path property names derived from PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
3434
# so the two lists cannot drift. Used by _update_sam_mappings_relative_paths to
3535
# identify which SAM-generated Mapping values are local paths needing adjustment.
36+
#
37+
# Includes both the dotted form (e.g. "Command.ScriptLocation") and its leaf
38+
# (e.g. "ScriptLocation"), because SAM-generated Mapping value-dicts are keyed
39+
# by the leaf segment so the third argument of Fn::FindInMap stays alphanumeric
40+
# (see _compute_mapping_name and _generate_artifact_mappings in
41+
# samcli/lib/package/language_extensions_packaging.py).
3642
_ARTIFACT_PATH_PROPERTIES = frozenset(
37-
prop for props in PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES.values() for prop in props
43+
name
44+
for props in PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES.values()
45+
for prop in props
46+
for name in (prop, prop.rsplit(".", 1)[-1])
3847
)
3948

4049

samcli/commands/build/build_context.py

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,12 @@ def _update_foreach_artifact_paths(
553553
"""
554554
from samcli.lib.cfn_language_extensions.models import PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
555555
from samcli.lib.cfn_language_extensions.sam_integration import resolve_collection
556+
from samcli.lib.package.language_extensions_packaging import (
557+
_get_prop_value,
558+
_leaf_prop_name,
559+
_resolve_property_paths,
560+
_set_prop_value,
561+
)
556562

557563
generated_mappings: Dict[str, Dict[str, Dict[str, str]]] = {}
558564

@@ -599,11 +605,17 @@ def _update_foreach_artifact_paths(
599605
if not isinstance(properties, dict):
600606
continue
601607

602-
for prop_name in PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES.get(resource_type, []):
603-
prop_value = properties.get(prop_name)
608+
candidate_paths = PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES.get(resource_type, [])
609+
for prop_name in _resolve_property_paths(candidate_paths, properties):
610+
prop_value = _get_prop_value(properties, prop_name)
604611
if prop_value is None:
605612
continue
606613

614+
# CloudFormation Mapping names and FindInMap third-args must be alphanumeric,
615+
# so dotted property names (e.g. "Command.ScriptLocation") use the leaf segment
616+
# for those identifiers while the dotted form is preserved for property addressing.
617+
leaf_name = _leaf_prop_name(prop_name)
618+
607619
if contains_loop_variable(prop_value, loop_variable) and collection_values:
608620
# Determine which outer loop variables the property references
609621
referenced_outer_vars = []
@@ -622,8 +634,12 @@ def _update_foreach_artifact_paths(
622634
referenced_outer_vars=referenced_outer_vars,
623635
)
624636
if mapping_entries:
625-
mapping_name = f"SAM{prop_name}{nesting_path}"
626-
if dynamic_props_count.get(prop_name, 0) > 1:
637+
mapping_name = f"SAM{leaf_name}{nesting_path}"
638+
# Collision count is keyed on the leaf so dotted paths
639+
# that share a leaf (e.g. "ImageUri" vs "Code.ImageUri")
640+
# get the resource-key suffix and don't overwrite each
641+
# other's Mapping entries.
642+
if dynamic_props_count.get(leaf_name, 0) > 1:
627643
suffix = sanitize_resource_key_for_mapping(resource_template_key)
628644
mapping_name = f"{mapping_name}{suffix}"
629645
generated_mappings[mapping_name] = mapping_entries
@@ -637,7 +653,7 @@ def _update_foreach_artifact_paths(
637653
else:
638654
lookup_key = {"Ref": loop_variable}
639655

640-
properties[prop_name] = {"Fn::FindInMap": [mapping_name, lookup_key, prop_name]}
656+
_set_prop_value(properties, prop_name, {"Fn::FindInMap": [mapping_name, lookup_key, leaf_name]})
641657
else:
642658
expanded_key = self._build_expanded_key(
643659
resource_template_key,
@@ -648,7 +664,7 @@ def _update_foreach_artifact_paths(
648664
if expanded_key:
649665
artifact_value = self._get_artifact_value(modified_resources, expanded_key, prop_name)
650666
if artifact_value is not None:
651-
properties[prop_name] = artifact_value
667+
_set_prop_value(properties, prop_name, artifact_value)
652668

653669
# Propagate auto dependency layer references from expanded functions
654670
# to the ForEach body. Each expanded function may have Layers entries
@@ -707,6 +723,11 @@ def _count_dynamic_properties(
707723
share the same property name (e.g., two resources both with DefinitionUri).
708724
"""
709725
from samcli.lib.cfn_language_extensions.models import PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
726+
from samcli.lib.package.language_extensions_packaging import (
727+
_get_prop_value,
728+
_leaf_prop_name,
729+
_resolve_property_paths,
730+
)
710731

711732
count: Counter = Counter()
712733
for rtk, rt in body.items():
@@ -718,10 +739,13 @@ def _count_dynamic_properties(
718739
rprops = rt.get("Properties", {})
719740
if not isinstance(rprops, dict):
720741
continue
721-
for pname in PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES.get(rtype, []):
722-
pval = rprops.get(pname)
742+
candidate_paths = PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES.get(rtype, [])
743+
for pname in _resolve_property_paths(candidate_paths, rprops):
744+
pval = _get_prop_value(rprops, pname)
723745
if pval is not None and contains_loop_variable(pval, loop_variable) and collection_values:
724-
count[pname] += 1
746+
# Count by leaf so collisions across resource types with
747+
# different dotted paths but the same leaf are detected.
748+
count[_leaf_prop_name(pname)] += 1
725749
return count
726750

727751
@staticmethod
@@ -759,8 +783,15 @@ def _collect_dynamic_mapping_entries(
759783
760784
For nested ForEach, enumerates all outer value combinations to find
761785
the fully-expanded resource name.
786+
787+
``prop_name`` may be a jmespath dotted path; the value-dict key stored
788+
in the Mapping uses the leaf segment so it stays alphanumeric and
789+
matches the third argument of the generated Fn::FindInMap.
762790
"""
791+
from samcli.lib.package.language_extensions_packaging import _leaf_prop_name
792+
763793
mapping_entries: Dict[str, Dict[str, str]] = {}
794+
leaf_name = _leaf_prop_name(prop_name)
764795

765796
for coll_value in collection_values:
766797
if outer_context:
@@ -778,7 +809,7 @@ def _collect_dynamic_mapping_entries(
778809
expanded_key = substitute_loop_variable(resource_template_key, loop_variable, coll_value)
779810
artifact_value = self._get_artifact_value(modified_resources, expanded_key, prop_name)
780811
if artifact_value is not None:
781-
mapping_entries[coll_value] = {prop_name: artifact_value}
812+
mapping_entries[coll_value] = {leaf_name: artifact_value}
782813

783814
return mapping_entries
784815

@@ -793,9 +824,15 @@ def _collect_nested_mapping_entry(
793824
mapping_entries: Dict[str, Dict[str, str]],
794825
referenced_outer_vars: Optional[List[Tuple[str, List[str]]]] = None,
795826
) -> None:
796-
"""Enumerate outer value combinations to find expanded resource for a nested ForEach."""
827+
"""Enumerate outer value combinations to find expanded resource for a nested ForEach.
828+
829+
``prop_name`` may be a jmespath dotted path; the value-dict key uses the leaf segment.
830+
"""
797831
import itertools
798832

833+
from samcli.lib.package.language_extensions_packaging import _leaf_prop_name
834+
835+
leaf_name = _leaf_prop_name(prop_name)
799836
outer_collections = [oc[1] for oc in outer_context]
800837
outer_vars = [oc[0] for oc in outer_context]
801838

@@ -821,7 +858,7 @@ def _collect_nested_mapping_entry(
821858
mapping_key = coll_value
822859

823860
if mapping_key not in mapping_entries:
824-
mapping_entries[mapping_key] = {prop_name: artifact_value}
861+
mapping_entries[mapping_key] = {leaf_name: artifact_value}
825862

826863
def _collect_foreach_layer_mappings(
827864
self,
@@ -895,14 +932,19 @@ def _extract_nested_stack_layer_output(modified_resources: Dict, expanded_key: s
895932

896933
@staticmethod
897934
def _get_artifact_value(modified_resources: Dict, expanded_key: str, prop_name: str) -> Optional[Any]:
898-
"""Extract an artifact property value from an expanded resource, or return None."""
935+
"""Extract an artifact property value from an expanded resource, or return None.
936+
937+
``prop_name`` may be a jmespath dotted path (e.g. "Command.ScriptLocation").
938+
"""
939+
from samcli.lib.package.language_extensions_packaging import _get_prop_value
940+
899941
modified_resource = modified_resources.get(expanded_key, {})
900942
if not isinstance(modified_resource, dict):
901943
return None
902944
modified_props = modified_resource.get("Properties", {})
903945
if not isinstance(modified_props, dict):
904946
return None
905-
return modified_props.get(prop_name)
947+
return _get_prop_value(modified_props, prop_name)
906948

907949
def _copy_artifact_paths(self, original_resource: Dict, modified_resource: Dict) -> None:
908950
"""
@@ -919,6 +961,7 @@ def _copy_artifact_paths(self, original_resource: Dict, modified_resource: Dict)
919961
The modified resource with updated artifact paths
920962
"""
921963
from samcli.lib.cfn_language_extensions.models import PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
964+
from samcli.lib.package.language_extensions_packaging import _get_prop_value, _set_prop_value
922965

923966
original_props = original_resource.get("Properties", {})
924967
modified_props = modified_resource.get("Properties", {})
@@ -929,8 +972,9 @@ def _copy_artifact_paths(self, original_resource: Dict, modified_resource: Dict)
929972
return
930973

931974
for prop_name in prop_names:
932-
if prop_name in modified_props:
933-
original_props[prop_name] = modified_props[prop_name]
975+
value = _get_prop_value(modified_props, prop_name)
976+
if value is not None:
977+
_set_prop_value(original_props, prop_name, value)
934978

935979
def _gen_success_msg(self, artifacts_dir: str, output_template_path: str, is_default_build_dir: bool) -> str:
936980
"""

0 commit comments

Comments
 (0)