diff --git a/docs/cfn-language-extensions.md b/docs/cfn-language-extensions.md index 8550de7f10c..b6b1f60fcb8 100644 --- a/docs/cfn-language-extensions.md +++ b/docs/cfn-language-extensions.md @@ -4,15 +4,71 @@ SAM CLI now supports templates that use the `AWS::LanguageExtensions` transform, ## How it works -When SAM CLI detects `AWS::LanguageExtensions` in a template's `Transform` section, it expands language extension constructs locally before running SAM transforms. This enables `sam build`, `sam package`, `sam deploy`, `sam sync`, `sam validate`, `sam local invoke`, and `sam local start-api` to work with templates that use these constructs. +When SAM CLI detects `AWS::LanguageExtensions` in a template's `Transform` section *and* the customer has opted in to local processing, it expands language extension constructs locally before running SAM transforms. This enables `sam build`, `sam package`, `sam deploy`, `sam sync`, `sam validate`, `sam local invoke`, `sam local start-api`, and `sam local start-lambda` to work with templates that use these constructs. -The expansion happens in two phases: +**Local processing is off by default.** When off, SAM CLI passes the template through unchanged; CloudFormation processes the `AWS::LanguageExtensions` transform server-side at deploy time (the pre-1.160.0 behavior). + +The expansion happens in two phases when enabled: 1. **Phase 1 (Language Extensions)** — `Fn::ForEach` loops are expanded, intrinsic functions are resolved where possible, and the template is converted to standard CloudFormation. 2. **Phase 2 (SAM Transform)** — The expanded template is processed by the SAM Translator as usual. The original template (with `Fn::ForEach` intact) is preserved for CloudFormation deployment, since CloudFormation processes the `AWS::LanguageExtensions` transform server-side. +## Enabling Language Extensions + +Local processing of `AWS::LanguageExtensions` is opt-in per command. Three equivalent activation methods, in priority order: + +1. **CLI flag** — pass `--language-extensions` on a single invocation: + + ```bash + sam build --language-extensions + sam package --language-extensions ... + sam deploy --language-extensions ... + ``` + + `--no-language-extensions` explicitly disables, overriding the env var below. + +2. **Environment variable** — set `SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1` to enable for the current shell: + + ```bash + export SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1 + sam build + sam local invoke MyFunction + ``` + + Truthy values (case-insensitive): `1`, `true`, `yes`. Anything else, including empty string, is off. + +3. **`samconfig.toml`** — persist the choice per project: + + ```toml + [default.build.parameters] + language_extensions = true + + [default.package.parameters] + language_extensions = true + + [default.deploy.parameters] + language_extensions = true + + [default.sync.parameters] + language_extensions = true + + [default.local_invoke.parameters] + language_extensions = true + + [default.local_start_api.parameters] + language_extensions = true + + [default.local_start_lambda.parameters] + language_extensions = true + + [default.validate.parameters] + language_extensions = true + ``` + +**Each command needs its own activation.** Passing `--language-extensions` to `sam build` does not propagate to a later `sam local invoke` — local processing is decided per command invocation. Use the env var or samconfig entry to enable across commands without repeating the flag. + ## Fn::ForEach `Fn::ForEach` generates multiple resources, conditions, or outputs from a single template definition: @@ -286,4 +342,4 @@ The following template issues are caught locally before the SAM transform runs: ## Telemetry -SAM CLI tracks usage of `AWS::LanguageExtensions` via the `CFNLanguageExtensions` telemetry feature flag. No template content is transmitted. +SAM CLI emits a `CFNLanguageExtensions` telemetry event when a command is invoked with `--language-extensions` (or its env-var equivalent) **and** the template declares the `AWS::LanguageExtensions` transform. The event fires once per invocation; no template content is transmitted. When local processing is off (the default), no event fires. diff --git a/samcli/commands/_utils/options.py b/samcli/commands/_utils/options.py index 5f99578090e..4801f5bab37 100644 --- a/samcli/commands/_utils/options.py +++ b/samcli/commands/_utils/options.py @@ -1074,3 +1074,23 @@ def container_env_var_file_click_option(cls): @parameterized_option def container_env_var_file_option(f, cls): return container_env_var_file_click_option(cls)(f) + + +def language_extensions_click_option(): + return click.option( + "--language-extensions/--no-language-extensions", + "language_extensions", + required=False, + default=None, + is_flag=True, + help=( + "Expand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, " + "Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before " + "running SAM transforms. Off by default. " + "Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1." + ), + ) + + +def language_extensions_option(f): + return language_extensions_click_option()(f) diff --git a/samcli/commands/build/build_context.py b/samcli/commands/build/build_context.py index 1c6355b030c..42aea1378a1 100644 --- a/samcli/commands/build/build_context.py +++ b/samcli/commands/build/build_context.py @@ -38,6 +38,7 @@ from samcli.lib.build.workflow_config import UnsupportedRuntimeException from samcli.lib.cfn_language_extensions.sam_integration import ( contains_loop_variable, + resolve_language_extensions_enabled, sanitize_resource_key_for_mapping, substitute_loop_variable, ) @@ -93,6 +94,7 @@ def __init__( mount_with: str = MountMode.READ.value, mount_symlinks: Optional[bool] = False, use_buildkit: Optional[bool] = False, + language_extensions: Optional[bool] = None, ) -> None: """ Initialize the class @@ -154,6 +156,10 @@ def __init__( Indicates if symlinks should be mounted inside the container use_buildkit Optional[bool]: Enable buildkit for container image builds + language_extensions Optional[bool]: + Tri-state user input from the --language-extensions/--no-language-extensions + flag. None means the user did not pass either form (resolver falls back to + SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS env var). """ self._resource_identifier = resource_identifier @@ -197,6 +203,7 @@ def __init__( self._mount_with = MountMode(mount_with) self._mount_symlinks = mount_symlinks self._use_buildkit = use_buildkit + self._language_extensions_enabled = resolve_language_extensions_enabled(language_extensions) def __enter__(self) -> "BuildContext": self.set_up() @@ -209,6 +216,7 @@ def set_up(self) -> None: self._template_file, parameter_overrides=self._parameter_overrides, global_parameter_overrides=self._global_parameter_overrides, + language_extensions_enabled=self._language_extensions_enabled, ) if remote_stack_full_paths: @@ -1088,6 +1096,10 @@ def cached(self) -> bool: def use_container(self) -> bool: return self._use_container + @property + def language_extensions_enabled(self) -> bool: + return self._language_extensions_enabled + @property def stacks(self) -> List[Stack]: return self._stacks diff --git a/samcli/commands/build/command.py b/samcli/commands/build/command.py index 7a77f19d53c..8ec13607e70 100644 --- a/samcli/commands/build/command.py +++ b/samcli/commands/build/command.py @@ -23,6 +23,7 @@ container_env_var_file_option, docker_common_options, hook_name_click_option, + language_extensions_option, manifest_option, mount_symlinks_option, parameter_override_option, @@ -85,6 +86,7 @@ @use_container_build_option @use_buildkit_option @build_in_source_option +@language_extensions_option @click.option( "--container-env-var", "-e", @@ -164,6 +166,7 @@ def cli( build_in_source: Optional[bool], mount_symlinks: Optional[bool], use_buildkit: Optional[bool], + language_extensions: Optional[bool], ) -> None: """ `sam build` command entry point @@ -197,6 +200,7 @@ def cli( mount_with, mount_symlinks, use_buildkit, + language_extensions, ) # pragma: no cover @@ -225,6 +229,7 @@ def do_cli( # pylint: disable=too-many-locals, too-many-statements mount_with: str, mount_symlinks: Optional[bool], use_buildkit: Optional[bool], + language_extensions: Optional[bool], ) -> None: """ Implementation of the ``cli`` method @@ -266,6 +271,7 @@ def do_cli( # pylint: disable=too-many-locals, too-many-statements mount_with=mount_with, mount_symlinks=mount_symlinks, use_buildkit=use_buildkit, + language_extensions=language_extensions, ) as ctx: ctx.run() diff --git a/samcli/commands/build/core/options.py b/samcli/commands/build/core/options.py index 565fcff841e..4b86570817f 100644 --- a/samcli/commands/build/core/options.py +++ b/samcli/commands/build/core/options.py @@ -38,7 +38,7 @@ "base_dir", ] -TEMPLATE_OPTIONS: List[str] = ["parameter_overrides"] +TEMPLATE_OPTIONS: List[str] = ["parameter_overrides", "language_extensions"] TERRAFORM_HOOK_OPTIONS: List[str] = ["terraform_project_root_path"] diff --git a/samcli/commands/deploy/command.py b/samcli/commands/deploy/command.py index dd750ce5e58..795fb30417a 100644 --- a/samcli/commands/deploy/command.py +++ b/samcli/commands/deploy/command.py @@ -19,6 +19,7 @@ image_repositories_option, image_repository_option, kms_key_id_option, + language_extensions_option, metadata_option, no_progressbar_option, notification_arns_option, @@ -38,6 +39,7 @@ from samcli.commands.deploy.utils import sanitize_parameter_overrides from samcli.lib.bootstrap.bootstrap import manage_stack, print_managed_s3_bucket_info from samcli.lib.bootstrap.companion_stack.companion_stack_manager import sync_ecr_stack +from samcli.lib.cfn_language_extensions.sam_integration import resolve_language_extensions_enabled from samcli.lib.cli_validation.image_repository_validation import image_repository_validation from samcli.lib.telemetry.metric import track_command from samcli.lib.utils import osutils @@ -159,6 +161,7 @@ @signing_profiles_option @no_progressbar_option @capabilities_option +@language_extensions_option @aws_creds_options @common_options @save_params_option @@ -194,6 +197,7 @@ def cli( signing_profiles, resolve_s3, resolve_image_repos, + language_extensions, save_params, config_file, config_env, @@ -233,6 +237,7 @@ def cli( config_file, config_env, resolve_image_repos, + language_extensions, disable_rollback, on_failure, max_wait_duration, @@ -267,6 +272,7 @@ def do_cli( config_file, config_env, resolve_image_repos, + language_extensions, disable_rollback, on_failure, max_wait_duration, @@ -279,6 +285,8 @@ def do_cli( from samcli.commands.deploy.guided_context import GuidedContext from samcli.commands.package.package_context import PackageContext + language_extensions_enabled = resolve_language_extensions_enabled(language_extensions) + if guided: # Allow for a guided deploy to prompt and save those details. guided_context = GuidedContext( @@ -300,6 +308,7 @@ def do_cli( config_env=config_env, config_file=config_file, disable_rollback=disable_rollback, + language_extensions_enabled=language_extensions_enabled, ) guided_context.run() else: @@ -338,6 +347,7 @@ def do_cli( profile=profile, signing_profiles=guided_context.signing_profiles if guided else signing_profiles, parameter_overrides=context_param_overrides, + language_extensions=language_extensions, ) as package_context: package_context.run() @@ -376,5 +386,6 @@ def do_cli( poll_delay=poll_delay, on_failure=on_failure, max_wait_duration=max_wait_duration, + language_extensions=language_extensions, ) as deploy_context: deploy_context.run() diff --git a/samcli/commands/deploy/core/options.py b/samcli/commands/deploy/core/options.py index 526271be659..b650cde1f80 100644 --- a/samcli/commands/deploy/core/options.py +++ b/samcli/commands/deploy/core/options.py @@ -43,6 +43,7 @@ CONFIGURATION_OPTION_NAMES: List[str] = ["config_env", "config_file"] + SAVE_PARAMS_OPTIONS ADDITIONAL_OPTIONS: List[str] = [ + "language_extensions", "no_progressbar", "signing_profiles", "template_file", diff --git a/samcli/commands/deploy/deploy_context.py b/samcli/commands/deploy/deploy_context.py index 33ac1711568..9a0cc640215 100644 --- a/samcli/commands/deploy/deploy_context.py +++ b/samcli/commands/deploy/deploy_context.py @@ -29,6 +29,7 @@ print_deploy_args, sanitize_parameter_overrides, ) +from samcli.lib.cfn_language_extensions.sam_integration import resolve_language_extensions_enabled from samcli.lib.deploy.deployer import Deployer from samcli.lib.deploy.utils import FailureMode from samcli.lib.intrinsic_resolver.intrinsics_symbol_table import IntrinsicsSymbolTable @@ -75,6 +76,7 @@ def __init__( poll_delay, on_failure, max_wait_duration, + language_extensions: Optional[bool] = None, ): self.template_file = template_file self.stack_name = stack_name @@ -99,7 +101,7 @@ def __init__( self.region = region self.profile = profile self.s3_uploader = None - self.deployer = None + self.deployer: Optional[Deployer] = None self.confirm_changeset = confirm_changeset self.signing_profiles = signing_profiles self.use_changeset = use_changeset @@ -108,6 +110,7 @@ def __init__( self.on_failure = FailureMode(on_failure) if on_failure else FailureMode.ROLLBACK self._max_template_size = 51200 self.max_wait_duration = max_wait_duration + self._language_extensions_enabled = resolve_language_extensions_enabled(language_extensions) def __enter__(self): return self @@ -115,6 +118,10 @@ def __enter__(self): def __exit__(self, *args): pass + @property + def language_extensions_enabled(self) -> bool: + return self._language_extensions_enabled + def run(self): """ Execute deployment based on the argument provided by customers and samconfig.toml. @@ -239,6 +246,7 @@ def deploy( self.template_file, parameter_overrides=sanitize_parameter_overrides(self.parameter_overrides), global_parameter_overrides=self.global_parameter_overrides, + language_extensions_enabled=self._language_extensions_enabled, ) auth_required_per_resource = auth_per_resource(stacks) @@ -246,6 +254,7 @@ def deploy( if not authorization_required: click.secho(f"{resource} has no authentication.", fg="yellow") + assert self.deployer is not None if use_changeset: try: result, changeset_type = self.deployer.create_and_wait_for_changeset( @@ -296,7 +305,7 @@ def deploy( role_arn=role_arn, notification_arns=notification_arns, s3_uploader=s3_uploader, - tags=tags, + tags=tags, # type: ignore[arg-type] on_failure=self.on_failure, ) LOG.debug(result) diff --git a/samcli/commands/deploy/guided_context.py b/samcli/commands/deploy/guided_context.py index 697a336c528..1a3335687ae 100644 --- a/samcli/commands/deploy/guided_context.py +++ b/samcli/commands/deploy/guided_context.py @@ -62,6 +62,7 @@ def __init__( config_env=None, config_file=None, disable_rollback=None, + language_extensions_enabled: bool = False, ): self.template_file = template_file self.stack_name = stack_name @@ -93,8 +94,9 @@ def __init__( self.start_bold = "\033[1m" self.end_bold = "\033[0m" self.color = Colored() - self.function_provider = None + self.function_provider: Optional[SamFunctionProvider] = None self.disable_rollback = disable_rollback + self._language_extensions_enabled = language_extensions_enabled @property def guided_capabilities(self): @@ -141,6 +143,7 @@ def guided_prompts(self, parameter_override_keys): self.template_file, parameter_overrides=sanitize_parameter_overrides(input_parameter_overrides), global_parameter_overrides=global_parameter_overrides, + language_extensions_enabled=self._language_extensions_enabled, ) click.secho("\t#Shows you resources changes to be deployed and require a 'Y' to initiate deploy") diff --git a/samcli/commands/local/cli_common/invoke_context.py b/samcli/commands/local/cli_common/invoke_context.py index ef3affcf296..3dd705cb9dd 100644 --- a/samcli/commands/local/cli_common/invoke_context.py +++ b/samcli/commands/local/cli_common/invoke_context.py @@ -17,6 +17,7 @@ from samcli.commands.local.cli_common.user_exceptions import DebugContextException, InvokeContextException from samcli.commands.local.lib.debug_context import DebugContext from samcli.commands.local.lib.local_lambda import LocalLambdaRunner +from samcli.lib.cfn_language_extensions.sam_integration import resolve_language_extensions_enabled from samcli.lib.providers.provider import Function, Stack from samcli.lib.providers.sam_function_provider import RefreshableSamFunctionProvider, SamFunctionProvider from samcli.lib.providers.sam_stack_provider import SamLocalStackProvider @@ -103,6 +104,7 @@ def __init__( no_mem_limit: Optional[bool] = False, container_dns: Optional[Tuple[str]] = None, function_logical_ids: Optional[Tuple[str, ...]] = None, + language_extensions: Optional[bool] = None, ) -> None: """ Initialize the context @@ -225,6 +227,8 @@ def __init__( self._mount_symlinks: Optional[bool] = mount_symlinks self._no_mem_limit = no_mem_limit + self._language_extensions_enabled = resolve_language_extensions_enabled(language_extensions) + # Note(xinhol): despite self._function_provider and self._stacks are initialized as None # they will be assigned with a non-None value in __enter__() and # it is only used in the context (after __enter__ is called) @@ -271,6 +275,9 @@ def __enter__(self) -> "InvokeContext": if self._function_logical_ids: _function_providers_kwargs["function_logical_ids"] = self._function_logical_ids + if self._containers_mode == ContainersMode.WARM: + _function_providers_kwargs["language_extensions_enabled"] = self._language_extensions_enabled + self._function_provider = _function_providers_class[self._containers_mode]( *_function_providers_args[self._containers_mode], **_function_providers_kwargs ) @@ -565,6 +572,15 @@ def function_identifier(self) -> str: "Possible options in your template: {}".format(all_function_full_paths) ) + @property + def language_extensions_enabled(self) -> bool: + """ + Returns whether CloudFormation language extensions are enabled. + + :return bool: True if language extensions should be processed, False otherwise + """ + return self._language_extensions_enabled + @property def lambda_runtime(self) -> LambdaRuntime: if not self._lambda_runtimes: @@ -705,6 +721,7 @@ def _get_stacks(self) -> List[Stack]: self._template_file, parameter_overrides=self._parameter_overrides, global_parameter_overrides=self._global_parameter_overrides, + language_extensions_enabled=self._language_extensions_enabled, ) return stacks except (TemplateNotFoundException, TemplateFailedParsingException) as ex: diff --git a/samcli/commands/local/invoke/cli.py b/samcli/commands/local/invoke/cli.py index 337453cdb62..6a0953d1ce9 100644 --- a/samcli/commands/local/invoke/cli.py +++ b/samcli/commands/local/invoke/cli.py @@ -13,6 +13,7 @@ from samcli.commands._utils.option_value_processor import process_image_options from samcli.commands._utils.options import ( hook_name_click_option, + language_extensions_option, mount_symlinks_option, skip_prepare_infra_option, terraform_plan_file_option, @@ -97,6 +98,7 @@ def get_metavar(self, param): help="Name for the durable execution (for durable functions only).", ) @mount_symlinks_option +@language_extensions_option @invoke_common_options @local_common_options @cli_framework_options @@ -138,6 +140,7 @@ def cli( terraform_plan_file, runtime, mount_symlinks, + language_extensions, no_memory_limit, container_dns, tenant_id, @@ -174,6 +177,7 @@ def cli( hook_name, runtime, mount_symlinks, + language_extensions, no_memory_limit, container_dns, tenant_id, @@ -207,6 +211,7 @@ def do_cli( # pylint: disable=R0914 hook_name, runtime, mount_symlinks, + language_extensions, no_mem_limit, container_dns, tenant_id, @@ -261,6 +266,7 @@ def do_cli( # pylint: disable=R0914 add_host=add_host, invoke_images=processed_invoke_images, mount_symlinks=mount_symlinks, + language_extensions=language_extensions, no_mem_limit=no_mem_limit, container_dns=container_dns, ) as context: diff --git a/samcli/commands/local/invoke/core/options.py b/samcli/commands/local/invoke/core/options.py index f56ba916488..23e160d44ee 100644 --- a/samcli/commands/local/invoke/core/options.py +++ b/samcli/commands/local/invoke/core/options.py @@ -16,6 +16,7 @@ TEMPLATE_OPTIONS: List[str] = [ "parameter_overrides", + "language_extensions", ] INVOKE_OPTIONS: List[str] = [ diff --git a/samcli/commands/local/start_api/cli.py b/samcli/commands/local/start_api/cli.py index 1d2aabea090..86966c2ff3b 100644 --- a/samcli/commands/local/start_api/cli.py +++ b/samcli/commands/local/start_api/cli.py @@ -15,6 +15,7 @@ from samcli.commands._utils.options import ( generate_next_command_recommendation, hook_name_click_option, + language_extensions_option, skip_prepare_infra_option, terraform_plan_file_option, ) @@ -99,6 +100,7 @@ required_param_lists=[["ssl_cert_file"]], help="Path to SSL key file (default: None)", ) +@language_extensions_option @invoke_common_options @warm_containers_common_options @local_common_options @@ -143,6 +145,7 @@ def cli( hook_name, skip_prepare_infra, terraform_plan_file, + language_extensions, ssl_cert_file, ssl_key_file, no_memory_limit, @@ -180,6 +183,7 @@ def cli( add_host, invoke_image, hook_name, + language_extensions, ssl_cert_file, ssl_key_file, no_memory_limit, @@ -214,6 +218,7 @@ def do_cli( # pylint: disable=R0914 add_host, invoke_image, hook_name, + language_extensions, ssl_cert_file, ssl_key_file, no_mem_limit, @@ -263,6 +268,7 @@ def do_cli( # pylint: disable=R0914 container_host_interface=container_host_interface, invoke_images=processed_invoke_images, add_host=add_host, + language_extensions=language_extensions, no_mem_limit=no_mem_limit, container_dns=container_dns, ) as invoke_context: diff --git a/samcli/commands/local/start_api/core/options.py b/samcli/commands/local/start_api/core/options.py index 7f3a9894a2f..b7078ea4578 100644 --- a/samcli/commands/local/start_api/core/options.py +++ b/samcli/commands/local/start_api/core/options.py @@ -16,6 +16,7 @@ TEMPLATE_OPTIONS: List[str] = [ "parameter_overrides", + "language_extensions", ] EXTENSION_OPTIONS: List[str] = ["hook_name", "skip_prepare_infra"] diff --git a/samcli/commands/local/start_lambda/cli.py b/samcli/commands/local/start_lambda/cli.py index e4250582700..1ad4a4a1881 100644 --- a/samcli/commands/local/start_lambda/cli.py +++ b/samcli/commands/local/start_lambda/cli.py @@ -13,6 +13,7 @@ from samcli.commands._utils.options import ( generate_next_command_recommendation, hook_name_click_option, + language_extensions_option, skip_prepare_infra_option, terraform_plan_file_option, ) @@ -64,6 +65,7 @@ ) @skip_prepare_infra_option @service_common_options(3001) +@language_extensions_option @invoke_common_options @warm_containers_common_options @local_common_options @@ -107,6 +109,7 @@ def cli( hook_name, skip_prepare_infra, terraform_plan_file, + language_extensions, no_memory_limit, container_dns, ): @@ -141,6 +144,7 @@ def cli( add_host, invoke_image, hook_name, + language_extensions, no_memory_limit, container_dns, ) # pragma: no cover @@ -172,6 +176,7 @@ def do_cli( # pylint: disable=R0914 add_host, invoke_image, hook_name, + language_extensions, no_mem_limit, container_dns, ): @@ -220,6 +225,7 @@ def do_cli( # pylint: disable=R0914 add_host=add_host, invoke_images=processed_invoke_images, function_logical_ids=function_logical_ids, + language_extensions=language_extensions, no_mem_limit=no_mem_limit, container_dns=container_dns, ) as invoke_context: diff --git a/samcli/commands/local/start_lambda/core/options.py b/samcli/commands/local/start_lambda/core/options.py index 126af8dc172..02131ba820b 100644 --- a/samcli/commands/local/start_lambda/core/options.py +++ b/samcli/commands/local/start_lambda/core/options.py @@ -16,6 +16,7 @@ TEMPLATE_OPTIONS: List[str] = [ "parameter_overrides", + "language_extensions", ] CONTAINER_OPTION_NAMES: List[str] = [ diff --git a/samcli/commands/package/command.py b/samcli/commands/package/command.py index ee3dd16185f..946b932718d 100644 --- a/samcli/commands/package/command.py +++ b/samcli/commands/package/command.py @@ -13,6 +13,7 @@ image_repositories_option, image_repository_option, kms_key_id_option, + language_extensions_option, metadata_option, no_progressbar_option, resolve_image_repos_option, @@ -91,6 +92,7 @@ def resources_and_properties_help_string(): @metadata_option @signing_profiles_option @no_progressbar_option +@language_extensions_option @common_options @aws_creds_options @save_params_option @@ -118,6 +120,7 @@ def cli( signing_profiles, resolve_s3, resolve_image_repos, + language_extensions, save_params, config_file, config_env, @@ -144,6 +147,7 @@ def cli( ctx.profile, resolve_s3, resolve_image_repos, + language_extensions, ) # pragma: no cover @@ -164,6 +168,7 @@ def do_cli( profile, resolve_s3, resolve_image_repos, + language_extensions, ): """ Implementation of the ``cli`` method @@ -195,5 +200,6 @@ def do_cli( profile=profile, signing_profiles=signing_profiles, resolve_image_repos=resolve_image_repos, + language_extensions=language_extensions, ) as package_context: package_context.run() diff --git a/samcli/commands/package/core/options.py b/samcli/commands/package/core/options.py index ce386eb689a..aa07d2d6c95 100644 --- a/samcli/commands/package/core/options.py +++ b/samcli/commands/package/core/options.py @@ -29,6 +29,7 @@ CONFIGURATION_OPTION_NAMES: List[str] = ["config_env", "config_file"] + SAVE_PARAMS_OPTIONS ADDITIONAL_OPTIONS: List[str] = [ + "language_extensions", "no_progressbar", "signing_profiles", "template_file", diff --git a/samcli/commands/package/package_context.py b/samcli/commands/package/package_context.py index 941636790a6..17529f60bef 100644 --- a/samcli/commands/package/package_context.py +++ b/samcli/commands/package/package_context.py @@ -25,6 +25,7 @@ from samcli.commands.package.exceptions import PackageFailedError from samcli.lib.bootstrap.companion_stack.companion_stack_manager import sync_ecr_stack +from samcli.lib.cfn_language_extensions.sam_integration import resolve_language_extensions_enabled from samcli.lib.intrinsic_resolver.intrinsics_symbol_table import IntrinsicsSymbolTable from samcli.lib.package.artifact_exporter import Template from samcli.lib.package.code_signer import CodeSigner @@ -78,6 +79,7 @@ def __init__( on_deploy=False, signing_profiles=None, resolve_image_repos=False, + language_extensions=None, ): self.template_file = template_file self.s3_bucket = s3_bucket @@ -97,6 +99,7 @@ def __init__( self.signing_profiles = signing_profiles self.parameter_overrides = parameter_overrides self.resolve_image_repos = resolve_image_repos + self._language_extensions_enabled: bool = resolve_language_extensions_enabled(language_extensions) self._global_parameter_overrides = {IntrinsicsSymbolTable.AWS_REGION: region} if region else {} def __enter__(self): @@ -121,6 +124,7 @@ def run(self): self.template_file, global_parameter_overrides=self._global_parameter_overrides, parameter_overrides=self.parameter_overrides, + language_extensions_enabled=self._language_extensions_enabled, ) self._warn_preview_runtime(stacks) self.image_repositories = self.image_repositories if self.image_repositories is not None else {} @@ -187,7 +191,9 @@ def _export(self, template_path, use_json): # Use the canonical expand_language_extensions() entry point (Phase 1) try: - result = expand_language_extensions(original_template_dict, parameter_values) + result = expand_language_extensions( + original_template_dict, parameter_values, enabled=self._language_extensions_enabled + ) except InvalidSamDocumentException as e: raise PackageFailedError(template_file=self.template_file, ex=str(e)) from e @@ -205,6 +211,7 @@ def _export(self, template_path, use_json): normalize_parameters=True, template_dict=copy.deepcopy(result.expanded_template), parameter_values=parameter_values, + language_extensions_enabled=self._language_extensions_enabled, ) exported_template = template.export() @@ -261,3 +268,7 @@ def write_output(output_file_name: Optional[str], data: str) -> None: with open(output_file_name, "w") as fp: fp.write(data) + + @property + def language_extensions_enabled(self) -> bool: + return self._language_extensions_enabled diff --git a/samcli/commands/sync/command.py b/samcli/commands/sync/command.py index a3bcbc324fa..11169164848 100644 --- a/samcli/commands/sync/command.py +++ b/samcli/commands/sync/command.py @@ -29,6 +29,7 @@ image_repositories_option, image_repository_option, kms_key_id_option, + language_extensions_option, metadata_option, notification_arns_option, parameter_override_option, @@ -47,6 +48,7 @@ from samcli.commands.sync.sync_context import SyncContext from samcli.lib.bootstrap.bootstrap import manage_stack from samcli.lib.build.bundler import EsbuildBundlerManager +from samcli.lib.cfn_language_extensions.sam_integration import resolve_language_extensions_enabled from samcli.lib.cli_validation.image_repository_validation import image_repository_validation from samcli.lib.providers.provider import ( ResourceIdentifier, @@ -179,6 +181,7 @@ @notification_arns_option @tags_option @capabilities_option(default=DEFAULT_CAPABILITIES) # pylint: disable=E1120 +@language_extensions_option @save_params_option @pass_context @track_command @@ -219,6 +222,7 @@ def cli( build_image: Optional[Tuple[str]], build_in_source: Optional[bool], watch_exclude: Optional[Dict[str, List[str]]], + language_extensions: Optional[bool], ) -> None: """ `sam sync` command entry point @@ -257,6 +261,7 @@ def cli( config_env, build_in_source, watch_exclude, + language_extensions, ) # pragma: no cover @@ -291,6 +296,7 @@ def do_cli( config_env: str, build_in_source: Optional[bool], watch_exclude: Optional[Dict[str, List[str]]], + language_extensions: Optional[bool], ) -> None: """ Implementation of the ``cli`` method @@ -312,7 +318,7 @@ def do_cli( s3_bucket_name = s3_bucket or manage_stack(profile=profile, region=region) if dependency_layer is True: - dependency_layer = check_enable_dependency_layer(template_file) + dependency_layer = check_enable_dependency_layer(template_file, language_extensions) # Note: ADL with use-container is not supported yet. Remove this logic once its supported. if use_container and dependency_layer: @@ -345,6 +351,7 @@ def do_cli( locate_layer_nested=True, build_in_source=build_in_source, build_images=processed_build_images, + language_extensions=language_extensions, ) as build_context: built_template = os.path.join(build_dir, DEFAULT_TEMPLATE_NAME) @@ -363,6 +370,7 @@ def do_cli( profile=profile, use_json=False, force_upload=True, + language_extensions=language_extensions, ) as package_context: # 500ms of sleep time between stack checks and describe stack events. DEFAULT_POLL_DELAY = 0.5 @@ -399,12 +407,14 @@ def do_cli( poll_delay=poll_delay, on_failure=None, max_wait_duration=60, + language_extensions=language_extensions, ) as deploy_context: with SyncContext( dependency_layer, build_context.build_dir, build_context.cache_dir, skip_deploy_sync, + language_extensions=language_extensions, ) as sync_context: if watch: watch_excludes_filter = watch_exclude or {} @@ -507,7 +517,9 @@ def execute_code_sync( use_built_resources: bool Boolean flag to whether to use pre-build resources from BuildContext or build resources from scratch """ - stacks = SamLocalStackProvider.get_stacks(template)[0] + stacks = SamLocalStackProvider.get_stacks( + template, language_extensions_enabled=sync_context.language_extensions_enabled + )[0] factory = SyncFlowFactory(build_context, deploy_context, sync_context, stacks, auto_dependency_layer) factory.load_physical_id_mapping() executor = SyncFlowExecutor() @@ -573,13 +585,15 @@ def execute_watch( watch_manager.start() -def check_enable_dependency_layer(template_file: str): +def check_enable_dependency_layer(template_file: str, language_extensions: Optional[bool] = None): """ Check if auto dependency layer should be enabled :param template_file: template file string + :param language_extensions: language extensions flag :return: True if ADL should be enabled, False otherwise """ - stacks, _ = SamLocalStackProvider.get_stacks(template_file) + language_extensions_enabled = resolve_language_extensions_enabled(language_extensions) + stacks, _ = SamLocalStackProvider.get_stacks(template_file, language_extensions_enabled=language_extensions_enabled) for stack in stacks: esbuild = EsbuildBundlerManager(stack) if esbuild.esbuild_configured(): diff --git a/samcli/commands/sync/core/options.py b/samcli/commands/sync/core/options.py index 0d2e34d5e35..6541a2ee157 100644 --- a/samcli/commands/sync/core/options.py +++ b/samcli/commands/sync/core/options.py @@ -27,6 +27,7 @@ "tags", "metadata", "build_image", + "language_extensions", ] CONFIGURATION_OPTION_NAMES: List[str] = ["config_env", "config_file"] + SAVE_PARAMS_OPTIONS diff --git a/samcli/commands/sync/sync_context.py b/samcli/commands/sync/sync_context.py index 7f3917be09d..ccd40a8ebf0 100644 --- a/samcli/commands/sync/sync_context.py +++ b/samcli/commands/sync/sync_context.py @@ -14,6 +14,7 @@ from tomlkit.toml_document import TOMLDocument from samcli.lib.build.build_graph import DEFAULT_DEPENDENCIES_DIR +from samcli.lib.cfn_language_extensions.sam_integration import resolve_language_extensions_enabled from samcli.lib.utils.osutils import rmtree_if_exists LOG = logging.getLogger(__name__) @@ -202,6 +203,7 @@ class SyncContext: _cache_dir: Path _file_path: Path skip_deploy_sync: bool + _language_extensions_enabled: bool def __init__( self, @@ -209,6 +211,7 @@ def __init__( build_dir: str, cache_dir: str, skip_deploy_sync: bool, + language_extensions: Optional[bool] = None, ): self._current_state = SyncState(dependency_layer, dict(), None) self._previous_state = None @@ -216,6 +219,7 @@ def __init__( self._build_dir = Path(build_dir) self._cache_dir = Path(cache_dir) self._file_path = Path(build_dir).parent.joinpath(DEFAULT_SYNC_STATE_FILE_NAME) + self._language_extensions_enabled = resolve_language_extensions_enabled(language_extensions) def __enter__(self) -> "SyncContext": with _lock: @@ -330,3 +334,8 @@ def _cleanup_build_folders(self) -> None: dependencies_dir = Path(DEFAULT_DEPENDENCIES_DIR) LOG.debug("Cleaning up dependencies directory: %s", dependencies_dir) rmtree_if_exists(dependencies_dir) + + @property + def language_extensions_enabled(self) -> bool: + """Whether language extensions are enabled for this sync context.""" + return self._language_extensions_enabled diff --git a/samcli/commands/validate/core/options.py b/samcli/commands/validate/core/options.py index 24efd90896e..b7fd35498a5 100644 --- a/samcli/commands/validate/core/options.py +++ b/samcli/commands/validate/core/options.py @@ -12,6 +12,8 @@ REQUIRED_OPTIONS: List[str] = ["template_file"] +TEMPLATE_OPTIONS: List[str] = ["language_extensions"] + AWS_CREDENTIAL_OPTION_NAMES: List[str] = ["region", "profile"] LINT_OPTION_NAMES: List[str] = [ @@ -21,11 +23,17 @@ CONFIGURATION_OPTION_NAMES: List[str] = ["config_env", "config_file"] + SAVE_PARAMS_OPTIONS ALL_OPTIONS: List[str] = ( - REQUIRED_OPTIONS + LINT_OPTION_NAMES + AWS_CREDENTIAL_OPTION_NAMES + CONFIGURATION_OPTION_NAMES + ALL_COMMON_OPTIONS + REQUIRED_OPTIONS + + TEMPLATE_OPTIONS + + LINT_OPTION_NAMES + + AWS_CREDENTIAL_OPTION_NAMES + + CONFIGURATION_OPTION_NAMES + + ALL_COMMON_OPTIONS ) OPTIONS_INFO: Dict[str, Dict] = { "Required Options": {"option_names": {opt: {"rank": idx} for idx, opt in enumerate(REQUIRED_OPTIONS)}}, + "Template Options": {"option_names": {opt: {"rank": idx} for idx, opt in enumerate(TEMPLATE_OPTIONS)}}, "Lint Options": {"option_names": {opt: {"rank": idx} for idx, opt in enumerate(LINT_OPTION_NAMES)}}, "AWS Credential Options": { "option_names": {opt: {"rank": idx} for idx, opt in enumerate(AWS_CREDENTIAL_OPTION_NAMES)} diff --git a/samcli/commands/validate/validate.py b/samcli/commands/validate/validate.py index c2cb3fc7271..b7f3c591a0c 100644 --- a/samcli/commands/validate/validate.py +++ b/samcli/commands/validate/validate.py @@ -17,9 +17,10 @@ from samcli.cli.main import common_options as cli_framework_options from samcli.commands._utils.cdk_support_decorators import unsupported_command_cdk from samcli.commands._utils.command_exception_handler import command_exception_handler -from samcli.commands._utils.options import template_option_without_build +from samcli.commands._utils.options import language_extensions_option, template_option_without_build from samcli.commands.exceptions import LinterRuleMatchedException, UserException from samcli.commands.validate.core.command import ValidateCommand +from samcli.lib.cfn_language_extensions.sam_integration import resolve_language_extensions_enabled from samcli.lib.telemetry.event import EventTracker from samcli.lib.telemetry.metric import track_command from samcli.lib.utils.version_checker import check_newer_version @@ -57,6 +58,7 @@ class SamTemplate: "Create a cfnlintrc config file to specify additional parameters. " "For more information, see: https://github.com/aws-cloudformation/cfn-lint", ) +@language_extensions_option @save_params_option @pass_context @track_command @@ -64,13 +66,13 @@ class SamTemplate: @print_cmdline_args @unsupported_command_cdk(alternative_command="cdk doctor") @command_exception_handler -def cli(ctx, template_file, config_file, config_env, lint, save_params): +def cli(ctx, template_file, config_file, config_env, lint, language_extensions, save_params): # All logic must be implemented in the ``do_cli`` method. This helps with easy unit testing - do_cli(ctx, template_file, lint) # pragma: no cover + do_cli(ctx, template_file, lint, language_extensions) # pragma: no cover -def do_cli(ctx, template, lint): +def do_cli(ctx, template, lint, language_extensions): """ Implementation of the ``cli`` method, just separated out for unit testing purposes """ @@ -92,6 +94,7 @@ def do_cli(ctx, template, lint): ManagedPolicyLoader(iam_client), profile=ctx.profile, region=ctx.region, + language_extensions_enabled=resolve_language_extensions_enabled(language_extensions), ) try: diff --git a/samcli/lib/bootstrap/companion_stack/companion_stack_manager.py b/samcli/lib/bootstrap/companion_stack/companion_stack_manager.py index cfe95e675d9..66dd6a290e1 100644 --- a/samcli/lib/bootstrap/companion_stack/companion_stack_manager.py +++ b/samcli/lib/bootstrap/companion_stack/companion_stack_manager.py @@ -307,7 +307,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)[0] + stacks = SamLocalStackProvider.get_stacks(template_file, language_extensions_enabled=False)[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 diff --git a/samcli/lib/cfn_language_extensions/sam_integration.py b/samcli/lib/cfn_language_extensions/sam_integration.py index fd973eadb85..e0189f7e022 100644 --- a/samcli/lib/cfn_language_extensions/sam_integration.py +++ b/samcli/lib/cfn_language_extensions/sam_integration.py @@ -12,6 +12,7 @@ import copy import logging +import os import re from dataclasses import dataclass, field from typing import Any, Dict, List, Optional, Tuple @@ -35,6 +36,38 @@ # Transform name for AWS Language Extensions AWS_LANGUAGE_EXTENSIONS_TRANSFORM = "AWS::LanguageExtensions" +# Env var that, when truthy, opts into local AWS::LanguageExtensions processing. +# A `--language-extensions` flag, when passed, takes precedence over this env var. +ENABLE_ENV_VAR = "SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS" +_TRUTHY_VALUES = frozenset({"1", "true", "yes"}) + + +def resolve_language_extensions_enabled(flag_value: Optional[bool]) -> bool: + """ + Resolve whether local AWS::LanguageExtensions expansion is enabled. + + Click flag wins over the env var. Tri-state input: + - ``True`` -> enabled (regardless of env var) + - ``False`` -> disabled (regardless of env var) + - ``None`` -> fall back to env var (off when unset or untruthy) + + Truthy env-var values (case-insensitive, whitespace-trimmed): ``1``, ``true``, ``yes``. + + Parameters + ---------- + flag_value : Optional[bool] + The Click flag value. ``None`` means the user did not pass + ``--language-extensions`` or ``--no-language-extensions``. + + Returns + ------- + bool + ``True`` if Phase 1 LE expansion should run, else ``False``. + """ + if flag_value is not None: + return flag_value + return os.environ.get(ENABLE_ENV_VAR, "").strip().lower() in _TRUTHY_VALUES + @dataclass(frozen=True) class LanguageExtensionResult: @@ -476,6 +509,8 @@ def detect_dynamic_artifact_properties( def expand_language_extensions( template: Dict[str, Any], parameter_values: Optional[Dict[str, Any]] = None, + *, + enabled: bool, ) -> LanguageExtensionResult: """ Canonical Phase 1 entry point for expanding CloudFormation Language Extensions. @@ -497,6 +532,10 @@ def expand_language_extensions( The raw template dictionary parameter_values : dict, optional Template parameter values (may include pseudo-parameters like AWS::Region) + enabled : bool + Required keyword. When False, returns a passthrough result without + inspecting the template (silent pre-1.160.0 behavior). When True, + runs the full Phase 1 pipeline if the template uses AWS::LanguageExtensions. Returns ------- @@ -509,6 +548,14 @@ def expand_language_extensions( InvalidSamDocumentException If the template contains invalid language extension syntax """ + if not enabled: + return LanguageExtensionResult( + expanded_template=template, + original_template=template, + dynamic_artifact_properties=[], + had_language_extensions=False, + ) + if not check_using_language_extension(template): return LanguageExtensionResult( expanded_template=template, diff --git a/samcli/lib/cli_validation/image_repository_validation.py b/samcli/lib/cli_validation/image_repository_validation.py index 67f4ea965e6..8bfa6605803 100644 --- a/samcli/lib/cli_validation/image_repository_validation.py +++ b/samcli/lib/cli_validation/image_repository_validation.py @@ -125,6 +125,7 @@ def _is_all_image_funcs_provided(template_file, image_repositories, parameters_o template_file, parameter_overrides=parameters_overrides, global_parameter_overrides=global_parameter_overrides, + language_extensions_enabled=False, ) # updated_repositories = map_resource_id_key_map_to_full_path(image_repositories, stacks) function_provider = SamFunctionProvider(stacks, ignore_code_extraction_warnings=True) diff --git a/samcli/lib/list/endpoints/endpoints_producer.py b/samcli/lib/list/endpoints/endpoints_producer.py index 70c82687ce2..ae58b55c19c 100644 --- a/samcli/lib/list/endpoints/endpoints_producer.py +++ b/samcli/lib/list/endpoints/endpoints_producer.py @@ -322,7 +322,9 @@ def produce(self): sam_template = get_template_data(self.template_file) translated_dict = self.get_translated_dict(template_file_dict=sam_template) - stacks, _ = SamLocalStackProvider.get_stacks(template_file="", template_dictionary=translated_dict) + stacks, _ = SamLocalStackProvider.get_stacks( + template_file="", template_dictionary=translated_dict, language_extensions_enabled=False + ) validate_stack(stacks) endpoints_list: list diff --git a/samcli/lib/list/resources/resource_mapping_producer.py b/samcli/lib/list/resources/resource_mapping_producer.py index 562f145f9a8..5aea983f3b4 100644 --- a/samcli/lib/list/resources/resource_mapping_producer.py +++ b/samcli/lib/list/resources/resource_mapping_producer.py @@ -133,7 +133,9 @@ def produce(self): translated_dict = self.get_translated_dict(template_file_dict=sam_template) - stacks, _ = SamLocalStackProvider.get_stacks(template_file="", template_dictionary=translated_dict) + stacks, _ = SamLocalStackProvider.get_stacks( + template_file="", template_dictionary=translated_dict, language_extensions_enabled=False + ) if not stacks or not stacks[ROOT_STACK].resources: raise SamListLocalResourcesNotFoundError(msg="No local resources found.") seen_resources = set() diff --git a/samcli/lib/package/artifact_exporter.py b/samcli/lib/package/artifact_exporter.py index e672c6a75b9..681a3390fbb 100644 --- a/samcli/lib/package/artifact_exporter.py +++ b/samcli/lib/package/artifact_exporter.py @@ -127,6 +127,7 @@ class CloudFormationStackResource(ResourceZip): RESOURCE_TYPE = AWS_CLOUDFORMATION_STACK PROPERTY_NAME = RESOURCES_WITH_LOCAL_PATHS[RESOURCE_TYPE][0] parent_parameter_values: Optional[Dict] = None + language_extensions_enabled: bool = False def do_export(self, resource_id, resource_dict, parent_dir): """ @@ -186,7 +187,9 @@ def do_export(self, resource_id, resource_dict, parent_dir): parameter_values.update(resolved_nested_params) try: - result = expand_language_extensions(child_template_dict, parameter_values) + result = expand_language_extensions( + child_template_dict, parameter_values, enabled=self.language_extensions_enabled + ) except InvalidSamDocumentException as e: # Expected failure path: the child template triggered the # AWS::LanguageExtensions transform but SAM CLI could not expand it @@ -232,6 +235,7 @@ def do_export(self, resource_id, resource_dict, parent_dir): parent_stack_id=resource_id, template_dict=copy.deepcopy(result.expanded_template), parameter_values=parameter_values, + language_extensions_enabled=self.language_extensions_enabled, ) exported_template = template.export() @@ -265,6 +269,7 @@ def do_export(self, resource_id, resource_dict, parent_dir): normalize_parameters=True, parent_stack_id=resource_id, parameter_values=parameter_values, + language_extensions_enabled=self.language_extensions_enabled, ).export() exported_template_str = yaml_dump(exported_template_dict) @@ -357,6 +362,7 @@ def __init__( parent_stack_id: str = "", parameter_values: Optional[Dict] = None, template_dict: Optional[Dict] = None, + language_extensions_enabled: bool = False, ): """ Reads the template and makes it ready for export @@ -400,6 +406,7 @@ def __init__( # Parameter values to pass down to child-template expansion (e.g. Fn::ForEach # collections that Ref a parameter). None preserves pre-existing behavior. self.parameter_values = parameter_values + self.language_extensions_enabled = language_extensions_enabled def _export_global_artifacts(self, template_dict: Dict) -> Dict: """ @@ -492,6 +499,7 @@ def export(self) -> Dict: # Export code resources exporter = exporter_class(self.uploaders, self.code_signer, cache) exporter.parent_parameter_values = self.parameter_values + exporter.language_extensions_enabled = self.language_extensions_enabled exporter.export(full_path, resource_dict, self.template_dir) return self.template_dict diff --git a/samcli/lib/providers/sam_function_provider.py b/samcli/lib/providers/sam_function_provider.py index 7cbe6b9db68..1b6e1f72258 100644 --- a/samcli/lib/providers/sam_function_provider.py +++ b/samcli/lib/providers/sam_function_provider.py @@ -893,6 +893,7 @@ def __init__( use_raw_codeuri: bool = False, ignore_code_extraction_warnings: bool = False, function_logical_ids: Optional[Tuple[str, ...]] = None, + language_extensions_enabled: bool = False, ) -> None: """ Initialize the class with SAM template data. The SAM template passed to this provider is assumed @@ -924,6 +925,7 @@ def __init__( self._ignore_code_extraction_warnings = ignore_code_extraction_warnings self._parameter_overrides = parameter_overrides self._global_parameter_overrides = global_parameter_overrides + self._language_extensions_enabled = language_extensions_enabled self.parent_templates_paths = [] for stack in self._stacks: if stack.is_root_stack: @@ -1020,6 +1022,7 @@ def _refresh_loaded_functions(self) -> None: template_file, parameter_overrides=self._parameter_overrides, global_parameter_overrides=self._global_parameter_overrides, + language_extensions_enabled=self._language_extensions_enabled, ) self._stacks += template_stacks except (TemplateNotFoundException, TemplateFailedParsingException) as ex: diff --git a/samcli/lib/providers/sam_stack_provider.py b/samcli/lib/providers/sam_stack_provider.py index b89aa94b132..88e6bd6e4c7 100644 --- a/samcli/lib/providers/sam_stack_provider.py +++ b/samcli/lib/providers/sam_stack_provider.py @@ -218,6 +218,7 @@ def get_stacks( metadata: Optional[Dict] = None, template_dictionary: Optional[Dict] = None, use_sam_transform: bool = True, + language_extensions_enabled: bool = False, ) -> Tuple[List[Stack], List[str]]: """ Recursively extract stacks from a template file. @@ -243,6 +244,12 @@ def get_stacks( dictionary representing the sam template. Only one of either template_dict or template_file is required use_sam_transform: bool Whether to transform the given template with Serverless Application Model. Default is True + language_extensions_enabled: bool + Whether to run AWS::LanguageExtensions Phase 1 expansion locally. + Off by default (matches pre-1.160.0 behavior). Callers at the SAM + CLI command boundary (BuildContext, PackageContext, DeployContext, + SyncContext, InvokeContext) resolve this via + resolve_language_extensions_enabled(). Returns ------- @@ -271,7 +278,9 @@ def get_stacks( parameter_values.update(IntrinsicsSymbolTable.DEFAULT_PSEUDO_PARAM_VALUES) parameter_values.update(merged_params or {}) - lang_ext_result = expand_language_extensions(template_dict, parameter_values=parameter_values) + lang_ext_result = expand_language_extensions( + template_dict, parameter_values=parameter_values, enabled=language_extensions_enabled + ) processed_template_dict = lang_ext_result.expanded_template # Store the original template (before language extensions processing) for CloudFormation deployment @@ -311,6 +320,7 @@ def get_stacks( global_parameter_overrides, child_stack.metadata, use_sam_transform=use_sam_transform, + language_extensions_enabled=language_extensions_enabled, ) stacks.extend(stacks_in_child) remote_stack_full_paths.extend(remote_stack_full_paths_in_child) diff --git a/samcli/lib/sync/infra_sync_executor.py b/samcli/lib/sync/infra_sync_executor.py index 22168b449fc..5bf9bd6db26 100644 --- a/samcli/lib/sync/infra_sync_executor.py +++ b/samcli/lib/sync/infra_sync_executor.py @@ -587,7 +587,7 @@ def _detect_foreach_code_changes( """ from samcli.lib.cfn_language_extensions.sam_integration import check_using_language_extension - if not check_using_language_extension(current_template): + if not self._sync_context.language_extensions_enabled or not check_using_language_extension(current_template): return from samcli.lib.cfn_language_extensions.sam_integration import expand_language_extensions @@ -607,10 +607,14 @@ def _detect_foreach_code_changes( deployed_params[param_name] = param_def["Default"] current_expanded = expand_language_extensions( - current_template, parameter_values=current_params + current_template, + parameter_values=current_params, + enabled=self._sync_context.language_extensions_enabled, ).expanded_template deployed_expanded = expand_language_extensions( - last_deployed_template, parameter_values=deployed_params + last_deployed_template, + parameter_values=deployed_params, + enabled=self._sync_context.language_extensions_enabled, ).expanded_template except Exception as e: LOG.warning( diff --git a/samcli/lib/sync/watch_manager.py b/samcli/lib/sync/watch_manager.py index bf73591f2ab..dcc464f38ec 100644 --- a/samcli/lib/sync/watch_manager.py +++ b/samcli/lib/sync/watch_manager.py @@ -120,7 +120,11 @@ def _update_stacks(self) -> None: Update all other member that also depends on the stacks. This should be called whenever there is a change to the template. """ - self._stacks = SamLocalStackProvider.get_stacks(self._template, use_sam_transform=False)[0] + self._stacks = SamLocalStackProvider.get_stacks( + self._template, + use_sam_transform=False, + language_extensions_enabled=self._sync_context.language_extensions_enabled, + )[0] self._sync_flow_factory = SyncFlowFactory( self._build_context, self._deploy_context, @@ -169,7 +173,11 @@ def _add_code_triggers(self) -> None: def _add_template_triggers(self) -> None: """Create TemplateTrigger and add its handlers to observer""" - stacks = SamLocalStackProvider.get_stacks(self._template, use_sam_transform=False)[0] + stacks = SamLocalStackProvider.get_stacks( + self._template, + use_sam_transform=False, + language_extensions_enabled=self._sync_context.language_extensions_enabled, + )[0] for stack in stacks: template = stack.location template_trigger = TemplateTrigger(template, stack.name, lambda _=None: self.queue_infra_sync()) diff --git a/samcli/lib/translate/sam_template_validator.py b/samcli/lib/translate/sam_template_validator.py index 5aae4b1a967..6245a58376a 100644 --- a/samcli/lib/translate/sam_template_validator.py +++ b/samcli/lib/translate/sam_template_validator.py @@ -31,6 +31,7 @@ def __init__( profile: Optional[str] = None, region: Optional[str] = None, parameter_overrides: Optional[dict] = None, + language_extensions_enabled: bool = False, ): """ Construct a SamTemplateValidator @@ -57,12 +58,15 @@ def __init__( Optional AWS region name parameter_overrides: Optional[dict] Template parameter overrides + language_extensions_enabled: bool + Whether to enable AWS::LanguageExtensions (default: False) """ self.sam_template = sam_template self.managed_policy_loader = managed_policy_loader self.sam_parser = parser.Parser() self.boto3_session = Session(profile_name=profile, region_name=region) self.parameter_overrides = parameter_overrides or {} + self._language_extensions_enabled = language_extensions_enabled def get_translated_template_if_valid(self): """ @@ -88,7 +92,9 @@ def get_translated_template_if_valid(self): # template is a no-op since the transform is still present but ForEach is already resolved). parameter_values = dict(IntrinsicsSymbolTable.DEFAULT_PSEUDO_PARAM_VALUES) parameter_values.update(self.parameter_overrides) - result = expand_language_extensions(self.sam_template, parameter_values=parameter_values) + result = expand_language_extensions( + self.sam_template, parameter_values=parameter_values, enabled=self._language_extensions_enabled + ) if result.had_language_extensions: self.sam_template = result.expanded_template diff --git a/schema/samcli.json b/schema/samcli.json index f2c93aa4492..b7e3f1ec7e3 100644 --- a/schema/samcli.json +++ b/schema/samcli.json @@ -237,7 +237,7 @@ "properties": { "parameters": { "title": "Parameters for the validate command", - "description": "Available parameters for the validate command:\n* template_file:\nAWS SAM template file.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* lint:\nRun linting validation on template through cfn-lint. Create a cfnlintrc config file to specify additional parameters. For more information, see: https://github.com/aws-cloudformation/cfn-lint\n* save_params:\nSave the parameters provided via the command line to the configuration file.", + "description": "Available parameters for the validate command:\n* template_file:\nAWS SAM template file.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* lint:\nRun linting validation on template through cfn-lint. Create a cfnlintrc config file to specify additional parameters. For more information, see: https://github.com/aws-cloudformation/cfn-lint\n* language_extensions:\nExpand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1.\n* save_params:\nSave the parameters provided via the command line to the configuration file.", "type": "object", "properties": { "template_file": { @@ -271,6 +271,11 @@ "type": "boolean", "description": "Run linting validation on template through cfn-lint. Create a cfnlintrc config file to specify additional parameters. For more information, see: https://github.com/aws-cloudformation/cfn-lint" }, + "language_extensions": { + "title": "language_extensions", + "type": "boolean", + "description": "Expand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1." + }, "save_params": { "title": "save_params", "type": "boolean", @@ -289,7 +294,7 @@ "properties": { "parameters": { "title": "Parameters for the build command", - "description": "Available parameters for the build command:\n* terraform_project_root_path:\nUsed for passing the Terraform project root directory path. Current directory will be used as a default value, if this parameter is not provided.\n* hook_name:\nHook package id to extend AWS SAM CLI commands functionality. \n\nExample: `terraform` to extend AWS SAM CLI commands functionality to support terraform applications. \n\nAvailable Hook Names: ['terraform']\n* skip_prepare_infra:\nSkip preparation stage when there are no infrastructure changes. Only used in conjunction with --hook-name.\n* use_container:\nBuild functions within an AWS Lambda-like container.\n* use_buildkit:\nEnable buildkit for container image builds. Requires Docker with buildx plugin or Finch CLI.\n* build_in_source:\nOpts in to build project in the source folder. The following workflows support building in source: ['nodejs16.x', 'nodejs18.x', 'nodejs20.x', 'nodejs22.x', 'Makefile', 'esbuild']\n* container_env_var:\nEnvironment variables to be passed into build containers\nResource format (FuncName.VarName=Value) or Global format (VarName=Value).\n\n Example: --container-env-var Func1.VAR1=value1 --container-env-var VAR2=value2\n* container_env_var_file:\nEnvironment variables json file (e.g., env_vars.json) to be passed to containers.\n* build_image:\nContainer image URIs for building functions/layers. You can specify for all functions/layers with just the image URI (--build-image public.ecr.aws/sam/build-nodejs18.x:latest). You can specify for each individual function with (--build-image FunctionLogicalID=public.ecr.aws/sam/build-nodejs18.x:latest). A combination of the two can be used. If a function does not have build image specified or an image URI for all functions, the default SAM CLI build images will be used.\n* exclude:\nName of the resource(s) to exclude from AWS SAM CLI build.\n* parallel:\nEnable parallel builds for AWS SAM template's functions and layers.\n* mount_with:\nSpecify mount mode for building functions/layers inside container. If it is mounted with write permissions, some files in source code directory may be changed/added by the build process. By default the source code directory is read only.\n* mount_symlinks:\nSpecify if symlinks at the top level of the code should be mounted inside the container. Activating this flag could allow access to locations outside of your workspace by using a symbolic link. By default symlinks are not mounted.\n* build_dir:\nDirectory to store build artifacts.Note: This directory will be first removed before starting a build.\n* cache_dir:\nDirectory to store cached artifacts. The default cache directory is .aws-sam/cache\n* base_dir:\nResolve relative paths to function's source code with respect to this directory. Use this if SAM template and source code are not in same enclosing folder. By default, relative paths are resolved with respect to the SAM template's location.\n* manifest:\nPath to a custom dependency manifest. Example: custom-package.json\n* cached:\nEnable cached builds.Reuse build artifacts that have not changed from previous builds. \n\nAWS SAM CLI evaluates if files in your project directory have changed. \n\nNote: AWS SAM CLI does not evaluate changes made to third party modules that the project depends on.Example: Python function includes a requirements.txt file with the following entry requests=1.x and the latest request module version changes from 1.1 to 1.2, AWS SAM CLI will not pull the latest version until a non-cached build is run.\n* template_file:\nAWS SAM template file.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* skip_pull_image:\nSkip pulling down the latest Docker image for Lambda runtime.\n* docker_network:\nName or ID of an existing docker network for AWS Lambda docker containers to connect to, along with the default bridge network. If not specified, the Lambda containers will only connect to the default bridge docker network.\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* save_params:\nSave the parameters provided via the command line to the configuration file.", + "description": "Available parameters for the build command:\n* terraform_project_root_path:\nUsed for passing the Terraform project root directory path. Current directory will be used as a default value, if this parameter is not provided.\n* hook_name:\nHook package id to extend AWS SAM CLI commands functionality. \n\nExample: `terraform` to extend AWS SAM CLI commands functionality to support terraform applications. \n\nAvailable Hook Names: ['terraform']\n* skip_prepare_infra:\nSkip preparation stage when there are no infrastructure changes. Only used in conjunction with --hook-name.\n* use_container:\nBuild functions within an AWS Lambda-like container.\n* use_buildkit:\nEnable buildkit for container image builds. Requires Docker with buildx plugin or Finch CLI.\n* build_in_source:\nOpts in to build project in the source folder. The following workflows support building in source: ['nodejs16.x', 'nodejs18.x', 'nodejs20.x', 'nodejs22.x', 'Makefile', 'esbuild']\n* language_extensions:\nExpand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1.\n* container_env_var:\nEnvironment variables to be passed into build containers\nResource format (FuncName.VarName=Value) or Global format (VarName=Value).\n\n Example: --container-env-var Func1.VAR1=value1 --container-env-var VAR2=value2\n* container_env_var_file:\nEnvironment variables json file (e.g., env_vars.json) to be passed to containers.\n* build_image:\nContainer image URIs for building functions/layers. You can specify for all functions/layers with just the image URI (--build-image public.ecr.aws/sam/build-nodejs18.x:latest). You can specify for each individual function with (--build-image FunctionLogicalID=public.ecr.aws/sam/build-nodejs18.x:latest). A combination of the two can be used. If a function does not have build image specified or an image URI for all functions, the default SAM CLI build images will be used.\n* exclude:\nName of the resource(s) to exclude from AWS SAM CLI build.\n* parallel:\nEnable parallel builds for AWS SAM template's functions and layers.\n* mount_with:\nSpecify mount mode for building functions/layers inside container. If it is mounted with write permissions, some files in source code directory may be changed/added by the build process. By default the source code directory is read only.\n* mount_symlinks:\nSpecify if symlinks at the top level of the code should be mounted inside the container. Activating this flag could allow access to locations outside of your workspace by using a symbolic link. By default symlinks are not mounted.\n* build_dir:\nDirectory to store build artifacts.Note: This directory will be first removed before starting a build.\n* cache_dir:\nDirectory to store cached artifacts. The default cache directory is .aws-sam/cache\n* base_dir:\nResolve relative paths to function's source code with respect to this directory. Use this if SAM template and source code are not in same enclosing folder. By default, relative paths are resolved with respect to the SAM template's location.\n* manifest:\nPath to a custom dependency manifest. Example: custom-package.json\n* cached:\nEnable cached builds.Reuse build artifacts that have not changed from previous builds. \n\nAWS SAM CLI evaluates if files in your project directory have changed. \n\nNote: AWS SAM CLI does not evaluate changes made to third party modules that the project depends on.Example: Python function includes a requirements.txt file with the following entry requests=1.x and the latest request module version changes from 1.1 to 1.2, AWS SAM CLI will not pull the latest version until a non-cached build is run.\n* template_file:\nAWS SAM template file.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* skip_pull_image:\nSkip pulling down the latest Docker image for Lambda runtime.\n* docker_network:\nName or ID of an existing docker network for AWS Lambda docker containers to connect to, along with the default bridge network. If not specified, the Lambda containers will only connect to the default bridge docker network.\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* save_params:\nSave the parameters provided via the command line to the configuration file.", "type": "object", "properties": { "terraform_project_root_path": { @@ -322,6 +327,11 @@ "type": "boolean", "description": "Opts in to build project in the source folder. The following workflows support building in source: ['nodejs16.x', 'nodejs18.x', 'nodejs20.x', 'nodejs22.x', 'Makefile', 'esbuild']" }, + "language_extensions": { + "title": "language_extensions", + "type": "boolean", + "description": "Expand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1." + }, "container_env_var": { "title": "container_env_var", "type": "string", @@ -500,7 +510,7 @@ "properties": { "parameters": { "title": "Parameters for the local invoke command", - "description": "Available parameters for the local invoke command:\n* terraform_plan_file:\nUsed for passing a custom plan file when executing the Terraform hook.\n* hook_name:\nHook package id to extend AWS SAM CLI commands functionality. \n\nExample: `terraform` to extend AWS SAM CLI commands functionality to support terraform applications. \n\nAvailable Hook Names: ['terraform']\n* skip_prepare_infra:\nSkip preparation stage when there are no infrastructure changes. Only used in conjunction with --hook-name.\n* event:\nJSON file containing event data passed to the Lambda function during invoke. If this option is not specified, no event is assumed. Pass in the value '-' to input JSON via stdin\n* no_event:\nDEPRECATED: By default no event is assumed.\n* runtime:\nLambda runtime used to invoke the function.\n\nRuntimes: dotnet10, dotnet8, dotnet6, go1.x, java25, java21, java17, java11, java8.al2, nodejs24.x, nodejs22.x, nodejs20.x, nodejs18.x, nodejs16.x, provided, provided.al2, provided.al2023, python3.9, python3.8, python3.14, python3.13, python3.12, python3.11, python3.10, ruby4.0, ruby3.4, ruby3.3, ruby3.2\n* tenant_id:\nTenant ID for multi-tenant Lambda functions. Used to ensure compute isolation between different tenants. Must be 1-256 characters, the allowed characters are a-z and A-Z, numbers, spaces, and the characters _ . : / = + - @\n* durable_execution_name:\nName for the durable execution (for durable functions only).\n* mount_symlinks:\nSpecify if symlinks at the top level of the code should be mounted inside the container. Activating this flag could allow access to locations outside of your workspace by using a symbolic link. By default symlinks are not mounted.\n* template_file:\nAWS SAM template which references built artifacts for resources in the template. (if applicable)\n* env_vars:\nJSON file containing values for Lambda function's environment variables.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* debug_port:\nWhen specified, Lambda function container will start in debug mode and will expose this port on localhost.\n* debugger_path:\nHost path to a debugger that will be mounted into the Lambda container.\n* debug_args:\nAdditional arguments to be passed to the debugger.\n* container_env_vars:\nJSON file containing additional environment variables to be set within the container when used in a debugging session locally.\n* docker_volume_basedir:\nSpecify the location basedir where the SAM template exists. If Docker is running on a remote machine, Path of the SAM template must be mounted on the Docker machine and modified to match the remote machine.\n* log_file:\nFile to capture output logs.\n* layer_cache_basedir:\nSpecify the location basedir where the lambda layers used by the template will be downloaded to.\n* skip_pull_image:\nSkip pulling down the latest Docker image for Lambda runtime.\n* docker_network:\nName or ID of an existing docker network for AWS Lambda docker containers to connect to, along with the default bridge network. If not specified, the Lambda containers will only connect to the default bridge docker network.\n* force_image_build:\nForce rebuilding the image used for invoking functions with layers.\n* shutdown:\nEmulate a shutdown event after invoke completes, to test extension handling of shutdown behavior.\n* container_host:\nHost of locally emulated Lambda container. This option is useful when the container runs on a different host than AWS SAM CLI. For example, if one wants to run AWS SAM CLI in a Docker container on macOS, this option could specify `host.docker.internal`\n* container_host_interface:\nIP address of the host network interface that container ports should bind to. Use 0.0.0.0 to bind to all interfaces.\n* add_host:\nPasses a hostname to IP address mapping to the Docker container's host file. This parameter can be passed multiple times.Example:--add-host example.com:127.0.0.1\n* container_dns:\nSet custom DNS servers for the Lambda container. This parameter can be passed multiple times to specify multiple DNS servers. Example: --container-dns 8.8.8.8 --container-dns 1.1.1.1\n* invoke_image:\nContainer image URIs for invoking functions or starting api and function. One can specify the image URI used for the local function invocation (--invoke-image public.ecr.aws/sam/build-nodejs20.x:latest). One can also specify for each individual function with (--invoke-image Function1=public.ecr.aws/sam/build-nodejs20.x:latest). If a function does not have invoke image specified, the default AWS SAM CLI emulation image will be used.\n* no_memory_limit:\nRemoves the Memory limit during emulation. With this parameter, the underlying container will run without a --memory parameter\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* save_params:\nSave the parameters provided via the command line to the configuration file.", + "description": "Available parameters for the local invoke command:\n* terraform_plan_file:\nUsed for passing a custom plan file when executing the Terraform hook.\n* hook_name:\nHook package id to extend AWS SAM CLI commands functionality. \n\nExample: `terraform` to extend AWS SAM CLI commands functionality to support terraform applications. \n\nAvailable Hook Names: ['terraform']\n* skip_prepare_infra:\nSkip preparation stage when there are no infrastructure changes. Only used in conjunction with --hook-name.\n* event:\nJSON file containing event data passed to the Lambda function during invoke. If this option is not specified, no event is assumed. Pass in the value '-' to input JSON via stdin\n* no_event:\nDEPRECATED: By default no event is assumed.\n* runtime:\nLambda runtime used to invoke the function.\n\nRuntimes: dotnet10, dotnet8, dotnet6, go1.x, java25, java21, java17, java11, java8.al2, nodejs24.x, nodejs22.x, nodejs20.x, nodejs18.x, nodejs16.x, provided, provided.al2, provided.al2023, python3.9, python3.8, python3.14, python3.13, python3.12, python3.11, python3.10, ruby4.0, ruby3.4, ruby3.3, ruby3.2\n* tenant_id:\nTenant ID for multi-tenant Lambda functions. Used to ensure compute isolation between different tenants. Must be 1-256 characters, the allowed characters are a-z and A-Z, numbers, spaces, and the characters _ . : / = + - @\n* durable_execution_name:\nName for the durable execution (for durable functions only).\n* mount_symlinks:\nSpecify if symlinks at the top level of the code should be mounted inside the container. Activating this flag could allow access to locations outside of your workspace by using a symbolic link. By default symlinks are not mounted.\n* language_extensions:\nExpand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1.\n* template_file:\nAWS SAM template which references built artifacts for resources in the template. (if applicable)\n* env_vars:\nJSON file containing values for Lambda function's environment variables.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* debug_port:\nWhen specified, Lambda function container will start in debug mode and will expose this port on localhost.\n* debugger_path:\nHost path to a debugger that will be mounted into the Lambda container.\n* debug_args:\nAdditional arguments to be passed to the debugger.\n* container_env_vars:\nJSON file containing additional environment variables to be set within the container when used in a debugging session locally.\n* docker_volume_basedir:\nSpecify the location basedir where the SAM template exists. If Docker is running on a remote machine, Path of the SAM template must be mounted on the Docker machine and modified to match the remote machine.\n* log_file:\nFile to capture output logs.\n* layer_cache_basedir:\nSpecify the location basedir where the lambda layers used by the template will be downloaded to.\n* skip_pull_image:\nSkip pulling down the latest Docker image for Lambda runtime.\n* docker_network:\nName or ID of an existing docker network for AWS Lambda docker containers to connect to, along with the default bridge network. If not specified, the Lambda containers will only connect to the default bridge docker network.\n* force_image_build:\nForce rebuilding the image used for invoking functions with layers.\n* shutdown:\nEmulate a shutdown event after invoke completes, to test extension handling of shutdown behavior.\n* container_host:\nHost of locally emulated Lambda container. This option is useful when the container runs on a different host than AWS SAM CLI. For example, if one wants to run AWS SAM CLI in a Docker container on macOS, this option could specify `host.docker.internal`\n* container_host_interface:\nIP address of the host network interface that container ports should bind to. Use 0.0.0.0 to bind to all interfaces.\n* add_host:\nPasses a hostname to IP address mapping to the Docker container's host file. This parameter can be passed multiple times.Example:--add-host example.com:127.0.0.1\n* container_dns:\nSet custom DNS servers for the Lambda container. This parameter can be passed multiple times to specify multiple DNS servers. Example: --container-dns 8.8.8.8 --container-dns 1.1.1.1\n* invoke_image:\nContainer image URIs for invoking functions or starting api and function. One can specify the image URI used for the local function invocation (--invoke-image public.ecr.aws/sam/build-nodejs20.x:latest). One can also specify for each individual function with (--invoke-image Function1=public.ecr.aws/sam/build-nodejs20.x:latest). If a function does not have invoke image specified, the default AWS SAM CLI emulation image will be used.\n* no_memory_limit:\nRemoves the Memory limit during emulation. With this parameter, the underlying container will run without a --memory parameter\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* save_params:\nSave the parameters provided via the command line to the configuration file.", "type": "object", "properties": { "terraform_plan_file": { @@ -579,6 +589,11 @@ "type": "boolean", "description": "Specify if symlinks at the top level of the code should be mounted inside the container. Activating this flag could allow access to locations outside of your workspace by using a symbolic link. By default symlinks are not mounted." }, + "language_extensions": { + "title": "language_extensions", + "type": "boolean", + "description": "Expand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1." + }, "template_file": { "title": "template_file", "type": "string", @@ -730,7 +745,7 @@ "properties": { "parameters": { "title": "Parameters for the local start api command", - "description": "Available parameters for the local start api command:\n* terraform_plan_file:\nUsed for passing a custom plan file when executing the Terraform hook.\n* hook_name:\nHook package id to extend AWS SAM CLI commands functionality. \n\nExample: `terraform` to extend AWS SAM CLI commands functionality to support terraform applications. \n\nAvailable Hook Names: ['terraform']\n* skip_prepare_infra:\nSkip preparation stage when there are no infrastructure changes. Only used in conjunction with --hook-name.\n* host:\nLocal hostname or IP address to bind to (default: '127.0.0.1')\n* port:\nLocal port number to listen on (default: '3000')\n* static_dir:\nAny static assets (e.g. CSS/Javascript/HTML) files located in this directory will be presented at /\n* disable_authorizer:\nDisable custom Lambda Authorizers from being parsed and invoked.\n* ssl_cert_file:\nPath to SSL certificate file (default: None)\n* ssl_key_file:\nPath to SSL key file (default: None)\n* template_file:\nAWS SAM template which references built artifacts for resources in the template. (if applicable)\n* env_vars:\nJSON file containing values for Lambda function's environment variables.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* debug_port:\nWhen specified, Lambda function container will start in debug mode and will expose this port on localhost.\n* debugger_path:\nHost path to a debugger that will be mounted into the Lambda container.\n* debug_args:\nAdditional arguments to be passed to the debugger.\n* container_env_vars:\nJSON file containing additional environment variables to be set within the container when used in a debugging session locally.\n* docker_volume_basedir:\nSpecify the location basedir where the SAM template exists. If Docker is running on a remote machine, Path of the SAM template must be mounted on the Docker machine and modified to match the remote machine.\n* log_file:\nFile to capture output logs.\n* layer_cache_basedir:\nSpecify the location basedir where the lambda layers used by the template will be downloaded to.\n* skip_pull_image:\nSkip pulling down the latest Docker image for Lambda runtime.\n* docker_network:\nName or ID of an existing docker network for AWS Lambda docker containers to connect to, along with the default bridge network. If not specified, the Lambda containers will only connect to the default bridge docker network.\n* force_image_build:\nForce rebuilding the image used for invoking functions with layers.\n* warm_containers:\nOptional. Specifies how AWS SAM CLI manages containers for each function.' \n\nTwo modes are available:\nEAGER: Containers for all functions are loaded at startup and persist between invocations.\nLAZY: Containers are only loaded when each function is first invoked. \n Those containers persist for additional invocations.\n* debug_function:\nOptional. Specifies the Lambda Function logicalId to apply debug options to when --warm-containers is specified. This parameter applies to --debug-port, --debugger-path, and --debug-args.\n* shutdown:\nEmulate a shutdown event after invoke completes, to test extension handling of shutdown behavior.\n* container_host:\nHost of locally emulated Lambda container. This option is useful when the container runs on a different host than AWS SAM CLI. For example, if one wants to run AWS SAM CLI in a Docker container on macOS, this option could specify `host.docker.internal`\n* container_host_interface:\nIP address of the host network interface that container ports should bind to. Use 0.0.0.0 to bind to all interfaces.\n* add_host:\nPasses a hostname to IP address mapping to the Docker container's host file. This parameter can be passed multiple times.Example:--add-host example.com:127.0.0.1\n* container_dns:\nSet custom DNS servers for the Lambda container. This parameter can be passed multiple times to specify multiple DNS servers. Example: --container-dns 8.8.8.8 --container-dns 1.1.1.1\n* invoke_image:\nContainer image URIs for invoking functions or starting api and function. One can specify the image URI used for the local function invocation (--invoke-image public.ecr.aws/sam/build-nodejs20.x:latest). One can also specify for each individual function with (--invoke-image Function1=public.ecr.aws/sam/build-nodejs20.x:latest). If a function does not have invoke image specified, the default AWS SAM CLI emulation image will be used.\n* no_memory_limit:\nRemoves the Memory limit during emulation. With this parameter, the underlying container will run without a --memory parameter\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* save_params:\nSave the parameters provided via the command line to the configuration file.", + "description": "Available parameters for the local start api command:\n* terraform_plan_file:\nUsed for passing a custom plan file when executing the Terraform hook.\n* hook_name:\nHook package id to extend AWS SAM CLI commands functionality. \n\nExample: `terraform` to extend AWS SAM CLI commands functionality to support terraform applications. \n\nAvailable Hook Names: ['terraform']\n* skip_prepare_infra:\nSkip preparation stage when there are no infrastructure changes. Only used in conjunction with --hook-name.\n* host:\nLocal hostname or IP address to bind to (default: '127.0.0.1')\n* port:\nLocal port number to listen on (default: '3000')\n* static_dir:\nAny static assets (e.g. CSS/Javascript/HTML) files located in this directory will be presented at /\n* disable_authorizer:\nDisable custom Lambda Authorizers from being parsed and invoked.\n* ssl_cert_file:\nPath to SSL certificate file (default: None)\n* ssl_key_file:\nPath to SSL key file (default: None)\n* language_extensions:\nExpand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1.\n* template_file:\nAWS SAM template which references built artifacts for resources in the template. (if applicable)\n* env_vars:\nJSON file containing values for Lambda function's environment variables.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* debug_port:\nWhen specified, Lambda function container will start in debug mode and will expose this port on localhost.\n* debugger_path:\nHost path to a debugger that will be mounted into the Lambda container.\n* debug_args:\nAdditional arguments to be passed to the debugger.\n* container_env_vars:\nJSON file containing additional environment variables to be set within the container when used in a debugging session locally.\n* docker_volume_basedir:\nSpecify the location basedir where the SAM template exists. If Docker is running on a remote machine, Path of the SAM template must be mounted on the Docker machine and modified to match the remote machine.\n* log_file:\nFile to capture output logs.\n* layer_cache_basedir:\nSpecify the location basedir where the lambda layers used by the template will be downloaded to.\n* skip_pull_image:\nSkip pulling down the latest Docker image for Lambda runtime.\n* docker_network:\nName or ID of an existing docker network for AWS Lambda docker containers to connect to, along with the default bridge network. If not specified, the Lambda containers will only connect to the default bridge docker network.\n* force_image_build:\nForce rebuilding the image used for invoking functions with layers.\n* warm_containers:\nOptional. Specifies how AWS SAM CLI manages containers for each function.' \n\nTwo modes are available:\nEAGER: Containers for all functions are loaded at startup and persist between invocations.\nLAZY: Containers are only loaded when each function is first invoked. \n Those containers persist for additional invocations.\n* debug_function:\nOptional. Specifies the Lambda Function logicalId to apply debug options to when --warm-containers is specified. This parameter applies to --debug-port, --debugger-path, and --debug-args.\n* shutdown:\nEmulate a shutdown event after invoke completes, to test extension handling of shutdown behavior.\n* container_host:\nHost of locally emulated Lambda container. This option is useful when the container runs on a different host than AWS SAM CLI. For example, if one wants to run AWS SAM CLI in a Docker container on macOS, this option could specify `host.docker.internal`\n* container_host_interface:\nIP address of the host network interface that container ports should bind to. Use 0.0.0.0 to bind to all interfaces.\n* add_host:\nPasses a hostname to IP address mapping to the Docker container's host file. This parameter can be passed multiple times.Example:--add-host example.com:127.0.0.1\n* container_dns:\nSet custom DNS servers for the Lambda container. This parameter can be passed multiple times to specify multiple DNS servers. Example: --container-dns 8.8.8.8 --container-dns 1.1.1.1\n* invoke_image:\nContainer image URIs for invoking functions or starting api and function. One can specify the image URI used for the local function invocation (--invoke-image public.ecr.aws/sam/build-nodejs20.x:latest). One can also specify for each individual function with (--invoke-image Function1=public.ecr.aws/sam/build-nodejs20.x:latest). If a function does not have invoke image specified, the default AWS SAM CLI emulation image will be used.\n* no_memory_limit:\nRemoves the Memory limit during emulation. With this parameter, the underlying container will run without a --memory parameter\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* save_params:\nSave the parameters provided via the command line to the configuration file.", "type": "object", "properties": { "terraform_plan_file": { @@ -781,6 +796,11 @@ "type": "string", "description": "Path to SSL key file (default: None)" }, + "language_extensions": { + "title": "language_extensions", + "type": "boolean", + "description": "Expand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1." + }, "template_file": { "title": "template_file", "type": "string", @@ -946,7 +966,7 @@ "properties": { "parameters": { "title": "Parameters for the local start lambda command", - "description": "Available parameters for the local start lambda command:\n* terraform_plan_file:\nUsed for passing a custom plan file when executing the Terraform hook.\n* hook_name:\nHook package id to extend AWS SAM CLI commands functionality. \n\nExample: `terraform` to extend AWS SAM CLI commands functionality to support terraform applications. \n\nAvailable Hook Names: ['terraform']\n* skip_prepare_infra:\nSkip preparation stage when there are no infrastructure changes. Only used in conjunction with --hook-name.\n* host:\nLocal hostname or IP address to bind to (default: '127.0.0.1')\n* port:\nLocal port number to listen on (default: '3001')\n* template_file:\nAWS SAM template which references built artifacts for resources in the template. (if applicable)\n* env_vars:\nJSON file containing values for Lambda function's environment variables.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* debug_port:\nWhen specified, Lambda function container will start in debug mode and will expose this port on localhost.\n* debugger_path:\nHost path to a debugger that will be mounted into the Lambda container.\n* debug_args:\nAdditional arguments to be passed to the debugger.\n* container_env_vars:\nJSON file containing additional environment variables to be set within the container when used in a debugging session locally.\n* docker_volume_basedir:\nSpecify the location basedir where the SAM template exists. If Docker is running on a remote machine, Path of the SAM template must be mounted on the Docker machine and modified to match the remote machine.\n* log_file:\nFile to capture output logs.\n* layer_cache_basedir:\nSpecify the location basedir where the lambda layers used by the template will be downloaded to.\n* skip_pull_image:\nSkip pulling down the latest Docker image for Lambda runtime.\n* docker_network:\nName or ID of an existing docker network for AWS Lambda docker containers to connect to, along with the default bridge network. If not specified, the Lambda containers will only connect to the default bridge docker network.\n* force_image_build:\nForce rebuilding the image used for invoking functions with layers.\n* warm_containers:\nOptional. Specifies how AWS SAM CLI manages containers for each function.' \n\nTwo modes are available:\nEAGER: Containers for all functions are loaded at startup and persist between invocations.\nLAZY: Containers are only loaded when each function is first invoked. \n Those containers persist for additional invocations.\n* debug_function:\nOptional. Specifies the Lambda Function logicalId to apply debug options to when --warm-containers is specified. This parameter applies to --debug-port, --debugger-path, and --debug-args.\n* shutdown:\nEmulate a shutdown event after invoke completes, to test extension handling of shutdown behavior.\n* container_host:\nHost of locally emulated Lambda container. This option is useful when the container runs on a different host than AWS SAM CLI. For example, if one wants to run AWS SAM CLI in a Docker container on macOS, this option could specify `host.docker.internal`\n* container_host_interface:\nIP address of the host network interface that container ports should bind to. Use 0.0.0.0 to bind to all interfaces.\n* add_host:\nPasses a hostname to IP address mapping to the Docker container's host file. This parameter can be passed multiple times.Example:--add-host example.com:127.0.0.1\n* container_dns:\nSet custom DNS servers for the Lambda container. This parameter can be passed multiple times to specify multiple DNS servers. Example: --container-dns 8.8.8.8 --container-dns 1.1.1.1\n* invoke_image:\nContainer image URIs for invoking functions or starting api and function. One can specify the image URI used for the local function invocation (--invoke-image public.ecr.aws/sam/build-nodejs20.x:latest). One can also specify for each individual function with (--invoke-image Function1=public.ecr.aws/sam/build-nodejs20.x:latest). If a function does not have invoke image specified, the default AWS SAM CLI emulation image will be used.\n* no_memory_limit:\nRemoves the Memory limit during emulation. With this parameter, the underlying container will run without a --memory parameter\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* save_params:\nSave the parameters provided via the command line to the configuration file.", + "description": "Available parameters for the local start lambda command:\n* terraform_plan_file:\nUsed for passing a custom plan file when executing the Terraform hook.\n* hook_name:\nHook package id to extend AWS SAM CLI commands functionality. \n\nExample: `terraform` to extend AWS SAM CLI commands functionality to support terraform applications. \n\nAvailable Hook Names: ['terraform']\n* skip_prepare_infra:\nSkip preparation stage when there are no infrastructure changes. Only used in conjunction with --hook-name.\n* host:\nLocal hostname or IP address to bind to (default: '127.0.0.1')\n* port:\nLocal port number to listen on (default: '3001')\n* language_extensions:\nExpand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1.\n* template_file:\nAWS SAM template which references built artifacts for resources in the template. (if applicable)\n* env_vars:\nJSON file containing values for Lambda function's environment variables.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* debug_port:\nWhen specified, Lambda function container will start in debug mode and will expose this port on localhost.\n* debugger_path:\nHost path to a debugger that will be mounted into the Lambda container.\n* debug_args:\nAdditional arguments to be passed to the debugger.\n* container_env_vars:\nJSON file containing additional environment variables to be set within the container when used in a debugging session locally.\n* docker_volume_basedir:\nSpecify the location basedir where the SAM template exists. If Docker is running on a remote machine, Path of the SAM template must be mounted on the Docker machine and modified to match the remote machine.\n* log_file:\nFile to capture output logs.\n* layer_cache_basedir:\nSpecify the location basedir where the lambda layers used by the template will be downloaded to.\n* skip_pull_image:\nSkip pulling down the latest Docker image for Lambda runtime.\n* docker_network:\nName or ID of an existing docker network for AWS Lambda docker containers to connect to, along with the default bridge network. If not specified, the Lambda containers will only connect to the default bridge docker network.\n* force_image_build:\nForce rebuilding the image used for invoking functions with layers.\n* warm_containers:\nOptional. Specifies how AWS SAM CLI manages containers for each function.' \n\nTwo modes are available:\nEAGER: Containers for all functions are loaded at startup and persist between invocations.\nLAZY: Containers are only loaded when each function is first invoked. \n Those containers persist for additional invocations.\n* debug_function:\nOptional. Specifies the Lambda Function logicalId to apply debug options to when --warm-containers is specified. This parameter applies to --debug-port, --debugger-path, and --debug-args.\n* shutdown:\nEmulate a shutdown event after invoke completes, to test extension handling of shutdown behavior.\n* container_host:\nHost of locally emulated Lambda container. This option is useful when the container runs on a different host than AWS SAM CLI. For example, if one wants to run AWS SAM CLI in a Docker container on macOS, this option could specify `host.docker.internal`\n* container_host_interface:\nIP address of the host network interface that container ports should bind to. Use 0.0.0.0 to bind to all interfaces.\n* add_host:\nPasses a hostname to IP address mapping to the Docker container's host file. This parameter can be passed multiple times.Example:--add-host example.com:127.0.0.1\n* container_dns:\nSet custom DNS servers for the Lambda container. This parameter can be passed multiple times to specify multiple DNS servers. Example: --container-dns 8.8.8.8 --container-dns 1.1.1.1\n* invoke_image:\nContainer image URIs for invoking functions or starting api and function. One can specify the image URI used for the local function invocation (--invoke-image public.ecr.aws/sam/build-nodejs20.x:latest). One can also specify for each individual function with (--invoke-image Function1=public.ecr.aws/sam/build-nodejs20.x:latest). If a function does not have invoke image specified, the default AWS SAM CLI emulation image will be used.\n* no_memory_limit:\nRemoves the Memory limit during emulation. With this parameter, the underlying container will run without a --memory parameter\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* save_params:\nSave the parameters provided via the command line to the configuration file.", "type": "object", "properties": { "terraform_plan_file": { @@ -976,6 +996,11 @@ "description": "Local port number to listen on (default: '3001')", "default": 3001 }, + "language_extensions": { + "title": "language_extensions", + "type": "boolean", + "description": "Expand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1." + }, "template_file": { "title": "template_file", "type": "string", @@ -1141,7 +1166,7 @@ "properties": { "parameters": { "title": "Parameters for the package command", - "description": "Available parameters for the package command:\n* template_file:\nAWS SAM template which references built artifacts for resources in the template. (if applicable)\n* output_template_file:\nThe path to the file where the command writes the output AWS CloudFormation template. If you don't specify a path, the command writes the template to the standard output.\n* s3_bucket:\nAWS S3 bucket where artifacts referenced in the template are uploaded.\n* image_repository:\nAWS ECR repository URI where artifacts referenced in the template are uploaded.\n* image_repositories:\nMapping of Function Logical ID to AWS ECR Repository URI.\n\nExample: Function_Logical_ID=ECR_Repo_Uri\nThis option can be specified multiple times.\n* s3_prefix:\nPrefix name that is added to the artifact's name when it is uploaded to the AWS S3 bucket.\n* kms_key_id:\nThe ID of an AWS KMS key that is used to encrypt artifacts that are at rest in the AWS S3 bucket.\n* use_json:\nIndicates whether to use JSON as the format for the output AWS CloudFormation template. YAML is used by default.\n* force_upload:\nIndicates whether to override existing files in the S3 bucket. Specify this flag to upload artifacts even if they match existing artifacts in the S3 bucket.\n* resolve_s3:\nAutomatically resolve AWS S3 bucket for non-guided deployments. Enabling this option will also create a managed default AWS S3 bucket for you. If one does not provide a --s3-bucket value, the managed bucket will be used. Do not use --guided with this option.\n* resolve_image_repos:\nAutomatically create and delete ECR repositories for image-based functions in non-guided deployments. A companion stack containing ECR repos for each function will be deployed along with the template stack. Automatically created image repositories will be deleted if the corresponding functions are removed.\n* metadata:\nMap of metadata to attach to ALL the artifacts that are referenced in the template.\n* signing_profiles:\nA string that contains Code Sign configuration parameters as FunctionOrLayerNameToSign=SigningProfileName:SigningProfileOwner Since signing profile owner is optional, it could also be written as FunctionOrLayerNameToSign=SigningProfileName\n* no_progressbar:\nDoes not showcase a progress bar when uploading artifacts to S3 and pushing docker images to ECR\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* save_params:\nSave the parameters provided via the command line to the configuration file.", + "description": "Available parameters for the package command:\n* template_file:\nAWS SAM template which references built artifacts for resources in the template. (if applicable)\n* output_template_file:\nThe path to the file where the command writes the output AWS CloudFormation template. If you don't specify a path, the command writes the template to the standard output.\n* s3_bucket:\nAWS S3 bucket where artifacts referenced in the template are uploaded.\n* image_repository:\nAWS ECR repository URI where artifacts referenced in the template are uploaded.\n* image_repositories:\nMapping of Function Logical ID to AWS ECR Repository URI.\n\nExample: Function_Logical_ID=ECR_Repo_Uri\nThis option can be specified multiple times.\n* s3_prefix:\nPrefix name that is added to the artifact's name when it is uploaded to the AWS S3 bucket.\n* kms_key_id:\nThe ID of an AWS KMS key that is used to encrypt artifacts that are at rest in the AWS S3 bucket.\n* use_json:\nIndicates whether to use JSON as the format for the output AWS CloudFormation template. YAML is used by default.\n* force_upload:\nIndicates whether to override existing files in the S3 bucket. Specify this flag to upload artifacts even if they match existing artifacts in the S3 bucket.\n* resolve_s3:\nAutomatically resolve AWS S3 bucket for non-guided deployments. Enabling this option will also create a managed default AWS S3 bucket for you. If one does not provide a --s3-bucket value, the managed bucket will be used. Do not use --guided with this option.\n* resolve_image_repos:\nAutomatically create and delete ECR repositories for image-based functions in non-guided deployments. A companion stack containing ECR repos for each function will be deployed along with the template stack. Automatically created image repositories will be deleted if the corresponding functions are removed.\n* metadata:\nMap of metadata to attach to ALL the artifacts that are referenced in the template.\n* signing_profiles:\nA string that contains Code Sign configuration parameters as FunctionOrLayerNameToSign=SigningProfileName:SigningProfileOwner Since signing profile owner is optional, it could also be written as FunctionOrLayerNameToSign=SigningProfileName\n* no_progressbar:\nDoes not showcase a progress bar when uploading artifacts to S3 and pushing docker images to ECR\n* language_extensions:\nExpand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1.\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* save_params:\nSave the parameters provided via the command line to the configuration file.", "type": "object", "properties": { "template_file": { @@ -1218,6 +1243,11 @@ "type": "boolean", "description": "Does not showcase a progress bar when uploading artifacts to S3 and pushing docker images to ECR" }, + "language_extensions": { + "title": "language_extensions", + "type": "boolean", + "description": "Expand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1." + }, "beta_features": { "title": "beta_features", "type": "boolean", @@ -1256,7 +1286,7 @@ "properties": { "parameters": { "title": "Parameters for the deploy command", - "description": "Available parameters for the deploy command:\n* guided:\nSpecify this flag to allow SAM CLI to guide you through the deployment using guided prompts.\n* template_file:\nAWS SAM template which references built artifacts for resources in the template. (if applicable)\n* no_execute_changeset:\nIndicates whether to execute the change set. Specify this flag to view stack changes before executing the change set.\n* fail_on_empty_changeset:\nSpecify whether AWS SAM CLI should return a non-zero exit code if there are no changes to be made to the stack. Defaults to a non-zero exit code.\n* confirm_changeset:\nPrompt to confirm if the computed changeset is to be deployed by SAM CLI.\n* disable_rollback:\nPreserves the state of previously provisioned resources when an operation fails.\n* on_failure:\nProvide an action to determine what will happen when a stack fails to create. Three actions are available:\n\n- ROLLBACK: This will rollback a stack to a previous known good state.\n\n- DELETE: The stack will rollback to a previous state if one exists, otherwise the stack will be deleted.\n\n- DO_NOTHING: The stack will not rollback or delete, this is the same as disabling rollback.\n\nDefault behaviour is ROLLBACK.\n\n\n\nThis option is mutually exclusive with --disable-rollback/--no-disable-rollback. You can provide\n--on-failure or --disable-rollback/--no-disable-rollback but not both at the same time.\n* max_wait_duration:\nMaximum duration in minutes to wait for the deployment to complete.\n* stack_name:\nName of the AWS CloudFormation stack.\n* s3_bucket:\nAWS S3 bucket where artifacts referenced in the template are uploaded.\n* image_repository:\nAWS ECR repository URI where artifacts referenced in the template are uploaded.\n* image_repositories:\nMapping of Function Logical ID to AWS ECR Repository URI.\n\nExample: Function_Logical_ID=ECR_Repo_Uri\nThis option can be specified multiple times.\n* force_upload:\nIndicates whether to override existing files in the S3 bucket. Specify this flag to upload artifacts even if they match existing artifacts in the S3 bucket.\n* s3_prefix:\nPrefix name that is added to the artifact's name when it is uploaded to the AWS S3 bucket.\n* kms_key_id:\nThe ID of an AWS KMS key that is used to encrypt artifacts that are at rest in the AWS S3 bucket.\n* role_arn:\nARN of an IAM role that AWS Cloudformation assumes when executing a deployment change set.\n* use_json:\nIndicates whether to use JSON as the format for the output AWS CloudFormation template. YAML is used by default.\n* resolve_s3:\nAutomatically resolve AWS S3 bucket for non-guided deployments. Enabling this option will also create a managed default AWS S3 bucket for you. If one does not provide a --s3-bucket value, the managed bucket will be used. Do not use --guided with this option.\n* resolve_image_repos:\nAutomatically create and delete ECR repositories for image-based functions in non-guided deployments. A companion stack containing ECR repos for each function will be deployed along with the template stack. Automatically created image repositories will be deleted if the corresponding functions are removed.\n* metadata:\nMap of metadata to attach to ALL the artifacts that are referenced in the template.\n* notification_arns:\nARNs of SNS topics that AWS Cloudformation associates with the stack.\n* tags:\nList of tags to associate with the stack.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* signing_profiles:\nA string that contains Code Sign configuration parameters as FunctionOrLayerNameToSign=SigningProfileName:SigningProfileOwner Since signing profile owner is optional, it could also be written as FunctionOrLayerNameToSign=SigningProfileName\n* no_progressbar:\nDoes not showcase a progress bar when uploading artifacts to S3 and pushing docker images to ECR\n* capabilities:\nList of capabilities that one must specify before AWS Cloudformation can create certain stacks.\n\nAccepted Values: CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_RESOURCE_POLICY, CAPABILITY_AUTO_EXPAND.\n\nLearn more at: https://docs.aws.amazon.com/serverlessrepo/latest/devguide/acknowledging-application-capabilities.html\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* save_params:\nSave the parameters provided via the command line to the configuration file.", + "description": "Available parameters for the deploy command:\n* guided:\nSpecify this flag to allow SAM CLI to guide you through the deployment using guided prompts.\n* template_file:\nAWS SAM template which references built artifacts for resources in the template. (if applicable)\n* no_execute_changeset:\nIndicates whether to execute the change set. Specify this flag to view stack changes before executing the change set.\n* fail_on_empty_changeset:\nSpecify whether AWS SAM CLI should return a non-zero exit code if there are no changes to be made to the stack. Defaults to a non-zero exit code.\n* confirm_changeset:\nPrompt to confirm if the computed changeset is to be deployed by SAM CLI.\n* disable_rollback:\nPreserves the state of previously provisioned resources when an operation fails.\n* on_failure:\nProvide an action to determine what will happen when a stack fails to create. Three actions are available:\n\n- ROLLBACK: This will rollback a stack to a previous known good state.\n\n- DELETE: The stack will rollback to a previous state if one exists, otherwise the stack will be deleted.\n\n- DO_NOTHING: The stack will not rollback or delete, this is the same as disabling rollback.\n\nDefault behaviour is ROLLBACK.\n\n\n\nThis option is mutually exclusive with --disable-rollback/--no-disable-rollback. You can provide\n--on-failure or --disable-rollback/--no-disable-rollback but not both at the same time.\n* max_wait_duration:\nMaximum duration in minutes to wait for the deployment to complete.\n* stack_name:\nName of the AWS CloudFormation stack.\n* s3_bucket:\nAWS S3 bucket where artifacts referenced in the template are uploaded.\n* image_repository:\nAWS ECR repository URI where artifacts referenced in the template are uploaded.\n* image_repositories:\nMapping of Function Logical ID to AWS ECR Repository URI.\n\nExample: Function_Logical_ID=ECR_Repo_Uri\nThis option can be specified multiple times.\n* force_upload:\nIndicates whether to override existing files in the S3 bucket. Specify this flag to upload artifacts even if they match existing artifacts in the S3 bucket.\n* s3_prefix:\nPrefix name that is added to the artifact's name when it is uploaded to the AWS S3 bucket.\n* kms_key_id:\nThe ID of an AWS KMS key that is used to encrypt artifacts that are at rest in the AWS S3 bucket.\n* role_arn:\nARN of an IAM role that AWS Cloudformation assumes when executing a deployment change set.\n* use_json:\nIndicates whether to use JSON as the format for the output AWS CloudFormation template. YAML is used by default.\n* resolve_s3:\nAutomatically resolve AWS S3 bucket for non-guided deployments. Enabling this option will also create a managed default AWS S3 bucket for you. If one does not provide a --s3-bucket value, the managed bucket will be used. Do not use --guided with this option.\n* resolve_image_repos:\nAutomatically create and delete ECR repositories for image-based functions in non-guided deployments. A companion stack containing ECR repos for each function will be deployed along with the template stack. Automatically created image repositories will be deleted if the corresponding functions are removed.\n* metadata:\nMap of metadata to attach to ALL the artifacts that are referenced in the template.\n* notification_arns:\nARNs of SNS topics that AWS Cloudformation associates with the stack.\n* tags:\nList of tags to associate with the stack.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* signing_profiles:\nA string that contains Code Sign configuration parameters as FunctionOrLayerNameToSign=SigningProfileName:SigningProfileOwner Since signing profile owner is optional, it could also be written as FunctionOrLayerNameToSign=SigningProfileName\n* no_progressbar:\nDoes not showcase a progress bar when uploading artifacts to S3 and pushing docker images to ECR\n* capabilities:\nList of capabilities that one must specify before AWS Cloudformation can create certain stacks.\n\nAccepted Values: CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_RESOURCE_POLICY, CAPABILITY_AUTO_EXPAND.\n\nLearn more at: https://docs.aws.amazon.com/serverlessrepo/latest/devguide/acknowledging-application-capabilities.html\n* language_extensions:\nExpand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* save_params:\nSave the parameters provided via the command line to the configuration file.", "type": "object", "properties": { "guided": { @@ -1426,6 +1456,11 @@ "type": "string" } }, + "language_extensions": { + "title": "language_extensions", + "type": "boolean", + "description": "Expand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1." + }, "profile": { "title": "profile", "type": "string", @@ -1744,7 +1779,7 @@ "properties": { "parameters": { "title": "Parameters for the sync command", - "description": "Available parameters for the sync command:\n* template_file:\nAWS SAM template file.\n* code:\nSync ONLY code resources. This includes Lambda Functions, API Gateway, and Step Functions.\n* watch:\nWatch local files and automatically sync with cloud.\n* resource_id:\nSync code for all the resources with the ID. To sync a resource within a nested stack, use the following pattern {ChildStack}/{logicalId}.\n* resource:\nSync code for all resources of the given resource type. Accepted values are ['AWS::Serverless::Function', 'AWS::Lambda::Function', 'AWS::Serverless::LayerVersion', 'AWS::Lambda::LayerVersion', 'AWS::Serverless::Api', 'AWS::ApiGateway::RestApi', 'AWS::Serverless::HttpApi', 'AWS::ApiGatewayV2::Api', 'AWS::Serverless::StateMachine', 'AWS::StepFunctions::StateMachine']\n* dependency_layer:\nSeparate dependencies of individual function into a Lambda layer for improved performance.\n* skip_deploy_sync:\nThis option will skip the initial infrastructure deployment if it is not required by comparing the local template with the template deployed in cloud.\n* container_env_var_file:\nEnvironment variables json file (e.g., env_vars.json) to be passed to containers.\n* watch_exclude:\nExcludes a file or folder from being observed for file changes. Files and folders that are excluded will not trigger a sync workflow. This option can be provided multiple times.\n\nExamples:\n\nHelloWorldFunction=package-lock.json\n\nChildStackA/FunctionName=database.sqlite3\n* stack_name:\nName of the AWS CloudFormation stack.\n* base_dir:\nResolve relative paths to function's source code with respect to this directory. Use this if SAM template and source code are not in same enclosing folder. By default, relative paths are resolved with respect to the SAM template's location.\n* use_container:\nBuild functions within an AWS Lambda-like container.\n* build_in_source:\nOpts in to build project in the source folder. The following workflows support building in source: ['nodejs16.x', 'nodejs18.x', 'nodejs20.x', 'nodejs22.x', 'Makefile', 'esbuild']\n* build_image:\nContainer image URIs for building functions/layers. You can specify for all functions/layers with just the image URI (--build-image public.ecr.aws/sam/build-nodejs18.x:latest). You can specify for each individual function with (--build-image FunctionLogicalID=public.ecr.aws/sam/build-nodejs18.x:latest). A combination of the two can be used. If a function does not have build image specified or an image URI for all functions, the default SAM CLI build images will be used.\n* image_repository:\nAWS ECR repository URI where artifacts referenced in the template are uploaded.\n* image_repositories:\nMapping of Function Logical ID to AWS ECR Repository URI.\n\nExample: Function_Logical_ID=ECR_Repo_Uri\nThis option can be specified multiple times.\n* s3_bucket:\nAWS S3 bucket where artifacts referenced in the template are uploaded.\n* s3_prefix:\nPrefix name that is added to the artifact's name when it is uploaded to the AWS S3 bucket.\n* kms_key_id:\nThe ID of an AWS KMS key that is used to encrypt artifacts that are at rest in the AWS S3 bucket.\n* role_arn:\nARN of an IAM role that AWS Cloudformation assumes when executing a deployment change set.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* metadata:\nMap of metadata to attach to ALL the artifacts that are referenced in the template.\n* notification_arns:\nARNs of SNS topics that AWS Cloudformation associates with the stack.\n* tags:\nList of tags to associate with the stack.\n* capabilities:\nList of capabilities that one must specify before AWS Cloudformation can create certain stacks.\n\nAccepted Values: CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_RESOURCE_POLICY, CAPABILITY_AUTO_EXPAND.\n\nLearn more at: https://docs.aws.amazon.com/serverlessrepo/latest/devguide/acknowledging-application-capabilities.html\n* save_params:\nSave the parameters provided via the command line to the configuration file.", + "description": "Available parameters for the sync command:\n* template_file:\nAWS SAM template file.\n* code:\nSync ONLY code resources. This includes Lambda Functions, API Gateway, and Step Functions.\n* watch:\nWatch local files and automatically sync with cloud.\n* resource_id:\nSync code for all the resources with the ID. To sync a resource within a nested stack, use the following pattern {ChildStack}/{logicalId}.\n* resource:\nSync code for all resources of the given resource type. Accepted values are ['AWS::Serverless::Function', 'AWS::Lambda::Function', 'AWS::Serverless::LayerVersion', 'AWS::Lambda::LayerVersion', 'AWS::Serverless::Api', 'AWS::ApiGateway::RestApi', 'AWS::Serverless::HttpApi', 'AWS::ApiGatewayV2::Api', 'AWS::Serverless::StateMachine', 'AWS::StepFunctions::StateMachine']\n* dependency_layer:\nSeparate dependencies of individual function into a Lambda layer for improved performance.\n* skip_deploy_sync:\nThis option will skip the initial infrastructure deployment if it is not required by comparing the local template with the template deployed in cloud.\n* container_env_var_file:\nEnvironment variables json file (e.g., env_vars.json) to be passed to containers.\n* watch_exclude:\nExcludes a file or folder from being observed for file changes. Files and folders that are excluded will not trigger a sync workflow. This option can be provided multiple times.\n\nExamples:\n\nHelloWorldFunction=package-lock.json\n\nChildStackA/FunctionName=database.sqlite3\n* stack_name:\nName of the AWS CloudFormation stack.\n* base_dir:\nResolve relative paths to function's source code with respect to this directory. Use this if SAM template and source code are not in same enclosing folder. By default, relative paths are resolved with respect to the SAM template's location.\n* use_container:\nBuild functions within an AWS Lambda-like container.\n* build_in_source:\nOpts in to build project in the source folder. The following workflows support building in source: ['nodejs16.x', 'nodejs18.x', 'nodejs20.x', 'nodejs22.x', 'Makefile', 'esbuild']\n* build_image:\nContainer image URIs for building functions/layers. You can specify for all functions/layers with just the image URI (--build-image public.ecr.aws/sam/build-nodejs18.x:latest). You can specify for each individual function with (--build-image FunctionLogicalID=public.ecr.aws/sam/build-nodejs18.x:latest). A combination of the two can be used. If a function does not have build image specified or an image URI for all functions, the default SAM CLI build images will be used.\n* image_repository:\nAWS ECR repository URI where artifacts referenced in the template are uploaded.\n* image_repositories:\nMapping of Function Logical ID to AWS ECR Repository URI.\n\nExample: Function_Logical_ID=ECR_Repo_Uri\nThis option can be specified multiple times.\n* s3_bucket:\nAWS S3 bucket where artifacts referenced in the template are uploaded.\n* s3_prefix:\nPrefix name that is added to the artifact's name when it is uploaded to the AWS S3 bucket.\n* kms_key_id:\nThe ID of an AWS KMS key that is used to encrypt artifacts that are at rest in the AWS S3 bucket.\n* role_arn:\nARN of an IAM role that AWS Cloudformation assumes when executing a deployment change set.\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* metadata:\nMap of metadata to attach to ALL the artifacts that are referenced in the template.\n* notification_arns:\nARNs of SNS topics that AWS Cloudformation associates with the stack.\n* tags:\nList of tags to associate with the stack.\n* capabilities:\nList of capabilities that one must specify before AWS Cloudformation can create certain stacks.\n\nAccepted Values: CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_RESOURCE_POLICY, CAPABILITY_AUTO_EXPAND.\n\nLearn more at: https://docs.aws.amazon.com/serverlessrepo/latest/devguide/acknowledging-application-capabilities.html\n* language_extensions:\nExpand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1.\n* save_params:\nSave the parameters provided via the command line to the configuration file.", "type": "object", "properties": { "template_file": { @@ -1942,6 +1977,11 @@ "type": "string" } }, + "language_extensions": { + "title": "language_extensions", + "type": "boolean", + "description": "Expand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before running SAM transforms. Off by default. Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1." + }, "save_params": { "title": "save_params", "type": "boolean", diff --git a/tests/integration/buildcmd/test_build_cmd.py b/tests/integration/buildcmd/test_build_cmd.py index cc65e75c290..536769a9e38 100644 --- a/tests/integration/buildcmd/test_build_cmd.py +++ b/tests/integration/buildcmd/test_build_cmd.py @@ -2168,7 +2168,7 @@ class TestBuildWithLanguageExtensions(BuildIntegBase): template = "language-extensions.yaml" def test_validation_does_not_error_out(self): - cmdlist = self.get_command_list() + cmdlist = self.get_command_list() + ["--language-extensions"] process_execute = run_command(cmdlist, cwd=self.working_dir) self.assertEqual(process_execute.process.returncode, 0) self.assertIn("template.yaml", os.listdir(self.default_build_dir)) diff --git a/tests/integration/buildcmd/test_build_cmd_language_extensions.py b/tests/integration/buildcmd/test_build_cmd_language_extensions.py index 829c9a2c576..eb779cc2f15 100644 --- a/tests/integration/buildcmd/test_build_cmd_language_extensions.py +++ b/tests/integration/buildcmd/test_build_cmd_language_extensions.py @@ -53,7 +53,7 @@ def test_build_with_foreach_template(self): """Test that sam build expands Fn::ForEach and builds each generated function.""" runtime = self._get_python_version() overrides = {"Runtime": runtime} - cmdlist = self.get_command_list(parameter_overrides=overrides) + cmdlist = self.get_command_list(parameter_overrides=overrides) + ["--language-extensions"] command_result = run_command(cmdlist, cwd=self.working_dir) @@ -71,7 +71,7 @@ def test_build_dynamic_codeuri_generates_mappings(self): runtime = self._get_python_version() overrides = {"Runtime": runtime} - cmdlist = self.get_command_list(parameter_overrides=overrides) + cmdlist = self.get_command_list(parameter_overrides=overrides) + ["--language-extensions"] command_result = run_command(cmdlist, cwd=self.working_dir) self.assertEqual(command_result.process.returncode, 0, f"Build failed: {command_result.stderr.decode('utf-8')}") @@ -119,7 +119,7 @@ def test_build_nested_foreach_dynamic_codeuri_generates_mappings(self): runtime = self._get_python_version() overrides = {"Runtime": runtime} - cmdlist = self.get_command_list(parameter_overrides=overrides) + cmdlist = self.get_command_list(parameter_overrides=overrides) + ["--language-extensions"] command_result = run_command(cmdlist, cwd=self.working_dir) self.assertEqual(command_result.process.returncode, 0, f"Build failed: {command_result.stderr.decode('utf-8')}") @@ -147,3 +147,28 @@ def test_build_nested_foreach_dynamic_codeuri_generates_mappings(self): for svc in ["Users", "Orders"]: func_dir = self.default_build_dir.joinpath(f"{env}{svc}Function") self.assertTrue(func_dir.exists(), f"Build artifact for {env}{svc}Function should exist") + + def test_build_without_flag_does_not_expand_foreach(self): + """Without --language-extensions, sam build does not expand Fn::ForEach.""" + runtime = self._get_python_version() + overrides = {"Runtime": runtime} + cmdlist = self.get_command_list(parameter_overrides=overrides) + + command_result = run_command(cmdlist, cwd=self.working_dir) + + if command_result.process.returncode == 0: + build_dir_files = os.listdir(str(self.default_build_dir)) + for function_name in self.FOREACH_GENERATED_FUNCTIONS: + self.assertNotIn( + function_name, + build_dir_files, + f"{function_name} should not exist when --language-extensions is off", + ) + else: + # Build failed because SAM transform cannot process unexpanded + # Fn::ForEach — this is acceptable opt-out behavior. + stderr = command_result.stderr.decode("utf-8", errors="replace") + self.assertTrue( + "ForEach" in stderr or "Invalid" in stderr or "Error" in stderr, + f"Build failure should relate to unexpanded template, got: {stderr[:200]}", + ) diff --git a/tests/integration/deploy/test_deploy_command.py b/tests/integration/deploy/test_deploy_command.py index cd66773921f..210b9b9d59a 100644 --- a/tests/integration/deploy/test_deploy_command.py +++ b/tests/integration/deploy/test_deploy_command.py @@ -20,6 +20,7 @@ RUN_BY_CANARY, SKIP_LMI_TESTS, UpdatableSARTemplate, + get_sam_command, ) # Deploy tests require credentials and CI/CD will only add credentials to the env if the PR is from the same repo. @@ -1717,9 +1718,29 @@ def test_deploy_with_language_extensions(self): stack_name = self._method_to_stack_name(self.id()) self.stacks.append({"name": stack_name}) + # Build first — sam deploy of a raw ForEach template requires a build step + build_dir = tempfile.mkdtemp() + build_command_list = [ + get_sam_command(), + "build", + "-t", + str(template), + "-b", + build_dir, + "--language-extensions", + ] + build_process = self.run_command(build_command_list) + self.assertEqual(build_process.process.returncode, 0, f"Build failed: {build_process.stderr}") + + built_template = Path(build_dir) / "template.yaml" deploy_command_list = self.get_deploy_command_list( - template_file=template, stack_name=stack_name, s3_prefix=self.s3_prefix, capabilities="CAPABILITY_IAM" + template_file=built_template, + stack_name=stack_name, + s3_prefix=self.s3_prefix, + s3_bucket=self.s3_bucket.name, + capabilities="CAPABILITY_IAM", ) + deploy_command_list.append("--language-extensions") deploy_process_execute = self.run_command(deploy_command_list) self.assertEqual(deploy_process_execute.process.returncode, 0) @@ -1811,6 +1832,7 @@ def test_package_and_deploy_language_extensions_dynamic_codeuri(self): s3_prefix=self.s3_prefix, output_template_file=output_template_path, ) + package_command_list.append("--language-extensions") package_process = self.run_command(command_list=package_command_list) self.assertEqual(package_process.process.returncode, 0) @@ -1865,7 +1887,7 @@ def test_package_and_deploy_language_extensions_dynamic_codeuri(self): tags="integ=true clarity=yes foo_bar=baz", confirm_changeset=False, ) - + deploy_command_list.append("--language-extensions") deploy_process = self.run_command(deploy_command_list) self.assertEqual(deploy_process.process.returncode, 0) finally: diff --git a/tests/integration/local/invoke/test_invoke_language_extensions.py b/tests/integration/local/invoke/test_invoke_language_extensions.py index 4e47f5a2593..ce0ac569a22 100644 --- a/tests/integration/local/invoke/test_invoke_language_extensions.py +++ b/tests/integration/local/invoke/test_invoke_language_extensions.py @@ -58,6 +58,7 @@ def test_invoke_foreach_expanded_function(self): str(self.build_dir), "--parameter-overrides", self._make_parameter_override_arg(overrides), + "--language-extensions", ] build_result = run_command(build_command, cwd=self.working_dir) @@ -74,7 +75,7 @@ def test_invoke_foreach_expanded_function(self): function_to_invoke="AlphaFunction", template_path=str(self.built_template_path), no_event=True, - ) + ) + ["--language-extensions"] stdout, stderr, invoke_exit_code = self.run_command(invoke_command, cwd=self.working_dir) self.assertEqual(invoke_exit_code, 0, f"invoke failed: stdout={stdout}, stderr={stderr}") diff --git a/tests/integration/local/start_api/test_start_api_language_extensions.py b/tests/integration/local/start_api/test_start_api_language_extensions.py index 85101f044a6..d0dd6ed8f14 100644 --- a/tests/integration/local/start_api/test_start_api_language_extensions.py +++ b/tests/integration/local/start_api/test_start_api_language_extensions.py @@ -5,24 +5,46 @@ """ import sys +from pathlib import Path import pytest import requests from .start_api_integ_base import StartApiIntegBaseClass +from tests.testing_utils import get_sam_command, run_command class TestStartApiLanguageExtensions(StartApiIntegBaseClass): """Integration tests for sam local start-api with Fn::ForEach-expanded endpoints.""" template_path = "/testdata/start_api/language-extensions-api.yaml" + build_before_invoke = True @classmethod def setUpClass(cls): python_version = f"python{sys.version_info.major}.{sys.version_info.minor}" cls.parameter_overrides = {"Runtime": python_version} + cls.build_overrides = {"Runtime": python_version} super().setUpClass() + @classmethod + def start_api(cls): + command = get_sam_command() + cls.command_list = [command, "local", "start-api", "-t", cls.template, "--language-extensions"] + super().start_api() + + @classmethod + def build(cls): + command = get_sam_command() + command_list = [command, "build", "--language-extensions"] + if cls.build_overrides: + overrides_arg = " ".join( + ["ParameterKey={},ParameterValue={}".format(key, value) for key, value in cls.build_overrides.items()] + ) + command_list += ["--parameter-overrides", overrides_arg] + working_dir = str(Path(cls.template).resolve().parents[0]) + run_command(command_list, cwd=working_dir) + def setUp(self): self.url = f"http://127.0.0.1:{self.port}" diff --git a/tests/integration/package/test_package_command_language_extensions.py b/tests/integration/package/test_package_command_language_extensions.py index 75b978ac4f5..2c71e295565 100644 --- a/tests/integration/package/test_package_command_language_extensions.py +++ b/tests/integration/package/test_package_command_language_extensions.py @@ -41,6 +41,7 @@ def test_package_preserves_foreach_structure_with_static_codeuri(self): output_template_file=output_template.name, force_upload=True, ) + command_list.append("--language-extensions") process = Popen(command_list, stdout=PIPE, stderr=PIPE) try: @@ -86,6 +87,7 @@ def test_package_generates_mappings_for_dynamic_codeuri(self): output_template_file=output_template.name, force_upload=True, ) + command_list.append("--language-extensions") process = Popen(command_list, stdout=PIPE, stderr=PIPE) try: diff --git a/tests/integration/validate/test_validate_language_extensions.py b/tests/integration/validate/test_validate_language_extensions.py index f739599bb26..158bf80ad5b 100644 --- a/tests/integration/validate/test_validate_language_extensions.py +++ b/tests/integration/validate/test_validate_language_extensions.py @@ -28,7 +28,7 @@ def test_depth_limit_validation_fails(self): """Nested ForEach exceeding depth limit should fail validation.""" template_path = self.test_data_path.joinpath("language-extensions-depth-limit", "template.yaml") - cmdlist = [get_sam_command(), "validate", "--template-file", str(template_path)] + cmdlist = [get_sam_command(), "validate", "--template-file", str(template_path), "--language-extensions"] result = run_command(cmdlist) self.assertNotEqual(result.process.returncode, 0) @@ -41,7 +41,7 @@ def test_invalid_syntax_validation_fails(self): """Invalid ForEach syntax should fail validation.""" template_path = self.test_data_path.joinpath("language-extensions-invalid-syntax", "template.yaml") - cmdlist = [get_sam_command(), "validate", "--template-file", str(template_path)] + cmdlist = [get_sam_command(), "validate", "--template-file", str(template_path), "--language-extensions"] result = run_command(cmdlist) self.assertNotEqual(result.process.returncode, 0) diff --git a/tests/unit/commands/buildcmd/test_build_context.py b/tests/unit/commands/buildcmd/test_build_context.py index 657bb0cb9e2..036a3e959f2 100644 --- a/tests/unit/commands/buildcmd/test_build_context.py +++ b/tests/unit/commands/buildcmd/test_build_context.py @@ -164,6 +164,7 @@ def test_must_setup_context( "template_file", parameter_overrides={"overrides": "value"}, global_parameter_overrides={"AWS::Region": "any_aws_region"}, + language_extensions_enabled=False, ) SamFunctionProviderMock.assert_called_once_with([stack], False, locate_layer_nested=False) pathlib_mock.Path.assert_called_once_with("template_file") @@ -499,7 +500,10 @@ def test_must_return_many_functions_to_build( self.assertEqual(resources_to_build.functions, [func1, func2, func6]) self.assertEqual(resources_to_build.layers, [layer1]) get_buildable_stacks_mock.assert_called_once_with( - "template_file", parameter_overrides={"overrides": "value"}, global_parameter_overrides=None + "template_file", + parameter_overrides={"overrides": "value"}, + global_parameter_overrides=None, + language_extensions_enabled=False, ) SamFunctionProviderMock.assert_called_once_with([stack], False, locate_layer_nested=False) pathlib_mock.Path.assert_called_once_with("template_file") @@ -2277,3 +2281,41 @@ def test_no_variable_present(self): from samcli.lib.cfn_language_extensions.sam_integration import substitute_loop_variable self.assertEqual(substitute_loop_variable("StaticFunction", "Name", "Alpha"), "StaticFunction") + + +class TestBuildContextLanguageExtensions(TestCase): + """language_extensions kwarg resolves to a bool stored on the context.""" + + def _ctx(self, **kwargs): + from samcli.commands.build.build_context import BuildContext + + defaults = dict( + resource_identifier=None, + template_file="template.yaml", + base_dir=None, + build_dir="build", + cache_dir=".cache", + cached=False, + parallel=False, + mode=None, + ) + defaults.update(kwargs) + return BuildContext(**defaults) + + def test_default_is_false(self): + ctx = self._ctx() + assert ctx.language_extensions_enabled is False + + def test_explicit_true(self): + ctx = self._ctx(language_extensions=True) + assert ctx.language_extensions_enabled is True + + def test_explicit_false_overrides_env(self): + with patch.dict(os.environ, {"SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS": "1"}, clear=False): + ctx = self._ctx(language_extensions=False) + assert ctx.language_extensions_enabled is False + + def test_none_with_env_truthy(self): + with patch.dict(os.environ, {"SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS": "true"}, clear=False): + ctx = self._ctx(language_extensions=None) + assert ctx.language_extensions_enabled is True diff --git a/tests/unit/commands/buildcmd/test_command.py b/tests/unit/commands/buildcmd/test_command.py index 365b18c011e..d32b27bf6f5 100644 --- a/tests/unit/commands/buildcmd/test_command.py +++ b/tests/unit/commands/buildcmd/test_command.py @@ -41,6 +41,7 @@ def test_must_succeed_build(self, os_mock, BuildContextMock, mock_build_click): mount_with=MountMode.READ, mount_symlinks=True, use_buildkit=False, + language_extensions=None, ) BuildContextMock.assert_called_with( @@ -68,6 +69,7 @@ def test_must_succeed_build(self, os_mock, BuildContextMock, mock_build_click): mount_with=MountMode.READ, mount_symlinks=True, use_buildkit=False, + language_extensions=None, ) ctx_mock.run.assert_called_with() self.assertEqual(ctx_mock.run.call_count, 1) diff --git a/tests/unit/commands/deploy/test_command.py b/tests/unit/commands/deploy/test_command.py index 51b3b89bc8d..b9e23e00ade 100644 --- a/tests/unit/commands/deploy/test_command.py +++ b/tests/unit/commands/deploy/test_command.py @@ -102,6 +102,7 @@ def test_all_args(self, mock_deploy_context, mock_deploy_click, mock_package_con config_env=self.config_env, config_file=self.config_file, resolve_image_repos=self.resolve_image_repos, + language_extensions=None, disable_rollback=self.disable_rollback, on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, @@ -133,6 +134,7 @@ def test_all_args(self, mock_deploy_context, mock_deploy_click, mock_package_con poll_delay=os.getenv("SAM_CLI_POLL_DELAY"), on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, + language_extensions=None, ) context_mock.run.assert_called_with() @@ -220,6 +222,7 @@ def test_all_args_guided_no_to_authorization_confirmation_prompt( config_env=self.config_env, config_file=self.config_file, resolve_image_repos=self.resolve_image_repos, + language_extensions=None, disable_rollback=self.disable_rollback, on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, @@ -322,6 +325,7 @@ def test_all_args_guided_use_defaults( config_env=self.config_env, config_file=self.config_file, resolve_image_repos=self.resolve_image_repos, + language_extensions=None, disable_rollback=self.disable_rollback, on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, @@ -350,9 +354,10 @@ def test_all_args_guided_use_defaults( signing_profiles=self.signing_profiles, use_changeset=self.use_changeset, disable_rollback=True, - poll_delay=5, + poll_delay=5.0, on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, + language_extensions=None, ) context_mock.run.assert_called_with() @@ -468,6 +473,7 @@ def test_all_args_guided( config_env=self.config_env, config_file=self.config_file, resolve_image_repos=self.resolve_image_repos, + language_extensions=None, disable_rollback=self.disable_rollback, on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, @@ -496,9 +502,10 @@ def test_all_args_guided( signing_profiles=self.signing_profiles, use_changeset=self.use_changeset, disable_rollback=True, - poll_delay=5, + poll_delay=5.0, on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, + language_extensions=None, ) context_mock.run.assert_called_with() @@ -617,6 +624,7 @@ def test_all_args_guided_no_save_echo_param_to_config( config_env=self.config_env, config_file=self.config_file, resolve_image_repos=self.resolve_image_repos, + language_extensions=None, disable_rollback=self.disable_rollback, on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, @@ -649,9 +657,10 @@ def test_all_args_guided_no_save_echo_param_to_config( signing_profiles=self.signing_profiles, use_changeset=self.use_changeset, disable_rollback=True, - poll_delay=5, + poll_delay=5.0, on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, + language_extensions=None, ) context_mock.run.assert_called_with() @@ -778,6 +787,7 @@ def test_all_args_guided_no_params_save_config( config_file=self.config_file, signing_profiles=self.signing_profiles, resolve_image_repos=self.resolve_image_repos, + language_extensions=None, disable_rollback=self.disable_rollback, on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, @@ -806,9 +816,10 @@ def test_all_args_guided_no_params_save_config( signing_profiles=self.signing_profiles, use_changeset=self.use_changeset, disable_rollback=True, - poll_delay=5, + poll_delay=5.0, on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, + language_extensions=None, ) context_mock.run.assert_called_with() @@ -919,6 +930,7 @@ def test_all_args_guided_no_params_no_save_config( config_env=self.config_env, signing_profiles=self.signing_profiles, resolve_image_repos=self.resolve_image_repos, + language_extensions=None, disable_rollback=self.disable_rollback, on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, @@ -947,9 +959,10 @@ def test_all_args_guided_no_params_no_save_config( signing_profiles=self.signing_profiles, use_changeset=self.use_changeset, disable_rollback=self.disable_rollback, - poll_delay=5, + poll_delay=5.0, on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, + language_extensions=None, ) context_mock.run.assert_called_with() @@ -997,6 +1010,7 @@ def test_all_args_resolve_s3( config_env=self.config_env, signing_profiles=self.signing_profiles, resolve_image_repos=self.resolve_image_repos, + language_extensions=None, disable_rollback=self.disable_rollback, on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, @@ -1028,6 +1042,7 @@ def test_all_args_resolve_s3( poll_delay=5, on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, + language_extensions=None, ) context_mock.run.assert_called_with() @@ -1063,6 +1078,7 @@ def test_resolve_s3_and_s3_bucket_both_set(self): config_env=self.config_env, signing_profiles=self.signing_profiles, resolve_image_repos=self.resolve_image_repos, + language_extensions=None, disable_rollback=self.disable_rollback, on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, @@ -1115,6 +1131,7 @@ def test_all_args_resolve_image_repos( config_env=self.config_env, signing_profiles=self.signing_profiles, resolve_image_repos=True, + language_extensions=None, disable_rollback=self.disable_rollback, on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, @@ -1143,9 +1160,10 @@ def test_all_args_resolve_image_repos( signing_profiles=self.signing_profiles, use_changeset=True, disable_rollback=self.disable_rollback, - poll_delay=5, + poll_delay=5.0, on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, + language_extensions=None, ) context_mock.run.assert_called_with() @@ -1190,6 +1208,7 @@ def test_passing_parameter_overrides_to_context( config_env=self.config_env, config_file=self.config_file, resolve_image_repos=self.resolve_image_repos, + language_extensions=None, disable_rollback=self.disable_rollback, on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, @@ -1221,6 +1240,7 @@ def test_passing_parameter_overrides_to_context( poll_delay=os.getenv("SAM_CLI_POLL_DELAY"), on_failure=self.on_failure, max_wait_duration=self.max_wait_duration, + language_extensions=None, ) mock_package_context.assert_called_with( @@ -1240,6 +1260,7 @@ def test_passing_parameter_overrides_to_context( profile=self.profile, signing_profiles=self.signing_profiles, parameter_overrides=self.parameter_overrides, + language_extensions=None, ) context_mock.run.assert_called_with() diff --git a/tests/unit/commands/deploy/test_deploy_context.py b/tests/unit/commands/deploy/test_deploy_context.py index 844d5138d19..840a3bbe062 100644 --- a/tests/unit/commands/deploy/test_deploy_context.py +++ b/tests/unit/commands/deploy/test_deploy_context.py @@ -169,7 +169,10 @@ def test_template_valid_execute_changeset_with_parameters( [{"ParameterKey": "a", "ParameterValue": "b"}, {"ParameterKey": "c", "UsePreviousValue": True}], ) patched_get_buildable_stacks.assert_called_once_with( - ANY, parameter_overrides={"a": "b"}, global_parameter_overrides={"AWS::Region": "any-aws-region"} + ANY, + parameter_overrides={"a": "b"}, + global_parameter_overrides={"AWS::Region": "any-aws-region"}, + language_extensions_enabled=False, ) @patch("boto3.Session") @@ -439,3 +442,46 @@ def test_sync_preserves_foreach_structure( # The template should NOT contain expanded function names self.assertNotIn("AlphaFunction:", cfn_template) self.assertNotIn("BetaFunction:", cfn_template) + + +class TestDeployContextLanguageExtensionsFlag(TestCase): + """Test cases for language_extensions kwarg support in DeployContext""" + + def _ctx(self, **kwargs): + from samcli.commands.deploy.deploy_context import DeployContext + + defaults = dict( + template_file="template.yaml", + stack_name="s", + s3_bucket=None, + image_repository=None, + image_repositories=None, + force_upload=False, + no_progressbar=False, + s3_prefix="", + kms_key_id=None, + parameter_overrides={}, + capabilities=(), + no_execute_changeset=False, + role_arn=None, + notification_arns=(), + fail_on_empty_changeset=False, + tags={}, + region=None, + profile=None, + confirm_changeset=False, + signing_profiles={}, + use_changeset=True, + disable_rollback=False, + poll_delay=0.5, + on_failure=None, + max_wait_duration=60, + ) + defaults.update(kwargs) + return DeployContext(**defaults) + + def test_default_is_false(self): + assert self._ctx().language_extensions_enabled is False + + def test_explicit_true(self): + assert self._ctx(language_extensions=True).language_extensions_enabled is True diff --git a/tests/unit/commands/deploy/test_guided_context.py b/tests/unit/commands/deploy/test_guided_context.py index b52c8fbac51..052d8a84a76 100644 --- a/tests/unit/commands/deploy/test_guided_context.py +++ b/tests/unit/commands/deploy/test_guided_context.py @@ -98,7 +98,10 @@ def test_guided_prompts_check_defaults_non_public_resources_zips( ] self.assertEqual(expected_prompt_calls, patched_prompt.call_args_list) patched_get_buildable_stacks.assert_called_once_with( - "template", parameter_overrides={}, global_parameter_overrides={"AWS::Region": ANY} + "template", + parameter_overrides={}, + global_parameter_overrides={"AWS::Region": ANY}, + language_extensions_enabled=False, ) @patch("samcli.commands.deploy.guided_context.get_resource_full_path_by_id") diff --git a/tests/unit/commands/local/cli_common/test_invoke_context.py b/tests/unit/commands/local/cli_common/test_invoke_context.py index fd2ea8846d6..e2720cc0e0b 100644 --- a/tests/unit/commands/local/cli_common/test_invoke_context.py +++ b/tests/unit/commands/local/cli_common/test_invoke_context.py @@ -224,7 +224,7 @@ def test_must_initialize_all_containers_if_warm_containers_is_enabled( invoke_context._get_stacks.assert_called_once() RefreshableSamFunctionProviderMock.assert_called_with( - stacks, parameter_overrides, global_parameter_overrides, True + stacks, parameter_overrides, global_parameter_overrides, True, language_extensions_enabled=False ) self.assertEqual(invoke_context._global_parameter_overrides, global_parameter_overrides) self.assertEqual(invoke_context._get_env_vars_value.call_count, 2) @@ -318,7 +318,7 @@ def test_must_set_debug_function_if_warm_containers_enabled_no_debug_function_pr invoke_context._get_stacks.assert_called_once() RefreshableSamFunctionProviderMock.assert_called_with( - stacks, parameter_overrides, global_parameter_overrides, True + stacks, parameter_overrides, global_parameter_overrides, True, language_extensions_enabled=False ) self.assertEqual(invoke_context._global_parameter_overrides, global_parameter_overrides) self.assertEqual(invoke_context._get_env_vars_value.call_count, 2) @@ -409,7 +409,7 @@ def test_no_container_will_be_initialized_if_lazy_containers_is_enabled( invoke_context._get_stacks.assert_called_once() RefreshableSamFunctionProviderMock.assert_called_with( - stacks, parameter_overrides, global_parameter_overrides, True + stacks, parameter_overrides, global_parameter_overrides, True, language_extensions_enabled=False ) self.assertEqual(invoke_context._global_parameter_overrides, global_parameter_overrides) self.assertEqual(invoke_context._get_env_vars_value.call_count, 2) @@ -1494,7 +1494,10 @@ def test_must_pass_custom_region(self, add_account_id_to_global_mock, get_stacks invoke_context = InvokeContext("template_file", aws_region="my-custom-region") invoke_context._get_stacks() get_stacks_mock.assert_called_with( - "template_file", parameter_overrides=None, global_parameter_overrides={"AWS::Region": "my-custom-region"} + "template_file", + parameter_overrides=None, + global_parameter_overrides={"AWS::Region": "my-custom-region"}, + language_extensions_enabled=False, ) @@ -1641,3 +1644,18 @@ def test_validation_matches_function_attributes(self, search_name, func_name, fu # Should not raise any exception invoke_context._validate_function_logical_ids() + + +class TestInvokeContextLanguageExtensions: + def _ctx(self, **kwargs): + from samcli.commands.local.cli_common.invoke_context import InvokeContext + + defaults = dict(template_file="template.yaml") + defaults.update(kwargs) + return InvokeContext(**defaults) + + def test_default_is_false(self): + assert self._ctx().language_extensions_enabled is False + + def test_explicit_true(self): + assert self._ctx(language_extensions=True).language_extensions_enabled is True diff --git a/tests/unit/commands/local/invoke/test_cli.py b/tests/unit/commands/local/invoke/test_cli.py index bae1f83d8f0..a502221e2a3 100644 --- a/tests/unit/commands/local/invoke/test_cli.py +++ b/tests/unit/commands/local/invoke/test_cli.py @@ -88,6 +88,7 @@ def call_cli(self): hook_name=self.hook_name, runtime=self.overide_runtime, mount_symlinks=self.mount_symlinks, + language_extensions=None, no_mem_limit=self.no_mem_limit, container_dns=self.container_dns, tenant_id=self.tenant_id, @@ -128,6 +129,7 @@ def test_cli_must_setup_context_and_invoke(self, get_event_mock, InvokeContextMo add_host=self.add_host, invoke_images={None: "amazon/aws-sam-cli-emulation-image-python3.9"}, mount_symlinks=self.mount_symlinks, + language_extensions=None, no_mem_limit=self.no_mem_limit, container_dns=self.container_dns, ) @@ -177,6 +179,7 @@ def test_cli_must_invoke_with_no_event(self, get_event_mock, InvokeContextMock): add_host=self.add_host, invoke_images={None: "amazon/aws-sam-cli-emulation-image-python3.9"}, mount_symlinks=self.mount_symlinks, + language_extensions=None, no_mem_limit=self.no_mem_limit, container_dns=self.container_dns, ) @@ -361,6 +364,7 @@ def test_cli_must_pass_tenant_id_to_invoke(self, get_event_mock, InvokeContextMo hook_name=self.hook_name, runtime=self.overide_runtime, mount_symlinks=self.mount_symlinks, + language_extensions=None, no_mem_limit=self.no_mem_limit, container_dns=self.container_dns, tenant_id=tenant_id, diff --git a/tests/unit/commands/local/start_api/test_cli.py b/tests/unit/commands/local/start_api/test_cli.py index 3eec1eb12a7..192e4213b42 100644 --- a/tests/unit/commands/local/start_api/test_cli.py +++ b/tests/unit/commands/local/start_api/test_cli.py @@ -99,6 +99,7 @@ def test_cli_must_setup_context_and_start_service(self, local_api_service_mock, container_host_interface=self.container_host_interface, add_host=self.add_host, invoke_images={}, + language_extensions=None, no_mem_limit=self.no_mem_limit, container_dns=self.container_dns, ) @@ -228,6 +229,7 @@ def call_cli(self): container_host_interface=self.container_host_interface, invoke_image=self.invoke_image, hook_name=self.hook_name, + language_extensions=None, ssl_cert_file=self.ssl_cert_file, ssl_key_file=self.ssl_key_file, disable_authorizer=self.disable_authorizer, diff --git a/tests/unit/commands/local/start_lambda/test_cli.py b/tests/unit/commands/local/start_lambda/test_cli.py index f4662622966..446ebe91062 100644 --- a/tests/unit/commands/local/start_lambda/test_cli.py +++ b/tests/unit/commands/local/start_lambda/test_cli.py @@ -87,6 +87,7 @@ def test_cli_must_setup_context_and_start_service(self, local_lambda_service_moc add_host=self.add_host, invoke_images={}, function_logical_ids=(), + language_extensions=None, no_mem_limit=self.no_mem_limit, container_dns=self.container_dns, ) @@ -191,6 +192,7 @@ def call_cli(self): add_host=self.add_host, invoke_image=self.invoke_image, hook_name=self.hook_name, + language_extensions=None, no_mem_limit=self.no_mem_limit, container_dns=self.container_dns, ) diff --git a/tests/unit/commands/package/test_command.py b/tests/unit/commands/package/test_command.py index 047320c5cc9..1443a516de3 100644 --- a/tests/unit/commands/package/test_command.py +++ b/tests/unit/commands/package/test_command.py @@ -46,6 +46,7 @@ def test_all_args(self, package_command_context, click_mock): profile=self.profile, resolve_s3=self.resolve_s3, resolve_image_repos=self.resolve_image_repos, + language_extensions=None, signing_profiles=self.signing_profiles, ) @@ -65,6 +66,7 @@ def test_all_args(self, package_command_context, click_mock): profile=self.profile, signing_profiles=self.signing_profiles, resolve_image_repos=self.resolve_image_repos, + language_extensions=None, ) context_mock.run.assert_called_with() @@ -94,6 +96,7 @@ def test_all_args_resolve_s3(self, mock_managed_stack, package_command_context, profile=self.profile, resolve_s3=True, resolve_image_repos=False, + language_extensions=None, signing_profiles=self.signing_profiles, ) @@ -113,6 +116,7 @@ def test_all_args_resolve_s3(self, mock_managed_stack, package_command_context, profile=self.profile, signing_profiles=self.signing_profiles, resolve_image_repos=False, + language_extensions=None, ) context_mock.run.assert_called_with() @@ -138,6 +142,7 @@ def test_resolve_image_repos_without_s3_bucket_raises_error(self, package_comman profile=self.profile, resolve_s3=False, resolve_image_repos=True, + language_extensions=None, signing_profiles=self.signing_profiles, ) @@ -163,6 +168,7 @@ def test_all_args_with_resolve_image_repos(self, package_command_context, click_ profile=self.profile, resolve_s3=False, resolve_image_repos=True, + language_extensions=None, signing_profiles=self.signing_profiles, ) @@ -182,6 +188,7 @@ def test_all_args_with_resolve_image_repos(self, package_command_context, click_ profile=self.profile, signing_profiles=self.signing_profiles, resolve_image_repos=True, + language_extensions=None, ) context_mock.run.assert_called_with() @@ -193,3 +200,32 @@ def test_resources_and_properties_help_string(self): self.assertIsInstance(help_string, str) # Should contain resource and location information self.assertTrue(len(help_string) > 0) + + +class TestPackageContextLanguageExtensions: + def _ctx(self, **kwargs): + from samcli.commands.package.package_context import PackageContext + + defaults = dict( + template_file="template.yaml", + s3_bucket="b", + image_repository=None, + image_repositories=None, + s3_prefix="", + kms_key_id=None, + output_template_file=None, + use_json=False, + force_upload=False, + no_progressbar=False, + metadata={}, + region=None, + profile=None, + ) + defaults.update(kwargs) + return PackageContext(**defaults) + + def test_default_is_false(self): + assert self._ctx().language_extensions_enabled is False + + def test_explicit_true(self): + assert self._ctx(language_extensions=True).language_extensions_enabled is True diff --git a/tests/unit/commands/package/test_package_context.py b/tests/unit/commands/package/test_package_context.py index 76ba34b6854..3e641c548b6 100644 --- a/tests/unit/commands/package/test_package_context.py +++ b/tests/unit/commands/package/test_package_context.py @@ -2522,6 +2522,7 @@ def test_export_with_language_extensions_and_dynamic_properties( metadata={}, region=None, profile=None, + language_extensions=True, ) # Mock uploaders and code_signer which are set in run() package_context.uploaders = Mock() @@ -2629,6 +2630,7 @@ def test_export_with_language_extensions_and_static_properties( metadata={}, region=None, profile=None, + language_extensions=True, ) # Mock uploaders and code_signer which are set in run() package_context.uploaders = Mock() @@ -3019,6 +3021,7 @@ def test_export_emits_warning_for_parameter_based_collection( region=None, profile=None, parameter_overrides={"ServiceNames": "Users,Orders"}, + language_extensions=True, ) # Mock uploaders and code_signer which are set in run() package_context.uploaders = Mock() diff --git a/tests/unit/commands/samconfig/test_samconfig.py b/tests/unit/commands/samconfig/test_samconfig.py index 99152dd0928..443aa3b998e 100644 --- a/tests/unit/commands/samconfig/test_samconfig.py +++ b/tests/unit/commands/samconfig/test_samconfig.py @@ -103,7 +103,7 @@ def test_validate(self, do_cli_mock): LOG.exception("Command failed", exc_info=result.exc_info) self.assertIsNone(result.exception) - do_cli_mock.assert_called_with(ANY, str(Path(os.getcwd(), "mytemplate.yaml")), False) + do_cli_mock.assert_called_with(ANY, str(Path(os.getcwd(), "mytemplate.yaml")), False, None) @patch("samcli.commands.build.command.do_cli") def test_build(self, do_cli_mock): @@ -165,6 +165,7 @@ def test_build(self, do_cli_mock): "READ", True, False, + None, ) @patch("samcli.commands.build.command.do_cli") @@ -226,6 +227,7 @@ def test_build_with_no_use_container(self, do_cli_mock): "READ", False, False, + None, ) @patch("samcli.commands.build.command.do_cli") @@ -286,6 +288,7 @@ def test_build_with_no_use_container_option(self, do_cli_mock): "READ", False, False, + None, ) @patch("samcli.commands.build.command.do_cli") @@ -347,6 +350,7 @@ def test_build_with_no_use_container_override(self, do_cli_mock): "READ", False, False, + None, ) @patch("samcli.commands.build.command.do_cli") @@ -409,6 +413,7 @@ def test_build_with_no_cached_override(self, do_cli_mock): "READ", False, False, + None, ) @patch("samcli.commands.build.command.do_cli") @@ -468,6 +473,7 @@ def test_build_with_container_env_vars(self, do_cli_mock): "READ", False, False, + None, ) @patch("samcli.commands.build.command.do_cli") @@ -526,6 +532,7 @@ def test_build_with_build_images(self, do_cli_mock): "READ", False, False, + None, ) @patch("samcli.commands.local.invoke.cli.do_cli") @@ -593,6 +600,7 @@ def test_local_invoke(self, do_cli_mock): None, None, True, + None, True, (), None, @@ -664,6 +672,7 @@ def test_local_invoke_with_runtime_params(self, do_cli_mock): None, "python3.11", True, + None, True, (), None, @@ -738,6 +747,7 @@ def test_local_start_api(self, do_cli_mock): None, None, None, + None, False, (), ) @@ -805,6 +815,7 @@ def test_local_start_lambda(self, do_cli_mock): {}, ("image",), None, + None, False, (), ) @@ -867,6 +878,7 @@ def test_package( None, False, False, + None, ) @patch("samcli.commands._utils.options.get_template_artifacts_format") @@ -972,6 +984,7 @@ def test_deploy(self, do_cli_mock, template_artifacts_mock1, template_artifacts_ "samconfig.toml", "default", False, + None, True, "ROLLBACK", 60, @@ -1087,6 +1100,7 @@ def test_deploy_different_parameter_override_format( "samconfig.toml", "default", False, + None, True, "ROLLBACK", 60, @@ -1327,6 +1341,7 @@ def test_sync( "default", False, {"HelloWorld": ["file.txt", "other.txt"], "HelloMars": ["single.file"]}, + None, ) @@ -1452,6 +1467,7 @@ def test_override_with_cli_params(self, do_cli_mock): {}, ("image",), None, + None, False, (), ) @@ -1555,6 +1571,7 @@ def test_override_with_cli_params_and_envvars(self, do_cli_mock): {}, ("image",), None, + None, True, (), ) @@ -1577,7 +1594,7 @@ def test_secondary_option_name_template_validate(self, do_cli_mock): LOG.exception("Command failed", exc_info=result.exc_info) self.assertIsNone(result.exception) - do_cli_mock.assert_called_with(ANY, str(Path(os.getcwd(), "mytemplate.yaml")), False) + do_cli_mock.assert_called_with(ANY, str(Path(os.getcwd(), "mytemplate.yaml")), False, None) @contextmanager diff --git a/tests/unit/commands/sync/test_command.py b/tests/unit/commands/sync/test_command.py index ba6147149dc..dff8d540b8f 100644 --- a/tests/unit/commands/sync/test_command.py +++ b/tests/unit/commands/sync/test_command.py @@ -150,6 +150,7 @@ def test_infra_must_succeed_sync( self.config_env, build_in_source=False, watch_exclude={}, + language_extensions=None, ) if use_container and auto_dependency_layer: @@ -175,6 +176,7 @@ def test_infra_must_succeed_sync( locate_layer_nested=True, build_in_source=False, build_images={}, + language_extensions=None, ) PackageContextMock.assert_called_with( @@ -191,6 +193,7 @@ def test_infra_must_succeed_sync( profile=self.profile, use_json=False, force_upload=True, + language_extensions=None, ) DeployContextMock.assert_called_with( @@ -219,6 +222,7 @@ def test_infra_must_succeed_sync( poll_delay=10, on_failure=None, max_wait_duration=60, + language_extensions=None, ) execute_infra_mock.assert_called_with( @@ -318,6 +322,7 @@ def test_watch_must_succeed_sync( self.config_env, build_in_source=False, watch_exclude={}, + language_extensions=None, ) BuildContextMock.assert_called_with( @@ -339,6 +344,7 @@ def test_watch_must_succeed_sync( locate_layer_nested=True, build_in_source=False, build_images={}, + language_extensions=None, ) PackageContextMock.assert_called_with( @@ -355,6 +361,7 @@ def test_watch_must_succeed_sync( profile=self.profile, use_json=False, force_upload=True, + language_extensions=None, ) DeployContextMock.assert_called_with( @@ -383,6 +390,7 @@ def test_watch_must_succeed_sync( poll_delay=0.5, on_failure=None, max_wait_duration=60, + language_extensions=None, ) execute_watch_mock.assert_called_once_with( template=self.template_file, @@ -470,6 +478,7 @@ def test_code_must_succeed_sync( self.config_env, build_in_source=None, watch_exclude={}, + language_extensions=None, ) execute_code_sync_mock.assert_called_once_with( template=self.template_file, @@ -860,7 +869,7 @@ def test_disables_adl_for_esbuild(self, stack_resources, expected, provider_mock [stack], "", ) - self.assertEqual(check_enable_dependency_layer("/template/file"), expected) + self.assertEqual(check_enable_dependency_layer("/template/file", None), expected) @patch("samcli.commands.sync.command.InfraSyncExecutor") def test_execute_infra_contexts(self, patch_infra_sync_executor): diff --git a/tests/unit/commands/sync/test_sync_context.py b/tests/unit/commands/sync/test_sync_context.py index ea2b03845ba..beb34c9df5c 100644 --- a/tests/unit/commands/sync/test_sync_context.py +++ b/tests/unit/commands/sync/test_sync_context.py @@ -1,3 +1,4 @@ +import os from pathlib import Path from unittest import TestCase, mock from unittest.mock import mock_open, call, patch, Mock, MagicMock @@ -488,3 +489,27 @@ def test_backward_compatibility_mixed_formats(self): current_time = datetime.now(timezone.utc) time_diff = current_time - sync_state.latest_infra_sync_time self.assertIsNotNone(time_diff) + + +class TestSyncContextLanguageExtensions(TestCase): + """Tests for language_extensions_enabled property""" + + def _ctx(self, **kwargs): + defaults = dict( + dependency_layer=False, + build_dir="build", + cache_dir=".cache", + skip_deploy_sync=False, + ) + defaults.update(kwargs) + return SyncContext(**defaults) + + def test_default_is_false(self): + assert self._ctx().language_extensions_enabled is False + + def test_explicit_true(self): + assert self._ctx(language_extensions=True).language_extensions_enabled is True + + def test_explicit_false_overrides_env(self): + with mock.patch.dict(os.environ, {"SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS": "1"}, clear=False): + assert self._ctx(language_extensions=False).language_extensions_enabled is False diff --git a/tests/unit/commands/validate/lib/test_sam_template_validator.py b/tests/unit/commands/validate/lib/test_sam_template_validator.py index 59b832d17a5..46a7c111afb 100644 --- a/tests/unit/commands/validate/lib/test_sam_template_validator.py +++ b/tests/unit/commands/validate/lib/test_sam_template_validator.py @@ -313,3 +313,43 @@ def test_replace_local_codeuri_with_no_resources(self): # check template self.assertEqual(validator.sam_template.get("Resources"), {}) + + +class TestSamTemplateValidatorLanguageExtensions(TestCase): + """SamTemplateValidator forwards language_extensions_enabled to expand_language_extensions.""" + + @patch("samcli.lib.translate.sam_template_validator.expand_language_extensions") + def test_disabled_passes_enabled_false(self, mock_expand): + mock_expand.return_value = Mock( + had_language_extensions=False, + expanded_template={"Resources": {}}, + ) + validator = SamTemplateValidator( + sam_template={"Resources": {}}, + managed_policy_loader=Mock(), + language_extensions_enabled=False, + ) + try: + validator.get_translated_template_if_valid() + except Exception: + pass + for call in mock_expand.call_args_list: + assert call.kwargs.get("enabled") is False + + @patch("samcli.lib.translate.sam_template_validator.expand_language_extensions") + def test_enabled_passes_enabled_true(self, mock_expand): + mock_expand.return_value = Mock( + had_language_extensions=False, + expanded_template={"Resources": {}}, + ) + validator = SamTemplateValidator( + sam_template={"Resources": {}}, + managed_policy_loader=Mock(), + language_extensions_enabled=True, + ) + try: + validator.get_translated_template_if_valid() + except Exception: + pass + for call in mock_expand.call_args_list: + assert call.kwargs.get("enabled") is True diff --git a/tests/unit/commands/validate/test_cli.py b/tests/unit/commands/validate/test_cli.py index f77a4711e91..38f33ddab0e 100644 --- a/tests/unit/commands/validate/test_cli.py +++ b/tests/unit/commands/validate/test_cli.py @@ -51,7 +51,12 @@ def test_template_fails_validation(self, patched_boto, read_sam_file_patch, clic template_validator.return_value = get_translated_template_if_valid_mock with self.assertRaises(InvalidSamTemplateException): - do_cli(ctx=ctx_mock(profile="profile", region="region"), template=template_path, lint=False) + do_cli( + ctx=ctx_mock(profile="profile", region="region"), + template=template_path, + lint=False, + language_extensions=None, + ) @patch("samcli.lib.translate.sam_template_validator.SamTemplateValidator") @patch("samcli.commands.validate.validate.click") @@ -66,7 +71,12 @@ def test_no_credentials_provided(self, patched_boto, read_sam_file_patch, click_ template_validator.return_value = get_translated_template_if_valid_mock with self.assertRaises(UserException): - do_cli(ctx=ctx_mock(profile="profile", region="region"), template=template_path, lint=False) + do_cli( + ctx=ctx_mock(profile="profile", region="region"), + template=template_path, + lint=False, + language_extensions=None, + ) @patch("samcli.lib.translate.sam_template_validator.SamTemplateValidator") @patch("samcli.commands.validate.validate.click") @@ -80,7 +90,12 @@ def test_template_passes_validation(self, patched_boto, read_sam_file_patch, cli get_translated_template_if_valid_mock.get_translated_template_if_valid.return_value = True template_validator.return_value = get_translated_template_if_valid_mock - do_cli(ctx=ctx_mock(profile="profile", region="region"), template=template_path, lint=False) + do_cli( + ctx=ctx_mock(profile="profile", region="region"), + template=template_path, + lint=False, + language_extensions=None, + ) @patch("samcli.commands.validate.validate._read_sam_file") @patch("samcli.commands.validate.validate.click") @@ -91,7 +106,9 @@ def test_lint_template_passes(self, click_patch, lint_patch, read_sam_file_patch read_sam_file_patch.return_value = SamTemplate(serialized="{}", deserialized={}) lint_patch.return_value = True - do_cli(ctx=ctx_lint_mock(debug=False, region="region"), template=template_path, lint=True) + do_cli( + ctx=ctx_lint_mock(debug=False, region="region"), template=template_path, lint=True, language_extensions=None + ) @patch("cfnlint.api.lint") @patch("samcli.commands.validate.validate.click") diff --git a/tests/unit/lib/cfn_language_extensions/test_phase_separation.py b/tests/unit/lib/cfn_language_extensions/test_phase_separation.py index 1808e032ec0..7eccaba88c1 100644 --- a/tests/unit/lib/cfn_language_extensions/test_phase_separation.py +++ b/tests/unit/lib/cfn_language_extensions/test_phase_separation.py @@ -169,7 +169,7 @@ def test_returns_result_with_expanded_template_for_language_extensions(self): }, } - result = expand_language_extensions(template) + result = expand_language_extensions(template, enabled=True) self.assertIsInstance(result, LanguageExtensionResult) self.assertTrue(result.had_language_extensions) @@ -185,7 +185,7 @@ def test_returns_had_language_extensions_false_for_non_langext_template(self): "Resources": {"MyFunc": {"Type": "AWS::Serverless::Function"}}, } - result = expand_language_extensions(template) + result = expand_language_extensions(template, enabled=True) self.assertIsInstance(result, LanguageExtensionResult) self.assertFalse(result.had_language_extensions) @@ -196,7 +196,7 @@ def test_returns_had_language_extensions_false_for_no_transform(self): """expand_language_extensions() returns had_language_extensions=False when no Transform.""" template = {"Resources": {"MyTopic": {"Type": "AWS::SNS::Topic"}}} - result = expand_language_extensions(template) + result = expand_language_extensions(template, enabled=True) self.assertFalse(result.had_language_extensions) @@ -218,7 +218,7 @@ def test_original_template_preserves_foreach_structure(self): }, } - result = expand_language_extensions(template) + result = expand_language_extensions(template, enabled=True) # Original template should preserve Fn::ForEach self.assertIn("Fn::ForEach::Services", result.original_template["Resources"]) @@ -241,7 +241,7 @@ def test_original_template_not_mutated(self): template_before = copy.deepcopy(template) - result = expand_language_extensions(template) + result = expand_language_extensions(template, enabled=True) # Input template must not be mutated by expansion self.assertEqual(template, template_before) @@ -269,7 +269,7 @@ def test_dynamic_artifact_properties_detected(self): }, } - result = expand_language_extensions(template) + result = expand_language_extensions(template, enabled=True) self.assertTrue(len(result.dynamic_artifact_properties) > 0) prop = result.dynamic_artifact_properties[0] @@ -291,7 +291,7 @@ def test_pseudo_parameter_extraction(self): } parameter_values = {"AWS::Region": "us-west-2", "AWS::AccountId": "123456789012"} - result = expand_language_extensions(template, parameter_values=parameter_values) + result = expand_language_extensions(template, parameter_values=parameter_values, enabled=True) self.assertTrue(result.had_language_extensions) # Pseudo-parameter should be resolved @@ -312,7 +312,7 @@ def test_invalid_template_raises_invalid_sam_document(self): } with self.assertRaises(InvalidSamDocumentException): - expand_language_extensions(template) + expand_language_extensions(template, enabled=True) def test_list_transform_with_language_extensions(self): """expand_language_extensions() works when Transform is a list containing AWS::LanguageExtensions.""" @@ -327,7 +327,7 @@ def test_list_transform_with_language_extensions(self): }, } - result = expand_language_extensions(template) + result = expand_language_extensions(template, enabled=True) self.assertTrue(result.had_language_extensions) self.assertIn("ATopic", result.expanded_template["Resources"]) @@ -616,6 +616,7 @@ def test_package_context_export_calls_expand_language_extensions(self, mock_expa ctx.template_file = "template.yaml" ctx.parameter_overrides = {} ctx._global_parameter_overrides = {} + ctx._language_extensions_enabled = True ctx.uploaders = MagicMock() ctx.code_signer = MagicMock() @@ -695,8 +696,8 @@ def test_single_expansion_per_unique_inputs( }, } - result1 = expand_language_extensions(copy.deepcopy(template)) - result2 = expand_language_extensions(copy.deepcopy(template)) + result1 = expand_language_extensions(copy.deepcopy(template), enabled=True) + result2 = expand_language_extensions(copy.deepcopy(template), enabled=True) assert isinstance(result1, LanguageExtensionResult) assert isinstance(result2, LanguageExtensionResult) @@ -737,7 +738,7 @@ def test_no_expansion_without_language_extensions_transform( }, } - result = expand_language_extensions(copy.deepcopy(template)) + result = expand_language_extensions(copy.deepcopy(template), enabled=True) assert result.had_language_extensions is False assert "MyResource" in result.expanded_template["Resources"] @@ -794,7 +795,7 @@ def test_result_equivalence_expanded_template_matches_direct_processing( }, } - result = expand_language_extensions(copy.deepcopy(template)) + result = expand_language_extensions(copy.deepcopy(template), enabled=True) direct_result = process_template_for_sam_cli(copy.deepcopy(template)) assert set(result.expanded_template["Resources"].keys()) == set(direct_result["Resources"].keys()) @@ -839,7 +840,7 @@ def test_result_preserves_original_template_structure( }, } - result = expand_language_extensions(copy.deepcopy(template)) + result = expand_language_extensions(copy.deepcopy(template), enabled=True) assert foreach_key in result.original_template["Resources"] @@ -889,7 +890,7 @@ def test_result_dynamic_properties_consistent( }, } - result = expand_language_extensions(copy.deepcopy(template)) + result = expand_language_extensions(copy.deepcopy(template), enabled=True) assert len(result.dynamic_artifact_properties) > 0 diff --git a/tests/unit/lib/cfn_language_extensions/test_resolve_language_extensions.py b/tests/unit/lib/cfn_language_extensions/test_resolve_language_extensions.py new file mode 100644 index 00000000000..cd01e4798ae --- /dev/null +++ b/tests/unit/lib/cfn_language_extensions/test_resolve_language_extensions.py @@ -0,0 +1,43 @@ +"""Unit tests for resolve_language_extensions_enabled().""" + +import os +from unittest import mock + +import pytest + +from samcli.lib.cfn_language_extensions.sam_integration import ( + ENABLE_ENV_VAR, + resolve_language_extensions_enabled, +) + + +class TestResolveLanguageExtensionsEnabled: + """Flag-vs-env precedence and truthy parsing.""" + + def test_flag_true_returns_true(self): + with mock.patch.dict(os.environ, {ENABLE_ENV_VAR: ""}, clear=False): + assert resolve_language_extensions_enabled(True) is True + + def test_flag_false_returns_false(self): + with mock.patch.dict(os.environ, {ENABLE_ENV_VAR: "1"}, clear=False): + # Flag wins over env var. + assert resolve_language_extensions_enabled(False) is False + + def test_flag_none_with_env_unset_returns_false(self): + env = {k: v for k, v in os.environ.items() if k != ENABLE_ENV_VAR} + with mock.patch.dict(os.environ, env, clear=True): + assert resolve_language_extensions_enabled(None) is False + + @pytest.mark.parametrize("value", ["1", "true", "TRUE", "True", "yes", "YES", "Yes"]) + def test_flag_none_with_truthy_env_returns_true(self, value): + with mock.patch.dict(os.environ, {ENABLE_ENV_VAR: value}, clear=False): + assert resolve_language_extensions_enabled(None) is True + + @pytest.mark.parametrize("value", ["0", "false", "no", "maybe", "", " ", "2"]) + def test_flag_none_with_untruthy_env_returns_false(self, value): + with mock.patch.dict(os.environ, {ENABLE_ENV_VAR: value}, clear=False): + assert resolve_language_extensions_enabled(None) is False + + def test_flag_none_with_whitespace_env_is_stripped(self): + with mock.patch.dict(os.environ, {ENABLE_ENV_VAR: " true "}, clear=False): + assert resolve_language_extensions_enabled(None) is True diff --git a/tests/unit/lib/cfn_language_extensions/test_sam_integration.py b/tests/unit/lib/cfn_language_extensions/test_sam_integration.py index 50daa2a40f5..da420e9c31b 100644 --- a/tests/unit/lib/cfn_language_extensions/test_sam_integration.py +++ b/tests/unit/lib/cfn_language_extensions/test_sam_integration.py @@ -621,12 +621,12 @@ class TestExpandLanguageExtensionsEdgeCases: def test_non_le_template_returns_result(self): template = {"Transform": "AWS::Serverless-2016-10-31", "Resources": {}} - result = expand_language_extensions(template) + result = expand_language_extensions(template, enabled=True) assert result.had_language_extensions is False def test_non_language_extension_template_returns_original_dict(self): template = {"Resources": {}} - result = expand_language_extensions(template) + result = expand_language_extensions(template, enabled=True) assert result.had_language_extensions is False # No-LE path returns the caller's dict directly (no freeze, no cache) assert result.expanded_template is template @@ -642,7 +642,7 @@ def test_mutation_does_not_affect_subsequent_calls(self): } } } - result1 = expand_language_extensions(template) + result1 = expand_language_extensions(template, enabled=True) result1.expanded_template["Resources"]["MyStack"]["Properties"]["Location"] = "SomeOther/template.yaml" # A fresh call should not be affected @@ -654,7 +654,7 @@ def test_mutation_does_not_affect_subsequent_calls(self): } } } - result2 = expand_language_extensions(template2) + result2 = expand_language_extensions(template2, enabled=True) assert result2.expanded_template["Resources"]["MyStack"]["Properties"]["Location"] == "./child.yaml" def test_non_invalid_template_exception_reraised(self): @@ -677,7 +677,7 @@ def test_non_invalid_template_exception_reraised(self): side_effect=RuntimeError("unexpected error"), ): with pytest.raises(RuntimeError, match="unexpected error"): - expand_language_extensions(template) + expand_language_extensions(template, enabled=True) def test_invalid_template_exception_converted(self): from samcli.lib.cfn_language_extensions.exceptions import ( @@ -695,7 +695,7 @@ def test_invalid_template_exception_converted(self): from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException with pytest.raises(InvalidSamDocumentException): - expand_language_extensions(template) + expand_language_extensions(template, enabled=True) def test_telemetry_tracked_when_language_extensions_used(self): """Verify UsedFeature telemetry event is emitted when language extensions are expanded.""" @@ -714,7 +714,7 @@ def test_telemetry_tracked_when_language_extensions_used(self): }, } with patch("samcli.lib.telemetry.event.EventTracker.track_event") as mock_track: - result = expand_language_extensions(template) + result = expand_language_extensions(template, enabled=True) assert result.had_language_extensions is True mock_track.assert_called_with("UsedFeature", "CFNLanguageExtensions") @@ -725,7 +725,7 @@ def test_telemetry_not_tracked_when_no_language_extensions(self): "Resources": {"Fn": {"Type": "AWS::Lambda::Function", "Properties": {}}}, } with patch("samcli.lib.telemetry.event.EventTracker.track_event") as mock_track: - result = expand_language_extensions(template) + result = expand_language_extensions(template, enabled=True) assert result.had_language_extensions is False mock_track.assert_not_called() @@ -798,7 +798,7 @@ def test_original_template_is_independent_copy(self): }, } - result = expand_language_extensions(template) + result = expand_language_extensions(template, enabled=True) # Mutate the caller's template template["Resources"]["NewResource"] = {"Type": "AWS::SQS::Queue"} @@ -810,7 +810,90 @@ def test_no_extensions_result_aliases_input(self): """No-LE path returns the caller's dict directly — no copy overhead.""" template = {"Resources": {"MyTopic": {"Type": "AWS::SNS::Topic"}}} - result = expand_language_extensions(template) + result = expand_language_extensions(template, enabled=True) assert result.original_template is template assert result.expanded_template is template + + +class TestExpandLanguageExtensionsEnabledKwarg: + """The `enabled` kwarg gates Phase 1 expansion.""" + + def test_disabled_returns_passthrough_for_le_template(self): + template = { + "Transform": "AWS::LanguageExtensions", + "Resources": { + "Fn::ForEach::Names": [ + "Name", + ["A", "B"], + {"${Name}Func": {"Type": "AWS::Serverless::Function"}}, + ] + }, + } + result = expand_language_extensions(template, enabled=False) + assert isinstance(result, LanguageExtensionResult) + assert result.had_language_extensions is False + assert result.expanded_template is template + assert result.original_template is template + assert result.dynamic_artifact_properties == [] + + def test_disabled_returns_passthrough_for_non_le_template(self): + template = {"Resources": {"Foo": {"Type": "AWS::S3::Bucket"}}} + result = expand_language_extensions(template, enabled=False) + assert result.had_language_extensions is False + assert result.expanded_template is template + + def test_enabled_with_le_template_expands(self): + template = { + "Transform": "AWS::LanguageExtensions", + "Resources": { + "Fn::ForEach::Names": [ + "Name", + ["A", "B"], + {"${Name}Func": {"Type": "AWS::SNS::Topic"}}, + ] + }, + } + result = expand_language_extensions(template, enabled=True) + assert result.had_language_extensions is True + assert "AFunc" in result.expanded_template["Resources"] + assert "BFunc" in result.expanded_template["Resources"] + + def test_enabled_kwarg_is_required(self): + template = {"Resources": {}} + with pytest.raises(TypeError): + expand_language_extensions(template) # missing enabled= + + +class TestExpandLanguageExtensionsTelemetry: + """Telemetry fires only when Phase 1 actually expanded a template.""" + + def test_telemetry_not_fired_when_disabled(self): + template = {"Transform": "AWS::LanguageExtensions", "Resources": {}} + with patch("samcli.lib.telemetry.event.EventTracker.track_event") as mock_track: + expand_language_extensions(template, enabled=False) + for call in mock_track.call_args_list: + assert "CFNLanguageExtensions" not in call.args + + def test_telemetry_not_fired_when_enabled_but_no_le_template(self): + template = {"Resources": {}} + with patch("samcli.lib.telemetry.event.EventTracker.track_event") as mock_track: + expand_language_extensions(template, enabled=True) + for call in mock_track.call_args_list: + assert "CFNLanguageExtensions" not in call.args + + def test_telemetry_fired_when_enabled_and_le_template(self): + template = { + "Transform": "AWS::LanguageExtensions", + "Resources": { + "Fn::ForEach::Names": [ + "Name", + ["A"], + {"${Name}T": {"Type": "AWS::SNS::Topic"}}, + ] + }, + } + with patch("samcli.lib.telemetry.event.EventTracker.track_event") as mock_track: + expand_language_extensions(template, enabled=True) + feature_calls = [c for c in mock_track.call_args_list if "CFNLanguageExtensions" in c.args] + assert len(feature_calls) == 1 diff --git a/tests/unit/lib/package/test_artifact_exporter.py b/tests/unit/lib/package/test_artifact_exporter.py index d9da6f2406b..59f7be0b1d3 100644 --- a/tests/unit/lib/package/test_artifact_exporter.py +++ b/tests/unit/lib/package/test_artifact_exporter.py @@ -1205,6 +1205,7 @@ def test_export_cloudformation_stack(self, TemplateMock): normalize_template=True, parent_stack_id="id", parameter_values=mock.ANY, + language_extensions_enabled=False, ) template_instance_mock.export.assert_called_once_with() self.s3_uploader_mock.upload.assert_called_once_with(mock.ANY, mock.ANY) @@ -1386,6 +1387,7 @@ def test_export_serverless_application(self, TemplateMock): normalize_template=True, parent_stack_id="id", parameter_values=mock.ANY, + language_extensions_enabled=False, ) template_instance_mock.export.assert_called_once_with() self.s3_uploader_mock.upload.assert_called_once_with(mock.ANY, mock.ANY) @@ -2827,3 +2829,39 @@ def test_unexpected_exception_logs_error_and_falls_back(self): import shutil shutil.rmtree(tmpdir, ignore_errors=True) + + +class TestTemplateLanguageExtensionsKwarg(unittest.TestCase): + """Tests for Template.__init__ language_extensions_enabled kwarg.""" + + def test_disabled_passes_enabled_false(self): + template = Template( + template_path="template.yaml", + parent_dir=os.path.abspath(os.getcwd()), + uploaders=mock.MagicMock(), + code_signer=mock.MagicMock(), + template_dict={"Resources": {}}, + language_extensions_enabled=False, + ) + self.assertFalse(template.language_extensions_enabled) + + def test_enabled_passes_enabled_true(self): + template = Template( + template_path="template.yaml", + parent_dir=os.path.abspath(os.getcwd()), + uploaders=mock.MagicMock(), + code_signer=mock.MagicMock(), + template_dict={"Resources": {}}, + language_extensions_enabled=True, + ) + self.assertTrue(template.language_extensions_enabled) + + def test_default_is_false(self): + template = Template( + template_path="template.yaml", + parent_dir=os.path.abspath(os.getcwd()), + uploaders=mock.MagicMock(), + code_signer=mock.MagicMock(), + template_dict={"Resources": {}}, + ) + self.assertFalse(template.language_extensions_enabled) diff --git a/tests/unit/lib/providers/test_sam_stack_provider_language_extensions.py b/tests/unit/lib/providers/test_sam_stack_provider_language_extensions.py index cca76238d66..88b95b2e8e0 100644 --- a/tests/unit/lib/providers/test_sam_stack_provider_language_extensions.py +++ b/tests/unit/lib/providers/test_sam_stack_provider_language_extensions.py @@ -153,3 +153,67 @@ def test_get_stacks_passes_merged_params_with_pseudo_params(self, mock_expand, m self.assertEqual(param_values["Names"], "A,B") # Should include pseudo-parameters self.assertIn("AWS::Region", param_values) + + +class TestGetStacksLanguageExtensionsEnabledKwarg(TestCase): + """get_stacks() forwards language_extensions_enabled to expand_language_extensions.""" + + @patch("samcli.lib.providers.sam_stack_provider.expand_language_extensions") + @patch("samcli.lib.providers.sam_stack_provider.get_template_data") + def test_disabled_passes_enabled_false(self, mock_get_template, mock_expand): + """When language_extensions_enabled=False, enabled=False is passed to expand_language_extensions.""" + from samcli.lib.providers.sam_stack_provider import SamLocalStackProvider + + template = {"Resources": {"MyFunc": {"Type": "AWS::Serverless::Function", "Properties": {"CodeUri": "."}}}} + mock_get_template.return_value = template + mock_expand.return_value = MagicMock( + expanded_template=template, + original_template=template, + dynamic_artifact_properties=[], + had_language_extensions=False, + ) + SamLocalStackProvider.get_stacks( + template_file="t.yaml", + language_extensions_enabled=False, + ) + for call in mock_expand.call_args_list: + self.assertIs(call.kwargs.get("enabled"), False) + + @patch("samcli.lib.providers.sam_stack_provider.expand_language_extensions") + @patch("samcli.lib.providers.sam_stack_provider.get_template_data") + def test_enabled_passes_enabled_true(self, mock_get_template, mock_expand): + """When language_extensions_enabled=True, enabled=True is passed to expand_language_extensions.""" + from samcli.lib.providers.sam_stack_provider import SamLocalStackProvider + + template = {"Resources": {"MyFunc": {"Type": "AWS::Serverless::Function", "Properties": {"CodeUri": "."}}}} + mock_get_template.return_value = template + mock_expand.return_value = MagicMock( + expanded_template=template, + original_template=template, + dynamic_artifact_properties=[], + had_language_extensions=False, + ) + SamLocalStackProvider.get_stacks( + template_file="t.yaml", + language_extensions_enabled=True, + ) + for call in mock_expand.call_args_list: + self.assertIs(call.kwargs.get("enabled"), True) + + @patch("samcli.lib.providers.sam_stack_provider.expand_language_extensions") + @patch("samcli.lib.providers.sam_stack_provider.get_template_data") + def test_default_is_disabled(self, mock_get_template, mock_expand): + """When the kwarg is omitted, enabled=False is passed to expand_language_extensions.""" + from samcli.lib.providers.sam_stack_provider import SamLocalStackProvider + + template = {"Resources": {"MyFunc": {"Type": "AWS::Serverless::Function", "Properties": {"CodeUri": "."}}}} + mock_get_template.return_value = template + mock_expand.return_value = MagicMock( + expanded_template=template, + original_template=template, + dynamic_artifact_properties=[], + had_language_extensions=False, + ) + SamLocalStackProvider.get_stacks(template_file="t.yaml") + for call in mock_expand.call_args_list: + self.assertIs(call.kwargs.get("enabled"), False) diff --git a/tests/unit/lib/samlib/test_wrapper.py b/tests/unit/lib/samlib/test_wrapper.py index 3ad95f10b5f..50bf273a537 100644 --- a/tests/unit/lib/samlib/test_wrapper.py +++ b/tests/unit/lib/samlib/test_wrapper.py @@ -140,7 +140,7 @@ def test_expand_language_extensions_success(self, mock_process): } mock_process.return_value = expected_output - result = expand_language_extensions(input_template, parameter_values={"AWS::Region": "us-east-1"}) + result = expand_language_extensions(input_template, parameter_values={"AWS::Region": "us-east-1"}, enabled=True) self.assertTrue(result.had_language_extensions) self.assertEqual(result.expanded_template, expected_output) @@ -159,7 +159,7 @@ def test_expand_language_extensions_with_pseudo_params(self, mock_process): } mock_process.return_value = template - result = expand_language_extensions(template, parameter_values=parameter_values) + result = expand_language_extensions(template, parameter_values=parameter_values, enabled=True) self.assertTrue(result.had_language_extensions) # Verify process_template_for_sam_cli was called with correct arguments @@ -180,7 +180,7 @@ def test_expand_language_extensions_error_handling(self, mock_process): mock_process.side_effect = LangExtInvalidTemplateException("Invalid Fn::ForEach syntax") with self.assertRaises(InvalidSamDocumentException) as context: - expand_language_extensions(template) + expand_language_extensions(template, enabled=True) self.assertIn("Invalid Fn::ForEach syntax", str(context.exception)) diff --git a/tests/unit/lib/sync/test_watch_manager.py b/tests/unit/lib/sync/test_watch_manager.py index bad254c7c13..cb4ba1bfaa9 100644 --- a/tests/unit/lib/sync/test_watch_manager.py +++ b/tests/unit/lib/sync/test_watch_manager.py @@ -63,7 +63,11 @@ def test_update_stacks( stacks, ] self.watch_manager._update_stacks() - get_stacks_mock.assert_called_once_with(self.template, use_sam_transform=False) + get_stacks_mock.assert_called_once_with( + self.template, + use_sam_transform=False, + language_extensions_enabled=self.sync_context.language_extensions_enabled, + ) sync_flow_factory_mock.assert_called_once_with( self.build_context, self.deploy_context, self.sync_context, stacks, False ) @@ -144,7 +148,11 @@ def test_add_template_triggers(self, get_stack_mock, template_trigger_mock): self.watch_manager._add_template_triggers() template_trigger_mock.assert_called_once_with(self.template, stack_name, ANY) - get_stack_mock.assert_called_with(self.template, use_sam_transform=False) + get_stack_mock.assert_called_with( + self.template, + use_sam_transform=False, + language_extensions_enabled=self.sync_context.language_extensions_enabled, + ) self.path_observer.schedule_handlers.assert_any_call(trigger.get_path_handlers.return_value) @patch("samcli.lib.sync.watch_manager.TemplateTrigger")