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

Add --add-host cli option to expose docker container option with the same name. #6078

Merged
merged 13 commits into from
Jan 11, 2024
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):
myukselen marked this conversation as resolved.
Show resolved Hide resolved
"""
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