Skip to content

Commit

Permalink
feat(LocalLambdaInvokeService): Command interface for `sam local star…
Browse files Browse the repository at this point in the history
…t-lambda` (aws#544)
  • Loading branch information
jfuss authored and sanathkr committed Jul 10, 2018
1 parent 23ffd03 commit 58f3fe5
Show file tree
Hide file tree
Showing 11 changed files with 321 additions and 33 deletions.
34 changes: 34 additions & 0 deletions samcli/commands/local/cli_common/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,40 @@ def template_click_option():
help="AWS SAM template file")


def service_common_options(port):
def construct_options(f):
"""
Common CLI Options that are shared for service related commands ('start-api' and 'start_lambda')
Parameters
----------
f function
Callback passed by Click
port int
port number to use
Returns
-------
function
The callback function
"""
service_options = [
click.option('--host',
default="127.0.0.1",
help="Local hostname or IP address to bind to (default: '127.0.0.1')"),
click.option("--port", "-p",
default=port,
help="Local port number to listen on (default: '{}')".format(str(port)))
]

# Reverse the list to maintain ordering of options in help text printed with --help
for option in reversed(service_options):
option(f)

return f
return construct_options


def invoke_common_options(f):
"""
Common CLI options shared by "local invoke" and "local start-api" commands
Expand Down
58 changes: 58 additions & 0 deletions samcli/commands/local/lib/local_lambda_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
Connects the CLI with Local Lambda Invoke Service.
"""
import logging

from samcli.local.lambda_service.local_lambda_invoke_service import LocalLambdaInvokeService

LOG = logging.getLogger(__name__)


class LocalLambdaService(object):
"""
Implementation of Local Lambda Invoke Service that is capable of serving the invoke path to your Lambda Functions
that are defined in a SAM file.
"""

def __init__(self,
lambda_invoke_context,
port,
host):
"""
Initialize the Local Lambda Invoke service.
:param samcli.commands.local.cli_common.invoke_context.InvokeContext lambda_invoke_context: Context object
that can help with Lambda invocation
:param int port: Port to listen on
:param string host: Local hostname or IP address to bind to
"""

self.port = port
self.host = host
self.lambda_runner = lambda_invoke_context.local_lambda_runner
self.stderr_stream = lambda_invoke_context.stderr

def start(self):
"""
Creates and starts the Local Lambda Invoke service. This method will block until the service is stopped
manually using an interrupt. After the service is started, callers can make HTTP requests to the endpoint
to invoke the Lambda function and receive a response.
NOTE: This is a blocking call that will not return until the thread is interrupted with SIGINT/SIGTERM
"""

# We care about passing only stderr to the Service and not stdout because stdout from Docker container
# contains the response to the API which is sent out as HTTP response. Only stderr needs to be printed
# to the console or a log file. stderr from Docker container contains runtime logs and output of print
# statements from the Lambda function
service = LocalLambdaInvokeService(lambda_runner=self.lambda_runner,
port=self.port,
host=self.host,
stderr=self.stderr_stream)

service.create()

LOG.info("Starting the Local Lambda Service. You can now invoke your Lambda Functions defined in your template"
"through the endpoint.")

service.run()
2 changes: 2 additions & 0 deletions samcli/commands/local/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .invoke.cli import cli as invoke_cli
from .start_api.cli import cli as start_api_cli
from .generate_event.cli import cli as generate_event_cli
from .start_lambda.cli import cli as start_lambda_cli


@click.group()
Expand All @@ -22,3 +23,4 @@ def cli():
cli.add_command(invoke_cli)
cli.add_command(start_api_cli)
cli.add_command(generate_event_cli)
cli.add_command(start_lambda_cli)
12 changes: 3 additions & 9 deletions samcli/commands/local/start_api/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import click

from samcli.cli.main import pass_context, common_options as cli_framework_options
from samcli.commands.local.cli_common.options import invoke_common_options
from samcli.commands.local.cli_common.options import invoke_common_options, service_common_options
from samcli.commands.local.cli_common.invoke_context import InvokeContext
from samcli.commands.local.lib.exceptions import NoApisDefined
from samcli.commands.local.cli_common.user_exceptions import UserException
Expand All @@ -29,12 +29,7 @@


@click.command("start-api", help=HELP_TEXT, short_help="Runs your APIs locally")
@click.option("--host",
default="127.0.0.1",
help="Local hostname or IP address to bind to (default: '127.0.0.1')")
@click.option("--port", "-p",
default=3000,
help="Local port number to listen on (default: '3000')")
@service_common_options(3000)
@click.option("--static-dir", "-s",
default="public",
help="Any static assets (e.g. CSS/Javascript/HTML) files located in this directory "
Expand All @@ -48,8 +43,7 @@ def cli(ctx,

# Common Options for Lambda Invoke
template, env_vars, debug_port, debug_args, docker_volume_basedir,
docker_network, log_file, skip_pull_image, profile
):
docker_network, log_file, skip_pull_image, profile):
# All logic must be implemented in the ``do_cli`` method. This helps with easy unit testing

do_cli(ctx, host, port, static_dir, template, env_vars, debug_port, debug_args, docker_volume_basedir,
Expand Down
Empty file.
79 changes: 79 additions & 0 deletions samcli/commands/local/start_lambda/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""
CLI command for "local start-lambda" command
"""

import logging
import click

from samcli.cli.main import pass_context, common_options as cli_framework_options
from samcli.commands.local.cli_common.options import invoke_common_options, service_common_options
from samcli.commands.local.cli_common.invoke_context import InvokeContext
from samcli.commands.local.cli_common.user_exceptions import UserException
from samcli.commands.local.lib.local_lambda_service import LocalLambdaService
from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException

LOG = logging.getLogger(__name__)

HELP_TEXT = """
Allows you to run a Local Lambda Service that will service the invoke path to your functions for quick development &
testing through the AWS CLI or SDKs. When run in a directory that contains your Serverless functions and your AWS
SAM template, it will create a local HTTP server that wil response to the invoke call to your functions.
When accessed (via browser, cli etc), it will launch a Docker container locally to invoke the function. It will read
the CodeUri property of AWS::Serverless::Function resource to find the path in your file system containing the Lambda
Function code. This could be the project's root directory for interpreted languages like Node & Python, or a build
directory that stores your compiled artifacts or a JAR file. If you are using a interpreted language, local changes
will be available immediately in Docker container on every invoke. For more compiled languages or projects requiring
complex packing support, we recommended you run your own building solution and point SAM to the directory or file
containing build artifacts.
"""


@click.command("start-lambda", help=HELP_TEXT, short_help="Runs a Local Lambda Service (for the Invoke path only)")
@service_common_options(3001)
@invoke_common_options
@cli_framework_options
@pass_context
def cli(ctx,
# start-lambda Specific Options
host, port,

# Common Options for Lambda Invoke
template, env_vars, debug_port, debug_args, docker_volume_basedir,
docker_network, log_file, skip_pull_image, profile
):
# All logic must be implemented in the ``do_cli`` method. This helps with easy unit testing

do_cli(ctx, host, port, template, env_vars, debug_port, debug_args, docker_volume_basedir,
docker_network, log_file, skip_pull_image, profile) # pragma: no cover


def do_cli(ctx, host, port, template, env_vars, debug_port, debug_args, # pylint: disable=R0914
docker_volume_basedir, docker_network, log_file, skip_pull_image, profile):
"""
Implementation of the ``cli`` method, just separated out for unit testing purposes
"""

LOG.debug("local start_lambda command is called")

# Pass all inputs to setup necessary context to invoke function locally.
# Handler exception raised by the processor for invalid args and print errors

try:
with InvokeContext(template_file=template,
function_identifier=None, # Don't scope to one particular function
env_vars_file=env_vars,
debug_port=debug_port,
debug_args=debug_args,
docker_volume_basedir=docker_volume_basedir,
docker_network=docker_network,
log_file=log_file,
skip_pull_image=skip_pull_image,
aws_profile=profile) as invoke_context:

service = LocalLambdaService(lambda_invoke_context=invoke_context,
port=port,
host=host)
service.start()

except InvalidSamDocumentException as ex:
raise UserException(str(ex))
45 changes: 45 additions & 0 deletions tests/unit/commands/local/lib/test_local_lambda_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from unittest import TestCase
from mock import Mock, patch

from samcli.commands.local.lib.local_lambda_service import LocalLambdaService


class TestLocalLambdaService(TestCase):

def test_initialization(self):
lambda_runner_mock = Mock()
stderr_mock = Mock()
lambda_invoke_context_mock = Mock()

lambda_invoke_context_mock.local_lambda_runner = lambda_runner_mock
lambda_invoke_context_mock.stderr = stderr_mock

service = LocalLambdaService(lambda_invoke_context=lambda_invoke_context_mock, port=3000, host='localhost')

self.assertEquals(service.port, 3000)
self.assertEquals(service.host, 'localhost')
self.assertEquals(service.lambda_runner, lambda_runner_mock)
self.assertEquals(service.stderr_stream, stderr_mock)

@patch('samcli.commands.local.lib.local_lambda_service.LocalLambdaInvokeService')
def test_start(self, local_lambda_invoke_service_mock):
lambda_runner_mock = Mock()
stderr_mock = Mock()
lambda_invoke_context_mock = Mock()

lambda_context_mock = Mock()
local_lambda_invoke_service_mock.return_value = lambda_context_mock

lambda_invoke_context_mock.local_lambda_runner = lambda_runner_mock
lambda_invoke_context_mock.stderr = stderr_mock

service = LocalLambdaService(lambda_invoke_context=lambda_invoke_context_mock, port=3000, host='localhost')

service.start()

local_lambda_invoke_service_mock.assert_called_once_with(lambda_runner=lambda_runner_mock,
port=3000,
host='localhost',
stderr=stderr_mock)
lambda_context_mock.create.assert_called_once()
lambda_context_mock.run.assert_called_once()
Empty file.
47 changes: 23 additions & 24 deletions tests/unit/commands/local/start_api/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,45 +30,45 @@ def setUp(self):

@patch("samcli.commands.local.start_api.cli.InvokeContext")
@patch("samcli.commands.local.start_api.cli.LocalApiService")
def test_cli_must_setup_context_and_start_service(self, LocalApiServiceMock, InvokeContextMock):
def test_cli_must_setup_context_and_start_service(self, local_api_service_mock, invoke_context_mock):

# Mock the __enter__ method to return a object inside a context manager
context_mock = Mock()
InvokeContextMock.return_value.__enter__.return_value = context_mock
invoke_context_mock.return_value.__enter__.return_value = context_mock

service_mock = Mock()
LocalApiServiceMock.return_value = service_mock
local_api_service_mock.return_value = service_mock

self.call_cli()

InvokeContextMock.assert_called_with(template_file=self.template,
function_identifier=None,
env_vars_file=self.env_vars,
debug_port=self.debug_port,
debug_args=self.debug_args,
docker_volume_basedir=self.docker_volume_basedir,
docker_network=self.docker_network,
log_file=self.log_file,
skip_pull_image=self.skip_pull_image,
aws_profile=self.profile)

LocalApiServiceMock.assert_called_with(lambda_invoke_context=context_mock,
port=self.port,
host=self.host,
static_dir=self.static_dir)
invoke_context_mock.assert_called_with(template_file=self.template,
function_identifier=None,
env_vars_file=self.env_vars,
debug_port=self.debug_port,
debug_args=self.debug_args,
docker_volume_basedir=self.docker_volume_basedir,
docker_network=self.docker_network,
log_file=self.log_file,
skip_pull_image=self.skip_pull_image,
aws_profile=self.profile)

local_api_service_mock.assert_called_with(lambda_invoke_context=context_mock,
port=self.port,
host=self.host,
static_dir=self.static_dir)

service_mock.start.assert_called_with()

@patch("samcli.commands.local.start_api.cli.InvokeContext")
@patch("samcli.commands.local.start_api.cli.LocalApiService")
def test_must_raise_if_no_api_defined(self, LocalApiServiceMock, InvokeContextMock):
def test_must_raise_if_no_api_defined(self, local_api_service_mock, invoke_context_mock):

# Mock the __enter__ method to return a object inside a context manager
context_mock = Mock()
InvokeContextMock.return_value.__enter__.return_value = context_mock
invoke_context_mock.return_value.__enter__.return_value = context_mock

service_mock = Mock()
LocalApiServiceMock.return_value = service_mock
local_api_service_mock.return_value = service_mock
service_mock.start.side_effect = NoApisDefined("no apis")

with self.assertRaises(UserException) as context:
Expand All @@ -79,10 +79,9 @@ def test_must_raise_if_no_api_defined(self, LocalApiServiceMock, InvokeContextMo
self.assertEquals(msg, expected)

@patch("samcli.commands.local.start_api.cli.InvokeContext")
@patch("samcli.commands.local.start_api.cli.LocalApiService")
def test_must_raise_user_exception_on_invalid_sam_template(self, LocalApiServiceMock, InvokeContextMock):
def test_must_raise_user_exception_on_invalid_sam_template(self, invoke_context_mock):

InvokeContextMock.side_effect = InvalidSamDocumentException("bad template")
invoke_context_mock.side_effect = InvalidSamDocumentException("bad template")

with self.assertRaises(UserException) as context:
self.call_cli()
Expand Down
Empty file.
Loading

0 comments on commit 58f3fe5

Please sign in to comment.