summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorПавел Жуков <33721692+LeaveMyYard@users.noreply.github.com>2023-02-24 14:47:44 +0200
committerПавел Жуков <33721692+LeaveMyYard@users.noreply.github.com>2023-02-24 14:47:44 +0200
commit26b0cf460167f5c6462b39b551ff4f138f86682c (patch)
tree6e197f23ba259e5fa156bf81a8e49f7b86b129af
parent6fade1efbc920637b42ff336f576cc7604d90627 (diff)
Finish code structure
-rw-r--r--.pre-commit-config.yaml31
-rw-r--r--pyproject.toml2
-rw-r--r--robusta_krr/__init__.py (renamed from robusta_krr/core/loader.py)0
-rw-r--r--robusta_krr/core/__init__.py0
-rw-r--r--robusta_krr/core/config.py20
-rw-r--r--robusta_krr/core/formatters.py44
-rw-r--r--robusta_krr/core/formatters/__init__.py27
-rw-r--r--robusta_krr/core/formatters/base.py23
-rw-r--r--robusta_krr/core/kubernetes.py39
-rw-r--r--robusta_krr/core/objects.py10
-rw-r--r--robusta_krr/core/prometheus.py27
-rw-r--r--robusta_krr/core/result.py32
-rw-r--r--robusta_krr/core/runner.py78
-rw-r--r--robusta_krr/core/strategies.py64
-rw-r--r--robusta_krr/core/strategies/__init__.py25
-rw-r--r--robusta_krr/core/strategies/base.py24
-rw-r--r--robusta_krr/core/strategies/simple.py19
-rw-r--r--robusta_krr/formatters/__init__.py3
-rw-r--r--robusta_krr/formatters/json.py (renamed from robusta_krr/core/formatters/json.py)13
-rw-r--r--robusta_krr/formatters/text.py (renamed from robusta_krr/core/formatters/text.py)9
-rw-r--r--robusta_krr/formatters/yaml.py (renamed from robusta_krr/core/formatters/yaml.py)9
-rw-r--r--robusta_krr/main.py6
-rw-r--r--robusta_krr/strategies/__init__.py1
-rw-r--r--robusta_krr/strategies/simple.py36
-rw-r--r--robusta_krr/utils/configurable.py3
25 files changed, 372 insertions, 173 deletions
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..a51d234
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,31 @@
+repos:
+ - repo: https://github.com/ambv/black
+ rev: 23.1.0
+ hooks:
+ - id: black
+ language_version: python3
+ args: [--config=pyproject.toml]
+
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v3.3.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+
+ - repo: https://github.com/pycqa/flake8
+ rev: 6.0.0
+ hooks:
+ - id: flake8
+ args: [--config=.flake8]
+
+ - repo: https://github.com/pycqa/isort
+ rev: 5.12.0
+ hooks:
+ - id: isort
+ args: [--settings-path=pyproject.toml]
+
+ - repo: https://github.com/pre-commit/mirrors-mypy
+ rev: v1.0.1
+ hooks:
+ - id: mypy
+ language: system
diff --git a/pyproject.toml b/pyproject.toml
index 79e178d..b735d6f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,7 +9,7 @@ packages = [{include = "robusta_krr"}]
[tool.black]
line-length = 120
-target-version = ['py37']
+target-version = ['py311']
[tool.isort]
line_length = 120
diff --git a/robusta_krr/core/loader.py b/robusta_krr/__init__.py
index e69de29..e69de29 100644
--- a/robusta_krr/core/loader.py
+++ b/robusta_krr/__init__.py
diff --git a/robusta_krr/core/__init__.py b/robusta_krr/core/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/robusta_krr/core/__init__.py
diff --git a/robusta_krr/core/config.py b/robusta_krr/core/config.py
index 57dd939..c26bffd 100644
--- a/robusta_krr/core/config.py
+++ b/robusta_krr/core/config.py
@@ -1,7 +1,9 @@
+from typing import get_args
+
import pydantic as pd
-from robusta_krr.core.formatters import FormatType
-from robusta_krr.core.strategies import StrategySettings, BaseStrategy, get_strategy_from_name
+from robusta_krr.core.formatters import BaseFormatter
+from robusta_krr.core.strategies import BaseStrategy, StrategySettings
class Config(pd.BaseSettings):
@@ -9,14 +11,20 @@ class Config(pd.BaseSettings):
verbose: bool = pd.Field(False)
prometheus_url: str | None = pd.Field(None)
- format: FormatType = pd.Field(FormatType.text)
+ format: str = pd.Field("text")
strategy: str = pd.Field("simple")
- strategy_settings: StrategySettings = pd.Field(StrategySettings())
def create_strategy(self) -> BaseStrategy:
- return get_strategy_from_name(self.strategy)(self.strategy_settings)
+ StrategyType = BaseStrategy.find(self.strategy)
+ StrategySettingsType: type[StrategySettings] = get_args(StrategyType.__orig_bases__[0])[0] # type: ignore
+ return StrategyType(StrategySettingsType())
@pd.validator("strategy")
def validate_strategy(cls, v: str) -> str:
- get_strategy_from_name(v) # raises if strategy is not found
+ BaseStrategy.find(v) # NOTE: raises if strategy is not found
+ return v
+
+ @pd.validator("format")
+ def validate_format(cls, v: str) -> str:
+ BaseFormatter.find(v) # NOTE: raises if strategy is not found
return v
diff --git a/robusta_krr/core/formatters.py b/robusta_krr/core/formatters.py
new file mode 100644
index 0000000..3f7549c
--- /dev/null
+++ b/robusta_krr/core/formatters.py
@@ -0,0 +1,44 @@
+from __future__ import annotations
+
+import abc
+import os
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from robusta_krr.core.result import Result
+
+
+DEFAULT_FORMATTERS_PATH = os.path.join(os.path.dirname(__file__), "formatters")
+
+
+class BaseFormatter(abc.ABC):
+ """Base class for result formatters."""
+
+ __display_name__: str
+
+ @abc.abstractmethod
+ def format(self, result: Result) -> str:
+ """Format the result.
+
+ Args:
+ result: The result to format.
+
+ Returns:
+ The formatted result.
+ """
+
+ @staticmethod
+ def find(name: str) -> type[BaseFormatter]:
+ """Get a strategy from its name."""
+
+ # NOTE: Load default formatters
+ from robusta_krr import formatters as _ # noqa: F401
+
+ formatters = {cls.__display_name__.lower(): cls for cls in BaseFormatter.__subclasses__()}
+ if name.lower() in formatters:
+ return formatters[name.lower()]
+
+ raise ValueError(f"Unknown formatter name: {name}. Available formatters: {', '.join(formatters)}")
+
+
+__all__ = ["BaseFormatter"]
diff --git a/robusta_krr/core/formatters/__init__.py b/robusta_krr/core/formatters/__init__.py
deleted file mode 100644
index 1716063..0000000
--- a/robusta_krr/core/formatters/__init__.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from .base import BaseFormatter
-from .json import JSONFormatter
-from .text import TextFormatter
-from .yaml import YAMLFormatter
-
-from enum import Enum
-
-
-class FormatType(str, Enum):
- json = "json"
- yaml = "yaml"
- text = "text"
-
-
-def get_formatter(format_name: FormatType) -> BaseFormatter:
- match format_name:
- case FormatType.json:
- return JSONFormatter()
- case FormatType.yaml:
- return YAMLFormatter()
- case FormatType.text:
- return TextFormatter()
- case _:
- raise ValueError(f"Unknown formatter: {format_name}")
-
-
-__all__ = ["BaseFormatter", "JSONFormatter", "TextFormatter", "YAMLFormatter", "get_formatter", "FormatType"]
diff --git a/robusta_krr/core/formatters/base.py b/robusta_krr/core/formatters/base.py
deleted file mode 100644
index 416e65e..0000000
--- a/robusta_krr/core/formatters/base.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from __future__ import annotations
-import abc
-
-from typing import TYPE_CHECKING
-
-if TYPE_CHECKING:
- from robusta_krr.core.result import Result
-
-
-class BaseFormatter(abc.ABC):
- """Base class for result formatters."""
-
- @abc.abstractmethod
- def format(self, result: Result) -> str:
- """Format the result.
-
- Args:
- result: The result to format.
-
- Returns:
- The formatted result.
- """
- raise NotImplementedError
diff --git a/robusta_krr/core/kubernetes.py b/robusta_krr/core/kubernetes.py
new file mode 100644
index 0000000..699345e
--- /dev/null
+++ b/robusta_krr/core/kubernetes.py
@@ -0,0 +1,39 @@
+import asyncio
+
+from robusta_krr.core.objects import K8sObjectData
+from robusta_krr.core.result import ResourceAllocations
+from robusta_krr.utils.configurable import Configurable
+
+
+# TODO: We need a way to connect to both being in the cluster and outside of a cluster
+class KubernetesLoader(Configurable):
+ # TODO: This is just a mock data for now, implement this later
+ async def list_scannable_objects(self) -> list[K8sObjectData]:
+ """List all scannable objects.
+
+ Returns:
+ A list of scannable objects.
+ """
+
+ self.debug("Listing scannable objects")
+ await asyncio.sleep(2.5) # Simulate a slow API call
+
+ return [
+ K8sObjectData(name="prometheus", kind="Deployment", namespace="default"),
+ K8sObjectData(name="grafana", kind="Deployment", namespace="default"),
+ K8sObjectData(name="alertmanager", kind="Deployment", namespace="default"),
+ K8sObjectData(name="robusta-runner", kind="Deployment", namespace="default"),
+ K8sObjectData(name="robusta-forwarder", kind="Deployment", namespace="default"),
+ ]
+
+ # TODO: This is just a mock data for now, implement this later
+ async def get_object_current_recommendations(self, object: K8sObjectData) -> ResourceAllocations:
+ from robusta_krr.core.result import ResourceType
+
+ self.debug(f"Getting current recommendations for {object}")
+ 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},
+ )
diff --git a/robusta_krr/core/objects.py b/robusta_krr/core/objects.py
new file mode 100644
index 0000000..370a588
--- /dev/null
+++ b/robusta_krr/core/objects.py
@@ -0,0 +1,10 @@
+import pydantic as pd
+
+
+class K8sObjectData(pd.BaseModel):
+ name: str
+ kind: str
+ namespace: str
+
+ def __str__(self) -> str:
+ return f"{self.kind}/{self.namespace}/{self.name}"
diff --git a/robusta_krr/core/prometheus.py b/robusta_krr/core/prometheus.py
new file mode 100644
index 0000000..fd98004
--- /dev/null
+++ b/robusta_krr/core/prometheus.py
@@ -0,0 +1,27 @@
+import asyncio
+import datetime
+import random
+
+from robusta_krr.core.objects import K8sObjectData
+from robusta_krr.core.result import ResourceType
+from robusta_krr.core.strategies import HistoryData
+from robusta_krr.utils.configurable import Configurable
+
+
+class PrometheusLoader(Configurable):
+ async def gather_data(
+ self,
+ object: K8sObjectData,
+ resource: ResourceType,
+ period: datetime.timedelta,
+ *,
+ timeframe: datetime.timedelta = datetime.timedelta(minutes=1),
+ ) -> HistoryData:
+ # TODO: This is mock function. Implement this later using the Prometheus API
+ self.debug(f"Gathering data for {object} and {resource} for the last {period}")
+ await asyncio.sleep(1.5) # Simulate a slow API call
+ points = int(period / timeframe)
+ return {
+ "container_1": [random.randrange(30, 300) for _ in range(points)],
+ "container_2": [random.randrange(70, 500) for _ in range(points)],
+ }
diff --git a/robusta_krr/core/result.py b/robusta_krr/core/result.py
index bd4f572..48ba9fd 100644
--- a/robusta_krr/core/result.py
+++ b/robusta_krr/core/result.py
@@ -1,12 +1,10 @@
-import pydantic as pd
import enum
+from typing import Any
-from robusta_krr.core.formatters import BaseFormatter, get_formatter, FormatType
-
+import pydantic as pd
-class ResourceRecommendation(pd.BaseModel):
- current: float
- recommended: float
+from robusta_krr.core.formatters import BaseFormatter
+from robusta_krr.core.objects import K8sObjectData
class ResourceType(str, enum.Enum):
@@ -14,22 +12,21 @@ class ResourceType(str, enum.Enum):
memory = "memory"
-class ObjectData(pd.BaseModel):
- name: str
- kind: str
- namespace: str
+class ResourceAllocations(pd.BaseModel):
+ requests: dict[ResourceType, float]
+ limits: dict[ResourceType, float]
class ResourceScan(pd.BaseModel):
- object: ObjectData
- requests: dict[ResourceType, ResourceRecommendation]
- limits: dict[ResourceType, ResourceRecommendation]
+ object: K8sObjectData
+ current: ResourceAllocations
+ recommended: ResourceAllocations
class Result(pd.BaseModel):
scans: list[ResourceScan]
- def format(self, formatter: BaseFormatter | FormatType) -> str:
+ def format(self, formatter: type[BaseFormatter] | str, **kwargs: Any) -> str:
"""Format the result.
Args:
@@ -39,7 +36,6 @@ class Result(pd.BaseModel):
The formatted result.
"""
- if isinstance(formatter, str):
- formatter = get_formatter(formatter)
-
- return formatter.format(self)
+ FormatterType = BaseFormatter.find(formatter) if isinstance(formatter, str) else formatter
+ _formatter = FormatterType(**kwargs)
+ return _formatter.format(self)
diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py
index d9664b7..86816e9 100644
--- a/robusta_krr/core/runner.py
+++ b/robusta_krr/core/runner.py
@@ -1,10 +1,22 @@
-from robusta_krr.core.result import Result
+import asyncio
+
+from robusta_krr.core.config import Config
+from robusta_krr.core.kubernetes import KubernetesLoader
+from robusta_krr.core.objects import K8sObjectData
+from robusta_krr.core.prometheus import PrometheusLoader
+from robusta_krr.core.result import ResourceAllocations, ResourceScan, ResourceType, Result
+from robusta_krr.core.strategies import ResourceRecommendation
from robusta_krr.utils.configurable import Configurable
from robusta_krr.utils.version import get_version
-from robusta_krr.core.strategies import HistoryData, ResourceType
class Runner(Configurable):
+ def __init__(self, config: Config) -> None:
+ super().__init__(config)
+ self._k8s_loader = KubernetesLoader(self.config)
+ self._prometheus_loader = PrometheusLoader(self.config)
+ self._strategy = self.config.create_strategy()
+
def _greet(self) -> None:
self.echo(f"Running Robusta's KRR (Kubernetes Resource Recommender) {get_version()}")
@@ -12,15 +24,63 @@ class Runner(Configurable):
formatted = result.format(self.config.format)
self.echo(formatted)
- def _collect_result(self) -> Result:
- data: HistoryData = {}
- strategy = self.config.create_strategy()
+ 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])
+
+ async def _calculate_object_recommendations(
+ self, object: K8sObjectData, resource: ResourceType
+ ) -> ResourceRecommendation:
+ data = await self._prometheus_loader.gather_data(
+ object,
+ resource,
+ self._strategy.settings.history_timedelta,
+ )
+
+ # NOTE: We run this in a threadpool as the strategy calculation might be CPU intensive
+ # TODO: Maybe we should do it in a processpool instead?
+ # But keep in mind that numpy calcluations will not block the GIL
+ return await asyncio.to_thread(self._strategy.run, data, object, resource)
+
+ async def _gather_objects_recommendations_for_resource(
+ self, objects: list[K8sObjectData], resource: ResourceType
+ ) -> list[ResourceRecommendation]:
+ return await asyncio.gather(*[self._calculate_object_recommendations(obj, resource) for obj in objects])
+
+ async def _gather_objects_recommendations(self, objects: list[K8sObjectData]) -> list[ResourceAllocations]:
+ per_resource_recommendations = await asyncio.gather(
+ *[self._gather_objects_recommendations_for_resource(objects, resource) for resource in ResourceType]
+ )
+
+ return [
+ ResourceAllocations(
+ requests={
+ resource_type: recommendations[i].request
+ for resource_type, recommendations in zip(ResourceType, per_resource_recommendations)
+ },
+ limits={
+ resource_type: recommendations[i].limit
+ for resource_type, recommendations in zip(ResourceType, per_resource_recommendations)
+ },
+ )
+ for i, _ in enumerate(objects)
+ ]
- strategy.run(data, {}, ResourceType.cpu)
+ async def _collect_result(self) -> Result:
+ objects = await self._k8s_loader.list_scannable_objects()
+ async with asyncio.TaskGroup() as tg:
+ current_allocations_task = tg.create_task(self._gather_objects_current_allocations(objects))
+ resource_recommendations_task = tg.create_task(self._gather_objects_recommendations(objects))
- return Result()
+ return Result(
+ scans=[
+ ResourceScan(object=obj, current=current, recommended=recommended)
+ for obj, current, recommended in zip(
+ objects, current_allocations_task.result(), resource_recommendations_task.result()
+ )
+ ]
+ )
- def run(self) -> None:
+ async def run(self) -> None:
self._greet()
- result = self._collect_result()
+ result = await self._collect_result()
self._process_result(result)
diff --git a/robusta_krr/core/strategies.py b/robusta_krr/core/strategies.py
new file mode 100644
index 0000000..8c6130d
--- /dev/null
+++ b/robusta_krr/core/strategies.py
@@ -0,0 +1,64 @@
+from __future__ import annotations
+
+import abc
+import datetime
+from typing import Generic, TypeVar
+
+import pydantic as pd
+
+from robusta_krr.core.result import K8sObjectData, ResourceType
+
+
+class ResourceRecommendation(pd.BaseModel):
+ request: float
+ limit: float
+
+
+class StrategySettings(pd.BaseModel):
+ history_duration: float = pd.Field(
+ 24 * 7 * 2, ge=1, description="The duration of the history data to use (in hours)."
+ )
+
+ @property
+ def history_timedelta(self) -> datetime.timedelta:
+ return datetime.timedelta(hours=self.history_duration)
+
+
+_StrategySettings = TypeVar("_StrategySettings", bound=StrategySettings)
+HistoryData = dict[str, list[float]]
+
+
+class BaseStrategy(abc.ABC, Generic[_StrategySettings]):
+ __display_name__: str
+ settings: _StrategySettings
+
+ def __init__(self, settings: _StrategySettings):
+ self.settings = settings
+
+ @abc.abstractmethod
+ def run(
+ self, history_data: HistoryData, object_data: K8sObjectData, resource_type: ResourceType
+ ) -> ResourceRecommendation:
+ """Run the strategy to calculate the recommendation"""
+
+ @staticmethod
+ def find(name: str) -> type[BaseStrategy]:
+ """Get a strategy from its name."""
+
+ # NOTE: Load default formatters
+ from robusta_krr import strategies as _ # noqa: F401
+
+ strategies = {cls.__display_name__.lower(): cls for cls in BaseStrategy.__subclasses__()}
+ if name.lower() in strategies:
+ return strategies[name.lower()]
+
+ raise ValueError(f"Unknown strategy name: {name}. Available strategies: {', '.join(strategies)}")
+
+
+__all__ = [
+ "BaseStrategy",
+ "StrategySettings",
+ "HistoryData",
+ "K8sObjectData",
+ "ResourceType",
+]
diff --git a/robusta_krr/core/strategies/__init__.py b/robusta_krr/core/strategies/__init__.py
deleted file mode 100644
index 309340e..0000000
--- a/robusta_krr/core/strategies/__init__.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from .base import BaseStrategy, StrategySettings, HistoryData, ObjectData, ResourceType
-from .simple import SimpleStrategy, SimpleStrategySettings
-
-
-def get_strategy_from_name(name: str) -> type[BaseStrategy]:
- """Get a strategy from its name."""
-
- strategies = {cls.__name__.lower(): cls for cls in BaseStrategy.__subclasses__()}
- if name.lower() in strategies:
- return strategies[name.lower()]
-
- raise ValueError(f"Unknown strategy name: {name}. Available strategies: {', '.join(strategies)}")
-
-
-__all__ = [
- "AVAILABLE_STRATEGIES",
- "get_strategy_from_name",
- "BaseStrategy",
- "StrategySettings",
- "HistoryData",
- "ObjectData",
- "ResourceType",
- "SimpleStrategy",
- "SimpleStrategySettings",
-]
diff --git a/robusta_krr/core/strategies/base.py b/robusta_krr/core/strategies/base.py
deleted file mode 100644
index ad2ae33..0000000
--- a/robusta_krr/core/strategies/base.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import abc
-import pydantic as pd
-from typing import Generic, TypeVar
-from robusta_krr.core.result import ResourceType, ObjectData
-
-
-class StrategySettings(pd.BaseModel):
- history_duration: float = pd.Field(
- 24 * 7 * 2, ge=1, description="The duration of the history data to use (in hours)."
- )
-
-
-_StrategySettings = TypeVar("_StrategySettings", bound=StrategySettings)
-HistoryData = dict[str, list[float]]
-
-
-class BaseStrategy(abc.ABC, Generic[_StrategySettings]):
- __name__: str
-
- def __init__(self, settings: _StrategySettings):
- self.settings = settings
-
- def run(self, history_data: HistoryData, object_data: ObjectData, resource_type: ResourceType) -> float:
- raise NotImplementedError
diff --git a/robusta_krr/core/strategies/simple.py b/robusta_krr/core/strategies/simple.py
deleted file mode 100644
index 5d2fe04..0000000
--- a/robusta_krr/core/strategies/simple.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import pydantic as pd
-from .base import BaseStrategy, StrategySettings, HistoryData, ObjectData, ResourceType
-
-
-class SimpleStrategySettings(StrategySettings):
- percentile: float = pd.Field(0.95, gt=0, le=1, description="The percentile to use for the recommendation.")
-
-
-class SimpleStrategy(BaseStrategy[StrategySettings]):
- __name__ = "simple"
-
- def run(self, history_data: HistoryData, object_data: ObjectData, resource_type: ResourceType) -> float:
- return self._calculate_percentile(
- [point for points in history_data.values() for point in points], self.settings.percentile
- )
-
- def _calculate_percentile(self, data: list[float], percentile: float) -> float:
- data = sorted(data)
- return data[int(len(data) * percentile)]
diff --git a/robusta_krr/formatters/__init__.py b/robusta_krr/formatters/__init__.py
new file mode 100644
index 0000000..ca0b95d
--- /dev/null
+++ b/robusta_krr/formatters/__init__.py
@@ -0,0 +1,3 @@
+from .json import JSONFormatter
+from .text import TextFormatter
+from .yaml import YAMLFormatter
diff --git a/robusta_krr/core/formatters/json.py b/robusta_krr/formatters/json.py
index 62d049e..c759131 100644
--- a/robusta_krr/core/formatters/json.py
+++ b/robusta_krr/formatters/json.py
@@ -1,16 +1,14 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
-
-from .base import BaseFormatter
-
-if TYPE_CHECKING:
- from robusta_krr.core.result import Result
+from robusta_krr.core.formatters import BaseFormatter
+from robusta_krr.core.result import Result
class JSONFormatter(BaseFormatter):
"""Formatter for JSON output."""
+ __display_name__ = "json"
+
def format(self, result: Result) -> str:
"""Format the result as JSON.
@@ -19,4 +17,5 @@ class JSONFormatter(BaseFormatter):
:returns: The formatted results.
:rtype: str
"""
- raise NotImplementedError
+
+ return result.json(indent=2)
diff --git a/robusta_krr/core/formatters/text.py b/robusta_krr/formatters/text.py
index 990264c..fa66bcb 100644
--- a/robusta_krr/core/formatters/text.py
+++ b/robusta_krr/formatters/text.py
@@ -1,15 +1,14 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
-from .base import BaseFormatter
-
-if TYPE_CHECKING:
- from robusta_krr.core.result import Result
+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.
diff --git a/robusta_krr/core/formatters/yaml.py b/robusta_krr/formatters/yaml.py
index 6e74251..3ab1fa3 100644
--- a/robusta_krr/core/formatters/yaml.py
+++ b/robusta_krr/formatters/yaml.py
@@ -1,15 +1,14 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
-from .base import BaseFormatter
-
-if TYPE_CHECKING:
- from robusta_krr.core.result import Result
+from robusta_krr.core.formatters import BaseFormatter
+from robusta_krr.core.result import Result
class YAMLFormatter(BaseFormatter):
"""Formatter for YAML output."""
+ __display_name__ = "yaml"
+
def format(self, result: Result) -> str:
"""Format the result as YAML.
diff --git a/robusta_krr/main.py b/robusta_krr/main.py
index 83cf0ff..5a23833 100644
--- a/robusta_krr/main.py
+++ b/robusta_krr/main.py
@@ -1,3 +1,4 @@
+import asyncio
from typing import Optional
import typer
@@ -5,7 +6,6 @@ import typer
from robusta_krr.core.config import Config
from robusta_krr.core.runner import Runner
from robusta_krr.utils.version import get_version
-from robusta_krr.core.formatters import FormatType
app = typer.Typer()
@@ -23,7 +23,7 @@ def run(
"-p",
help="Prometheus URL. If not provided, will attempt to find it in kubernetes cluster",
),
- format: FormatType = typer.Option("text", "--formatter", "-f", help="Output formatter"),
+ format: str = typer.Option("text", "--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"),
@@ -36,7 +36,7 @@ def run(
strategy=strategy,
)
runner = Runner(config)
- runner.run()
+ asyncio.run(runner.run())
if __name__ == "__main__":
diff --git a/robusta_krr/strategies/__init__.py b/robusta_krr/strategies/__init__.py
new file mode 100644
index 0000000..05e029b
--- /dev/null
+++ b/robusta_krr/strategies/__init__.py
@@ -0,0 +1 @@
+from .simple import SimpleStrategy
diff --git a/robusta_krr/strategies/simple.py b/robusta_krr/strategies/simple.py
new file mode 100644
index 0000000..c4a37e3
--- /dev/null
+++ b/robusta_krr/strategies/simple.py
@@ -0,0 +1,36 @@
+import pydantic as pd
+
+from robusta_krr.core.strategies import (
+ BaseStrategy,
+ HistoryData,
+ K8sObjectData,
+ ResourceRecommendation,
+ ResourceType,
+ StrategySettings,
+)
+
+
+class SimpleStrategySettings(StrategySettings):
+ request_percentile: float = pd.Field(
+ 0.9, gt=0, le=1, description="The percentile to use for the request recommendation."
+ )
+ limit_percentile: float = pd.Field(
+ 0.99, gt=0, le=1, description="The percentile to use for the limit recommendation."
+ )
+
+
+class SimpleStrategy(BaseStrategy[SimpleStrategySettings]):
+ __display_name__ = "simple"
+
+ def run(
+ self, history_data: HistoryData, object_data: K8sObjectData, resource_type: ResourceType
+ ) -> ResourceRecommendation:
+ points_flatten = [point for points in history_data.values() for point in points]
+ return ResourceRecommendation(
+ request=self._calculate_percentile(points_flatten, self.settings.request_percentile),
+ limit=self._calculate_percentile(points_flatten, self.settings.limit_percentile),
+ )
+
+ def _calculate_percentile(self, data: list[float], percentile: float) -> float:
+ data = sorted(data)
+ return data[int(len(data) * percentile)]
diff --git a/robusta_krr/utils/configurable.py b/robusta_krr/utils/configurable.py
index f357c17..870dede 100644
--- a/robusta_krr/utils/configurable.py
+++ b/robusta_krr/utils/configurable.py
@@ -1,6 +1,7 @@
-from robusta_krr.core.config import Config
import typer
+from robusta_krr.core.config import Config
+
class Configurable:
"""