Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle edge cases with function sync flow in sam sync command #5222

Merged
merged 10 commits into from
Jun 2, 2023
76 changes: 20 additions & 56 deletions samcli/commands/build/build_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,40 @@
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
from samcli.commands._utils.template import move_template
mndeveci marked this conversation as resolved.
Show resolved Hide resolved
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
from samcli.lib.build.exceptions import InvalidBuildGraphException
mndeveci marked this conversation as resolved.
Show resolved Hide resolved
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.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
from samcli.local.lambdafn.exceptions import ResourceNotFound
mndeveci marked this conversation as resolved.
Show resolved Hide resolved

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -583,11 +579,7 @@ def collect_all_build_resources(self) -> ResourcesToBuildCollector:
result = ResourcesToBuildCollector()
excludes: Tuple[str, ...] = self._exclude if self._exclude is not None else ()
result.add_functions(
[
f
for f in self.function_provider.get_all()
if (f.name not in excludes) and BuildContext._is_function_buildable(f)
]
[f for f in self.function_provider.get_all() if (f.name not in excludes) and f.build_info.is_buildable()]
)
result.add_layers(
[
Expand Down Expand Up @@ -650,34 +642,6 @@ def _collect_single_buildable_layer(

resource_collector.add_layer(layer)

@staticmethod
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This functionality is moved to Function class and FunctionBuildInfo enum below.

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
Expand Down
53 changes: 53 additions & 0 deletions samcli/lib/providers/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -138,6 +161,36 @@ def architecture(self) -> str:
)
return str(arch_list[0])

@property
def build_info(self) -> FunctionBuildInfo:
mndeveci marked this conversation as resolved.
Show resolved Hide resolved
if self.inlinecode:
LOG.debug("Skip building inline function: %s", self.full_path)
return FunctionBuildInfo.InlineCode

if isinstance(self.codeuri, str) and self.codeuri.endswith(".zip"):
LOG.debug("Skip building zip function: %s", self.full_path)
return FunctionBuildInfo.PreZipped

if self.skip_build:
LOG.debug("Skip building pre-built function: %s", self.full_path)
return FunctionBuildInfo.SkipBuild

if self.packagetype == IMAGE:
metadata = self.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.",
self.full_path,
)
return FunctionBuildInfo.NonBuildableImage
return FunctionBuildInfo.BuildableImage

return FunctionBuildInfo.BuildableZip


class ResourcesToBuildCollector:
def __init__(self) -> None:
Expand Down
26 changes: 26 additions & 0 deletions samcli/lib/sync/flows/zip_function_sync_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import hashlib
import logging
import os
import shutil
import tempfile
import uuid
from contextlib import ExitStack
Expand Down Expand Up @@ -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())
100 changes: 79 additions & 21 deletions samcli/lib/sync/sync_flow_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, get_resource_by_id
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
Expand All @@ -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,
Expand Down Expand Up @@ -153,13 +157,33 @@ def _create_lambda_flow(
resource: Dict[str, Any],
mndeveci marked this conversation as resolved.
Show resolved Hide resolved
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.build_info.is_buildable():
if 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.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,
Expand All @@ -169,17 +193,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.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,
Expand All @@ -188,7 +217,36 @@ 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.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,
Expand Down
Loading
Loading