use std::io::Write;
use std::path::{Path, PathBuf};
use crate::Error;
const COMPOSE_FILE: &str = "docker-compose.yml";
const COMPOSE_TEMPLATE: &str = r#"# Generated by `sail:install` for local development only. Published ports
# bind 127.0.0.1 and the credentials below are fixed development defaults:
# replace them before exposing any of these services.
version: '3'
services:
postgres:
image: postgres:15
ports:
- "127.0.0.1:5432:5432"
environment:
POSTGRES_DB: rullst
POSTGRES_USER: root
POSTGRES_PASSWORD: password
volumes:
- sail-postgres:/var/lib/postgresql/data
redis:
image: redis:alpine
ports:
- "127.0.0.1:6379:6379"
volumes:
- sail-redis:/data
meilisearch:
image: getmeili/meilisearch:latest
ports:
- "127.0.0.1:7700:7700"
environment:
MEILI_MASTER_KEY: sail
volumes:
- sail-meilisearch:/meili_data
pgadmin:
image: dpage/pgadmin4
ports:
- "127.0.0.1:5050:80"
environment:
PGADMIN_DEFAULT_EMAIL: admin@rullst.com
PGADMIN_DEFAULT_PASSWORD: password
volumes:
sail-postgres:
driver: local
sail-redis:
driver: local
sail-meilisearch:
driver: local
"#;
pub(super) fn install(directory: &Path, force: bool) -> Result<PathBuf, Error> {
let path = directory.join(COMPOSE_FILE);
let file = if force {
std::fs::File::create(&path)
} else {
std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
};
let mut file = file.map_err(|error| {
if error.kind() == std::io::ErrorKind::AlreadyExists {
Error::Validation(format!(
"{COMPOSE_FILE} already exists; rerun `sail:install --force` to replace it"
))
} else {
Error::Internal(format!("Failed to write {COMPOSE_FILE}: {error}"))
}
})?;
file.write_all(COMPOSE_TEMPLATE.as_bytes())
.map_err(|error| Error::Internal(format!("Failed to write {COMPOSE_FILE}: {error}")))?;
Ok(path)
}
#[cfg(test)]
mod tests {
use super::*;
fn temporary_directory() -> PathBuf {
let directory =
std::env::temp_dir().join(format!("rullst-sail-{}", uuid::Uuid::new_v4().simple()));
std::fs::create_dir(&directory).expect("create temporary directory");
directory
}
#[test]
fn install_binds_loopback_and_never_overwrites_without_force() {
let directory = temporary_directory();
let path = install(&directory, false).expect("first install");
let content = std::fs::read_to_string(&path).expect("read compose file");
assert!(content.contains("postgres:15"));
assert!(content.contains("redis:alpine"));
for published in ["5432:5432", "6379:6379", "7700:7700", "5050:80"] {
assert!(content.contains(&format!("\"127.0.0.1:{published}\"")));
assert!(!content.contains(&format!("\"{published}\"")));
}
std::fs::write(&path, "services: {}\n").expect("replace with a custom file");
let refused = install(&directory, false).expect_err("existing file must be kept");
assert!(matches!(refused, Error::Validation(_)));
assert_eq!(
std::fs::read_to_string(&path).expect("read kept file"),
"services: {}\n"
);
install(&directory, true).expect("forced install");
let forced = std::fs::read_to_string(&path).expect("read forced file");
assert!(forced.contains("127.0.0.1:5432:5432"));
std::fs::remove_dir_all(directory).expect("remove temporary directory");
}
}