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: sam remote invoke help text and UX fixes #5366

Merged
merged 12 commits into from
Jun 16, 2023
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"]
Copy link
Contributor

Choose a reason for hiding this comment

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

Not specific to this PR, I don't really don't like this as we need to keep parameter names in multiple places :(

Copy link
Contributor

Choose a reason for hiding this comment

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

We can consolidate for sure!


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():
Copy link
Contributor

Choose a reason for hiding this comment

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

just checking: is this the same behavior as local?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, sam local behaves in a similar way when - is passed to -e file path option. Click intelligently treats this as being read from stdin as the file stream.

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