use std::collections::HashMap;
use std::io::Write;
use futures_util::StreamExt;
use crate::compose::types::ComposeFile;
use crate::error::{ComposeError, Result};
use super::RunOptions;
use crate::engine::Engine;
use crate::libpod::API_PREFIX;
impl Engine {
pub async fn run(
&self,
file: &ComposeFile,
service_name: &str,
opts: RunOptions,
) -> Result<()> {
let RunOptions {
cmd,
rm,
detach,
env_overrides,
name_override,
service_ports,
} = opts;
let super::RunOverrides {
user,
workdir,
entrypoint,
volumes,
publish,
interactive,
no_deps,
} = self.run_overrides.clone();
let env_files = self.run_env_files.clone();
let labels = self.run_labels.clone();
let service = file
.services
.get(service_name)
.ok_or_else(|| ComposeError::ServiceNotFound(service_name.into()))?;
if !no_deps {
let deps: Vec<String> = super::expand_targets(file, &[service_name.to_string()], false)
.map(|set| set.into_iter().filter(|n| n != service_name).collect())
.unwrap_or_default();
if !deps.is_empty() {
self.up_with_options(file, true, &[], &deps, false, false, false)
.await?;
}
}
let user_named = name_override.is_some();
let run_name = name_override.unwrap_or_else(|| {
format!("{}-{service_name}-run-{}", self.project, std::process::id())
});
let mut run_service = service.clone();
if !cmd.is_empty() {
run_service.command = Some(crate::compose::types::Command::Exec(cmd));
}
if let Some(ep) = entrypoint {
run_service.entrypoint = Some(crate::compose::types::Command::Exec(vec![ep]));
}
if let Some(u) = user {
run_service.user = Some(u);
}
if let Some(w) = workdir {
run_service.working_dir = Some(w);
}
if interactive {
run_service.stdin_open = Some(true);
}
for v in volumes {
run_service
.volumes
.push(crate::compose::types::VolumeMount::Short(v));
}
if !labels.is_empty() {
let mut list: Vec<String> = run_service
.labels
.to_map()
.into_iter()
.map(|(k, v)| if v.is_empty() { k } else { format!("{k}={v}") })
.collect();
list.extend(labels);
run_service.labels = crate::compose::types::Labels::List(list);
}
let env_file_vars = if env_files.is_empty() {
HashMap::new()
} else {
crate::env_file::load_env_files(&env_files, &self.base_dir)?
};
if !env_file_vars.is_empty() || !env_overrides.is_empty() {
run_service.environment = crate::compose::types::EnvVars::List(merge_run_environment(
env_file_vars,
run_service.environment.to_map(),
env_overrides,
));
}
run_service.restart = None;
if !service_ports {
run_service.ports.clear();
}
for p in publish {
run_service
.ports
.push(crate::compose::types::PortMapping::Short(p));
}
run_service.tty = None;
self.create_networks(file).await?;
self.create_inline_secrets(file).await?;
if user_named && self.container_exists(&run_name).await? {
return Err(ComposeError::Unsupported(format!(
"the container name \"{run_name}\" is already in use; remove the existing \
container or choose a different --name"
)));
}
let rm_path = format!(
"{API_PREFIX}/containers/{}?force=true",
crate::libpod::urlencoded(&run_name),
);
if let Err(e) = self
.create_and_start(&run_name, service_name, &run_service, file, true)
.await
{
if rm {
let _ = self.client.delete_ok(&rm_path).await;
}
return Err(e);
}
if detach {
crate::ui::result_line(&run_name);
return Ok(());
}
let outcome: Result<i64> = async {
let logs_path = format!(
"{API_PREFIX}/containers/{}/logs?follow=true&stdout=true&stderr=true",
crate::libpod::urlencoded(&run_name),
);
let logs_resp = self
.client
.get_stream(&logs_path)
.await
.map_err(ComposeError::Podman)?;
let mut log_stream = crate::libpod::parse_multiplexed(logs_resp.into_body());
{
let mut out = std::io::stdout().lock();
while let Some(msg) = log_stream.next().await {
match msg.map_err(ComposeError::Podman)? {
crate::libpod::LogOutput::StdOut { message } => {
let _ = out.write_all(String::from_utf8_lossy(&message).as_bytes());
let _ = out.flush();
}
crate::libpod::LogOutput::StdErr { message } => {
let mut err = std::io::stderr().lock();
let _ = err.write_all(String::from_utf8_lossy(&message).as_bytes());
let _ = err.flush();
}
}
}
}
let wait_path = format!(
"{API_PREFIX}/containers/{}/wait?condition=stopped",
crate::libpod::urlencoded(&run_name),
);
self.client
.post_empty_json_unbounded::<i64>(&wait_path)
.await
.map_err(ComposeError::Podman)
}
.await;
if rm {
if let Err(e) = self.client.delete_ok(&rm_path).await {
tracing::debug!("run cleanup delete {run_name}: {e}");
}
}
let exit_code = outcome?;
if exit_code != 0 {
return Err(crate::error::ComposeError::RunExited(exit_code));
}
Ok(())
}
}
fn merge_run_environment(
env_file_vars: HashMap<String, String>,
service_env: HashMap<String, Option<String>>,
env_overrides: Vec<String>,
) -> Vec<String> {
let mut map: HashMap<String, Option<String>> = env_file_vars
.into_iter()
.map(|(k, v)| (k, Some(v)))
.collect();
for (k, v) in service_env {
map.insert(k, v);
}
let mut env_list: Vec<String> = map
.into_iter()
.map(|(k, v)| v.map_or_else(|| k.clone(), |v| format!("{k}={v}")))
.collect();
env_list.extend(env_overrides);
env_list
}
#[cfg(test)]
mod tests {
use super::merge_run_environment;
use std::collections::HashMap;
fn lookup<'a>(list: &'a [String], key: &str) -> Option<&'a str> {
list.iter().rev().find_map(|e| match e.split_once('=') {
Some((k, v)) if k == key => Some(v),
_ => None,
})
}
#[test]
fn env_file_seeds_environment() {
let file: HashMap<String, String> = [("FOO".to_string(), "from-file".to_string())].into();
let list = merge_run_environment(file, HashMap::new(), Vec::new());
assert_eq!(lookup(&list, "FOO"), Some("from-file"));
}
#[test]
fn service_environment_overrides_env_file() {
let file: HashMap<String, String> = [("FOO".to_string(), "from-file".to_string())].into();
let service: HashMap<String, Option<String>> =
[("FOO".to_string(), Some("from-service".to_string()))].into();
let list = merge_run_environment(file, service, Vec::new());
assert_eq!(lookup(&list, "FOO"), Some("from-service"));
}
#[test]
fn dash_e_override_wins_over_all() {
let file: HashMap<String, String> = [("FOO".to_string(), "from-file".to_string())].into();
let service: HashMap<String, Option<String>> =
[("FOO".to_string(), Some("from-service".to_string()))].into();
let list = merge_run_environment(file, service, vec!["FOO=from-cli".to_string()]);
assert_eq!(lookup(&list, "FOO"), Some("from-cli"));
}
#[test]
fn distinct_keys_from_each_layer_are_kept() {
let file: HashMap<String, String> = [("A".to_string(), "a".to_string())].into();
let service: HashMap<String, Option<String>> =
[("B".to_string(), Some("b".to_string()))].into();
let list = merge_run_environment(file, service, vec!["C=c".to_string()]);
assert_eq!(lookup(&list, "A"), Some("a"));
assert_eq!(lookup(&list, "B"), Some("b"));
assert_eq!(lookup(&list, "C"), Some("c"));
}
}