1use super::compression::{CompressionConfig, compress_tool_output};
21use rig::completion::ToolDefinition;
22use rig::tool::Tool;
23use serde::{Deserialize, Serialize};
24use serde_json::json;
25use std::path::PathBuf;
26
27use crate::analyzer::k8s_optimize::{
28 K8sOptimizeConfig, OptimizationResult, PrometheusAuth, PrometheusClient, Severity, analyze,
29 analyze_content, bytes_to_memory_string, millicores_to_cpu_string, parse_cpu_to_millicores,
30 parse_memory_to_bytes, rule_codes, rule_description,
31};
32
33#[derive(Debug, Deserialize)]
35pub struct K8sOptimizeArgs {
36 #[serde(default)]
38 pub path: Option<String>,
39
40 #[serde(default)]
42 pub content: Option<String>,
43
44 #[serde(default)]
46 pub severity: Option<String>,
47
48 #[serde(default)]
50 pub threshold: Option<u8>,
51
52 #[serde(default)]
54 pub include_info: bool,
55
56 #[serde(default)]
58 pub include_system: bool,
59
60 #[serde(default)]
62 pub full: bool,
63
64 #[serde(default)]
67 pub cluster: Option<String>,
68
69 #[serde(default)]
72 pub prometheus: Option<String>,
73
74 #[serde(default)]
77 pub prometheus_auth_type: Option<String>,
78
79 #[serde(default)]
81 pub prometheus_username: Option<String>,
82
83 #[serde(default)]
85 pub prometheus_password: Option<String>,
86
87 #[serde(default)]
89 pub prometheus_token: Option<String>,
90
91 #[serde(default)]
93 pub period: Option<String>,
94
95 #[serde(default)]
98 pub cloud_provider: Option<String>,
99
100 #[serde(default)]
102 pub region: Option<String>,
103}
104
105#[derive(Debug, thiserror::Error)]
107#[error("K8s optimize error: {0}")]
108pub struct K8sOptimizeError(String);
109
110struct PrometheusEnhancement {
112 enhanced_count: usize,
114 no_data_count: usize,
116 prometheus_data: Vec<serde_json::Value>,
118}
119
120fn find_helm_charts(path: &std::path::Path) -> Vec<PathBuf> {
122 let mut charts = Vec::new();
123
124 if path.join("Chart.yaml").exists() {
125 charts.push(path.to_path_buf());
126 return charts;
127 }
128
129 if let Ok(entries) = std::fs::read_dir(path) {
130 for entry in entries.flatten() {
131 let entry_path = entry.path();
132 if entry_path.is_dir() {
133 if entry_path.join("Chart.yaml").exists() {
134 charts.push(entry_path);
135 } else if let Ok(sub_entries) = std::fs::read_dir(&entry_path) {
136 for sub_entry in sub_entries.flatten() {
137 let sub_path = sub_entry.path();
138 if sub_path.is_dir() && sub_path.join("Chart.yaml").exists() {
139 charts.push(sub_path);
140 }
141 }
142 }
143 }
144 }
145 }
146
147 charts
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct K8sOptimizeTool {
153 project_root: PathBuf,
154}
155
156impl K8sOptimizeTool {
157 pub fn new(project_root: PathBuf) -> Self {
159 Self { project_root }
160 }
161
162 fn build_prometheus_auth(args: &K8sOptimizeArgs) -> PrometheusAuth {
164 match args.prometheus_auth_type.as_deref() {
165 Some("basic") => {
166 if let (Some(username), Some(password)) =
167 (&args.prometheus_username, &args.prometheus_password)
168 {
169 PrometheusAuth::Basic {
170 username: username.clone(),
171 password: password.clone(),
172 }
173 } else {
174 PrometheusAuth::None
175 }
176 }
177 Some("bearer") => {
178 if let Some(token) = &args.prometheus_token {
179 PrometheusAuth::Bearer(token.clone())
180 } else {
181 PrometheusAuth::None
182 }
183 }
184 _ => PrometheusAuth::None,
185 }
186 }
187
188 async fn enhance_with_prometheus(
193 &self,
194 result: &mut OptimizationResult,
195 client: &PrometheusClient,
196 period: &str,
197 ) -> PrometheusEnhancement {
198 let mut enhanced_count = 0;
199 let mut no_data_count = 0;
200 let mut prometheus_data: Vec<serde_json::Value> = Vec::new();
201
202 for rec in &mut result.recommendations {
203 let namespace = rec.namespace.as_deref().unwrap_or("default");
204 let workload_name = &rec.resource_name;
205 let container = &rec.container;
206
207 let current_cpu_millicores = rec
209 .current
210 .cpu_request
211 .as_ref()
212 .and_then(|s| parse_cpu_to_millicores(s));
213 let current_memory_bytes = rec
214 .current
215 .memory_request
216 .as_ref()
217 .and_then(|s| parse_memory_to_bytes(s));
218
219 match client
221 .get_container_history(namespace, workload_name, container, period)
222 .await
223 {
224 Ok(history) => {
225 let historical_rec = PrometheusClient::generate_recommendation(
227 &history,
228 current_cpu_millicores,
229 current_memory_bytes,
230 20, );
232
233 let cpu_str = millicores_to_cpu_string(historical_rec.recommended_cpu_request);
235 let mem_str = bytes_to_memory_string(historical_rec.recommended_memory_request);
236 let cpu_limit_str =
237 millicores_to_cpu_string(historical_rec.recommended_cpu_request * 2);
238
239 prometheus_data.push(serde_json::json!({
241 "workload": format!("{}/{}", namespace, workload_name),
242 "container": container,
243 "period": period,
244 "samples": history.sample_count,
245 "cpu_usage": {
246 "min": history.cpu_min,
247 "p50": history.cpu_p50,
248 "p95": history.cpu_p95,
249 "p99": history.cpu_p99,
250 "max": history.cpu_max,
251 "avg": history.cpu_avg,
252 },
253 "memory_usage": {
254 "min_bytes": history.memory_min,
255 "p50_bytes": history.memory_p50,
256 "p95_bytes": history.memory_p95,
257 "p99_bytes": history.memory_p99,
258 "max_bytes": history.memory_max,
259 "avg_bytes": history.memory_avg,
260 },
261 "recommendation": {
262 "cpu_request": cpu_str,
263 "memory_request": mem_str,
264 "cpu_savings_pct": historical_rec.cpu_savings_pct,
265 "memory_savings_pct": historical_rec.memory_savings_pct,
266 "confidence": historical_rec.confidence,
267 }
268 }));
269
270 rec.recommended.cpu_request = Some(cpu_str.clone());
272 rec.recommended.memory_request = Some(mem_str.clone());
273
274 rec.fix_yaml = format!(
276 "resources:\n requests:\n cpu: \"{}\"\n memory: \"{}\"\n limits:\n cpu: \"{}\" # 2x request\n memory: \"{}\"",
277 cpu_str, mem_str, cpu_limit_str, mem_str,
278 );
279
280 rec.message = format!(
282 "{} [DATA-DRIVEN: P99 usage CPU={}m, Memory={}Mi over {}, confidence={}%]",
283 rec.message,
284 history.cpu_p99,
285 history.memory_p99 / (1024 * 1024),
286 period,
287 historical_rec.confidence
288 );
289
290 enhanced_count += 1;
291 }
292 Err(_) => {
293 no_data_count += 1;
295 }
296 }
297 }
298
299 PrometheusEnhancement {
300 enhanced_count,
301 no_data_count,
302 prometheus_data,
303 }
304 }
305
306 fn build_config(&self, args: &K8sOptimizeArgs) -> K8sOptimizeConfig {
308 let mut config = K8sOptimizeConfig::default();
309
310 if let Some(severity_str) = &args.severity {
311 if let Some(severity) = Severity::parse(severity_str) {
312 config = config.with_severity(severity);
313 }
314 }
315
316 if let Some(threshold) = args.threshold {
317 config = config.with_threshold(threshold);
318 }
319
320 if args.include_info {
321 config = config.with_info();
322 }
323
324 if args.include_system {
325 config = config.with_system();
326 }
327
328 config
329 }
330
331 fn format_for_agent(
333 &self,
334 result: &OptimizationResult,
335 args: &K8sOptimizeArgs,
336 ) -> serde_json::Value {
337 let mut response = json!({
339 "summary": {
340 "resources_analyzed": result.summary.resources_analyzed,
341 "containers_analyzed": result.summary.containers_analyzed,
342 "over_provisioned": result.summary.over_provisioned,
343 "under_provisioned": result.summary.under_provisioned,
344 "missing_requests": result.summary.missing_requests,
345 "missing_limits": result.summary.missing_limits,
346 "optimal": result.summary.optimal,
347 "total_waste_percentage": result.summary.total_waste_percentage,
348 "mode": result.metadata.mode.to_string(),
349 },
350 "recommendations": result.recommendations.iter().map(|r| {
351 json!({
352 "resource": format!("{}/{}", r.resource_kind, r.resource_name),
353 "container": r.container,
354 "namespace": r.namespace,
355 "file": r.file_path.display().to_string(),
356 "line": r.line,
357 "issue": r.issue.to_string(),
358 "severity": r.severity.as_str(),
359 "message": r.message,
360 "workload_type": r.workload_type.as_str(),
361 "rule_code": r.rule_code.as_str(),
362 "rule_description": rule_description(r.rule_code.as_str()),
363 "current": {
364 "cpu_request": r.current.cpu_request,
365 "cpu_limit": r.current.cpu_limit,
366 "memory_request": r.current.memory_request,
367 "memory_limit": r.current.memory_limit,
368 },
369 "recommended": {
370 "cpu_request": r.recommended.cpu_request,
371 "cpu_limit": r.recommended.cpu_limit,
372 "memory_request": r.recommended.memory_request,
373 "memory_limit": r.recommended.memory_limit,
374 },
375 "fix_yaml": r.fix_yaml,
376 "quick_fix": {
378 "action": "replace_resources",
379 "file": r.file_path.display().to_string(),
380 "container": r.container.clone(),
381 "yaml": r.fix_yaml.clone(),
382 }
383 })
384 }).collect::<Vec<_>>(),
385 "analysis_metadata": {
386 "duration_ms": result.metadata.duration_ms,
387 "path": result.metadata.path.display().to_string(),
388 "version": result.metadata.version.clone(),
389 "timestamp": result.metadata.timestamp.clone(),
390 }
391 });
392
393 if !result.warnings.is_empty() {
395 response["warnings"] = json!(
396 result
397 .warnings
398 .iter()
399 .map(|w| {
400 json!({
401 "resource": w.resource,
402 "issue": w.issue.to_string(),
403 "severity": w.severity.as_str(),
404 "message": w.message,
405 })
406 })
407 .collect::<Vec<_>>()
408 );
409 }
410
411 if let Some(savings) = result.summary.estimated_monthly_savings_usd {
413 response["estimated_savings"] = json!({
414 "monthly_usd": savings,
415 "annual_usd": savings * 12.0,
416 });
417 }
418
419 response["rule_codes"] = json!({
421 rule_codes::NO_CPU_REQUEST: rule_description(rule_codes::NO_CPU_REQUEST),
422 rule_codes::NO_MEMORY_REQUEST: rule_description(rule_codes::NO_MEMORY_REQUEST),
423 rule_codes::NO_CPU_LIMIT: rule_description(rule_codes::NO_CPU_LIMIT),
424 rule_codes::NO_MEMORY_LIMIT: rule_description(rule_codes::NO_MEMORY_LIMIT),
425 rule_codes::HIGH_CPU_REQUEST: rule_description(rule_codes::HIGH_CPU_REQUEST),
426 rule_codes::HIGH_MEMORY_REQUEST: rule_description(rule_codes::HIGH_MEMORY_REQUEST),
427 rule_codes::EXCESSIVE_CPU_RATIO: rule_description(rule_codes::EXCESSIVE_CPU_RATIO),
428 rule_codes::EXCESSIVE_MEMORY_RATIO: rule_description(rule_codes::EXCESSIVE_MEMORY_RATIO),
429 rule_codes::REQUESTS_EQUAL_LIMITS: rule_description(rule_codes::REQUESTS_EQUAL_LIMITS),
430 rule_codes::UNBALANCED_RESOURCES: rule_description(rule_codes::UNBALANCED_RESOURCES),
431 });
432
433 if args.cluster.is_some() || args.prometheus.is_some() {
435 response["live_analysis"] = json!({
436 "enabled": args.prometheus.is_some(),
437 "cluster": args.cluster.clone(),
438 "prometheus": args.prometheus.clone(),
439 "prometheus_auth": if args.prometheus_auth_type.is_some() {
440 args.prometheus_auth_type.clone()
441 } else {
442 Some("none".to_string())
443 },
444 "period": args.period.clone().unwrap_or_else(|| "7d".to_string()),
445 "note": if args.prometheus.is_some() {
446 "Historical metrics analysis using Prometheus data."
447 } else {
448 "Live analysis requires Prometheus. Use prometheus_discover and prometheus_connect to set up."
449 },
450 });
451 }
452
453 if args.cloud_provider.is_some() {
455 response["cost_estimation"] = json!({
456 "enabled": true,
457 "provider": args.cloud_provider.clone(),
458 "region": args.region.clone().unwrap_or_else(|| "us-east-1".to_string()),
459 "note": "Cost estimation uses approximate on-demand pricing. Actual costs may vary.",
460 });
461 }
462
463 let action_items: Vec<String> = result
465 .recommendations
466 .iter()
467 .filter(|r| r.severity >= Severity::Medium)
468 .map(|r| {
469 format!(
470 "[{}] {} in {}/{}",
471 r.rule_code.as_str(),
472 r.message,
473 r.resource_kind,
474 r.resource_name
475 )
476 })
477 .collect();
478
479 if !action_items.is_empty() {
480 response["action_items"] = json!(action_items);
481 }
482
483 response
484 }
485}
486
487impl Tool for K8sOptimizeTool {
488 const NAME: &'static str = "k8s_optimize";
489
490 type Args = K8sOptimizeArgs;
491 type Output = String;
492 type Error = K8sOptimizeError;
493
494 async fn definition(&self, _prompt: String) -> ToolDefinition {
495 ToolDefinition {
496 name: Self::NAME.to_string(),
497 description: r#"Analyze Kubernetes manifests for resource optimization.
498
499**IMPORTANT: Only use when user EXPLICITLY asks about:**
500- "optimize my K8s resources" / "right-size my pods"
501- "full analysis" / "comprehensive check" (use full=true)
502- Over-provisioned or under-provisioned resources
503- Cost optimization for Kubernetes
504
505**DO NOT use for:**
506- General K8s linting without optimization focus (use kubelint)
507- Tasks where user didn't ask about optimization
508
509## For Live Cluster Analysis with Historical Metrics
510
511**RECOMMENDED FLOW when user wants data-driven optimization:**
5121. First use `prometheus_discover` to find Prometheus in cluster
5132. Use `prometheus_connect` to establish connection (starts port-forward)
5143. Call `k8s_optimize` with the prometheus URL from step 2
515
516Port-forward is preferred (no auth needed). Auth is only needed for external Prometheus URLs.
517
518## Modes
519- **Standard**: Resource optimization analysis only
520- **Full** (full=true): Comprehensive analysis including:
521 - Resource optimization (CPU/memory waste)
522 - Security checks (kubelint - privileged, RBAC, etc.)
523 - Helm validation (if charts present)
524- **Live**: With prometheus URL for historical metrics (data-driven recommendations)
525
526## Returns (analysis only - does NOT apply changes)
527- Summary with issue counts and waste percentage
528- Recommendations with suggested values (based on actual usage if Prometheus provided)
529- Security findings (if full=true)
530- Does NOT automatically modify files"#
531 .to_string(),
532 parameters: json!({
533 "type": "object",
534 "properties": {
535 "path": {
536 "type": "string",
537 "description": "Path to K8s manifest file or directory (relative to project root). Examples: 'k8s/', 'deployments/api.yaml', 'charts/myapp/', 'terraform/'"
538 },
539 "content": {
540 "type": "string",
541 "description": "Inline YAML content to analyze (alternative to path)"
542 },
543 "severity": {
544 "type": "string",
545 "description": "Minimum severity to report: 'critical', 'high', 'medium', 'low', 'info'. Default: 'medium'",
546 "enum": ["critical", "high", "medium", "low", "info"]
547 },
548 "threshold": {
549 "type": "integer",
550 "description": "Minimum waste percentage to report (default: 10)"
551 },
552 "include_info": {
553 "type": "boolean",
554 "description": "Include info-level suggestions (default: false)"
555 },
556 "include_system": {
557 "type": "boolean",
558 "description": "Include system namespaces like kube-system (default: false)"
559 },
560 "full": {
561 "type": "boolean",
562 "description": "Run FULL comprehensive analysis: optimize + kubelint security + helmlint. Use when user asks for 'full analysis' or 'check everything'."
563 },
564 "cluster": {
565 "type": "string",
566 "description": "Connect to a Kubernetes cluster for live analysis (kubeconfig context name). Requires cluster connectivity."
567 },
568 "prometheus": {
569 "type": "string",
570 "description": "Prometheus URL for historical metrics (from prometheus_connect tool, e.g., 'http://localhost:52431')"
571 },
572 "prometheus_auth_type": {
573 "type": "string",
574 "description": "Prometheus auth type (only for external URL, NOT for port-forward): 'none', 'basic', 'bearer'",
575 "enum": ["none", "basic", "bearer"]
576 },
577 "prometheus_username": {
578 "type": "string",
579 "description": "Username for Prometheus basic auth (only for external URL)"
580 },
581 "prometheus_password": {
582 "type": "string",
583 "description": "Password for Prometheus basic auth (only for external URL)"
584 },
585 "prometheus_token": {
586 "type": "string",
587 "description": "Bearer token for Prometheus auth (only for external URL)"
588 },
589 "period": {
590 "type": "string",
591 "description": "Analysis period for live metrics (e.g., '7d', '24h', '1h'). Default: '7d'"
592 },
593 "cloud_provider": {
594 "type": "string",
595 "description": "Cloud provider for cost estimation: 'aws', 'gcp', 'azure', 'onprem'",
596 "enum": ["aws", "gcp", "azure", "onprem"]
597 },
598 "region": {
599 "type": "string",
600 "description": "Cloud region for pricing (e.g., 'us-east-1', 'us-central1')"
601 }
602 }
603 }),
604 }
605 }
606
607 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
608 let config = self.build_config(&args);
609
610 let mut result = if args.content.as_ref().is_some_and(|c| !c.trim().is_empty()) {
612 analyze_content(args.content.as_ref().unwrap(), &config)
614 } else {
615 let path = args.path.as_deref().unwrap_or(".");
617 let full_path = if std::path::Path::new(path).is_absolute() {
618 PathBuf::from(path)
619 } else {
620 self.project_root.join(path)
621 };
622
623 if !full_path.exists() {
624 return Err(K8sOptimizeError(format!(
625 "Path not found: {}",
626 full_path.display()
627 )));
628 }
629
630 analyze(&full_path, &config)
631 };
632
633 let prometheus_enhancement = if let Some(prometheus_url) = &args.prometheus {
635 let auth = Self::build_prometheus_auth(&args);
636 match PrometheusClient::with_auth(prometheus_url, auth) {
637 Ok(client) => {
638 if client.is_available().await {
639 let period = args.period.as_deref().unwrap_or("7d");
640 Some(
641 self.enhance_with_prometheus(&mut result, &client, period)
642 .await,
643 )
644 } else {
645 None
646 }
647 }
648 Err(_) => None,
649 }
650 } else {
651 None
652 };
653
654 let mut output = self.format_for_agent(&result, &args);
656
657 if args.full {
658 let path = args.path.as_deref().unwrap_or(".");
659 let full_path = if std::path::Path::new(path).is_absolute() {
660 PathBuf::from(path)
661 } else {
662 self.project_root.join(path)
663 };
664
665 let kubelint_config =
667 crate::analyzer::kubelint::KubelintConfig::default().with_all_builtin();
668 let kubelint_result = crate::analyzer::kubelint::lint(&full_path, &kubelint_config);
669
670 output["security_analysis"] = json!({
671 "objects_analyzed": kubelint_result.summary.objects_analyzed,
672 "checks_run": kubelint_result.summary.checks_run,
673 "issues_found": kubelint_result.failures.len(),
674 "findings": kubelint_result.failures.iter().take(20).map(|f| {
675 json!({
676 "code": f.code.to_string(),
677 "severity": format!("{:?}", f.severity).to_lowercase(),
678 "object": format!("{}/{}", f.object_kind, f.object_name),
679 "message": f.message,
680 "remediation": f.remediation,
681 })
682 }).collect::<Vec<_>>(),
683 });
684
685 let helm_charts = find_helm_charts(&full_path);
687 if !helm_charts.is_empty() {
688 let helmlint_config = crate::analyzer::helmlint::HelmlintConfig::default();
689 let mut chart_results: Vec<serde_json::Value> = Vec::new();
690
691 for chart_path in &helm_charts {
692 let chart_name = chart_path
693 .file_name()
694 .map(|n| n.to_string_lossy().to_string())
695 .unwrap_or_else(|| "unknown".to_string());
696 let helmlint_result =
697 crate::analyzer::helmlint::lint_chart(chart_path, &helmlint_config);
698
699 chart_results.push(json!({
700 "chart": chart_name,
701 "issues": helmlint_result.failures.iter().map(|f| {
702 json!({
703 "code": f.code.to_string(),
704 "severity": format!("{:?}", f.severity).to_lowercase(),
705 "message": f.message,
706 })
707 }).collect::<Vec<_>>(),
708 }));
709 }
710
711 output["helm_validation"] = json!({
712 "charts_analyzed": helm_charts.len(),
713 "results": chart_results,
714 });
715 }
716
717 output["analysis_mode"] = json!("full");
718 }
719
720 if let Some(enhancement) = prometheus_enhancement {
722 output["prometheus_analysis"] = json!({
723 "enabled": true,
724 "url": args.prometheus,
725 "period": args.period.clone().unwrap_or_else(|| "7d".to_string()),
726 "workloads_enhanced": enhancement.enhanced_count,
727 "workloads_no_data": enhancement.no_data_count,
728 "mode": if enhancement.enhanced_count > 0 { "data-driven" } else { "static" },
729 "historical_data": enhancement.prometheus_data,
730 "note": if enhancement.enhanced_count > 0 {
731 format!(
732 "Recommendations for {} workloads are based on actual P99 usage from Prometheus. {} workloads had no historical data.",
733 enhancement.enhanced_count,
734 enhancement.no_data_count
735 )
736 } else {
737 "No historical data found in Prometheus for the analyzed workloads. Recommendations are heuristic-based.".to_string()
738 }
739 });
740
741 if enhancement.enhanced_count > 0 {
743 output["summary"]["mode"] = json!("prometheus");
744 }
745 }
746
747 let config = CompressionConfig::default();
750 Ok(compress_tool_output(&output, "k8s_optimize", &config))
751 }
752}
753
754#[cfg(test)]
755mod tests {
756 use super::*;
757
758 #[test]
759 fn test_tool_name() {
760 assert_eq!(K8sOptimizeTool::NAME, "k8s_optimize");
761 }
762
763 #[tokio::test]
764 async fn test_analyze_content() {
765 let tool = K8sOptimizeTool::new(PathBuf::from("."));
766
767 let yaml = r#"
768apiVersion: apps/v1
769kind: Deployment
770metadata:
771 name: test-app
772spec:
773 replicas: 1
774 selector:
775 matchLabels:
776 app: test
777 template:
778 spec:
779 containers:
780 - name: app
781 image: myapp:v1
782"#;
783
784 let args = K8sOptimizeArgs {
785 path: None,
786 content: Some(yaml.to_string()),
787 severity: None,
788 threshold: None,
789 include_info: false,
790 include_system: true,
791 full: false,
792 cluster: None,
793 prometheus: None,
794 prometheus_auth_type: None,
795 prometheus_username: None,
796 prometheus_password: None,
797 prometheus_token: None,
798 period: None,
799 cloud_provider: None,
800 region: None,
801 };
802
803 let result = tool.call(args).await.unwrap();
804 assert!(result.contains("summary"));
805 assert!(result.contains("recommendations"));
806 assert!(result.contains("rule_codes"));
807 }
808
809 #[tokio::test]
810 async fn test_build_config() {
811 let tool = K8sOptimizeTool::new(PathBuf::from("."));
812
813 let args = K8sOptimizeArgs {
814 path: None,
815 content: None,
816 severity: Some("high".to_string()),
817 threshold: Some(20),
818 include_info: true,
819 include_system: true,
820 full: false,
821 cluster: None,
822 prometheus: None,
823 prometheus_auth_type: None,
824 prometheus_username: None,
825 prometheus_password: None,
826 prometheus_token: None,
827 period: None,
828 cloud_provider: None,
829 region: None,
830 };
831
832 let config = tool.build_config(&args);
833 assert_eq!(config.waste_threshold_percent, 20);
834 assert!(config.include_info);
835 assert!(config.include_system);
836 }
837
838 #[tokio::test]
839 async fn test_output_format() {
840 let tool = K8sOptimizeTool::new(PathBuf::from("."));
841
842 let yaml = r#"
843apiVersion: apps/v1
844kind: Deployment
845metadata:
846 name: over-provisioned
847spec:
848 replicas: 1
849 selector:
850 matchLabels:
851 app: test
852 template:
853 spec:
854 containers:
855 - name: nginx
856 image: nginx:1.21
857 resources:
858 requests:
859 cpu: 4000m
860 memory: 8Gi
861 limits:
862 cpu: 8000m
863 memory: 16Gi
864"#;
865
866 let args = K8sOptimizeArgs {
867 path: None,
868 content: Some(yaml.to_string()),
869 severity: None,
870 threshold: None,
871 include_info: false,
872 include_system: true,
873 full: false,
874 cluster: None,
875 prometheus: None,
876 prometheus_auth_type: None,
877 prometheus_username: None,
878 prometheus_password: None,
879 prometheus_token: None,
880 period: None,
881 cloud_provider: Some("aws".to_string()),
882 region: Some("us-east-1".to_string()),
883 };
884
885 let result = tool.call(args).await.unwrap();
886
887 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
889
890 assert!(json.get("summary").is_some());
891 assert!(json.get("recommendations").is_some());
892 assert!(json.get("rule_codes").is_some());
893 assert!(json.get("cost_estimation").is_some());
894 }
895
896 #[test]
897 fn test_build_prometheus_auth_none() {
898 let args = K8sOptimizeArgs {
899 path: None,
900 content: None,
901 severity: None,
902 threshold: None,
903 include_info: false,
904 include_system: false,
905 full: false,
906 cluster: None,
907 prometheus: Some("http://localhost:9090".to_string()),
908 prometheus_auth_type: None,
909 prometheus_username: None,
910 prometheus_password: None,
911 prometheus_token: None,
912 period: None,
913 cloud_provider: None,
914 region: None,
915 };
916
917 let auth = K8sOptimizeTool::build_prometheus_auth(&args);
918 assert!(matches!(auth, PrometheusAuth::None));
919 }
920
921 #[test]
922 fn test_build_prometheus_auth_basic() {
923 let args = K8sOptimizeArgs {
924 path: None,
925 content: None,
926 severity: None,
927 threshold: None,
928 include_info: false,
929 include_system: false,
930 full: false,
931 cluster: None,
932 prometheus: Some("https://prometheus.example.com".to_string()),
933 prometheus_auth_type: Some("basic".to_string()),
934 prometheus_username: Some("admin".to_string()),
935 prometheus_password: Some("secret".to_string()),
936 prometheus_token: None,
937 period: None,
938 cloud_provider: None,
939 region: None,
940 };
941
942 let auth = K8sOptimizeTool::build_prometheus_auth(&args);
943 match auth {
944 PrometheusAuth::Basic { username, password } => {
945 assert_eq!(username, "admin");
946 assert_eq!(password, "secret");
947 }
948 _ => panic!("Expected Basic auth"),
949 }
950 }
951}