Skip to content

Commit

Permalink
[resotocore][feat] Allow filtering of report checks (#1637)
Browse files Browse the repository at this point in the history
  • Loading branch information
aquamatthias committed Jun 5, 2023
1 parent 7fe700b commit 47605cc
Show file tree
Hide file tree
Showing 5 changed files with 57 additions and 26 deletions.
28 changes: 16 additions & 12 deletions resotocore/resotocore/cli/cli.py
Expand Up @@ -499,11 +499,16 @@ async def combine_query_parts(
parts = list(takewhile(lambda x: isinstance(x.command, SearchCLIPart), commands))
if parts:
query, options, query_parts = await self.create_query(parts, ctx)
ctx_wq = evolve(ctx, query=query, query_options=options)
# re-evaluate remaining commands - to take the adapted context into account
remaining = [self.command(c.name, c.arg, ctx_wq) for c in commands[len(parts) :]] # noqa: E203
return ctx_wq, [*query_parts, *remaining]
return ctx, commands
ctx_wq = evolve(ctx, query=query, query_options=options, commands=commands)
remaining = [
self.command(c.name, c.arg, ctx_wq, position=pos) for pos, c in enumerate(commands[len(parts) :])
] # noqa: E203
rewritten_parts = [*query_parts, *remaining]
else:
ctx_wq = evolve(ctx, commands=commands)
rewritten_parts = [self.command(c.name, c.arg, ctx_wq, position=pos) for pos, c in enumerate(commands)]
# re-evaluate remaining commands - to take the adapted context into account
return ctx_wq, rewritten_parts

def rewrite_command_line(cmds: List[ExecutableCommand], ctx: CLIContext) -> List[ExecutableCommand]:
"""
Expand Down Expand Up @@ -554,13 +559,12 @@ def adjust_context(parsed: ParsedCommands) -> CLIContext:

async def parse_line(parsed: ParsedCommands) -> ParsedCommandLine:
ctx = adjust_context(parsed)
ctx, commands = await combine_query_parts(
[self.command(c.cmd, c.args, ctx, position=i) for i, c in enumerate(parsed.commands)], ctx
)
rewritten = rewrite_command_line(commands, ctx)
not_met = [r for cmd in rewritten for r in cmd.action.required if r.name not in context.uploaded_files]
envelope = {k: v for cmd in rewritten for k, v in cmd.action.envelope.items()}
return ParsedCommandLine(ctx, parsed, rewritten, not_met, envelope)
executable = [self.command(c.cmd, c.args, ctx, position=i) for i, c in enumerate(parsed.commands)]
rewritten = rewrite_command_line(executable, ctx)
ctx, commands = await combine_query_parts(rewritten, ctx)
not_met = [r for cmd in commands for r in cmd.action.required if r.name not in context.uploaded_files]
envelope = {k: v for cmd in commands for k, v in cmd.action.envelope.items()}
return ParsedCommandLine(ctx, parsed, commands, not_met, envelope)

def expand_aliases(line: ParsedCommands) -> ParsedCommands:
def expand_alias(alias_cmd: ParsedCommand) -> List[ParsedCommand]:
Expand Down
19 changes: 11 additions & 8 deletions resotocore/resotocore/cli/command.py
Expand Up @@ -67,6 +67,7 @@
parse_time_or_delta,
strip_quotes,
)
from resotocore.cli.dependencies import CLIDependencies
from resotocore.cli.model import (
CLICommand,
CLIContext,
Expand All @@ -83,8 +84,8 @@
NoTerminalOutput,
ArgsInfo,
ArgInfo,
EntityProvider,
)
from resotocore.cli.dependencies import CLIDependencies
from resotocore.cli.tip_of_the_day import SuggestionPolicy, SuggestionStrategy, get_suggestion_strategy
from resotocore.config import ConfigEntity
from resotocore.db.async_arangodb import AsyncCursor
Expand Down Expand Up @@ -170,7 +171,7 @@
# Such a part is not executed, but builds a search, which is executed.
# Therefore, the parse method is implemented in a dummy fashion here.
# The real interpretation happens in CLI.create_query.
class SearchCLIPart(CLICommand, ABC):
class SearchCLIPart(CLICommand, EntityProvider, ABC):
def parse(self, arg: Optional[str] = None, ctx: CLIContext = EmptyContext, **kwargs: Any) -> CLIAction:
return CLISource.empty()

Expand Down Expand Up @@ -1309,7 +1310,7 @@ async def to_count(in_stream: AsyncIterator[JsonElement]) -> AsyncIterator[JsonE
return CLIFlow(to_count)


class ExecuteSearchCommand(CLICommand, InternalPart):
class ExecuteSearchCommand(CLICommand, InternalPart, EntityProvider):
"""
```shell
execute_search [--with-edges] [--explain] <search-statement>
Expand Down Expand Up @@ -1891,7 +1892,7 @@ def show(k: ComplexKind) -> bool:
return CLISource(source)


class SetDesiredStateBase(CLICommand, ABC):
class SetDesiredStateBase(CLICommand, EntityProvider, ABC):
@abstractmethod
def patch(self, arg: Optional[str], ctx: CLIContext) -> Json:
# deriving classes need to define how to patch
Expand Down Expand Up @@ -2027,7 +2028,7 @@ async def set_desired(
yield elem


class SetMetadataStateBase(CLICommand, ABC):
class SetMetadataStateBase(CLICommand, EntityProvider, ABC):
@abstractmethod
def patch(self, arg: Optional[str], ctx: CLIContext) -> Json:
# deriving classes need to define how to patch
Expand Down Expand Up @@ -4656,7 +4657,7 @@ async def create_certificate(
return CLISource.single(lambda: stream.just(self.rendered_help(ctx)))


class ReportCommand(CLICommand):
class ReportCommand(CLICommand, EntityProvider):
"""
```shell
report benchmarks list
Expand Down Expand Up @@ -4737,6 +4738,7 @@ def args_info(self) -> ArgsInfo:
possible_values=[s.value for s in ReportSeverity],
),
ArgInfo("--only-failing", False, help_text="Filter only failing checks."),
ArgInfo("--only-check-results", False, help_text="Only dump results."),
]
return {
"benchmark": {
Expand Down Expand Up @@ -4803,7 +4805,7 @@ async def run_benchmark(parsed_args: Namespace) -> AsyncIterator[Json]:
only_failing=parsed_args.only_failing,
)
if not result.is_empty():
for node in result.to_graph():
for node in result.to_graph(parsed_args.only_check_results):
yield node

async def run_check(parsed_args: Namespace) -> AsyncIterator[Json]:
Expand All @@ -4815,7 +4817,7 @@ async def run_check(parsed_args: Namespace) -> AsyncIterator[Json]:
only_failing=parsed_args.only_failing,
)
if not parsed_args.only_failing or result.has_failed():
for node in result.to_graph():
for node in result.to_graph(parsed_args.only_check_results):
yield node

async def show_help() -> AsyncIterator[str]:
Expand All @@ -4826,6 +4828,7 @@ async def show_help() -> AsyncIterator[str]:
run_parser.add_argument("--accounts", nargs="+")
run_parser.add_argument("--severity", type=ReportSeverity, choices=list(ReportSeverity))
run_parser.add_argument("--only-failing", action="store_true", default=False)
run_parser.add_argument("--only-check-results", action="store_true", default=False)

action = self.action_from_arg(arg)
args = re.split("\\s+", arg.strip(), maxsplit=2) if arg else []
Expand Down
15 changes: 13 additions & 2 deletions resotocore/resotocore/cli/model.py
Expand Up @@ -65,6 +65,7 @@ class CLIContext:
uploaded_files: Dict[str, str] = field(factory=dict) # id -> path
query: Optional[Query] = None
query_options: Dict[str, Any] = field(factory=dict)
commands: List[ExecutableCommand] = field(factory=list)
console_renderer: Optional[ConsoleRenderer] = None
source: Optional[str] = None # who is calling

Expand All @@ -73,8 +74,12 @@ def graph_name(self) -> GraphName:
return GraphName(self.env["graph"])

def variable_in_section(self, variable: str) -> str:
# if there is no query, always assume the root section
section = self.env.get("section") if self.query else PathRoot
# if there is no entity provider, always assume the root section
section = (
self.env.get("section")
if self.query or self.commands and isinstance(self.commands[0].command, EntityProvider)
else PathRoot
)
return variable_to_absolute(section, variable)

def render_console(self, element: Union[str, JupyterMixin]) -> str:
Expand Down Expand Up @@ -528,6 +533,12 @@ class InternalPart(ABC):
"""


class EntityProvider(ABC):
"""
Mark this command as a provider of entities with: id, reported, desired, metadata.
"""


class OutputTransformer(ABC):
"""
Mark all commands that transform the output stream (formatting).
Expand Down
15 changes: 11 additions & 4 deletions resotocore/resotocore/report/__init__.py
Expand Up @@ -284,17 +284,24 @@ def from_node(js: Json) -> BenchmarkResult:
severity=if_set(reported.get("severity"), ReportSeverity),
)

def to_graph(self) -> List[Json]:
def to_graph(self, only_checks: bool = False) -> List[Json]:
result = []

def visit_check_collection(collection: CheckCollectionResult) -> None:
result.append(collection.to_node())
if not only_checks:
result.append(collection.to_node())
for check in collection.checks:
result.append(check.to_node())
result.append({"from": collection.node_id, "to": check.node_id, "type": "edge", "edge_type": "default"})
if not only_checks:
result.append(
{"from": collection.node_id, "to": check.node_id, "type": "edge", "edge_type": "default"}
)
for child in collection.children:
visit_check_collection(child)
result.append({"from": collection.node_id, "to": child.node_id, "type": "edge", "edge_type": "default"})
if not only_checks:
result.append(
{"from": collection.node_id, "to": child.node_id, "type": "edge", "edge_type": "default"}
)

visit_check_collection(self)
return result
Expand Down
Expand Up @@ -16,3 +16,9 @@ async def test_benchmark_renderer(inspector_service: InspectorService, test_benc
render_result[0]
== "# Report for account sub_root\n\nTitle: test\n\nVersion: 1.5\n\nSummary: all 2 checks failed\n\n## Failed Checks \n\n- ❌ medium: Test\n- ❌ medium: Test\n\n\n## Section 1 (all checks ❌)\n\nTest section.\n\n- ❌ **medium**: Test\n\n - Risk: Some risk\n\n - There are 11 `foo` resources failing this check.\n\n - Remediation: Some remediation text. See [Link](https://example.com) for more details.\n\n## Section 2 (all checks ❌)\n\nTest section.\n\n- ❌ **medium**: Test\n\n - Risk: Some risk\n\n - There are 11 `foo` resources failing this check.\n\n - Remediation: Some remediation text. See [Link](https://example.com) for more details.\n\n" # noqa: E501
)

# only render checks
check_result = bench_result.to_graph(True)
assert len(check_result) == 2
for c in check_result:
assert c["kind"] == "report_check_result"

0 comments on commit 47605cc

Please sign in to comment.