-
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 all 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 |
---|---|---|
@@ -0,0 +1,118 @@ | ||
""" | ||
Common CLI options shared by various commands | ||
""" | ||
|
||
import os | ||
import logging | ||
from functools import partial | ||
|
||
import click | ||
from samcli.cli.types import CfnParameterOverridesType | ||
|
||
_TEMPLATE_OPTION_DEFAULT_VALUE = "template.[yaml|yml]" | ||
|
||
|
||
LOG = logging.getLogger(__name__) | ||
|
||
|
||
def get_or_default_template_file_name(ctx, param, provided_value, include_build): | ||
""" | ||
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 | ||
""" | ||
|
||
search_paths = [ | ||
"template.yaml", | ||
"template.yml", | ||
] | ||
|
||
if include_build: | ||
search_paths.insert(0, os.path.join(".aws-sam", "build", "template.yaml")) | ||
|
||
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" | ||
|
||
for option in search_paths: | ||
if os.path.exists(option): | ||
provided_value = option | ||
break | ||
|
||
result = os.path.abspath(provided_value) | ||
LOG.debug("Using SAM Template at %s", result) | ||
return result | ||
|
||
|
||
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_option_without_build(f): | ||
""" | ||
Common ClI option for template | ||
|
||
:param f: Callback passed by Click | ||
:return: Callback | ||
""" | ||
return template_click_option(include_build=False)(f) | ||
|
||
|
||
def template_click_option(include_build=True): | ||
""" | ||
Click Option for template option | ||
""" | ||
return click.option('--template', '-t', | ||
default=_TEMPLATE_OPTION_DEFAULT_VALUE, | ||
type=click.Path(), | ||
envvar="SAM_TEMPLATE_FILE", | ||
callback=partial(get_or_default_template_file_name, include_build=include_build), | ||
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."), | ||
] | ||
|
||
|
||
def parameter_override_click_option(): | ||
return click.option("--parameter-overrides", | ||
type=CfnParameterOverridesType(), | ||
help="Optional. A string that contains CloudFormation parameter overrides encoded as key=value " | ||
"pairs. Use the same format as the AWS CLI, e.g. 'ParameterKey=KeyPairName," | ||
"ParameterValue=MyKey ParameterKey=InstanceType,ParameterValue=t1.micro'") | ||
|
||
|
||
def parameter_override_option(f): | ||
return parameter_override_click_option()(f) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
""" | ||
Utilities to manipulate template | ||
""" | ||
|
||
import yaml | ||
|
||
try: | ||
import pathlib | ||
except ImportError: | ||
import pathlib2 as pathlib | ||
|
||
from samcli.yamlhelper import yaml_parse | ||
|
||
|
||
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 | ||
""" | ||
|
||
if not pathlib.Path(template_file).exists(): | ||
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,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,123 @@ | ||
""" | ||
Context object used by build command | ||
""" | ||
|
||
import os | ||
import shutil | ||
|
||
try: | ||
import pathlib | ||
except ImportError: | ||
import pathlib2 as pathlib | ||
|
||
from samcli.local.docker.manager import ContainerManager | ||
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 | ||
|
||
|
||
class BuildContext(object): | ||
|
||
_BUILD_DIR_PERMISSIONS = 0o755 | ||
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. Maybe a comment on why these permissions? 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. Done |
||
|
||
def __init__(self, | ||
template_file, | ||
base_dir, | ||
build_dir, | ||
manifest_path=None, | ||
clean=False, | ||
use_container=False, | ||
parameter_overrides=None, | ||
docker_network=None, | ||
skip_pull_image=False): | ||
|
||
self._template_file = template_file | ||
self._base_dir = base_dir | ||
self._build_dir = build_dir | ||
self._manifest_path = manifest_path | ||
self._clean = clean | ||
self._use_container = use_container | ||
self._parameter_overrides = parameter_overrides | ||
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): | ||
try: | ||
self._template_dict = get_template_data(self._template_file) | ||
except ValueError as ex: | ||
raise UserException(str(ex)) | ||
|
||
self._function_provider = SamFunctionProvider(self._template_dict, self._parameter_overrides) | ||
|
||
if not self._base_dir: | ||
# Base directory, if not provided, is the directory containing the template | ||
self._base_dir = str(pathlib.Path(self._template_file).resolve().parent) | ||
|
||
self._build_dir = self._setup_build_dir(self._build_dir, self._clean) | ||
|
||
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 | ||
|
||
@staticmethod | ||
def _setup_build_dir(build_dir, clean): | ||
|
||
# Get absolute path | ||
build_dir = str(pathlib.Path(build_dir).resolve()) | ||
|
||
if not pathlib.Path(build_dir).exists(): | ||
# Build directory does not exist. Create the directory and all intermediate paths | ||
os.makedirs(build_dir, BuildContext._BUILD_DIR_PERMISSIONS) | ||
|
||
if os.listdir(build_dir) and clean: | ||
# Build folder contains something inside. Clear everything. | ||
shutil.rmtree(build_dir) | ||
# this would have cleared the parent folder as well. So recreate it. | ||
os.mkdir(build_dir, BuildContext._BUILD_DIR_PERMISSIONS) | ||
|
||
return build_dir | ||
|
||
@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 base_dir(self): | ||
return self._base_dir | ||
|
||
@property | ||
def use_container(self): | ||
return self._use_container | ||
|
||
@property | ||
def output_template_path(self): | ||
return os.path.join(self._build_dir, "template.yaml") | ||
|
||
@property | ||
def manifest_path_override(self): | ||
if self._manifest_path: | ||
return os.path.abspath(self._manifest_path) | ||
|
||
return None |
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.