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

feat: Make remote invoke command available #5381

Merged
merged 8 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
7 changes: 6 additions & 1 deletion samcli/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"samcli.commands.pipeline.pipeline",
"samcli.commands.list.list",
"samcli.commands.docs",
# "samcli.commands.remote.remote",
"samcli.commands.remote.remote",
# We intentionally do not expose the `bootstrap` command for now. We might open it up later
# "samcli.commands.bootstrap",
]
Expand Down Expand Up @@ -173,6 +173,11 @@ def format_commands(self, ctx: click.Context, formatter: RootCommandHelpTextForm
text=SAM_CLI_COMMANDS.get("sync", ""),
extra_row_modifiers=[HighlightNewRowNameModifier()],
),
RowDefinition(
name="remote",
text=SAM_CLI_COMMANDS.get("remote", ""),
extra_row_modifiers=[HighlightNewRowNameModifier()],
),
],
)

Expand Down
1 change: 1 addition & 0 deletions samcli/cli/root/command_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"validate": "Validate an AWS SAM template.",
"build": "Build your AWS serverless function code.",
"local": "Run your AWS serverless function locally.",
"remote": "Invoke or send an event to cloud resources in your CFN stack",
"package": "Package an AWS SAM application.",
"deploy": "Deploy an AWS SAM application.",
"delete": "Delete an AWS SAM application and the artifacts created by sam deploy.",
Expand Down
4 changes: 2 additions & 2 deletions samcli/commands/remote/remote_invoke_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def _populate_resource_summary(self) -> None:
see _get_from_physical_resource_id for details.
"""
if not self._stack_name and not self._resource_id:
raise InvalidRemoteInvokeParameters("Either --stack-name or --resource-id parameter should be provided")
raise InvalidRemoteInvokeParameters("Either --stack-name option or resource_id argument should be provided")

try:
if not self._resource_id:
Expand Down Expand Up @@ -162,7 +162,7 @@ def _get_single_resource_from_stack(self) -> CloudFormationResourceSummary:
if len(resource_summaries) > 1:
raise AmbiguousResourceForRemoteInvoke(
f"{self._stack_name} contains more than one resource that could be used with remote invoke, "
f"please provide --resource-id to resolve ambiguity."
f"please provide resource_id argument to resolve ambiguity."
)

# fail if no resource summary found with given types
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def wrapped(*args, **kwargs):

def stack_name_or_resource_id_atleast_one_option_validation(func):
"""
This function validates that atleast one of --stack-name or --resource-id should is be provided
This function validates that atleast one of --stack-name option or resource_id argument should is be provided

Parameters
----------
Expand Down
1 change: 1 addition & 0 deletions samcli/lib/docs/documentation_links.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"list endpoints": "https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-list-endpoints.html",
"list resources": "https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-list-resources.html",
"deploy": "https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-deploy.html",
"remote invoke": "https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-remote-invoke.html",
hnnasit marked this conversation as resolved.
Show resolved Hide resolved
"package": "https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-package.html",
"delete": "https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-delete.html",
"sync": "https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-sync.html",
Expand Down
2 changes: 1 addition & 1 deletion samcli/lib/remote_invoke/lambda_invoke_executors.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def validate_action_parameters(self, parameters: dict) -> None:
"""
for parameter_key, parameter_value in parameters.items():
if parameter_key == FUNCTION_NAME:
LOG.warning("FunctionName is defined using the value provided for --resource-id option.")
LOG.warning("FunctionName is defined using the value provided for resource_id argument.")
elif parameter_key == PAYLOAD:
LOG.warning("Payload is defined using the value provided for either --event or --event-file options.")
else:
Expand Down
6 changes: 6 additions & 0 deletions tests/end_to_end/end_to_end_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from tests.integration.init.test_init_base import InitIntegBase
from tests.integration.package.package_integ_base import PackageIntegBase
from tests.integration.local.invoke.invoke_integ_base import InvokeIntegBase
from tests.integration.remote.invoke.remote_invoke_integ_base import RemoteInvokeIntegBase
from tests.integration.sync.sync_integ_base import SyncIntegBase
from tests.integration.list.stack_outputs.stack_outputs_integ_base import StackOutputsIntegBase
import logging
Expand Down Expand Up @@ -73,6 +74,11 @@ def _get_package_command(self, s3_prefix, use_json=False, output_template_file=N
def _get_local_command(self, function_name):
return InvokeIntegBase.get_command_list(function_to_invoke=function_name)

def _get_remote_invoke_command(self, stack_name, resource_id, event, output):
return RemoteInvokeIntegBase.get_command_list(
stack_name=stack_name, resource_id=resource_id, event=event, output=output
)

def _get_delete_command(self, stack_name):
return self.get_delete_command_list(stack_name=stack_name, region=self.region_name, no_prompts=True)

Expand Down
19 changes: 13 additions & 6 deletions tests/end_to_end/test_runtimes_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from tests.end_to_end.test_stages import (
DefaultInitStage,
PackageDownloadZipFunctionStage,
DefaultRemoteInvokeStage,
DefaultDeleteStage,
EndToEndBaseStage,
DefaultSyncStage,
Expand Down Expand Up @@ -47,8 +46,10 @@ def validate(self, command_result: CommandResult):

class RemoteInvokeValidator(BaseValidator):
def validate(self, command_result: CommandResult):
self.assertEqual(command_result.process.get("StatusCode"), 200)
self.assertEqual(command_result.process.get("FunctionError", ""), "")
response = json.loads(command_result.stdout.decode("utf-8"))
self.assertEqual(command_result.process.returncode, 0)
self.assertEqual(response["StatusCode"], 200)
self.assertEqual(response.get("FunctionError", ""), "")


class StackOutputsValidator(BaseValidator):
Expand All @@ -75,18 +76,21 @@ class TestHelloWorldDefaultEndToEnd(EndToEndBase):

def test_hello_world_default_workflow(self):
stack_name = self._method_to_stack_name(self.id())
function_name = "HelloWorldFunction"
event = '{"hello": "world"}'
with EndToEndTestContext(self.app_name) as e2e_context:
self.template_path = e2e_context.template_path
init_command_list = self._get_init_command(e2e_context.working_directory)
build_command_list = self.get_command_list()
deploy_command_list = self._get_deploy_command(stack_name)
stack_outputs_command_list = self._get_stack_outputs_command(stack_name)
remote_invoke_command_list = self._get_remote_invoke_command(stack_name, function_name, event, "json")
delete_command_list = self._get_delete_command(stack_name)
stages = [
DefaultInitStage(InitValidator(e2e_context), e2e_context, init_command_list, self.app_name),
EndToEndBaseStage(BuildValidator(e2e_context), e2e_context, build_command_list),
EndToEndBaseStage(BaseValidator(e2e_context), e2e_context, deploy_command_list),
DefaultRemoteInvokeStage(RemoteInvokeValidator(e2e_context), e2e_context, stack_name),
EndToEndBaseStage(RemoteInvokeValidator(e2e_context), e2e_context, remote_invoke_command_list),
EndToEndBaseStage(BaseValidator(e2e_context), e2e_context, stack_outputs_command_list),
DefaultDeleteStage(BaseValidator(e2e_context), e2e_context, delete_command_list, stack_name),
]
Expand Down Expand Up @@ -117,7 +121,7 @@ def test_hello_world_workflow(self):
package_command_list = self._get_package_command(
s3_prefix="end-to-end-package-test", use_json=True, output_template_file="packaged_template.json"
)
local_command_list = self._get_local_command("HelloWorldFunction")
local_command_list = self._get_local_command(function_name)
stages = [
DefaultInitStage(InitValidator(e2e_context), e2e_context, init_command_list, self.app_name),
EndToEndBaseStage(BuildValidator(e2e_context), e2e_context, build_command_list),
Expand All @@ -141,17 +145,20 @@ class TestHelloWorldDefaultSyncEndToEnd(EndToEndBase):
app_template = "hello-world"

def test_go_hello_world_default_workflow(self):
function_name = "HelloWorldFunction"
event = '{"hello": "world"}'
stack_name = self._method_to_stack_name(self.id())
with EndToEndTestContext(self.app_name) as e2e_context:
self.template_path = e2e_context.template_path
init_command_list = self._get_init_command(e2e_context.working_directory)
sync_command_list = self._get_sync_command(stack_name)
stack_outputs_command_list = self._get_stack_outputs_command(stack_name)
remote_invoke_command_list = self._get_remote_invoke_command(stack_name, function_name, event, "json")
delete_command_list = self._get_delete_command(stack_name)
stages = [
DefaultInitStage(InitValidator(e2e_context), e2e_context, init_command_list, self.app_name),
DefaultSyncStage(BaseValidator(e2e_context), e2e_context, sync_command_list),
DefaultRemoteInvokeStage(RemoteInvokeValidator(e2e_context), e2e_context, stack_name),
EndToEndBaseStage(RemoteInvokeValidator(e2e_context), e2e_context, remote_invoke_command_list),
EndToEndBaseStage(BaseValidator(e2e_context), e2e_context, stack_outputs_command_list),
DefaultDeleteStage(BaseValidator(e2e_context), e2e_context, delete_command_list, stack_name),
]
Expand Down
15 changes: 0 additions & 15 deletions tests/end_to_end/test_stages.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,6 @@ def _delete_default_samconfig(self):
pass


class DefaultRemoteInvokeStage(EndToEndBaseStage):
def __init__(self, validator, test_context, stack_name):
super().__init__(validator, test_context)
self.stack_name = stack_name
self.lambda_client = boto3.client("lambda")
self.resource = boto3.resource("cloudformation")

def run_stage(self) -> CommandResult:
lambda_output = self.lambda_client.invoke(FunctionName=self._get_lambda_physical_id())
return CommandResult(lambda_output, "", "")

def _get_lambda_physical_id(self):
return self.resource.StackResource(self.stack_name, "HelloWorldFunction").physical_resource_id


class DefaultDeleteStage(EndToEndBaseStage):
def __init__(self, validator, test_context, command_list, stack_name):
super().__init__(validator, test_context, command_list)
Expand Down
Empty file.
Empty file.
105 changes: 105 additions & 0 deletions tests/integration/remote/invoke/remote_invoke_integ_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from unittest import TestCase, skipIf
from pathlib import Path
from typing import Optional

from tests.testing_utils import (
get_sam_command,
run_command,
RUNNING_ON_CI,
RUNNING_TEST_FOR_MASTER_ON_CI,
RUN_BY_CANARY,
)
from tests.integration.deploy.deploy_integ_base import DeployIntegBase

from samcli.lib.utils.boto_utils import get_boto_resource_provider_with_config, get_boto_client_provider_with_config
from samcli.lib.utils.cloudformation import get_resource_summaries

SKIP_REMOTE_INVOKE_TESTS = RUNNING_ON_CI and RUNNING_TEST_FOR_MASTER_ON_CI and not RUN_BY_CANARY


@skipIf(SKIP_REMOTE_INVOKE_TESTS, "Skip remote invoke tests in CI/CD only")
class RemoteInvokeIntegBase(TestCase):
template: Optional[Path] = None

@classmethod
def setUpClass(cls):
cls.cmd = get_sam_command()
cls.test_data_path = cls.get_integ_dir().joinpath("testdata")
if cls.template:
cls.template_path = str(cls.test_data_path.joinpath("remote_invoke", cls.template))
cls.events_folder_path = cls.test_data_path.joinpath("remote_invoke", "events")

@staticmethod
def get_integ_dir():
return Path(__file__).resolve().parents[2]

@staticmethod
def remote_invoke_deploy_stack(stack_name, template_path):

deploy_cmd = DeployIntegBase.get_deploy_command_list(
stack_name=stack_name,
template_file=template_path,
resolve_s3=True,
capabilities_list=["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"],
)

run_command(deploy_cmd)

@classmethod
def create_resources_and_boto_clients(cls):
cls.remote_invoke_deploy_stack(cls.stack_name, cls.template_path)
stack_resource_summaries = get_resource_summaries(
get_boto_resource_provider_with_config(),
get_boto_client_provider_with_config(),
cls.stack_name,
)
cls.stack_resources = {
resource_full_path: stack_resource_summary.physical_resource_id
for resource_full_path, stack_resource_summary in stack_resource_summaries.items()
}
cls.cfn_client = get_boto_client_provider_with_config()("cloudformation")
cls.lambda_client = get_boto_client_provider_with_config()("lambda")

@staticmethod
def get_command_list(
stack_name=None,
resource_id=None,
event=None,
event_file=None,
parameter_list=None,
output=None,
region=None,
profile=None,
beta_features=None,
):
command_list = [get_sam_command(), "remote", "invoke"]

if stack_name:
command_list = command_list + ["--stack-name", stack_name]

if event:
command_list = command_list + ["-e", event]

if event_file:
command_list = command_list + ["--event-file", event_file]

if profile:
command_list = command_list + ["--parameter", parameter]

if output:
command_list = command_list + ["--output", output]

if parameter_list:
for (parameter, value) in parameter_list:
command_list = command_list + ["--parameter", f"{parameter}={value}"]

if region:
command_list = command_list + ["--region", region]

if beta_features is not None:
command_list = command_list + ["--beta-features" if beta_features else "--no-beta-features"]

if resource_id:
command_list = command_list + [resource_id]

return command_list
2 changes: 2 additions & 0 deletions tests/unit/cli/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ def test_get_command_root_command_text(self):
("local", "local command output"),
("validate", "validate command output"),
("sync", "sync command output"),
("remote", "remote command output"),
],
"Deploy your App": [("package", "package command output"), ("deploy", "deploy command output")],
"Monitor your App": [("logs", "logs command output"), ("traces", "traces command output")],
Expand All @@ -194,6 +195,7 @@ def test_get_command_root_command_text(self):
"local": "local command output",
"validate": "validate command output",
"sync": "sync command output",
"remote": "remote command output",
"package": "package command output",
"deploy": "deploy command output",
"logs": "logs command output",
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/commands/docs/core/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test_formatter(self):
),
description[0][0],
)
self.assertEqual(len(commands), 19)
self.assertEqual(len(commands), 20)
all_commands = set(DocsCommandContext().get_complete_command_paths())
formatter_commands = set([command[0] for command in commands])
self.assertEqual(all_commands, formatter_commands)
Expand Down
1 change: 1 addition & 0 deletions tests/unit/commands/docs/test_docs_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"list endpoints",
"list resources",
"deploy",
"remote invoke",
"package",
"delete",
"sync",
Expand Down