Skip to content

Commit 1a182c8

Browse files
committed
feat: add compose pull command support
Add `ComposePullCommand` with options for ignore-buildable, ignore-pull-failures, include-deps, pull policy and quiet mode
1 parent 2f08e43 commit 1a182c8

3 files changed

Lines changed: 322 additions & 9 deletions

File tree

src/command/compose.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub mod ls;
1919
pub mod pause;
2020
pub mod port;
2121
pub mod ps;
22+
pub mod pull;
2223
pub mod push;
2324
pub mod restart;
2425
pub mod rm;
@@ -51,6 +52,7 @@ pub use port::{ComposePortCommand, ComposePortResult};
5152
pub use ps::{
5253
ComposeContainerInfo, ComposePsCommand, ComposePsResult, ContainerStatus, PortPublisher,
5354
};
55+
pub use pull::{ComposePullCommand, ComposePullResult};
5456
pub use push::{ComposePushCommand, ComposePushResult};
5557
pub use restart::{ComposeRestartCommand, ComposeRestartResult};
5658
pub use rm::{ComposeRmCommand, ComposeRmResult};

src/command/compose/pull.rs

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
//! Docker Compose pull command implementation using unified trait pattern.
2+
3+
use crate::command::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4+
use crate::error::Result;
5+
use async_trait::async_trait;
6+
7+
/// Pull policy for compose pull command
8+
#[derive(Debug, Clone, Copy)]
9+
pub enum PullPolicy {
10+
/// Always pull images
11+
Always,
12+
/// Pull missing images only
13+
Missing,
14+
}
15+
16+
impl std::fmt::Display for PullPolicy {
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)]
28+
pub 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<PullPolicy>,
43+
/// Pull without printing progress information
44+
pub quiet: bool,
45+
}
46+
47+
/// Result from compose pull command
48+
#[derive(Debug, Clone)]
49+
pub 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+
60+
impl 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: PullPolicy) -> 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+
130+
impl Default for ComposePullCommand {
131+
fn default() -> Self {
132+
Self::new()
133+
}
134+
}
135+
136+
#[async_trait]
137+
impl 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+
165+
impl 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+
207+
impl 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)]
222+
mod 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(PullPolicy::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(PullPolicy::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+
}

src/compose.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ pub use crate::command::compose::{
4343
ComposeImagesResult, ComposeKillCommand, ComposeKillResult, ComposeLogsCommand,
4444
ComposeLogsResult, ComposeLsCommand, ComposePauseCommand, ComposePauseResult,
4545
ComposePortCommand, ComposePortResult, ComposeProject, ComposePsCommand, ComposePsResult,
46-
ComposePushCommand, ComposePushResult, ComposeRestartCommand, ComposeRestartResult,
47-
ComposeRmCommand, ComposeRmResult, ComposeRunCommand, ComposeRunResult, ComposeScaleCommand,
48-
ComposeScaleResult, ComposeStartCommand, ComposeStartResult, ComposeStopCommand,
49-
ComposeStopResult, ComposeTopCommand, ComposeTopResult, ComposeUnpauseCommand,
50-
ComposeUnpauseResult, ComposeUpCommand, ComposeUpResult, ComposeVersionCommand,
51-
ComposeVersionResult, ComposeWaitCommand, ComposeWaitResult, ComposeWatchCommand,
52-
ComposeWatchResult, ConfigFormat, ContainerStatus, ConvertFormat, ImageInfo, ImagesFormat,
53-
LsFormat, LsResult, PortPublisher, ProgressOutput, PullPolicy, RemoveImages, VersionFormat,
54-
VersionInfo,
46+
ComposePullCommand, ComposePullResult, ComposePushCommand, ComposePushResult,
47+
ComposeRestartCommand, ComposeRestartResult, ComposeRmCommand, ComposeRmResult,
48+
ComposeRunCommand, ComposeRunResult, ComposeScaleCommand, ComposeScaleResult,
49+
ComposeStartCommand, ComposeStartResult, ComposeStopCommand, ComposeStopResult,
50+
ComposeTopCommand, ComposeTopResult, ComposeUnpauseCommand, ComposeUnpauseResult,
51+
ComposeUpCommand, ComposeUpResult, ComposeVersionCommand, ComposeVersionResult,
52+
ComposeWaitCommand, ComposeWaitResult, ComposeWatchCommand, ComposeWatchResult, ConfigFormat,
53+
ContainerStatus, ConvertFormat, ImageInfo, ImagesFormat, LsFormat, LsResult, PortPublisher,
54+
ProgressOutput, PullPolicy, RemoveImages, VersionFormat, VersionInfo,
5555
};

0 commit comments

Comments
 (0)