Skip to content

Commit

Permalink
feat(build): Build a single function (#1146)
Browse files Browse the repository at this point in the history
* feat(build): Build a single function

* Remove prints and fix spelling mistakes
  • Loading branch information
jfuss authored and sriram-mv committed Apr 24, 2019
1 parent d813551 commit 4512794
Show file tree
Hide file tree
Showing 10 changed files with 245 additions and 34 deletions.
3 changes: 2 additions & 1 deletion designs/sam_build_cmd.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ Success criteria for the change
- Python with PIP
- Golang with Go CLI
- Dotnetcore with DotNet CLI
2. Each Lambda function in SAM template gets built
2. Each Lambda function in SAM template gets built by default unless a `function_identifier` (LogicalID) is passed
to the build command
3. Produce stable builds (best effort): If the source files did not
change, built artifacts should not change.
4. Built artifacts should \"just work\" with `sam local` and
Expand Down
22 changes: 22 additions & 0 deletions samcli/commands/build/build_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Context object used by build command
"""

import logging
import os
import shutil

Expand All @@ -14,6 +15,9 @@
from samcli.commands.local.lib.sam_function_provider import SamFunctionProvider
from samcli.commands._utils.template import get_template_data
from samcli.commands.exceptions import UserException
from samcli.local.lambdafn.exceptions import FunctionNotFound

LOG = logging.getLogger(__name__)


class BuildContext(object):
Expand All @@ -23,6 +27,7 @@ class BuildContext(object):
_BUILD_DIR_PERMISSIONS = 0o755

def __init__(self,
function_identifier,
template_file,
base_dir,
build_dir,
Expand All @@ -34,6 +39,7 @@ def __init__(self,
docker_network=None,
skip_pull_image=False):

self._function_identifier = function_identifier
self._template_file = template_file
self._base_dir = base_dir
self._build_dir = build_dir
Expand Down Expand Up @@ -128,3 +134,19 @@ def manifest_path_override(self):
@property
def mode(self):
return self._mode

@property
def functions_to_build(self):
if self._function_identifier:
function = self._function_provider.get(self._function_identifier)

if not function:
all_functions = [f.name for f in self._function_provider.get_all()]
available_function_message = "{} not found. Possible options in your template: {}" \
.format(self._function_identifier, all_functions)
LOG.info(available_function_message)
raise FunctionNotFound("Unable to find a Function with name '%s'", self._function_identifier)

return [function]

return self._function_provider.get_all()
27 changes: 17 additions & 10 deletions samcli/commands/build/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from samcli.lib.build.app_builder import ApplicationBuilder, BuildError, UnsupportedBuilderLibraryVersionError, \
ContainerBuildNotSupported
from samcli.lib.build.workflow_config import UnsupportedRuntimeException
from samcli.local.lambdafn.exceptions import FunctionNotFound
from samcli.commands._utils.template import move_template

LOG = logging.getLogger(__name__)
Expand Down Expand Up @@ -81,8 +82,10 @@
@docker_common_options
@cli_framework_options
@aws_creds_options
@click.argument('function_identifier', required=False)
@pass_context
def cli(ctx,
function_identifier,
template,
base_dir,
build_dir,
Expand All @@ -96,11 +99,12 @@ def cli(ctx,

mode = _get_mode_value_from_envvar("SAM_BUILD_MODE", choices=["debug"])

do_cli(template, base_dir, build_dir, True, use_container, manifest, docker_network,
do_cli(function_identifier, template, base_dir, build_dir, True, use_container, manifest, docker_network,
skip_pull_image, parameter_overrides, mode) # pragma: no cover


def do_cli(template, # pylint: disable=too-many-locals
def do_cli(function_identifier, # pylint: disable=too-many-locals
template,
base_dir,
build_dir,
clean,
Expand All @@ -119,7 +123,8 @@ def do_cli(template, # pylint: disable=too-many-locals
if use_container:
LOG.info("Starting Build inside a container")

with BuildContext(template,
with BuildContext(function_identifier,
template,
base_dir,
build_dir,
clean=clean,
Expand All @@ -129,14 +134,16 @@ def do_cli(template, # pylint: disable=too-many-locals
docker_network=docker_network,
skip_pull_image=skip_pull_image,
mode=mode) as ctx:
try:
builder = ApplicationBuilder(ctx.functions_to_build,
ctx.build_dir,
ctx.base_dir,
manifest_path_override=ctx.manifest_path_override,
container_manager=ctx.container_manager,
mode=ctx.mode)
except FunctionNotFound as ex:
raise UserException(str(ex))

builder = ApplicationBuilder(ctx.function_provider,
ctx.build_dir,
ctx.base_dir,
manifest_path_override=ctx.manifest_path_override,
container_manager=ctx.container_manager,
mode=ctx.mode
)
try:
artifacts = builder.build()
modified_template = builder.update_template(ctx.template_dict,
Expand Down
10 changes: 5 additions & 5 deletions samcli/lib/build/app_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class ApplicationBuilder(object):
"""

def __init__(self,
function_provider,
functions_to_build,
build_dir,
base_dir,
manifest_path_override=None,
Expand All @@ -61,8 +61,8 @@ def __init__(self,
Parameters
----------
function_provider : samcli.commands.local.lib.sam_function_provider.SamFunctionProvider
Provider that can vend out functions available in the SAM template
functions_to_build: Iterator
Iterator that can vend out functions available in the SAM template
build_dir : str
Path to the directory where we will be storing built artifacts
Expand All @@ -79,7 +79,7 @@ def __init__(self,
mode : str
Optional, name of the build mode to use ex: 'debug'
"""
self._function_provider = function_provider
self._functions_to_build = functions_to_build
self._build_dir = build_dir
self._base_dir = base_dir
self._manifest_path_override = manifest_path_override
Expand All @@ -100,7 +100,7 @@ def build(self):

result = {}

for lambda_function in self._function_provider.get_all():
for lambda_function in self._functions_to_build:

LOG.info("Building resource '%s'", lambda_function.name)
result[lambda_function.name] = self._build_function(lambda_function.name,
Expand Down
12 changes: 9 additions & 3 deletions tests/integration/buildcmd/build_integ_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@


class BuildIntegBase(TestCase):
template = "template.yaml"

@classmethod
def setUpClass(cls):
Expand All @@ -34,7 +35,7 @@ def setUpClass(cls):
cls.scratch_dir = str(Path(__file__).resolve().parent.joinpath("scratch"))

cls.test_data_path = str(Path(integration_dir, "testdata", "buildcmd"))
cls.template_path = str(Path(cls.test_data_path, "template.yaml"))
cls.template_path = str(Path(cls.test_data_path, cls.template))

def setUp(self):

Expand All @@ -61,9 +62,14 @@ def base_command(cls):
return command

def get_command_list(self, build_dir=None, base_dir=None, manifest_path=None, use_container=None,
parameter_overrides=None, mode=None):
parameter_overrides=None, mode=None, function_identifier=None):

command_list = [self.cmd, "build", "-t", self.template_path]
command_list = [self.cmd, "build"]

if function_identifier:
command_list += [function_identifier]

command_list += ["-t", self.template_path]

if parameter_overrides:
command_list += ["--parameter-overrides", self._make_parameter_override_arg(parameter_overrides)]
Expand Down
80 changes: 79 additions & 1 deletion tests/integration/buildcmd/test_build_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ def _verify_built_artifact(self, build_dir, function_logical_id, expected_files)
function_logical_id)

all_artifacts = set(os.listdir(str(resource_artifact_dir)))
print(all_artifacts)
actual_files = all_artifacts.intersection(expected_files)
self.assertEquals(actual_files, expected_files)

Expand Down Expand Up @@ -402,3 +401,82 @@ def _verify_built_artifact(self, build_dir, function_logical_id, expected_files)
all_artifacts = set(os.listdir(str(resource_artifact_dir)))
actual_files = all_artifacts.intersection(expected_files)
self.assertEquals(actual_files, expected_files)


class TestBuildCommand_SingleFunctionBuilds(BuildIntegBase):
template = "many-functions-template.yaml"

EXPECTED_FILES_GLOBAL_MANIFEST = set()
EXPECTED_FILES_PROJECT_MANIFEST = {'__init__.py', 'main.py', 'numpy',
# 'cryptography',
"jinja2",
'requirements.txt'}

def test_function_not_found(self):
overrides = {"Runtime": 'python3.7', "CodeUri": "Python", "Handler": "main.handler"}
cmdlist = self.get_command_list(parameter_overrides=overrides,
function_identifier="FunctionNotInTemplate")

process = subprocess.Popen(cmdlist, cwd=self.working_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process.communicate()

self.assertEquals(process.returncode, 1)
self.assertIn('FunctionNotInTemplate not found', str(stderr.decode('utf8')))

@parameterized.expand([
("python3.7", False, "FunctionOne"),
("python3.7", "use_container", "FunctionOne"),
("python3.7", False, "FunctionTwo"),
("python3.7", "use_container", "FunctionTwo")
])
def test_build_single_function(self, runtime, use_container, function_identifier):
# Don't run test on wrong Python versions
py_version = self._get_python_version()
if py_version != runtime:
self.skipTest("Current Python version '{}' does not match Lambda runtime version '{}'".format(py_version,
runtime))

overrides = {"Runtime": runtime, "CodeUri": "Python", "Handler": "main.handler"}
cmdlist = self.get_command_list(use_container=use_container,
parameter_overrides=overrides,
function_identifier=function_identifier)

LOG.info("Running Command: {}", cmdlist)
process = subprocess.Popen(cmdlist, cwd=self.working_dir)
process.wait()

self._verify_built_artifact(self.default_build_dir, function_identifier,
self.EXPECTED_FILES_PROJECT_MANIFEST)

expected = {
"pi": "3.14",
"jinja": "Hello World"
}
self._verify_invoke_built_function(self.built_template,
function_identifier,
self._make_parameter_override_arg(overrides),
expected)
self.verify_docker_container_cleanedup(runtime)

def _verify_built_artifact(self, build_dir, function_logical_id, expected_files):
self.assertTrue(build_dir.exists(), "Build directory should be created")

build_dir_files = os.listdir(str(build_dir))
self.assertIn("template.yaml", build_dir_files)
self.assertIn(function_logical_id, build_dir_files)

template_path = build_dir.joinpath("template.yaml")
resource_artifact_dir = build_dir.joinpath(function_logical_id)

# Make sure the template has correct CodeUri for resource
self._verify_resource_property(str(template_path),
function_logical_id,
"CodeUri",
function_logical_id)

all_artifacts = set(os.listdir(str(resource_artifact_dir)))
actual_files = all_artifacts.intersection(expected_files)
self.assertEquals(actual_files, expected_files)

def _get_python_version(self):
return "python{}.{}".format(sys.version_info.major, sys.version_info.minor)
28 changes: 28 additions & 0 deletions tests/integration/testdata/buildcmd/many-functions-template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
AWSTemplateFormatVersion : '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Parameteres:
Runtime:
Type: String
CodeUri:
Type: String
Handler:
Type: String

Resources:

FunctionOne:
Type: AWS::Serverless::Function
Properties:
Handler: !Ref Handler
Runtime: !Ref Runtime
CodeUri: !Ref CodeUri
Timeout: 600

FunctionTwo:
Type: AWS::Serverless::Function
Properties:
Handler: !Ref Handler
Runtime: !Ref Runtime
CodeUri: !Ref CodeUri
Timeout: 600
Loading

0 comments on commit 4512794

Please sign in to comment.