Skip to content

Commit 988870f

Browse files
committed
Added Preflight checks functionality
1 parent 4c88b7d commit 988870f

3 files changed

Lines changed: 361 additions & 0 deletions

File tree

cumulusci/cli/checks.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import logging
2+
3+
import click
4+
5+
from cumulusci.cli.ui import CliTable
6+
from cumulusci.core.config import FlowConfig
7+
from cumulusci.core.flowrunner import PreflightFlowCoordinator
8+
9+
from .runtime import pass_runtime
10+
11+
12+
@click.group(
13+
"checks",
14+
help="Commands for getting information about Preflight checks for a Radagast plan",
15+
)
16+
def checks():
17+
pass
18+
19+
20+
@checks.command(name="info", help="Displays checks for a plan")
21+
@click.argument("plan_name")
22+
@pass_runtime(require_project=True)
23+
def checks_info(runtime, plan_name):
24+
25+
"""
26+
Displays checks for a Radagast plan.
27+
"""
28+
29+
# Check if the plan exists or not
30+
plans = runtime.project_config.plans or {}
31+
if plan_name not in plans:
32+
raise click.UsageError(
33+
f"Unknown plan '{plan_name}'. To view available plans run: `cci plan list`"
34+
)
35+
36+
# Get the checks under a plan
37+
preflight_checks = [
38+
[check.get("action", ""), check.get("message", ""), check.get("when", "")]
39+
for check in plans[plan_name].get("checks", [])
40+
]
41+
# Create Cli Table to display the checks
42+
plan_preflight_checks_table = CliTable(
43+
title="Plan Preflights",
44+
data=[
45+
["Action", "Message", "When"],
46+
*preflight_checks,
47+
],
48+
)
49+
plan_preflight_checks_table.echo()
50+
51+
52+
@checks.command(name="run", help="Runs checks under a plan")
53+
@click.argument("plan_name")
54+
@click.option(
55+
"--org",
56+
help="Specify the target org. By default, runs against the current default org",
57+
)
58+
@pass_runtime(require_keychain=True, require_project=True)
59+
def checks_run(runtime, plan_name, org):
60+
"""
61+
Runs checks for a Radagast plan.
62+
"""
63+
plans = runtime.project_config.plans or {}
64+
65+
# Check if the plan exists or not
66+
if plan_name not in plans:
67+
raise click.UsageError(
68+
f"Unknown plan '{plan_name}'. To view available plans run: `cci plan list`"
69+
)
70+
71+
logger = logging.getLogger("cumulusci.flows")
72+
org_logger = logging.getLogger("cumulusci.core.config.base_config")
73+
74+
def _rule(fill="=", length=60, new_line=False):
75+
logger.info(f"{fill * length}")
76+
if new_line:
77+
logger.info("")
78+
79+
org, org_config = runtime.get_org(org)
80+
81+
# Print the org details
82+
_rule(fill="-", new_line=False)
83+
logger.info("Organization:")
84+
logger.info(f" Username: {org_config.username}")
85+
logger.info(f" Org Id: {org_config.org_id}")
86+
_rule(fill="-", new_line=True)
87+
logger.info(f"Running preflight checks for the plan {plan_name} ...")
88+
_rule(new_line=True)
89+
checks = plans[plan_name]["checks"]
90+
91+
# Check if there are no checks available under the plan
92+
if checks.length == 0:
93+
raise click.UsageError(
94+
f"No checks exists for the '{plan_name}'. To view available checks run: `cci checks info`"
95+
)
96+
97+
# Run the preflight checks under the plan
98+
flow_config = FlowConfig({"checks": checks, "steps": {}})
99+
flow_coordinator = PreflightFlowCoordinator(
100+
runtime.project_config,
101+
flow_config,
102+
name="preflight",
103+
)
104+
# Ignore the logs coming via the pre flight coordinator execution
105+
logger.setLevel(logging.WARNING)
106+
org_logger.setLevel(logging.WARNING)
107+
flow_coordinator.run(org_config)
108+
logger.setLevel(logging.INFO)
109+
org_logger.setLevel(logging.INFO)
110+
results = flow_coordinator.preflight_results
111+
112+
# Check if the there are any errors/warnings while running checks
113+
if results:
114+
raise_error = False
115+
print(results.items())
116+
for step_name, step_results in results.items():
117+
table_header_row = ["Status", "Message"]
118+
table_data = [table_header_row]
119+
120+
for result in step_results:
121+
table_data.append([result["status"], result["message"]])
122+
if result["status"] == "error":
123+
raise_error = True
124+
table = CliTable(
125+
table_data,
126+
)
127+
table.echo(plain=True)
128+
if raise_error:
129+
# Raise an exception if there are any failed pre flight checks
130+
raise Exception(
131+
"Some of the checks failed with errors. Please check the logs for details."
132+
)
133+
else:
134+
logger.info("The preflight checks ran succesfully with the warnings.")
135+
else:
136+
logger.info("The preflight checks ran succesfully.")

cumulusci/cli/tests/test_checks.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
from unittest import mock
2+
3+
import click
4+
import pytest
5+
6+
from cumulusci.cli.runtime import CliRuntime
7+
8+
from .. import checks
9+
from .utils import run_click_command
10+
11+
12+
@pytest.fixture
13+
def runtime():
14+
runtime = CliRuntime()
15+
runtime.project_config.config["plans"] = {
16+
"plan 1": {
17+
"title": "Test Plan #1",
18+
"slug": "plan1_slug",
19+
"tier": "primary",
20+
"preflight_message": "This is a preflight message",
21+
"error_message": "This is an error message",
22+
"steps": {
23+
1: {
24+
"task": "run_tests",
25+
"ui_options": {
26+
"name": "Run Tests",
27+
"is_recommended": False,
28+
"is_required": False,
29+
},
30+
"checks": [
31+
{
32+
"when": "soon",
33+
"action": "error",
34+
"message": "Danger Will Robinson!",
35+
}
36+
],
37+
}
38+
},
39+
"checks": [
40+
{
41+
"when": "'test package' not in tasks.get_installed_packages()",
42+
"action": "error",
43+
"message": "Test Package must be installed in your org.",
44+
}
45+
],
46+
},
47+
}
48+
49+
yield runtime
50+
51+
52+
@mock.patch("cumulusci.cli.checks.CliTable")
53+
def test_checks_info(cli_table, runtime):
54+
55+
run_click_command(checks.checks_info, runtime=runtime, plan_name="plan 1")
56+
cli_table.assert_called_once_with(
57+
title="Plan Preflights",
58+
data=[
59+
["Action", "Message", "When"],
60+
[
61+
"error",
62+
"Test Package must be installed in your org.",
63+
"'test package' not in tasks.get_installed_packages()",
64+
],
65+
],
66+
)
67+
68+
69+
@mock.patch("cumulusci.cli.checks.PreflightFlowCoordinator")
70+
def test_checks_run(mock_flow_coordinator, runtime):
71+
org_config = mock.Mock(scratch=True, config={})
72+
org_config.username = "test_user"
73+
org_config.org_id = "test_org_id"
74+
runtime.get_org = mock.Mock(return_value=("test", org_config))
75+
76+
mock_flow_coordinator_return_value = mock.Mock()
77+
mock_flow_coordinator_return_value.preflight_results = {} # No errors or warnings
78+
mock_flow_coordinator.return_value = mock_flow_coordinator_return_value
79+
with mock.patch("logging.getLogger") as mock_logger:
80+
81+
run_click_command(
82+
checks.checks_run,
83+
runtime=runtime,
84+
plan_name="plan 1",
85+
org="test",
86+
)
87+
mock_logger.assert_called()
88+
mock_flow_coordinator.assert_called_once_with(
89+
runtime.project_config, mock.ANY, name="preflight"
90+
)
91+
92+
93+
@mock.patch("cumulusci.cli.checks.PreflightFlowCoordinator")
94+
@mock.patch("logging.getLogger")
95+
def test_checks_run_unknown_plan(mock_flow_coordinator, mock_logger, runtime):
96+
org_config = mock.Mock(scratch=True, config={})
97+
org_config.username = "test_user"
98+
org_config.org_id = "test_org_id"
99+
runtime.get_org = mock.Mock(return_value=("test", org_config))
100+
101+
mock_flow_coordinator_return_value = mock.Mock()
102+
mock_flow_coordinator_return_value.preflight_results = {} # No errors or warnings
103+
mock_flow_coordinator.return_value = mock_flow_coordinator_return_value
104+
with pytest.raises(click.UsageError) as error:
105+
106+
run_click_command(
107+
checks.checks_run,
108+
runtime=runtime,
109+
plan_name="unknown plan",
110+
org="test",
111+
)
112+
mock_flow_coordinator.assert_called_once_with(
113+
runtime.project_config, mock.ANY, name="preflight"
114+
)
115+
assert "Unknown plan 'unknown_plan'" in error.value
116+
117+
118+
@mock.patch("cumulusci.cli.checks.PreflightFlowCoordinator")
119+
def test_checks_run_with_warnings(mock_flow_coordinator, runtime):
120+
org_config = mock.Mock(scratch=True, config={})
121+
org_config.username = "test_user"
122+
org_config.org_id = "test_org_id"
123+
runtime.get_org = mock.Mock(return_value=("test", org_config))
124+
125+
mock_flow_coordinator.return_value.preflight_results = {
126+
None: [
127+
{
128+
"status": "warning",
129+
"message": "You need Context Service AdminPsl in your Org assigned Admin User to use this feature. Contact your Administrator.",
130+
}
131+
]
132+
}
133+
134+
run_click_command(
135+
checks.checks_run,
136+
runtime=runtime,
137+
plan_name="plan 1",
138+
org="test",
139+
)
140+
mock_flow_coordinator.assert_called_once_with(
141+
runtime.project_config, mock.ANY, name="preflight"
142+
)
143+
144+
145+
@mock.patch("cumulusci.cli.checks.PreflightFlowCoordinator")
146+
def test_checks_run_with_errors(mock_flow_coordinator, runtime):
147+
org_config = mock.Mock(scratch=True, config={})
148+
org_config.username = "test_user"
149+
org_config.org_id = "test_org_id"
150+
runtime.get_org = mock.Mock(return_value=("test", org_config))
151+
152+
mock_flow_coordinator.return_value.preflight_results = {
153+
None: [
154+
{
155+
"status": "error",
156+
"message": "You need Context Service AdminPsl in your Org assigned Admin User to use this feature. Contact your Administrator.",
157+
}
158+
]
159+
}
160+
with pytest.raises(Exception) as error:
161+
162+
run_click_command(
163+
checks.checks_run,
164+
runtime=runtime,
165+
plan_name="plan 1",
166+
org="test",
167+
)
168+
mock_flow_coordinator.assert_called_once_with(
169+
runtime.project_config, mock.ANY, name="preflight"
170+
)
171+
assert (
172+
"Some of the checks failed with errors. Please check the logs for details."
173+
in error.value
174+
)

docs/cli.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,43 @@ $ cci plan info config
253253
7 Express Setup - Advisor Sharing Metadata No Yes
254254
```
255255

256+
## Checks Info and Options
257+
258+
Your project may have one or more defined MetaDeploy plans, though none
259+
come preconfigured with CumulusCI. The plans will have multiple preflight checks, for
260+
listing the checks on plan `<name>`, run the following command:
261+
262+
```console
263+
$ cci checks info <name>
264+
```
265+
266+
Information about each preflight check under a plan includes:
267+
268+
- Action
269+
- Message
270+
- When
271+
272+
By default all of the above information is displayed.
273+
274+
The following example shows the output of a typical plan, in this case a
275+
plan named 'install'.
276+
277+
```console
278+
$ cci checks info install
279+
Plan Preflights
280+
281+
Action Message When
282+
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
283+
error You need ComplianceAdminAddOn licence in your Org to use this feature. Contact your Administrator. 'ComplianceAdminPsl' not in tasks.get_available_permission_set_licenses()
284+
error You need ComplianceUserAddOn licence in your Org to use this feature. Contact your Administrator. 'ComplianceUserPsl' not in tasks.get_available_permission_set_licenses()
285+
error You need BREDesigner Psl in your Org assigned Admin User to use this feature. Contact your Administrator. 'BREDesigner' not in tasks.get_assigned_permission_set_licenses()
286+
error You need BRE Runtime Psl in your Org assigned Admin User to use this feature. Contact your Administrator. 'BRERuntime' not in tasks.get_assigned_permission_set_licenses()
287+
warning You need Context Service AdminPsl in your Org assigned Admin User to use this feature. Contact your Administrator. 'ContextServiceAdminPsl' not in tasks.get_assigned_permission_set_licenses()
288+
warning You need Context Service Runtime Psl in your Org assigned to Admin User to use this feature. Contact your Administrator. 'ContextServiceRuntimePsl' not in tasks.get_assigned_permission_set_licenses()
289+
error You need Decision Explainer PSL in your Org to use this feature. Contact your Administrator. 'DecisionExplainerPSL' not in tasks.get_available_permission_set_licenses()
290+
291+
```
292+
256293
## Run Tasks and Flows
257294

258295
Execute a specific task or flow with the `run` command.
@@ -272,6 +309,20 @@ For example, the `run_tests` task executes Apex unit tests in a given
272309
org. Assuming there exists an org named `dev`, you can run this task
273310
against it with the command `cci task run run_tests --org dev`.
274311

312+
## Run Checks
313+
314+
Execute preflight checks for a plan with the `run` command.
315+
316+
```console
317+
$ cci checks run <name> --org <org>
318+
```
319+
320+
This command runs the preflight checks for the plan `<name>` against the org `<org>`.
321+
322+
```{tip}
323+
You can see a list of available orgs by running `cci org list`.
324+
```
325+
275326
### Get Help Running Tasks
276327

277328
If you're not certain about what a specific command does, use the

0 commit comments

Comments
 (0)