Skip to content

feat: Add CloudFormation Language Extensions support (Fn::ForEach)#8637

Open
bnusunny wants to merge 13 commits intodevelopfrom
feat-language-extension
Open

feat: Add CloudFormation Language Extensions support (Fn::ForEach)#8637
bnusunny wants to merge 13 commits intodevelopfrom
feat-language-extension

Conversation

@bnusunny
Copy link
Contributor

@bnusunny bnusunny commented Feb 9, 2026

Description

This PR adds support for CloudFormation Language Extensions in SAM CLI, addressing GitHub issue #5647.

Features

  • Fn::ForEach - Iterate over collections to generate resources
  • Fn::Length - Get the length of an array
  • Fn::ToJsonString - Convert objects to JSON strings
  • Fn::FindInMap with DefaultValue - Map lookups with fallback values
  • Conditional DeletionPolicy/UpdateReplacePolicy - Use intrinsic functions like Fn::If in resource policies

Key Design Decisions

  1. In-Memory Expansion Only - Templates are expanded in memory for SAM CLI operations, but the original unexpanded template is preserved for CloudFormation deployment
  2. Dynamic Artifact Properties via Mappings - Fn::ForEach blocks with dynamic artifact properties (e.g., CodeUri: ./src/${Name}) are supported via a Mappings transformation
  3. Locally Resolvable Collections Only - Fn::ForEach collections must be resolvable locally; cloud-dependent values (Fn::GetAtt, Fn::ImportValue) are not supported with clear error messages

Supported Commands

  • sam build - Builds all expanded functions, preserves original template
  • sam package - Preserves Fn::ForEach structure with S3 URIs
  • sam deploy - Uploads original template for CloudFormation to process
  • sam validate - Validates language extension syntax
  • sam local invoke - Invokes expanded functions by name
  • sam local start-api - Serves ForEach-generated API endpoints
  • sam local start-lambda - Serves all expanded functions

Example

Transform:
  - AWS::LanguageExtensions
  - AWS::Serverless-2016-10-31

Resources:
  Fn::ForEach::Functions:
    - Name
    - [Alpha, Beta, Gamma]
    - ${Name}Function:
        Type: AWS::Serverless::Function
        Properties:
          Handler: ${Name}.handler
          CodeUri: ./src
          Runtime: python3.9

Resolves #5647

Testing

  • Comprehensive unit tests for the language extensions engine
  • Integration tests for all supported commands
  • Test templates covering static/dynamic CodeUri, nested stacks, parameter collections

Checklist

  • Unit tests added
  • Integration tests added
  • Documentation in code comments
  • Error messages include actionable workarounds

@bnusunny bnusunny requested a review from a team as a code owner February 9, 2026 00:16
@github-actions github-actions bot added area/package sam package command area/deploy sam deploy command area/build sam build command pr/internal labels Feb 9, 2026
@bnusunny bnusunny force-pushed the feat-language-extension branch from 0be94d0 to 5d6cbf3 Compare February 9, 2026 00:33
@bnusunny
Copy link
Contributor Author

bnusunny commented Feb 9, 2026

@bnusunny bnusunny force-pushed the feat-language-extension branch 7 times, most recently from e68efa0 to 4ed8396 Compare February 13, 2026 02:55
@bnusunny bnusunny force-pushed the feat-language-extension branch 15 times, most recently from 5324dad to 707baad Compare February 18, 2026 23:59
@bnusunny bnusunny force-pushed the feat-language-extension branch from 9baaa0d to d322ba2 Compare March 10, 2026 04:23
@bnusunny bnusunny requested a review from reedham-aws March 10, 2026 04:52
@bnusunny bnusunny force-pushed the feat-language-extension branch 5 times, most recently from b2a44b9 to 6713896 Compare March 12, 2026 04:52
@bnusunny
Copy link
Contributor Author

@bnusunny bnusunny force-pushed the feat-language-extension branch from 9b224a7 to e3ff752 Compare March 18, 2026 22:01
bnusunny added 13 commits March 18, 2026 22:23
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}/.
- Move inline imports to top level in sam_stack_provider.py and test_template.py
- Add missing assertion in test_handles_empty_mappings
- Uncomment Fn::ForEach::Topics block to test non-Lambda resource types
- Update mock patch paths to match top-level import locations
@bnusunny bnusunny force-pushed the feat-language-extension branch from e3ff752 to 75df927 Compare March 18, 2026 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/build sam build command area/deploy sam deploy command area/package sam package command pr/internal

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: Support LanguageExtensions feature Fn::ForEach

3 participants