summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--poetry.lock14
-rw-r--r--pyproject.toml1
-rw-r--r--robusta_krr/core/config.py4
-rw-r--r--robusta_krr/core/formatters.py4
-rw-r--r--robusta_krr/core/kubernetes.py4
-rw-r--r--robusta_krr/core/result.py44
-rw-r--r--robusta_krr/core/runner.py3
-rw-r--r--robusta_krr/formatters/__init__.py2
-rw-r--r--robusta_krr/formatters/table.py42
-rw-r--r--robusta_krr/formatters/text.py20
-rw-r--r--robusta_krr/formatters/yaml.py4
-rw-r--r--robusta_krr/main.py2
-rw-r--r--robusta_krr/utils/configurable.py15
13 files changed, 119 insertions, 40 deletions
diff --git a/poetry.lock b/poetry.lock
index ecc9324..83d1017 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -383,6 +383,18 @@ doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1
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"
description = "Backported and Experimental Type Hints for Python 3.7+"
@@ -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))