Skip to content

Commit

Permalink
feat: sam Commands Understand Local File Paths for ImageUri
Browse files Browse the repository at this point in the history
As a summary, sam has learned to load an an image from a local archive
before proceeding with the `build`, `package`, and `deploy` commands.

When running `sam build` with an `ImageUri` pointing to a local file,
sam will load that archive into an image, then write the ID of the
image to the `ImageUri` property of the built template. ID works the
same as a tag for the Docker API, so business continues as usual from
here. The reason behind writing ID is that a loaded image could be
associated with multiple tags, and selecting one arbtrarily leads to
difficulties in the deploy command.

The package and deploy commands have three kinds of value for
`ImageUri` to consider. First, a value of the form
`{repo}:{tag}`. This functions as it always has. Second, an image
ID (in the form `sha256:{digest}`) which is probably the output of
`sam build`. In this case, the tag translation uses the name of the
as its input. Otherwise, they'd all end up with names starting with
"sha256". Last, a local file. In this case, it proceeds as it does in
the build command: Load the archive into an image first, then pass the
resource name into tag translation.

See: #6909
  • Loading branch information
chrisoverzero committed Apr 10, 2024
1 parent da2ce02 commit 91879e1
Show file tree
Hide file tree
Showing 12 changed files with 190 additions and 53 deletions.
18 changes: 18 additions & 0 deletions samcli/lib/build/app_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import logging
import os
import pathlib
from pathlib import Path
from typing import Dict, List, NamedTuple, Optional, cast

import docker
Expand Down Expand Up @@ -250,6 +251,7 @@ def _get_build_graph(
function_build_details = FunctionBuildDefinition(
function.runtime,
function.codeuri,
function.imageuri,
function.packagetype,
function.architecture,
function.metadata,
Expand Down Expand Up @@ -460,6 +462,16 @@ def _stream_lambda_image_build_logs(self, build_logs: List[Dict[str, str]], func
except LogStreamError as ex:
raise DockerBuildFailed(msg=f"{function_name} failed to build: {str(ex)}") from ex

def _load_lambda_image(self, image_archive_path: str) -> str:
with open(image_archive_path, mode="rb") as image_archive:
try:
[image, *rest] = self._docker_client.images.load(image_archive)
if len(rest) != 0:
raise DockerBuildFailed("Archive must represent a single image")
return f"{image.id}"
except docker.errors.APIError as ex:
raise DockerBuildFailed(msg=str(ex)) from ex

def _build_layer(
self,
layer_name: str,
Expand Down Expand Up @@ -599,6 +611,7 @@ def _build_function( # pylint: disable=R1710
self,
function_name: str,
codeuri: str,
imageuri: Optional[str],
packagetype: str,
runtime: str,
architecture: str,
Expand All @@ -619,6 +632,9 @@ def _build_function( # pylint: disable=R1710
Name or LogicalId of the function
codeuri : str
Path to where the code lives
imageuri : str
Location of the Lambda Image which is of the form {image}:{tag}, sha256:{digest},
or a path to a local archive
packagetype : str
The package type, 'Zip' or 'Image', see samcli/lib/utils/packagetype.py
runtime : str
Expand Down Expand Up @@ -646,6 +662,8 @@ def _build_function( # pylint: disable=R1710
Path to the location where built artifacts are available
"""
if packagetype == IMAGE:
if imageuri and Path(imageuri).is_file(): # something exists at this path and what exists is a file
return self._load_lambda_image(imageuri) # should be an image archive – load it instead of building it
# pylint: disable=fixme
# FIXME: _build_lambda_image assumes metadata is not None, we need to throw an exception here
return self._build_lambda_image(
Expand Down
4 changes: 4 additions & 0 deletions samcli/lib/build/build_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def _toml_table_to_function_build_definition(uuid: str, toml_table: tomlkit.api.
function_build_definition = FunctionBuildDefinition(
toml_table.get(RUNTIME_FIELD),
toml_table.get(CODE_URI_FIELD),
None,
toml_table.get(PACKAGETYPE_FIELD, ZIP),
toml_table.get(ARCHITECTURE_FIELD, X86_64),
dict(toml_table.get(METADATA_FIELD, {})),
Expand Down Expand Up @@ -584,6 +585,7 @@ def __init__(
self,
runtime: Optional[str],
codeuri: Optional[str],
imageuri: Optional[str],
packagetype: str,
architecture: str,
metadata: Optional[Dict],
Expand All @@ -595,6 +597,7 @@ def __init__(
super().__init__(source_hash, manifest_hash, env_vars, architecture)
self.runtime = runtime
self.codeuri = codeuri
self.imageuri = imageuri
self.packagetype = packagetype
self.handler = handler

Expand Down Expand Up @@ -688,6 +691,7 @@ def __eq__(self, other: Any) -> bool:
return (
self.runtime == other.runtime
and self.codeuri == other.codeuri
and self.imageuri == other.imageuri
and self.packagetype == other.packagetype
and self.metadata == other.metadata
and self.env_vars == other.env_vars
Expand Down
5 changes: 4 additions & 1 deletion samcli/lib/build/build_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@ def __init__(
self,
build_graph: BuildGraph,
build_dir: str,
build_function: Callable[[str, str, str, str, str, Optional[str], str, dict, dict, Optional[str], bool], str],
build_function: Callable[
[str, str, Optional[str], str, str, str, Optional[str], str, dict, dict, Optional[str], bool], str
],
build_layer: Callable[[str, str, str, List[str], str, str, dict, Optional[str], bool, Optional[Dict]], str],
cached: bool = False,
) -> None:
Expand Down Expand Up @@ -166,6 +168,7 @@ def build_single_function_definition(self, build_definition: FunctionBuildDefini
result = self._build_function(
build_definition.get_function_name(),
build_definition.codeuri, # type: ignore
build_definition.imageuri,
build_definition.packagetype,
build_definition.runtime, # type: ignore
build_definition.architecture,
Expand Down
21 changes: 19 additions & 2 deletions samcli/lib/package/ecr_uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import base64
import logging
from io import StringIO
from pathlib import Path
from typing import Dict

import botocore
Expand Down Expand Up @@ -76,10 +77,26 @@ def upload(self, image, resource_name):
if not self.login_session_active:
self.login()
self.login_session_active = True

# cosborn: We sometimes use the resource_name as the `image` parameter to `tag_translation`.
# This is because these two cases (directly from an archive or by ID) are effectively anonymous,
# so the best identifier we've been given is the resource name.
try:
docker_img = self.docker_client.images.get(image)
if Path(image).is_file():
with open(image, mode="rb") as image_archive:
[docker_img, *rest] = self.docker_client.images.load(image_archive)
if len(rest) != 0:
raise DockerPushFailedError("Archive must represent a single image")
_tag = tag_translation(resource_name, docker_image_id=docker_img.id, gen_tag=self.tag)
else:
# If it's not a file, it's gotta be a {repo}:{tag} or a sha256:{digest}
docker_img = self.docker_client.images.get(image)
_tag = tag_translation(
resource_name if image == docker_img.id else image,
docker_image_id=docker_img.id,
gen_tag=self.tag,
)

_tag = tag_translation(image, docker_image_id=docker_img.id, gen_tag=self.tag)
repository = (
self.ecr_repo
if not self.ecr_repo_multi or not isinstance(self.ecr_repo_multi, dict)
Expand Down
7 changes: 5 additions & 2 deletions samcli/lib/providers/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import posixpath
from collections import namedtuple
from enum import Enum
from pathlib import Path
from typing import Any, Dict, Iterator, List, NamedTuple, Optional, Set, Union, cast

from samcli.commands.local.cli_common.user_exceptions import (
Expand Down Expand Up @@ -953,6 +954,7 @@ def get_function_build_info(
packagetype: str,
inlinecode: Optional[str],
codeuri: Optional[str],
imageuri: Optional[str],
metadata: Optional[Dict],
) -> FunctionBuildInfo:
"""
Expand All @@ -974,8 +976,9 @@ def get_function_build_info(
metadata = metadata or {}
dockerfile = cast(str, metadata.get("Dockerfile", ""))
docker_context = cast(str, metadata.get("DockerContext", ""))

if not dockerfile or not docker_context:
buildable = dockerfile and docker_context
loadable = imageuri and Path(imageuri).is_file()
if not buildable and not loadable:
LOG.debug(
"Skip Building %s function, as it is missing either Dockerfile or DockerContext "
"metadata properties.",
Expand Down
2 changes: 1 addition & 1 deletion samcli/lib/providers/sam_function_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ def _build_function_configuration(
raise MissingFunctionHandlerException(f"Could not find handler for function: {name}")

function_build_info = get_function_build_info(
get_full_path(stack.stack_path, function_id), package_type, inlinecode, codeuri, metadata
get_full_path(stack.stack_path, function_id), package_type, inlinecode, codeuri, imageuri, metadata
)

return Function(
Expand Down
6 changes: 4 additions & 2 deletions samcli/local/docker/lambda_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ def __init__(
runtime str
Name of the Lambda runtime
imageuri str
Name of the Lambda Image which is of the form {image}:{tag}
Location of the Lambda Image which is of the form {image}:{tag}, sha256:{digest},
or a path to a local archive
handler str
Handler of the function to run
packagetype str
Expand Down Expand Up @@ -240,7 +241,8 @@ def _get_image(
packagetype : str
Package type for the lambda function which is either zip or image.
image : str
Name of the Lambda Image which is of the form {image}:{tag}
Location of the Lambda Image which is of the form {image}:{tag}, sha256:{digest},
or a path to a local archive
layers : List[str]
List of layers
architecture : str
Expand Down
3 changes: 2 additions & 1 deletion samcli/local/lambdafn/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ def __init__(
handler : str
Handler method
imageuri : str
Name of the Lambda Image which is of the form {image}:{tag}
Location of the Lambda Image which is of the form {image}:{tag}, sha256:{digest},
or a path to a local archive
imageconfig : str
Image configuration which can be used set to entrypoint, command and working dir for the container.
packagetype : str
Expand Down
4 changes: 3 additions & 1 deletion tests/unit/commands/buildcmd/test_build_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ def get_function(
function_url_config=None,
stack_path="",
runtime_management_config=None,
function_build_info=get_function_build_info("stack/function", packagetype, inlinecode, codeuri, metadata),
function_build_info=get_function_build_info(
"stack/function", packagetype, inlinecode, codeuri, imageuri, metadata
),
)


Expand Down
Loading

0 comments on commit 91879e1

Please sign in to comment.