Skip to main content

ruvector_memopt/apps/
docker.rs

1//! Docker container resource management
2//!
3//! Monitors and manages Docker containers, allowing users to:
4//! - View container resource usage (memory, CPU)
5//! - Pause/unpause containers
6//! - Stop unused containers
7//! - Identify resource-heavy containers
8
9use super::{OptimizationAction, OptimizationResult};
10use serde::{Deserialize, Serialize};
11use std::process::Command;
12
13/// Docker container info
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ContainerInfo {
16    pub id: String,
17    pub name: String,
18    pub image: String,
19    pub status: ContainerStatus,
20    pub memory_mb: f64,
21    pub memory_limit_mb: f64,
22    pub memory_percent: f64,
23    pub cpu_percent: f64,
24    pub created: String,
25    pub ports: Vec<String>,
26    pub is_idle: bool,
27}
28
29/// Container status
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31pub enum ContainerStatus {
32    Running,
33    Paused,
34    Exited,
35    Created,
36    Restarting,
37    Dead,
38    Unknown,
39}
40
41impl ContainerStatus {
42    fn from_str(s: &str) -> Self {
43        let s = s.to_lowercase();
44        if s.contains("running") {
45            ContainerStatus::Running
46        } else if s.contains("paused") {
47            ContainerStatus::Paused
48        } else if s.contains("exited") {
49            ContainerStatus::Exited
50        } else if s.contains("created") {
51            ContainerStatus::Created
52        } else if s.contains("restarting") {
53            ContainerStatus::Restarting
54        } else if s.contains("dead") {
55            ContainerStatus::Dead
56        } else {
57            ContainerStatus::Unknown
58        }
59    }
60}
61
62impl ContainerInfo {
63    /// Get suggested action for this container
64    pub fn get_suggested_action(&self) -> OptimizationAction {
65        match self.status {
66            ContainerStatus::Running => {
67                if self.memory_mb > 2000.0 {
68                    OptimizationAction::StopContainer
69                } else if self.is_idle && self.memory_mb > 500.0 {
70                    OptimizationAction::PauseContainer
71                } else {
72                    OptimizationAction::None
73                }
74            }
75            ContainerStatus::Paused => OptimizationAction::None,
76            _ => OptimizationAction::None,
77        }
78    }
79}
80
81/// Docker manager
82pub struct DockerManager {
83    containers: Vec<ContainerInfo>,
84    docker_available: bool,
85    last_update: std::time::Instant,
86}
87
88impl DockerManager {
89    pub fn new() -> Self {
90        let mut manager = Self {
91            containers: Vec::new(),
92            docker_available: false,
93            last_update: std::time::Instant::now(),
94        };
95        manager.check_docker();
96        manager
97    }
98
99    /// Check if Docker is available
100    fn check_docker(&mut self) {
101        self.docker_available = Command::new("docker")
102            .arg("version")
103            .output()
104            .map(|o| o.status.success())
105            .unwrap_or(false);
106    }
107
108    /// Check if Docker is available
109    pub fn is_available(&self) -> bool {
110        self.docker_available
111    }
112
113    /// Refresh container data
114    pub fn refresh(&mut self) {
115        if !self.docker_available {
116            return;
117        }
118
119        self.containers.clear();
120
121        // Get container list with stats
122        // Format: ID|Name|Image|Status|Memory|MemLimit|MemPerc|CPUPerc|Created|Ports
123        let output = Command::new("docker")
124            .args([
125                "stats",
126                "--no-stream",
127                "--format",
128                "{{.ID}}|{{.Name}}|{{.Container}}|{{.MemUsage}}|{{.MemPerc}}|{{.CPUPerc}}",
129            ])
130            .output();
131
132        if let Ok(output) = output {
133            if output.status.success() {
134                let stdout = String::from_utf8_lossy(&output.stdout);
135                for line in stdout.lines() {
136                    if let Some(container) = self.parse_stats_line(line) {
137                        self.containers.push(container);
138                    }
139                }
140            }
141        }
142
143        // Get additional container info
144        self.enrich_container_info();
145
146        self.last_update = std::time::Instant::now();
147    }
148
149    /// Parse a docker stats output line
150    fn parse_stats_line(&self, line: &str) -> Option<ContainerInfo> {
151        let parts: Vec<&str> = line.split('|').collect();
152        if parts.len() < 6 {
153            return None;
154        }
155
156        let id = parts[0].trim().to_string();
157        let name = parts[1].trim().to_string();
158
159        // Parse memory usage (e.g., "100MiB / 2GiB")
160        let mem_parts: Vec<&str> = parts[3].split('/').collect();
161        let memory_mb = parse_memory_string(mem_parts.get(0).unwrap_or(&"0"));
162        let memory_limit_mb = parse_memory_string(mem_parts.get(1).unwrap_or(&"0"));
163
164        // Parse percentages
165        let memory_percent = parts[4]
166            .trim()
167            .trim_end_matches('%')
168            .parse::<f64>()
169            .unwrap_or(0.0);
170        let cpu_percent = parts[5]
171            .trim()
172            .trim_end_matches('%')
173            .parse::<f64>()
174            .unwrap_or(0.0);
175
176        Some(ContainerInfo {
177            id,
178            name,
179            image: String::new(),
180            status: ContainerStatus::Running,
181            memory_mb,
182            memory_limit_mb,
183            memory_percent,
184            cpu_percent,
185            created: String::new(),
186            ports: Vec::new(),
187            is_idle: cpu_percent < 1.0,
188        })
189    }
190
191    /// Get additional container info via docker inspect
192    fn enrich_container_info(&mut self) {
193        for container in &mut self.containers {
194            // Get image and status
195            let output = Command::new("docker")
196                .args([
197                    "inspect",
198                    "--format",
199                    "{{.Config.Image}}|{{.State.Status}}|{{.Created}}",
200                    &container.id,
201                ])
202                .output();
203
204            if let Ok(output) = output {
205                if output.status.success() {
206                    let stdout = String::from_utf8_lossy(&output.stdout);
207                    let parts: Vec<&str> = stdout.trim().split('|').collect();
208                    if parts.len() >= 3 {
209                        container.image = parts[0].to_string();
210                        container.status = ContainerStatus::from_str(parts[1]);
211                        container.created = parts[2].to_string();
212                    }
213                }
214            }
215
216            // Get ports
217            let output = Command::new("docker")
218                .args(["port", &container.id])
219                .output();
220
221            if let Ok(output) = output {
222                if output.status.success() {
223                    let stdout = String::from_utf8_lossy(&output.stdout);
224                    container.ports = stdout.lines().map(|s| s.to_string()).collect();
225                }
226            }
227        }
228    }
229
230    /// Get all containers
231    pub fn get_containers(&self) -> &[ContainerInfo] {
232        &self.containers
233    }
234
235    /// Get running containers
236    pub fn get_running(&self) -> Vec<&ContainerInfo> {
237        self.containers
238            .iter()
239            .filter(|c| c.status == ContainerStatus::Running)
240            .collect()
241    }
242
243    /// Get total memory usage
244    pub fn total_memory_mb(&self) -> f64 {
245        self.containers.iter().map(|c| c.memory_mb).sum()
246    }
247
248    /// Get idle containers (running but low CPU)
249    pub fn get_idle_containers(&self) -> Vec<&ContainerInfo> {
250        self.containers
251            .iter()
252            .filter(|c| c.status == ContainerStatus::Running && c.is_idle)
253            .collect()
254    }
255
256    /// Pause a container
257    pub fn pause_container(&self, id: &str) -> OptimizationResult {
258        let output = Command::new("docker")
259            .args(["pause", id])
260            .output();
261
262        let container_name = self.containers
263            .iter()
264            .find(|c| c.id == id || c.name == id)
265            .map(|c| c.name.clone())
266            .unwrap_or_else(|| id.to_string());
267
268        match output {
269            Ok(output) if output.status.success() => OptimizationResult {
270                app_name: container_name,
271                action: OptimizationAction::PauseContainer,
272                success: true,
273                memory_freed_mb: 0.0, // Pausing doesn't free memory but stops CPU usage
274                message: "Container paused successfully".to_string(),
275            },
276            Ok(output) => OptimizationResult {
277                app_name: container_name,
278                action: OptimizationAction::PauseContainer,
279                success: false,
280                memory_freed_mb: 0.0,
281                message: String::from_utf8_lossy(&output.stderr).to_string(),
282            },
283            Err(e) => OptimizationResult {
284                app_name: container_name,
285                action: OptimizationAction::PauseContainer,
286                success: false,
287                memory_freed_mb: 0.0,
288                message: e.to_string(),
289            },
290        }
291    }
292
293    /// Unpause a container
294    pub fn unpause_container(&self, id: &str) -> OptimizationResult {
295        let output = Command::new("docker")
296            .args(["unpause", id])
297            .output();
298
299        let container_name = self.containers
300            .iter()
301            .find(|c| c.id == id || c.name == id)
302            .map(|c| c.name.clone())
303            .unwrap_or_else(|| id.to_string());
304
305        match output {
306            Ok(output) if output.status.success() => OptimizationResult {
307                app_name: container_name,
308                action: OptimizationAction::None,
309                success: true,
310                memory_freed_mb: 0.0,
311                message: "Container unpaused successfully".to_string(),
312            },
313            Ok(output) => OptimizationResult {
314                app_name: container_name,
315                action: OptimizationAction::None,
316                success: false,
317                memory_freed_mb: 0.0,
318                message: String::from_utf8_lossy(&output.stderr).to_string(),
319            },
320            Err(e) => OptimizationResult {
321                app_name: container_name,
322                action: OptimizationAction::None,
323                success: false,
324                memory_freed_mb: 0.0,
325                message: e.to_string(),
326            },
327        }
328    }
329
330    /// Stop a container
331    pub fn stop_container(&self, id: &str) -> OptimizationResult {
332        let container = self.containers
333            .iter()
334            .find(|c| c.id == id || c.name == id);
335
336        let (container_name, memory_freed) = container
337            .map(|c| (c.name.clone(), c.memory_mb))
338            .unwrap_or_else(|| (id.to_string(), 0.0));
339
340        let output = Command::new("docker")
341            .args(["stop", id])
342            .output();
343
344        match output {
345            Ok(output) if output.status.success() => OptimizationResult {
346                app_name: container_name,
347                action: OptimizationAction::StopContainer,
348                success: true,
349                memory_freed_mb: memory_freed,
350                message: format!("Container stopped, freed ~{:.0} MB", memory_freed),
351            },
352            Ok(output) => OptimizationResult {
353                app_name: container_name,
354                action: OptimizationAction::StopContainer,
355                success: false,
356                memory_freed_mb: 0.0,
357                message: String::from_utf8_lossy(&output.stderr).to_string(),
358            },
359            Err(e) => OptimizationResult {
360                app_name: container_name,
361                action: OptimizationAction::StopContainer,
362                success: false,
363                memory_freed_mb: 0.0,
364                message: e.to_string(),
365            },
366        }
367    }
368
369    /// Get optimization suggestions
370    pub fn get_suggestions(&self) -> Vec<(String, OptimizationAction, String)> {
371        let mut suggestions = Vec::new();
372
373        for container in &self.containers {
374            let action = container.get_suggested_action();
375            if action != OptimizationAction::None {
376                let reason = match &action {
377                    OptimizationAction::StopContainer => {
378                        format!(
379                            "Container '{}' is using {:.0} MB - consider stopping if not needed",
380                            container.name, container.memory_mb
381                        )
382                    }
383                    OptimizationAction::PauseContainer => {
384                        format!(
385                            "Container '{}' is idle but using {:.0} MB - consider pausing",
386                            container.name, container.memory_mb
387                        )
388                    }
389                    _ => continue,
390                };
391
392                suggestions.push((container.name.clone(), action, reason));
393            }
394        }
395
396        suggestions
397    }
398
399    /// Print container summary
400    pub fn print_summary(&self) {
401        if !self.docker_available {
402            println!("\n🐳 Docker: Not available or not running");
403            return;
404        }
405
406        println!("\n🐳 Docker Container Resource Usage\n");
407
408        if self.containers.is_empty() {
409            println!("No running containers found.");
410            return;
411        }
412
413        println!("β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”");
414        println!("β”‚ Container            β”‚ Memory    β”‚ CPU      β”‚ Status   β”‚ Image    β”‚");
415        println!("β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€");
416
417        let mut containers: Vec<_> = self.containers.iter().collect();
418        containers.sort_by(|a, b| b.memory_mb.partial_cmp(&a.memory_mb).unwrap());
419
420        for container in &containers {
421            let status = match container.status {
422                ContainerStatus::Running if container.is_idle => "πŸ’€ Idle",
423                ContainerStatus::Running => "🟒 Running",
424                ContainerStatus::Paused => "⏸️ Paused",
425                ContainerStatus::Exited => "⏹️ Exited",
426                _ => "❓ Unknown",
427            };
428
429            let image = truncate(&container.image, 8);
430
431            println!(
432                "β”‚ {:20} β”‚ {:>7.0} MB β”‚ {:>6.1}%  β”‚ {:8} β”‚ {:8} β”‚",
433                truncate(&container.name, 20),
434                container.memory_mb,
435                container.cpu_percent,
436                status,
437                image
438            );
439        }
440
441        println!("β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜");
442
443        let total: f64 = containers.iter().map(|c| c.memory_mb).sum();
444        let running = containers.iter().filter(|c| c.status == ContainerStatus::Running).count();
445        let idle = self.get_idle_containers().len();
446
447        println!(
448            "\nTotal: {:.0} MB across {} containers ({} running, {} idle)",
449            total,
450            containers.len(),
451            running,
452            idle
453        );
454
455        // Suggestions
456        let suggestions = self.get_suggestions();
457        if !suggestions.is_empty() {
458            println!("\nπŸ’‘ Suggestions:");
459            for (_, _, reason) in suggestions.iter().take(3) {
460                println!("   β€’ {}", reason);
461            }
462        }
463    }
464}
465
466impl Default for DockerManager {
467    fn default() -> Self {
468        Self::new()
469    }
470}
471
472/// Parse memory string like "100MiB" or "2GiB" to MB
473fn parse_memory_string(s: &str) -> f64 {
474    let s = s.trim().to_lowercase();
475
476    // Extract number and unit
477    let mut num_str = String::new();
478    let mut unit = String::new();
479
480    for c in s.chars() {
481        if c.is_ascii_digit() || c == '.' {
482            num_str.push(c);
483        } else if c.is_alphabetic() {
484            unit.push(c);
485        }
486    }
487
488    let num: f64 = num_str.parse().unwrap_or(0.0);
489
490    match unit.as_str() {
491        "b" => num / (1024.0 * 1024.0),
492        "kib" | "kb" | "k" => num / 1024.0,
493        "mib" | "mb" | "m" => num,
494        "gib" | "gb" | "g" => num * 1024.0,
495        "tib" | "tb" | "t" => num * 1024.0 * 1024.0,
496        _ => num,
497    }
498}
499
500fn truncate(s: &str, max: usize) -> String {
501    if s.len() <= max {
502        format!("{:width$}", s, width = max)
503    } else {
504        format!("{}...", &s[..max.saturating_sub(3)])
505    }
506}