#![allow(dead_code)]
mod docker_compose;
mod tilt;
pub use docker_compose::DockerComposeBackend;
pub use tilt::TiltBackend;
use std::fmt;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServiceBackend {
DockerCompose,
Tilt,
}
impl ServiceBackend {
pub fn name(&self) -> &'static str {
match self {
Self::DockerCompose => "Docker Compose",
Self::Tilt => "Tilt",
}
}
pub fn config_files(&self) -> &'static [&'static str] {
match self {
Self::DockerCompose => &[
"docker-compose.yml",
"docker-compose.yaml",
"compose.yml",
"compose.yaml",
],
Self::Tilt => &["Tiltfile"],
}
}
pub fn check_command(&self) -> &'static str {
match self {
Self::DockerCompose => "docker",
Self::Tilt => "tilt",
}
}
}
impl fmt::Display for ServiceBackend {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Debug)]
pub enum ServiceError {
BackendNotInstalled(ServiceBackend),
ConfigNotFound(ServiceBackend),
CommandFailed {
backend: ServiceBackend,
operation: &'static str,
stderr: String,
exit_code: Option<i32>,
},
IoError(std::io::Error),
}
impl fmt::Display for ServiceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BackendNotInstalled(backend) => {
write!(
f,
"{} is not installed. Install it with: jarvy setup",
backend
)
}
Self::ConfigNotFound(backend) => {
write!(f, "No {} config file found in project", backend)
}
Self::CommandFailed {
backend,
operation,
stderr,
exit_code,
} => {
write!(f, "{} {} failed", backend, operation)?;
if let Some(code) = exit_code {
write!(f, " (exit code {})", code)?;
}
if !stderr.is_empty() {
write!(f, ": {}", stderr)?;
}
Ok(())
}
Self::IoError(e) => write!(f, "IO error: {}", e),
}
}
}
impl std::error::Error for ServiceError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::IoError(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for ServiceError {
fn from(e: std::io::Error) -> Self {
Self::IoError(e)
}
}
#[derive(Debug)]
pub struct ServiceResult {
pub success: bool,
pub message: String,
pub backend: ServiceBackend,
}
#[derive(Debug)]
pub struct ServiceStatus {
pub backend: ServiceBackend,
pub installed: bool,
pub running: bool,
pub details: String,
}
pub trait ServiceBackendOps {
fn is_installed(&self) -> bool;
fn find_config(&self, dir: &Path) -> Option<PathBuf>;
fn start(&self, config_path: &Path, detach: bool) -> Result<ServiceResult, ServiceError>;
fn stop(&self, config_path: &Path) -> Result<ServiceResult, ServiceError>;
fn status(&self, config_path: &Path) -> Result<ServiceStatus, ServiceError>;
fn restart(&self, config_path: &Path, detach: bool) -> Result<ServiceResult, ServiceError> {
self.stop(config_path)?;
self.start(config_path, detach)
}
}
pub fn detect_backend(dir: &Path) -> Option<(ServiceBackend, PathBuf)> {
let docker = DockerComposeBackend;
if let Some(path) = docker.find_config(dir) {
return Some((ServiceBackend::DockerCompose, path));
}
let tilt = TiltBackend;
if let Some(path) = tilt.find_config(dir) {
return Some((ServiceBackend::Tilt, path));
}
None
}
pub fn detect_backend_with_config(
dir: &Path,
compose_file: Option<&Path>,
tilt_file: Option<&Path>,
) -> Option<(ServiceBackend, PathBuf)> {
if let Some(compose) = compose_file {
let path = if compose.is_absolute() {
compose.to_path_buf()
} else {
dir.join(compose)
};
if path.exists() {
return Some((ServiceBackend::DockerCompose, path));
}
}
if let Some(tilt) = tilt_file {
let path = if tilt.is_absolute() {
tilt.to_path_buf()
} else {
dir.join(tilt)
};
if path.exists() {
return Some((ServiceBackend::Tilt, path));
}
}
detect_backend(dir)
}
pub fn get_backend(backend: ServiceBackend) -> Box<dyn ServiceBackendOps> {
match backend {
ServiceBackend::DockerCompose => Box::new(DockerComposeBackend),
ServiceBackend::Tilt => Box::new(TiltBackend),
}
}
fn command_exists(cmd: &str) -> bool {
Command::new("which")
.arg(cmd)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn run_command(cmd: &str, args: &[&str], working_dir: &Path) -> Result<Output, std::io::Error> {
Command::new(cmd)
.args(args)
.current_dir(working_dir)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
use tempfile::TempDir;
#[test]
fn test_service_backend_name() {
assert_eq!(ServiceBackend::DockerCompose.name(), "Docker Compose");
assert_eq!(ServiceBackend::Tilt.name(), "Tilt");
}
#[test]
fn test_detect_docker_compose() {
let temp = TempDir::new().unwrap();
let compose_path = temp.path().join("docker-compose.yml");
File::create(&compose_path)
.unwrap()
.write_all(b"version: '3'\n")
.unwrap();
let result = detect_backend(temp.path());
assert!(result.is_some());
let (backend, path) = result.unwrap();
assert_eq!(backend, ServiceBackend::DockerCompose);
assert_eq!(path, compose_path);
}
#[test]
fn test_detect_compose_yml() {
let temp = TempDir::new().unwrap();
let compose_path = temp.path().join("compose.yml");
File::create(&compose_path)
.unwrap()
.write_all(b"version: '3'\n")
.unwrap();
let result = detect_backend(temp.path());
assert!(result.is_some());
let (backend, _) = result.unwrap();
assert_eq!(backend, ServiceBackend::DockerCompose);
}
#[test]
fn test_detect_tiltfile() {
let temp = TempDir::new().unwrap();
let tilt_path = temp.path().join("Tiltfile");
File::create(&tilt_path)
.unwrap()
.write_all(b"# Tiltfile\n")
.unwrap();
let result = detect_backend(temp.path());
assert!(result.is_some());
let (backend, path) = result.unwrap();
assert_eq!(backend, ServiceBackend::Tilt);
assert_eq!(path, tilt_path);
}
#[test]
fn test_docker_compose_priority() {
let temp = TempDir::new().unwrap();
File::create(temp.path().join("docker-compose.yml")).unwrap();
File::create(temp.path().join("Tiltfile")).unwrap();
let result = detect_backend(temp.path());
assert!(result.is_some());
let (backend, _) = result.unwrap();
assert_eq!(backend, ServiceBackend::DockerCompose);
}
#[test]
fn test_no_service_found() {
let temp = TempDir::new().unwrap();
let result = detect_backend(temp.path());
assert!(result.is_none());
}
#[test]
fn test_config_override() {
let temp = TempDir::new().unwrap();
let subdir = temp.path().join("docker");
std::fs::create_dir(&subdir).unwrap();
let compose_path = subdir.join("compose.yml");
File::create(&compose_path).unwrap();
assert!(detect_backend(temp.path()).is_none());
let result =
detect_backend_with_config(temp.path(), Some(Path::new("docker/compose.yml")), None);
assert!(result.is_some());
let (backend, _) = result.unwrap();
assert_eq!(backend, ServiceBackend::DockerCompose);
}
}