use crate::{bakefile::Task, docker};
use std::{
collections::{HashMap, HashSet},
io::Read,
path::Path,
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
};
#[derive(Clone)]
pub enum Context {
Container(
String, Arc<AtomicBool>, Arc<Mutex<HashSet<String>>>, ),
Image(
String, ),
}
impl Drop for Context {
fn drop(&mut self) {
if let Context::Container(container, running, active_containers) = self {
{
active_containers.lock().unwrap().remove(container);
}
if let Err(e) = docker::delete_container(container, running) {
error!("{}", e);
}
}
}
}
fn container_context(
container: &str,
running: &Arc<AtomicBool>,
active_containers: &Arc<Mutex<HashSet<String>>>,
) -> Context {
{
active_containers
.lock()
.unwrap()
.insert(container.to_owned());
}
Context::Container(
container.to_owned(),
running.to_owned(),
active_containers.to_owned(),
)
}
#[allow(clippy::too_many_arguments)]
pub fn run<R: Read>(
settings: &super::Settings,
bakefile_dir: &Path,
environment: &HashMap<String, String>,
running: &Arc<AtomicBool>,
active_containers: &Arc<Mutex<HashSet<String>>>,
task: &Task,
cache_key: &str,
caching_enabled: bool,
context: Context,
mut tar: R,
) -> Result<Context, String> {
let image = format!("{}:{}", settings.docker_repo, cache_key);
let mut cached = false;
if caching_enabled {
cached =
settings.read_local_cache && docker::image_exists(&image, running)?;
if !cached && settings.read_remote_cache {
if let Err(e) = docker::pull_image(&image, running) {
if !running.load(Ordering::SeqCst) {
return Err(e);
}
} else {
cached = true;
}
}
}
if cached {
if task.output_paths.is_empty() {
Ok(Context::Image(image))
} else {
let container = docker::create_container(&image, running)?;
docker::copy_from_container(
&container,
&task.output_paths,
&task.location,
bakefile_dir,
running,
)?;
Ok(container_context(&container, running, active_containers))
}
} else {
let mut commands_to_run = vec![];
commands_to_run.push(format!(
"mkdir --parents {}",
shell_escape(&task.location.to_string_lossy())
));
commands_to_run.push(format!(
"chown --recursive --no-dereference {} {}",
shell_escape(&task.user),
shell_escape(&task.location.to_string_lossy())
));
if let Some(command) = &task.command {
commands_to_run.push(format!(
"cd {}",
shell_escape(&task.location.to_string_lossy())
));
for variable in task.environment.keys() {
commands_to_run.push(format!(
"export {}={}",
shell_escape(variable),
shell_escape(&environment[variable]), ));
}
commands_to_run.push(format!(
"su --command={} {}",
shell_escape(&command),
shell_escape(&task.user)
));
}
let (container, context) = match &context {
Context::Container(container, _, _) => {
(container.to_owned(), context)
}
Context::Image(context_image) => {
if !docker::image_exists(&context_image, running)? {
docker::pull_image(&context_image, running)?;
}
let container = docker::create_container(&context_image, running)?;
(
container.clone(),
container_context(&container, running, active_containers),
)
}
};
if !task.input_paths.is_empty() {
docker::copy_into_container(&container, &mut tar, running)?;
}
docker::start_container(
&container,
&commands_to_run.join(" && "),
running,
)
.map_err(|_| {
if running.load(Ordering::SeqCst) {
"Task failed."
} else {
super::INTERRUPT_MESSAGE
}
.to_owned()
})?;
if !task.output_paths.is_empty() {
docker::copy_from_container(
&container,
&task.output_paths,
&task.location,
bakefile_dir,
running,
)?;
}
if caching_enabled {
if settings.write_local_cache && settings.write_remote_cache {
docker::commit_container(&container, &image, running)?;
docker::push_image(&image, running)?;
} else if settings.write_local_cache && !settings.write_remote_cache {
docker::commit_container(&container, &image, running)?;
} else if !settings.write_local_cache && settings.write_remote_cache {
let temp_image =
format!("{}:{}", settings.docker_repo, docker::random_tag());
docker::commit_container(&container, &temp_image, running)?;
docker::push_image(&temp_image, running)?;
docker::delete_image(&temp_image, running)?;
}
}
Ok(context)
}
}
fn shell_escape(command: &str) -> String {
format!("'{}'", command.replace("'", "'\\''"))
}
#[cfg(test)]
mod tests {
use crate::runner::shell_escape;
#[test]
fn shell_escape_empty() {
assert_eq!(shell_escape(""), "''");
}
#[test]
fn shell_escape_word() {
assert_eq!(shell_escape("foo"), "'foo'");
}
#[test]
fn shell_escape_single_quote() {
assert_eq!(shell_escape("f'o'o"), "'f'\\''o'\\''o'");
}
}