feat: Add CloudFormation Language Extensions support (Fn::ForEach)#8637
feat: Add CloudFormation Language Extensions support (Fn::ForEach)#8637
Conversation
0be94d0 to
5d6cbf3
Compare
|
Integration test: https://github.com/aws/aws-sam-cli/actions/runs/21808626015 |
e68efa0 to
4ed8396
Compare
5324dad to
707baad
Compare
9baaa0d to
d322ba2
Compare
00160e1 to
b2a44b9
Compare
Implement a local CloudFormation Language Extensions processor supporting: - Fn::ForEach loop expansion in Resources, Conditions, and Outputs - Fn::Length, Fn::ToJsonString intrinsic functions - Fn::FindInMap with DefaultValue support - Conditional DeletionPolicy/UpdateReplacePolicy - Nested ForEach depth validation (max 5 levels) - Partial resolution mode preserving unresolvable references Pipeline architecture: TemplateParsingProcessor -> ForEachProcessor -> IntrinsicResolverProcessor -> DeletionPolicyProcessor -> UpdateReplacePolicyProcessor Includes comprehensive unit tests and CloudFormation compatibility suite.
Wire the language extensions library into SAM CLI with two-phase architecture: - Phase 1: expand_language_extensions() -> LanguageExtensionResult - Phase 2: SamTranslatorWrapper.run_plugins() (SAM transform only) Key components: - expand_language_extensions() canonical entry point - SamTranslatorWrapper receives pre-expanded template (Phase 2 only) - SamLocalStackProvider.get_stacks() calls expand_language_extensions() - SamTemplateValidator calls expand_language_extensions() - DynamicArtifactProperty dataclass for Mappings transformation - Fn::ForEach guards in artifact_exporter, normalizer, cdk/utils
- _get_template_for_output() preserves Fn::ForEach in build output - _update_foreach_artifact_paths() generates Mappings for dynamic artifact properties with per-function build paths - Recursive nested Fn::ForEach support - ForEach-aware path resolution skips Docker image URIs Test templates: static CodeUri, dynamic CodeUri, parameter collections, nested stacks, nested ForEach, dynamic ImageUri, depth validation.
Package:
- _export() calls expand_language_extensions() for Phase 1
- Preserves Fn::ForEach in packaged template with S3 URIs
- Generates Mappings for dynamic artifact properties
- _find_artifact_uri_for_resource() handles all export formats:
string, {S3Bucket,S3Key}, {Bucket,Key}, {ImageUri}
- Recursive nested Fn::ForEach support
- Warning for parameter-based collections
Deploy:
- Uploads original unexpanded template to CloudFormation
- Clear error for missing Mapping keys
Integration tests for CodeUri, ContentUri, DefinitionUri, ImageUri,
BodyS3Location across all packageable resource types.
- sam validate: valid ForEach, invalid syntax, cloud-dependent collections, dynamic CodeUri, nested depth validation (5 valid, 6 invalid) - sam local invoke: expanded function names from ForEach - sam local start-api: ForEach-generated API endpoints
Track CFNLanguageExtensions as a UsedFeature event when templates with AWS::LanguageExtensions transform are expanded. Emitted once per expansion in expand_language_extensions().
Remove redundant and AWS-dependent integration tests, keeping 9 essential tests across build, package, validate, local invoke, and start-api. Delete 34 orphaned testdata directories.
YAML parsing produces Python booleans for bare true/false values, but parameter overrides from --parameter-overrides are always strings. Fn::Equals was using Python == which returns False for 'true' == True. CloudFormation Fn::Equals performs string comparison, so convert both operands to their string representations before comparing. Booleans are lowercased to produce 'true'/'false' matching CFN serialization.
… only Language extension functions are only supported in these three sections per AWS::LanguageExtensions transform documentation. Previously the intrinsic resolver also processed Parameters, Mappings, Metadata, etc.
The name iter_regular_resources better conveys that ForEach blocks are skipped. Removes the backward-compatible alias.
Extract duplicated _to_boolean logic from condition_resolver.py and fn_if.py into IntrinsicFunctionResolver.to_boolean() static method. Replace os.path.isfile() + os.path.getmtime() two-step check with a single try/except around getmtime() to eliminate the race condition.
Remove 9 integration tests whose test data directories were removed in
an earlier commit: validate/language-extensions/, buildcmd/language-
extensions-dynamic-imageuri/, language-extensions-foreach/, and
language-extensions-nested-foreach-{valid,invalid}/.
b2a44b9 to
6713896
Compare
|
Integration test run: https://github.com/aws/aws-sam-cli/actions/runs/22987281510 |
| - Inventory | ||
|
|
||
|
|
||
| # Fn::ForEach::Topics: |
There was a problem hiding this comment.
Should this be uncommented?
| - Env | ||
| - - dev | ||
| - prod | ||
| - Fn::ForEach::Services: |
There was a problem hiding this comment.
Why are we testing 2 nested stacks when I see we are also testing nested stacks that are 6 deep? I feel only the latter is necessary
| Runtime: !Ref Runtime | ||
|
|
||
| Resources: | ||
| Bucket: |
There was a problem hiding this comment.
We are we removing testing with lang extensions with different resources?
|
|
||
| def test_skips_non_sam_mappings(self): | ||
| """Mappings without the SAM prefix should not be modified.""" | ||
| from samcli.commands._utils.template import _update_sam_mappings_relative_paths |
There was a problem hiding this comment.
move to top for all imports in the test methods
|
|
||
| def test_updates_relative_paths_in_sam_mappings(self): | ||
| """SAM-prefixed Mapping values that are relative paths should be adjusted.""" | ||
| from samcli.commands._utils.template import _update_sam_mappings_relative_paths |
|
|
||
| _update_sam_mappings_relative_paths({}, "/original", "/new") | ||
| _update_sam_mappings_relative_paths(None, "/original", "/new") | ||
|
|
There was a problem hiding this comment.
there is no assertion here?
| Resources: | ||
| # Test Fn::ForEach generating multiple Lambda functions with static CodeUri | ||
| # This generates AlphaFunction and BetaFunction, both using the same CodeUri | ||
| Fn::ForEach::Functions: |
There was a problem hiding this comment.
are we testing the other extensions like Fn::Length, Fn::FindInMap etc? Maybe I missed it
Description
This PR adds support for CloudFormation Language Extensions in SAM CLI, addressing GitHub issue #5647.
Features
Fn::Ifin resource policiesKey Design Decisions
Fn::ForEachblocks with dynamic artifact properties (e.g.,CodeUri: ./src/${Name}) are supported via a Mappings transformationFn::ForEachcollections must be resolvable locally; cloud-dependent values (Fn::GetAtt,Fn::ImportValue) are not supported with clear error messagesSupported Commands
sam build- Builds all expanded functions, preserves original templatesam package- PreservesFn::ForEachstructure with S3 URIssam deploy- Uploads original template for CloudFormation to processsam validate- Validates language extension syntaxsam local invoke- Invokes expanded functions by namesam local start-api- Serves ForEach-generated API endpointssam local start-lambda- Serves all expanded functionsExample
Resolves #5647
Testing
Checklist