All checks were successful
Test / frontend-typecheck (pull_request) Successful in 1m49s
Test / frontend-tests (pull_request) Successful in 1m46s
PR Review Automation / review (pull_request) Successful in 4m24s
Test / rust-fmt-check (pull_request) Successful in 12m1s
Test / rust-clippy (pull_request) Successful in 13m46s
Test / rust-tests (pull_request) Successful in 15m8s
BLOCKER fixes: - Implement create_azuredevops_workitem instead of returning a stub error, reusing the existing create_work_item integration helper and writing an audit-log entry on success. - Log kill failures in PtySession::Drop so leaked child processes surface in tracing rather than being silently swallowed. - Add explicit PTY cleanup on every exit path of run_session_io (process exit, read error, write error, resize error, terminate command). - Treat PTY resize failures as fatal: emit terminal-error to the frontend and break the session loop instead of just warning. WARNING fixes: - Remove the dead extract_json_path_value helper from commands/kube.rs. - Wrap temp kubeconfig files in commands/metrics.rs in an RAII guard (TempKubeconfig) so they're removed on early-return / panic paths. - Wrap temp kubeconfig files in commands/shell.rs PTY-session starters in a disarmable RAII guard (KubeconfigGuard); if kubectl resolution fails we no longer leak the file. - Drop the `clear;` prefix from the kubectl-exec shell fallback so containers without `clear`/`tput` don't print a confusing error. SUGGESTION fixes: - Document why node CPU/memory percentages are 0.0 in metrics::client and link the gap to future work fetching node capacity. - Add a module-level doc comment to AppState describing the synchronization expectations (std vs tokio Mutex) for each public field, and warn against holding std::sync MutexGuards across .await. Verified: cargo fmt --check, cargo clippy -- -D warnings, and cargo test (377 passed, 6 ignored) all pass.
247 lines
7.6 KiB
Rust
247 lines
7.6 KiB
Rust
use anyhow::{Context, Result};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
pub struct PodMetrics {
|
|
pub name: String,
|
|
pub namespace: String,
|
|
pub containers: Vec<ContainerMetrics>,
|
|
pub cpu: String, // e.g., "100m"
|
|
pub memory: String, // e.g., "256Mi"
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
pub struct ContainerMetrics {
|
|
pub name: String,
|
|
pub cpu: String,
|
|
pub memory: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
pub struct NodeMetrics {
|
|
pub name: String,
|
|
pub cpu: String,
|
|
pub memory: String,
|
|
pub cpu_percent: f64,
|
|
pub memory_percent: f64,
|
|
}
|
|
|
|
/// Parse kubectl top pods output (JSON format)
|
|
pub fn parse_pod_metrics(json_output: &str) -> Result<Vec<PodMetrics>> {
|
|
let value: serde_json::Value =
|
|
serde_json::from_str(json_output).context("Failed to parse kubectl top pods JSON")?;
|
|
|
|
let items = value
|
|
.get("items")
|
|
.and_then(|v| v.as_array())
|
|
.context("Missing items array")?;
|
|
|
|
let mut metrics = Vec::new();
|
|
|
|
for item in items {
|
|
let name = item
|
|
.get("metadata")
|
|
.and_then(|m| m.get("name"))
|
|
.and_then(|n| n.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
|
|
let namespace = item
|
|
.get("metadata")
|
|
.and_then(|m| m.get("namespace"))
|
|
.and_then(|n| n.as_str())
|
|
.unwrap_or("default")
|
|
.to_string();
|
|
|
|
let containers_data = item.get("containers").and_then(|c| c.as_array());
|
|
|
|
let mut containers = Vec::new();
|
|
let mut total_cpu_nano = 0u64;
|
|
let mut total_memory_kb = 0u64;
|
|
|
|
if let Some(containers_data) = containers_data {
|
|
for container in containers_data {
|
|
let container_name = container
|
|
.get("name")
|
|
.and_then(|n| n.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
|
|
let cpu_usage = container
|
|
.get("usage")
|
|
.and_then(|u| u.get("cpu"))
|
|
.and_then(|c| c.as_str())
|
|
.unwrap_or("0")
|
|
.to_string();
|
|
|
|
let memory_usage = container
|
|
.get("usage")
|
|
.and_then(|u| u.get("memory"))
|
|
.and_then(|m| m.as_str())
|
|
.unwrap_or("0")
|
|
.to_string();
|
|
|
|
// Parse for totals
|
|
total_cpu_nano += parse_cpu_to_nanocores(&cpu_usage);
|
|
total_memory_kb += parse_memory_to_kb(&memory_usage);
|
|
|
|
containers.push(ContainerMetrics {
|
|
name: container_name,
|
|
cpu: cpu_usage,
|
|
memory: memory_usage,
|
|
});
|
|
}
|
|
}
|
|
|
|
metrics.push(PodMetrics {
|
|
name,
|
|
namespace,
|
|
containers,
|
|
cpu: format_cpu_from_nanocores(total_cpu_nano),
|
|
memory: format_memory_from_kb(total_memory_kb),
|
|
});
|
|
}
|
|
|
|
Ok(metrics)
|
|
}
|
|
|
|
/// Parse kubectl top nodes output (JSON format)
|
|
pub fn parse_node_metrics(json_output: &str) -> Result<Vec<NodeMetrics>> {
|
|
let value: serde_json::Value =
|
|
serde_json::from_str(json_output).context("Failed to parse kubectl top nodes JSON")?;
|
|
|
|
let items = value
|
|
.get("items")
|
|
.and_then(|v| v.as_array())
|
|
.context("Missing items array")?;
|
|
|
|
let mut metrics = Vec::new();
|
|
|
|
for item in items {
|
|
let name = item
|
|
.get("metadata")
|
|
.and_then(|m| m.get("name"))
|
|
.and_then(|n| n.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
|
|
let cpu = item
|
|
.get("usage")
|
|
.and_then(|u| u.get("cpu"))
|
|
.and_then(|c| c.as_str())
|
|
.unwrap_or("0")
|
|
.to_string();
|
|
|
|
let memory = item
|
|
.get("usage")
|
|
.and_then(|u| u.get("memory"))
|
|
.and_then(|m| m.as_str())
|
|
.unwrap_or("0")
|
|
.to_string();
|
|
|
|
// Calculate percentages (simplified - would need capacity from kubectl get nodes).
|
|
//
|
|
// TODO(metrics): Populate these from node `status.capacity` once we add
|
|
// a second kubectl call to fetch node capacity. The metrics-server JSON
|
|
// returned by `kubectl top nodes` only reports raw `usage` (cpu in
|
|
// nanocores, memory in Ki), not the node's allocatable totals, so we
|
|
// cannot compute a real percentage from this response alone.
|
|
// Until that work is done these are reported as 0.0 and the frontend
|
|
// hides the percent column. Tracking issue: see Telemetry/Metrics
|
|
// backlog in the project tracker.
|
|
let cpu_percent = 0.0;
|
|
let memory_percent = 0.0;
|
|
|
|
metrics.push(NodeMetrics {
|
|
name,
|
|
cpu,
|
|
memory,
|
|
cpu_percent,
|
|
memory_percent,
|
|
});
|
|
}
|
|
|
|
Ok(metrics)
|
|
}
|
|
|
|
/// Parse CPU string to nanocores (e.g., "100m" -> 100000000, "2" -> 2000000000)
|
|
fn parse_cpu_to_nanocores(cpu: &str) -> u64 {
|
|
if cpu.ends_with('n') {
|
|
cpu.trim_end_matches('n').parse::<u64>().unwrap_or(0)
|
|
} else if cpu.ends_with('u') {
|
|
cpu.trim_end_matches('u').parse::<u64>().unwrap_or(0) * 1000
|
|
} else if cpu.ends_with('m') {
|
|
cpu.trim_end_matches('m').parse::<u64>().unwrap_or(0) * 1_000_000
|
|
} else {
|
|
cpu.parse::<u64>().unwrap_or(0) * 1_000_000_000
|
|
}
|
|
}
|
|
|
|
/// Parse memory string to kilobytes (e.g., "256Mi" -> 262144, "1Gi" -> 1048576)
|
|
fn parse_memory_to_kb(memory: &str) -> u64 {
|
|
if memory.ends_with("Ki") {
|
|
memory.trim_end_matches("Ki").parse::<u64>().unwrap_or(0)
|
|
} else if memory.ends_with("Mi") {
|
|
memory.trim_end_matches("Mi").parse::<u64>().unwrap_or(0) * 1024
|
|
} else if memory.ends_with("Gi") {
|
|
memory.trim_end_matches("Gi").parse::<u64>().unwrap_or(0) * 1024 * 1024
|
|
} else if memory.ends_with("Ti") {
|
|
memory.trim_end_matches("Ti").parse::<u64>().unwrap_or(0) * 1024 * 1024 * 1024
|
|
} else {
|
|
memory.parse::<u64>().unwrap_or(0) / 1024 // Assume bytes
|
|
}
|
|
}
|
|
|
|
/// Format nanocores back to human-readable (e.g., 100000000 -> "100m")
|
|
fn format_cpu_from_nanocores(nanocores: u64) -> String {
|
|
if nanocores >= 1_000_000_000 {
|
|
format!("{:.1}", nanocores as f64 / 1_000_000_000.0)
|
|
} else {
|
|
format!("{}m", nanocores / 1_000_000)
|
|
}
|
|
}
|
|
|
|
/// Format kilobytes back to human-readable (e.g., 262144 -> "256Mi")
|
|
fn format_memory_from_kb(kb: u64) -> String {
|
|
if kb >= 1024 * 1024 * 1024 {
|
|
format!("{:.1}Ti", kb as f64 / (1024.0 * 1024.0 * 1024.0))
|
|
} else if kb >= 1024 * 1024 {
|
|
format!("{:.0}Gi", kb as f64 / (1024.0 * 1024.0))
|
|
} else if kb >= 1024 {
|
|
format!("{:.0}Mi", kb as f64 / 1024.0)
|
|
} else {
|
|
format!("{}Ki", kb)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_parse_cpu() {
|
|
assert_eq!(parse_cpu_to_nanocores("100m"), 100_000_000);
|
|
assert_eq!(parse_cpu_to_nanocores("2"), 2_000_000_000);
|
|
assert_eq!(parse_cpu_to_nanocores("500u"), 500_000);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_memory() {
|
|
assert_eq!(parse_memory_to_kb("256Mi"), 262_144);
|
|
assert_eq!(parse_memory_to_kb("1Gi"), 1_048_576);
|
|
assert_eq!(parse_memory_to_kb("512Ki"), 512);
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_cpu() {
|
|
assert_eq!(format_cpu_from_nanocores(100_000_000), "100m");
|
|
assert_eq!(format_cpu_from_nanocores(2_000_000_000), "2.0");
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_memory() {
|
|
assert_eq!(format_memory_from_kb(262_144), "256Mi");
|
|
assert_eq!(format_memory_from_kb(1_048_576), "1Gi");
|
|
}
|
|
}
|