Skip to content

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

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

feat: Add CloudFormation Language Extensions support (Fn::ForEach)#8637
bnusunny wants to merge 12 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 4 times, most recently 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 4 times, most recently from 00160e1 to b2a44b9 Compare March 11, 2026 22:42
bnusunny added 12 commits March 12, 2026 04:52
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}/.
@bnusunny bnusunny force-pushed the feat-language-extension branch from b2a44b9 to 6713896 Compare March 12, 2026 04:52
@bnusunny
Copy link
Contributor Author

- Inventory


# Fn::ForEach::Topics:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be uncommented?

- Env
- - dev
- prod
- Fn::ForEach::Services:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

@seshubaws seshubaws Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to top


_update_sam_mappings_relative_paths({}, "/original", "/new")
_update_sam_mappings_relative_paths(None, "/original", "/new")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we testing the other extensions like Fn::Length, Fn::FindInMap etc? Maybe I missed it

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