use super::service::{LocalDynamoDB, LocalQueue, LocalSqlDB, Service};
use crate::process::Process;
use crate::{error::Error, writer::Writer};
use eyre::Context;
use serde_json::{Map, Value};
use std::{path::Path, path::PathBuf, process, process::Stdio};
pub struct Docker<'a> {
build_path: PathBuf,
is_started: bool,
services: Vec<Service<'a>>,
}
impl<'a> Docker<'a> {
pub fn new(build_path: &Path) -> Self {
Self {
build_path: build_path.to_owned(),
is_started: false,
services: vec![],
}
}
pub fn start(&mut self, writer: &Writer) -> eyre::Result<()> {
if self.services.is_empty() {
return Ok(());
}
let docker_compose_file = self.docker_compose_string()?;
let dest = self.docker_compose_path();
std::fs::write(&dest, docker_compose_file)
.inspect_err(|e| {
log::error!("Failed to write DOCKER_COMPOSE_FILE to {:?}: {}", dest, e)
})
.wrap_err(Error::new(
"Failed to set up Docker",
Some(&format!("Make sure you can write to {dest:?}")),
))?;
let file_path = dest.to_string_lossy();
let child = process::Command::new("docker-compose")
.args(["-f", &file_path, "up", "-d"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.wrap_err("Failed to execute docker-compose")?;
let mut process = Process::new(child, writer);
let status = process.log()?;
if !status.success() {
process.print_error()?;
return Err(Error::new(
"Failed to start Docker containers",
Some("Make sure the docker is installed and running."),
)
.into());
}
self.is_started = true;
Ok(())
}
pub fn stop(&self) -> eyre::Result<()> {
if !self.is_started {
return Ok(());
}
let status = process::Command::new("docker-compose")
.args(["-f", &self.docker_compose_path().to_string_lossy(), "down"])
.stderr(Stdio::null())
.stdout(Stdio::null())
.status()
.inspect_err(|e| log::error!("Error: {}", e))
.wrap_err("Failed to execute docker-compose")?;
if !status.success() {
return Err(eyre::eyre!(
"docker-compose command failed with exit code: {}",
status
));
}
Ok(())
}
pub async fn provision(&self) -> eyre::Result<()> {
let tasks = self
.services
.iter()
.map(|service| async move {
match service {
Service::DynamoDB(db) => db
.provision()
.await
.wrap_err("Failed to provision local DynamoDB"),
Service::Queue(queue) => queue
.provision()
.await
.wrap_err("Failed to provision local SQS queue"),
Service::SqlDB(db) => db
.provision()
.await
.wrap_err("Failed to provision local SQL DB"),
}
})
.collect::<Vec<_>>();
futures::future::try_join_all(tasks)
.await
.wrap_err("Failed to provision services")?;
Ok(())
}
pub fn with_dynamodb(&mut self, dynamodb: LocalDynamoDB) {
self.services.push(Service::DynamoDB(dynamodb));
}
pub fn with_sqldb(&mut self, sqldb: LocalSqlDB<'a>) {
self.services.push(Service::SqlDB(sqldb));
}
pub fn with_queue(&mut self, queue: LocalQueue) {
self.services.push(Service::Queue(queue));
}
fn docker_compose_string(&self) -> eyre::Result<String> {
let mut services = Map::new();
for service in &self.services {
let service_snippet = match service {
Service::DynamoDB(service) => service.docker_compose_snippet(),
Service::Queue(service) => service.docker_compose_snippet(),
Service::SqlDB(service) => service.docker_compose_snippet(),
};
let mapping: Map<String, Value> = serde_saphyr::from_str(&service_snippet)
.wrap_err("failed to parse service YAML snippet")?;
for (k, v) in mapping {
services.insert(k.clone(), v.clone());
}
}
let root = Map::from_iter([("services".to_string(), Value::from(services))]);
let docker_compose =
serde_saphyr::to_string(&root).wrap_err("failed to serialize docker-compose YAML")?;
Ok(docker_compose)
}
fn docker_compose_path(&self) -> PathBuf {
self.build_path.join("docker-compose.yml")
}
}
impl Drop for Docker<'_> {
fn drop(&mut self) {
self.stop().unwrap();
}
}