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 all 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
4 changes: 3 additions & 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 Expand Up @@ -378,5 +378,7 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk

# Temporary scratch directory used by the tests
tests/integration/buildcmd/scratch

# End of https://www.gitignore.io/api/osx,node,macos,linux,python,windows,pycharm,intellij,sublimetext,visualstudiocode
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.2-dev
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.
118 changes: 118 additions & 0 deletions samcli/commands/_utils/options.py
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()):
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."),
]


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)
30 changes: 30 additions & 0 deletions samcli/commands/_utils/template.py
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)))
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
123 changes: 123 additions & 0 deletions samcli/commands/build/build_context.py
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
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe a comment on why these permissions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
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 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
Loading