diff options
| 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 |
| commit | 26b0cf460167f5c6462b39b551ff4f138f86682c (patch) | |
| tree | 6e197f23ba259e5fa156bf81a8e49f7b86b129af | |
| parent | 6fade1efbc920637b42ff336f576cc7604d90627 (diff) | |
Finish code structure
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: """ |
