Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions samcli/commands/_utils/command_exception_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""
Contains method decorator which can be used to convert common exceptions into click exceptions
which will end exeecution gracefully
"""
from functools import wraps
from typing import Callable, Dict, Any, Optional

from botocore.exceptions import NoRegionError, ClientError

from samcli.commands._utils.options import parameterized_option
from samcli.commands.exceptions import CredentialsError, RegionError
from samcli.lib.utils.boto_utils import get_client_error_code


@parameterized_option
def command_exception_handler(f, additional_mapping: Optional[Dict[Any, Callable[[Any], None]]] = None):
"""
This function returns a wrapped function definition, which handles configured exceptions gracefully
"""

def decorator_command_exception_handler(func):
@wraps(func)
def wrapper_command_exception_handler(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as ex:
exception_type = type(ex)

# check if there is a custom handling defined
exception_handler = (additional_mapping or {}).get(exception_type)
if exception_handler:
exception_handler(ex)

# if no custom handling defined search for default handlers
exception_handler = COMMON_EXCEPTION_HANDLER_MAPPING.get(exception_type)
if exception_handler:
exception_handler(ex)

# if no handler defined, raise the exception
raise ex

return wrapper_command_exception_handler

return decorator_command_exception_handler(f)


def _handle_no_region_error(ex: NoRegionError) -> None:
raise RegionError(
"No region information found. Please provide --region parameter or configure default region settings. "
"\nFor more information please visit https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/"
"setup-credentials.html#setup-credentials-setting-region"
)


def _handle_client_errors(ex: ClientError) -> None:
error_code = get_client_error_code(ex)

if error_code in ("ExpiredToken", "ExpiredTokenException"):
raise CredentialsError(
"Your credential configuration is invalid or has expired token value. \nFor more information please "
"visit: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html"
)

raise ex


COMMON_EXCEPTION_HANDLER_MAPPING: Dict[Any, Callable] = {
NoRegionError: _handle_no_region_error,
ClientError: _handle_client_errors,
}
2 changes: 1 addition & 1 deletion samcli/commands/_utils/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def prompt_experimental(
if is_experimental_enabled(config_entry):
update_experimental_context()
return True
confirmed = click.confirm(prompt, default=False)
confirmed = click.confirm(Colored().yellow(prompt), default=False)
if confirmed:
set_experimental(config_entry=config_entry, enabled=True)
update_experimental_context()
Expand Down
2 changes: 1 addition & 1 deletion samcli/commands/deploy/deploy_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ def deploy(
s3_uploader=s3_uploader,
tags=tags,
)
LOG.info(result)
LOG.debug(result)

except deploy_exceptions.DeployFailedError as ex:
LOG.error(str(ex))
Expand Down
6 changes: 6 additions & 0 deletions samcli/commands/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,9 @@ class InvalidImageException(UserException):
"""
Value provided to --build-image or --invoke-image is invalid URI
"""


class InvalidStackNameException(UserException):
"""
Value provided to --stack-name is invalid
"""
17 changes: 13 additions & 4 deletions samcli/commands/logs/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,20 @@

from samcli.cli.cli_config_file import configuration_option, TomlProvider
from samcli.cli.main import pass_context, common_options as cli_framework_options, aws_creds_options, print_cmdline_args
from samcli.commands._utils.options import common_observability_options
from samcli.lib.telemetry.metric import track_command
from samcli.lib.utils.version_checker import check_newer_version
from samcli.commands._utils.experimental import (
ExperimentalFlag,
force_experimental_option,
experimental,
prompt_experimental,
)
from samcli.commands._utils.options import common_observability_options
from samcli.commands.logs.validation_and_exception_handlers import (
SAM_LOGS_ADDITIONAL_EXCEPTION_HANDLERS,
stack_name_cw_log_group_validation,
)
from samcli.lib.telemetry.metric import track_command
from samcli.commands._utils.command_exception_handler import command_exception_handler
from samcli.lib.utils.version_checker import check_newer_version

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -97,6 +102,8 @@
@force_experimental_option("include_traces", config_entry=ExperimentalFlag.Accelerate) # pylint: disable=E1120
@force_experimental_option("cw_log_group", config_entry=ExperimentalFlag.Accelerate) # pylint: disable=E1120
@force_experimental_option("output", config_entry=ExperimentalFlag.Accelerate) # pylint: disable=E1120
@command_exception_handler(SAM_LOGS_ADDITIONAL_EXCEPTION_HANDLERS)
@stack_name_cw_log_group_validation
def cli(
ctx,
name,
Expand Down Expand Up @@ -169,7 +176,9 @@ def do_cli(

boto_client_provider = get_boto_client_provider_with_config(region=region, profile=profile)
boto_resource_provider = get_boto_resource_provider_with_config(region=region, profile=profile)
resource_logical_id_resolver = ResourcePhysicalIdResolver(boto_resource_provider, stack_name, names)
resource_logical_id_resolver = ResourcePhysicalIdResolver(
boto_resource_provider, boto_client_provider, stack_name, names
)

# only fetch all resources when no CloudWatch log group defined
fetch_all_when_no_resource_name_given = not cw_log_groups
Expand Down
13 changes: 12 additions & 1 deletion samcli/commands/logs/logs_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,13 @@ class ResourcePhysicalIdResolver:
def __init__(
self,
boto_resource_provider: BotoProviderType,
boto_client_provider: BotoProviderType,
stack_name: str,
resource_names: Optional[List[str]] = None,
supported_resource_types: Optional[Set[str]] = None,
):
self._boto_resource_provider = boto_resource_provider
self._boto_client_provider = boto_client_provider
self._stack_name = stack_name
if resource_names is None:
resource_names = []
Expand Down Expand Up @@ -126,7 +128,10 @@ def _fetch_resources_from_stack(
"""
LOG.debug("Getting logical id of the all resources for stack '%s'", self._stack_name)
stack_resources = get_resource_summaries(
self._boto_resource_provider, self._stack_name, ResourcePhysicalIdResolver.DEFAULT_SUPPORTED_RESOURCES
self._boto_resource_provider,
self._boto_client_provider,
self._stack_name,
ResourcePhysicalIdResolver.DEFAULT_SUPPORTED_RESOURCES,
)

if selected_resource_names:
Expand Down Expand Up @@ -161,4 +166,10 @@ def _get_selected_resources(
selected_resource = resource_summaries.get(selected_resource_name)
if selected_resource:
resources.append(selected_resource)
else:
LOG.warning(
"Resource name (%s) does not exist. Available resource names: %s",
selected_resource_name,
", ".join(resource_summaries.keys()),
)
return resources
16 changes: 14 additions & 2 deletions samcli/commands/logs/puller_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import logging
from typing import List, Optional

from botocore.exceptions import ClientError

from samcli.commands.exceptions import UserException
from samcli.commands.logs.console_consumers import CWConsoleEventConsumer
from samcli.commands.traces.traces_puller_factory import generate_trace_puller
Expand All @@ -25,7 +27,7 @@
ObservabilityCombinedPuller,
)
from samcli.lib.observability.util import OutputOption
from samcli.lib.utils.boto_utils import BotoProviderType
from samcli.lib.utils.boto_utils import BotoProviderType, get_client_error_code
from samcli.lib.utils.cloudformation import CloudFormationResourceSummary
from samcli.lib.utils.colors import Colored

Expand Down Expand Up @@ -101,9 +103,11 @@ def generate_puller(
# populate puller instances for the additional CloudWatch log groups
for cw_log_group in additional_cw_log_groups:
consumer = generate_consumer(filter_pattern, output)
logs_client = boto_client_provider("logs")
_validate_cw_log_group_name(cw_log_group, logs_client)
pullers.append(
CWLogPuller(
boto_client_provider("logs"),
logs_client,
consumer,
cw_log_group,
)
Expand All @@ -122,6 +126,14 @@ def generate_puller(
return ObservabilityCombinedPuller(pullers)


def _validate_cw_log_group_name(cw_log_group, logs_client):
try:
_ = logs_client.describe_log_streams(logGroupName=cw_log_group, limit=1)
except ClientError as ex:
if get_client_error_code(ex) == "ResourceNotFoundException":
LOG.warning("CloudWatch log group name (%s) does not exist.", cw_log_group)


def generate_consumer(
filter_pattern: Optional[str] = None, output: OutputOption = OutputOption.text, resource_name: Optional[str] = None
):
Expand Down
68 changes: 68 additions & 0 deletions samcli/commands/logs/validation_and_exception_handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""
Contains helper functions for validation and exception handling of "sam logs" command
"""
from functools import wraps
from typing import Dict, Any, Callable

import click
from botocore.exceptions import ClientError
from click import Context, BadOptionUsage

from samcli.commands.exceptions import InvalidStackNameException
from samcli.lib.utils.boto_utils import get_client_error_code


def stack_name_cw_log_group_validation(func):
"""
Wrapper Validation function that will run last after the all cli parmaters have been loaded
to check for conditions surrounding `--stack-name` and `--cw-log-group`. The
reason they are done last instead of in callback functions, is because the options depend
on each other, and this breaks cyclic dependencies.

:param func: Click command function
:return: Click command function after validation
"""

@wraps(func)
def wrapped(*args, **kwargs):
ctx = click.get_current_context()
stack_name = ctx.params.get("stack_name")
cw_log_groups = ctx.params.get("cw_log_group")
names = ctx.params.get("name")

# if --name is provided --stack-name should be provided as well
if names and not stack_name:
raise BadOptionUsage(
option_name="--stack-name",
ctx=ctx,
message="Missing option. Please provide '--stack-name' when using '--name' option",
)

# either --stack-name or --cw-log-group flags should be provided
if not stack_name and not cw_log_groups:
raise BadOptionUsage(
option_name="--stack-name",
ctx=ctx,
message="Missing option. Please provide '--stack-name' or '--cw-log-group'",
)

return func(*args, **kwargs)

return wrapped


def _handle_client_error(ex: ClientError) -> None:
"""
Handles client error which was caused by ListStackResources event
"""
operation_name = ex.operation_name
client_error_code = get_client_error_code(ex)
if client_error_code == "ValidationError" and operation_name == "ListStackResources":
click_context: Context = click.get_current_context()
stack_name_value = click_context.params.get("stack_name")
raise InvalidStackNameException(
f"Invalid --stack-name parameter. Stack with id '{stack_name_value}' does not exist"
)


SAM_LOGS_ADDITIONAL_EXCEPTION_HANDLERS: Dict[Any, Callable] = {ClientError: _handle_client_error}
17 changes: 5 additions & 12 deletions samcli/commands/sync/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,18 +242,6 @@ def do_cli(
from samcli.commands.package.package_context import PackageContext
from samcli.commands.deploy.deploy_context import DeployContext

s3_bucket = manage_stack(profile=profile, region=region)
click.echo(f"\n\t\tManaged S3 bucket: {s3_bucket}")

click.echo(f"\n\t\tDefault capabilities applied: {DEFAULT_CAPABILITIES}")
click.echo("To override with customized capabilities, use --capabilities flag or set it in samconfig.toml")

build_dir = DEFAULT_BUILD_DIR_WITH_AUTO_DEPENDENCY_LAYER if dependency_layer else DEFAULT_BUILD_DIR
LOG.debug("Using build directory as %s", build_dir)

build_dir = DEFAULT_BUILD_DIR_WITH_AUTO_DEPENDENCY_LAYER if dependency_layer else DEFAULT_BUILD_DIR
LOG.debug("Using build directory as %s", build_dir)

confirmation_text = SYNC_CONFIRMATION_TEXT

if not is_experimental_enabled(ExperimentalFlag.Accelerate):
Expand All @@ -265,6 +253,11 @@ def do_cli(
set_experimental(ExperimentalFlag.Accelerate)
update_experimental_context()

s3_bucket = manage_stack(profile=profile, region=region)

build_dir = DEFAULT_BUILD_DIR_WITH_AUTO_DEPENDENCY_LAYER if dependency_layer else DEFAULT_BUILD_DIR
LOG.debug("Using build directory as %s", build_dir)

with BuildContext(
resource_identifier=None,
template_file=template_file,
Expand Down
2 changes: 2 additions & 0 deletions samcli/commands/traces/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from samcli.cli.cli_config_file import configuration_option, TomlProvider
from samcli.cli.main import pass_context, common_options as cli_framework_options, aws_creds_options, print_cmdline_args
from samcli.commands._utils.command_exception_handler import command_exception_handler
from samcli.commands._utils.options import common_observability_options
from samcli.lib.observability.util import OutputOption
from samcli.lib.telemetry.metric import track_command
Expand Down Expand Up @@ -36,6 +37,7 @@
@track_command
@check_newer_version
@print_cmdline_args
@command_exception_handler
def cli(
ctx,
trace_id,
Expand Down
15 changes: 15 additions & 0 deletions samcli/lib/build/build_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import logging
import os
import threading
from abc import abstractmethod
from pathlib import Path
from typing import Sequence, Tuple, List, Any, Optional, Dict, cast, NamedTuple
from copy import deepcopy
Expand Down Expand Up @@ -501,6 +502,10 @@ def dependencies_dir(self) -> str:
def env_vars(self) -> Dict:
return deepcopy(self._env_vars)

@abstractmethod
def get_resource_full_paths(self) -> str:
"""Returns string representation of resources' full path information for this build definition"""


class LayerBuildDefinition(AbstractBuildDefinition):
"""
Expand All @@ -527,6 +532,12 @@ def __init__(
# this and move "layer" out of LayerBuildDefinition to take advantage of type check.
self.layer: LayerVersion = None # type: ignore

def get_resource_full_paths(self) -> str:
if not self.layer:
LOG.debug("LayerBuildDefinition with uuid (%s) doesn't have a layer assigned to it", self.uuid)
return ""
return self.layer.full_path

def __str__(self) -> str:
return (
f"LayerBuildDefinition({self.full_path}, {self.codeuri}, {self.source_hash}, {self.uuid}, "
Expand Down Expand Up @@ -616,6 +627,10 @@ def get_build_dir(self, artifact_root_dir: str) -> str:
self._validate_functions()
return self.functions[0].get_build_dir(artifact_root_dir)

def get_resource_full_paths(self) -> str:
"""Returns list of functions' full path information as a list of str"""
return ", ".join([function.full_path for function in self.functions])

def _validate_functions(self) -> None:
if not self.functions:
raise InvalidBuildGraphException("Build definition doesn't have any function definition to build")
Expand Down
Loading