1use super::background::BackgroundProcessManager;
17use crate::agent::ui::prometheus_display::{ConnectionMode, PrometheusConnectionDisplay};
18use crate::analyzer::k8s_optimize::{PrometheusAuth, PrometheusClient};
19use rig::completion::ToolDefinition;
20use rig::tool::Tool;
21use serde::Deserialize;
22use serde_json::json;
23use std::sync::Arc;
24
25#[derive(Debug, Deserialize)]
27pub struct PrometheusConnectArgs {
28 #[serde(default)]
30 pub service: Option<String>,
31
32 #[serde(default)]
34 pub namespace: Option<String>,
35
36 #[serde(default)]
38 pub url: Option<String>,
39
40 #[serde(default)]
42 pub port: Option<u16>,
43
44 #[serde(default)]
46 pub auth_type: Option<String>,
47
48 #[serde(default)]
50 pub username: Option<String>,
51
52 #[serde(default)]
54 pub password: Option<String>,
55
56 #[serde(default)]
58 pub token: Option<String>,
59}
60
61#[derive(Debug, thiserror::Error)]
63#[error("Prometheus connect error: {0}")]
64pub struct PrometheusConnectError(String);
65
66#[derive(Clone)]
68pub struct PrometheusConnectTool {
69 bg_manager: Arc<BackgroundProcessManager>,
70}
71
72impl PrometheusConnectTool {
73 pub fn new(bg_manager: Arc<BackgroundProcessManager>) -> Self {
75 Self { bg_manager }
76 }
77
78 fn build_auth(args: &PrometheusConnectArgs) -> PrometheusAuth {
80 match args.auth_type.as_deref() {
81 Some("basic") => {
82 if let (Some(username), Some(password)) = (&args.username, &args.password) {
83 PrometheusAuth::Basic {
84 username: username.clone(),
85 password: password.clone(),
86 }
87 } else {
88 PrometheusAuth::None
89 }
90 }
91 Some("bearer") => {
92 if let Some(token) = &args.token {
93 PrometheusAuth::Bearer(token.clone())
94 } else {
95 PrometheusAuth::None
96 }
97 }
98 _ => PrometheusAuth::None,
99 }
100 }
101
102 async fn test_connection(url: &str, auth: PrometheusAuth) -> bool {
104 match PrometheusClient::with_auth(url, auth) {
105 Ok(client) => client.is_available().await,
106 Err(_) => false,
107 }
108 }
109}
110
111impl Tool for PrometheusConnectTool {
112 const NAME: &'static str = "prometheus_connect";
113
114 type Args = PrometheusConnectArgs;
115 type Output = String;
116 type Error = PrometheusConnectError;
117
118 async fn definition(&self, _prompt: String) -> ToolDefinition {
119 ToolDefinition {
120 name: Self::NAME.to_string(),
121 description: r#"Connect to Prometheus for K8s optimization analysis.
122
123**Use after prometheus_discover or when user provides a URL.**
124
125**Connection Methods (in order of preference):**
126
1271. **Port-forward** (recommended) - NO authentication needed
128 - Provide: service, namespace, port
129 - Starts kubectl port-forward in background
130 - Direct pod connection bypasses auth
131
1322. **External URL** - May require authentication
133 - Provide: url
134 - Optional: auth_type, username/password or token
135
136**Examples:**
137
138Port-forward (no auth):
139```json
140{"service": "prometheus-server", "namespace": "monitoring", "port": 9090}
141```
142
143External URL without auth:
144```json
145{"url": "http://prometheus.example.com"}
146```
147
148External URL with basic auth:
149```json
150{"url": "https://prometheus.example.com", "auth_type": "basic", "username": "admin", "password": "secret"}
151```
152
153**Returns:**
154- Connection URL for use with k8s_optimize
155- Connection mode (port-forward or direct)
156- Local port (if port-forward)"#
157 .to_string(),
158 parameters: json!({
159 "type": "object",
160 "properties": {
161 "service": {
162 "type": "string",
163 "description": "Kubernetes service name (for port-forward)"
164 },
165 "namespace": {
166 "type": "string",
167 "description": "Kubernetes namespace (for port-forward)"
168 },
169 "url": {
170 "type": "string",
171 "description": "External Prometheus URL (alternative to port-forward)"
172 },
173 "port": {
174 "type": "integer",
175 "description": "Target port (default: 9090)"
176 },
177 "auth_type": {
178 "type": "string",
179 "description": "Authentication type for external URL: 'none', 'basic', 'bearer'",
180 "enum": ["none", "basic", "bearer"]
181 },
182 "username": {
183 "type": "string",
184 "description": "Username for basic auth (only for external URL)"
185 },
186 "password": {
187 "type": "string",
188 "description": "Password for basic auth (only for external URL)"
189 },
190 "token": {
191 "type": "string",
192 "description": "Bearer token (only for external URL)"
193 }
194 }
195 }),
196 }
197 }
198
199 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
200 let target_port = args.port.unwrap_or(9090);
201
202 if let (Some(service), Some(namespace)) = (&args.service, &args.namespace) {
204 let resource = format!("svc/{}", service);
205 let display = PrometheusConnectionDisplay::new(ConnectionMode::PortForward);
206 let target = format!("{}/{}", namespace, service);
207 display.start(&target);
208
209 match self
211 .bg_manager
212 .start_port_forward("prometheus-port-forward", &resource, namespace, target_port)
213 .await
214 {
215 Ok(local_port) => {
216 let url = format!("http://localhost:{}", local_port);
217 display.port_forward_established(local_port, service, namespace);
218
219 tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
221 display.testing_connection();
222
223 let mut connected = false;
225 for attempt in 0..6 {
226 if Self::test_connection(&url, PrometheusAuth::None).await {
227 connected = true;
228 break;
229 }
230 let delay = match attempt {
232 0 | 1 => 1000,
233 2 | 3 => 2000,
234 _ => 3000,
235 };
236 tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await;
237 }
238
239 if connected {
240 display.connected(&url, false);
241 display.background_process_info("prometheus-port-forward");
242 display.ready_for_use(&url);
243
244 let response = json!({
245 "connected": true,
246 "url": url,
247 "mode": "port-forward",
248 "local_port": local_port,
249 "service": service,
250 "namespace": namespace,
251 "process_id": "prometheus-port-forward",
252 "note": "Port-forward established. No authentication needed.",
253 "usage": {
254 "k8s_optimize": {
255 "prometheus": url
256 }
257 }
258 });
259 return Ok(serde_json::to_string_pretty(&response)
260 .unwrap_or_else(|_| "{}".to_string()));
261 } else {
262 let _ = self.bg_manager.stop("prometheus-port-forward").await;
264
265 display.connection_failed(
266 "Port-forward started but Prometheus not responding",
267 &[
268 "Verify the service is correct",
269 "Check if Prometheus pod is running",
270 "The service might need more time to start",
271 ],
272 );
273
274 let response = json!({
275 "connected": false,
276 "url": url,
277 "mode": "port-forward",
278 "local_port": local_port,
279 "error": "Port-forward started but Prometheus not responding",
280 "suggestions": [
281 format!("Verify the service is correct with: kubectl get svc -n {}", namespace),
282 format!("Check if Prometheus pod is running: kubectl get pods -n {} | grep prometheus", namespace),
283 "The service might need more time to start".to_string()
284 ]
285 });
286 return Ok(serde_json::to_string_pretty(&response)
287 .unwrap_or_else(|_| "{}".to_string()));
288 }
289 }
290 Err(e) => {
291 display.connection_failed(
293 &format!("Port-forward failed: {}", e),
294 &[
295 "Check if kubectl is configured correctly",
296 "Verify the service exists",
297 "Try providing an external URL instead",
298 ],
299 );
300
301 let response = json!({
302 "connected": false,
303 "mode": "port-forward",
304 "error": format!("Port-forward failed: {}", e),
305 "suggestions": [
306 "Check if kubectl is configured correctly",
307 format!("Verify the service exists: kubectl get svc -n {}", namespace),
308 "Try providing an external URL instead"
309 ]
310 });
311 return Ok(serde_json::to_string_pretty(&response)
312 .unwrap_or_else(|_| "{}".to_string()));
313 }
314 }
315 }
316
317 if let Some(url) = &args.url {
319 let display = PrometheusConnectionDisplay::new(ConnectionMode::DirectUrl);
320 display.start(url);
321 display.testing_connection();
322
323 if Self::test_connection(url, PrometheusAuth::None).await {
325 display.connected(url, false);
326 display.ready_for_use(url);
327
328 let response = json!({
329 "connected": true,
330 "url": url,
331 "mode": "direct",
332 "authenticated": false,
333 "note": "Connected without authentication",
334 "usage": {
335 "k8s_optimize": {
336 "prometheus": url
337 }
338 }
339 });
340 return Ok(
341 serde_json::to_string_pretty(&response).unwrap_or_else(|_| "{}".to_string())
342 );
343 }
344
345 let auth = Self::build_auth(&args);
347 if !matches!(auth, PrometheusAuth::None) {
348 if Self::test_connection(url, auth).await {
349 display.connected(url, true);
350 display.ready_for_use(url);
351
352 let response = json!({
353 "connected": true,
354 "url": url,
355 "mode": "direct",
356 "authenticated": true,
357 "auth_type": args.auth_type,
358 "note": "Connected with authentication",
359 "usage": {
360 "k8s_optimize": {
361 "prometheus": url,
362 "auth_type": args.auth_type,
363 "username": args.username,
364 }
366 }
367 });
368 return Ok(serde_json::to_string_pretty(&response)
369 .unwrap_or_else(|_| "{}".to_string()));
370 }
371 }
372
373 if args.auth_type.is_none() {
375 display.auth_required();
376 }
377
378 display.connection_failed(
379 "Connection failed",
380 if args.auth_type.is_none() {
381 &[
382 "The URL might require authentication",
383 "Try with auth_type='basic' or 'bearer'",
384 "Verify the URL is correct and accessible",
385 ]
386 } else {
387 &[
388 "Authentication credentials might be incorrect",
389 "Verify the username/password or token",
390 "Check if the auth_type matches what the server expects",
391 ]
392 },
393 );
394
395 let response = json!({
396 "connected": false,
397 "url": url,
398 "mode": "direct",
399 "error": "Connection failed",
400 "suggestions": if args.auth_type.is_none() {
401 vec![
402 "The URL might require authentication",
403 "Try with auth_type='basic' and username/password",
404 "Or try auth_type='bearer' with a token",
405 "Verify the URL is correct and accessible"
406 ]
407 } else {
408 vec![
409 "Authentication credentials might be incorrect",
410 "Verify the username/password or token",
411 "Check if the auth_type matches what the server expects"
412 ]
413 }
414 });
415 return Ok(serde_json::to_string_pretty(&response).unwrap_or_else(|_| "{}".to_string()));
416 }
417
418 let response = json!({
420 "connected": false,
421 "error": "No service or URL provided",
422 "hint": "Either provide service+namespace for port-forward, or provide a URL",
423 "examples": [
424 {
425 "port-forward": {
426 "service": "prometheus-server",
427 "namespace": "monitoring",
428 "port": 9090
429 }
430 },
431 {
432 "external": {
433 "url": "http://prometheus.example.com"
434 }
435 }
436 ]
437 });
438 Ok(serde_json::to_string_pretty(&response).unwrap_or_else(|_| "{}".to_string()))
439 }
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 #[test]
447 fn test_tool_name() {
448 assert_eq!(PrometheusConnectTool::NAME, "prometheus_connect");
449 }
450
451 #[test]
452 fn test_build_auth_none() {
453 let args = PrometheusConnectArgs {
454 service: None,
455 namespace: None,
456 url: Some("http://localhost".to_string()),
457 port: None,
458 auth_type: None,
459 username: None,
460 password: None,
461 token: None,
462 };
463
464 let auth = PrometheusConnectTool::build_auth(&args);
465 assert!(matches!(auth, PrometheusAuth::None));
466 }
467
468 #[test]
469 fn test_build_auth_basic() {
470 let args = PrometheusConnectArgs {
471 service: None,
472 namespace: None,
473 url: Some("http://localhost".to_string()),
474 port: None,
475 auth_type: Some("basic".to_string()),
476 username: Some("admin".to_string()),
477 password: Some("secret".to_string()),
478 token: None,
479 };
480
481 let auth = PrometheusConnectTool::build_auth(&args);
482 match auth {
483 PrometheusAuth::Basic { username, password } => {
484 assert_eq!(username, "admin");
485 assert_eq!(password, "secret");
486 }
487 _ => panic!("Expected Basic auth"),
488 }
489 }
490
491 #[test]
492 fn test_build_auth_bearer() {
493 let args = PrometheusConnectArgs {
494 service: None,
495 namespace: None,
496 url: Some("http://localhost".to_string()),
497 port: None,
498 auth_type: Some("bearer".to_string()),
499 username: None,
500 password: None,
501 token: Some("mytoken".to_string()),
502 };
503
504 let auth = PrometheusConnectTool::build_auth(&args);
505 match auth {
506 PrometheusAuth::Bearer(token) => {
507 assert_eq!(token, "mytoken");
508 }
509 _ => panic!("Expected Bearer auth"),
510 }
511 }
512}