Skip to content
Merged
27 changes: 27 additions & 0 deletions samcli/cli/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,33 @@ def convert(self, value, param, ctx):
return {key: _value}


class DockerAdditionalHostType(click.ParamType):
"""
Custom Parameter Type for managing Docker container's host file for local commands
"""

name = "list"
MIN_KEY_VALUE_PAIR_LENGTH = 2

def convert(self, value, param, ctx):
"""Converts the user provided parameters value with the format "host:IP" to dict
{"host": "IP"}

Parameters
------------
value: User provided value for the click option
param: click parameter
ctx: Context
"""
host_ip_pair = value.split(":", maxsplit=1)
if len(host_ip_pair) < self.MIN_KEY_VALUE_PAIR_LENGTH:
raise click.BadParameter(f"{param.opts[0]} is not a valid format, it needs to be of the form hostname:IP")
host = host_ip_pair[0]
ip = host_ip_pair[1]
LOG.debug("Converting provided %s option value to dict", param.opts[0])
return {host: ip}


class RemoteInvokeOutputFormatType(click.Choice):
"""
Custom Parameter Type for output-format option of remote invoke command.
Expand Down
15 changes: 15 additions & 0 deletions samcli/commands/_utils/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,21 @@ def remote_invoke_boto_parameter_callback(ctx, param, provided_value):
return boto_api_parameters


def local_add_host_callback(ctx, param, provided_value):
"""
Create a dictionary of hostnames to IP addresses to add into Docker container's hosts file.
:param ctx: Click Context
:param param: Param name
:param provided_value: Value provided by Click, after being processed by DockerAdditionalHostType.
:return: dictionary of hostnames to IP addresses.
"""
extra_hosts = {}
for value in provided_value:
extra_hosts.update(value)

return extra_hosts


def artifact_callback(ctx, param, provided_value, artifact):
"""
Provide an error if there are zip/image artifact based resources,
Expand Down
7 changes: 7 additions & 0 deletions samcli/commands/local/cli_common/invoke_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ def __init__(
shutdown: bool = False,
container_host: Optional[str] = None,
container_host_interface: Optional[str] = None,
add_host: Optional[dict] = None,
invoke_images: Optional[str] = None,
) -> None:
"""
Expand Down Expand Up @@ -148,6 +149,8 @@ def __init__(
Optional. Host of locally emulated Lambda container
container_host_interface string
Optional. Interface that Docker host binds ports to
add_host dict
Optional. Docker extra hosts support from --add-host parameters
invoke_images dict
Optional. A dictionary that defines the custom invoke image URI of each function
"""
Expand Down Expand Up @@ -177,6 +180,9 @@ def __init__(

self._container_host = container_host
self._container_host_interface = container_host_interface

self._extra_hosts: Optional[Dict] = add_host

self._invoke_images = invoke_images

self._containers_mode = ContainersMode.COLD
Expand Down Expand Up @@ -396,6 +402,7 @@ def local_lambda_runner(self) -> LocalLambdaRunner:
debug_context=self._debug_context,
container_host=self._container_host,
container_host_interface=self._container_host_interface,
extra_hosts=self._extra_hosts,
)
return self._local_lambda_runner

Expand Down
20 changes: 19 additions & 1 deletion samcli/commands/local/cli_common/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@

import click

from samcli.commands._utils.options import docker_click_options, parameter_override_click_option, template_click_option
from samcli.cli.types import DockerAdditionalHostType
from samcli.commands._utils.options import (
docker_click_options,
local_add_host_callback,
parameter_override_click_option,
template_click_option,
)
from samcli.commands.local.cli_common.invoke_context import ContainersInitializationMode
from samcli.local.docker.container import DEFAULT_CONTAINER_HOST_INTERFACE

Expand Down Expand Up @@ -65,6 +71,18 @@ def local_common_options(f):
help="IP address of the host network interface that container ports should bind to. "
"Use 0.0.0.0 to bind to all interfaces.",
),
click.option(
"--add-host",
multiple=True,
type=DockerAdditionalHostType(),
callback=local_add_host_callback,
required=False,
help="Passes 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",
),
click.option(
"--invoke-image",
"-ii",
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 @@ -90,6 +90,7 @@ def cli(
config_env,
container_host,
container_host_interface,
add_host,
invoke_image,
hook_name,
skip_prepare_infra,
Expand Down Expand Up @@ -121,6 +122,7 @@ def cli(
parameter_overrides,
container_host,
container_host_interface,
add_host,
invoke_image,
hook_name,
) # pragma: no cover
Expand All @@ -147,6 +149,7 @@ def do_cli( # pylint: disable=R0914
parameter_overrides,
container_host,
container_host_interface,
add_host,
invoke_image,
hook_name,
):
Expand Down Expand Up @@ -195,6 +198,7 @@ def do_cli( # pylint: disable=R0914
shutdown=shutdown,
container_host=container_host,
container_host_interface=container_host_interface,
add_host=add_host,
invoke_images=processed_invoke_images,
) as context:
# Invoke the function
Expand Down
1 change: 1 addition & 0 deletions samcli/commands/local/invoke/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"shutdown",
"container_host",
"container_host_interface",
"add_host",
"invoke_image",
]

Expand Down
4 changes: 4 additions & 0 deletions samcli/commands/local/lib/local_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def __init__(
debug_context: Optional[DebugContext] = None,
container_host: Optional[str] = None,
container_host_interface: Optional[str] = None,
extra_hosts: Optional[dict] = None,
) -> None:
"""
Initializes the class
Expand All @@ -65,6 +66,7 @@ def __init__(
: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
:param dict extra_hosts: Optional. Dict of hostname to IP resolutions
"""

self.local_runtime = local_runtime
Expand All @@ -78,6 +80,7 @@ def __init__(
self._boto3_region: Optional[str] = None
self.container_host = container_host
self.container_host_interface = container_host_interface
self.extra_hosts = extra_hosts

def invoke(
self,
Expand Down Expand Up @@ -149,6 +152,7 @@ def invoke(
stderr=stderr,
container_host=self.container_host,
container_host_interface=self.container_host_interface,
extra_hosts=self.extra_hosts,
)
except ContainerResponseException:
# NOTE(sriram-mv): This should still result in a exit code zero to avoid regressions.
Expand Down
2 changes: 2 additions & 0 deletions samcli/commands/local/start_api/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ def cli(
debug_function,
container_host,
container_host_interface,
add_host,
invoke_image,
hook_name,
skip_prepare_infra,
Expand Down Expand Up @@ -150,6 +151,7 @@ def cli(
debug_function,
container_host,
container_host_interface,
add_host,
invoke_image,
hook_name,
) # pragma: no cover
Expand Down
1 change: 1 addition & 0 deletions samcli/commands/local/start_api/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"shutdown",
"container_host",
"container_host_interface",
"add_host",
"invoke_image",
"disable_authorizer",
]
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 @@ -96,6 +96,7 @@ def cli(
debug_function,
container_host,
container_host_interface,
add_host,
invoke_image,
hook_name,
skip_prepare_infra,
Expand Down Expand Up @@ -128,6 +129,7 @@ def cli(
debug_function,
container_host,
container_host_interface,
add_host,
invoke_image,
hook_name,
) # pragma: no cover
Expand Down Expand Up @@ -155,6 +157,7 @@ def do_cli( # pylint: disable=R0914
debug_function,
container_host,
container_host_interface,
add_host,
invoke_image,
hook_name,
):
Expand Down Expand Up @@ -200,6 +203,7 @@ def do_cli( # pylint: disable=R0914
shutdown=shutdown,
container_host=container_host,
container_host_interface=container_host_interface,
add_host=add_host,
invoke_images=processed_invoke_images,
) as invoke_context:
service = LocalLambdaService(lambda_invoke_context=invoke_context, port=port, host=host)
Expand Down
1 change: 1 addition & 0 deletions samcli/commands/local/start_lambda/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"shutdown",
"container_host",
"container_host_interface",
"add_host",
"invoke_image",
]

Expand Down
6 changes: 6 additions & 0 deletions samcli/local/docker/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def __init__(
container_host_interface=DEFAULT_CONTAINER_HOST_INTERFACE,
mount_with_write: bool = False,
host_tmp_dir: Optional[str] = None,
extra_hosts: Optional[dict] = None,
):
"""
Initializes the class with given configuration. This does not automatically create or run the container.
Expand All @@ -99,6 +100,7 @@ def __init__(
:param bool mount_with_write: Optional. Mount source code directory with write permissions when
building on container
:param string host_tmp_dir: Optional. Temporary directory on the host when mounting with write permissions.
:param dict extra_hosts: Optional. Dict of hostname to IP resolutions
"""

self._image = image
Expand All @@ -113,6 +115,7 @@ def __init__(
self._container_opts = container_opts
self._additional_volumes = additional_volumes
self._logs_thread = None
self._extra_hosts = extra_hosts

# Use the given Docker client or create new one
self.docker_client = docker_client or docker.from_env(version=DOCKER_MIN_API_VERSION)
Expand Down Expand Up @@ -213,6 +216,9 @@ def create(self):
# Ex: 128m => 128MB
kwargs["mem_limit"] = "{}m".format(self._memory_limit_mb)

if self._extra_hosts:
kwargs["extra_hosts"] = self._extra_hosts

real_container = self.docker_client.containers.create(self._image, **kwargs)
self.id = real_container.id

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 @@ -51,6 +51,7 @@ def __init__(
debug_options=None,
container_host=None,
container_host_interface=DEFAULT_CONTAINER_HOST_INTERFACE,
extra_hosts=None,
function_full_path=None,
):
"""
Expand Down Expand Up @@ -87,6 +88,8 @@ def __init__(
Optional. Host of locally emulated Lambda container
container_host_interface
Optional. Interface that Docker host binds ports to
extra_hosts
Optional. Dict of hostname to IP resolutions
function_full_path str
Optional. The function full path, unique in all stacks
"""
Expand Down Expand Up @@ -138,6 +141,7 @@ def __init__(
additional_volumes=additional_volumes,
container_host=container_host,
container_host_interface=container_host_interface,
extra_hosts=extra_hosts,
)

@staticmethod
Expand Down
20 changes: 16 additions & 4 deletions samcli/local/lambdafn/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ 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, container_host=None, container_host_interface=None):
def create(
self, function_config, debug_context=None, container_host=None, container_host_interface=None, extra_hosts=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 @@ -97,6 +99,7 @@ def create(self, function_config, debug_context=None, container_host=None, conta
debug_options=debug_context,
container_host=container_host,
container_host_interface=container_host_interface,
extra_hosts=extra_hosts,
function_full_path=function_config.full_path,
)
try:
Expand Down Expand Up @@ -159,6 +162,7 @@ def invoke(
stderr: Optional[StreamWriter] = None,
container_host=None,
container_host_interface=None,
extra_hosts=None,
):
"""
Invoke the given Lambda function locally.
Expand All @@ -181,12 +185,16 @@ def invoke(
Host of locally emulated Lambda container
:param string container_host_interface: Optional.
Interface that Docker host binds ports to
:param dict extra_hosts: Optional.
Dict of hostname to IP resolutions
:raises Keyboard
"""
container = None
try:
# Start the container. This call returns immediately after the container starts
container = self.create(function_config, debug_context, container_host, container_host_interface)
container = self.create(
function_config, debug_context, container_host, container_host_interface, extra_hosts
)
container = self.run(container, function_config, debug_context)
# Setup appropriate interrupt - timeout or Ctrl+C - before function starts executing and
# get callback function to start timeout timer
Expand Down Expand Up @@ -351,7 +359,9 @@ def __init__(self, container_manager, image_builder, observer=None):

super().__init__(container_manager, image_builder)

def create(self, function_config, debug_context=None, container_host=None, container_host_interface=None):
def create(
self, function_config, debug_context=None, container_host=None, container_host_interface=None, extra_hosts=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 @@ -405,7 +415,9 @@ def create(self, function_config, debug_context=None, container_host=None, conta
self._observer.watch(function_config)
self._observer.start()

container = super().create(function_config, debug_context, container_host, container_host_interface)
container = super().create(
function_config, debug_context, container_host, container_host_interface, extra_hosts
)
self._function_configs[function_config.full_path] = function_config
self._containers[function_config.full_path] = container

Expand Down
Loading