def test_function_result_is_available_in_stdout_and_logs_in_stderr(self): # This is the JSON result from Lambda function # Convert to proper binary type to be compatible with Python 2 & 3 expected_output = b'{"a":"b"}' expected_stderr = b"**This string is printed from Lambda function**" layer_downloader = LayerDownloader("./", "./") image_builder = LambdaImage(layer_downloader, False, False) container = LambdaContainer(self.runtime, self.handler, self.code_dir, self.layers, image_builder) stdout_stream = io.BytesIO() stderr_stream = io.BytesIO() stdout_stream_writer = StreamWriter(stdout_stream) stderr_stream_writer = StreamWriter(stderr_stream) with self._create(container): container.start() container.wait_for_logs(stdout=stdout_stream_writer, stderr=stderr_stream_writer) function_output = stdout_stream.getvalue() function_stderr = stderr_stream.getvalue() self.assertEqual(function_output.strip(), expected_output) self.assertIn(expected_stderr, function_stderr)
def test_must_configure_container_properly(self, get_entry_point_mock, get_exposed_ports_mock, get_image_mock): image = "image" ports = {"a": "b"} entry = [1, 2, 3] expected_cmd = [self.handler] get_image_mock.return_value = image get_exposed_ports_mock.return_value = ports get_entry_point_mock.return_value = entry container = LambdaContainer(self.runtime, self.handler, self.code_dir, env_vars=self.env_var, memory_mb=self.memory_mb, debug_port=self.debug_port, debug_args=self.debug_arg) self.assertEquals(image, container._image) self.assertEquals(expected_cmd, container._cmd) self.assertEquals("/var/task", container._working_dir) self.assertEquals(self.code_dir, container._host_dir) self.assertEquals(ports, container._exposed_ports) self.assertEquals(entry, container._entrypoint) self.assertEquals(self.env_var, container._env_vars) self.assertEquals(self.memory_mb, container._memory_limit_mb) get_image_mock.assert_called_with(self.runtime) get_exposed_ports_mock.assert_called_with(self.debug_port) get_entry_point_mock.assert_called_with(self.runtime, self.debug_port, self.debug_arg)
def test_debug_port_is_created_on_host(self): layer_downloader = LayerDownloader("./", "./") image_builder = LambdaImage(layer_downloader, False) container = LambdaContainer(self.runtime, self.handler, self.code_dir, self.layers, image_builder, debug_options=self.debug_context) with self._create(container): container.start() # After container is started, query the container to make sure it is bound to the right ports port_binding = self.docker_client.api.port(container.id, self.debug_port) self.assertIsNotNone( port_binding, "Container must be bound to a port on host machine") self.assertEquals(1, len(port_binding), "Only one port must be bound to the container") self.assertEquals(port_binding[0]["HostPort"], str(self.debug_port))
def test_must_configure_container_properly_image_no_debug( self, get_debug_settings_mock, get_additional_volumes_mock, get_additional_options_mock, get_exposed_ports_mock, get_image_mock, get_config_mock, ): self.packagetype = IMAGE self.imageuri = "mylambda_image:v1" self.runtime = None image = IMAGE ports = {"a": "b"} addtl_options = {} addtl_volumes = {} expected_cmd = ["mycommand"] get_image_mock.return_value = image get_debug_settings_mock.return_value = (LambdaContainer._DEFAULT_ENTRYPOINT, {}) get_config_mock.return_value = { "Cmd": ["mycommand"], "Entrypoint": ["my-additional-entrypoint"], "WorkingDir": "/var/mytask", } get_exposed_ports_mock.return_value = ports get_additional_options_mock.return_value = addtl_options get_additional_volumes_mock.return_value = addtl_volumes expected_env_vars = {**self.env_var} image_builder_mock = Mock() container = LambdaContainer( image_config=self.image_config, imageuri=self.imageuri, packagetype=self.packagetype, runtime=self.runtime, handler=self.handler, code_dir=self.code_dir, layers=[], lambda_image=image_builder_mock, env_vars=self.env_var, memory_mb=self.memory_mb, debug_options=self.debug_options, ) self.assertEqual(image, container._image) self.assertEqual(expected_cmd, container._cmd) self.assertEqual(get_config_mock()["WorkingDir"], container._working_dir) self.assertEqual(self.code_dir, container._host_dir) self.assertEqual(ports, container._exposed_ports) self.assertEqual(LambdaContainer._DEFAULT_ENTRYPOINT + get_config_mock()["Entrypoint"], container._entrypoint) self.assertEqual({**expected_env_vars, **{"AWS_LAMBDA_FUNCTION_HANDLER": "mycommand"}}, container._env_vars) self.assertEqual(self.memory_mb, container._memory_limit_mb) get_image_mock.assert_called_with(image_builder_mock, self.runtime, self.packagetype, self.imageuri, []) get_exposed_ports_mock.assert_called_with(self.debug_options) get_additional_options_mock.assert_called_with(self.runtime, self.debug_options) get_additional_volumes_mock.assert_called_with(self.runtime, self.debug_options)
def test_must_fail_for_unsupported_runtime(self): runtime = "foo" with self.assertRaises(ValueError) as context: LambdaContainer(runtime, self.handler, self.code_dir) self.assertEquals(str(context.exception), "Unsupported Lambda runtime foo")
def test_must_configure_container_properly_zip( self, get_additional_volumes_mock, get_additional_options_mock, get_debug_settings_mock, get_exposed_ports_mock, get_image_mock, ): image = IMAGE ports = {"a": "b"} addtl_options = {} addtl_volumes = {} debug_settings = ([1, 2, 3], {"a": "b"}) expected_cmd = [] get_image_mock.return_value = image get_exposed_ports_mock.return_value = ports get_debug_settings_mock.return_value = debug_settings get_additional_options_mock.return_value = addtl_options get_additional_volumes_mock.return_value = addtl_volumes expected_env_vars = {**self.env_var, **debug_settings[1]} image_builder_mock = Mock() container = LambdaContainer( image_config=self.image_config, imageuri=self.imageuri, packagetype=self.packagetype, runtime=self.runtime, handler=self.handler, code_dir=self.code_dir, layers=[], lambda_image=image_builder_mock, env_vars=self.env_var, memory_mb=self.memory_mb, debug_options=self.debug_options, ) self.assertEqual(image, container._image) self.assertEqual(expected_cmd, container._cmd) self.assertEqual("/var/task", container._working_dir) self.assertEqual(self.code_dir, container._host_dir) self.assertEqual(ports, container._exposed_ports) self.assertEqual(debug_settings[0], container._entrypoint) self.assertEqual(expected_env_vars, container._env_vars) self.assertEqual(self.memory_mb, container._memory_limit_mb) get_image_mock.assert_called_with(image_builder_mock, self.runtime, self.packagetype, self.imageuri, [], self.debug_options) get_exposed_ports_mock.assert_called_with(self.debug_options) get_debug_settings_mock.assert_called_with(self.runtime, self.debug_options) get_additional_options_mock.assert_called_with(self.runtime, self.debug_options) get_additional_volumes_mock.assert_called_with(self.runtime, self.debug_options)
def test_must_fail_for_unsupported_runtime(self): runtime = "foo" image_builder_mock = Mock() with self.assertRaises(ValueError) as context: LambdaContainer(runtime, self.handler, self.code_dir, [], image_builder_mock) self.assertEqual(str(context.exception), "Unsupported Lambda runtime foo")
def test_must_configure_container_properly( self, get_additional_volumes_mock, get_additional_options_mock, get_entry_point_mock, get_exposed_ports_mock, get_image_mock, ): image = "image" ports = {"a": "b"} addtl_options = {} addtl_volumes = {} entry = [1, 2, 3] expected_cmd = [self.handler] get_image_mock.return_value = image get_exposed_ports_mock.return_value = ports get_entry_point_mock.return_value = entry get_additional_options_mock.return_value = addtl_options get_additional_volumes_mock.return_value = addtl_volumes image_builder_mock = Mock() container = LambdaContainer( self.runtime, self.handler, self.code_dir, layers=[], image_builder=image_builder_mock, env_vars=self.env_var, memory_mb=self.memory_mb, debug_options=self.debug_options, ) self.assertEqual(image, container._image) self.assertEqual(expected_cmd, container._cmd) self.assertEqual("/var/task", container._working_dir) self.assertEqual(self.code_dir, container._host_dir) self.assertEqual(ports, container._exposed_ports) self.assertEqual(entry, container._entrypoint) self.assertEqual(self.env_var, container._env_vars) self.assertEqual(self.memory_mb, container._memory_limit_mb) get_image_mock.assert_called_with(image_builder_mock, self.runtime, []) get_exposed_ports_mock.assert_called_with(self.debug_options) get_entry_point_mock.assert_called_with(self.runtime, self.debug_options) get_additional_options_mock.assert_called_with(self.runtime, self.debug_options) get_additional_volumes_mock.assert_called_with(self.debug_options)
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 if the function_config.name equals debug_context.debug-function or the warm_containers option is disabled Parameters ---------- function_config FunctionConfig 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 ------- Container the created container """ # Generate a dictionary of environment variable key:values env_vars = function_config.env_vars.resolve() code_dir = self._get_code_dir(function_config.code_abs_path) container = LambdaContainer( function_config.runtime, function_config.imageuri, function_config.handler, function_config.packagetype, function_config.imageconfig, code_dir, function_config.layers, self._image_builder, 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. self._container_manager.create(container) return container except KeyboardInterrupt: LOG.debug("Ctrl+C was pressed. Aborting container creation") raise
def test_container_is_attached_to_network(self): container = LambdaContainer(self.runtime, self.handler, self.code_dir) with self._network_create() as network: # Ask the container to attach to the network container.network_id = network.id with self._create(container): container.start() # Now that the container has been created, it would be connected to the network # Fetch the latest information about this network from server network.reload() self.assertEquals(1, len(network.containers)) self.assertEquals(container.id, network.containers[0].id)
def test_basic_creation(self): """ A docker container must be successfully created """ layer_downloader = LayerDownloader("./", "./") image_builder = LambdaImage(layer_downloader, False, False) container = LambdaContainer(self.runtime, self.handler, self.code_dir, self.layers, image_builder) self.assertIsNone(container.id, "Container must not have ID before creation") # Create the container and verify its properties with self._create(container): self.assertIsNotNone(container.id, "Container must have an ID") # Call Docker API to make sure container indeed exists actual_container = self.docker_client.containers.get(container.id) self.assertEquals(actual_container.status, "created") self.assertTrue(self.expected_docker_image in actual_container.image.tags, "Image name of the container must be " + self.expected_docker_image)
def test_must_fail_for_unsupported_runtime(self): runtime = "foo" image_builder_mock = Mock() with self.assertRaises(ValueError) as context: LambdaContainer( runtime=runtime, imageuri=self.imageuri, handler=self.handler, packagetype=self.packagetype, image_config=self.image_config, code_dir=self.code_dir, layers=[], lambda_image=image_builder_mock, ) self.assertEqual(str(context.exception), "Unsupported Lambda runtime foo")
def test_function_result_is_available_in_stdout_and_logs_in_stderr(self): # This is the JSON result from Lambda function # Convert to proper binary type to be compatible with Python 2 & 3 expected_output = six.binary_type('{"a":"b"}'.encode('utf-8')) expected_stderr = six.binary_type( "**This string is printed from Lambda function**".encode("utf-8")) container = LambdaContainer(self.runtime, self.handler, self.code_dir) stdout_stream = io.BytesIO() stderr_stream = io.BytesIO() with self._create(container): container.start() container.wait_for_logs(stdout=stdout_stream, stderr=stderr_stream) function_output = stdout_stream.getvalue() function_stderr = stderr_stream.getvalue() self.assertEquals(function_output.strip(), expected_output) self.assertIn(expected_stderr, function_stderr)
def invoke(self, function_config, event, debug_context=None, stdout=None, stderr=None): """ Invoke the given Lambda function locally. ##### NOTE: THIS IS A LONG BLOCKING CALL ##### This method will block until either the Lambda function completes or timed out, which could be seconds. A blocking call will block the thread preventing any other operations from happening. If you are using this method in a web-server or in contexts where your application needs to be responsive when function is running, take care to invoke the function in a separate thread. Co-Routines or micro-threads might not perform well because the underlying implementation essentially blocks on a socket, which is synchronous. :param FunctionConfig function_config: Configuration of the function to invoke :param event: String input event passed to Lambda function :param DebugContext debug_context: Debugging context for the function (includes port, args, and path) :param io.IOBase stdout: Optional. IO Stream to that receives stdout text from container. :param io.IOBase stderr: Optional. IO Stream that receives stderr text from container :raises Keyboard """ timer = None # Update with event input environ = function_config.env_vars environ.add_lambda_event_body(event) # Generate a dictionary of environment variable key:values env_vars = environ.resolve() with self._get_code_dir(function_config.code_abs_path) as code_dir: container = LambdaContainer(function_config.runtime, function_config.handler, code_dir, function_config.layers, self._image_builder, memory_mb=function_config.memory, env_vars=env_vars, debug_options=debug_context) try: # Start the container. This call returns immediately after the container starts self._container_manager.run(container) # Setup appropriate interrupt - timeout or Ctrl+C - before function starts executing. # # Start the timer **after** container starts. Container startup takes several seconds, only after which, # our Lambda function code will run. Starting the timer is a reasonable approximation that function has # started running. timer = self._configure_interrupt(function_config.name, function_config.timeout, container, bool(debug_context)) # NOTE: BLOCKING METHOD # Block the thread waiting to fetch logs from the container. This method will return after container # terminates, either successfully or killed by one of the interrupt handlers above. container.wait_for_logs(stdout=stdout, stderr=stderr) except KeyboardInterrupt: # When user presses Ctrl+C, we receive a Keyboard Interrupt. This is especially very common when # container is in debugging mode. We have special handling of Ctrl+C. So handle KeyboardInterrupt # and swallow the exception. The ``finally`` block will also take care of cleaning it up. LOG.debug("Ctrl+C was pressed. Aborting Lambda execution") finally: # We will be done with execution, if either the execution completed or an interrupt was fired # Any case, cleanup the timer and container. # # If we are in debugging mode, timer would not be created. So skip cleanup of the timer if timer: timer.cancel() self._container_manager.stop(container)