Skip to content

Commit e98bf08

Browse files
committed
Merge branch 'main' into support-newer-python
2 parents 9abb927 + 79680ee commit e98bf08

File tree

10 files changed

+1237
-764
lines changed

10 files changed

+1237
-764
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Use the official Python 3.9 slim image as the base image
1+
# Use the official Python 3.12 slim image as the base image
22
FROM python:3.12-slim AS builder
33
ENV LANG=C.UTF-8
44
ENV PYTHONDONTWRITEBYTECODE=1

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
<a href="https://github.com/robusta-dev/krr/issues">Request Feature</a>
2929
·
3030
<a href="#support">Support</a>
31-
<br /> Like KRR? Please ⭐ this repository to show your support!
31+
<br />
3232
<br />
3333
<a href="https://trendshift.io/repositories/7087" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7087" alt="robusta-dev%2Fkrr | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
3434
</p>
@@ -64,6 +64,10 @@
6464

6565
Robusta KRR (Kubernetes Resource Recommender) is a CLI tool for **optimizing resource allocation** in Kubernetes clusters. It gathers pod usage data from Prometheus and **recommends requests and limits** for CPU and memory. This **reduces costs and improves performance**.
6666

67+
### Auto-Apply Mode
68+
69+
**New:** Put right-sizing on auto-pilot by applying recommendations automatically. [Request beta access](https://robusta-dev.typeform.com/krr-auto-apply).
70+
6771
### Data Integrations
6872

6973
[![Used to send data to KRR](./images/krr-datasources.svg)](#data-source-integrations)

enforcer/Dockerfile

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
1-
# Use the official Python 3.9 slim image as the base image
2-
FROM python:3.12-slim
1+
FROM python:3.12-alpine
32
ENV LANG=C.UTF-8
43
ENV PYTHONDONTWRITEBYTECODE=1
54
ENV PYTHONUNBUFFERED=1
65
ENV PATH="/app/venv/bin:$PATH"
76

8-
# We're installing here libexpat1, to upgrade the package to include a fix to 3 high CVEs. CVE-2024-45491,CVE-2024-45490,CVE-2024-45492
9-
RUN apt-get update \
10-
&& apt-get install -y --no-install-recommends libexpat1 \
11-
&& rm -rf /var/lib/apt/lists/*
12-
137
# Set the working directory
148
WORKDIR /app/enforcer
159

enforcer/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
fastapi==0.109.2
1+
fastapi==0.115.12
22
uvicorn==0.27.1
33
pydantic==2.6.1
44
supabase==2.5

poetry.lock

Lines changed: 1126 additions & 747 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@ kubernetes = "^26.1.0"
3030
prometheus-api-client = "0.5.3"
3131
numpy = ">=1.26.4,<1.27.0"
3232
alive-progress = "^3.1.2"
33-
prometrix = "0.2.0"
33+
prometrix = "0.2.1"
3434
slack-sdk = "^3.21.3"
3535
pandas = "2.2.2"
3636
requests = "2.32.0"
3737
pyyaml = "6.0.1"
3838
typing-extensions = "4.6.0"
3939
idna = "3.7"
4040
urllib3 = "^1.26.20"
41-
setuptools = "^70.0.0"
41+
setuptools = "^80.9.0"
4242
zipp = "^3.19.1"
4343
tenacity = "^9.0.0"
4444

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ requests==2.32.0 ; python_version >= "3.9" and python_full_version < "3.13"
4242
rich==12.6.0 ; python_version >= "3.9" and python_full_version < "3.13"
4343
rsa==4.9 ; python_version >= "3.9" and python_full_version < "3.13"
4444
s3transfer==0.10.0 ; python_version >= "3.9" and python_full_version < "3.13"
45-
setuptools==70.3.0 ; python_version >= "3.9" and python_full_version < "3.13"
45+
setuptools==78.1.1 ; python_version >= "3.9" and python_full_version < "3.13"
4646
shellingham==1.5.4 ; python_version >= "3.9" and python_full_version < "3.13"
4747
six==1.16.0 ; python_version >= "3.9" and python_full_version < "3.13"
4848
slack-sdk==3.27.1 ; python_version >= "3.9" and python_full_version < "3.13"

robusta_krr/core/models/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ class Config(pd.BaseSettings):
6060
width: Optional[int] = pd.Field(None, ge=1)
6161
show_severity: bool = True
6262

63+
# Publishing to url settings
64+
publish_scan_url: Optional[str] = pd.Field(None)
65+
start_time: Optional[str] = pd.Field(None)
66+
scan_id: Optional[str] = pd.Field(None)
67+
6368
# Output Settings
6469
file_output: Optional[str] = pd.Field(None)
6570
file_output_dynamic: bool = pd.Field(False)

robusta_krr/core/runner.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@
33
import math
44
import os
55
import sys
6+
import time
67
import warnings
78
from concurrent.futures import ThreadPoolExecutor
89
from typing import Optional, Union
910
from datetime import timedelta, datetime
1011
from prometrix import PrometheusNotFound
1112
from rich.console import Console
1213
from slack_sdk import WebClient
13-
14+
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
15+
import requests
16+
import json
17+
import traceback
1418
from robusta_krr.core.abstract.strategies import ResourceRecommendation, RunResult
1519
from robusta_krr.core.integrations.kubernetes import KubernetesLoader
1620
from robusta_krr.core.integrations.prometheus import ClusterNotSpecifiedException, PrometheusMetricsLoader
@@ -104,6 +108,8 @@ def _process_result(self, result: Result) -> None:
104108
result.errors = self.errors
105109

106110
Formatter = settings.Formatter
111+
112+
self._send_result(settings.publish_scan_url, settings.start_time, settings.scan_id, result)
107113
formatted = result.format(Formatter)
108114
rich = getattr(Formatter, "__rich_console__", False)
109115

@@ -329,6 +335,7 @@ async def run(self) -> int:
329335
except Exception as e:
330336
logger.error(f"Could not load kubernetes configuration: {e}")
331337
logger.error("Try to explicitly set --context and/or --kubeconfig flags.")
338+
publish_error(f"Could not load kubernetes configuration: {e}")
332339
return 1 # Exit with error
333340

334341
try:
@@ -348,9 +355,71 @@ async def run(self) -> int:
348355
self._process_result(result)
349356
except (ClusterNotSpecifiedException, CriticalRunnerException) as e:
350357
logger.critical(e)
358+
publish_error(traceback.format_exc())
351359
return 1 # Exit with error
352360
except Exception:
353361
logger.exception("An unexpected error occurred")
362+
publish_error(traceback.format_exc())
354363
return 1 # Exit with error
355364
else:
356365
return 0 # Exit with success
366+
367+
def _send_result(self, url: str, start_time: datetime, scan_id: str, result: Result):
368+
result_dict = json.loads(result.json(indent=2))
369+
_send_scan_payload(url, scan_id, start_time, result_dict, is_error=False)
370+
371+
def publish_input_error(url: str, scan_id: str, start_time: str, error: str):
372+
_send_scan_payload(url, scan_id, start_time, error, is_error=True)
373+
374+
def publish_error(error: str):
375+
_send_scan_payload(settings.publish_scan_url, settings.scan_id, settings.start_time, error, is_error=True)
376+
377+
@retry(
378+
stop=stop_after_attempt(3),
379+
wait=wait_exponential(multiplier=1, min=1, max=8),
380+
reraise=True
381+
)
382+
def _post_scan_request(url: str, headers: dict, payload: dict, scan_id: str, is_error: bool):
383+
logger_msg = "Sending error scan" if is_error else "Sending scan"
384+
logger.info(f"{logger_msg} for scan_id={scan_id} to url={url}")
385+
response = requests.post(url, headers=headers, json=payload)
386+
logger.info(f"scan_id={scan_id} | Status code: {response.status_code}")
387+
logger.info(f"scan_id={scan_id} | Response body: {response.text}")
388+
return response
389+
390+
391+
def _send_scan_payload(
392+
url: str,
393+
scan_id: str,
394+
start_time: Union[str, datetime],
395+
result_data: Union[str, dict],
396+
is_error: bool = False
397+
):
398+
if not url or not scan_id or not start_time:
399+
logger.debug(f"Missing required parameters: url={bool(url)}, scan_id={bool(scan_id)}, start_time={bool(start_time)}")
400+
return
401+
402+
logger.debug(f"Preparing to send scan payload. scan_id={scan_id}, is_error={is_error}")
403+
404+
headers = {"Content-Type": "application/json"}
405+
406+
if isinstance(start_time, datetime):
407+
logger.debug(f"Converting datetime to ISO format for scan_id={scan_id}")
408+
start_time = start_time.isoformat()
409+
410+
action_request = {
411+
"action_name": "process_scan",
412+
"action_params": {
413+
"result": result_data,
414+
"scan_type": "krr",
415+
"scan_id": scan_id,
416+
"start_time": start_time,
417+
}
418+
}
419+
420+
try:
421+
_post_scan_request(url, headers, action_request, scan_id, is_error)
422+
except requests.exceptions.RequestException as e:
423+
logger.error(f"scan_id={scan_id} | All retry attempts failed due to RequestException: {e}", exc_info=True)
424+
except Exception as e:
425+
logger.error(f"scan_id={scan_id} | Unexpected error after retries: {e}", exc_info=True)

robusta_krr/main.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from robusta_krr.core.abstract import formatters
1818
from robusta_krr.core.abstract.strategies import BaseStrategy
1919
from robusta_krr.core.models.config import Config
20-
from robusta_krr.core.runner import Runner
20+
from robusta_krr.core.runner import Runner, publish_input_error
2121
from robusta_krr.utils.version import get_version
2222

2323
app = typer.Typer(
@@ -266,6 +266,24 @@ def run_strategy(
266266
help="Send to output to a slack channel, must have SLACK_BOT_TOKEN",
267267
rich_help_panel="Output Settings",
268268
),
269+
publish_scan_url: Optional[str] = typer.Option(
270+
None,
271+
"--publish_scan_url",
272+
help="Sends the output to a robusta_runner instance",
273+
rich_help_panel="Publish Scan Settings",
274+
),
275+
start_time: Optional[str] = typer.Option(
276+
None,
277+
"--start_time",
278+
help="Start time of the scan",
279+
rich_help_panel="Publish Scan Settings",
280+
),
281+
scan_id: Optional[str] = typer.Option(
282+
None,
283+
"--scan_id",
284+
help="A UUID scan identifier",
285+
rich_help_panel="Publish Scan Settings",
286+
),
269287
**strategy_args,
270288
) -> None:
271289
f"""Run KRR using the `{_strategy_name}` strategy"""
@@ -310,10 +328,14 @@ def run_strategy(
310328
show_severity=show_severity,
311329
strategy=_strategy_name,
312330
other_args=strategy_args,
313-
)
331+
publish_scan_url=publish_scan_url,
332+
start_time=start_time,
333+
scan_id=scan_id,
334+
)
314335
Config.set_config(config)
315-
except ValidationError:
336+
except ValidationError as e:
316337
logger.exception("Error occured while parsing arguments")
338+
publish_input_error( publish_scan_url, start_time, scan_id, str(e))
317339
else:
318340
runner = Runner()
319341
exit_code = asyncio.run(runner.run())

0 commit comments

Comments
 (0)