Skip to content

Commit

Permalink
feat: sam test command (lambda + sqs) (aws#364)
Browse files Browse the repository at this point in the history
* refactor for test command + some unit test fixes

* remove some test command changes which accidentally went into refactor one

* remove some test command changes which accidentally went into refactor one

* sam test implementation and some groundwork

* when neither payload nor payload_file is provided, it will use sys.stdin to read the payload

* fix typo

* use protocol instead of complex callable

* use utf-8 for encoding

* fix typo

* use protocol instead of complex callable

* address comments

* address comments

* feat(Accelerate): CI Fixes (aws#368)

* Reverted pylint to 2.6.x

* Disables PyLint for Generic

* Updated boto3-stubs

* Fixed Another PyLint Generic Issue

* Fixed Test Typing Issues

* Fixed Python3.7 and 3.8 re.escape Test Issue

* Added ECR Login

* fix unit tests

* address comments

* address comments

* address comments

* update log message

* flip the flag back once we have new log group is created

* feat: sam test lambda implementation (aws#366)

* sam test lambda implementation

* when neither payload nor payload_file is provided, it will use sys.stdin to read the payload

* use utf-8 for encoding

* fix typo

* use protocol instead of complex callable

* address comments & update with ability to pipe file input into test execution

* feat: sam test sqs implementation (aws#367)

* sam test sqs implementation

* when neither payload nor payload_file is provided, it will use sys.stdin to read the payload

* use utf-8 for encoding

* fix typo

* use protocol instead of complex callable

* address comments & update with ability to pipe file input into test execution

* address comments & update sqs test executor

* added extra debug logging

* address comments

* address comments

* make black

Co-authored-by: Cosh_ <[email protected]>
  • Loading branch information
mndeveci and CoshUS authored Jul 15, 2021
1 parent 669839c commit c50c19d
Show file tree
Hide file tree
Showing 37 changed files with 1,768 additions and 225 deletions.
1 change: 1 addition & 0 deletions samcli/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"samcli.commands.publish",
"samcli.commands.traces",
"samcli.commands.sync",
"samcli.commands.test",
# We intentionally do not expose the `bootstrap` command for now. We might open it up later
# "samcli.commands.bootstrap",
]
Expand Down
1 change: 1 addition & 0 deletions samcli/commands/_utils/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
AWS_GLUE_JOB = "AWS::Glue::Job"
AWS_SERVERLESS_STATEMACHINE = "AWS::Serverless::StateMachine"
AWS_STEPFUNCTIONS_STATEMACHINE = "AWS::StepFunctions::StateMachine"
AWS_SQS_QUEUE = "AWS::SQS::Queue"

METADATA_WITH_LOCAL_PATHS = {AWS_SERVERLESSREPO_APPLICATION: ["LicenseUrl", "ReadmeUrl"]}

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 @@ -32,7 +32,7 @@
from samcli.lib.deploy.deployer import Deployer
from samcli.lib.package.s3_uploader import S3Uploader
from samcli.lib.providers.sam_stack_provider import SamLocalStackProvider
from samcli.lib.utils.botoconfig import get_boto_config_with_user_agent
from samcli.lib.utils.boto_utils import get_boto_config_with_user_agent
from samcli.yamlhelper import yaml_parse

LOG = logging.getLogger(__name__)
Expand Down
15 changes: 5 additions & 10 deletions samcli/commands/logs/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,29 +114,24 @@ def do_cli(
"""
Implementation of the ``cli`` method
"""
import boto3

from datetime import datetime
from typing import Callable, Any

from samcli.commands.logs.logs_context import parse_time, ResourcePhysicalIdResolver
from samcli.commands.logs.puller_factory import generate_puller
from samcli.lib.utils.botoconfig import get_boto_config_with_user_agent
from samcli.lib.utils.boto_utils import get_boto_client_provider_with_config, get_boto_resource_provider_with_config

sanitized_start_time = parse_time(start_time, "start-time")
sanitized_end_time = parse_time(end_time, "end-time") or datetime.utcnow()

boto_config = get_boto_config_with_user_agent(region_name=region)
logs_client_generator: Callable[[], Any] = lambda: boto3.session.Session().client("logs", config=boto_config)
cfn_resource = boto3.resource("cloudformation", config=boto_config)
xray_client = boto3.client("xray", config=boto_config) if include_tracing else None
resource_logical_id_resolver = ResourcePhysicalIdResolver(cfn_resource, stack_name, names)
boto_client_provider = get_boto_client_provider_with_config(region_name=region)
boto_resource_provider = get_boto_resource_provider_with_config(region_name=region)
resource_logical_id_resolver = ResourcePhysicalIdResolver(boto_resource_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
puller = generate_puller(
logs_client_generator,
xray_client,
boto_client_provider,
resource_logical_id_resolver.get_resource_information(fetch_all_when_no_resource_name_given),
filter_pattern,
cw_log_groups,
Expand Down
34 changes: 10 additions & 24 deletions samcli/commands/logs/logs_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

from samcli.commands._utils.resources import AWS_LAMBDA_FUNCTION, AWS_APIGATEWAY_RESTAPI, AWS_APIGATEWAY_HTTPAPI
from samcli.commands.exceptions import UserException
from samcli.lib.utils.boto_utils import BotoProviderType
from samcli.lib.utils.cloudformation import get_resource_summaries
from samcli.lib.utils.time import to_utc, parse_date

LOG = logging.getLogger(__name__)
Expand Down Expand Up @@ -60,12 +62,12 @@ class ResourcePhysicalIdResolver:

def __init__(
self,
cfn_resource: Any,
boto_resource_provider: BotoProviderType,
stack_name: str,
resource_names: Optional[List[str]] = None,
supported_resource_types: Optional[Set[str]] = None,
):
self._cfn_resource = cfn_resource
self._boto_resource_provider = boto_resource_provider
self._stack_name = stack_name
if resource_names is None:
resource_names = []
Expand Down Expand Up @@ -112,33 +114,17 @@ def _fetch_resources_from_stack(self, selected_resource_names: Optional[Set[str]
"""
results = []
LOG.debug("Getting logical id of the all resources for stack '%s'", self._stack_name)
stack_resources = self._get_stack_resources()
stack_resources = get_resource_summaries(
self._boto_resource_provider, self._stack_name, ResourcePhysicalIdResolver.DEFAULT_SUPPORTED_RESOURCES
)

if selected_resource_names is None:
selected_resource_names = {stack_resource.logical_id for stack_resource in stack_resources}
selected_resource_names = {stack_resource.logical_resource_id for stack_resource in stack_resources}

for resource in stack_resources:
# if resource name is not selected, continue
if resource.logical_id not in selected_resource_names:
LOG.debug("Resource (%s) is not selected with given input", resource.logical_id)
continue
# if resource type is not supported, continue
if not self.is_supported_resource(resource.resource_type):
LOG.debug(
"Resource (%s) with type (%s) is not supported, skipping",
resource.logical_id,
resource.resource_type,
)
if resource.logical_resource_id not in selected_resource_names:
LOG.debug("Resource (%s) is not selected with given input", resource.logical_resource_id)
continue
results.append(resource)
return results

def _get_stack_resources(self) -> Any:
"""
Fetches all resource information for the given stack, response is type of StackResourceSummariesCollection
"""
cfn_stack = self._cfn_resource.Stack(self._stack_name)
return cfn_stack.resource_summaries.all()

def is_supported_resource(self, resource_type: str) -> bool:
return resource_type in self._supported_resource_types
31 changes: 16 additions & 15 deletions samcli/commands/logs/puller_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
with its producers and consumers
"""
import logging
from typing import List, Optional, Callable, Any
from typing import List, Optional

from samcli.commands.exceptions import UserException
from samcli.commands.logs.console_consumers import CWConsoleEventConsumer
Expand All @@ -24,6 +24,8 @@
ObservabilityEventConsumer,
ObservabilityCombinedPuller,
)
from samcli.lib.utils.boto_utils import BotoProviderType
from samcli.lib.utils.cloudformation import CloudFormationResourceSummary
from samcli.lib.utils.colors import Colored

LOG = logging.getLogger(__name__)
Expand All @@ -37,9 +39,8 @@ class NoPullerGeneratedException(UserException):


def generate_puller(
logs_client_generator: Callable[[], Any],
xray_client: Any,
resource_information_list: List[Any],
boto_client_provider: BotoProviderType,
resource_information_list: List[CloudFormationResourceSummary],
filter_pattern: Optional[str] = None,
additional_cw_log_groups: Optional[List[str]] = None,
output_dir: Optional[str] = None,
Expand All @@ -51,12 +52,10 @@ def generate_puller(
Parameters
----------
logs_client_generator: Callable[[], CloudWatchLogsClient]
CloudWatchLogsClient generator, which will create a new instance of the client with a new session that could be
boto_client_provider: BotoProviderType
Boto3 client generator, which will create a new instance of the client with a new session that could be
used within different threads/coroutines
xray_client: boto3.client
Boto3 xray client which will be used to fetch the debug traces
resource_information_list : List[ResourceInformation]
resource_information_list : List[CloudFormationResourceSummary]
List of resource information, which keeps logical id, physical id and type of the resources
filter_pattern : Optional[str]
Optional filter pattern which will be used to filter incoming events
Expand All @@ -82,16 +81,18 @@ def generate_puller(
resource_information.resource_type, resource_information.physical_resource_id
)
if not cw_log_group_name:
LOG.warning("Can't find CloudWatch LogGroup name for resource (%s)", resource_information.logical_id)
LOG.warning(
"Can't find CloudWatch LogGroup name for resource (%s)", resource_information.logical_resource_id
)
continue

consumer = generate_consumer(filter_pattern, output_dir, resource_information.logical_id)
consumer = generate_consumer(filter_pattern, output_dir, resource_information.logical_resource_id)
pullers.append(
CWLogPuller(
logs_client_generator(),
boto_client_provider("logs"),
consumer,
cw_log_group_name,
resource_information.logical_id,
resource_information.logical_resource_id,
)
)

Expand All @@ -100,15 +101,15 @@ def generate_puller(
consumer = generate_consumer(filter_pattern, output_dir)
pullers.append(
CWLogPuller(
logs_client_generator(),
boto_client_provider("logs"),
consumer,
cw_log_group,
)
)

# if tracing flag is set, add the xray traces puller to fetch debug traces
if include_tracing:
trace_puller = generate_trace_puller(xray_client, output_dir)
trace_puller = generate_trace_puller(boto_client_provider("xray"), output_dir)
pullers.append(trace_puller)

# if no puller have been collected, raise an exception since there is nothing to pull
Expand Down
2 changes: 1 addition & 1 deletion samcli/commands/package/package_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from samcli.lib.package.code_signer import CodeSigner
from samcli.lib.package.s3_uploader import S3Uploader
from samcli.lib.package.uploaders import Uploaders
from samcli.lib.utils.botoconfig import get_boto_config_with_user_agent
from samcli.lib.utils.boto_utils import get_boto_config_with_user_agent
from samcli.yamlhelper import yaml_dump

LOG = logging.getLogger(__name__)
Expand Down
4 changes: 4 additions & 0 deletions samcli/commands/test/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""`sam test` command."""

# Expose the cli object here
from .command import cli # noqa
135 changes: 135 additions & 0 deletions samcli/commands/test/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""CLI command for "test" command."""
import logging
import sys
from io import TextIOWrapper
from typing import cast

import click

from samcli.cli.cli_config_file import configuration_option, TomlProvider
from samcli.cli.context import Context
from samcli.cli.main import print_cmdline_args, pass_context, aws_creds_options, common_options
from samcli.lib.cli_validation.payload_file_validation import payload_and_payload_file_options_validation
from samcli.lib.telemetry.metric import track_command
from samcli.lib.utils.version_checker import check_newer_version

LOG = logging.getLogger(__name__)

HELP_TEXT = """
Invoke or send test data to remote resources in your CFN stack
"""
SHORT_HELP = "Test a deployed resource"


@click.command("test", help=HELP_TEXT, short_help=SHORT_HELP)
@configuration_option(provider=TomlProvider(section="parameters"))
@click.option("--stack-name", required=True, help="Name of the stack to get the resource information from")
@click.option("--resource-id", required=True, help="Name of the resource that will be tested")
@click.option(
"--payload",
help="The payload that will be sent to the resource. The target parameter will depend on the resource type. "
"For instance: 'Payload' for Lambda, 'Entries' for SQS and 'Records' for Kinesis.",
)
@click.option(
"--payload-file",
type=click.File("r", encoding="utf-8"),
help="The file that contains the payload that will be sent to the resource",
)
@click.option(
"--tail",
is_flag=True,
help="Use this option to start tailing logs and XRay information for the given resource. "
"The execution will continue until it is explicitly interrupted with Ctrl + C",
)
@payload_and_payload_file_options_validation
@common_options
@aws_creds_options
@pass_context
@track_command
@check_newer_version
@print_cmdline_args
def cli(
ctx: Context,
stack_name: str,
resource_id: str,
payload: str,
payload_file: TextIOWrapper,
tail: bool,
config_file: str,
config_env: str,
) -> None:
"""
`sam test` command entry point
"""

do_cli(stack_name, resource_id, payload, payload_file, tail, ctx.region, ctx.profile, config_file, config_env)


def do_cli(
stack_name: str,
resource_id: str,
payload: str,
payload_file: TextIOWrapper,
tail: bool,
region: str,
profile: str,
config_file: str,
config_env: str,
) -> None:
"""
Implementation of the ``cli`` method
"""
from samcli.lib.test.test_executor_factory import TestExecutorFactory
from samcli.lib.test.test_executors import TestExecutionInfo
from samcli.lib.utils.boto_utils import get_boto_client_provider_with_config, get_boto_resource_provider_with_config
from samcli.lib.utils.cloudformation import get_resource_summary
from samcli.commands.logs.logs_context import ResourcePhysicalIdResolver
from samcli.commands.logs.puller_factory import generate_puller

from datetime import datetime

# create clients and required
boto_client_provider = get_boto_client_provider_with_config(region_name=region)
boto_resource_provider = get_boto_resource_provider_with_config(region_name=region)

# get resource summary
resource_summary = get_resource_summary(boto_resource_provider, stack_name, resource_id)
if not resource_summary:
LOG.error("Can't find the resource %s in given stack %s", resource_id, stack_name)
return

# generate executor with given resource
test_executor_factory = TestExecutorFactory(boto_client_provider)
test_executor = test_executor_factory.create_test_executor(resource_summary)

if not test_executor:
LOG.error("Resource (%s) is not supported with 'sam test' command", resource_id)
return

# set start_time for pulling logs later
start_time = datetime.utcnow()

# if no payload nor payload_file argument is given, read from stdin
if not payload and not payload_file:
LOG.info("Neither --payload nor --payload-file option have been provided, reading from stdin")
payload_file = cast(TextIOWrapper, sys.stdin)

test_exec_info = TestExecutionInfo(payload, payload_file)

# run execution
test_result = test_executor.execute(test_exec_info)

if test_result.is_succeeded():
LOG.info("Test succeeded, result: %s", test_result.response)

if tail:
LOG.debug("Tailing is enabled, generating puller instance to start tailing")
resource_logical_id_resolver = ResourcePhysicalIdResolver(boto_resource_provider, stack_name, [])
log_trace_puller = generate_puller(
boto_client_provider, resource_logical_id_resolver.get_resource_information(), include_tracing=True
)
LOG.debug("Starting to pull logs and XRay traces, press Ctrl + C to stop it")
log_trace_puller.tail(start_time)

else:
LOG.error("Test execution failed with following error", exc_info=test_result.exception)
2 changes: 1 addition & 1 deletion samcli/commands/traces/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def do_cli(trace_ids, start_time, end_time, tailing, output_dir, region):
import boto3
from samcli.commands.logs.logs_context import parse_time
from samcli.commands.traces.traces_puller_factory import generate_trace_puller
from samcli.lib.utils.botoconfig import get_boto_config_with_user_agent
from samcli.lib.utils.boto_utils import get_boto_config_with_user_agent

sanitized_start_time = parse_time(start_time, "start-time")
sanitized_end_time = parse_time(end_time, "end-time") or datetime.utcnow()
Expand Down
Loading

0 comments on commit c50c19d

Please sign in to comment.