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

Implementation of "sam build" CLI command #766

Merged
merged 19 commits into from
Nov 19, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ __pycache__/

# Distribution / packaging
.Python
build/
/build/
develop-eggs/
dist/
downloads/
Expand Down
2 changes: 1 addition & 1 deletion requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ docker>=3.3.0
dateparser~=0.7
python-dateutil~=2.6
pathlib2~=2.3.2; python_version<"3.4"

aws_lambda_builders==0.0.1
1 change: 1 addition & 0 deletions samcli/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"samcli.commands.deploy",
"samcli.commands.package",
"samcli.commands.logs",
"samcli.commands.build"
}


Expand Down
Empty file.
75 changes: 75 additions & 0 deletions samcli/commands/_utils/options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@

import os
import click

_TEMPLATE_OPTION_DEFAULT_VALUE = "template.[yaml|yml]"


def get_or_default_template_file_name(ctx, param, provided_value):
"""
Default value for the template file name option is more complex than what Click can handle.
This method either returns user provided file name or one of the two default options (template.yaml/template.yml)
depending on the file that exists

:param ctx: Click Context
:param param: Param name
:param provided_value: Value provided by Click. It could either be the default value or provided by user.
:return: Actual value to be used in the CLI
"""

if provided_value == _TEMPLATE_OPTION_DEFAULT_VALUE:
# Default value was used. Value can either be template.yaml or template.yml. Decide based on which file exists
# .yml is the default, even if it does not exist.
provided_value = "template.yml"

option = "template.yaml"
if os.path.exists(option):
provided_value = option

return os.path.abspath(provided_value)


def template_common_option(f):
"""
Common ClI option for template

:param f: Callback passed by Click
:return: Callback
"""
return template_click_option()(f)


def template_click_option():
"""
Click Option for template option
"""
return click.option('--template', '-t',
default=_TEMPLATE_OPTION_DEFAULT_VALUE,
type=click.Path(),
envvar="SAM_TEMPLATE_FILE",
callback=get_or_default_template_file_name,
show_default=True,
help="AWS SAM template file")


def docker_common_options(f):
for option in reversed(docker_click_options()):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: What does reversed really give us?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It allows us to list options in the code in the order it will appear in help text. Just for our convenience.

option(f)

return f


def docker_click_options():

return [
click.option('--skip-pull-image',
is_flag=True,
help="Specify whether CLI should skip pulling down the latest Docker image for Lambda runtime.",
envvar="SAM_SKIP_PULL_IMAGE"),

click.option('--docker-network',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to pass environment variables to the container ?

envvar="SAM_DOCKER_NETWORK",
help="Specifies the name or id of an existing docker network to lambda docker "
"containers should connect to, along with the default bridge network. If not specified, "
"the Lambda containers will only connect to the default bridge docker network."),
]
6 changes: 6 additions & 0 deletions samcli/commands/build/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
`sam build` command
"""

# Expose the cli object here
from .command import cli # noqa
113 changes: 113 additions & 0 deletions samcli/commands/build/build_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@

import os
import shutil
import yaml

from samcli.yamlhelper import yaml_parse
from samcli.local.docker.manager import ContainerManager
from samcli.commands.local.lib.sam_function_provider import SamFunctionProvider


class BuildContext(object):

def __init__(self, template_file,
source_root,
build_dir,
clean=False,
use_container=False,
docker_network=None,
skip_pull_image=False):

self._template_file = template_file
self._source_root = source_root
self._build_dir = build_dir
self._clean = clean
self._use_container = use_container
self._docker_network = docker_network
self._skip_pull_image = skip_pull_image

self._function_provider = None
self._template_dict = None
self._app_builder = None
self._container_manager = None

def __enter__(self):
self._template_dict = self._get_template_data(self._template_file)
self._function_provider = SamFunctionProvider(self._template_dict)

self._build_dir = self._setup_build_dir()

if self._use_container:
self._container_manager = ContainerManager(docker_network_id=self._docker_network,
skip_pull_image=self._skip_pull_image)

return self

def __exit__(self, *args):
pass

@property
def container_manager(self):
return self._container_manager

@property
def function_provider(self):
return self._function_provider
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we go one level further and just provide a class that conforms to a provider instead of a specific one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly. But this is a good easy place to start


@property
def template_dict(self):
return self._template_dict

@property
def build_dir(self):
return self._build_dir

@property
def source_root(self):
return self._source_root

@property
def use_container(self):
return self._use_container

@property
def output_template_path(self):
return os.path.join(self._build_dir, "built-template.yaml")

def _setup_build_dir(self):

# Get absolute path
self._build_dir = os.path.abspath(self._build_dir)

if not os.path.exists(self._build_dir):
# TODO: What permissions should I apply to this directory?
os.mkdir(self._build_dir)

if os.listdir(self._build_dir) and self._clean:
# Build folder contains something inside. Clear everything.
shutil.rmtree(self._build_dir)
# this would have cleared the parent folder as well. So recreate it.
os.mkdir(self._build_dir)

return self._build_dir

@staticmethod
def _get_template_data(template_file):
"""
Read the template file, parse it as JSON/YAML and return the template as a dictionary.

:param string template_file: Path to the template to read
:return dict: Template data as a dictionary
:raises InvokeContextException: If template file was not found or the data was not a JSON/YAML
"""
# TODO: This method was copied from InvokeContext. Move it into a common folder
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget about this.


if not os.path.exists(template_file):
raise ValueError("Template file not found at {}".format(template_file))

with open(template_file, 'r') as fp:
try:
return yaml_parse(fp.read())
except (ValueError, yaml.YAMLError) as ex:
raise ValueError("Failed to parse template: {}".format(str(ex)))

83 changes: 83 additions & 0 deletions samcli/commands/build/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""
CLI command for "build" command
"""

import os
import logging
import click

from samcli.yamlhelper import yaml_dump
from samcli.cli.main import pass_context, common_options as cli_framework_options, aws_creds_options
from samcli.commands._utils.options import template_common_option as template_option, docker_common_options
from samcli.commands.build.build_context import BuildContext
from samcli.lib.build.app_builder import ApplicationBuilder

LOG = logging.getLogger(__name__)

HELP_TEXT = """
Use this command to build your Lambda function source code and generate artifacts that can be deployed to AWS Lambda
"""


@click.command("build", help=HELP_TEXT, short_help="Build your Lambda function code")
@click.option('--build-dir', '-b',
default="build",
type=click.Path(),
help="Path to a folder where the built artifacts will be stored")
@click.option("--source-root", "-s",
default=os.getcwd(),
type=click.Path(),
help="Resolve relative paths to function's source code with respect to this folder. Use this if "
"SAM template and your source code are not in same enclosing folder")
@click.option("--native", "-n",
is_flag=True,
help="Run the builds inside a AWS Lambda like Docker container")
@click.option("--clean", "-c",
is_flag=True,
help="Do a clean build by first deleting everything within the build directory")
@template_option
@docker_common_options
@cli_framework_options
@aws_creds_options
@pass_context
def cli(ctx,
template,
source_root,
build_dir,
clean,
native,
docker_network,
skip_pull_image):
# All logic must be implemented in the ``do_cli`` method. This helps with easy unit testing

do_cli(template, source_root, build_dir, clean, native, docker_network, skip_pull_image) # pragma: no cover


def do_cli(template, source_root, build_dir, clean, use_container, docker_network, skip_pull_image):
"""
Implementation of the ``cli`` method
"""

LOG.debug("'build' command is called")

with BuildContext(template,
source_root,
build_dir,
clean=True, # TODO: Forcing a clean build for testing. REmove this
use_container=use_container,
docker_network=docker_network,
skip_pull_image=skip_pull_image) as ctx:

builder = ApplicationBuilder(ctx.function_provider,
ctx.build_dir,
ctx.source_root,
container_manager=ctx.container_manager,
)
artifacts = builder.build()
modified_template = builder.update_template(ctx.template_dict,
ctx.output_template_path,
artifacts)

with open(ctx.output_template_path, "w") as fp:
fp.write(yaml_dump(modified_template))

62 changes: 2 additions & 60 deletions samcli/commands/local/cli_common/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,10 @@
Common CLI options for invoke command
"""

import os
import click
from samcli.commands._utils.options import template_click_option, docker_click_options
from samcli.cli.types import CfnParameterOverridesType

_TEMPLATE_OPTION_DEFAULT_VALUE = "template.[yaml|yml]"


def get_or_default_template_file_name(ctx, param, provided_value):
"""
Default value for the template file name option is more complex than what Click can handle.
This method either returns user provided file name or one of the two default options (template.yaml/template.yml)
depending on the file that exists

:param ctx: Click Context
:param param: Param name
:param provided_value: Value provided by Click. It could either be the default value or provided by user.
:return: Actual value to be used in the CLI
"""

if provided_value == _TEMPLATE_OPTION_DEFAULT_VALUE:
# Default value was used. Value can either be template.yaml or template.yml. Decide based on which file exists
# .yml is the default, even if it does not exist.
provided_value = "template.yml"

option = "template.yaml"
if os.path.exists(option):
provided_value = option

return os.path.abspath(provided_value)


def template_common_option(f):
"""
Common ClI option for template

:param f: Callback passed by Click
:return: Callback
"""
return template_click_option()(f)


def template_click_option():
"""
Click Option for template option
"""
return click.option('--template', '-t',
default=_TEMPLATE_OPTION_DEFAULT_VALUE,
type=click.Path(),
envvar="SAM_TEMPLATE_FILE",
callback=get_or_default_template_file_name,
show_default=True,
help="AWS SAM template file")


def service_common_options(port):
def construct_options(f):
Expand Down Expand Up @@ -128,19 +79,10 @@ def invoke_common_options(f):
"a remote machine, you must mount the path where the SAM file exists on the docker machine "
"and modify this value to match the remote machine."),

click.option('--docker-network',
envvar="SAM_DOCKER_NETWORK",
help="Specifies the name or id of an existing docker network to lambda docker "
"containers should connect to, along with the default bridge network. If not specified, "
"the Lambda containers will only connect to the default bridge docker network."),

click.option('--log-file', '-l',
help="logfile to send runtime logs to."),

click.option('--skip-pull-image',
is_flag=True,
help="Specify whether CLI should skip pulling down the latest Docker image for Lambda runtime.",
envvar="SAM_SKIP_PULL_IMAGE"),
] + docker_click_options() + [

click.option('--profile',
help="Specify which AWS credentials profile to use."),
Expand Down
2 changes: 1 addition & 1 deletion samcli/commands/validate/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from samcli.commands.exceptions import UserException
from samcli.cli.main import pass_context, common_options as cli_framework_options, aws_creds_options
from samcli.commands.local.cli_common.options import template_common_option as template_option
from samcli.commands._utils.options import template_common_option as template_option
from samcli.commands.local.cli_common.user_exceptions import InvalidSamTemplateException, SamTemplateNotFoundException
from samcli.yamlhelper import yaml_parse
from .lib.exceptions import InvalidSamDocumentException
Expand Down
Empty file added samcli/lib/build/__init__.py
Empty file.
Loading