Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add option --container-host to commands local start-api, local start-lambda and local invoke #2700

Merged
merged 13 commits into from
Mar 23, 2021
6 changes: 6 additions & 0 deletions samcli/commands/local/cli_common/invoke_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def __init__(
warm_container_initialization_mode: Optional[str] = None,
debug_function: Optional[str] = None,
shutdown: bool = False,
container_host: Optional[str] = None,
) -> None:
"""
Initialize the context
Expand Down Expand Up @@ -121,6 +122,8 @@ def __init__(
option is enabled
shutdown bool
Optional. If True, perform a SHUTDOWN event when tearing down containers. Default False.
container_host string
Optional. Host of locally emulated Lambda container
"""
self._template_file = template_file
self._function_identifier = function_identifier
Expand All @@ -146,6 +149,8 @@ def __init__(
self._aws_profile = aws_profile
self._shutdown = shutdown

self._container_host = container_host

self._containers_mode = ContainersMode.COLD
self._containers_initializing_mode = ContainersInitializationMode.LAZY

Expand Down Expand Up @@ -327,6 +332,7 @@ def local_lambda_runner(self) -> LocalLambdaRunner:
aws_region=self._aws_region,
env_vars_values=self._env_vars_value,
debug_context=self._debug_context,
container_host=self._container_host,
)
return self._local_lambda_runner

Expand Down
11 changes: 10 additions & 1 deletion samcli/commands/local/cli_common/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,16 @@ def local_common_options(f):
default=False,
help="If set, will emulate a shutdown event after the invoke completes, "
"in order to test extension handling of shutdown behavior.",
)
),
click.option(
"--container-host",
Copy link
Contributor

Choose a reason for hiding this comment

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

For any security reasons, do we need to validate if this option valid?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a good point. Let me check on this and get back to you.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For security reasons, there is nothing we can validate. There is a use case that docker daemon is running on a different host and the customer wants to use that host. We won't be able to understand if it's a good host provided by customer or just a bad actor.

default="localhost",
show_default=True,
help="Host of locally emulated Lambda container. "
"This option is useful when the container run on a different host than SAM CLI. "
"For example, if you want to run SAM CLI in a Docker container on macOS, "
"use this option with host.docker.internal",
),
]

# Reverse the list to maintain ordering of options in help text printed with --help
Expand Down
4 changes: 4 additions & 0 deletions samcli/commands/local/invoke/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def cli(
parameter_overrides,
config_file,
config_env,
container_host,
):
"""
`sam local invoke` command entry point
Expand All @@ -97,6 +98,7 @@ def cli(
force_image_build,
shutdown,
parameter_overrides,
container_host,
) # pragma: no cover


Expand All @@ -119,6 +121,7 @@ def do_cli( # pylint: disable=R0914
force_image_build,
shutdown,
parameter_overrides,
container_host,
):
"""
Implementation of the ``cli`` method, just separated out for unit testing purposes
Expand Down Expand Up @@ -161,6 +164,7 @@ def do_cli( # pylint: disable=R0914
aws_region=ctx.region,
aws_profile=ctx.profile,
shutdown=shutdown,
container_host=container_host,
) as context:

# Invoke the function
Expand Down
12 changes: 11 additions & 1 deletion samcli/commands/local/lib/local_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def __init__(
aws_region: Optional[str] = None,
env_vars_values: Optional[Dict[Any, Any]] = None,
debug_context: Optional[DebugContext] = None,
container_host: Optional[str] = None,
Copy link
Contributor

Choose a reason for hiding this comment

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

Since there is a default value for the parameter, would this param be None anytime?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This param won't be None in the current use case. The reasons I set it to None:

  • I'd like to have minimum impact on the current use case. If this class is initialized somewhere else, we shouldn't assume it's localhost unless it's passed from cli.
  • Match to the overall style.

) -> None:
"""
Initializes the class
Expand All @@ -55,6 +56,7 @@ def __init__(
:param string aws_region: Optional. AWS Region to use.
:param dict env_vars_values: Optional. Dictionary containing values of environment variables.
:param DebugContext debug_context: Optional. Debug context for the function (includes port, args, and path).
:param string container_host: Optional. Host of locally emulated Lambda container
"""

self.local_runtime = local_runtime
Expand All @@ -66,6 +68,7 @@ def __init__(
self.debug_context = debug_context
self._boto3_session_creds: Optional[Dict[str, str]] = None
self._boto3_region: Optional[str] = None
self.container_host = container_host

def invoke(
self,
Expand Down Expand Up @@ -121,7 +124,14 @@ def invoke(

# Invoke the function
try:
self.local_runtime.invoke(config, event, debug_context=self.debug_context, stdout=stdout, stderr=stderr)
self.local_runtime.invoke(
config,
event,
debug_context=self.debug_context,
stdout=stdout,
stderr=stderr,
container_host=self.container_host,
)
except ContainerResponseException:
# NOTE(sriram-mv): This should still result in a exit code zero to avoid regressions.
LOG.info("No response from invoke container for %s", function.name)
Expand Down
4 changes: 4 additions & 0 deletions samcli/commands/local/start_api/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def cli(
warm_containers,
shutdown,
debug_function,
container_host,
):
"""
`sam local start-api` command entry point
Expand Down Expand Up @@ -108,6 +109,7 @@ def cli(
warm_containers,
shutdown,
debug_function,
container_host,
) # pragma: no cover


Expand All @@ -132,6 +134,7 @@ def do_cli( # pylint: disable=R0914
warm_containers,
shutdown,
debug_function,
container_host,
):
"""
Implementation of the ``cli`` method, just separated out for unit testing purposes
Expand Down Expand Up @@ -172,6 +175,7 @@ def do_cli( # pylint: disable=R0914
warm_container_initialization_mode=warm_containers,
debug_function=debug_function,
shutdown=shutdown,
container_host=container_host,
) as invoke_context:

service = LocalApiService(lambda_invoke_context=invoke_context, port=port, host=host, static_dir=static_dir)
Expand Down
4 changes: 4 additions & 0 deletions samcli/commands/local/start_lambda/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def cli(
warm_containers,
shutdown,
debug_function,
container_host,
):
"""
`sam local start-lambda` command entry point
Expand All @@ -118,6 +119,7 @@ def cli(
warm_containers,
shutdown,
debug_function,
container_host,
) # pragma: no cover


Expand All @@ -141,6 +143,7 @@ def do_cli( # pylint: disable=R0914
warm_containers,
shutdown,
debug_function,
container_host,
):
"""
Implementation of the ``cli`` method, just separated out for unit testing purposes
Expand Down Expand Up @@ -180,6 +183,7 @@ def do_cli( # pylint: disable=R0914
warm_container_initialization_mode=warm_containers,
debug_function=debug_function,
shutdown=shutdown,
container_host=container_host,
) as invoke_context:

service = LocalLambdaService(lambda_invoke_context=invoke_context, port=port, host=host)
Expand Down
10 changes: 8 additions & 2 deletions samcli/local/docker/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class Container:
_STDOUT_FRAME_TYPE = 1
_STDERR_FRAME_TYPE = 2
RAPID_PORT_CONTAINER = "8080"
URL = "http://localhost:{port}/2015-03-31/functions/{function_name}/invocations"
URL = "http://{host}:{port}/2015-03-31/functions/{function_name}/invocations"
# Set connection timeout to 1 sec to support the large input.
RAPID_CONNECTION_TIMEOUT = 1

Expand All @@ -55,6 +55,7 @@ def __init__(
docker_client=None,
container_opts=None,
additional_volumes=None,
container_host="localhost",
):
"""
Initializes the class with given configuration. This does not automatically create or run the container.
Expand All @@ -71,6 +72,7 @@ def __init__(
:param docker_client: Optional, a docker client to replace the default one loaded from env
:param container_opts: Optional, a dictionary containing the container options
:param additional_volumes: Optional list of additional volumes
:param string container_host: Optional. Host of locally emulated Lambda container
Copy link
Contributor

Choose a reason for hiding this comment

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

this cannot be optional anymore, because if customers do not specify a value, click will use the default value localhost.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same reason as my another comment below.

"""

self._image = image
Expand All @@ -96,6 +98,9 @@ def __init__(
# selecting the first free port in a range that's not ephemeral.
self._start_port_range = 5000
self._end_port_range = 9000

self._container_host = container_host

try:
self.rapid_port_host = find_free_port(start=self._start_port_range, end=self._end_port_range)
except NoFreePortsError as ex:
Expand Down Expand Up @@ -266,8 +271,9 @@ def wait_for_http_response(self, name, event, stdout):
# TODO(sriram-mv): `aws-lambda-rie` is in a mode where the function_name is always "function"
# NOTE(sriram-mv): There is a connection timeout set on the http call to `aws-lambda-rie`, however there is not
# a read time out for the response received from the server.

resp = requests.post(
self.URL.format(port=self.rapid_port_host, function_name="function"),
self.URL.format(host=self._container_host, port=self.rapid_port_host, function_name="function"),
data=event.encode("utf-8"),
timeout=(self.RAPID_CONNECTION_TIMEOUT, None),
)
Expand Down
4 changes: 4 additions & 0 deletions samcli/local/docker/lambda_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def __init__(
memory_mb=128,
env_vars=None,
debug_options=None,
container_host=None,
):
"""
Initializes the class
Expand Down Expand Up @@ -74,6 +75,8 @@ def __init__(
Optional. Dictionary containing environment variables passed to container
debug_options DebugContext
Optional. Contains container debugging info (port, debugger path)
container_host string
Optional. Host of locally emulated Lambda container
"""
if not Runtime.has_value(runtime) and not packagetype == IMAGE:
raise ValueError("Unsupported Lambda runtime {}".format(runtime))
Expand Down Expand Up @@ -119,6 +122,7 @@ def __init__(
env_vars=env_vars,
container_opts=additional_options,
additional_volumes=additional_volumes,
container_host=container_host,
)

@staticmethod
Expand Down
14 changes: 10 additions & 4 deletions samcli/local/lambdafn/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def __init__(self, container_manager, image_builder):
self._image_builder = image_builder
self._temp_uncompressed_paths_to_be_cleaned = []

def create(self, function_config, debug_context=None):
def create(self, function_config, debug_context=None, container_host=None):
"""
Create a new Container for the passed function, then store it in a dictionary using the function name,
so it can be retrieved later and used in the other functions. Make sure to use the debug_context only
Expand All @@ -56,6 +56,8 @@ def create(self, function_config, debug_context=None):
Configuration of the function to create a new Container for it.
debug_context DebugContext
Debugging context for the function (includes port, args, and path)
container_host string
Host of locally emulated Lambda container

Returns
-------
Expand All @@ -78,6 +80,7 @@ def create(self, function_config, debug_context=None):
memory_mb=function_config.memory,
env_vars=env_vars,
debug_options=debug_context,
container_host=container_host,
)
try:
# create the container.
Expand Down Expand Up @@ -132,6 +135,7 @@ def invoke(
debug_context=None,
stdout: Optional[StreamWriter] = None,
stderr: Optional[StreamWriter] = None,
container_host=None,
):
"""
Invoke the given Lambda function locally.
Expand All @@ -150,13 +154,15 @@ def invoke(
StreamWriter that receives stdout text from container.
:param samcli.lib.utils.stream_writer.StreamWriter stderr: Optional.
StreamWriter that receives stderr text from container.
:param string container_host: Optional.
Host of locally emulated Lambda container
:raises Keyboard
"""
timer = None
container = None
try:
# Start the container. This call returns immediately after the container starts
container = self.create(function_config, debug_context)
container = self.create(function_config, debug_context, container_host)
container = self.run(container, function_config, debug_context)
# Setup appropriate interrupt - timeout or Ctrl+C - before function starts executing.
#
Expand Down Expand Up @@ -301,7 +307,7 @@ def __init__(self, container_manager, image_builder):

super().__init__(container_manager, image_builder)

def create(self, function_config, debug_context=None):
def create(self, function_config, debug_context=None, container_host=None):
"""
Create a new Container for the passed function, then store it in a dictionary using the function name,
so it can be retrieved later and used in the other functions. Make sure to use the debug_context only
Expand Down Expand Up @@ -336,7 +342,7 @@ def create(self, function_config, debug_context=None):
)
debug_context = None

container = super().create(function_config, debug_context)
container = super().create(function_config, debug_context, container_host)
self._containers[function_config.name] = container

self._observer.watch(function_config)
Expand Down
Loading