Skip to main content

docker_wrapper/command/compose/
pull.rs

1//! Docker Compose pull command implementation using unified trait pattern.
2
3use crate::command::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6
7/// Pull policy for compose pull command
8#[derive(Debug, Clone, Copy)]
9pub enum ComposePullPolicy {
10    /// Always pull images
11    Always,
12    /// Pull missing images only
13    Missing,
14}
15
16impl std::fmt::Display for ComposePullPolicy {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        match self {
19            Self::Always => write!(f, "always"),
20            Self::Missing => write!(f, "missing"),
21        }
22    }
23}
24
25/// Docker Compose pull command builder
26#[allow(clippy::struct_excessive_bools)] // Multiple boolean flags are appropriate for pull command
27#[derive(Debug, Clone)]
28pub struct ComposePullCommand {
29    /// Base command executor
30    pub executor: CommandExecutor,
31    /// Base compose configuration
32    pub config: ComposeConfig,
33    /// Services to pull images for (empty for all)
34    pub services: Vec<String>,
35    /// Ignore images that can be built
36    pub ignore_buildable: bool,
37    /// Pull what it can and ignore images with pull failures
38    pub ignore_pull_failures: bool,
39    /// Also pull services declared as dependencies
40    pub include_deps: bool,
41    /// Pull policy
42    pub policy: Option<ComposePullPolicy>,
43    /// Pull without printing progress information
44    pub quiet: bool,
45}
46
47/// Result from compose pull command
48#[derive(Debug, Clone)]
49pub struct ComposePullResult {
50    /// Raw stdout output
51    pub stdout: String,
52    /// Raw stderr output
53    pub stderr: String,
54    /// Success status
55    pub success: bool,
56    /// Services that were pulled
57    pub services: Vec<String>,
58}
59
60impl ComposePullCommand {
61    /// Create a new compose pull command
62    #[must_use]
63    pub fn new() -> Self {
64        Self {
65            executor: CommandExecutor::new(),
66            config: ComposeConfig::new(),
67            services: Vec::new(),
68            ignore_buildable: false,
69            ignore_pull_failures: false,
70            include_deps: false,
71            policy: None,
72            quiet: false,
73        }
74    }
75
76    /// Add a service to pull
77    #[must_use]
78    pub fn service(mut self, service: impl Into<String>) -> Self {
79        self.services.push(service.into());
80        self
81    }
82
83    /// Add multiple services to pull
84    #[must_use]
85    pub fn services<I, S>(mut self, services: I) -> Self
86    where
87        I: IntoIterator<Item = S>,
88        S: Into<String>,
89    {
90        self.services.extend(services.into_iter().map(Into::into));
91        self
92    }
93
94    /// Ignore images that can be built
95    #[must_use]
96    pub fn ignore_buildable(mut self) -> Self {
97        self.ignore_buildable = true;
98        self
99    }
100
101    /// Pull what it can and ignore images with pull failures
102    #[must_use]
103    pub fn ignore_pull_failures(mut self) -> Self {
104        self.ignore_pull_failures = true;
105        self
106    }
107
108    /// Also pull services declared as dependencies
109    #[must_use]
110    pub fn include_deps(mut self) -> Self {
111        self.include_deps = true;
112        self
113    }
114
115    /// Set pull policy
116    #[must_use]
117    pub fn policy(mut self, policy: ComposePullPolicy) -> Self {
118        self.policy = Some(policy);
119        self
120    }
121
122    /// Pull without printing progress information
123    #[must_use]
124    pub fn quiet(mut self) -> Self {
125        self.quiet = true;
126        self
127    }
128}
129
130impl Default for ComposePullCommand {
131    fn default() -> Self {
132        Self::new()
133    }
134}
135
136#[async_trait]
137impl DockerCommand for ComposePullCommand {
138    type Output = ComposePullResult;
139
140    fn get_executor(&self) -> &CommandExecutor {
141        &self.executor
142    }
143
144    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
145        &mut self.executor
146    }
147
148    fn build_command_args(&self) -> Vec<String> {
149        <Self as ComposeCommand>::build_command_args(self)
150    }
151
152    async fn execute(&self) -> Result<Self::Output> {
153        let args = <Self as ComposeCommand>::build_command_args(self);
154        let output = self.execute_command(args).await?;
155
156        Ok(ComposePullResult {
157            stdout: output.stdout,
158            stderr: output.stderr,
159            success: output.success,
160            services: self.services.clone(),
161        })
162    }
163}
164
165impl ComposeCommand for ComposePullCommand {
166    fn get_config(&self) -> &ComposeConfig {
167        &self.config
168    }
169
170    fn get_config_mut(&mut self) -> &mut ComposeConfig {
171        &mut self.config
172    }
173
174    fn subcommand(&self) -> &'static str {
175        "pull"
176    }
177
178    fn build_subcommand_args(&self) -> Vec<String> {
179        let mut args = Vec::new();
180
181        if self.ignore_buildable {
182            args.push("--ignore-buildable".to_string());
183        }
184
185        if self.ignore_pull_failures {
186            args.push("--ignore-pull-failures".to_string());
187        }
188
189        if self.include_deps {
190            args.push("--include-deps".to_string());
191        }
192
193        if let Some(ref policy) = self.policy {
194            args.push("--policy".to_string());
195            args.push(policy.to_string());
196        }
197
198        if self.quiet {
199            args.push("--quiet".to_string());
200        }
201
202        args.extend(self.services.clone());
203        args
204    }
205}
206
207impl ComposePullResult {
208    /// Check if the command was successful
209    #[must_use]
210    pub fn success(&self) -> bool {
211        self.success
212    }
213
214    /// Get the services that were pulled
215    #[must_use]
216    pub fn services(&self) -> &[String] {
217        &self.services
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn test_compose_pull_basic() {
227        let cmd = ComposePullCommand::new();
228        let args = cmd.build_subcommand_args();
229        assert!(args.is_empty());
230
231        let full_args = ComposeCommand::build_command_args(&cmd);
232        assert_eq!(full_args[0], "compose");
233        assert!(full_args.contains(&"pull".to_string()));
234    }
235
236    #[test]
237    fn test_compose_pull_with_options() {
238        let cmd = ComposePullCommand::new()
239            .ignore_buildable()
240            .ignore_pull_failures()
241            .include_deps()
242            .quiet()
243            .service("web");
244
245        let args = cmd.build_subcommand_args();
246        assert!(args.contains(&"--ignore-buildable".to_string()));
247        assert!(args.contains(&"--ignore-pull-failures".to_string()));
248        assert!(args.contains(&"--include-deps".to_string()));
249        assert!(args.contains(&"--quiet".to_string()));
250        assert!(args.contains(&"web".to_string()));
251    }
252
253    #[test]
254    fn test_compose_pull_with_policy() {
255        let cmd = ComposePullCommand::new()
256            .policy(ComposePullPolicy::Always)
257            .service("db");
258
259        let args = cmd.build_subcommand_args();
260        assert!(args.contains(&"--policy".to_string()));
261        assert!(args.contains(&"always".to_string()));
262        assert!(args.contains(&"db".to_string()));
263    }
264
265    #[test]
266    fn test_compose_pull_with_missing_policy() {
267        let cmd = ComposePullCommand::new().policy(ComposePullPolicy::Missing);
268
269        let args = cmd.build_subcommand_args();
270        assert!(args.contains(&"--policy".to_string()));
271        assert!(args.contains(&"missing".to_string()));
272    }
273
274    #[test]
275    fn test_compose_pull_multiple_services() {
276        let cmd = ComposePullCommand::new()
277            .service("web")
278            .service("db")
279            .service("redis");
280
281        let args = cmd.build_subcommand_args();
282        assert!(args.contains(&"web".to_string()));
283        assert!(args.contains(&"db".to_string()));
284        assert!(args.contains(&"redis".to_string()));
285    }
286
287    #[test]
288    fn test_compose_pull_services_batch() {
289        let cmd = ComposePullCommand::new().services(vec!["web", "db"]);
290
291        let args = cmd.build_subcommand_args();
292        assert!(args.contains(&"web".to_string()));
293        assert!(args.contains(&"db".to_string()));
294    }
295
296    #[test]
297    fn test_compose_pull_config_integration() {
298        let cmd = ComposePullCommand::new()
299            .file("docker-compose.yml")
300            .project_name("myapp")
301            .service("api");
302
303        let args = ComposeCommand::build_command_args(&cmd);
304        assert!(args.contains(&"--file".to_string()));
305        assert!(args.contains(&"docker-compose.yml".to_string()));
306        assert!(args.contains(&"--project-name".to_string()));
307        assert!(args.contains(&"myapp".to_string()));
308        assert!(args.contains(&"pull".to_string()));
309        assert!(args.contains(&"api".to_string()));
310    }
311}