Skip to content
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
2 changes: 2 additions & 0 deletions samcli/commands/build/build_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def __init__(
skip_pull_image: bool = False,
container_env_var: Optional[dict] = None,
container_env_var_file: Optional[str] = None,
build_images: Optional[dict] = None,
) -> None:

self._resource_identifier = resource_identifier
Expand All @@ -65,6 +66,7 @@ def __init__(
self._cached = cached
self._container_env_var = container_env_var
self._container_env_var_file = container_env_var_file
self._build_images = build_images

self._function_provider: Optional[SamFunctionProvider] = None
self._layer_provider: Optional[SamLayerProvider] = None
Expand Down
96 changes: 81 additions & 15 deletions samcli/commands/build/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import os
import logging
from typing import List, Optional, Dict
from typing import List, Optional, Dict, Tuple
import click

from samcli.cli.context import Context
Expand All @@ -19,6 +19,7 @@
from samcli.lib.telemetry.metric import track_command
from samcli.cli.cli_config_file import configuration_option, TomlProvider
from samcli.lib.utils.version_checker import check_newer_version
from samcli.commands.build.exceptions import InvalidBuildImageException

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -124,6 +125,20 @@
type=click.Path(), # Must be a json file
help="Path to environment variable json file (e.g., env_vars.json) to pass into build containers",
)
@click.option(
"--build-image",
"-bi",
default=None,
multiple=True, # Can pass in multiple build images
required=False,
help="Container image URIs for building functions. "
"You can specify for all functions with just the image URI "
"(--build-image public.ecr.aws/sam/build-nodejs14.x:latest). "
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not have two options for this? We have seen in the past overriding options for different cases can become complex quickly. Seems like having one option for all and one option to specify per function might be safer to have.

Side question: Did we consider putting this data on the function in the template Metadata like we do for other build information?

Copy link
Contributor Author

@CoshUS CoshUS Mar 24, 2021

Choose a reason for hiding this comment

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

This was done for consistency with the env var option as both of them have the same format and similar effect. We can bring this into the UX review for a further discussion.
Metadata in the template is not in scope of this change, but we can explore it in the future.

Copy link
Contributor

Choose a reason for hiding this comment

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

I believe right now this is safe to have them in one option. Previously the container env var option have the the same overriding and precedence pattern which we feel that is safe and would not be too complex to cause any issues. Anyways we have a ux review set up on Friday as well, @jfuss if you can join to discuss this it would be nice as well!

Copy link
Contributor

Choose a reason for hiding this comment

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

Did we consider putting this data on the function in the template Metadata like we do for other build information?

I think this is a good call-out. We should have a consistent story about how parameters are passed. How do we choose between the template or CLI arguments? Why are CLI arguments are preferred here versus the template?

"You can specify for each individual function with "
"(--build-image FunctionLogicalID=public.ecr.aws/sam/build-nodejs14.x:latest). "
"A combination of the two can be used. If a function does not have build image specified or "
"an image URI for all functions, the default SAM CLI build images will be used.",
)
@click.option(
"--parallel",
"-p",
Expand Down Expand Up @@ -173,8 +188,9 @@ def cli(
parallel: bool,
manifest: Optional[str],
docker_network: Optional[str],
container_env_var: Optional[List[str]],
container_env_var: Optional[Tuple[str]],
container_env_var_file: Optional[str],
build_image: Optional[Tuple[str]],
skip_pull_image: bool,
parameter_overrides: dict,
config_file: str,
Expand Down Expand Up @@ -204,6 +220,7 @@ def cli(
mode,
container_env_var,
container_env_var_file,
build_image,
) # pragma: no cover


Expand All @@ -222,8 +239,9 @@ def do_cli( # pylint: disable=too-many-locals, too-many-statements
skip_pull_image: bool,
parameter_overrides: Dict,
mode: Optional[str],
container_env_var: Optional[List[str]],
container_env_var: Optional[Tuple[str]],
container_env_var_file: Optional[str],
build_image: Optional[Tuple[str]],
) -> None:
"""
Implementation of the ``cli`` method
Expand All @@ -250,6 +268,7 @@ def do_cli( # pylint: disable=too-many-locals, too-many-statements
LOG.info("Starting Build inside a container")

processed_env_vars = _process_env_var(container_env_var)
processed_build_images = _process_image_options(build_image)

with BuildContext(
function_identifier,
Expand All @@ -267,6 +286,7 @@ def do_cli( # pylint: disable=too-many-locals, too-many-statements
mode=mode,
container_env_var=processed_env_vars,
container_env_var_file=container_env_var_file,
build_images=processed_build_images,
) as ctx:
try:
builder = ApplicationBuilder(
Expand All @@ -282,6 +302,7 @@ def do_cli( # pylint: disable=too-many-locals, too-many-statements
parallel=parallel,
container_env_var=processed_env_vars,
container_env_var_file=container_env_var_file,
build_images=processed_build_images,
)
except FunctionNotFound as ex:
raise UserException(str(ex), wrapped_from=ex.__class__.__name__) from ex
Expand Down Expand Up @@ -376,12 +397,12 @@ def _get_mode_value_from_envvar(name: str, choices: List[str]) -> Optional[str]:
return mode


def _process_env_var(container_env_var: Optional[List[str]]) -> Dict:
def _process_env_var(container_env_var: Optional[Tuple[str]]) -> Dict:
"""
Parameters
----------
container_env_var : list
the list of command line env vars received from --container-env-var flag
container_env_var : Tuple
the tuple of command line env vars received from --container-env-var flag
Each input format needs to be either function specific format (FuncName.VarName=Value)
or global format (VarName=Value)

Expand All @@ -396,19 +417,14 @@ def _process_env_var(container_env_var: Optional[List[str]]) -> Dict:
for env_var in container_env_var:
location_key = "Parameters"

if "=" not in env_var:
LOG.error("Invalid command line --container-env-var input %s, skipped", env_var)
continue

key, value = env_var.split("=", 1)
env_var_name = key
env_var_name, value = _parse_key_value_pair(env_var)

if not value.strip():
if not env_var_name or not value:
LOG.error("Invalid command line --container-env-var input %s, skipped", env_var)
continue

if "." in key:
location_key, env_var_name = key.split(".", 1)
if "." in env_var_name:
location_key, env_var_name = env_var_name.split(".", 1)
if not location_key.strip() or not env_var_name.strip():
LOG.error("Invalid command line --container-env-var input %s, skipped", env_var)
continue
Expand All @@ -418,3 +434,53 @@ def _process_env_var(container_env_var: Optional[List[str]]) -> Dict:
processed_env_vars[location_key][env_var_name] = value

return processed_env_vars


def _process_image_options(image_args: Optional[Tuple[str]]) -> Dict:
"""
Parameters
----------
image_args : Tuple
Tuple of command line image options in the format of
"Function1=public.ecr.aws/abc/abc:latest" or
"public.ecr.aws/abc/abc:latest"

Returns
-------
dictionary
Function as key and the corresponding image URI as value.
Global default image URI is contained in the None key.
"""
build_images: Dict[Optional[str], str] = dict()
if image_args:
for build_image_string in image_args:
function_name, image_uri = _parse_key_value_pair(build_image_string)
if not image_uri:
raise InvalidBuildImageException(f"Invalid command line --build-image input {build_image_string}.")
build_images[function_name] = image_uri

return build_images


def _parse_key_value_pair(arg: str) -> Tuple[Optional[str], str]:
"""
Parameters
----------
arg : str
Arg in the format of "Value" or "Key=Value"
Returns
-------
key : Optional[str]
If key is not specified, None will be the key.
value : str
"""
key: Optional[str]
value: str
if "=" in arg:
parts = arg.split("=", 1)
key = parts[0].strip()
value = parts[1].strip()
else:
key = None
value = arg.strip()
return key, value
6 changes: 6 additions & 0 deletions samcli/commands/build/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ class MissingBuildMethodException(UserException):
"""
Exception to be thrown when a layer is tried to build without BuildMethod
"""


class InvalidBuildImageException(UserException):
"""
Value provided to --build-image is invalid
"""
46 changes: 26 additions & 20 deletions samcli/lib/build/app_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def __init__(
docker_client: Optional[docker.DockerClient] = None,
container_env_var: Optional[Dict] = None,
container_env_var_file: Optional[str] = None,
build_images: Optional[dict] = None,
Copy link
Contributor

Choose a reason for hiding this comment

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

Missed this during review, will change it to Dict in next pr for consistency

) -> None:
"""
Initialize the class
Expand Down Expand Up @@ -103,6 +104,8 @@ def __init__(
An optional dictionary of environment variables to pass to the container
container_env_var_file : Optional[str]
An optional path to file that contains environment variables to pass to the container
build_images : Optional[Dict]
An optional dictionary of build images to be used for building functions
"""
self._resources_to_build = resources_to_build
self._build_dir = build_dir
Expand All @@ -122,6 +125,7 @@ def __init__(
self._colored = Colored()
self._container_env_var = container_env_var
self._container_env_var_file = container_env_var_file
self._build_images = build_images

def build(self) -> Dict[str, str]:
"""
Expand Down Expand Up @@ -428,28 +432,23 @@ def _build_layer(

# By default prefer to build in-process for speed
build_runtime = specified_workflow
build_method = self._build_function_in_process
options = ApplicationBuilder._get_build_options(layer_name, config.language, None)
if self._container_manager:
build_method = self._build_function_on_container
if config.language == "provided":
LOG.warning(
"For container layer build, first compatible runtime is chosen as build target for container."
)
# Only set to this value if specified workflow is makefile
# which will result in config language as provided
build_runtime = compatible_runtimes[0]
options = ApplicationBuilder._get_build_options(layer_name, config.language, None)
self._build_function_on_container(
config, code_dir, artifact_subdir, manifest_path, build_runtime, options, container_env_vars
)
else:
self._build_function_in_process(
config, code_dir, artifact_subdir, scratch_dir, manifest_path, build_runtime, options
)

build_method(
config,
code_dir,
artifact_subdir,
scratch_dir,
manifest_path,
build_runtime,
options,
container_env_vars,
)
# Not including subfolder in return so that we copy subfolder, instead of copying artifacts inside it.
return artifact_dir

Expand Down Expand Up @@ -520,21 +519,28 @@ def _build_function( # pylint: disable=R1710

options = ApplicationBuilder._get_build_options(function_name, config.language, handler)
# By default prefer to build in-process for speed
build_method = self._build_function_in_process
if self._container_manager:
build_method = self._build_function_on_container
return build_method(
image = None
if self._build_images is not None:
if function_name in self._build_images:
image = self._build_images[function_name]
elif None in self._build_images:
image = self._build_images[None]

return self._build_function_on_container(
config,
code_dir,
artifact_dir,
scratch_dir,
manifest_path,
runtime,
options,
container_env_vars,
image,
)

return build_method(config, code_dir, artifact_dir, scratch_dir, manifest_path, runtime, options)
return self._build_function_in_process(
config, code_dir, artifact_dir, scratch_dir, manifest_path, runtime, options
)

# pylint: disable=fixme
# FIXME: we need to throw an exception here, packagetype could be something else
Expand Down Expand Up @@ -572,7 +578,6 @@ def _build_function_in_process(
manifest_path: str,
runtime: str,
options: Optional[dict],
container_env_vars: Optional[Dict] = None,
) -> str:

builder = LambdaBuilder(
Expand Down Expand Up @@ -604,11 +609,11 @@ def _build_function_on_container(
config: CONFIG,
source_dir: str,
artifacts_dir: str,
scratch_dir: str,
manifest_path: str,
runtime: str,
options: Optional[Dict],
container_env_vars: Optional[Dict] = None,
build_image: Optional[str] = None,
) -> str:
# _build_function_on_container() is only called when self._container_manager if not None
if not self._container_manager:
Expand Down Expand Up @@ -642,6 +647,7 @@ def _build_function_on_container(
executable_search_paths=config.executable_search_paths,
mode=self._mode,
env_vars=container_env_vars,
image=build_image,
)

try:
Expand Down
7 changes: 5 additions & 2 deletions samcli/local/docker/lambda_build_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class LambdaBuildContainer(Container):
"""

_IMAGE_URI_PREFIX = "public.ecr.aws/sam/build"
_IMAGE_TAG = "latest"
_BUILDERS_EXECUTABLE = "lambda-builders"

def __init__( # pylint: disable=too-many-locals
Expand All @@ -36,6 +37,7 @@ def __init__( # pylint: disable=too-many-locals
log_level=None,
mode=None,
env_vars=None,
image=None,
):

abs_manifest_path = pathlib.Path(manifest_path).resolve()
Expand Down Expand Up @@ -74,7 +76,8 @@ def __init__( # pylint: disable=too-many-locals
mode,
)

image = LambdaBuildContainer._get_image(runtime)
if image is None:
image = LambdaBuildContainer._get_image(runtime)
entry = LambdaBuildContainer._get_entrypoint(request_json)
cmd = []

Expand Down Expand Up @@ -234,4 +237,4 @@ def _convert_to_container_dirs(host_paths_to_convert, host_to_container_path_map

@staticmethod
def _get_image(runtime):
return f"{LambdaBuildContainer._IMAGE_URI_PREFIX}-{runtime}"
return f"{LambdaBuildContainer._IMAGE_URI_PREFIX}-{runtime}:{LambdaBuildContainer._IMAGE_TAG}"
4 changes: 3 additions & 1 deletion samcli/local/docker/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def stop(self, container: Container) -> None:
container.stop()
container.delete()

def pull_image(self, image_name, tag="latest", stream=None):
def pull_image(self, image_name, tag=None, stream=None):
"""
Ask Docker to pull the container image with given name.

Expand All @@ -142,6 +142,8 @@ def pull_image(self, image_name, tag="latest", stream=None):
DockerImagePullFailedException
If the Docker image was not available in the server
"""
if tag is None:
tag = image_name.split(":")[1] if ":" in image_name else "latest"
Comment on lines +145 to +146
Copy link
Contributor

Choose a reason for hiding this comment

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

Which tag does it use if image_name has a tag and tag is set?

# use a global lock to get the image lock
with self._lock:
image_lock = self._lock_per_image.get(image_name)
Expand Down
1 change: 1 addition & 0 deletions tests/integration/buildcmd/build_integ_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ def verify_pulling_only_latest_tag(self, runtime):
len(images) > 1,
f"Other version of the build image {image_name} was pulled",
)
self.assertEqual(f"public.ecr.aws/sam/build-{runtime}:latest", images[0].tags[0])

def _make_parameter_override_arg(self, overrides):
return " ".join(["ParameterKey={},ParameterValue={}".format(key, value) for key, value in overrides.items()])
Expand Down
Loading