Skip to content

Commit

Permalink
feat: sam remote invoke help text and UX fixes (aws#5366)
Browse files Browse the repository at this point in the history
* Improve remote invoke help text and fix some UX bugs

* Updated help text for parameter option

* Updated test class name

* Updated test method name

* Updated help text for output-format and event-file

* Address feedback

* Updated help text for parameter option

* Changed --output-format name to output and the values to text/json

* Handle empty event for lambda and read from stdin when - is passed for event-file
  • Loading branch information
hnnasit authored and Leonardo Gama committed Jun 22, 2023
1 parent 2c8fab9 commit ad770e8
Show file tree
Hide file tree
Showing 20 changed files with 449 additions and 69 deletions.
8 changes: 7 additions & 1 deletion samcli/commands/_utils/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,13 @@ def remote_invoke_parameter_click_option():
type=RemoteInvokeBotoApiParameterType(),
callback=remote_invoke_boto_parameter_callback,
required=False,
help="Additional parameters for the boto API call.\n" "Lambda APIs: invoke and invoke_with_response_stream",
help="Additional parameters that can be passed to invoke the resource.\n"
"The following additional parameters can be used to invoke a lambda resource and get a buffered response: "
"InvocationType='Event'|'RequestResponse'|'DryRun', LogType='None'|'Tail', "
"ClientContext='base64-encoded string' Qualifier='string'. "
"The following additional parameters can be used to invoke a lambda resource with response streaming: "
"InvocationType='RequestResponse'|'DryRun', LogType='None'|'Tail', "
"ClientContext='base64-encoded string', Qualifier='string'.",
)


Expand Down
42 changes: 30 additions & 12 deletions samcli/commands/remote/invoke/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
from samcli.cli.context import Context
from samcli.cli.main import aws_creds_options, common_options, pass_context, print_cmdline_args
from samcli.cli.types import RemoteInvokeOutputFormatType
from samcli.commands._utils.command_exception_handler import command_exception_handler
from samcli.commands._utils.options import remote_invoke_parameter_option
from samcli.commands.remote.invoke.core.command import RemoteInvokeCommand
from samcli.lib.cli_validation.remote_invoke_options_validations import (
event_and_event_file_options_validation,
stack_name_or_resource_id_atleast_one_option_validation,
Expand All @@ -20,30 +22,45 @@
LOG = logging.getLogger(__name__)

HELP_TEXT = """
Invoke or send an event to cloud resources in your CFN stack
Invoke or send an event to resources in the cloud.
"""
SHORT_HELP = "Invoke a deployed resource in the cloud"

DESCRIPTION = """
Invoke or send an event to resources in the cloud.
An event body can be passed using either -e (--event) or --event-file parameter.
Returned response will be written to stdout. Lambda logs will be written to stderr.
"""


@click.command("invoke", help=HELP_TEXT, short_help=SHORT_HELP)
@click.command(
"invoke",
cls=RemoteInvokeCommand,
help=HELP_TEXT,
description=DESCRIPTION,
short_help=SHORT_HELP,
requires_credentials=True,
context_settings={"max_content_width": 120},
)
@configuration_option(provider=TomlProvider(section="parameters"))
@click.option("--stack-name", required=False, help="Name of the stack to get the resource information from")
@click.option("--resource-id", required=False, help="Name of the resource that will be invoked")
@click.argument("resource-id", required=False)
@click.option(
"--event",
"-e",
help="The event that will be sent to the resource. The target parameter will depend on the resource type. "
"For instance: 'Payload' for Lambda",
"For instance: 'Payload' for Lambda which can be passed as a JSON string",
)
@click.option(
"--event-file",
type=click.File("r", encoding="utf-8"),
help="The file that contains the event that will be sent to the resource",
help="The file that contains the event that will be sent to the resource.",
)
@click.option(
"--output-format",
help="Output format for the boto API response",
default=RemoteInvokeOutputFormat.DEFAULT.name.lower(),
"--output",
help="Output the results from the command in a given output format. "
"The text format prints a readable AWS API response. The json format prints the full AWS API response.",
default=RemoteInvokeOutputFormat.TEXT.name.lower(),
type=RemoteInvokeOutputFormatType(RemoteInvokeOutputFormat),
)
@remote_invoke_parameter_option
Expand All @@ -55,13 +72,14 @@
@track_command
@check_newer_version
@print_cmdline_args
@command_exception_handler
def cli(
ctx: Context,
stack_name: str,
resource_id: str,
event: str,
event_file: TextIOWrapper,
output_format: RemoteInvokeOutputFormat,
output: RemoteInvokeOutputFormat,
parameter: dict,
config_file: str,
config_env: str,
Expand All @@ -75,7 +93,7 @@ def cli(
resource_id,
event,
event_file,
output_format,
output,
parameter,
ctx.region,
ctx.profile,
Expand All @@ -89,7 +107,7 @@ def do_cli(
resource_id: str,
event: str,
event_file: TextIOWrapper,
output_format: RemoteInvokeOutputFormat,
output: RemoteInvokeOutputFormat,
parameter: dict,
region: str,
profile: str,
Expand Down Expand Up @@ -120,7 +138,7 @@ def do_cli(
) as remote_invoke_context:

remote_invoke_input = RemoteInvokeExecutionInfo(
payload=event, payload_file=event_file, parameters=parameter, output_format=output_format
payload=event, payload_file=event_file, parameters=parameter, output_format=output
)

remote_invoke_context.run(remote_invoke_input=remote_invoke_input)
Expand Down
Empty file.
158 changes: 158 additions & 0 deletions samcli/commands/remote/invoke/core/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"""
Invoke Command Class.
"""
import json

from click import Context, style

from samcli.cli.core.command import CoreCommand
from samcli.cli.row_modifiers import RowDefinition, ShowcaseRowModifier
from samcli.commands.remote.invoke.core.formatters import RemoteInvokeCommandHelpTextFormatter
from samcli.commands.remote.invoke.core.options import OPTIONS_INFO


class RemoteInvokeCommand(CoreCommand):
class CustomFormatterContext(Context):
formatter_class = RemoteInvokeCommandHelpTextFormatter

context_class = CustomFormatterContext

@staticmethod
def format_examples(ctx: Context, formatter: RemoteInvokeCommandHelpTextFormatter):
with formatter.indented_section(name="Examples", extra_indents=1):
with formatter.indented_section(name="Invoke default lambda function with empty event", extra_indents=1):
formatter.write_rd(
[
RowDefinition(
text="\n",
),
RowDefinition(
name=style(f"${ctx.command_path} --stack-name hello-world"),
extra_row_modifiers=[ShowcaseRowModifier()],
),
]
)
with formatter.indented_section(
name="Invoke default lambda function with event passed as text input", extra_indents=1
):
formatter.write_rd(
[
RowDefinition(
text="\n",
),
RowDefinition(
name=style(
f"${ctx.command_path} --stack-name hello-world -e '{json.dumps({'message':'hello!'})}'"
),
extra_row_modifiers=[ShowcaseRowModifier()],
),
]
)
with formatter.indented_section(name="Invoke named lambda function with an event file", extra_indents=1):
formatter.write_rd(
[
RowDefinition(
text="\n",
),
RowDefinition(
name=style(
f"${ctx.command_path} --stack-name "
f"hello-world HelloWorldFunction --event-file event.json"
),
extra_row_modifiers=[ShowcaseRowModifier()],
),
]
)
with formatter.indented_section(name="Invoke lambda function with event as stdin input", extra_indents=1):
formatter.write_rd(
[
RowDefinition(
text="\n",
),
RowDefinition(
name=style(
f"$ echo '{json.dumps({'message':'hello!'})}' | "
f"{ctx.command_path} HelloWorldFunction --event-file -"
),
extra_row_modifiers=[ShowcaseRowModifier()],
),
]
)
with formatter.indented_section(
name="Invoke lambda function using lambda ARN and get the full AWS API response", extra_indents=1
):
formatter.write_rd(
[
RowDefinition(
text="\n",
),
RowDefinition(
name=style(
f"${ctx.command_path} arn:aws:lambda:us-west-2:123456789012:function:my-function -e <>"
f" --output json"
),
extra_row_modifiers=[ShowcaseRowModifier()],
),
]
)
with formatter.indented_section(
name="Asynchronously invoke lambda function with additional boto parameters", extra_indents=1
):
formatter.write_rd(
[
RowDefinition(
text="\n",
),
RowDefinition(
name=style(
f"${ctx.command_path} HelloWorldFunction -e <> "
f"--parameter InvocationType=Event --parameter Qualifier=MyQualifier"
),
extra_row_modifiers=[ShowcaseRowModifier()],
),
]
)
with formatter.indented_section(
name="Dry invoke a lambda function to validate parameter values and user/role permissions",
extra_indents=1,
):
formatter.write_rd(
[
RowDefinition(
text="\n",
),
RowDefinition(
name=style(
f"${ctx.command_path} HelloWorldFunction -e <> --output json "
f"--parameter InvocationType=DryRun"
),
extra_row_modifiers=[ShowcaseRowModifier()],
),
]
)

@staticmethod
def format_acronyms(formatter: RemoteInvokeCommandHelpTextFormatter):
with formatter.indented_section(name="Acronyms", extra_indents=1):
formatter.write_rd(
[
RowDefinition(
name="ARN",
text="Amazon Resource Name",
extra_row_modifiers=[ShowcaseRowModifier()],
),
]
)

def format_options(self, ctx: Context, formatter: RemoteInvokeCommandHelpTextFormatter) -> None: # type:ignore
# NOTE: `ignore` is put in place here for mypy even though it is the correct behavior,
# as the `formatter_class` can be set in subclass of Command. If ignore is not set,
# mypy raises argument needs to be HelpFormatter as super class defines it.

self.format_description(formatter)
RemoteInvokeCommand.format_examples(ctx, formatter)
RemoteInvokeCommand.format_acronyms(formatter)

CoreCommand._format_options(
ctx=ctx, params=self.get_params(ctx), formatter=formatter, formatting_options=OPTIONS_INFO
)
21 changes: 21 additions & 0 deletions samcli/commands/remote/invoke/core/formatters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""
Remote Invoke Command Formatter.
"""
from samcli.cli.formatters import RootCommandHelpTextFormatter
from samcli.cli.row_modifiers import BaseLineRowModifier
from samcli.commands.remote.invoke.core.options import ALL_OPTIONS


class RemoteInvokeCommandHelpTextFormatter(RootCommandHelpTextFormatter):
ADDITIVE_JUSTIFICATION = 17

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# NOTE: Add Additional space after determining the longest option.
# However, do not justify with padding for more than half the width of
# the terminal to retain aesthetics.
self.left_justification_length = min(
max([len(option) for option in ALL_OPTIONS]) + self.ADDITIVE_JUSTIFICATION,
self.width // 2 - self.indent_increment,
)
self.modifiers = [BaseLineRowModifier()]
54 changes: 54 additions & 0 deletions samcli/commands/remote/invoke/core/options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
Remote Invoke Command Options related Datastructures for formatting.
"""
from typing import Dict, List

from samcli.cli.core.options import ALL_COMMON_OPTIONS, add_common_options_info
from samcli.cli.row_modifiers import RowDefinition

# NOTE: The ordering of the option lists matter, they are the order
# in which options will be displayed.

INFRASTRUCTURE_OPTION_NAMES: List[str] = ["stack_name"]

INPUT_EVENT_OPTIONS: List[str] = ["event", "event_file"]

ADDITIONAL_OPTIONS: List[str] = ["parameter", "output"]

AWS_CREDENTIAL_OPTION_NAMES: List[str] = ["region", "profile"]

CONFIGURATION_OPTION_NAMES: List[str] = ["config_env", "config_file"]

OTHER_OPTIONS: List[str] = ["debug"]

ALL_OPTIONS: List[str] = (
INFRASTRUCTURE_OPTION_NAMES
+ INPUT_EVENT_OPTIONS
+ ADDITIONAL_OPTIONS
+ AWS_CREDENTIAL_OPTION_NAMES
+ CONFIGURATION_OPTION_NAMES
+ ALL_COMMON_OPTIONS
)

OPTIONS_INFO: Dict[str, Dict] = {
"Infrastructure Options": {
"option_names": {opt: {"rank": idx} for idx, opt in enumerate(INFRASTRUCTURE_OPTION_NAMES)}
},
"Input Event Options": {"option_names": {opt: {"rank": idx} for idx, opt in enumerate(INPUT_EVENT_OPTIONS)}},
"Additional Options": {"option_names": {opt: {"rank": idx} for idx, opt in enumerate(ADDITIONAL_OPTIONS)}},
"AWS Credential Options": {
"option_names": {opt: {"rank": idx} for idx, opt in enumerate(AWS_CREDENTIAL_OPTION_NAMES)}
},
"Configuration Options": {
"option_names": {opt: {"rank": idx} for idx, opt in enumerate(CONFIGURATION_OPTION_NAMES)},
"extras": [
RowDefinition(name="Learn more about configuration files at:"),
RowDefinition(
name="https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli"
"-config.html. "
),
],
},
}

add_common_options_info(OPTIONS_INFO)
13 changes: 5 additions & 8 deletions samcli/lib/cli_validation/remote_invoke_options_validations.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import logging
import sys
from functools import wraps
from io import TextIOWrapper
from typing import cast

import click

Expand All @@ -17,7 +15,7 @@
def event_and_event_file_options_validation(func):
"""
This function validates the cases when both --event and --event-file are provided and
neither option is provided
logs if "-" is provided for --event-file and event is read from stdin.
Parameters
----------
Expand Down Expand Up @@ -48,10 +46,9 @@ def wrapped(*args, **kwargs):

validator.validate()

# if no event nor event_file arguments are given, read from stdin
if not event and not event_file:
LOG.debug("Neither --event nor --event-file options have been provided, reading from stdin")
kwargs["event_file"] = cast(TextIOWrapper, sys.stdin)
# If "-" is provided for --event-file, click uses it as a special file to refer to stdin.
if event_file and event_file.fileno() == sys.stdin.fileno():
LOG.info("Reading event from stdin (you can also pass it from file with --event-file)")
return func(*args, **kwargs)

return wrapped
Expand Down Expand Up @@ -83,7 +80,7 @@ def wrapped(*args, **kwargs):
exception=click.BadOptionUsage(
option_name="--resource-id",
ctx=ctx,
message="Atleast 1 of --stack-name or --resource-id parameters should be provided.",
message="At least 1 of --stack-name or --resource-id parameters should be provided.",
),
)

Expand Down
Loading

0 comments on commit ad770e8

Please sign in to comment.