From d65125aeafb4679929a64e2beac72aee379a36f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D0=B0=D0=B2=D0=B5=D0=BB=20=D0=96=D1=83=D0=BA=D0=BE?= =?UTF-8?q?=D0=B2?= <33721692+LeaveMyYard@users.noreply.github.com> Date: Fri, 24 Feb 2023 17:27:02 +0200 Subject: Implement more formatters, fix verbose flag --- poetry.lock | 14 +++++++++++- pyproject.toml | 1 + robusta_krr/core/config.py | 4 ++-- robusta_krr/core/formatters.py | 4 ++-- robusta_krr/core/kubernetes.py | 4 ++-- robusta_krr/core/result.py | 44 +++++++++++++++++++++++++++++++++++--- robusta_krr/core/runner.py | 3 ++- robusta_krr/formatters/__init__.py | 2 +- robusta_krr/formatters/table.py | 42 ++++++++++++++++++++++++++++++++++++ robusta_krr/formatters/text.py | 20 ----------------- robusta_krr/formatters/yaml.py | 4 +++- robusta_krr/main.py | 2 +- robusta_krr/utils/configurable.py | 15 +++++++------ 13 files changed, 119 insertions(+), 40 deletions(-) create mode 100644 robusta_krr/formatters/table.py delete mode 100644 robusta_krr/formatters/text.py diff --git a/poetry.lock b/poetry.lock index ecc9324..83d1017 100644 --- a/poetry.lock +++ b/poetry.lock @@ -382,6 +382,18 @@ dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2 doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"] +[[package]] +name = "types-pyyaml" +version = "6.0.12.8" +description = "Typing stubs for PyYAML" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "types-PyYAML-6.0.12.8.tar.gz", hash = "sha256:19304869a89d49af00be681e7b267414df213f4eb89634c4495fa62e8f942b9f"}, + {file = "types_PyYAML-6.0.12.8-py3-none-any.whl", hash = "sha256:5314a4b2580999b2ea06b2e5f9a7763d860d6e09cdf21c0e9561daa9cbd60178"}, +] + [[package]] name = "typing-extensions" version = "4.5.0" @@ -397,4 +409,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "494a1b73e35f3b6310b6c149b93dec26d8cd303ce226d1fb6e5a831e674113c0" +content-hash = "8be8e97d0cb36735344fbc3d8f4e341db290785c6eadf3fbe9354fe12876d512" diff --git a/pyproject.toml b/pyproject.toml index b735d6f..440d7bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ mypy = "^1.0.1" black = "^23.1.0" isort = "^5.12.0" flake8 = "^6.0.0" +types-pyyaml = "^6.0.12.8" [build-system] requires = ["poetry-core"] diff --git a/robusta_krr/core/config.py b/robusta_krr/core/config.py index c26bffd..1ee7365 100644 --- a/robusta_krr/core/config.py +++ b/robusta_krr/core/config.py @@ -11,8 +11,8 @@ class Config(pd.BaseSettings): verbose: bool = pd.Field(False) prometheus_url: str | None = pd.Field(None) - format: str = pd.Field("text") - strategy: str = pd.Field("simple") + format: str + strategy: str def create_strategy(self) -> BaseStrategy: StrategyType = BaseStrategy.find(self.strategy) diff --git a/robusta_krr/core/formatters.py b/robusta_krr/core/formatters.py index 50ec90b..8487f7f 100644 --- a/robusta_krr/core/formatters.py +++ b/robusta_krr/core/formatters.py @@ -2,7 +2,7 @@ from __future__ import annotations import abc import os -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from robusta_krr.core.result import Result @@ -20,7 +20,7 @@ class BaseFormatter(abc.ABC): return self.__display_name__.title() @abc.abstractmethod - def format(self, result: Result) -> str: + def format(self, result: Result) -> Any: """Format the result. Args: diff --git a/robusta_krr/core/kubernetes.py b/robusta_krr/core/kubernetes.py index 699345e..3022800 100644 --- a/robusta_krr/core/kubernetes.py +++ b/robusta_krr/core/kubernetes.py @@ -34,6 +34,6 @@ class KubernetesLoader(Configurable): await asyncio.sleep(1.5) # Simulate a slow API call return ResourceAllocations( - requests={ResourceType.cpu: 30, ResourceType.memory: 300}, - limits={ResourceType.cpu: 50, ResourceType.memory: 600}, + requests={ResourceType.CPU: 30, ResourceType.Memory: 300}, + limits={ResourceType.CPU: 50, ResourceType.Memory: 600}, ) diff --git a/robusta_krr/core/result.py b/robusta_krr/core/result.py index 48ba9fd..670a004 100644 --- a/robusta_krr/core/result.py +++ b/robusta_krr/core/result.py @@ -1,4 +1,5 @@ import enum +import itertools from typing import Any import pydantic as pd @@ -8,8 +9,8 @@ from robusta_krr.core.objects import K8sObjectData class ResourceType(str, enum.Enum): - cpu = "cpu" - memory = "memory" + CPU = "cpu" + Memory = "memory" class ResourceAllocations(pd.BaseModel): @@ -25,8 +26,13 @@ class ResourceScan(pd.BaseModel): class Result(pd.BaseModel): scans: list[ResourceScan] + score: float = 0.0 - def format(self, formatter: type[BaseFormatter] | str, **kwargs: Any) -> str: + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.score = self.__calculate_score() + + def format(self, formatter: type[BaseFormatter] | str, **kwargs: Any) -> Any: """Format the result. Args: @@ -39,3 +45,35 @@ class Result(pd.BaseModel): FormatterType = BaseFormatter.find(formatter) if isinstance(formatter, str) else formatter _formatter = FormatterType(**kwargs) return _formatter.format(self) + + @staticmethod + def __percentage_difference(current: float, recommended: float) -> float: + """Get the percentage difference between two numbers. + + Args: + current: The current value. + recommended: The recommended value. + + Returns: + The percentage difference. + """ + + return (recommended - current) / current * 100 + + def __calculate_score(self) -> float: + """Get the score of the result. + + Returns: + The score of the result. + """ + + total_diff = 0.0 + for scan, resource_type in itertools.product(self.scans, ResourceType): + total_diff += self.__percentage_difference( + scan.current.requests[resource_type], scan.recommended.requests[resource_type] + ) + total_diff += self.__percentage_difference( + scan.current.limits[resource_type], scan.recommended.limits[resource_type] + ) + + return max(0, round(100 - total_diff / len(self.scans) / len(ResourceType) / 50, 2)) # 50 is just a constant diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py index 37150e2..20115df 100644 --- a/robusta_krr/core/runner.py +++ b/robusta_krr/core/runner.py @@ -28,7 +28,8 @@ class Runner(Configurable): def _process_result(self, result: Result) -> None: formatted = result.format(self.config.format) - self.echo(formatted) + self.echo("\n", no_prefix=True) + self.console.print(formatted) async def _gather_objects_current_allocations(self, objects: list[K8sObjectData]) -> list[ResourceAllocations]: return await asyncio.gather(*[self._k8s_loader.get_object_current_recommendations(obj) for obj in objects]) diff --git a/robusta_krr/formatters/__init__.py b/robusta_krr/formatters/__init__.py index ca0b95d..cdb12bb 100644 --- a/robusta_krr/formatters/__init__.py +++ b/robusta_krr/formatters/__init__.py @@ -1,3 +1,3 @@ from .json import JSONFormatter -from .text import TextFormatter +from .table import TableFormatter from .yaml import YAMLFormatter diff --git a/robusta_krr/formatters/table.py b/robusta_krr/formatters/table.py new file mode 100644 index 0000000..df2fb88 --- /dev/null +++ b/robusta_krr/formatters/table.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from robusta_krr.core.formatters import BaseFormatter +from robusta_krr.core.result import Result, ResourceType + +from rich.table import Table + + +class TableFormatter(BaseFormatter): + """Formatter for text output.""" + + __display_name__ = "table" + + def format(self, result: Result) -> Table: + """Format the result as text. + + :param result: The result to format. + :type result: :class:`core.result.Result` + :returns: The formatted results. + :rtype: str + """ + + table = Table(show_header=True, header_style="bold magenta", title=f"Scan result ({result.score} points)") + + table.add_column("Number", justify="right", style="dim", no_wrap=True) + table.add_column("Name", style="cyan") + for resource in ResourceType: + table.add_column(f"{resource.name} Requests", style="green") + table.add_column(f"{resource.name} Limits", style="green") + + for i, item in enumerate(result.scans): + table.add_row( + str(i), + f"{item.object.kind} {item.object.namespace}/{item.object.name}", + *[ + f"{getattr(item.current, selector)[resource]} -> {getattr(item.recommended, selector)[resource]}" + for resource in ResourceType + for selector in ["requests", "limits"] + ], + ) + + return table diff --git a/robusta_krr/formatters/text.py b/robusta_krr/formatters/text.py deleted file mode 100644 index fa66bcb..0000000 --- a/robusta_krr/formatters/text.py +++ /dev/null @@ -1,20 +0,0 @@ -from __future__ import annotations - -from robusta_krr.core.formatters import BaseFormatter -from robusta_krr.core.result import Result - - -class TextFormatter(BaseFormatter): - """Formatter for text output.""" - - __display_name__ = "text" - - def format(self, result: Result) -> str: - """Format the result as text. - - :param result: The result to format. - :type result: :class:`core.result.Result` - :returns: The formatted results. - :rtype: str - """ - return "Example result." diff --git a/robusta_krr/formatters/yaml.py b/robusta_krr/formatters/yaml.py index 3ab1fa3..b9d7ca4 100644 --- a/robusta_krr/formatters/yaml.py +++ b/robusta_krr/formatters/yaml.py @@ -2,6 +2,8 @@ from __future__ import annotations from robusta_krr.core.formatters import BaseFormatter from robusta_krr.core.result import Result +import yaml +import json class YAMLFormatter(BaseFormatter): @@ -17,4 +19,4 @@ class YAMLFormatter(BaseFormatter): :returns: The formatted results. :rtype: str """ - raise NotImplementedError + return yaml.dump(json.loads(result.json()), sort_keys=False) diff --git a/robusta_krr/main.py b/robusta_krr/main.py index 5a23833..0aa31f3 100644 --- a/robusta_krr/main.py +++ b/robusta_krr/main.py @@ -23,7 +23,7 @@ def run( "-p", help="Prometheus URL. If not provided, will attempt to find it in kubernetes cluster", ), - format: str = typer.Option("text", "--formatter", "-f", help="Output formatter"), + format: str = typer.Option("table", "--formatter", "-f", help="Output formatter"), strategy: str = typer.Option("simple", "--strategy", "-s", help="Strategy to use"), verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose mode"), quiet: bool = typer.Option(False, "--quiet", "-q", help="Enable quiet mode"), diff --git a/robusta_krr/utils/configurable.py b/robusta_krr/utils/configurable.py index c45d436..dd55f13 100644 --- a/robusta_krr/utils/configurable.py +++ b/robusta_krr/utils/configurable.py @@ -1,9 +1,11 @@ -import typer -from rich import print +from rich.console import Console from robusta_krr.core.config import Config +console = Console() + + class Configurable: """ A class that can be configured with a Config object. @@ -12,6 +14,7 @@ class Configurable: def __init__(self, config: Config) -> None: self.config = config + self.console = console @staticmethod def __add_prefix(text: str, prefix: str, /, no_prefix: bool) -> str: @@ -23,13 +26,13 @@ class Configurable: If quiet mode is enabled, the message will not be echoed """ - if not self.config.quiet and self.config.verbose: - print(self.__add_prefix(message, "[bold green][INFO][/bold green]", no_prefix=no_prefix)) + if not self.config.quiet: + self.console.print(self.__add_prefix(message, "[bold green][INFO][/bold green]", no_prefix=no_prefix)) def debug(self, message: str = "") -> None: """ Echoes a message to the user if verbose mode is enabled """ - if self.config.verbose: - print(self.__add_prefix(message, "[bold green][DEBUG][/bold green]", no_prefix=False)) + if self.config.verbose and not self.config.quiet: + self.console.print(self.__add_prefix(message, "[bold green][DEBUG][/bold green]", no_prefix=False)) -- cgit v1.2.3