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

Added Preflight checks functionality #3874

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
118 changes: 112 additions & 6 deletions cumulusci/cli/checks.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,52 @@
import logging

import click

from cumulusci.cli.ui import CliTable
from cumulusci.core.config import FlowConfig
from cumulusci.core.flowrunner import PreflightFlowCoordinator

from .runtime import pass_runtime


@click.group("checks", help="Commands for running preflight checks for a plan")
@click.group(
"checks",
help="Commands for getting information about Preflight checks for a Radagast plan",
)
def checks():
pass


@checks.command(name="info", help="Displays preflight checks for a plan")
@checks.command(name="info", help="Displays checks for a plan")
@click.argument("plan_name")
@pass_runtime(require_project=True)
def checks_info(runtime, plan_name):
click.echo("This plan has the following preflight checks: ")

"""
Displays checks for a Radagast plan.
"""

# Check if the plan exists or not
plans = runtime.project_config.plans or {}
if plan_name not in plans:
raise click.UsageError(
f"Unknown plan '{plan_name}'. To view available plans run: `cci plan list`"
)

# Get the checks under a plan
preflight_checks = [
[check.get("action", ""), check.get("message", ""), check.get("when", "")]
for check in plans[plan_name].get("checks", [])
]
# Create Cli Table to display the checks
plan_preflight_checks_table = CliTable(
title="Plan Preflights",
data=[
["Action", "Message", "When"],
*preflight_checks,
],
)
plan_preflight_checks_table.echo()


@checks.command(name="run", help="Runs checks under a plan")
Expand All @@ -23,8 +57,80 @@ def checks_info(runtime, plan_name):
)
@pass_runtime(require_keychain=True, require_project=True)
def checks_run(runtime, plan_name, org):
"""
Runs checks for a Radagast plan.
"""
plans = runtime.project_config.plans or {}

# Check if the plan exists or not
if plan_name not in plans:
raise click.UsageError(
f"Unknown plan '{plan_name}'. To view available plans run: `cci plan list`"
)

logger = logging.getLogger("cumulusci.flows")
org_logger = logging.getLogger("cumulusci.core.config.base_config")

def _rule(fill="=", length=60, new_line=False):
logger.info(f"{fill * length}")
if new_line:
logger.info("")

# Get necessary configs
org, org_config = runtime.get_org(org)
m = "Running checks for the plan " + plan_name
click.echo(m)

# Print the org details
_rule(fill="-", new_line=False)
logger.info("Organization:")
logger.info(f" Username: {org_config.username}")
logger.info(f" Org Id: {org_config.org_id}")
_rule(fill="-", new_line=True)
logger.info(f"Running preflight checks for the plan {plan_name} ...")
_rule(new_line=True)
checks = plans[plan_name]["checks"]

# Check if there are no checks available under the plan
if len(checks) == 0:
raise click.UsageError(
f"No checks exists for the '{plan_name}'. To view available checks run: `cci checks info`"
)

# Run the preflight checks under the plan
flow_config = FlowConfig({"checks": checks, "steps": {}})
flow_coordinator = PreflightFlowCoordinator(
runtime.project_config,
flow_config,
name="preflight",
)
# Ignore the logs coming via the pre flight coordinator execution
logger.setLevel(logging.WARNING)
org_logger.setLevel(logging.WARNING)
flow_coordinator.run(org_config)
logger.setLevel(logging.INFO)
org_logger.setLevel(logging.INFO)
results = flow_coordinator.preflight_results

# Check if the there are any errors/warnings while running checks
if results:
raise_error = False
print(results.items())
for step_name, step_results in results.items():
table_header_row = ["Status", "Message"]
table_data = [table_header_row]

for result in step_results:
table_data.append([result["status"], result["message"]])
if result["status"] == "error":
raise_error = True
table = CliTable(
table_data,
)
table.echo(plain=True)
if raise_error:
# Raise an exception if there are any failed pre flight checks
raise Exception(
"Some of the checks failed with errors. Please check the logs for details."
)
else:
logger.info("The preflight checks ran succesfully with the warnings.")
else:
logger.info("The preflight checks ran succesfully.")
183 changes: 147 additions & 36 deletions cumulusci/cli/tests/test_checks.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,174 @@
from unittest import mock

import click
import pytest

from cumulusci.cli.runtime import CliRuntime

from .. import checks
from .utils import run_click_command


@mock.patch("click.echo")
def test_flow_info(echo):

runtime = CliRuntime(
config={
"flows": {
"test": {
"steps": {
1: {
"task": "test_task",
"options": {"option_name": "option_value"},
@pytest.fixture
def runtime():
runtime = CliRuntime()
runtime.project_config.config["plans"] = {
"plan 1": {
"title": "Test Plan #1",
"slug": "plan1_slug",
"tier": "primary",
"preflight_message": "This is a preflight message",
"error_message": "This is an error message",
"steps": {
1: {
"task": "run_tests",
"ui_options": {
"name": "Run Tests",
"is_recommended": False,
"is_required": False,
},
"checks": [
{
"when": "soon",
"action": "error",
"message": "Danger Will Robinson!",
}
}
],
}
},
"tasks": {
"test_task": {
"class_path": "cumulusci.cli.tests.test_flow.DummyTask",
"description": "Test Task",
"checks": [
{
"when": "'test package' not in tasks.get_installed_packages()",
"action": "error",
"message": "Test Package must be installed in your org.",
}
},
],
},
load_keychain=False,
}

yield runtime


@mock.patch("cumulusci.cli.checks.CliTable")
def test_checks_info(cli_table, runtime):

run_click_command(checks.checks_info, runtime=runtime, plan_name="plan 1")
cli_table.assert_called_once_with(
title="Plan Preflights",
data=[
["Action", "Message", "When"],
[
"error",
"Test Package must be installed in your org.",
"'test package' not in tasks.get_installed_packages()",
],
],
)

run_click_command(checks.checks_info, runtime=runtime, plan_name="test")

echo.assert_called_with("This plan has the following preflight checks: ")
@mock.patch("cumulusci.cli.checks.PreflightFlowCoordinator")
def test_checks_run(mock_flow_coordinator, runtime):
org_config = mock.Mock(scratch=True, config={})
org_config.username = "test_user"
org_config.org_id = "test_org_id"
runtime.get_org = mock.Mock(return_value=("test", org_config))

mock_flow_coordinator_return_value = mock.Mock()
mock_flow_coordinator_return_value.preflight_results = {} # No errors or warnings
mock_flow_coordinator.return_value = mock_flow_coordinator_return_value
with mock.patch("logging.getLogger") as mock_logger:

run_click_command(
checks.checks_run,
runtime=runtime,
plan_name="plan 1",
org="test",
)
mock_logger.assert_called()
mock_flow_coordinator.assert_called_once_with(
runtime.project_config, mock.ANY, name="preflight"
)


@mock.patch("click.echo")
def test_checks_run(echo):
@mock.patch("cumulusci.cli.checks.PreflightFlowCoordinator")
@mock.patch("logging.getLogger")
def test_checks_run_unknown_plan(mock_flow_coordinator, mock_logger, runtime):
org_config = mock.Mock(scratch=True, config={})
runtime = CliRuntime(
config={
"flows": {"test": {"steps": {1: {"task": "test_task"}}}},
"tasks": {
"test_task": {
"class_path": "cumulusci.cli.tests.test_flow.DummyTask",
"description": "Test Task",
}
},
},
load_keychain=False,
)
org_config.username = "test_user"
org_config.org_id = "test_org_id"
runtime.get_org = mock.Mock(return_value=("test", org_config))

mock_flow_coordinator_return_value = mock.Mock()
mock_flow_coordinator_return_value.preflight_results = {} # No errors or warnings
mock_flow_coordinator.return_value = mock_flow_coordinator_return_value
with pytest.raises(click.UsageError) as error:

run_click_command(
checks.checks_run,
runtime=runtime,
plan_name="unknown plan",
org="test",
)
mock_flow_coordinator.assert_called_once_with(
runtime.project_config, mock.ANY, name="preflight"
)
assert "Unknown plan 'unknown_plan'" in error.value


@mock.patch("cumulusci.cli.checks.PreflightFlowCoordinator")
def test_checks_run_with_warnings(mock_flow_coordinator, runtime):
org_config = mock.Mock(scratch=True, config={})
org_config.username = "test_user"
org_config.org_id = "test_org_id"
runtime.get_org = mock.Mock(return_value=("test", org_config))

mock_flow_coordinator.return_value.preflight_results = {
None: [
{
"status": "warning",
"message": "You need Context Service AdminPsl in your Org assigned Admin User to use this feature. Contact your Administrator.",
}
]
}

run_click_command(
checks.checks_run,
runtime=runtime,
plan_name="test",
plan_name="plan 1",
org="test",
)
mock_flow_coordinator.assert_called_once_with(
runtime.project_config, mock.ANY, name="preflight"
)


@mock.patch("cumulusci.cli.checks.PreflightFlowCoordinator")
def test_checks_run_with_errors(mock_flow_coordinator, runtime):
org_config = mock.Mock(scratch=True, config={})
org_config.username = "test_user"
org_config.org_id = "test_org_id"
runtime.get_org = mock.Mock(return_value=("test", org_config))

mock_flow_coordinator.return_value.preflight_results = {
None: [
{
"status": "error",
"message": "You need Context Service AdminPsl in your Org assigned Admin User to use this feature. Contact your Administrator.",
}
]
}
with pytest.raises(Exception) as error:

echo.assert_called_with("Running checks for the plan test")
run_click_command(
checks.checks_run,
runtime=runtime,
plan_name="plan 1",
org="test",
)
mock_flow_coordinator.assert_called_once_with(
runtime.project_config, mock.ANY, name="preflight"
)
assert (
"Some of the checks failed with errors. Please check the logs for details."
in error.value
)
Loading
Loading