diff --git a/samcli/commands/build/build_context.py b/samcli/commands/build/build_context.py index 47d412511e..a6d6b69102 100644 --- a/samcli/commands/build/build_context.py +++ b/samcli/commands/build/build_context.py @@ -5,44 +5,46 @@ import os import pathlib import shutil -from typing import Dict, Optional, List, Tuple, cast +from typing import Dict, Optional, List, Tuple import click -from samcli.commands.build.utils import prompt_user_to_enable_mount_with_write_if_needed, MountMode -from samcli.lib.build.bundler import EsbuildBundlerManager -from samcli.lib.providers.sam_api_provider import SamApiProvider -from samcli.lib.telemetry.event import EventTracker -from samcli.lib.utils.packagetype import IMAGE - -from samcli.commands._utils.template import get_template_data +from samcli.commands._utils.constants import DEFAULT_BUILD_DIR from samcli.commands._utils.experimental import ExperimentalFlag, prompt_experimental +from samcli.commands._utils.template import ( + get_template_data, + move_template, +) from samcli.commands.build.exceptions import InvalidBuildDirException, MissingBuildMethodException +from samcli.commands.build.utils import prompt_user_to_enable_mount_with_write_if_needed, MountMode +from samcli.commands.exceptions import UserException from samcli.lib.bootstrap.nested_stack.nested_stack_manager import NestedStackManager +from samcli.lib.build.app_builder import ( + ApplicationBuilder, + BuildError, + UnsupportedBuilderLibraryVersionError, + ApplicationBuildResult, +) from samcli.lib.build.build_graph import DEFAULT_DEPENDENCIES_DIR +from samcli.lib.build.bundler import EsbuildBundlerManager +from samcli.lib.build.exceptions import ( + BuildInsideContainerError, + InvalidBuildGraphException, +) +from samcli.lib.build.workflow_config import UnsupportedRuntimeException from samcli.lib.intrinsic_resolver.intrinsics_symbol_table import IntrinsicsSymbolTable from samcli.lib.providers.provider import ResourcesToBuildCollector, Stack, Function, LayerVersion +from samcli.lib.providers.sam_api_provider import SamApiProvider from samcli.lib.providers.sam_function_provider import SamFunctionProvider from samcli.lib.providers.sam_layer_provider import SamLayerProvider from samcli.lib.providers.sam_stack_provider import SamLocalStackProvider +from samcli.lib.telemetry.event import EventTracker from samcli.lib.utils.osutils import BUILD_DIR_PERMISSIONS from samcli.local.docker.manager import ContainerManager -from samcli.local.lambdafn.exceptions import ResourceNotFound -from samcli.lib.build.exceptions import BuildInsideContainerError - -from samcli.commands.exceptions import UserException - -from samcli.lib.build.app_builder import ( - ApplicationBuilder, - BuildError, - UnsupportedBuilderLibraryVersionError, - ApplicationBuildResult, +from samcli.local.lambdafn.exceptions import ( + FunctionNotFound, + ResourceNotFound, ) -from samcli.commands._utils.constants import DEFAULT_BUILD_DIR -from samcli.lib.build.workflow_config import UnsupportedRuntimeException -from samcli.local.lambdafn.exceptions import FunctionNotFound -from samcli.commands._utils.template import move_template -from samcli.lib.build.exceptions import InvalidBuildGraphException LOG = logging.getLogger(__name__) @@ -586,7 +588,7 @@ def collect_all_build_resources(self) -> ResourcesToBuildCollector: [ f for f in self.function_provider.get_all() - if (f.name not in excludes) and BuildContext._is_function_buildable(f) + if (f.name not in excludes) and f.function_build_info.is_buildable() ] ) result.add_layers( @@ -650,34 +652,6 @@ def _collect_single_buildable_layer( resource_collector.add_layer(layer) - @staticmethod - def _is_function_buildable(function: Function): - # no need to build inline functions - if function.inlinecode: - LOG.debug("Skip building inline function: %s", function.full_path) - return False - # no need to build functions that are already packaged as a zip file - 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", "")) - docker_context = cast(str, metadata.get("DockerContext", "")) - if not dockerfile or not docker_context: - LOG.debug( - "Skip Building %s function, as it is missing either Dockerfile or DockerContext " - "metadata properties.", - function.full_path, - ) - return False - return True - @staticmethod def is_layer_buildable(layer: LayerVersion): # if build method is not specified, it is not buildable diff --git a/samcli/lib/providers/provider.py b/samcli/lib/providers/provider.py index 3a2a9039ac..fc87e5bc81 100644 --- a/samcli/lib/providers/provider.py +++ b/samcli/lib/providers/provider.py @@ -7,6 +7,7 @@ import os import posixpath from collections import namedtuple +from enum import Enum, auto from typing import TYPE_CHECKING, Any, Dict, Iterator, List, NamedTuple, Optional, Set, Union, cast from samcli.commands.local.cli_common.user_exceptions import ( @@ -21,6 +22,7 @@ ResourceMetadataNormalizer, ) from samcli.lib.utils.architecture import X86_64 +from samcli.lib.utils.packagetype import IMAGE if TYPE_CHECKING: # pragma: no cover # avoid circular import, https://docs.python.org/3/library/typing.html#typing.TYPE_CHECKING @@ -35,6 +37,27 @@ CORS_MAX_AGE_HEADER = "Access-Control-Max-Age" +class FunctionBuildInfo(Enum): + """ + Represents information about function's build, see values for details + """ + + # buildable + BuildableZip = auto(), "Regular ZIP function which can be build with SAM CLI" + BuildableImage = auto(), "Regular IMAGE function which can be build with SAM CLI" + # non-buildable + InlineCode = auto(), "A ZIP function which has inline code, non buildable" + PreZipped = auto(), "A ZIP function which points to a .zip file, non buildable" + SkipBuild = auto(), "A Function which is denoted with SkipBuild in metadata, non buildable" + NonBuildableImage = auto(), "An IMAGE function which is missing some information to build, non buildable" + + def is_buildable(self) -> bool: + """ + Returns whether this build info can be buildable nor not + """ + return self in {FunctionBuildInfo.BuildableZip, FunctionBuildInfo.BuildableImage} + + class Function(NamedTuple): """ Named Tuple to representing the properties of a Lambda Function @@ -82,6 +105,8 @@ class Function(NamedTuple): architectures: Optional[List[str]] # The function url configuration function_url_config: Optional[Dict] + # FunctionBuildInfo see implementation doc for its details + function_build_info: FunctionBuildInfo # The path of the stack relative to the root stack, it is empty for functions in root stack stack_path: str = "" # Configuration for runtime management. Includes the fields `UpdateRuntimeOn` and `RuntimeVersionArn` (optional). @@ -105,7 +130,7 @@ def skip_build(self) -> bool: 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 + return get_skip_build(self.metadata) def get_build_dir(self, build_root_dir: str) -> str: """ @@ -872,6 +897,52 @@ def get_unique_resource_ids( return output_resource_ids +def get_skip_build(metadata: Optional[Dict]) -> bool: + """ + Returns the value of SkipBuild property from Metadata, False if it is not defined + """ + return metadata.get(SAM_METADATA_SKIP_BUILD_KEY, False) if metadata else False + + +def get_function_build_info( + full_path: str, + packagetype: str, + inlinecode: Optional[str], + codeuri: Optional[str], + metadata: Optional[Dict], +) -> FunctionBuildInfo: + """ + Populates FunctionBuildInfo from the given information. + """ + if inlinecode: + LOG.debug("Skip building inline function: %s", full_path) + return FunctionBuildInfo.InlineCode + + if isinstance(codeuri, str) and codeuri.endswith(".zip"): + LOG.debug("Skip building zip function: %s", full_path) + return FunctionBuildInfo.PreZipped + + if get_skip_build(metadata): + LOG.debug("Skip building pre-built function: %s", full_path) + return FunctionBuildInfo.SkipBuild + + if packagetype == IMAGE: + metadata = metadata or {} + dockerfile = cast(str, metadata.get("Dockerfile", "")) + docker_context = cast(str, metadata.get("DockerContext", "")) + + if not dockerfile or not docker_context: + LOG.debug( + "Skip Building %s function, as it is missing either Dockerfile or DockerContext " + "metadata properties.", + full_path, + ) + return FunctionBuildInfo.NonBuildableImage + return FunctionBuildInfo.BuildableImage + + return FunctionBuildInfo.BuildableZip + + def _get_build_dir(resource: Union[Function, LayerVersion], build_root: str) -> str: """ Return the build directory to place build artifact diff --git a/samcli/lib/providers/sam_function_provider.py b/samcli/lib/providers/sam_function_provider.py index 728eb9a164..b7adfa597a 100644 --- a/samcli/lib/providers/sam_function_provider.py +++ b/samcli/lib/providers/sam_function_provider.py @@ -20,7 +20,7 @@ ) from ..build.constants import DEPRECATED_RUNTIMES -from .provider import Function, LayerVersion, Stack +from .provider import Function, LayerVersion, Stack, get_full_path, get_function_build_info from .sam_base_provider import SamBaseProvider from .sam_stack_provider import SamLocalStackProvider @@ -444,12 +444,17 @@ def _build_function_configuration( LOG.debug("--base-dir is not presented, adjusting uri %s relative to %s", codeuri, stack.location) codeuri = SamLocalStackProvider.normalize_resource_path(stack.location, codeuri) + package_type = resource_properties.get("PackageType", ZIP) + function_build_info = get_function_build_info( + get_full_path(stack.stack_path, function_id), package_type, inlinecode, codeuri, metadata + ) + return Function( stack_path=stack.stack_path, function_id=function_id, name=name, functionname=resource_properties.get("FunctionName", name), - packagetype=resource_properties.get("PackageType", ZIP), + packagetype=package_type, runtime=resource_properties.get("Runtime"), memory=resource_properties.get("MemorySize"), timeout=resource_properties.get("Timeout"), @@ -467,6 +472,7 @@ def _build_function_configuration( architectures=resource_properties.get("Architectures", None), function_url_config=resource_properties.get("FunctionUrlConfig"), runtime_management_config=resource_properties.get("RuntimeManagementConfig"), + function_build_info=function_build_info, ) @staticmethod diff --git a/samcli/lib/sync/flows/zip_function_sync_flow.py b/samcli/lib/sync/flows/zip_function_sync_flow.py index e7f738d209..eb16db4eee 100644 --- a/samcli/lib/sync/flows/zip_function_sync_flow.py +++ b/samcli/lib/sync/flows/zip_function_sync_flow.py @@ -3,6 +3,7 @@ import hashlib import logging import os +import shutil import tempfile import uuid from contextlib import ExitStack @@ -226,3 +227,28 @@ def _get_function_api_calls(self) -> List[ResourceAPICall]: @staticmethod def _combine_dependencies() -> bool: return True + + +class ZipFunctionSyncFlowSkipBuildZipFile(ZipFunctionSyncFlow): + """ + Alternative implementation for ZipFunctionSyncFlow, which uses pre-built zip file for running sync flow + """ + + def gather_resources(self) -> None: + self._zip_file = os.path.join(tempfile.gettempdir(), f"data-{uuid.uuid4().hex}") + shutil.copy2(cast(str, self._function.codeuri), self._zip_file) + LOG.debug("%sCreated artifact ZIP file: %s", self.log_prefix, self._zip_file) + self._local_sha = file_checksum(self._zip_file, hashlib.sha256()) + + +class ZipFunctionSyncFlowSkipBuildDirectory(ZipFunctionSyncFlow): + """ + Alternative implementation for ZipFunctionSyncFlow, which doesn't build function but zips folder directly + since function is annotated with SkipBuild inside its Metadata + """ + + def gather_resources(self) -> None: + zip_file_path = os.path.join(tempfile.gettempdir(), f"data-{uuid.uuid4().hex}") + self._zip_file = make_zip_with_lambda_permissions(zip_file_path, self._function.codeuri) + LOG.debug("%sCreated artifact ZIP file: %s", self.log_prefix, self._zip_file) + self._local_sha = file_checksum(cast(str, self._zip_file), hashlib.sha256()) diff --git a/samcli/lib/sync/sync_flow_factory.py b/samcli/lib/sync/sync_flow_factory.py index baa887a803..f9704bf95c 100644 --- a/samcli/lib/sync/sync_flow_factory.py +++ b/samcli/lib/sync/sync_flow_factory.py @@ -1,6 +1,6 @@ """SyncFlow Factory for creating SyncFlows based on resource types""" import logging -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, cast +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, cast from botocore.exceptions import ClientError @@ -9,7 +9,7 @@ from samcli.lib.bootstrap.nested_stack.nested_stack_manager import NestedStackManager from samcli.lib.build.app_builder import ApplicationBuildResult from samcli.lib.package.utils import is_local_folder, is_zip_file -from samcli.lib.providers.provider import ResourceIdentifier, Stack, get_resource_by_id +from samcli.lib.providers.provider import Function, FunctionBuildInfo, ResourceIdentifier, Stack from samcli.lib.sync.flows.auto_dependency_layer_sync_flow import AutoDependencyLayerParentSyncFlow from samcli.lib.sync.flows.function_sync_flow import FunctionSyncFlow from samcli.lib.sync.flows.http_api_sync_flow import HttpApiSyncFlow @@ -21,7 +21,11 @@ ) from samcli.lib.sync.flows.rest_api_sync_flow import RestApiSyncFlow from samcli.lib.sync.flows.stepfunctions_sync_flow import StepFunctionsSyncFlow -from samcli.lib.sync.flows.zip_function_sync_flow import ZipFunctionSyncFlow +from samcli.lib.sync.flows.zip_function_sync_flow import ( + ZipFunctionSyncFlow, + ZipFunctionSyncFlowSkipBuildDirectory, + ZipFunctionSyncFlowSkipBuildZipFile, +) from samcli.lib.sync.sync_flow import SyncFlow from samcli.lib.utils.boto_utils import ( get_boto_client_provider_with_config, @@ -150,16 +154,35 @@ def load_physical_id_mapping(self) -> None: def _create_lambda_flow( self, resource_identifier: ResourceIdentifier, - resource: Dict[str, Any], application_build_result: Optional[ApplicationBuildResult], ) -> Optional[FunctionSyncFlow]: - resource_properties = resource.get("Properties", dict()) - package_type = resource_properties.get("PackageType", ZIP) - runtime = resource_properties.get("Runtime") - if package_type == ZIP: - # only return auto dependency layer sync if runtime is supported - if self._auto_dependency_layer and NestedStackManager.is_runtime_supported(runtime): - return AutoDependencyLayerParentSyncFlow( + function = self._build_context.function_provider.get(str(resource_identifier)) + if not function: + LOG.warning("Can't find function resource with '%s' logical id", str(resource_identifier)) + return None + + if function.packagetype == ZIP: + return self._create_zip_type_lambda_flow(resource_identifier, application_build_result, function) + if function.packagetype == IMAGE: + return self._create_image_type_lambda_flow(resource_identifier, application_build_result, function) + return None + + def _create_zip_type_lambda_flow( + self, + resource_identifier: ResourceIdentifier, + application_build_result: Optional[ApplicationBuildResult], + function: Function, + ) -> Optional[FunctionSyncFlow]: + if not function.function_build_info.is_buildable(): + if function.function_build_info == FunctionBuildInfo.InlineCode: + LOG.debug( + "No need to create sync flow for a function with InlineCode '%s' resource", str(resource_identifier) + ) + return None + if function.function_build_info == FunctionBuildInfo.PreZipped: + # if codeuri points to zip file, use ZipFunctionSyncFlowSkipBuildZipFile sync flow + LOG.debug("Creating ZipFunctionSyncFlowSkipBuildZipFile for '%s' resource", resource_identifier) + return ZipFunctionSyncFlowSkipBuildZipFile( str(resource_identifier), self._build_context, self._deploy_context, @@ -169,17 +192,22 @@ def _create_lambda_flow( application_build_result, ) - return ZipFunctionSyncFlow( - str(resource_identifier), - self._build_context, - self._deploy_context, - self._sync_context, - self._physical_id_mapping, - self._stacks, - application_build_result, - ) - if package_type == IMAGE: - return ImageFunctionSyncFlow( + if function.function_build_info == FunctionBuildInfo.SkipBuild: + # if function is marked with SkipBuild, use ZipFunctionSyncFlowSkipBuildDirectory sync flow + LOG.debug("Creating ZipFunctionSyncFlowSkipBuildDirectory for '%s' resource", resource_identifier) + return ZipFunctionSyncFlowSkipBuildDirectory( + str(resource_identifier), + self._build_context, + self._deploy_context, + self._sync_context, + self._physical_id_mapping, + self._stacks, + application_build_result, + ) + + # only return auto dependency layer sync if runtime is supported + if self._auto_dependency_layer and NestedStackManager.is_runtime_supported(function.runtime): + return AutoDependencyLayerParentSyncFlow( str(resource_identifier), self._build_context, self._deploy_context, @@ -188,12 +216,40 @@ def _create_lambda_flow( self._stacks, application_build_result, ) - return None + + return ZipFunctionSyncFlow( + str(resource_identifier), + self._build_context, + self._deploy_context, + self._sync_context, + self._physical_id_mapping, + self._stacks, + application_build_result, + ) + + def _create_image_type_lambda_flow( + self, + resource_identifier: ResourceIdentifier, + application_build_result: Optional[ApplicationBuildResult], + function: Function, + ) -> Optional[FunctionSyncFlow]: + if not function.function_build_info.is_buildable(): + LOG.warning("Can't build image type function with '%s' logical id", str(resource_identifier)) + return None + + return ImageFunctionSyncFlow( + str(resource_identifier), + self._build_context, + self._deploy_context, + self._sync_context, + self._physical_id_mapping, + self._stacks, + application_build_result, + ) def _create_layer_flow( self, resource_identifier: ResourceIdentifier, - resource: Dict[str, Any], application_build_result: Optional[ApplicationBuildResult], ) -> Optional[SyncFlow]: layer = self._build_context.layer_provider.get(str(resource_identifier)) @@ -242,7 +298,6 @@ def _create_layer_flow( def _create_rest_api_flow( self, resource_identifier: ResourceIdentifier, - resource: Dict[str, Any], application_build_result: Optional[ApplicationBuildResult], ) -> SyncFlow: return RestApiSyncFlow( @@ -257,7 +312,6 @@ def _create_rest_api_flow( def _create_api_flow( self, resource_identifier: ResourceIdentifier, - resource: Dict[str, Any], application_build_result: Optional[ApplicationBuildResult], ) -> SyncFlow: return HttpApiSyncFlow( @@ -272,7 +326,6 @@ def _create_api_flow( def _create_stepfunctions_flow( self, resource_identifier: ResourceIdentifier, - resource: Dict[str, Any], application_build_result: Optional[ApplicationBuildResult], ) -> Optional[SyncFlow]: return StepFunctionsSyncFlow( @@ -285,7 +338,7 @@ def _create_stepfunctions_flow( ) GeneratorFunction = Callable[ - ["SyncFlowFactory", ResourceIdentifier, Dict[str, Any], Optional[ApplicationBuildResult]], Optional[SyncFlow] + ["SyncFlowFactory", ResourceIdentifier, Optional[ApplicationBuildResult]], Optional[SyncFlow] ] GENERATOR_MAPPING: Dict[str, GeneratorFunction] = { AWS_LAMBDA_FUNCTION: _create_lambda_flow, @@ -308,10 +361,7 @@ def _get_generator_mapping(self) -> Dict[str, GeneratorFunction]: # pylint: dis def create_sync_flow( self, resource_identifier: ResourceIdentifier, application_build_result: Optional[ApplicationBuildResult] = None ) -> Optional[SyncFlow]: - resource = get_resource_by_id(self._stacks, resource_identifier) generator = self._get_generator_function(resource_identifier) - if not generator or not resource: + if not generator: return None - return cast(SyncFlowFactory.GeneratorFunction, generator)( - self, resource_identifier, resource, application_build_result - ) + return cast(SyncFlowFactory.GeneratorFunction, generator)(self, resource_identifier, application_build_result) diff --git a/tests/integration/sync/test_sync_code.py b/tests/integration/sync/test_sync_code.py index 22210a241a..c462339336 100644 --- a/tests/integration/sync/test_sync_code.py +++ b/tests/integration/sync/test_sync_code.py @@ -702,3 +702,93 @@ def test_sync_code_layer(self, layer_path, layer_logical_id, function_logical_id lambda_response = json.loads(self._get_lambda_response(lambda_function)) self.assertIn("extra_message", lambda_response) self.assertEqual(lambda_response.get("message_from_layer"), expected_value) + + +class TestFunctionWithPreZippedCodeUri(TestSyncCodeBase): + template = "template-pre-zipped.yaml" + folder = "code" + dependency_layer = False + + def test_pre_zipped_function(self): + # CFN Api call here to collect all the stack resources + self.stack_resources = self._get_stacks(TestSyncCodeBase.stack_name) + lambda_functions = self.stack_resources.get(AWS_LAMBDA_FUNCTION) + + # first verify current state of the function + for lambda_function in lambda_functions: + if lambda_function == "HelloWorldFunction": + lambda_response = json.loads(self._get_lambda_response(lambda_function)) + self.assertIn("message", lambda_response) + self.assertEqual(lambda_response.get("message"), "hello world") + + # update function code with new values + self.update_file( + self.test_data_path.joinpath(self.folder, "after", "pre_zipped_function", "app.zip"), + self.test_data_path.joinpath(self.folder, "before", "pre_zipped_function", "app.zip"), + ) + + # Run code sync + sync_command_list = self.get_sync_command_list( + template_file=TestSyncCodeBase.template_path, + code=True, + watch=False, + resource_list=["AWS::Serverless::Function"], + stack_name=TestSyncCodeBase.stack_name, + image_repository=self.ecr_repo_name, + s3_prefix=self.s3_prefix, + kms_key_id=self.kms_key, + ) + sync_process_execute = run_command_with_input(sync_command_list, "y\n".encode(), cwd=self.test_data_path) + self.assertEqual(sync_process_execute.process.returncode, 0) + + # Verify changed lambda response + for lambda_function in lambda_functions: + if lambda_function == "HelloWorldFunction": + lambda_response = json.loads(self._get_lambda_response(lambda_function)) + self.assertIn("message", lambda_response) + self.assertEqual(lambda_response.get("message"), "hello mars") + + +class TestFunctionWithSkipBuild(TestSyncCodeBase): + template = "template-skip-build.yaml" + folder = "code" + dependency_layer = False + + def test_skip_build(self): + # CFN Api call here to collect all the stack resources + self.stack_resources = self._get_stacks(TestSyncCodeBase.stack_name) + lambda_functions = self.stack_resources.get(AWS_LAMBDA_FUNCTION) + + # first verify current state of the function + for lambda_function in lambda_functions: + if lambda_function == "HelloWorldFunction": + lambda_response = json.loads(self._get_lambda_response(lambda_function)) + self.assertIn("message", lambda_response) + self.assertEqual(lambda_response.get("message"), "hello world") + + # update function code with new values + self.update_file( + self.test_data_path.joinpath(self.folder, "after", "python_function_no_deps", "app_without_numpy.py"), + self.test_data_path.joinpath(self.folder, "before", "python_function_no_deps", "app.py"), + ) + + # Run code sync + sync_command_list = self.get_sync_command_list( + template_file=TestSyncCodeBase.template_path, + code=True, + watch=False, + resource_list=["AWS::Serverless::Function"], + stack_name=TestSyncCodeBase.stack_name, + image_repository=self.ecr_repo_name, + s3_prefix=self.s3_prefix, + kms_key_id=self.kms_key, + ) + sync_process_execute = run_command_with_input(sync_command_list, "y\n".encode(), cwd=self.test_data_path) + self.assertEqual(sync_process_execute.process.returncode, 0) + + # Verify changed lambda response + for lambda_function in lambda_functions: + if lambda_function == "HelloWorldFunction": + lambda_response = json.loads(self._get_lambda_response(lambda_function)) + self.assertIn("message", lambda_response) + self.assertEqual(lambda_response.get("message"), "hello mars") diff --git a/tests/integration/testdata/sync/code/after/pre_zipped_function/app.zip b/tests/integration/testdata/sync/code/after/pre_zipped_function/app.zip new file mode 100644 index 0000000000..756db1b579 Binary files /dev/null and b/tests/integration/testdata/sync/code/after/pre_zipped_function/app.zip differ diff --git a/tests/integration/testdata/sync/code/before/pre_zipped_function/app.zip b/tests/integration/testdata/sync/code/before/pre_zipped_function/app.zip new file mode 100644 index 0000000000..20119328ed Binary files /dev/null and b/tests/integration/testdata/sync/code/before/pre_zipped_function/app.zip differ diff --git a/tests/integration/testdata/sync/code/before/template-pre-zipped.yaml b/tests/integration/testdata/sync/code/before/template-pre-zipped.yaml new file mode 100644 index 0000000000..75801e5b63 --- /dev/null +++ b/tests/integration/testdata/sync/code/before/template-pre-zipped.yaml @@ -0,0 +1,14 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 + +Globals: + Function: + Timeout: 10 + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: pre_zipped_function/app.zip + Handler: app.lambda_handler + Runtime: python3.10 diff --git a/tests/integration/testdata/sync/code/before/template-skip-build.yaml b/tests/integration/testdata/sync/code/before/template-skip-build.yaml new file mode 100644 index 0000000000..ec7b417280 --- /dev/null +++ b/tests/integration/testdata/sync/code/before/template-skip-build.yaml @@ -0,0 +1,16 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 + +Globals: + Function: + Timeout: 10 + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: python_function_no_deps/ + Handler: app.lambda_handler + Runtime: python3.10 + Metadata: + SkipBuild: true diff --git a/tests/unit/commands/buildcmd/test_build_context.py b/tests/unit/commands/buildcmd/test_build_context.py index 3cf556a9a1..417ce473e3 100644 --- a/tests/unit/commands/buildcmd/test_build_context.py +++ b/tests/unit/commands/buildcmd/test_build_context.py @@ -4,14 +4,9 @@ from parameterized import parameterized -from samcli.commands.build.utils import MountMode -from samcli.lib.build.build_graph import DEFAULT_DEPENDENCIES_DIR -from samcli.lib.build.bundler import EsbuildBundlerManager -from samcli.lib.utils.osutils import BUILD_DIR_PERMISSIONS -from samcli.lib.utils.packagetype import ZIP, IMAGE -from samcli.local.lambdafn.exceptions import ResourceNotFound from samcli.commands.build.build_context import BuildContext from samcli.commands.build.exceptions import InvalidBuildDirException, MissingBuildMethodException +from samcli.commands.build.utils import MountMode from samcli.commands.exceptions import UserException from samcli.lib.build.app_builder import ( BuildError, @@ -19,8 +14,14 @@ BuildInsideContainerError, ApplicationBuildResult, ) +from samcli.lib.build.build_graph import DEFAULT_DEPENDENCIES_DIR +from samcli.lib.build.bundler import EsbuildBundlerManager from samcli.lib.build.workflow_config import UnsupportedRuntimeException +from samcli.lib.providers.provider import Function, get_function_build_info +from samcli.lib.utils.osutils import BUILD_DIR_PERMISSIONS +from samcli.lib.utils.packagetype import ZIP, IMAGE from samcli.local.lambdafn.exceptions import FunctionNotFound +from samcli.local.lambdafn.exceptions import ResourceNotFound class DeepWrap(Exception): @@ -36,29 +37,46 @@ def __init__(self, name, build_method, codeuri="layer_src", skip_build=False): self.skip_build = skip_build -class DummyFunction: - def __init__( - self, - name, - layers=[], - inlinecode=None, - codeuri="src", - imageuri="image:latest", - packagetype=ZIP, - metadata=None, - skip_build=False, - runtime=None, - ): - self.name = name - self.layers = layers - self.inlinecode = inlinecode - self.codeuri = codeuri - self.imageuri = imageuri - self.full_path = Mock() - self.packagetype = packagetype - self.metadata = metadata if metadata else {} - self.skip_build = skip_build - self.runtime = runtime +def get_function( + name, + layers=None, + inlinecode=None, + codeuri="src", + imageuri="image:latest", + packagetype=ZIP, + metadata=None, + skip_build=False, + runtime=None, +) -> Function: + layers = layers or [] + metadata = metadata or {} + if skip_build: + metadata["SkipBuild"] = "True" + return Function( + function_id=name, + functionname=name, + name=name, + runtime=runtime, + memory=None, + timeout=None, + handler=None, + imageuri=imageuri, + packagetype=packagetype, + imageconfig=None, + codeuri=codeuri, + environment=None, + rolearn=None, + layers=layers, + events=None, + metadata=metadata, + inlinecode=inlinecode, + codesign_config_arn=None, + architectures=None, + function_url_config=None, + stack_path="", + runtime_management_config=None, + function_build_info=get_function_build_info("stack/function", packagetype, inlinecode, codeuri, metadata), + ) class DummyStack: @@ -93,7 +111,7 @@ def test_must_setup_context( layer_provider_mock.get.return_value = layer1 layerprovider = SamLayerProviderMock.return_value = layer_provider_mock - function1 = DummyFunction("func1") + function1 = get_function("func1") func_provider_mock = Mock() func_provider_mock.get.return_value = function1 funcprovider = SamFunctionProviderMock.return_value = func_provider_mock @@ -171,7 +189,7 @@ def test_must_fail_with_illegal_identifier( get_buildable_stacks_mock.return_value = ([stack], []) func_provider_mock = Mock() func_provider_mock.get.return_value = None - func_provider_mock.get_all.return_value = [DummyFunction("func1"), DummyFunction("func2")] + func_provider_mock.get_all.return_value = [get_function("func1"), get_function("func2")] funcprovider = SamFunctionProviderMock.return_value = func_provider_mock layer_provider_mock = Mock() @@ -288,7 +306,7 @@ def test_must_return_buildable_dependent_layer_when_function_is_build( layer_provider_mock.get.return_value = layer1 layerprovider = SamLayerProviderMock.return_value = layer_provider_mock - func1 = DummyFunction("func1", [layer1, layer2]) + func1 = get_function("func1", [layer1, layer2]) func_provider_mock = Mock() func_provider_mock.get.return_value = func1 funcprovider = SamFunctionProviderMock.return_value = func_provider_mock @@ -405,15 +423,15 @@ def test_must_return_many_functions_to_build( stack = Mock() stack.template_dict = template_dict get_buildable_stacks_mock.return_value = ([stack], []) - func1 = DummyFunction("func1") - func2 = DummyFunction("func2") - func3_skipped = DummyFunction("func3", inlinecode="def handler(): pass", codeuri=None) - func4_skipped = DummyFunction("func4", codeuri="packaged_function.zip") - func5_skipped = DummyFunction("func5", codeuri=None, packagetype=IMAGE) - func6 = DummyFunction( + func1 = get_function("func1") + func2 = get_function("func2") + func3_skipped = get_function("func3", inlinecode="def handler(): pass", codeuri=None) + func4_skipped = get_function("func4", codeuri="packaged_function.zip") + func5_skipped = get_function("func5", codeuri=None, packagetype=IMAGE) + func6 = get_function( "func6", packagetype=IMAGE, metadata={"DockerContext": "/path", "Dockerfile": "DockerFile"} ) - func7_skipped = DummyFunction("func7", skip_build=True) + func7_skipped = get_function("func7", skip_build=True) func_provider_mock = Mock() func_provider_mock.get_all.return_value = [ @@ -521,7 +539,7 @@ def test_must_exclude_functions_from_build( stack.template_dict = template_dict get_buildable_stacks_mock.return_value = ([stack], []) - funcs = [DummyFunction(f) for f in resources_to_build] + funcs = [get_function(f) for f in resources_to_build] resource_to_exclude = None for f in funcs: if f.name == resource_identifier: @@ -682,7 +700,7 @@ def test_run_sync_build_context( layer_provider_mock = Mock() layer_provider_mock.get.return_value = layer1 layerprovider = SamLayerProviderMock.return_value = layer_provider_mock - func1 = DummyFunction("func1", [layer1]) + func1 = get_function("func1", [layer1]) func_provider_mock = Mock() func_provider_mock.get.return_value = func1 funcprovider = SamFunctionProviderMock.return_value = func_provider_mock @@ -943,7 +961,7 @@ def test_run_build_context( layer_provider_mock = Mock() layer_provider_mock.get.return_value = layer1 layerprovider = SamLayerProviderMock.return_value = layer_provider_mock - func1 = DummyFunction("func1", [layer1]) + func1 = get_function("func1", [layer1]) func_provider_mock = Mock() func_provider_mock.get.return_value = func1 funcprovider = SamFunctionProviderMock.return_value = func_provider_mock @@ -1115,7 +1133,7 @@ def test_must_catch_known_exceptions( layer_provider_mock = Mock() layer_provider_mock.get.return_value = layer1 layerprovider = SamLayerProviderMock.return_value = layer_provider_mock - func1 = DummyFunction("func1", [layer1]) + func1 = get_function("func1", [layer1]) func_provider_mock = Mock() func_provider_mock.get.return_value = func1 funcprovider = SamFunctionProviderMock.return_value = func_provider_mock @@ -1193,7 +1211,7 @@ def test_must_catch_function_not_found_exception( layer_provider_mock = Mock() layer_provider_mock.get.return_value = layer1 layerprovider = SamLayerProviderMock.return_value = layer_provider_mock - func1 = DummyFunction("func1", [layer1]) + func1 = get_function("func1", [layer1]) func_provider_mock = Mock() func_provider_mock.get.return_value = func1 funcprovider = SamFunctionProviderMock.return_value = func_provider_mock diff --git a/tests/unit/commands/buildcmd/test_utils.py b/tests/unit/commands/buildcmd/test_utils.py index 2a28a65002..20a79b3f0f 100644 --- a/tests/unit/commands/buildcmd/test_utils.py +++ b/tests/unit/commands/buildcmd/test_utils.py @@ -7,7 +7,7 @@ from samcli.commands.build.utils import prompt_user_to_enable_mount_with_write_if_needed from samcli.lib.utils.architecture import X86_64 from samcli.lib.utils.packagetype import ZIP, IMAGE -from samcli.lib.providers.provider import ResourcesToBuildCollector, Function, LayerVersion +from samcli.lib.providers.provider import ResourcesToBuildCollector, Function, LayerVersion, FunctionBuildInfo class TestBuildUtils(TestCase): @@ -52,6 +52,7 @@ def test_must_prompt_for_layer(self, prompt_mock): codesign_config_arn=None, function_url_config=None, runtime_management_config=None, + function_build_info=FunctionBuildInfo.BuildableZip, ) resources_to_build = ResourcesToBuildCollector() @@ -101,6 +102,7 @@ def test_must_prompt_for_function(self, prompt_mock): codesign_config_arn=None, function_url_config=None, runtime_management_config=None, + function_build_info=FunctionBuildInfo.BuildableZip, ) resources_to_build = ResourcesToBuildCollector() @@ -152,6 +154,7 @@ def test_must_prompt_for_function_with_specified_workflow(self, prompt_mock): codesign_config_arn=None, function_url_config=None, runtime_management_config=None, + function_build_info=FunctionBuildInfo.BuildableZip, ) resources_to_build = ResourcesToBuildCollector() @@ -201,6 +204,7 @@ def test_must_not_prompt_for_image_function(self, prompt_mock): codesign_config_arn=None, function_url_config=None, runtime_management_config=None, + function_build_info=FunctionBuildInfo.BuildableImage, ) resources_to_build = ResourcesToBuildCollector() @@ -250,6 +254,7 @@ def test_must_not_prompt(self, prompt_mock): codesign_config_arn=None, function_url_config=None, runtime_management_config=None, + function_build_info=FunctionBuildInfo.BuildableZip, ) resources_to_build = ResourcesToBuildCollector() diff --git a/tests/unit/commands/local/lib/test_local_lambda.py b/tests/unit/commands/local/lib/test_local_lambda.py index e71d006366..dc5f4384bd 100644 --- a/tests/unit/commands/local/lib/test_local_lambda.py +++ b/tests/unit/commands/local/lib/test_local_lambda.py @@ -10,7 +10,7 @@ from samcli.lib.utils.architecture import X86_64, ARM64 from samcli.commands.local.lib.local_lambda import LocalLambdaRunner -from samcli.lib.providers.provider import Function +from samcli.lib.providers.provider import Function, FunctionBuildInfo from samcli.lib.utils.packagetype import ZIP, IMAGE from samcli.local.docker.container import ContainerResponseException from samcli.local.lambdafn.exceptions import FunctionNotFound @@ -252,6 +252,7 @@ def test_must_work_with_override_values( codesign_config_arn=None, function_url_config=None, runtime_management_config=None, + function_build_info=FunctionBuildInfo.BuildableZip, ) self.local_lambda.env_vars_values = env_vars_values @@ -305,6 +306,7 @@ def test_must_not_work_with_invalid_override_values(self, env_vars_values, expec codesign_config_arn=None, function_url_config=None, runtime_management_config=None, + function_build_info=FunctionBuildInfo.BuildableZip, ) self.local_lambda.env_vars_values = env_vars_values @@ -348,6 +350,7 @@ def test_must_work_with_invalid_environment_variable(self, environment_variable, codesign_config_arn=None, function_url_config=None, runtime_management_config=None, + function_build_info=FunctionBuildInfo.BuildableZip, ) self.local_lambda.env_vars_values = {} @@ -427,6 +430,7 @@ def test_must_work(self, FunctionConfigMock, is_debugging_mock, resolve_code_pat codesign_config_arn=None, function_url_config=None, runtime_management_config=None, + function_build_info=FunctionBuildInfo.BuildableZip, ) config = "someconfig" @@ -495,6 +499,7 @@ def test_timeout_set_to_max_during_debugging( function_url_config=None, codesign_config_arn=None, runtime_management_config=None, + function_build_info=FunctionBuildInfo.BuildableZip, ) config = "someconfig" diff --git a/tests/unit/commands/local/lib/test_provider.py b/tests/unit/commands/local/lib/test_provider.py index d3b5260c51..6270c40d44 100644 --- a/tests/unit/commands/local/lib/test_provider.py +++ b/tests/unit/commands/local/lib/test_provider.py @@ -18,6 +18,7 @@ get_unique_resource_ids, Function, get_resource_full_path_by_id, + FunctionBuildInfo, ) from samcli.commands.local.cli_common.user_exceptions import ( InvalidLayerVersionArn, @@ -293,6 +294,7 @@ def setUp(self) -> None: None, [ARM64], None, + FunctionBuildInfo.BuildableZip, "stackpath", None, ) diff --git a/tests/unit/commands/local/lib/test_sam_function_provider.py b/tests/unit/commands/local/lib/test_sam_function_provider.py index aed832e090..6d2c8f716b 100644 --- a/tests/unit/commands/local/lib/test_sam_function_provider.py +++ b/tests/unit/commands/local/lib/test_sam_function_provider.py @@ -8,7 +8,7 @@ from samcli.lib.utils.architecture import X86_64, ARM64 from samcli.commands.local.cli_common.user_exceptions import InvalidLayerVersionArn -from samcli.lib.providers.provider import Function, LayerVersion, Stack +from samcli.lib.providers.provider import Function, LayerVersion, Stack, FunctionBuildInfo from samcli.lib.providers.sam_function_provider import SamFunctionProvider, RefreshableSamFunctionProvider from samcli.lib.providers.exceptions import InvalidLayerReference from samcli.lib.utils.packagetype import IMAGE, ZIP @@ -302,6 +302,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="", + function_build_info=FunctionBuildInfo.BuildableZip, ), ), ( @@ -328,6 +329,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="", + function_build_info=FunctionBuildInfo.InlineCode, ), ), ( @@ -354,6 +356,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="", + function_build_info=FunctionBuildInfo.BuildableZip, ), ), ("SamFunc2", None), # codeuri is a s3 location, ignored @@ -387,6 +390,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="", + function_build_info=FunctionBuildInfo.BuildableImage, ), ), ( @@ -418,6 +422,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="", + function_build_info=FunctionBuildInfo.BuildableImage, ), ), ("SamFuncWithImage3", None), # imageuri is ecr location, ignored @@ -450,6 +455,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="", + function_build_info=FunctionBuildInfo.BuildableImage, ), ), ( @@ -476,6 +482,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="", + function_build_info=FunctionBuildInfo.BuildableZip, ), ), ( @@ -506,6 +513,7 @@ def setUp(self): "UpdateRuntimeOn": "Manual", "RuntimeVersionArn": "arn:aws:lambda:us-east-1::runtime:python3.9::0af1966588ced06e3143ae720245c9b7aeaae213c6921c12c742a166679cc505", }, + function_build_info=FunctionBuildInfo.BuildableZip, ), ), ("LambdaFunc1", None), # codeuri is a s3 location, ignored @@ -538,6 +546,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="", + function_build_info=FunctionBuildInfo.BuildableImage, ), ), ( @@ -569,6 +578,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="", + function_build_info=FunctionBuildInfo.BuildableImage, ), ), ("LambdaFuncWithImage3", None), # imageuri is a ecr location, ignored @@ -601,6 +611,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="", + function_build_info=FunctionBuildInfo.BuildableImage, ), ), ( @@ -627,6 +638,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="", + function_build_info=FunctionBuildInfo.InlineCode, ), ), ( @@ -653,6 +665,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="", + function_build_info=FunctionBuildInfo.BuildableZip, ), ), ( @@ -679,6 +692,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="", + function_build_info=FunctionBuildInfo.BuildableZip, ), ), ( @@ -705,6 +719,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="", + function_build_info=FunctionBuildInfo.BuildableZip, ), ), ( @@ -731,6 +746,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="ChildStack", + function_build_info=FunctionBuildInfo.BuildableZip, ), ), ( @@ -757,6 +773,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="ChildStack", + function_build_info=FunctionBuildInfo.BuildableZip, ), ), ( @@ -788,6 +805,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="ChildStack", + function_build_info=FunctionBuildInfo.BuildableImage, ), ), ( @@ -814,6 +832,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="", + function_build_info=FunctionBuildInfo.BuildableZip, ), ), ( @@ -840,6 +859,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="", + function_build_info=FunctionBuildInfo.BuildableZip, ), ), ( @@ -872,6 +892,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="", + function_build_info=FunctionBuildInfo.BuildableZip, ), ), ( @@ -904,6 +925,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="", + function_build_info=FunctionBuildInfo.BuildableZip, ), ), ( @@ -936,6 +958,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="ChildStack", + function_build_info=FunctionBuildInfo.BuildableZip, ), ), ( @@ -968,6 +991,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="ChildStack", + function_build_info=FunctionBuildInfo.BuildableZip, ), ), ( @@ -1000,6 +1024,7 @@ def setUp(self): architectures=None, function_url_config=None, stack_path="ChildStack", + function_build_info=FunctionBuildInfo.BuildableZip, ), ), ( @@ -1453,6 +1478,7 @@ def test_must_convert_zip(self): architectures=[X86_64], function_url_config=None, stack_path=STACK_PATH, + function_build_info=FunctionBuildInfo.BuildableZip, ) result = SamFunctionProvider._convert_sam_function_resource(STACK, name, properties, ["Layer1", "Layer2"]) @@ -1495,6 +1521,7 @@ def test_must_convert_image(self): architectures=None, function_url_config=None, stack_path=STACK_PATH, + function_build_info=FunctionBuildInfo.NonBuildableImage, ) result = SamFunctionProvider._convert_sam_function_resource(STACK, name, properties, []) @@ -1527,6 +1554,7 @@ def test_must_skip_non_existent_properties(self): architectures=None, function_url_config=None, stack_path=STACK_PATH, + function_build_info=FunctionBuildInfo.BuildableZip, ) result = SamFunctionProvider._convert_sam_function_resource(STACK, name, properties, []) @@ -1573,6 +1601,7 @@ def test_must_use_inlinecode(self): architectures=[X86_64], function_url_config=None, stack_path=STACK_PATH, + function_build_info=FunctionBuildInfo.InlineCode, ) result = SamFunctionProvider._convert_sam_function_resource(STACK, name, properties, []) @@ -1613,6 +1642,7 @@ def test_must_prioritize_inlinecode(self): architectures=[ARM64], function_url_config=None, stack_path=STACK_PATH, + function_build_info=FunctionBuildInfo.InlineCode, ) result = SamFunctionProvider._convert_sam_function_resource(STACK, name, properties, []) @@ -1668,6 +1698,7 @@ def test_must_convert(self): architectures=None, function_url_config=None, stack_path=STACK_PATH, + function_build_info=FunctionBuildInfo.BuildableZip, ) result = SamFunctionProvider._convert_lambda_function_resource(STACK, name, properties, ["Layer1", "Layer2"]) @@ -1708,6 +1739,7 @@ def test_must_use_inlinecode(self): architectures=[ARM64], function_url_config=None, stack_path=STACK_PATH, + function_build_info=FunctionBuildInfo.InlineCode, ) result = SamFunctionProvider._convert_lambda_function_resource(STACK, name, properties, []) @@ -1740,6 +1772,7 @@ def test_must_skip_non_existent_properties(self): architectures=None, function_url_config=None, stack_path=STACK_PATH, + function_build_info=FunctionBuildInfo.BuildableZip, ) result = SamFunctionProvider._convert_lambda_function_resource(STACK, name, properties, []) @@ -1882,6 +1915,7 @@ def test_must_return_function_value(self): architectures=None, function_url_config=None, stack_path=STACK_PATH, + function_build_info=Mock(), ) provider.functions = {"func1": function} @@ -1912,6 +1946,7 @@ def test_found_by_different_ids(self): architectures=None, function_url_config=None, stack_path=posixpath.join("this_is", "stack_path_C"), + function_build_info=Mock(), ) function2 = Function( @@ -1936,6 +1971,7 @@ def test_found_by_different_ids(self): architectures=None, function_url_config=None, stack_path=posixpath.join("this_is", "stack_path_B"), + function_build_info=Mock(), ) function3 = Function( @@ -1960,6 +1996,7 @@ def test_found_by_different_ids(self): architectures=None, function_url_config=None, stack_path=posixpath.join("this_is", "stack_path_A"), + function_build_info=Mock(), ) function4 = Function( @@ -1984,6 +2021,7 @@ def test_found_by_different_ids(self): architectures=None, function_url_config=None, stack_path=posixpath.join("this_is", "stack_path_D"), + function_build_info=Mock(), ) provider.functions = {"func1": function1, "func2": function2, "func3": function3, "func4": function4} diff --git a/tests/unit/lib/build_module/test_app_builder.py b/tests/unit/lib/build_module/test_app_builder.py index ba1f47e50b..34f7cc5ae0 100644 --- a/tests/unit/lib/build_module/test_app_builder.py +++ b/tests/unit/lib/build_module/test_app_builder.py @@ -12,7 +12,7 @@ from parameterized import parameterized from samcli.lib.build.workflow_config import UnsupportedRuntimeException -from samcli.lib.providers.provider import ResourcesToBuildCollector, Function +from samcli.lib.providers.provider import ResourcesToBuildCollector, Function, FunctionBuildInfo from samcli.lib.build.app_builder import ( ApplicationBuilder, UnsupportedBuilderLibraryVersionError, @@ -440,6 +440,7 @@ def test_must_raise_for_functions_with_multi_architecture(self, persist_mock, re architectures=[X86_64, ARM64], stack_path="", function_url_config=None, + function_build_info=FunctionBuildInfo.BuildableZip, ) resources_to_build_collector = ResourcesToBuildCollector() @@ -498,6 +499,7 @@ def test_must_not_use_dep_layer_for_non_cached(self): architectures=[X86_64], stack_path="", function_url_config=None, + function_build_info=FunctionBuildInfo.BuildableZip, ) resources_to_build_collector = ResourcesToBuildCollector() diff --git a/tests/unit/lib/build_module/test_build_graph.py b/tests/unit/lib/build_module/test_build_graph.py index c1485b0a8d..e0d1f524c9 100644 --- a/tests/unit/lib/build_module/test_build_graph.py +++ b/tests/unit/lib/build_module/test_build_graph.py @@ -34,7 +34,7 @@ BuildHashingInformation, HANDLER_FIELD, ) -from samcli.lib.providers.provider import Function, LayerVersion +from samcli.lib.providers.provider import Function, LayerVersion, FunctionBuildInfo from samcli.lib.utils import osutils from samcli.lib.utils.packagetype import ZIP @@ -60,6 +60,7 @@ def generate_function( inlinecode=None, architectures=[X86_64], stack_path="", + function_build_info=FunctionBuildInfo.BuildableZip, ): if metadata is None: metadata = {} @@ -85,6 +86,7 @@ def generate_function( codesign_config_arn, architectures, stack_path, + function_build_info, ) diff --git a/tests/unit/lib/sync/test_sync_flow_factory.py b/tests/unit/lib/sync/test_sync_flow_factory.py index 908b01d14a..632d25bba5 100644 --- a/tests/unit/lib/sync/test_sync_flow_factory.py +++ b/tests/unit/lib/sync/test_sync_flow_factory.py @@ -4,8 +4,10 @@ from parameterized import parameterized +from samcli.lib.providers.provider import FunctionBuildInfo from samcli.lib.sync.sync_flow_factory import SyncCodeResources, SyncFlowFactory from samcli.lib.utils.cloudformation import CloudFormationResourceSummary +from samcli.lib.utils.packagetype import IMAGE, ZIP from samcli.lib.utils.resources import ( AWS_SERVERLESS_FUNCTION, AWS_LAMBDA_FUNCTION, @@ -21,7 +23,7 @@ class TestSyncFlowFactory(TestCase): - def create_factory(self, auto_dependency_layer: bool = False): + def create_factory(self, auto_dependency_layer: bool = False, build_context=None): stack_resource = MagicMock() stack_resource.resources = { "Resource1": { @@ -37,7 +39,7 @@ def create_factory(self, auto_dependency_layer: bool = False): }, } factory = SyncFlowFactory( - build_context=MagicMock(), + build_context=build_context or MagicMock(), deploy_context=MagicMock(), sync_context=MagicMock(), stacks=[stack_resource, MagicMock()], @@ -68,14 +70,45 @@ def test_load_physical_id_mapping( {"Resource1": "PhysicalResource1", "Resource2": "PhysicalResource2"}, ) - @parameterized.expand([(None,), (Mock(),)]) + @parameterized.expand( + itertools.product( + [None, Mock()], + [ + FunctionBuildInfo.BuildableZip, + FunctionBuildInfo.PreZipped, + FunctionBuildInfo.InlineCode, + FunctionBuildInfo.SkipBuild, + ], + ) + ) @patch("samcli.lib.sync.sync_flow_factory.ImageFunctionSyncFlow") + @patch("samcli.lib.sync.sync_flow_factory.ZipFunctionSyncFlowSkipBuildZipFile") + @patch("samcli.lib.sync.sync_flow_factory.ZipFunctionSyncFlowSkipBuildDirectory") @patch("samcli.lib.sync.sync_flow_factory.ZipFunctionSyncFlow") - def test_create_lambda_flow_zip(self, pre_build_artifacts, zip_function_mock, image_function_mock): - factory = self.create_factory() - resource = {"Properties": {"PackageType": "Zip"}} - result = factory._create_lambda_flow("Function1", resource, pre_build_artifacts) - self.assertEqual(result, zip_function_mock.return_value) + def test_create_lambda_flow_zip( + self, + pre_build_artifacts, + function_build_info, + zip_function_mock, + zip_function_skip_build_directory_mock, + zip_function_skip_build_zip_mock, + _, + ): + build_context = MagicMock() + build_context.function_provider.get.return_value = Mock( + packagetype=ZIP, function_build_info=function_build_info + ) + factory = self.create_factory(build_context=build_context) + result = factory._create_lambda_flow("Function1", pre_build_artifacts) + + if function_build_info == FunctionBuildInfo.BuildableZip: + self.assertEqual(result, zip_function_mock.return_value) + if function_build_info == FunctionBuildInfo.PreZipped: + self.assertEqual(result, zip_function_skip_build_zip_mock.return_value) + if function_build_info == FunctionBuildInfo.SkipBuild: + self.assertEqual(result, zip_function_skip_build_directory_mock.return_value) + if function_build_info == FunctionBuildInfo.InlineCode: + self.assertIsNone(result) @parameterized.expand([(None,), (Mock(),)]) @patch("samcli.lib.sync.sync_flow_factory.ImageFunctionSyncFlow") @@ -84,9 +117,12 @@ def test_create_lambda_flow_zip(self, pre_build_artifacts, zip_function_mock, im def test_create_lambda_flow_zip_with_auto_dependency_layer( self, pre_build_artifacts, auto_dependency_layer_mock, zip_function_mock, image_function_mock ): - factory = self.create_factory(True) - resource = {"Properties": {"PackageType": "Zip", "Runtime": "python3.8"}} - result = factory._create_lambda_flow("Function1", resource, pre_build_artifacts) + build_context = MagicMock() + build_context.function_provider.get.return_value = Mock( + packagetype=ZIP, build_info=FunctionBuildInfo.BuildableZip, runtime="python3.8" + ) + factory = self.create_factory(True, build_context=build_context) + result = factory._create_lambda_flow("Function1", pre_build_artifacts) self.assertEqual(result, auto_dependency_layer_mock.return_value) @parameterized.expand([(None,), (Mock(),)]) @@ -96,19 +132,30 @@ def test_create_lambda_flow_zip_with_auto_dependency_layer( def test_create_lambda_flow_zip_with_unsupported_runtime_auto_dependency_layer( self, pre_build_artifacts, auto_dependency_layer_mock, zip_function_mock, image_function_mock ): - factory = self.create_factory(True) - resource = {"Properties": {"PackageType": "Zip", "Runtime": "ruby2.7"}} - result = factory._create_lambda_flow("Function1", resource, pre_build_artifacts) + build_context = MagicMock() + build_context.function_provider.get.return_value = Mock( + packagetype=ZIP, build_info=FunctionBuildInfo.BuildableZip, runtime="ruby2.7" + ) + factory = self.create_factory(True, build_context=build_context) + result = factory._create_lambda_flow("Function1", pre_build_artifacts) self.assertEqual(result, zip_function_mock.return_value) - @parameterized.expand([(None,), (Mock(),)]) + @parameterized.expand( + itertools.product([None, Mock()], [FunctionBuildInfo.BuildableImage, FunctionBuildInfo.NonBuildableImage]) + ) @patch("samcli.lib.sync.sync_flow_factory.ImageFunctionSyncFlow") @patch("samcli.lib.sync.sync_flow_factory.ZipFunctionSyncFlow") - def test_create_lambda_flow_image(self, pre_build_artifacts, zip_function_mock, image_function_mock): - factory = self.create_factory() - resource = {"Properties": {"PackageType": "Image"}} - result = factory._create_lambda_flow("Function1", resource, pre_build_artifacts) - self.assertEqual(result, image_function_mock.return_value) + def test_create_lambda_flow_image(self, pre_build_artifacts, function_build_info, _, image_function_mock): + build_context = MagicMock() + build_context.function_provider.get.return_value = Mock( + packagetype=IMAGE, function_build_info=function_build_info + ) + factory = self.create_factory(build_context=build_context) + result = factory._create_lambda_flow("Function1", pre_build_artifacts) + if function_build_info == FunctionBuildInfo.BuildableImage: + self.assertEqual(result, image_function_mock.return_value) + else: + self.assertIsNone(result) @parameterized.expand([(None,), (Mock(),)]) @patch("samcli.lib.sync.sync_flow_factory.LayerSyncFlow") @@ -116,7 +163,7 @@ def test_create_layer_flow(self, pre_build_artifacts, layer_sync_mock): factory = self.create_factory() # mock layer for not having SkipBuild:True factory._build_context.layer_provider.get.return_value = Mock(skip_build=False) - result = factory._create_layer_flow("Layer1", {}, pre_build_artifacts) + result = factory._create_layer_flow("Layer1", pre_build_artifacts) self.assertEqual(result, layer_sync_mock.return_value) @parameterized.expand(itertools.product([Mock(build_method=None), Mock(skip_build=True)], [None, Mock()])) @@ -130,7 +177,7 @@ def test_create_layer_flow_with_skip_build_directory( factory._build_context.layer_provider.get.return_value = layer_mock # codeuri should resolve as directory is_local_folder_mock.return_value = True - result = factory._create_layer_flow("Layer1", {}, pre_build_artifacts) + result = factory._create_layer_flow("Layer1", pre_build_artifacts) self.assertEqual(result, layer_sync_mock.return_value) @parameterized.expand(itertools.product([Mock(build_method=None), Mock(skip_build=True)], [None, Mock()])) @@ -145,14 +192,14 @@ def test_create_layer_flow_with_skip_build_zip( # codeuri should resolve as zip file is_local_folder_mock.return_value = False is_zip_file_mock.return_value = True - result = factory._create_layer_flow("Layer1", {}, pre_build_artifacts) + result = factory._create_layer_flow("Layer1", pre_build_artifacts) self.assertEqual(result, layer_sync_mock.return_value) @parameterized.expand([(None,), (Mock(),)]) def test_create_layer_flow_with_no_layer(self, pre_build_artifacts): factory = self.create_factory() factory._build_context.layer_provider.get.return_value = None - result = factory._create_layer_flow("Layer1", {}, pre_build_artifacts) + result = factory._create_layer_flow("Layer1", pre_build_artifacts) self.assertIsNone(result) @parameterized.expand([(None,), (Mock(),)]) @@ -160,37 +207,33 @@ def test_create_layer_flow_with_no_layer(self, pre_build_artifacts): @patch("samcli.lib.sync.sync_flow_factory.ZipFunctionSyncFlow") def test_create_lambda_flow_other(self, pre_build_artifacts, zip_function_mock, image_function_mock): factory = self.create_factory() - resource = {"Properties": {"PackageType": "Other"}} - result = factory._create_lambda_flow("Function1", resource, pre_build_artifacts) + result = factory._create_lambda_flow("Function1", pre_build_artifacts) self.assertEqual(result, None) @patch("samcli.lib.sync.sync_flow_factory.RestApiSyncFlow") def test_create_rest_api_flow(self, rest_api_sync_mock): factory = self.create_factory() - result = factory._create_rest_api_flow("API1", {}, None) + result = factory._create_rest_api_flow("API1", None) self.assertEqual(result, rest_api_sync_mock.return_value) @patch("samcli.lib.sync.sync_flow_factory.HttpApiSyncFlow") def test_create_api_flow(self, http_api_sync_mock): factory = self.create_factory() - result = factory._create_api_flow("API1", {}, None) + result = factory._create_api_flow("API1", None) self.assertEqual(result, http_api_sync_mock.return_value) @patch("samcli.lib.sync.sync_flow_factory.StepFunctionsSyncFlow") def test_create_stepfunctions_flow(self, stepfunctions_sync_mock): factory = self.create_factory() - result = factory._create_stepfunctions_flow("StateMachine1", {}, None) + result = factory._create_stepfunctions_flow("StateMachine1", None) self.assertEqual(result, stepfunctions_sync_mock.return_value) @parameterized.expand([(None,), (Mock(),)]) - @patch("samcli.lib.sync.sync_flow_factory.get_resource_by_id") - def test_create_sync_flow(self, pre_build_artifacts, get_resource_by_id_mock): + def test_create_sync_flow(self, pre_build_artifacts): factory = self.create_factory() sync_flow = MagicMock() resource_identifier = MagicMock() - get_resource_by_id = MagicMock() - get_resource_by_id_mock.return_value = get_resource_by_id generator_mock = MagicMock() generator_mock.return_value = sync_flow @@ -201,21 +244,15 @@ def test_create_sync_flow(self, pre_build_artifacts, get_resource_by_id_mock): result = factory.create_sync_flow(resource_identifier, pre_build_artifacts) self.assertEqual(result, sync_flow) - generator_mock.assert_called_once_with(factory, resource_identifier, get_resource_by_id, pre_build_artifacts) + generator_mock.assert_called_once_with(factory, resource_identifier, pre_build_artifacts) - @patch("samcli.lib.sync.sync_flow_factory.get_resource_by_id") - def test_create_unknown_resource_sync_flow(self, get_resource_by_id_mock): - get_resource_by_id_mock.return_value = None + def test_create_unknown_resource_sync_flow(self): factory = self.create_factory() self.assertIsNone(factory.create_sync_flow(MagicMock())) - @patch("samcli.lib.sync.sync_flow_factory.get_resource_by_id") - def test_create_none_generator_sync_flow(self, get_resource_by_id_mock): + def test_create_none_generator_sync_flow(self): factory = self.create_factory() - resource_identifier = MagicMock() - get_resource_by_id = MagicMock() - get_resource_by_id_mock.return_value = get_resource_by_id get_generator_function_mock = MagicMock() get_generator_function_mock.return_value = None