use crate::command::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
use crate::error::Result;
use async_trait::async_trait;
use std::collections::HashMap;
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)] pub struct ComposeRunCommand {
pub executor: CommandExecutor,
pub config: ComposeConfig,
pub service: String,
pub command: Vec<String>,
pub detach: bool,
pub rm: bool,
pub no_deps: bool,
pub no_tty: bool,
pub interactive: bool,
pub entrypoint: Option<String>,
pub env: HashMap<String, String>,
pub labels: HashMap<String, String>,
pub name: Option<String>,
pub publish: Vec<String>,
pub user: Option<String>,
pub workdir: Option<String>,
pub volumes: Vec<String>,
pub volume_rm: bool,
}
#[derive(Debug, Clone)]
pub struct ComposeRunResult {
pub stdout: String,
pub stderr: String,
pub success: bool,
pub exit_code: i32,
pub service: String,
pub detached: bool,
}
impl ComposeRunCommand {
#[must_use]
pub fn new(service: impl Into<String>) -> Self {
Self {
executor: CommandExecutor::new(),
config: ComposeConfig::new(),
service: service.into(),
command: Vec::new(),
detach: false,
rm: false,
no_deps: false,
no_tty: false,
interactive: false,
entrypoint: None,
env: HashMap::new(),
labels: HashMap::new(),
name: None,
publish: Vec::new(),
user: None,
workdir: None,
volumes: Vec::new(),
volume_rm: false,
}
}
#[must_use]
pub fn cmd<I, S>(mut self, command: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.command = command.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn arg(mut self, arg: impl Into<String>) -> Self {
self.command.push(arg.into());
self
}
#[must_use]
pub fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.command.extend(args.into_iter().map(Into::into));
self
}
#[must_use]
pub fn detach(mut self) -> Self {
self.detach = true;
self
}
#[must_use]
pub fn rm(mut self) -> Self {
self.rm = true;
self
}
#[must_use]
pub fn no_deps(mut self) -> Self {
self.no_deps = true;
self
}
#[must_use]
pub fn no_tty(mut self) -> Self {
self.no_tty = true;
self
}
#[must_use]
pub fn interactive(mut self) -> Self {
self.interactive = true;
self
}
#[must_use]
pub fn entrypoint(mut self, entrypoint: impl Into<String>) -> Self {
self.entrypoint = Some(entrypoint.into());
self
}
#[must_use]
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.insert(key.into(), value.into());
self
}
#[must_use]
pub fn envs(mut self, env_vars: HashMap<String, String>) -> Self {
self.env.extend(env_vars);
self
}
#[must_use]
pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.labels.insert(key.into(), value.into());
self
}
#[must_use]
pub fn labels(mut self, labels: HashMap<String, String>) -> Self {
self.labels.extend(labels);
self
}
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn publish(mut self, publish: impl Into<String>) -> Self {
self.publish.push(publish.into());
self
}
#[must_use]
pub fn user(mut self, user: impl Into<String>) -> Self {
self.user = Some(user.into());
self
}
#[must_use]
pub fn workdir(mut self, workdir: impl Into<String>) -> Self {
self.workdir = Some(workdir.into());
self
}
#[must_use]
pub fn volume(mut self, volume: impl Into<String>) -> Self {
self.volumes.push(volume.into());
self
}
#[must_use]
pub fn volume_rm(mut self) -> Self {
self.volume_rm = true;
self
}
}
#[async_trait]
impl DockerCommand for ComposeRunCommand {
type Output = ComposeRunResult;
fn get_executor(&self) -> &CommandExecutor {
&self.executor
}
fn get_executor_mut(&mut self) -> &mut CommandExecutor {
&mut self.executor
}
fn build_command_args(&self) -> Vec<String> {
<Self as ComposeCommand>::build_command_args(self)
}
async fn execute(&self) -> Result<Self::Output> {
let args = <Self as ComposeCommand>::build_command_args(self);
let output = self.execute_command(args).await?;
Ok(ComposeRunResult {
stdout: output.stdout,
stderr: output.stderr,
success: output.success,
exit_code: output.exit_code,
service: self.service.clone(),
detached: self.detach,
})
}
}
impl ComposeCommand for ComposeRunCommand {
fn get_config(&self) -> &ComposeConfig {
&self.config
}
fn get_config_mut(&mut self) -> &mut ComposeConfig {
&mut self.config
}
fn subcommand(&self) -> &'static str {
"run"
}
fn build_subcommand_args(&self) -> Vec<String> {
let mut args = Vec::new();
if self.detach {
args.push("--detach".to_string());
}
if self.rm {
args.push("--rm".to_string());
}
if self.no_deps {
args.push("--no-deps".to_string());
}
if self.no_tty {
args.push("--no-TTY".to_string());
}
if self.interactive {
args.push("--interactive".to_string());
}
if let Some(ref entrypoint) = self.entrypoint {
args.push("--entrypoint".to_string());
args.push(entrypoint.clone());
}
for (key, value) in &self.env {
args.push("--env".to_string());
args.push(format!("{key}={value}"));
}
for (key, value) in &self.labels {
args.push("--label".to_string());
args.push(format!("{key}={value}"));
}
if let Some(ref name) = self.name {
args.push("--name".to_string());
args.push(name.clone());
}
for publish in &self.publish {
args.push("--publish".to_string());
args.push(publish.clone());
}
if let Some(ref user) = self.user {
args.push("--user".to_string());
args.push(user.clone());
}
if let Some(ref workdir) = self.workdir {
args.push("--workdir".to_string());
args.push(workdir.clone());
}
for volume in &self.volumes {
args.push("--volume".to_string());
args.push(volume.clone());
}
if self.volume_rm {
args.push("--volume".to_string());
args.push("rm".to_string());
}
args.push(self.service.clone());
args.extend(self.command.clone());
args
}
}
impl ComposeRunResult {
#[must_use]
pub fn success(&self) -> bool {
self.success
}
#[must_use]
pub fn exit_code(&self) -> i32 {
self.exit_code
}
#[must_use]
pub fn service(&self) -> &str {
&self.service
}
#[must_use]
pub fn is_detached(&self) -> bool {
self.detached
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compose_run_basic() {
let cmd = ComposeRunCommand::new("web");
let args = cmd.build_subcommand_args();
assert!(args.contains(&"web".to_string()));
let full_args = ComposeCommand::build_command_args(&cmd);
assert_eq!(full_args[0], "compose");
assert!(full_args.contains(&"run".to_string()));
assert!(full_args.contains(&"web".to_string()));
}
#[test]
fn test_compose_run_with_command() {
let cmd = ComposeRunCommand::new("worker").cmd(vec!["python", "script.py"]);
let args = cmd.build_subcommand_args();
assert!(args.contains(&"worker".to_string()));
assert!(args.contains(&"python".to_string()));
assert!(args.contains(&"script.py".to_string()));
}
#[test]
fn test_compose_run_with_flags() {
let cmd = ComposeRunCommand::new("app")
.detach()
.rm()
.no_deps()
.interactive();
let args = cmd.build_subcommand_args();
assert!(args.contains(&"--detach".to_string()));
assert!(args.contains(&"--rm".to_string()));
assert!(args.contains(&"--no-deps".to_string()));
assert!(args.contains(&"--interactive".to_string()));
}
#[test]
fn test_compose_run_with_env_and_labels() {
let cmd = ComposeRunCommand::new("test")
.env("NODE_ENV", "development")
.env("DEBUG", "true")
.label("version", "1.0")
.label("component", "api");
let args = cmd.build_subcommand_args();
assert!(args.contains(&"--env".to_string()));
assert!(args.contains(&"NODE_ENV=development".to_string()));
assert!(args.contains(&"DEBUG=true".to_string()));
assert!(args.contains(&"--label".to_string()));
assert!(args.contains(&"version=1.0".to_string()));
assert!(args.contains(&"component=api".to_string()));
}
#[test]
fn test_compose_run_all_options() {
let cmd = ComposeRunCommand::new("database")
.detach()
.rm()
.name("test-db")
.user("postgres")
.workdir("/app")
.volume("/data:/var/lib/postgresql/data")
.publish("5432:5432")
.entrypoint("docker-entrypoint.sh")
.cmd(vec!["postgres"])
.env("POSTGRES_DB", "testdb")
.label("env", "test");
let args = cmd.build_subcommand_args();
assert!(args.contains(&"--detach".to_string()));
assert!(args.contains(&"--rm".to_string()));
assert!(args.contains(&"--name".to_string()));
assert!(args.contains(&"test-db".to_string()));
assert!(args.contains(&"--user".to_string()));
assert!(args.contains(&"postgres".to_string()));
assert!(args.contains(&"--workdir".to_string()));
assert!(args.contains(&"/app".to_string()));
assert!(args.contains(&"--volume".to_string()));
assert!(args.contains(&"/data:/var/lib/postgresql/data".to_string()));
assert!(args.contains(&"--publish".to_string()));
assert!(args.contains(&"5432:5432".to_string()));
assert!(args.contains(&"--entrypoint".to_string()));
assert!(args.contains(&"docker-entrypoint.sh".to_string()));
assert!(args.contains(&"database".to_string()));
assert!(args.contains(&"postgres".to_string()));
assert!(args.contains(&"POSTGRES_DB=testdb".to_string()));
assert!(args.contains(&"env=test".to_string()));
}
#[test]
fn test_compose_config_integration() {
let cmd = ComposeRunCommand::new("worker")
.file("docker-compose.yml")
.project_name("my-project")
.rm()
.cmd(vec!["python", "worker.py"]);
let args = ComposeCommand::build_command_args(&cmd);
assert!(args.contains(&"--file".to_string()));
assert!(args.contains(&"docker-compose.yml".to_string()));
assert!(args.contains(&"--project-name".to_string()));
assert!(args.contains(&"my-project".to_string()));
assert!(args.contains(&"--rm".to_string()));
assert!(args.contains(&"worker".to_string()));
assert!(args.contains(&"python".to_string()));
assert!(args.contains(&"worker.py".to_string()));
}
}