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

Add new "st2 action-alias execute-and-format" command for easier ChatOps alias development and testing #5143

Merged
merged 13 commits into from
Mar 27, 2022
Merged
Show file tree
Hide file tree
Changes from 10 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
14 changes: 14 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ Changelog
in development
--------------

Added
~~~~~

* Add new ``st2 action-alias test <message string>`` CLI command which allows users to easily
test action alias matching and result formatting.

This command will first try to find a matching alias (same as ``st2 action-alias match``
command) and if a match is found, trigger an execution (same as ``st2 action-alias execute``
command) and format the execution result.

This means it uses exactly the same flow as commands on chat, but the interaction avoids
chat and hubot which should make testing and developing aliases easier and faster. #5143

Contributed by @Kami.

3.6.0 - October 29, 2021
------------------------
Expand Down
19 changes: 18 additions & 1 deletion st2client/st2client/commands/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,15 @@ def _run_and_print_child_task_list(self, execution, args, **kwargs):
attribute_transform_functions=self.attribute_transform_functions,
)

def _get_execution_result(self, execution, action_exec_mgr, args, **kwargs):
def _get_execution_result(
self, execution, action_exec_mgr, args, force_retry_on_finish=False, **kwargs
):
"""
:param force_retry_on_finish: True to retry execution details on finish even if the
execution which is passed to this method has already finished.
This ensures we have latest state available for that
execution.
"""
pending_statuses = [
LIVEACTION_STATUS_REQUESTED,
LIVEACTION_STATUS_SCHEDULED,
Expand All @@ -636,8 +644,11 @@ def _get_execution_result(self, execution, action_exec_mgr, args, **kwargs):
print("")
return execution

poll_counter = 0

if not args.action_async:
while execution.status in pending_statuses:
poll_counter += 1
time.sleep(self.poll_interval)
if not args.json and not args.yaml:
sys.stdout.write(".")
Expand All @@ -646,6 +657,12 @@ def _get_execution_result(self, execution, action_exec_mgr, args, **kwargs):

sys.stdout.write("\n")

if poll_counter == 0 and force_retry_on_finish:
# In some situations we want to retrieve execution details from API even if it has
# already finished before performing even a single poll. This ensures we have the
# latest data for a particular execution.
execution = action_exec_mgr.get_by_id(execution.id, **kwargs)

if execution.status == LIVEACTION_STATUS_CANCELED:
return execution

Expand Down
115 changes: 115 additions & 0 deletions st2client/st2client/commands/action_alias.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
from __future__ import absolute_import

from st2client.models import core
from st2client.models.action import Execution
from st2client.models.action_alias import ActionAlias
from st2client.models.action_alias import ActionAliasMatch
from st2client.commands import resource
from st2client.commands.action import ActionRunCommandMixin
from st2client.formatters import table


Expand All @@ -43,6 +45,9 @@ def __init__(self, description, app, subparsers, parent_parser=None):
self.commands["execute"] = ActionAliasExecuteCommand(
self.resource, self.app, self.subparsers, add_help=True
)
self.commands["test"] = ActionAliasTestCommand(
self.resource, self.app, self.subparsers, add_help=True
)


class ActionAliasListCommand(resource.ContentPackResourceListCommand):
Expand Down Expand Up @@ -173,3 +178,113 @@ def run_and_print(self, args, **kwargs):
"To get the results, execute:\n st2 execution get %s"
% (execution.execution["id"])
)


class ActionAliasTestCommand(ActionRunCommandMixin, resource.ResourceCommand):
display_attributes = ["name"]

def __init__(self, resource, *args, **kwargs):
super(ActionAliasTestCommand, self).__init__(
Kami marked this conversation as resolved.
Show resolved Hide resolved
resource,
"test",
(
"Execute the command text by finding a matching %s and format the result."
% resource.get_display_name().lower()
),
*args,
**kwargs,
)

self.parser.add_argument(
"command_text",
metavar="command",
help=(
"Execute the command text by finding a matching %s."
% resource.get_display_name().lower()
),
)
self.parser.add_argument(
"-u",
"--user",
type=str,
default=None,
help="User under which to run the action (admins only).",
)

self._add_common_options()
self.parser.add_argument(
"-a",
"--async",
action="store_true",
dest="action_async",
help="Do not wait for action to finish.",
)

@resource.add_auth_token_to_kwargs_from_cli
def run(self, args, **kwargs):
payload = core.Resource()
payload.command = args.command_text
payload.user = args.user or ""
payload.source_channel = "cli"

alias_execution_mgr = self.app.client.managers["ActionAliasExecution"]
execution = alias_execution_mgr.match_and_execute(payload)
return execution

def run_and_print(self, args, **kwargs):
# 1. Trigger the execution via alias
print("Triggering execution via action alias")
print("")

# NOTE: This will return an error and abort if command matches no aliases so no additional
# checks are needed
result = self.run(args, **kwargs)
execution = Execution.deserialize(result.execution)

# 2. Wait for it to complete
print(
"Execution (%s) has been started, waiting for it to finish..."
% (execution.id)
)
print("")

action_exec_mgr = self.app.client.managers["Execution"]
execution = self._get_execution_result(
execution=execution, action_exec_mgr=action_exec_mgr, args=args, **kwargs
)
execution_id = execution.id

# 3. Run chatops.format_result action with the result of the completed execution
print("")
print(f"Execution ({execution_id}) has finished, rendering result...")
print("")

format_execution = Execution()
format_execution.action = "chatops.format_execution_result"
format_execution.parameters = {"execution_id": execution_id}
format_execution.user = args.user or ""

format_execution = action_exec_mgr.create(format_execution, **kwargs)

print(
"Execution (%s) has been started, waiting for it to finish..."
% (format_execution.id)
)
print("")

# 4. Wait for chatops.format_execution_result to finish and print the result
format_execution = self._get_execution_result(
execution=format_execution,
action_exec_mgr=action_exec_mgr,
args=args,
force_retry_on_finish=True,
**kwargs,
)

print("")
print("Formatted ChatOps result message")
print("")
print("=" * 80)
print(format_execution.result["result"]["message"])
print("=" * 80)
print("")
75 changes: 72 additions & 3 deletions st2client/tests/unit/test_action_alias.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@
from tests import base

from st2client import shell
from st2client import models
cognifloyd marked this conversation as resolved.
Show resolved Hide resolved
from st2client.utils import httpclient

MOCK_MATCH_AND_EXECUTE_RESULT = {
MOCK_MATCH_AND_EXECUTE_RESULT_1 = {
"results": [
{
"execution": {
Expand All @@ -34,6 +35,24 @@
]
}

MOCK_MATCH_AND_EXECUTE_RESULT_2 = {
"results": [
{
"execution": {"id": "mock-id-execute", "status": "succeeded"},
"actionalias": {"ref": "mock-ref"},
"liveaction": {
"id": "mock-id",
},
}
]
}

MOCK_CREATE_EXECUTION_RESULT = {
"id": "mock-id-format-execution",
"status": "succeeded",
"result": {"result": {"message": "Result formatted message"}},
}


class ActionAliasCommandTestCase(base.BaseCLITestCase):
def __init__(self, *args, **kwargs):
Expand All @@ -45,11 +64,11 @@ def __init__(self, *args, **kwargs):
"post",
mock.MagicMock(
return_value=base.FakeResponse(
json.dumps(MOCK_MATCH_AND_EXECUTE_RESULT), 200, "OK"
json.dumps(MOCK_MATCH_AND_EXECUTE_RESULT_1), 200, "OK"
)
),
)
def test_match_and_execute(self):
def test_match_and_execute_success(self):
ret = self.shell.run(["action-alias", "execute", "run whoami on localhost"])
self.assertEqual(ret, 0)

Expand All @@ -66,3 +85,53 @@ def test_match_and_execute(self):

self.assertTrue("Matching Action-alias: 'mock-ref'" in mock_stdout)
self.assertTrue("st2 execution get mock-id" in mock_stdout)

@mock.patch.object(
httpclient.HTTPClient,
"post",
mock.MagicMock(
side_effect=[
base.FakeResponse(
json.dumps(MOCK_MATCH_AND_EXECUTE_RESULT_2), 200, "OK"
),
base.FakeResponse(json.dumps(MOCK_CREATE_EXECUTION_RESULT), 200, "OK"),
]
),
)
@mock.patch.object(
models.ResourceManager,
"get_by_id",
mock.MagicMock(return_value=models.Execution(**MOCK_CREATE_EXECUTION_RESULT)),
cognifloyd marked this conversation as resolved.
Show resolved Hide resolved
)
def test_test_command_success(self):
ret = self.shell.run(["action-alias", "test", "run whoami on localhost"])
self.assertEqual(ret, 0)

expected_args = {
"command": "run whoami on localhost",
"user": "",
"source_channel": "cli",
}
httpclient.HTTPClient.post.assert_any_call(
"/aliasexecution/match_and_execute", expected_args
)

expected_args = {
"action": "chatops.format_execution_result",
"parameters": {"execution_id": "mock-id-execute"},
"user": "",
}
httpclient.HTTPClient.post.assert_any_call("/executions", expected_args)

mock_stdout = self.stdout.getvalue()

self.assertTrue(
"Execution (mock-id-execute) has been started, waiting for it to finish"
in mock_stdout
)
self.assertTrue(
"Execution (mock-id-format-execution) has been started, waiting for it to "
"finish" in mock_stdout
)
self.assertTrue("Formatted ChatOps result message" in mock_stdout)
self.assertTrue("Result formatted message" in mock_stdout)
2 changes: 1 addition & 1 deletion test-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pre-commit==2.1.0
bandit==1.7.0
ipython<6.0.0
isort>=4.2.5
mock==3.0.3
mock==3.0.5
nose>=1.3.7
tabulate
unittest2
Expand Down