use std::{iter::repeat_with, path::PathBuf};
use anyhow::{anyhow, Result};
use atty::Stream;
use clap::crate_version;
use fastrand::Rng;
use crate::{
commands::{create, start},
compose::types::{
parse_port, parse_service_volume, Compose, FileReference, Port, Service, ServiceVolume,
ServiceVolumeType,
},
config::Config,
podman::Podman,
utils::{parse_key_val, parse_key_val_opt},
};
#[derive(clap::Args, Debug)]
#[command(next_display_order = None)]
pub(crate) struct Args {
service: String,
command: String,
args: Vec<String>,
#[arg(short, long)]
detach: bool,
#[arg(short, long, value_parser = parse_key_val_opt::<String, String>)]
env: Vec<(String, Option<String>)>,
#[arg(short, long, value_parser = parse_key_val::<String, String>)]
label: Vec<(String, String)>,
#[arg(long)]
rm: bool,
#[arg(short = 'T', long = "no-TTY", default_value_t = !atty::is(Stream::Stdout))]
no_tty: bool,
#[arg(long)]
name: Option<String>,
#[arg(short, long)]
user: Option<String>,
#[arg(short, long)]
workdir: Option<PathBuf>,
#[arg(long)]
entrypoint: Option<String>,
#[arg(long)]
no_deps: bool,
#[arg(short, long, value_parser = parse_service_volume)]
volume: Vec<ServiceVolume>,
#[arg(short, long, value_parser = parse_port, conflicts_with = "service_ports")]
publish: Vec<Port>,
#[arg(long)]
use_aliases: bool,
#[arg(long, conflicts_with = "publish")]
service_ports: bool,
#[arg(long)]
remove_orphans: bool,
}
async fn run_container(
podman: &Podman,
file: &Compose,
service: &Service,
args: Args,
) -> Result<()> {
let project_name = file.name.as_ref().unwrap();
let mut rng = Rng::new();
let id = hex::encode(repeat_with(|| rng.u8(..)).take(6).collect::<Vec<_>>());
let container_name = format!("{project_name}_{}_run_{id}", args.service);
let requirements = if args.no_deps {
Vec::new()
} else {
service
.depends_on
.keys()
.filter_map(|service_name| {
file.services.get(service_name).map(|service| {
(1..=service
.deploy
.as_ref()
.and_then(|deploy| deploy.replicas)
.or(service.scale)
.unwrap_or(1))
.map(move |i| {
service
.container_name
.clone()
.unwrap_or_else(|| format!("{project_name}_{service_name}_{i}"))
})
})
})
.flatten()
.collect::<Vec<_>>()
};
let labels = [
("version", crate_version!()),
("project", project_name),
("service", &args.service),
("oneoff", "true"),
]
.into_iter()
.map(|label| format!("io.podman.compose.{}={}", label.0, label.1))
.collect::<Vec<_>>();
let pull_policy = service.pull_policy.as_ref().map(ToString::to_string);
let networks = service
.networks
.iter()
.map(|(name, network)| {
let name = file.networks[name].name.clone().unwrap();
let mut network = network
.as_ref()
.map(ToString::to_string)
.unwrap_or_default();
if let Some(mac_address) = service.mac_address.as_ref() {
if network.is_empty() {
network = format!(":mac={mac_address}");
} else {
network = format!("{network},mac={mac_address}");
}
}
format!("{name}{network}")
})
.collect::<Vec<_>>();
let volumes = service
.volumes
.iter()
.flat_map(|volume| {
let volume = match &volume.r#type {
ServiceVolumeType::Volume(Some(source)) => ServiceVolume {
r#type: ServiceVolumeType::Volume(file.volumes[source].name.clone()),
..volume.clone()
},
_ => volume.clone(),
};
[
String::from(match volume.r#type {
ServiceVolumeType::Volume(_) | ServiceVolumeType::Bind(_) => "--volume",
ServiceVolumeType::Tmpfs => "--tmpfs",
}),
volume.to_string(),
]
})
.collect::<Vec<_>>();
let secrets = service
.secrets
.iter()
.map(|secret| {
FileReference {
source: file.secrets[&secret.source].name.clone().unwrap(),
..secret.clone()
}
.to_string()
})
.collect::<Vec<_>>();
let (global_args, service_args) = service.to_args();
podman
.attach(
global_args
.iter()
.map(AsRef::as_ref)
.chain([
"run",
"--interactive",
"--pod",
project_name,
"--name",
&container_name,
])
.chain(
requirements
.iter()
.flat_map(|requirement| ["--requires", requirement]),
)
.chain(labels.iter().flat_map(|label| ["--label", label]))
.chain(if let Some(pull_policy) = &pull_policy {
vec!["--pull", pull_policy]
} else {
vec![]
})
.chain(networks.iter().flat_map(|network| ["--network", network]))
.chain(if service.networks.contains_key("default") {
vec!["--network-alias", &args.service]
} else {
vec![]
})
.chain(volumes.iter().map(AsRef::as_ref))
.chain(secrets.iter().flat_map(|secret| ["--secret", secret]))
.chain(if args.detach {
vec!["--detach"]
} else {
vec![]
})
.chain(if args.rm { vec!["--rm"] } else { vec![] })
.chain(if args.no_tty { vec![] } else { vec!["--tty"] })
.chain(service_args.iter().map(AsRef::as_ref)),
)
.await
}
pub(crate) async fn run(
args: Args,
podman: &Podman,
file: &Compose,
config: &Config,
) -> Result<()> {
let service = file
.services
.get(&args.service)
.ok_or_else(|| anyhow!("No such service: \"{}\"", args.service))?;
let services = service.depends_on.keys().cloned().collect::<Vec<_>>();
if !args.no_deps {
create::run(
create::Args {
services: services.clone(),
pull: None,
force_recreate: false,
no_recreate: false,
remove_orphans: args.remove_orphans,
},
podman,
file,
config,
)
.await?;
start::run(start::Args { services }, podman, file, config).await?;
}
let mut service = service.clone();
service.command = vec![args.command.clone()];
service.command.extend(args.args.clone());
service.environment.extend(args.env.clone());
service.labels.extend(args.label.clone());
service.container_name = args.name.clone().or(service.container_name);
service.user = args.user.clone().or(service.user);
service.working_dir = args.workdir.clone().or(service.working_dir);
service.entrypoint = args
.entrypoint
.as_ref()
.map(|entrypoint| shell_words::split(entrypoint))
.transpose()?
.unwrap_or(service.entrypoint);
service.volumes.extend(args.volume.clone());
service.ports = if args.service_ports {
service.ports
} else {
args.publish.clone()
};
if !args.use_aliases {
for network in service.networks.values_mut().flatten() {
network.aliases.clear();
}
}
run_container(podman, file, &service, args).await?;
Ok(())
}