-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Changes from 3 commits
3c1edf1
b559395
9f9f7d4
bc334d0
a5effdd
cc3081f
a747c62
1ecf6b6
6048d67
85b9179
1a74396
4a8bde1
c8d8429
28b7898
1aee623
a0fc001
495ffbe
3d6e30f
7242af9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -230,7 +230,7 @@ __pycache__/ | |
|
||
# Distribution / packaging | ||
.Python | ||
build/ | ||
/build/ | ||
develop-eggs/ | ||
dist/ | ||
downloads/ | ||
|
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()): | ||
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', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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."), | ||
] |
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 |
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) | ||
sanathkr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
sanathkr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if not os.path.exists(self._build_dir): | ||
# TODO: What permissions should I apply to this directory? | ||
sanathkr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
sanathkr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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))) | ||
|
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(), | ||
sanathkr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
sanathkr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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)) | ||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.