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 and --container-host-interface to sam local commands #2806

Merged
merged 10 commits into from
Apr 19, 2021
15 changes: 14 additions & 1 deletion samcli/commands/local/cli_common/invoke_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ def __init__(
warm_container_initialization_mode: Optional[str] = None,
debug_function: Optional[str] = None,
shutdown: bool = False,
container_host: Optional[str] = None,
container_host_interface: Optional[str] = None,
) -> None:
"""
Initialize the context
Expand Down Expand Up @@ -121,6 +123,10 @@ 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
container_host_interface string
Optional. Interface that Docker host binds ports to
"""
self._template_file = template_file
self._function_identifier = function_identifier
Expand All @@ -146,6 +152,9 @@ def __init__(
self._aws_profile = aws_profile
self._shutdown = shutdown

self._container_host = container_host
self._container_host_interface = container_host_interface

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

Expand Down Expand Up @@ -243,7 +252,9 @@ def _initialize_all_functions_containers(self) -> None:

def initialize_function_container(function: Function) -> None:
function_config = self.local_lambda_runner.get_invoke_config(function)
self.lambda_runtime.run(None, function_config, self._debug_context)
self.lambda_runtime.run(
None, function_config, self._debug_context, self._container_host, self._container_host_interface
)

try:
async_context = AsyncContext()
Expand Down Expand Up @@ -327,6 +338,8 @@ 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,
container_host_interface=self._container_host_interface,
)
return self._local_lambda_runner

Expand Down
19 changes: 18 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,24 @@ 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",
default="localhost",
show_default=True,
help="Host of locally emulated Lambda container. "
"This option is useful when the container runs 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",
),
click.option(
"--container-host-interface",
default="127.0.0.1",
show_default=True,
help="Interface that Docker host binds ports to. "
"This option is useful when users want to bind ports to a different interface. "
"Note: the input should be an IP address. ",
Copy link
Contributor

Choose a reason for hiding this comment

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

Also mention that 0.0.0.0 binds to all interfaces?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure!

),
]

# Reverse the list to maintain ordering of options in help text printed with --help
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 @@ -72,6 +72,8 @@ def cli(
parameter_overrides,
config_file,
config_env,
container_host,
container_host_interface,
):
"""
`sam local invoke` command entry point
Expand All @@ -97,6 +99,8 @@ def cli(
force_image_build,
shutdown,
parameter_overrides,
container_host,
container_host_interface,
) # pragma: no cover


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

# Invoke the function
Expand Down
16 changes: 15 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,8 @@ 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,
container_host_interface: Optional[str] = None,
) -> None:
"""
Initializes the class
Expand All @@ -55,6 +57,8 @@ 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
:param string container_host_interface: Optional. Interface that Docker host binds ports to
"""

self.local_runtime = local_runtime
Expand All @@ -66,6 +70,8 @@ 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
self.container_host_interface = container_host_interface

def invoke(
self,
Expand Down Expand Up @@ -121,7 +127,15 @@ 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,
container_host_interface=self.container_host_interface,
)
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
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 @@ -81,6 +81,8 @@ def cli(
warm_containers,
shutdown,
debug_function,
container_host,
container_host_interface,
):
"""
`sam local start-api` command entry point
Expand Down Expand Up @@ -108,6 +110,8 @@ def cli(
warm_containers,
shutdown,
debug_function,
container_host,
container_host_interface,
) # pragma: no cover


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

service = LocalApiService(lambda_invoke_context=invoke_context, port=port, host=host, static_dir=static_dir)
Expand Down
8 changes: 8 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,8 @@ def cli(
warm_containers,
shutdown,
debug_function,
container_host,
container_host_interface,
):
"""
`sam local start-lambda` command entry point
Expand All @@ -118,6 +120,8 @@ def cli(
warm_containers,
shutdown,
debug_function,
container_host,
container_host_interface,
) # pragma: no cover


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

service = LocalLambdaService(lambda_invoke_context=invoke_context, port=port, host=host)
Expand Down
20 changes: 16 additions & 4 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,8 @@ def __init__(
docker_client=None,
container_opts=None,
additional_volumes=None,
container_host="localhost",
container_host_interface="127.0.0.1",
):
"""
Initializes the class with given configuration. This does not automatically create or run the container.
Expand All @@ -71,6 +73,8 @@ 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
:param string container_host_interface: Optional. Interface that Docker host binds ports to
"""

self._image = image
Expand All @@ -96,6 +100,10 @@ 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
self._container_host_interface = container_host_interface

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 @@ -150,11 +158,14 @@ def create(self):
if self._env_vars:
kwargs["environment"] = self._env_vars

kwargs["ports"] = {self.RAPID_PORT_CONTAINER: ("127.0.0.1", self.rapid_port_host)}
kwargs["ports"] = {self.RAPID_PORT_CONTAINER: (self._container_host_interface, self.rapid_port_host)}

if self._exposed_ports:
kwargs["ports"].update(
{container_port: ("127.0.0.1", host_port) for container_port, host_port in self._exposed_ports.items()}
{
container_port: (self._container_host_interface, host_port)
for container_port, host_port in self._exposed_ports.items()
}
)

if self._entrypoint:
Expand Down Expand Up @@ -266,8 +277,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
8 changes: 8 additions & 0 deletions samcli/local/docker/lambda_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ def __init__(
memory_mb=128,
env_vars=None,
debug_options=None,
container_host=None,
container_host_interface=None,
):
"""
Initializes the class
Expand Down Expand Up @@ -74,6 +76,10 @@ 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
container_host_interface
Optional. Interface that Docker host binds ports to
"""
if not Runtime.has_value(runtime) and not packagetype == IMAGE:
raise ValueError("Unsupported Lambda runtime {}".format(runtime))
Expand Down Expand Up @@ -119,6 +125,8 @@ def __init__(
env_vars=env_vars,
container_opts=additional_options,
additional_volumes=additional_volumes,
container_host=container_host,
container_host_interface=container_host_interface,
)

@staticmethod
Expand Down
Loading