Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ jmespath~=1.0.1
ruamel_yaml~=0.18.15
PyYAML~=6.0
cookiecutter~=2.6.0
python-dotenv~=1.0.0
aws-sam-translator==1.100.0
#docker minor version updates can include breaking changes. Auto update micro version only.
docker~=7.1.0
Expand Down
7 changes: 7 additions & 0 deletions requirements/reproducible-linux.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
#
# pip-compile --allow-unsafe --generate-hashes --output-file=requirements/reproducible-linux.txt
#
--extra-index-url https://plugin.us-east-1.prod.workshops.aws
--trusted-host plugin.us-east-1.prod.workshops.aws

annotated-types==0.7.0 \
--hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \
--hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89
Expand Down Expand Up @@ -602,6 +605,10 @@ python-dateutil==2.9.0.post0 \
# arrow
# botocore
# dateparser
python-dotenv==1.0.1 \
--hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \
--hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a
# via aws-sam-cli (setup.py)
python-slugify==8.0.4 \
--hash=sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8 \
--hash=sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856
Expand Down
7 changes: 7 additions & 0 deletions requirements/reproducible-mac.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
#
# pip-compile --allow-unsafe --generate-hashes --output-file=requirements/reproducible-mac.txt
#
--extra-index-url https://plugin.us-east-1.prod.workshops.aws
--trusted-host plugin.us-east-1.prod.workshops.aws

annotated-types==0.7.0 \
--hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \
--hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89
Expand Down Expand Up @@ -601,6 +604,10 @@ python-dateutil==2.9.0.post0 \
# arrow
# botocore
# dateparser
python-dotenv==1.0.1 \
--hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \
--hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a
# via aws-sam-cli (setup.py)
python-slugify==8.0.4 \
--hash=sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8 \
--hash=sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856
Expand Down
85 changes: 83 additions & 2 deletions samcli/commands/local/cli_common/invoke_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from pathlib import Path
from typing import Any, Dict, List, Optional, TextIO, Tuple, Type, cast

from dotenv import dotenv_values

from samcli.commands._utils.template import TemplateFailedParsingException, TemplateNotFoundException
from samcli.commands.exceptions import ContainersInitializationException
from samcli.commands.local.cli_common.user_exceptions import DebugContextException, InvokeContextException
Expand Down Expand Up @@ -81,6 +83,7 @@ def __init__(
template_file: str,
function_identifier: Optional[str] = None,
env_vars_file: Optional[str] = None,
dotenv_file: Optional[str] = None,
docker_volume_basedir: Optional[str] = None,
docker_network: Optional[str] = None,
log_file: Optional[str] = None,
Expand All @@ -89,6 +92,7 @@ def __init__(
debug_args: Optional[str] = None,
debugger_path: Optional[str] = None,
container_env_vars_file: Optional[str] = None,
container_dotenv_file: Optional[str] = None,
parameter_overrides: Optional[Dict] = None,
layer_cache_basedir: Optional[str] = None,
force_image_build: Optional[bool] = None,
Expand All @@ -115,6 +119,8 @@ def __init__(
Identifier of the function to invoke
env_vars_file str
Path to a file containing values for environment variables
dotenv_file str
Path to .env file containing environment variables
docker_volume_basedir str
Directory for the Docker volume
docker_network str
Expand All @@ -131,6 +137,10 @@ def __init__(
Additional arguments passed to the debugger
debugger_path str
Path to the directory of the debugger to mount on Docker
container_env_vars_file str
Path to a file containing values for container environment variables
container_dotenv_file str
Path to .env file containing container environment variables
parameter_overrides dict
Values for the template parameters
layer_cache_basedir str
Expand Down Expand Up @@ -164,6 +174,7 @@ def __init__(
self._template_file = template_file
self._function_identifier = function_identifier
self._env_vars_file = env_vars_file
self._dotenv_file = dotenv_file
self._docker_volume_basedir = docker_volume_basedir
self._docker_network = docker_network
self._log_file = log_file
Expand All @@ -172,6 +183,7 @@ def __init__(
self._debug_args = debug_args
self._debugger_path = debugger_path
self._container_env_vars_file = container_env_vars_file
self._container_dotenv_file = container_dotenv_file

self._parameter_overrides = parameter_overrides
# Override certain CloudFormation pseudo-parameters based on values provided by customer
Expand Down Expand Up @@ -249,8 +261,54 @@ def __enter__(self) -> "InvokeContext":
*_function_providers_args[self._containers_mode]
)

self._env_vars_value = self._get_env_vars_value(self._env_vars_file)
self._container_env_vars_value = self._get_env_vars_value(self._container_env_vars_file)
# Load environment variables from both dotenv and JSON files
# dotenv values are loaded first, then env_vars JSON can override them
dotenv_vars = self._get_dotenv_values(self._dotenv_file)
env_vars = self._get_env_vars_value(self._env_vars_file)

# Wrap dotenv vars in "Parameters" key for proper format
# The env_vars system expects: {"Parameters": {key:value}} or {FunctionName: {key:value}}
dotenv_wrapped = {"Parameters": dotenv_vars} if dotenv_vars else None

# Merge dotenv and env_vars, with env_vars taking precedence
if dotenv_wrapped and env_vars:
# If both exist, merge them with env_vars taking precedence
merged = {**dotenv_wrapped}
for key, value in env_vars.items():
if key == "Parameters":
# Merge Parameters sections, with env_vars taking precedence
# dotenv_wrapped always has "Parameters" key when it exists
merged["Parameters"] = {**merged["Parameters"], **value}
else:
# For function-specific overrides like {FunctionName: {key:value}}
merged[key] = value
self._env_vars_value = merged
elif dotenv_wrapped:
self._env_vars_value = dotenv_wrapped
else:
self._env_vars_value = env_vars

# Load container environment variables from both dotenv and JSON files
# Container env vars are used for debugging and should be flat key-value pairs
container_dotenv_vars = self._get_dotenv_values(self._container_dotenv_file)
container_env_vars = self._get_env_vars_value(self._container_env_vars_file)

# Debug logging
LOG.debug("Container dotenv vars loaded: %s", container_dotenv_vars)
LOG.debug("Container env vars (JSON) loaded: %s", container_env_vars)

# Merge container dotenv and container env_vars, with container env_vars taking precedence
# Unlike regular env_vars, container env_vars stay flat (no Parameters wrapping) for debugging
if container_dotenv_vars and container_env_vars:
# If both exist, merge them with container env_vars taking precedence
self._container_env_vars_value = {**container_dotenv_vars, **container_env_vars}
elif container_dotenv_vars:
self._container_env_vars_value = container_dotenv_vars
else:
self._container_env_vars_value = container_env_vars

LOG.debug("Final container env vars value: %s", self._container_env_vars_value)

self._log_file_handle = self._setup_log_file(self._log_file)

# in case of warm containers && debugging is enabled && if debug-function property is not provided, so
Expand Down Expand Up @@ -520,6 +578,29 @@ def _get_stacks(self) -> List[Stack]:
LOG.debug("Can't read stacks information, either template is not found or it is invalid", exc_info=ex)
raise ex

@staticmethod
def _get_dotenv_values(filename: Optional[str]) -> Optional[Dict]:
"""
If the user provided a .env file, this method will read the file and return its values as a dictionary

:param string filename: Path to .env file containing environment variable values
:return dict: Value of environment variables from .env file, if provided. None otherwise
:raises InvokeContextException: If the file was not found or could not be parsed
"""
if not filename:
return None

try:
# dotenv_values returns a dictionary with all variables from the .env file
# It handles comments, quotes, multiline values, etc.
env_dict = dotenv_values(filename)
# Filter out None values and convert to strings
return {k: str(v) if v is not None else "" for k, v in env_dict.items()}
except Exception as ex:
raise InvalidEnvironmentVariablesFileException(
"Could not read environment variables from .env file {}: {}".format(filename, str(ex))
) from ex

@staticmethod
def _get_env_vars_value(filename: Optional[str]) -> Optional[Dict]:
"""
Expand Down
16 changes: 16 additions & 0 deletions samcli/commands/local/cli_common/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ def invoke_common_options(f):
type=click.Path(exists=True),
help="JSON file containing values for Lambda function's environment variables.",
),
click.option(
"--dotenv",
type=click.Path(exists=True),
help="Path to a .env file containing environment variables for Lambda functions. "
"Variables defined here will apply to all functions. "
"If both --env-vars and --dotenv are provided, variables from both will be merged "
"with --env-vars taking precedence.",
),
parameter_override_click_option(),
click.option(
"--debug-port",
Expand All @@ -196,6 +204,14 @@ def invoke_common_options(f):
help="JSON file containing additional environment variables to be set within the container when "
"used in a debugging session locally.",
),
click.option(
"--container-dotenv",
type=click.Path(exists=True),
help="Path to a .env file containing additional environment variables to be set within the container "
"when used in a debugging session locally. "
"If both --container-env-vars and --container-dotenv are provided, variables from both will be merged "
"with --container-env-vars taking precedence.",
),
click.option(
"--docker-volume-basedir",
"-v",
Expand Down
8 changes: 8 additions & 0 deletions samcli/commands/local/invoke/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,12 @@ def cli(
event,
no_event,
env_vars,
dotenv,
debug_port,
debug_args,
debugger_path,
container_env_vars,
container_dotenv,
docker_volume_basedir,
docker_network,
log_file,
Expand Down Expand Up @@ -130,10 +132,12 @@ def cli(
event,
no_event,
env_vars,
dotenv,
debug_port,
debug_args,
debugger_path,
container_env_vars,
container_dotenv,
docker_volume_basedir,
docker_network,
log_file,
Expand All @@ -160,10 +164,12 @@ def do_cli( # pylint: disable=R0914
event,
no_event,
env_vars,
dotenv,
debug_port,
debug_args,
debugger_path,
container_env_vars,
container_dotenv,
docker_volume_basedir,
docker_network,
log_file,
Expand Down Expand Up @@ -210,6 +216,7 @@ def do_cli( # pylint: disable=R0914
template_file=template,
function_identifier=function_identifier,
env_vars_file=env_vars,
dotenv_file=dotenv,
docker_volume_basedir=docker_volume_basedir,
docker_network=docker_network,
log_file=log_file,
Expand All @@ -218,6 +225,7 @@ def do_cli( # pylint: disable=R0914
debug_args=debug_args,
debugger_path=debugger_path,
container_env_vars_file=container_env_vars,
container_dotenv_file=container_dotenv,
parameter_overrides=parameter_overrides,
layer_cache_basedir=layer_cache_basedir,
force_image_build=force_image_build,
Expand Down
2 changes: 2 additions & 0 deletions samcli/commands/local/invoke/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
"event",
"no_event",
"env_vars",
"dotenv",
"container_env_vars",
"container_dotenv",
"debug_port",
"debugger_path",
"debug_args",
Expand Down
3 changes: 2 additions & 1 deletion samcli/commands/local/lib/debug_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ def __init__(
self.container_env_vars = container_env_vars

def __bool__(self):
return bool(self.debug_ports)
# DebugContext is "truthy" if we have either debug ports OR container env vars
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this change for? Why is this needed now and wasn't needed previously?

Copy link
Author

Choose a reason for hiding this comment

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

The change was needed because --container-dotenv adds container env vars even without debug ports. Previously, __bool__() only checked debug_ports, making it false when only container env vars exist. Changed to return bool(self.debug_function) to properly handle warm containers with selective debugging.

return bool(self.debug_ports or self.container_env_vars)

def __nonzero__(self):
return self.__bool__()
8 changes: 8 additions & 0 deletions samcli/commands/local/start_api/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,12 @@ def cli(
# Common Options for Lambda Invoke
template_file,
env_vars,
dotenv,
debug_port,
debug_args,
debugger_path,
container_env_vars,
container_dotenv,
docker_volume_basedir,
docker_network,
log_file,
Expand Down Expand Up @@ -156,10 +158,12 @@ def cli(
static_dir,
template_file,
env_vars,
dotenv,
debug_port,
debug_args,
debugger_path,
container_env_vars,
container_dotenv,
docker_volume_basedir,
docker_network,
log_file,
Expand Down Expand Up @@ -189,10 +193,12 @@ def do_cli( # pylint: disable=R0914
static_dir,
template,
env_vars,
dotenv,
debug_port,
debug_args,
debugger_path,
container_env_vars,
container_dotenv,
docker_volume_basedir,
docker_network,
log_file,
Expand Down Expand Up @@ -236,6 +242,7 @@ def do_cli( # pylint: disable=R0914
template_file=template,
function_identifier=None, # Don't scope to one particular function
env_vars_file=env_vars,
dotenv_file=dotenv,
docker_volume_basedir=docker_volume_basedir,
docker_network=docker_network,
log_file=log_file,
Expand All @@ -244,6 +251,7 @@ def do_cli( # pylint: disable=R0914
debug_args=debug_args,
debugger_path=debugger_path,
container_env_vars_file=container_env_vars,
container_dotenv_file=container_dotenv,
parameter_overrides=parameter_overrides,
layer_cache_basedir=layer_cache_basedir,
force_image_build=force_image_build,
Expand Down
2 changes: 2 additions & 0 deletions samcli/commands/local/start_api/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"ssl_cert_file",
"ssl_key_file",
"env_vars",
"dotenv",
"container_env_vars",
"container_dotenv",
"debug_port",
"debugger_path",
"debug_args",
Expand Down
Loading
Loading