Skip to content

Add option for min diff for shorter result list and tresholds #424

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@ Most helpful flags:

- `--cpu-min` Sets the minimum recommended cpu value in millicores
- `--mem-min` Sets the minimum recommended memory value in MB
- `--cpu-min-diff` Sets the minimum cpu difference for recommendation in millicores
- `--mem-min-diff` Sets the minimum memory difference for recommendation in MB
- `--cpu-min-percent` Sets the minimum cpu difference in percentage for recommendation
- `--mem-min-percent` Sets the minimum memory difference in percentage for recommendation
- `--history_duration` The duration of the Prometheus history data to use (in hours)

More specific information on Strategy Settings can be found using
Expand Down
4 changes: 4 additions & 0 deletions robusta_krr/core/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ class Config(pd.BaseSettings):
# Value settings
cpu_min_value: int = pd.Field(10, ge=0) # in millicores
memory_min_value: int = pd.Field(100, ge=0) # in megabytes
cpu_min_diff: int = pd.Field(0, ge=0) # in millicores
memory_min_diff: int = pd.Field(0, ge=0) # in megabytes
cpu_min_percent: int = pd.Field(0, ge=0) # in millicores
memory_min_percent: int = pd.Field(0, ge=0) # in megabytes

# Prometheus Settings
prometheus_url: Optional[str] = pd.Field(None)
Expand Down
93 changes: 89 additions & 4 deletions robusta_krr/core/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import sys
import warnings
from concurrent.futures import ThreadPoolExecutor
from typing import Optional, Union
from typing import Optional, Union, List
from datetime import timedelta, datetime
from prometrix import PrometheusNotFound
from rich.console import Console
Expand Down Expand Up @@ -33,6 +33,49 @@ def custom_print(*objects, rich: bool = True, force: bool = False) -> None:
if not settings.quiet or force:
print_func(*objects) # type: ignore

# Helper function to make the main logic cleaner
def _meets_filter_criteria(
current_val: Optional[float],
recommended_val: Optional[float],
min_diff: float,
min_percent: float,
resource: ResourceType,
) -> bool:
"""
Checks if the difference between current and recommended values meets the threshold.
For CPU, min_diff is in millicores, values are in cores.
For Memory, min_diff is in MB, values are in bytes.
"""
current = current_val if current_val is not None else 0.0
recommended = recommended_val if recommended_val is not None else 0.0

# If no change, it doesn't meet any "difference" criteria unless thresholds are zero
if current == recommended and min_diff != 0.0 and min_percent != 0.0:
return False

# Absolute difference check
try:
abs_diff_raw = abs(recommended - current)
if resource == ResourceType.CPU:
abs_diff = abs_diff_raw * 1000
else:
abs_diff = abs_diff_raw / (1024**2)
except TypeError:
logger.error(
f"TypeError: current_val: {current_val}, recommended_val: {recommended_val}, min_diff: {min_diff}, min_percent: {min_percent}")
return True

if abs_diff < min_diff and min_diff != 0.0:
return False

if min_percent != 0.0:
if current > 0: # Avoid division by zero; if current is 0, any increase is infinite percent
percent_diff = (abs_diff_raw / current) * 100
if percent_diff < min_percent:
return False

return True


class CriticalRunnerException(Exception): ...

Expand Down Expand Up @@ -300,6 +343,48 @@ async def _collect_result(self) -> Result:

successful_scans = [scan for scan in scans if scan is not None]

filtered_scans: List[ResourceScan] = []
for scan in successful_scans:
if scan.object is None or scan.object.allocations is None or scan.recommended is None:
logger.debug(f"Skipping scan for {scan.object.name if scan.object else 'Unknown'} due to missing data for filtering.")
continue

current_cpu_request = scan.object.allocations.requests.get(ResourceType.CPU)
current_memory_request = scan.object.allocations.requests.get(ResourceType.Memory)

recommended_cpu_request = rec.value if (rec := scan.recommended.requests.get(ResourceType.CPU)) else None
recommended_memory_request = rec.value if (rec := scan.recommended.requests.get(ResourceType.Memory)) else None

# Check CPU criteria
cpu_meets_criteria = _meets_filter_criteria(
current_val=current_cpu_request,
recommended_val=recommended_cpu_request,
min_diff=float(settings.cpu_min_diff),
min_percent=float(settings.cpu_min_percent),
resource=ResourceType.CPU,
)

# Check Memory criteria
memory_meets_criteria = _meets_filter_criteria(
current_val=current_memory_request,
recommended_val=recommended_memory_request,
min_diff=float(settings.memory_min_diff),
min_percent=float(settings.memory_min_percent),
resource=ResourceType.Memory,
)

if cpu_meets_criteria or memory_meets_criteria:
filtered_scans.append(scan)
else:
logger.debug(
f"Scan for {scan.object.name} (container: {scan.object.container}) did not meet filter criteria. "
f"CPU met: {cpu_meets_criteria}, Memory met: {memory_meets_criteria}. "
f"Current CPU: {current_cpu_request}, Rec CPU: {recommended_cpu_request}. "
f"Current Mem: {current_memory_request}, Rec Mem: {recommended_memory_request}."
)

logger.info(f"Gathered {len(scans)} total scans, {len(successful_scans)} were valid, {len(filtered_scans)} met filter criteria.")

if len(scans) == 0:
logger.warning("Current filters resulted in no objects available to scan.")
logger.warning("Try to change the filters or check if there is anything available.")
Expand All @@ -308,11 +393,11 @@ async def _collect_result(self) -> Result:
"Note that you are using the '*' namespace filter, which by default excludes kube-system."
)
raise CriticalRunnerException("No objects available to scan.")
elif len(successful_scans) == 0:
raise CriticalRunnerException("No successful scans were made. Check the logs for more information.")
elif len(filtered_scans) == 0:
raise CriticalRunnerException("No successful filtered scans were made. Check the logs for more information.")

return Result(
scans=successful_scans,
scans=filtered_scans,
description=f"[b]{self._strategy.display_name.title()} Strategy[/b]\n\n{self._strategy.description}",
strategy=StrategyData(
name=str(self._strategy).lower(),
Expand Down
28 changes: 28 additions & 0 deletions robusta_krr/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,30 @@ def run_strategy(
help="Sets the minimum recommended memory value in MB.",
rich_help_panel="Recommendation Settings",
),
cpu_min_diff: int = typer.Option(
0,
"--cpu-min-diff",
help="Sets the minimum cpu difference for recommendation in millicores.",
rich_help_panel="Recommendation Settings",
),
memory_min_diff: int = typer.Option(
0,
"--mem-min-diff",
help="Sets the minimum memory difference for recommendation in MB.",
rich_help_panel="Recommendation Settings",
),
cpu_min_percent: int = typer.Option(
0,
"--cpu-min-percent",
help="Sets the minimum cpu difference in percentage for recommendation.",
rich_help_panel="Recommendation Settings",
),
memory_min_percent: int = typer.Option(
0,
"--mem-min-percent",
help="Sets the minimum memory difference in percentage for recommendation.",
rich_help_panel="Recommendation Settings",
),
max_workers: int = typer.Option(
10,
"--max-workers",
Expand Down Expand Up @@ -301,6 +325,10 @@ def run_strategy(
verbose=verbose,
cpu_min_value=cpu_min_value,
memory_min_value=memory_min_value,
cpu_min_diff=cpu_min_diff,
memory_min_diff=memory_min_diff,
cpu_min_percent=cpu_min_percent,
memory_min_percent=memory_min_percent,
quiet=quiet,
log_to_stderr=log_to_stderr,
width=width,
Expand Down
4 changes: 4 additions & 0 deletions tests/formatters/test_csv_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@
"selector": null,
"cpu_min_value": 10,
"memory_min_value": 100,
"cpu_min_diff": 0,
"memory_min_diff": 0,
"cpu_min_percent": 0,
"memory_min_percent": 0,
"prometheus_url": null,
"prometheus_auth_header": null,
"prometheus_other_headers": {},
Expand Down