diff --git a/samcli/commands/local/cli_common/invoke_context.py b/samcli/commands/local/cli_common/invoke_context.py index cad942a2fb..26ba15d210 100644 --- a/samcli/commands/local/cli_common/invoke_context.py +++ b/samcli/commands/local/cli_common/invoke_context.py @@ -78,6 +78,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 @@ -124,6 +126,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 @@ -151,6 +157,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 @@ -251,7 +260,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() @@ -335,6 +346,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 diff --git a/samcli/commands/local/cli_common/options.py b/samcli/commands/local/cli_common/options.py index e5d19413c1..528c5772d3 100644 --- a/samcli/commands/local/cli_common/options.py +++ b/samcli/commands/local/cli_common/options.py @@ -48,7 +48,23 @@ 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="IP address of the host network interface that container ports should bind to. " + "Use 0.0.0.0 to bind to all interfaces.", + ), ] # Reverse the list to maintain ordering of options in help text printed with --help diff --git a/samcli/commands/local/invoke/cli.py b/samcli/commands/local/invoke/cli.py index 7ae8364a38..01c7ac808b 100644 --- a/samcli/commands/local/invoke/cli.py +++ b/samcli/commands/local/invoke/cli.py @@ -80,6 +80,8 @@ def cli( parameter_overrides, config_file, config_env, + container_host, + container_host_interface, cdk_app, cdk_context, project_type: str, @@ -109,6 +111,8 @@ def cli( force_image_build, shutdown, parameter_overrides, + container_host, + container_host_interface, project_type, iac, project, @@ -134,6 +138,8 @@ def do_cli( # pylint: disable=R0914 force_image_build, shutdown, parameter_overrides, + container_host, + container_host_interface, project_type: str, iac: IacPlugin, project: Project, @@ -179,6 +185,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, iac=iac, project=project, ) as context: diff --git a/samcli/commands/local/lib/local_lambda.py b/samcli/commands/local/lib/local_lambda.py index ef54b00984..ddc0bebafc 100644 --- a/samcli/commands/local/lib/local_lambda.py +++ b/samcli/commands/local/lib/local_lambda.py @@ -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 @@ -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 @@ -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, @@ -120,7 +126,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) diff --git a/samcli/commands/local/start_api/cli.py b/samcli/commands/local/start_api/cli.py index f1947038b3..3c88668775 100644 --- a/samcli/commands/local/start_api/cli.py +++ b/samcli/commands/local/start_api/cli.py @@ -88,6 +88,8 @@ def cli( warm_containers, shutdown, debug_function, + container_host, + container_host_interface, project_type, cdk_context, cdk_app, @@ -119,6 +121,8 @@ def cli( warm_containers, shutdown, debug_function, + container_host, + container_host_interface, project_type, iac, project, @@ -146,6 +150,8 @@ def do_cli( # pylint: disable=R0914 warm_containers, shutdown, debug_function, + container_host, + container_host_interface, project_type, iac, project, @@ -189,6 +195,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, iac=iac, project=project, ) as invoke_context: diff --git a/samcli/commands/local/start_lambda/cli.py b/samcli/commands/local/start_lambda/cli.py index 6ff7af27fd..f4974fad77 100644 --- a/samcli/commands/local/start_lambda/cli.py +++ b/samcli/commands/local/start_lambda/cli.py @@ -100,6 +100,8 @@ def cli( warm_containers, shutdown, debug_function, + container_host, + container_host_interface, cdk_context, project_type, cdk_app, @@ -130,6 +132,8 @@ def cli( warm_containers, shutdown, debug_function, + container_host, + container_host_interface, iac, project, ) # pragma: no cover @@ -155,6 +159,8 @@ def do_cli( # pylint: disable=R0914 warm_containers, shutdown, debug_function, + container_host, + container_host_interface, iac: IacPlugin, project: Project, ): @@ -196,6 +202,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, iac=iac, project=project, ) as invoke_context: diff --git a/samcli/local/docker/container.py b/samcli/local/docker/container.py index fbaf237224..a43e485513 100644 --- a/samcli/local/docker/container.py +++ b/samcli/local/docker/container.py @@ -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 @@ -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. @@ -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 @@ -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: @@ -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: @@ -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), ) diff --git a/samcli/local/docker/lambda_container.py b/samcli/local/docker/lambda_container.py index 87d79f1b44..98f519729b 100644 --- a/samcli/local/docker/lambda_container.py +++ b/samcli/local/docker/lambda_container.py @@ -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 @@ -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)) @@ -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 diff --git a/samcli/local/lambdafn/runtime.py b/samcli/local/lambdafn/runtime.py index 759d02fd79..d898cf1626 100644 --- a/samcli/local/lambdafn/runtime.py +++ b/samcli/local/lambdafn/runtime.py @@ -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, container_host_interface=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 @@ -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 ------- @@ -78,6 +80,8 @@ 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, + container_host_interface=container_host_interface, ) try: # create the container. @@ -88,7 +92,7 @@ def create(self, function_config, debug_context=None): LOG.debug("Ctrl+C was pressed. Aborting container creation") raise - def run(self, container, function_config, debug_context): + def run(self, container, function_config, debug_context, container_host=None, container_host_interface=None): """ Find the created container for the passed Lambda function, then using the ContainerManager run this container. @@ -102,6 +106,11 @@ def run(self, container, function_config, debug_context): Configuration of the function to run its created container. debug_context DebugContext Debugging context for the function (includes port, args, and path) + container_host string + Host of locally emulated Lambda container + container_host_interface string + Optional. Interface that Docker host binds ports to + Returns ------- Container @@ -109,7 +118,7 @@ def run(self, container, function_config, debug_context): """ if not container: - container = self.create(function_config, debug_context) + container = self.create(function_config, debug_context, container_host, container_host_interface) if container.is_running(): LOG.info("Lambda function '%s' is already running", function_config.name) @@ -132,6 +141,8 @@ def invoke( debug_context=None, stdout: Optional[StreamWriter] = None, stderr: Optional[StreamWriter] = None, + container_host=None, + container_host_interface=None, ): """ Invoke the given Lambda function locally. @@ -150,13 +161,17 @@ 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 + :param string container_host_interface: Optional. + Interface that Docker host binds ports to :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_host_interface) container = self.run(container, function_config, debug_context) # Setup appropriate interrupt - timeout or Ctrl+C - before function starts executing. # @@ -301,7 +316,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, container_host_interface=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 @@ -313,6 +328,10 @@ 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 + container_host_interface string + Interface that Docker host binds ports to Returns ------- @@ -336,7 +355,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, container_host_interface) self._containers[function_config.name] = container self._observer.watch(function_config) diff --git a/tests/unit/commands/local/cli_common/test_invoke_context.py b/tests/unit/commands/local/cli_common/test_invoke_context.py index 770aed4b18..88af92b553 100644 --- a/tests/unit/commands/local/cli_common/test_invoke_context.py +++ b/tests/unit/commands/local/cli_common/test_invoke_context.py @@ -571,6 +571,8 @@ def test_must_create_runner( env_vars_values=ANY, aws_profile="profile", aws_region="region", + container_host=None, + container_host_interface=None, ) result = self.context.local_lambda_runner @@ -644,6 +646,82 @@ def test_must_create_runner_using_warm_containers( env_vars_values=ANY, aws_profile="profile", aws_region="region", + container_host=None, + container_host_interface=None, + ) + + result = self.context.local_lambda_runner + self.assertEqual(result, runner_mock) + # assert that lambda runner is created only one time, and the cached version used in the second call + self.assertEqual(LocalLambdaMock.call_count, 1) + + @patch("samcli.commands.local.cli_common.invoke_context.LambdaImage") + @patch("samcli.commands.local.cli_common.invoke_context.LayerDownloader") + @patch("samcli.commands.local.cli_common.invoke_context.LambdaRuntime") + @patch("samcli.commands.local.cli_common.invoke_context.LocalLambdaRunner") + @patch("samcli.commands.local.cli_common.invoke_context.SamFunctionProvider") + def test_must_create_runner_with_container_host_option( + self, SamFunctionProviderMock, LocalLambdaMock, LambdaRuntimeMock, download_layers_mock, lambda_image_patch + ): + runtime_mock = Mock() + LambdaRuntimeMock.return_value = runtime_mock + + runner_mock = Mock() + LocalLambdaMock.return_value = runner_mock + + download_mock = Mock() + download_layers_mock.return_value = download_mock + + image_mock = Mock() + lambda_image_patch.return_value = image_mock + + cwd = "cwd" + self.context = InvokeContext( + template_file="template_file", + function_identifier="id", + env_vars_file="env_vars_file", + docker_volume_basedir="volumedir", + docker_network="network", + log_file="log_file", + skip_pull_image=True, + force_image_build=True, + debug_ports=[1111], + debugger_path="path-to-debugger", + debug_args="args", + aws_profile="profile", + aws_region="region", + container_host="abcdef", + container_host_interface="192.168.100.101", + ) + self.context.get_cwd = Mock() + self.context.get_cwd.return_value = cwd + + self.context._get_stacks = Mock() + self.context._get_stacks.return_value = [Mock()] + self.context._get_env_vars_value = Mock() + self.context._setup_log_file = Mock() + self.context._get_debug_context = Mock(return_value=None) + + container_manager_mock = Mock() + container_manager_mock.is_docker_reachable = PropertyMock(return_value=True) + self.context._get_container_manager = Mock(return_value=container_manager_mock) + + with self.context: + result = self.context.local_lambda_runner + self.assertEqual(result, runner_mock) + + LambdaRuntimeMock.assert_called_with(container_manager_mock, image_mock) + lambda_image_patch.assert_called_once_with(download_mock, True, True) + LocalLambdaMock.assert_called_with( + local_runtime=runtime_mock, + function_provider=ANY, + cwd=cwd, + debug_context=None, + env_vars_values=ANY, + aws_profile="profile", + aws_region="region", + container_host="abcdef", + container_host_interface="192.168.100.101", ) result = self.context.local_lambda_runner diff --git a/tests/unit/commands/local/invoke/test_cli.py b/tests/unit/commands/local/invoke/test_cli.py index e450d68033..0352b88fe9 100644 --- a/tests/unit/commands/local/invoke/test_cli.py +++ b/tests/unit/commands/local/invoke/test_cli.py @@ -41,6 +41,8 @@ def setUp(self): self.shutdown = False self.region_name = "region" self.profile = "profile" + self.container_host = "localhost" + self.container_host_interface = "127.0.0.1" @patch("samcli.commands.local.cli_common.invoke_context.InvokeContext") @patch("samcli.commands.local.invoke.cli._get_event") @@ -77,6 +79,8 @@ def test_cli_must_setup_context_and_invoke(self, get_event_mock, InvokeContextMo layer_cache_basedir=self.layer_cache_basedir, force_image_build=self.force_image_build, shutdown=self.shutdown, + container_host=self.container_host, + container_host_interface=self.container_host_interface, project_type="CFN", iac=iac_mock, project=project_mock, @@ -100,6 +104,8 @@ def test_cli_must_setup_context_and_invoke(self, get_event_mock, InvokeContextMo shutdown=self.shutdown, aws_region=self.region_name, aws_profile=self.profile, + container_host=self.container_host, + container_host_interface=self.container_host_interface, iac=iac_mock, project=project_mock, ) @@ -142,6 +148,8 @@ def test_cli_must_invoke_with_no_event(self, get_event_mock, InvokeContextMock): layer_cache_basedir=self.layer_cache_basedir, force_image_build=self.force_image_build, shutdown=self.shutdown, + container_host=self.container_host, + container_host_interface=self.container_host_interface, project_type="CFN", iac=iac_mock, project=project_mock, @@ -165,6 +173,8 @@ def test_cli_must_invoke_with_no_event(self, get_event_mock, InvokeContextMock): shutdown=self.shutdown, aws_region=self.region_name, aws_profile=self.profile, + container_host=self.container_host, + container_host_interface=self.container_host_interface, iac=iac_mock, project=project_mock, ) @@ -219,6 +229,8 @@ def test_must_raise_user_exception_on_function_not_found( layer_cache_basedir=self.layer_cache_basedir, force_image_build=self.force_image_build, shutdown=self.shutdown, + container_host=self.container_host, + container_host_interface=self.container_host_interface, project_type="CFN", iac=Mock(), project=Mock(), @@ -274,6 +286,8 @@ def test_must_raise_user_exception_on_function_local_invoke_image_not_found_for_ layer_cache_basedir=self.layer_cache_basedir, force_image_build=self.force_image_build, shutdown=self.shutdown, + container_host=self.container_host, + container_host_interface=self.container_host_interface, project_type="CFN", iac=Mock(), project=Mock(), @@ -327,6 +341,8 @@ def test_must_raise_user_exception_on_invalid_sam_template( layer_cache_basedir=self.layer_cache_basedir, force_image_build=self.force_image_build, shutdown=self.shutdown, + container_host=self.container_host, + container_host_interface=self.container_host_interface, project_type="CFN", iac=Mock(), project=Mock(), @@ -368,6 +384,8 @@ def test_must_raise_user_exception_on_invalid_env_vars(self, get_event_mock, Inv layer_cache_basedir=self.layer_cache_basedir, force_image_build=self.force_image_build, shutdown=self.shutdown, + container_host=self.container_host, + container_host_interface=self.container_host_interface, project_type="CFN", iac=Mock(), project=Mock(), @@ -423,6 +441,8 @@ def test_must_raise_user_exception_on_function_no_free_ports( layer_cache_basedir=self.layer_cache_basedir, force_image_build=self.force_image_build, shutdown=self.shutdown, + container_host=self.container_host, + container_host_interface=self.container_host_interface, project_type="CFN", iac=Mock(), project=Mock(), diff --git a/tests/unit/commands/local/lib/test_local_lambda.py b/tests/unit/commands/local/lib/test_local_lambda.py index 83486b72ae..c5d9a2aee7 100644 --- a/tests/unit/commands/local/lib/test_local_lambda.py +++ b/tests/unit/commands/local/lib/test_local_lambda.py @@ -523,7 +523,13 @@ def test_must_work(self): self.local_lambda.invoke(name, event, stdout, stderr) self.runtime_mock.invoke.assert_called_with( - invoke_config, event, debug_context=None, stdout=stdout, stderr=stderr + invoke_config, + event, + debug_context=None, + stdout=stdout, + stderr=stderr, + container_host=None, + container_host_interface=None, ) def test_must_work_packagetype_ZIP(self): @@ -541,7 +547,13 @@ def test_must_work_packagetype_ZIP(self): self.local_lambda.invoke(name, event, stdout, stderr) self.runtime_mock.invoke.assert_called_with( - invoke_config, event, debug_context=None, stdout=stdout, stderr=stderr + invoke_config, + event, + debug_context=None, + stdout=stdout, + stderr=stderr, + container_host=None, + container_host_interface=None, ) def test_must_raise_if_no_privilege(self): @@ -614,7 +626,13 @@ def test_works_if_imageuri_and_Image_packagetype(self): self.local_lambda.get_invoke_config.return_value = invoke_config self.local_lambda.invoke(name, event, stdout, stderr) self.runtime_mock.invoke.assert_called_with( - invoke_config, event, debug_context=None, stdout=stdout, stderr=stderr + invoke_config, + event, + debug_context=None, + stdout=stdout, + stderr=stderr, + container_host=None, + container_host_interface=None, ) def test_must_raise_if_imageuri_not_found(self): @@ -630,6 +648,53 @@ def test_must_raise_if_imageuri_not_found(self): self.local_lambda.invoke(name, event, stdout, stderr) +class TestLocalLambda_invoke_with_container_host_option(TestCase): + def setUp(self): + self.runtime_mock = Mock() + self.function_provider_mock = Mock() + self.cwd = "/my/current/working/directory" + self.debug_context = None + self.aws_profile = "myprofile" + self.aws_region = "region" + self.env_vars_values = {} + self.container_host = "localhost" + self.container_host_interface = "127.0.0.1" + + self.local_lambda = LocalLambdaRunner( + self.runtime_mock, + self.function_provider_mock, + self.cwd, + env_vars_values=self.env_vars_values, + debug_context=self.debug_context, + container_host=self.container_host, + container_host_interface=self.container_host_interface, + ) + + def test_must_work(self): + name = "name" + event = "event" + stdout = "stdout" + stderr = "stderr" + function = Mock(functionname="name") + invoke_config = "config" + + self.function_provider_mock.get_all.return_value = [function] + self.local_lambda.get_invoke_config = Mock() + self.local_lambda.get_invoke_config.return_value = invoke_config + + self.local_lambda.invoke(name, event, stdout, stderr) + + self.runtime_mock.invoke.assert_called_with( + invoke_config, + event, + debug_context=None, + stdout=stdout, + stderr=stderr, + container_host="localhost", + container_host_interface="127.0.0.1", + ) + + class TestLocalLambda_is_debugging(TestCase): def setUp(self): self.runtime_mock = Mock() @@ -652,7 +717,6 @@ def test_must_be_on(self): self.assertTrue(self.local_lambda.is_debugging()) def test_must_be_off(self): - self.local_lambda = LocalLambdaRunner( self.runtime_mock, self.function_provider_mock, diff --git a/tests/unit/commands/local/start_api/test_cli.py b/tests/unit/commands/local/start_api/test_cli.py index 7df8ccb583..b8ec24ed2d 100644 --- a/tests/unit/commands/local/start_api/test_cli.py +++ b/tests/unit/commands/local/start_api/test_cli.py @@ -47,6 +47,9 @@ def setUp(self): self.port = 123 self.static_dir = "staticdir" + self.container_host = "localhost" + self.container_host_interface = "127.0.0.1" + self.iac = Mock() self.project = Mock() @@ -85,6 +88,8 @@ def test_cli_must_setup_context_and_start_service(self, local_api_service_mock, warm_container_initialization_mode=self.warm_containers, debug_function=self.debug_function, shutdown=self.shutdown, + container_host=self.container_host, + container_host_interface=self.container_host_interface, iac=self.iac, project=self.project, ) @@ -193,6 +198,8 @@ def call_cli(self): warm_containers=self.warm_containers, debug_function=self.debug_function, shutdown=self.shutdown, + container_host=self.container_host, + container_host_interface=self.container_host_interface, project_type="CFN", iac=self.iac, project=self.project, diff --git a/tests/unit/commands/local/start_lambda/test_cli.py b/tests/unit/commands/local/start_lambda/test_cli.py index 7a6ce366fc..ee45ff67d3 100644 --- a/tests/unit/commands/local/start_lambda/test_cli.py +++ b/tests/unit/commands/local/start_lambda/test_cli.py @@ -40,6 +40,9 @@ def setUp(self): self.host = "host" self.port = 123 + self.container_host = "localhost" + self.container_host_interface = "127.0.0.1" + self.iac = Mock() self.project = Mock() @@ -77,6 +80,8 @@ def test_cli_must_setup_context_and_start_service(self, local_lambda_service_moc warm_container_initialization_mode=self.warm_containers, debug_function=self.debug_function, shutdown=self.shutdown, + container_host=self.container_host, + container_host_interface=self.container_host_interface, iac=self.iac, project=self.project, ) @@ -162,6 +167,8 @@ def call_cli(self): warm_containers=self.warm_containers, debug_function=self.debug_function, shutdown=self.shutdown, + container_host=self.container_host, + container_host_interface=self.container_host_interface, iac=self.iac, project=self.project, ) diff --git a/tests/unit/commands/samconfig/test_samconfig.py b/tests/unit/commands/samconfig/test_samconfig.py index 4b27b01827..35777b7fc3 100644 --- a/tests/unit/commands/samconfig/test_samconfig.py +++ b/tests/unit/commands/samconfig/test_samconfig.py @@ -345,6 +345,8 @@ def test_local_invoke(self, get_iac_plugin_mock, do_cli_mock): True, True, {"Key": "Value", "Key2": "Value2"}, + "localhost", + "127.0.0.1", "CFN", iac_mock, project_mock, @@ -415,6 +417,8 @@ def test_local_start_api(self, get_iac_plugin_mock, do_cli_mock): None, False, None, + "localhost", + "127.0.0.1", "CFN", iac_mock, project_mock, @@ -483,6 +487,8 @@ def test_local_start_lambda(self, get_iac_plugin_mock, do_cli_mock): None, False, None, + "localhost", + "127.0.0.1", iac_mock, project_mock, ) @@ -928,6 +934,10 @@ def test_override_with_cli_params(self, get_iac_plugin_mock, do_cli_mock): "--shutdown", "--parameter-overrides", "A=123 C=D E=F12! G=H", + "--container-host", + "localhost", + "--container-host-interface", + "127.0.0.1", ], ) @@ -957,6 +967,8 @@ def test_override_with_cli_params(self, get_iac_plugin_mock, do_cli_mock): None, True, None, + "localhost", + "127.0.0.1", iac_mock, project_mock, ) @@ -1056,6 +1068,8 @@ def test_override_with_cli_params_and_envvars(self, get_iac_plugin_mock, do_cli_ None, False, None, + "localhost", + "127.0.0.1", iac_mock, project_mock, ) diff --git a/tests/unit/local/docker/test_container.py b/tests/unit/local/docker/test_container.py index a853bfd5bb..3c9b18b89f 100644 --- a/tests/unit/local/docker/test_container.py +++ b/tests/unit/local/docker/test_container.py @@ -65,6 +65,8 @@ def setUp(self): self.env_vars = {"key": "value"} self.container_opts = {"container": "opts"} self.additional_volumes = {"/somepath": {"blah": "blah value"}} + self.container_host = "localhost" + self.container_host_interface = "127.0.0.1" self.mock_docker_client = Mock() self.mock_docker_client.containers = Mock() @@ -138,6 +140,8 @@ def test_must_create_container_including_all_optional_values(self): docker_client=self.mock_docker_client, container_opts=self.container_opts, additional_volumes=self.additional_volumes, + container_host=self.container_host, + container_host_interface=self.container_host_interface, ) container_id = container.create() @@ -153,7 +157,7 @@ def test_must_create_container_including_all_optional_values(self): use_config_proxy=True, environment=self.env_vars, ports={ - container_port: ("127.0.0.1", host_port) + container_port: (self.container_host_interface, host_port) for container_port, host_port in {**self.exposed_ports, **self.always_exposed_ports}.items() }, entrypoint=self.entrypoint, @@ -513,12 +517,18 @@ def setUp(self): self.cmd = ["cmd"] self.working_dir = "working_dir" self.host_dir = "host_dir" + self.container_host = "localhost" self.mock_docker_client = Mock() self.mock_docker_client.containers = Mock() self.mock_docker_client.containers.get = Mock() self.container = Container( - self.image, self.cmd, self.working_dir, self.host_dir, docker_client=self.mock_docker_client + self.image, + self.cmd, + self.working_dir, + self.host_dir, + docker_client=self.mock_docker_client, + container_host=self.container_host, ) self.container.id = "someid" diff --git a/tests/unit/local/lambdafn/test_runtime.py b/tests/unit/local/lambdafn/test_runtime.py index d109039f8e..c4031a0142 100644 --- a/tests/unit/local/lambdafn/test_runtime.py +++ b/tests/unit/local/lambdafn/test_runtime.py @@ -81,6 +81,8 @@ def test_must_create_lambda_container(self, LambdaContainerMock): debug_options=debug_options, env_vars=self.env_var_value, memory_mb=self.DEFAULT_MEMORY, + container_host=None, + container_host_interface=None, ) # Run the container and get results self.manager_mock.create.assert_called_with(container) @@ -161,7 +163,7 @@ def test_must_create_container_first_if_passed_container_is_none(self): create_mock.return_value = container self.runtime.run(None, self.func_config, debug_context=debug_options) - create_mock.assert_called_with(self.func_config, debug_options) + create_mock.assert_called_with(self.func_config, debug_options, None, None) self.manager_mock.run.assert_called_with(container) def test_must_skip_run_running_container(self): @@ -269,6 +271,8 @@ def test_must_run_container_and_wait_for_result(self, LambdaContainerMock): debug_options=debug_options, env_vars=self.env_var_value, memory_mb=self.DEFAULT_MEMORY, + container_host=None, + container_host_interface=None, ) # Run the container and get results @@ -595,6 +599,8 @@ def test_must_run_container_then_wait_for_result_and_container_not_stopped( debug_options=debug_options, env_vars=self.env_var_value, memory_mb=self.DEFAULT_MEMORY, + container_host=None, + container_host_interface=None, ) # Run the container and get results @@ -672,6 +678,8 @@ def test_must_create_non_cached_container(self, LambdaContainerMock, LambdaFunct debug_options=debug_options, env_vars=self.env_var_value, memory_mb=self.DEFAULT_MEMORY, + container_host=None, + container_host_interface=None, ) self.manager_mock.create.assert_called_with(container) @@ -737,6 +745,8 @@ def test_must_ignore_debug_options_if_function_name_is_not_debug_function( debug_options=None, env_vars=self.env_var_value, memory_mb=self.DEFAULT_MEMORY, + container_host=None, + container_host_interface=None, ) self.manager_mock.create.assert_called_with(container) # validate that the created container got cached