docker_wrapper/command/
volume.rs

1//! Docker volume management commands.
2//!
3//! This module provides commands for managing Docker volumes.
4
5use crate::command::{CommandExecutor, CommandOutput, DockerCommand};
6use crate::error::Result;
7use async_trait::async_trait;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use std::collections::HashMap;
11
12/// Docker volume create command
13#[derive(Debug, Clone)]
14pub struct VolumeCreateCommand {
15    name: Option<String>,
16    driver: Option<String>,
17    driver_opts: HashMap<String, String>,
18    labels: HashMap<String, String>,
19    /// Command executor
20    pub executor: CommandExecutor,
21}
22
23impl VolumeCreateCommand {
24    /// Create a new volume create command
25    #[must_use]
26    pub fn new() -> Self {
27        Self {
28            name: None,
29            driver: None,
30            driver_opts: HashMap::new(),
31            labels: HashMap::new(),
32            executor: CommandExecutor::new(),
33        }
34    }
35
36    /// Set volume name
37    #[must_use]
38    pub fn name(mut self, name: impl Into<String>) -> Self {
39        self.name = Some(name.into());
40        self
41    }
42
43    /// Set the volume driver
44    #[must_use]
45    pub fn driver(mut self, driver: impl Into<String>) -> Self {
46        self.driver = Some(driver.into());
47        self
48    }
49
50    /// Add a driver option
51    #[must_use]
52    pub fn driver_opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
53        self.driver_opts.insert(key.into(), value.into());
54        self
55    }
56
57    /// Add a label
58    #[must_use]
59    pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
60        self.labels.insert(key.into(), value.into());
61        self
62    }
63
64    /// Execute the command
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if the Docker daemon is not running or the command fails
69    pub async fn run(&self) -> Result<VolumeCreateResult> {
70        self.execute().await.map(VolumeCreateResult::from)
71    }
72}
73
74impl Default for VolumeCreateCommand {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80#[async_trait]
81impl DockerCommand for VolumeCreateCommand {
82    type Output = CommandOutput;
83
84    fn build_command_args(&self) -> Vec<String> {
85        let mut args = vec!["volume".to_string(), "create".to_string()];
86
87        if let Some(ref driver) = self.driver {
88            args.push("--driver".to_string());
89            args.push(driver.clone());
90        }
91
92        for (key, value) in &self.driver_opts {
93            args.push("--opt".to_string());
94            args.push(format!("{key}={value}"));
95        }
96
97        for (key, value) in &self.labels {
98            args.push("--label".to_string());
99            args.push(format!("{key}={value}"));
100        }
101
102        if let Some(ref name) = self.name {
103            args.push(name.clone());
104        }
105
106        args.extend(self.executor.raw_args.clone());
107        args
108    }
109
110    async fn execute(&self) -> Result<Self::Output> {
111        let args = self.build_command_args();
112        let command_name = args[0].clone();
113        let command_args = args[1..].to_vec();
114        self.executor
115            .execute_command(&command_name, command_args)
116            .await
117    }
118
119    fn get_executor(&self) -> &CommandExecutor {
120        &self.executor
121    }
122
123    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
124        &mut self.executor
125    }
126}
127
128/// Result from volume create
129#[derive(Debug, Clone)]
130pub struct VolumeCreateResult {
131    /// The name of the created volume
132    pub volume_name: String,
133    /// Raw command output
134    pub raw_output: CommandOutput,
135}
136
137impl From<CommandOutput> for VolumeCreateResult {
138    fn from(output: CommandOutput) -> Self {
139        Self {
140            volume_name: output.stdout.trim().to_string(),
141            raw_output: output,
142        }
143    }
144}
145
146/// Docker volume ls command
147#[derive(Debug, Clone)]
148pub struct VolumeLsCommand {
149    filters: HashMap<String, String>,
150    format: Option<String>,
151    quiet: bool,
152    /// Command executor
153    pub executor: CommandExecutor,
154}
155
156impl VolumeLsCommand {
157    /// Create a new volume ls command
158    #[must_use]
159    pub fn new() -> Self {
160        Self {
161            filters: HashMap::new(),
162            format: None,
163            quiet: false,
164            executor: CommandExecutor::new(),
165        }
166    }
167
168    /// Add a filter
169    #[must_use]
170    pub fn filter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
171        self.filters.insert(key.into(), value.into());
172        self
173    }
174
175    /// Set format
176    #[must_use]
177    pub fn format(mut self, format: impl Into<String>) -> Self {
178        self.format = Some(format.into());
179        self
180    }
181
182    /// Only display volume names
183    #[must_use]
184    pub fn quiet(mut self) -> Self {
185        self.quiet = true;
186        self
187    }
188
189    /// Execute the command
190    ///
191    /// # Errors
192    ///
193    /// Returns an error if the Docker daemon is not running or the command fails
194    pub async fn run(&self) -> Result<VolumeLsOutput> {
195        self.execute().await.map(VolumeLsOutput::from)
196    }
197}
198
199impl Default for VolumeLsCommand {
200    fn default() -> Self {
201        Self::new()
202    }
203}
204
205#[async_trait]
206impl DockerCommand for VolumeLsCommand {
207    type Output = CommandOutput;
208
209    fn build_command_args(&self) -> Vec<String> {
210        let mut args = vec!["volume".to_string(), "ls".to_string()];
211
212        for (key, value) in &self.filters {
213            args.push("--filter".to_string());
214            args.push(format!("{key}={value}"));
215        }
216
217        if let Some(ref format) = self.format {
218            args.push("--format".to_string());
219            args.push(format.clone());
220        }
221
222        if self.quiet {
223            args.push("--quiet".to_string());
224        }
225
226        args.extend(self.executor.raw_args.clone());
227        args
228    }
229
230    async fn execute(&self) -> Result<Self::Output> {
231        let args = self.build_command_args();
232        let command_name = args[0].clone();
233        let command_args = args[1..].to_vec();
234        self.executor
235            .execute_command(&command_name, command_args)
236            .await
237    }
238
239    fn get_executor(&self) -> &CommandExecutor {
240        &self.executor
241    }
242
243    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
244        &mut self.executor
245    }
246}
247
248/// Volume information
249#[derive(Debug, Clone, Serialize, Deserialize)]
250#[serde(rename_all = "PascalCase")]
251pub struct VolumeInfo {
252    /// Volume driver
253    #[serde(default)]
254    pub driver: String,
255    /// Volume name
256    #[serde(default)]
257    pub name: String,
258    /// Mount point on the host
259    #[serde(default)]
260    pub mountpoint: String,
261    /// Volume scope (local, global)
262    #[serde(default)]
263    pub scope: String,
264    /// Volume labels
265    #[serde(default)]
266    pub labels: HashMap<String, String>,
267}
268
269/// Output from volume ls
270#[derive(Debug, Clone)]
271pub struct VolumeLsOutput {
272    /// List of volumes
273    pub volumes: Vec<VolumeInfo>,
274    /// Raw command output
275    pub raw_output: CommandOutput,
276}
277
278impl From<CommandOutput> for VolumeLsOutput {
279    fn from(output: CommandOutput) -> Self {
280        let volumes = if output.stdout.starts_with('[') {
281            serde_json::from_str(&output.stdout).unwrap_or_default()
282        } else {
283            vec![]
284        };
285
286        Self {
287            volumes,
288            raw_output: output,
289        }
290    }
291}
292
293/// Docker volume rm command
294#[derive(Debug, Clone)]
295pub struct VolumeRmCommand {
296    volumes: Vec<String>,
297    force: bool,
298    /// Command executor
299    pub executor: CommandExecutor,
300}
301
302impl VolumeRmCommand {
303    /// Create a new volume rm command
304    #[must_use]
305    pub fn new(volume: impl Into<String>) -> Self {
306        Self {
307            volumes: vec![volume.into()],
308            force: false,
309            executor: CommandExecutor::new(),
310        }
311    }
312
313    /// Add a volume to remove
314    #[must_use]
315    pub fn add_volume(mut self, volume: impl Into<String>) -> Self {
316        self.volumes.push(volume.into());
317        self
318    }
319
320    /// Force removal
321    #[must_use]
322    pub fn force(mut self) -> Self {
323        self.force = true;
324        self
325    }
326
327    /// Execute the command
328    ///
329    /// # Errors
330    ///
331    /// Returns an error if the Docker daemon is not running or the command fails
332    pub async fn run(&self) -> Result<VolumeRmResult> {
333        self.execute().await.map(VolumeRmResult::from)
334    }
335}
336
337#[async_trait]
338impl DockerCommand for VolumeRmCommand {
339    type Output = CommandOutput;
340
341    fn build_command_args(&self) -> Vec<String> {
342        let mut args = vec!["volume".to_string(), "rm".to_string()];
343
344        if self.force {
345            args.push("--force".to_string());
346        }
347
348        for volume in &self.volumes {
349            args.push(volume.clone());
350        }
351
352        args.extend(self.executor.raw_args.clone());
353        args
354    }
355
356    async fn execute(&self) -> Result<Self::Output> {
357        let args = self.build_command_args();
358        let command_name = args[0].clone();
359        let command_args = args[1..].to_vec();
360        self.executor
361            .execute_command(&command_name, command_args)
362            .await
363    }
364
365    fn get_executor(&self) -> &CommandExecutor {
366        &self.executor
367    }
368
369    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
370        &mut self.executor
371    }
372}
373
374/// Result from volume rm
375#[derive(Debug, Clone)]
376pub struct VolumeRmResult {
377    /// Names of removed volumes
378    pub removed_volumes: Vec<String>,
379    /// Raw command output
380    pub raw_output: CommandOutput,
381}
382
383impl From<CommandOutput> for VolumeRmResult {
384    fn from(output: CommandOutput) -> Self {
385        let removed_volumes = output
386            .stdout
387            .lines()
388            .filter(|line| !line.is_empty())
389            .map(String::from)
390            .collect();
391
392        Self {
393            removed_volumes,
394            raw_output: output,
395        }
396    }
397}
398
399/// Docker volume inspect command
400#[derive(Debug, Clone)]
401pub struct VolumeInspectCommand {
402    volumes: Vec<String>,
403    format: Option<String>,
404    /// Command executor
405    pub executor: CommandExecutor,
406}
407
408impl VolumeInspectCommand {
409    /// Create a new volume inspect command
410    #[must_use]
411    pub fn new(volume: impl Into<String>) -> Self {
412        Self {
413            volumes: vec![volume.into()],
414            format: None,
415            executor: CommandExecutor::new(),
416        }
417    }
418
419    /// Set format
420    #[must_use]
421    pub fn format(mut self, format: impl Into<String>) -> Self {
422        self.format = Some(format.into());
423        self
424    }
425
426    /// Execute the command
427    ///
428    /// # Errors
429    ///
430    /// Returns an error if the Docker daemon is not running or the command fails
431    pub async fn run(&self) -> Result<VolumeInspectOutput> {
432        self.execute().await.map(VolumeInspectOutput::from)
433    }
434}
435
436#[async_trait]
437impl DockerCommand for VolumeInspectCommand {
438    type Output = CommandOutput;
439
440    fn build_command_args(&self) -> Vec<String> {
441        let mut args = vec!["volume".to_string(), "inspect".to_string()];
442
443        if let Some(ref format) = self.format {
444            args.push("--format".to_string());
445            args.push(format.clone());
446        }
447
448        for volume in &self.volumes {
449            args.push(volume.clone());
450        }
451
452        args.extend(self.executor.raw_args.clone());
453        args
454    }
455
456    async fn execute(&self) -> Result<Self::Output> {
457        let args = self.build_command_args();
458        let command_name = args[0].clone();
459        let command_args = args[1..].to_vec();
460        self.executor
461            .execute_command(&command_name, command_args)
462            .await
463    }
464
465    fn get_executor(&self) -> &CommandExecutor {
466        &self.executor
467    }
468
469    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
470        &mut self.executor
471    }
472}
473
474/// Output from volume inspect
475#[derive(Debug, Clone)]
476pub struct VolumeInspectOutput {
477    /// Parsed JSON output
478    pub json: Option<Value>,
479    /// Raw command output
480    pub raw_output: CommandOutput,
481}
482
483impl From<CommandOutput> for VolumeInspectOutput {
484    fn from(output: CommandOutput) -> Self {
485        let json = serde_json::from_str(&output.stdout).ok();
486        Self {
487            json,
488            raw_output: output,
489        }
490    }
491}
492
493/// Docker volume prune command
494#[derive(Debug, Clone)]
495pub struct VolumePruneCommand {
496    all: bool,
497    filters: HashMap<String, String>,
498    force: bool,
499    /// Command executor
500    pub executor: CommandExecutor,
501}
502
503impl VolumePruneCommand {
504    /// Create a new volume prune command
505    #[must_use]
506    pub fn new() -> Self {
507        Self {
508            all: false,
509            filters: HashMap::new(),
510            force: false,
511            executor: CommandExecutor::new(),
512        }
513    }
514
515    /// Remove all unused volumes
516    #[must_use]
517    pub fn all(mut self) -> Self {
518        self.all = true;
519        self
520    }
521
522    /// Add a filter
523    #[must_use]
524    pub fn filter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
525        self.filters.insert(key.into(), value.into());
526        self
527    }
528
529    /// Do not prompt for confirmation
530    #[must_use]
531    pub fn force(mut self) -> Self {
532        self.force = true;
533        self
534    }
535
536    /// Execute the command
537    ///
538    /// # Errors
539    ///
540    /// Returns an error if the Docker daemon is not running or the command fails
541    pub async fn run(&self) -> Result<VolumePruneResult> {
542        self.execute().await.map(VolumePruneResult::from)
543    }
544}
545
546impl Default for VolumePruneCommand {
547    fn default() -> Self {
548        Self::new()
549    }
550}
551
552#[async_trait]
553impl DockerCommand for VolumePruneCommand {
554    type Output = CommandOutput;
555
556    fn build_command_args(&self) -> Vec<String> {
557        let mut args = vec!["volume".to_string(), "prune".to_string()];
558
559        if self.all {
560            args.push("--all".to_string());
561        }
562
563        for (key, value) in &self.filters {
564            args.push("--filter".to_string());
565            args.push(format!("{key}={value}"));
566        }
567
568        if self.force {
569            args.push("--force".to_string());
570        }
571
572        args.extend(self.executor.raw_args.clone());
573        args
574    }
575
576    async fn execute(&self) -> Result<Self::Output> {
577        let args = self.build_command_args();
578        let command_name = args[0].clone();
579        let command_args = args[1..].to_vec();
580        self.executor
581            .execute_command(&command_name, command_args)
582            .await
583    }
584
585    fn get_executor(&self) -> &CommandExecutor {
586        &self.executor
587    }
588
589    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
590        &mut self.executor
591    }
592}
593
594/// Result from volume prune
595#[derive(Debug, Clone)]
596pub struct VolumePruneResult {
597    /// Names of deleted volumes
598    pub deleted_volumes: Vec<String>,
599    /// Amount of disk space reclaimed in bytes
600    pub space_reclaimed: Option<u64>,
601    /// Raw command output
602    pub raw_output: CommandOutput,
603}
604
605impl From<CommandOutput> for VolumePruneResult {
606    fn from(output: CommandOutput) -> Self {
607        let deleted_volumes = Vec::new(); // Parse from output if needed
608        Self {
609            deleted_volumes,
610            space_reclaimed: None,
611            raw_output: output,
612        }
613    }
614}
615
616#[cfg(test)]
617mod tests {
618    use super::*;
619
620    #[test]
621    fn test_volume_create() {
622        let cmd = VolumeCreateCommand::new().name("my-volume");
623        let args = cmd.build_command_args();
624        assert_eq!(args, vec!["volume", "create", "my-volume"]);
625    }
626
627    #[test]
628    fn test_volume_ls() {
629        let cmd = VolumeLsCommand::new();
630        let args = cmd.build_command_args();
631        assert_eq!(args, vec!["volume", "ls"]);
632    }
633
634    #[test]
635    fn test_volume_rm() {
636        let cmd = VolumeRmCommand::new("my-volume");
637        let args = cmd.build_command_args();
638        assert_eq!(args, vec!["volume", "rm", "my-volume"]);
639    }
640
641    #[test]
642    fn test_volume_inspect() {
643        let cmd = VolumeInspectCommand::new("my-volume");
644        let args = cmd.build_command_args();
645        assert_eq!(args, vec!["volume", "inspect", "my-volume"]);
646    }
647
648    #[test]
649    fn test_volume_prune() {
650        let cmd = VolumePruneCommand::new().force();
651        let args = cmd.build_command_args();
652        assert_eq!(args, vec!["volume", "prune", "--force"]);
653    }
654}