Skip to content
10 changes: 10 additions & 0 deletions samcli/commands/_utils/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from botocore.utils import set_value_from_jmespath

from samcli.commands.exceptions import UserException
from samcli.lib.samlib.resource_metadata_normalizer import ASSET_PATH_METADATA_KEY
from samcli.lib.utils.packagetype import ZIP, IMAGE
from samcli.yamlhelper import yaml_parse, yaml_dump
from samcli.lib.utils.resources import (
Expand Down Expand Up @@ -170,6 +171,15 @@ def _update_relative_paths(template_dict, original_root, new_root):

set_value_from_jmespath(properties, path_prop_name, updated_path)

metadata = resource.get("Metadata", {})
if ASSET_PATH_METADATA_KEY in metadata:
path = metadata.get(ASSET_PATH_METADATA_KEY, "")
updated_path = _resolve_relative_to(path, original_root, new_root)
if not updated_path:
# This path does not need to get updated
continue
metadata[ASSET_PATH_METADATA_KEY] = updated_path

# AWS::Includes can be anywhere within the template dictionary. Hence we need to recurse through the
# dictionary in a separate method to find and update relative paths in there
template_dict = _update_aws_include_relative_path(template_dict, original_root, new_root)
Expand Down
12 changes: 10 additions & 2 deletions samcli/commands/build/build_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ def _collect_single_function_and_dependent_layers(
return

resource_collector.add_function(function)
resource_collector.add_layers([l for l in function.layers if l.build_method is not None])
resource_collector.add_layers([l for l in function.layers if l.build_method is not None and not l.skip_build])

def _collect_single_buildable_layer(
self, resource_identifier: str, resource_collector: ResourcesToBuildCollector
Expand Down Expand Up @@ -466,7 +466,11 @@ def _is_function_buildable(function: Function):
if isinstance(function.codeuri, str) and function.codeuri.endswith(".zip"):
LOG.debug("Skip building zip function: %s", function.full_path)
return False

# skip build the functions that marked as skip-build
if function.skip_build:
LOG.debug("Skip building pre-built function: %s", function.full_path)
return False
# skip build the functions with Image Package Type with no docker context or docker file metadata
if function.packagetype == IMAGE:
metadata = function.metadata if function.metadata else {}
dockerfile = cast(str, metadata.get("Dockerfile", ""))
Expand All @@ -490,4 +494,8 @@ def _is_layer_buildable(layer: LayerVersion):
if isinstance(layer.codeuri, str) and layer.codeuri.endswith(".zip"):
LOG.debug("Skip building zip layer: %s", layer.full_path)
return False
# skip build the functions that marked as skip-build
if layer.skip_build:
LOG.debug("Skip building pre-built layer: %s", layer.full_path)
return False
return True
20 changes: 20 additions & 0 deletions samcli/lib/providers/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
InvalidFunctionPropertyType,
)
from samcli.lib.providers.sam_base_provider import SamBaseProvider
from samcli.lib.samlib.resource_metadata_normalizer import SAM_METADATA_SKIP_BUILD_KEY
from samcli.lib.utils.architecture import X86_64

if TYPE_CHECKING: # pragma: no cover
Expand Down Expand Up @@ -83,6 +84,15 @@ def full_path(self) -> str:
"""
return get_full_path(self.stack_path, self.function_id)

@property
def skip_build(self) -> bool:
"""
Check if the function metadata contains SkipBuild property to determines if SAM should skip building this
resource. It means that the customer is building the Lambda function code outside SAM, and the provided code
path is already built.
"""
return self.metadata.get(SAM_METADATA_SKIP_BUILD_KEY, False) if self.metadata else False

def get_build_dir(self, build_root_dir: str) -> str:
"""
Return the artifact directory based on the build root dir
Expand Down Expand Up @@ -192,6 +202,7 @@ def __init__(

self._build_architecture = cast(str, metadata.get("BuildArchitecture", X86_64))
self._compatible_architectures = compatible_architectures
self._skip_build = bool(metadata.get(SAM_METADATA_SKIP_BUILD_KEY, False))

@staticmethod
def _compute_layer_version(is_defined_within_template: bool, arn: str) -> Optional[int]:
Expand Down Expand Up @@ -260,6 +271,15 @@ def _compute_layer_name(is_defined_within_template: bool, arn: str) -> str:
def stack_path(self) -> str:
return self._stack_path

@property
def skip_build(self) -> bool:
"""
Check if the function metadata contains SkipBuild property to determines if SAM should skip building this
resource. It means that the customer is building the Lambda function code outside SAM, and the provided code
path is already built.
"""
return self._skip_build

@property
def arn(self) -> str:
return self._arn
Expand Down
17 changes: 15 additions & 2 deletions samcli/lib/samlib/resource_metadata_normalizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
SAM_METADATA_DOCKER_CONTEXT_KEY = "DockerContext"
SAM_METADATA_DOCKER_BUILD_ARGS_KEY = "DockerBuildArgs"

ASSET_BUNDLED_METADATA_KEY = "aws:asset:is-bundled"
SAM_METADATA_SKIP_BUILD_KEY = "SkipBuild"

LOG = logging.getLogger(__name__)


Expand All @@ -45,7 +48,7 @@ def normalize(template_dict):

if asset_property == IMAGE_ASSET_PROPERTY:
asset_metadata = ResourceMetadataNormalizer._extract_image_asset_metadata(resource_metadata)
ResourceMetadataNormalizer._update_resource_image_asset_metadata(resource_metadata, asset_metadata)
ResourceMetadataNormalizer._update_resource_metadata(resource_metadata, asset_metadata)
# For image-type functions, the asset path is expected to be the name of the Docker image.
# When building, we set the name of the image to be the logical id of the function.
asset_path = logical_id.lower()
Expand All @@ -54,6 +57,16 @@ def normalize(template_dict):

ResourceMetadataNormalizer._replace_property(asset_property, asset_path, resource, logical_id)

# Set SkipBuild metadata iff is-bundled metadata exists, and value is True
skip_build = resource_metadata.get(ASSET_BUNDLED_METADATA_KEY, False)
if skip_build:
ResourceMetadataNormalizer._update_resource_metadata(
resource_metadata,
{
SAM_METADATA_SKIP_BUILD_KEY: True,
},
)

@staticmethod
def _replace_property(property_key, property_value, resource, logical_id):
"""
Expand Down Expand Up @@ -115,7 +128,7 @@ def _extract_image_asset_metadata(metadata):
}

@staticmethod
def _update_resource_image_asset_metadata(metadata, updated_values):
def _update_resource_metadata(metadata, updated_values):
"""
Update the metadata values for image-type lambda functions

Expand Down
2 changes: 2 additions & 0 deletions tests/integration/buildcmd/build_integ_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ def _verify_invoke_built_function(self, template_path, function_logical_id, over
overrides,
]

LOG.info("Running invoke Command: {}".format(cmdlist))

process_execute = run_command(cmdlist)
process_execute.process.wait()

Expand Down
100 changes: 100 additions & 0 deletions tests/integration/buildcmd/test_build_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
from typing import Set
from unittest import skipIf

import jmespath
import docker
import jmespath
import pytest
from parameterized import parameterized, parameterized_class

from samcli.lib.utils import osutils
from samcli.yamlhelper import yaml_parse
from tests.testing_utils import (
IS_WINDOWS,
RUNNING_ON_CI,
Expand Down Expand Up @@ -136,6 +139,103 @@ def test_with_default_requirements(self, runtime):
)


@skipIf(
# Hits public ECR pull limitation, move it to canary tests
((not RUN_BY_CANARY) or (IS_WINDOWS and RUNNING_ON_CI) and not CI_OVERRIDE),
"Skip build tests on windows when running in CI unless overridden",
)
@parameterized_class(
("template", "SKIPPED_FUNCTION_LOGICAL_ID", "src_code_path", "src_code_prop", "metadata_key"),
[
("template_function_flagged_to_skip_build.yaml", "SkippedFunction", "PreBuiltPython", "CodeUri", None),
("template_cfn_function_flagged_to_skip_build.yaml", "SkippedFunction", "PreBuiltPython", "Code", None),
(
"cdk_v1_synthesized_template_python_function_construct.json",
"SkippedFunctionDA0220D7",
"asset.7023fd47c81480184154c6e0e870d6920c50e35d8fae977873016832e127ded9",
None,
"aws:asset:path",
),
(
"cdk_v1_synthesized_template_function_construct_with_skip_build_metadata.json",
"SkippedFunctionDA0220D7",
"asset.7023fd47c81480184154c6e0e870d6920c50e35d8fae977873016832e127ded9",
None,
"aws:asset:path",
),
(
"cdk_v2_synthesized_template_python_function_construct.json",
"SkippedFunctionDA0220D7",
"asset.7023fd47c81480184154c6e0e870d6920c50e35d8fae977873016832e127ded9",
None,
"aws:asset:path",
),
(
"cdk_v2_synthesized_template_function_construct_with_skip_build_metadata.json",
"RandomSpaceFunction4F8564D0",
"asset.7023fd47c81480184154c6e0e870d6920c50e35d8fae977873016832e127ded9",
None,
"aws:asset:path",
),
],
)
class TestSkipBuildingFlaggedFunctions(BuildIntegPythonBase):
template = "template_cfn_function_flagged_to_skip_build.yaml"
SKIPPED_FUNCTION_LOGICAL_ID = "SkippedFunction"
src_code_path = "PreBuiltPython"
src_code_prop = "Code"
metadata_key = None

@pytest.mark.flaky(reruns=3)
def test_with_default_requirements(self):
self._validate_skipped_built_function(
self.default_build_dir,
self.SKIPPED_FUNCTION_LOGICAL_ID,
self.test_data_path,
self.src_code_path,
self.src_code_prop,
self.metadata_key,
)

def _validate_skipped_built_function(
self, build_dir, skipped_function_logical_id, relative_path, src_code_path, src_code_prop, metadata_key
):

cmdlist = self.get_command_list()

LOG.info("Running Command: {}".format(cmdlist))
run_command(cmdlist, cwd=self.working_dir)

self.assertTrue(build_dir.exists(), "Build directory should be created")

build_dir_files = os.listdir(str(build_dir))
self.assertNotIn(skipped_function_logical_id, build_dir_files)

expected_value = os.path.relpath(
os.path.normpath(os.path.join(str(relative_path), src_code_path)),
str(self.default_build_dir),
)

with open(self.built_template, "r") as fp:
template_dict = yaml_parse(fp.read())
if src_code_prop:
self.assertEqual(
expected_value,
jmespath.search(
f"Resources.{skipped_function_logical_id}.Properties.{src_code_prop}", template_dict
),
)
if metadata_key:
metadata = jmespath.search(f"Resources.{skipped_function_logical_id}.Metadata", template_dict)
metadata = metadata if metadata else {}
self.assertEqual(expected_value, metadata.get(metadata_key, ""))
expected = "Hello World"
if not SKIP_DOCKER_TESTS:
self._verify_invoke_built_function(
self.built_template, skipped_function_logical_id, self._make_parameter_override_arg({}), expected
)


@skipIf(
((IS_WINDOWS and RUNNING_ON_CI) and not CI_OVERRIDE),
"Skip build tests on windows when running in CI unless overridden",
Expand Down
Empty file.
2 changes: 2 additions & 0 deletions tests/integration/testdata/buildcmd/PreBuiltPython/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def handler(event, context):
return "Hello World"
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def handler(event, context):
return "Hello World"
Loading