1use super::background::BackgroundProcessManager;
17use super::error::{ErrorCategory, format_error_for_llm};
18use crate::agent::ui::prometheus_display::{ConnectionMode, PrometheusConnectionDisplay};
19use crate::analyzer::k8s_optimize::{PrometheusAuth, PrometheusClient};
20use rig::completion::ToolDefinition;
21use rig::tool::Tool;
22use serde::Deserialize;
23use serde_json::json;
24use std::sync::Arc;
25
26#[derive(Debug, Deserialize)]
28pub struct PrometheusConnectArgs {
29 #[serde(default)]
31 pub service: Option<String>,
32
33 #[serde(default)]
35 pub namespace: Option<String>,
36
37 #[serde(default)]
39 pub url: Option<String>,
40
41 #[serde(default)]
43 pub port: Option<u16>,
44
45 #[serde(default)]
47 pub auth_type: Option<String>,
48
49 #[serde(default)]
51 pub username: Option<String>,
52
53 #[serde(default)]
55 pub password: Option<String>,
56
57 #[serde(default)]
59 pub token: Option<String>,
60}
61
62#[derive(Debug, thiserror::Error)]
64#[error("Prometheus connect error: {0}")]
65pub struct PrometheusConnectError(String);
66
67#[derive(Clone)]
69pub struct PrometheusConnectTool {
70 bg_manager: Arc<BackgroundProcessManager>,
71}
72
73impl PrometheusConnectTool {
74 pub fn new(bg_manager: Arc<BackgroundProcessManager>) -> Self {
76 Self { bg_manager }
77 }
78
79 fn validate_port(port: u16) -> Result<(), String> {
81 if port == 0 {
82 return Err("Port must be between 1 and 65535 (got 0)".to_string());
83 }
84 Ok(())
85 }
86
87 fn validate_url(url: &str) -> Result<(), String> {
89 let url_lower = url.to_lowercase();
90 if !url_lower.starts_with("http://") && !url_lower.starts_with("https://") {
91 return Err(format!(
92 "URL must start with http:// or https:// (got '{}')",
93 url
94 ));
95 }
96 Ok(())
97 }
98
99 fn build_auth(args: &PrometheusConnectArgs) -> PrometheusAuth {
101 match args.auth_type.as_deref() {
102 Some("basic") => {
103 if let (Some(username), Some(password)) = (&args.username, &args.password) {
104 PrometheusAuth::Basic {
105 username: username.clone(),
106 password: password.clone(),
107 }
108 } else {
109 PrometheusAuth::None
110 }
111 }
112 Some("bearer") => {
113 if let Some(token) = &args.token {
114 PrometheusAuth::Bearer(token.clone())
115 } else {
116 PrometheusAuth::None
117 }
118 }
119 _ => PrometheusAuth::None,
120 }
121 }
122
123 async fn test_connection(url: &str, auth: PrometheusAuth) -> bool {
125 match PrometheusClient::with_auth(url, auth) {
126 Ok(client) => client.is_available().await,
127 Err(_) => false,
128 }
129 }
130}
131
132impl Tool for PrometheusConnectTool {
133 const NAME: &'static str = "prometheus_connect";
134
135 type Args = PrometheusConnectArgs;
136 type Output = String;
137 type Error = PrometheusConnectError;
138
139 async fn definition(&self, _prompt: String) -> ToolDefinition {
140 ToolDefinition {
141 name: Self::NAME.to_string(),
142 description: r#"Connect to Prometheus for K8s optimization analysis.
143
144**Use after prometheus_discover or when user provides a URL.**
145
146**Connection Methods (in order of preference):**
147
1481. **Port-forward** (recommended) - NO authentication needed
149 - Provide: service, namespace, port
150 - Starts kubectl port-forward in background
151 - Direct pod connection bypasses auth
152
1532. **External URL** - May require authentication
154 - Provide: url
155 - Optional: auth_type, username/password or token
156
157**Examples:**
158
159Port-forward (no auth):
160```json
161{"service": "prometheus-server", "namespace": "monitoring", "port": 9090}
162```
163
164External URL without auth:
165```json
166{"url": "http://prometheus.example.com"}
167```
168
169External URL with basic auth:
170```json
171{"url": "https://prometheus.example.com", "auth_type": "basic", "username": "admin", "password": "secret"}
172```
173
174**Returns:**
175- Connection URL for use with k8s_optimize
176- Connection mode (port-forward or direct)
177- Local port (if port-forward)"#
178 .to_string(),
179 parameters: json!({
180 "type": "object",
181 "properties": {
182 "service": {
183 "type": "string",
184 "description": "Kubernetes service name (for port-forward)"
185 },
186 "namespace": {
187 "type": "string",
188 "description": "Kubernetes namespace (for port-forward)"
189 },
190 "url": {
191 "type": "string",
192 "description": "External Prometheus URL (alternative to port-forward)"
193 },
194 "port": {
195 "type": "integer",
196 "description": "Target port (default: 9090)"
197 },
198 "auth_type": {
199 "type": "string",
200 "description": "Authentication type for external URL: 'none', 'basic', 'bearer'",
201 "enum": ["none", "basic", "bearer"]
202 },
203 "username": {
204 "type": "string",
205 "description": "Username for basic auth (only for external URL)"
206 },
207 "password": {
208 "type": "string",
209 "description": "Password for basic auth (only for external URL)"
210 },
211 "token": {
212 "type": "string",
213 "description": "Bearer token (only for external URL)"
214 }
215 }
216 }),
217 }
218 }
219
220 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
221 if let Some(port) = args.port {
223 if let Err(e) = Self::validate_port(port) {
224 return Ok(format_error_for_llm(
225 "prometheus_connect",
226 ErrorCategory::ValidationFailed,
227 &e,
228 Some(vec![
229 "Port must be a valid TCP port between 1 and 65535",
230 "Common Prometheus port is 9090 (default if not specified)",
231 ]),
232 ));
233 }
234 }
235
236 if let Some(ref url) = args.url {
238 if let Err(e) = Self::validate_url(url) {
239 return Ok(format_error_for_llm(
240 "prometheus_connect",
241 ErrorCategory::ValidationFailed,
242 &e,
243 Some(vec![
244 "URL must start with http:// or https://",
245 "Example: http://prometheus.example.com or https://prometheus.example.com",
246 ]),
247 ));
248 }
249 }
250
251 let target_port = args.port.unwrap_or(9090);
252
253 if let (Some(service), Some(namespace)) = (&args.service, &args.namespace) {
255 let resource = format!("svc/{}", service);
256 let display = PrometheusConnectionDisplay::new(ConnectionMode::PortForward);
257 let target = format!("{}/{}", namespace, service);
258 display.start(&target);
259
260 match self
262 .bg_manager
263 .start_port_forward("prometheus-port-forward", &resource, namespace, target_port)
264 .await
265 {
266 Ok(local_port) => {
267 let url = format!("http://localhost:{}", local_port);
268 display.port_forward_established(local_port, service, namespace);
269
270 tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
272 display.testing_connection();
273
274 let mut connected = false;
276 for attempt in 0..6 {
277 if Self::test_connection(&url, PrometheusAuth::None).await {
278 connected = true;
279 break;
280 }
281 let delay = match attempt {
283 0 | 1 => 1000,
284 2 | 3 => 2000,
285 _ => 3000,
286 };
287 tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await;
288 }
289
290 if connected {
291 display.connected(&url, false);
292 display.background_process_info("prometheus-port-forward");
293 display.ready_for_use(&url);
294
295 let response = json!({
296 "connected": true,
297 "url": url,
298 "mode": "port-forward",
299 "local_port": local_port,
300 "service": service,
301 "namespace": namespace,
302 "process_id": "prometheus-port-forward",
303 "note": "Port-forward established. No authentication needed.",
304 "usage": {
305 "k8s_optimize": {
306 "prometheus": url
307 }
308 }
309 });
310 return Ok(serde_json::to_string_pretty(&response)
311 .unwrap_or_else(|_| "{}".to_string()));
312 } else {
313 let _ = self.bg_manager.stop("prometheus-port-forward").await;
315
316 display.connection_failed(
317 "Port-forward started but Prometheus not responding",
318 &[
319 "Verify the service is correct",
320 "Check if Prometheus pod is running",
321 "The service might need more time to start",
322 ],
323 );
324
325 return Ok(format_error_for_llm(
326 "prometheus_connect",
327 ErrorCategory::NetworkError,
328 "Port-forward started but Prometheus not responding",
329 Some(vec![
330 &format!(
331 "Verify the service is correct: kubectl get svc -n {}",
332 namespace
333 ),
334 &format!(
335 "Check if Prometheus pod is running: kubectl get pods -n {} | grep prometheus",
336 namespace
337 ),
338 "The service might need more time to start - try again in a few seconds",
339 ]),
340 ));
341 }
342 }
343 Err(e) => {
344 display.connection_failed(
346 &format!("Port-forward failed: {}", e),
347 &[
348 "Check if kubectl is configured correctly",
349 "Verify the service exists",
350 "Try providing an external URL instead",
351 ],
352 );
353
354 return Ok(format_error_for_llm(
355 "prometheus_connect",
356 ErrorCategory::ExternalCommandFailed,
357 &format!("Port-forward failed: {}", e),
358 Some(vec![
359 "Check if kubectl is configured correctly: kubectl config current-context",
360 &format!(
361 "Verify the service exists: kubectl get svc -n {}",
362 namespace
363 ),
364 "Try providing an external URL instead",
365 ]),
366 ));
367 }
368 }
369 }
370
371 if let Some(url) = &args.url {
373 let display = PrometheusConnectionDisplay::new(ConnectionMode::DirectUrl);
374 display.start(url);
375 display.testing_connection();
376
377 if Self::test_connection(url, PrometheusAuth::None).await {
379 display.connected(url, false);
380 display.ready_for_use(url);
381
382 let response = json!({
383 "connected": true,
384 "url": url,
385 "mode": "direct",
386 "authenticated": false,
387 "note": "Connected without authentication",
388 "usage": {
389 "k8s_optimize": {
390 "prometheus": url
391 }
392 }
393 });
394 return Ok(
395 serde_json::to_string_pretty(&response).unwrap_or_else(|_| "{}".to_string())
396 );
397 }
398
399 let auth = Self::build_auth(&args);
401 if !matches!(auth, PrometheusAuth::None) && Self::test_connection(url, auth).await {
402 display.connected(url, true);
403 display.ready_for_use(url);
404
405 let response = json!({
406 "connected": true,
407 "url": url,
408 "mode": "direct",
409 "authenticated": true,
410 "auth_type": args.auth_type,
411 "note": "Connected with authentication",
412 "usage": {
413 "k8s_optimize": {
414 "prometheus": url,
415 "auth_type": args.auth_type,
416 "username": args.username,
417 }
419 }
420 });
421 return Ok(
422 serde_json::to_string_pretty(&response).unwrap_or_else(|_| "{}".to_string())
423 );
424 }
425
426 if args.auth_type.is_none() {
428 display.auth_required();
429
430 display.connection_failed(
431 "Connection failed - URL may require authentication",
432 &[
433 "Try with auth_type='basic' and username/password",
434 "Or try auth_type='bearer' with a token",
435 "Verify the URL is correct and accessible",
436 ],
437 );
438
439 let test_url_suggestion =
440 format!("Test URL manually: curl -s {}/api/v1/status/config", url);
441 return Ok(format_error_for_llm(
442 "prometheus_connect",
443 ErrorCategory::NetworkError,
444 "Connection failed - URL may require authentication",
445 Some(vec![
446 "Try with auth_type='basic' and username/password",
447 "Or try auth_type='bearer' with a token",
448 "Verify the URL is correct and accessible",
449 &test_url_suggestion,
450 ]),
451 ));
452 } else {
453 display.connection_failed(
454 "Connection failed - authentication credentials may be incorrect",
455 &[
456 "Verify the username/password or token",
457 "Check if the auth_type matches what the server expects",
458 "Ensure the user has permission to access Prometheus API",
459 ],
460 );
461
462 return Ok(format_error_for_llm(
463 "prometheus_connect",
464 ErrorCategory::NetworkError,
465 "Connection failed - authentication credentials may be incorrect",
466 Some(vec![
467 "Verify the username/password or token",
468 "Check if the auth_type matches what the server expects",
469 "Ensure the user has permission to access Prometheus API",
470 ]),
471 ));
472 }
473 }
474
475 Ok(format_error_for_llm(
477 "prometheus_connect",
478 ErrorCategory::ValidationFailed,
479 "No service or URL provided",
480 Some(vec![
481 "Provide service + namespace for port-forward: {\"service\": \"prometheus-server\", \"namespace\": \"monitoring\"}",
482 "Or provide url for external Prometheus: {\"url\": \"http://prometheus.example.com\"}",
483 "Use prometheus_discover to find available Prometheus instances",
484 ]),
485 ))
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492
493 #[test]
494 fn test_tool_name() {
495 assert_eq!(PrometheusConnectTool::NAME, "prometheus_connect");
496 }
497
498 #[test]
499 fn test_validate_port_valid() {
500 assert!(PrometheusConnectTool::validate_port(9090).is_ok());
501 assert!(PrometheusConnectTool::validate_port(1).is_ok());
502 assert!(PrometheusConnectTool::validate_port(65535).is_ok());
503 }
504
505 #[test]
506 fn test_validate_port_invalid() {
507 let result = PrometheusConnectTool::validate_port(0);
508 assert!(result.is_err());
509 assert!(
510 result
511 .unwrap_err()
512 .contains("Port must be between 1 and 65535")
513 );
514 }
515
516 #[test]
517 fn test_validate_url_valid() {
518 assert!(PrometheusConnectTool::validate_url("http://prometheus.example.com").is_ok());
519 assert!(PrometheusConnectTool::validate_url("https://prometheus.example.com").is_ok());
520 assert!(PrometheusConnectTool::validate_url("HTTP://PROMETHEUS.EXAMPLE.COM").is_ok());
521 assert!(PrometheusConnectTool::validate_url("HTTPS://prometheus.example.com").is_ok());
522 }
523
524 #[test]
525 fn test_validate_url_invalid() {
526 let result = PrometheusConnectTool::validate_url("prometheus.example.com");
528 assert!(result.is_err());
529 assert!(
530 result
531 .unwrap_err()
532 .contains("must start with http:// or https://")
533 );
534
535 let result = PrometheusConnectTool::validate_url("ftp://prometheus.example.com");
537 assert!(result.is_err());
538
539 let result = PrometheusConnectTool::validate_url("/api/v1/query");
541 assert!(result.is_err());
542 }
543
544 #[tokio::test]
545 async fn test_missing_service_and_url_error() {
546 let bg_manager = Arc::new(BackgroundProcessManager::new());
548 let tool = PrometheusConnectTool::new(bg_manager);
549
550 let args = PrometheusConnectArgs {
551 service: None,
552 namespace: None,
553 url: None,
554 port: None,
555 auth_type: None,
556 username: None,
557 password: None,
558 token: None,
559 };
560
561 let result = tool.call(args).await.unwrap();
562
563 assert!(result.contains("\"error\": true"));
565 assert!(result.contains("VALIDATION_FAILED"));
566 assert!(result.contains("No service or URL provided"));
567 assert!(result.contains("suggestions"));
568 }
569
570 #[tokio::test]
571 async fn test_invalid_port_validation() {
572 let bg_manager = Arc::new(BackgroundProcessManager::new());
574 let tool = PrometheusConnectTool::new(bg_manager);
575
576 let args = PrometheusConnectArgs {
577 service: Some("prometheus".to_string()),
578 namespace: Some("monitoring".to_string()),
579 url: None,
580 port: Some(0), auth_type: None,
582 username: None,
583 password: None,
584 token: None,
585 };
586
587 let result = tool.call(args).await.unwrap();
588
589 assert!(result.contains("\"error\": true"));
591 assert!(result.contains("VALIDATION_FAILED"));
592 assert!(result.contains("Port must be between 1 and 65535"));
593 }
594
595 #[tokio::test]
596 async fn test_malformed_url_validation() {
597 let bg_manager = Arc::new(BackgroundProcessManager::new());
599 let tool = PrometheusConnectTool::new(bg_manager);
600
601 let args = PrometheusConnectArgs {
602 service: None,
603 namespace: None,
604 url: Some("prometheus.example.com".to_string()), port: None,
606 auth_type: None,
607 username: None,
608 password: None,
609 token: None,
610 };
611
612 let result = tool.call(args).await.unwrap();
613
614 assert!(result.contains("\"error\": true"));
616 assert!(result.contains("VALIDATION_FAILED"));
617 assert!(result.contains("must start with http:// or https://"));
618 assert!(result.contains("suggestions"));
619 }
620
621 #[test]
622 fn test_build_auth_none() {
623 let args = PrometheusConnectArgs {
624 service: None,
625 namespace: None,
626 url: Some("http://localhost".to_string()),
627 port: None,
628 auth_type: None,
629 username: None,
630 password: None,
631 token: None,
632 };
633
634 let auth = PrometheusConnectTool::build_auth(&args);
635 assert!(matches!(auth, PrometheusAuth::None));
636 }
637
638 #[test]
639 fn test_build_auth_basic() {
640 let args = PrometheusConnectArgs {
641 service: None,
642 namespace: None,
643 url: Some("http://localhost".to_string()),
644 port: None,
645 auth_type: Some("basic".to_string()),
646 username: Some("admin".to_string()),
647 password: Some("secret".to_string()),
648 token: None,
649 };
650
651 let auth = PrometheusConnectTool::build_auth(&args);
652 match auth {
653 PrometheusAuth::Basic { username, password } => {
654 assert_eq!(username, "admin");
655 assert_eq!(password, "secret");
656 }
657 _ => panic!("Expected Basic auth"),
658 }
659 }
660
661 #[test]
662 fn test_build_auth_bearer() {
663 let args = PrometheusConnectArgs {
664 service: None,
665 namespace: None,
666 url: Some("http://localhost".to_string()),
667 port: None,
668 auth_type: Some("bearer".to_string()),
669 username: None,
670 password: None,
671 token: Some("mytoken".to_string()),
672 };
673
674 let auth = PrometheusConnectTool::build_auth(&args);
675 match auth {
676 PrometheusAuth::Bearer(token) => {
677 assert_eq!(token, "mytoken");
678 }
679 _ => panic!("Expected Bearer auth"),
680 }
681 }
682}