#![allow(dead_code)]
mod types;
pub use types::{DeployConfig, DeployResult, DeployStrategy};
use anyhow::{Context, Result};
use std::path::Path;
use std::process::Command;
pub struct DeploymentService;
impl DeploymentService {
pub fn new() -> Self {
Self
}
pub fn test_connection(&self, config: &DeployConfig) -> Result<()> {
let mut cmd = Command::new("ssh");
cmd.arg("-p").arg(config.port.to_string());
if let Some(key_path) = &config.ssh_key {
cmd.arg("-i").arg(key_path);
}
cmd.arg("-o")
.arg("ConnectTimeout=5")
.arg("-o")
.arg("StrictHostKeyChecking=no")
.arg(format!("{}@{}", config.user, config.host))
.arg("echo 'Connection test successful'");
let output = cmd.output().context("Failed to execute SSH command")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow::anyhow!(
"SSH connection failed: {}. Please check:\n\
- Host is reachable: {}\n\
- Port is correct: {}\n\
- SSH key is valid: {:?}\n\
- User has access: {}",
stderr,
config.host,
config.port,
config.ssh_key,
config.user
));
}
Ok(())
}
pub fn create_backup(&self, config: &DeployConfig) -> Result<()> {
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
let backup_name = format!("backup_{}", timestamp);
let remote_cmd = format!(
"mkdir -p {} && [ -d {} ] && cp -r {} {}/{} || echo 'No previous installation to backup'",
config.backup_dir, config.remote_dir, config.remote_dir, config.backup_dir, backup_name
);
self.run_remote_command(config, &remote_cmd)?;
Ok(())
}
pub fn upload_package(&self, config: &DeployConfig, package_path: &Path) -> Result<()> {
if !package_path.exists() {
return Err(anyhow::anyhow!("Package not found: {}", package_path.display()));
}
let remote_cmd = format!("mkdir -p {}/staging", config.remote_dir);
self.run_remote_command(config, &remote_cmd)?;
let mut cmd = Command::new("rsync");
cmd.arg("-avz")
.arg("--progress")
.arg("-e")
.arg(format!("ssh -p {}", config.port));
if let Some(key_path) = &config.ssh_key {
cmd.arg("-e")
.arg(format!("ssh -p {} -i {}", config.port, key_path.display()));
}
cmd.arg(package_path).arg(format!(
"{}@{}:{}/staging/",
config.user, config.host, config.remote_dir
));
let status = cmd.status().context("Failed to execute rsync")?;
if !status.success() {
return Err(anyhow::anyhow!("Package upload failed"));
}
let package_name = package_path.file_name().unwrap().to_string_lossy().to_string();
let extract_cmd = format!("cd {}/staging && tar -xzf {}", config.remote_dir, package_name);
self.run_remote_command(config, &extract_cmd)?;
Ok(())
}
pub async fn deploy(
&self,
config: &DeployConfig,
package_path: &Path,
strategy: DeployStrategy,
) -> Result<DeployResult> {
let backup_created = self.create_backup(config).is_ok();
self.upload_package(config, package_path)?;
match strategy {
DeployStrategy::Direct => self.deploy_direct(config).await?,
DeployStrategy::Canary => self.deploy_canary(config).await?,
DeployStrategy::Rolling => self.deploy_rolling(config).await?,
DeployStrategy::BlueGreen => self.deploy_blue_green(config).await?,
}
let health_check_passed = self.health_check(config).await.unwrap_or(false);
if !health_check_passed {
self.rollback(config).await?;
return Ok(DeployResult {
success: false,
message: "Deployment failed health check, rolled back".to_string(),
health_check_passed: false,
backup_created,
});
}
Ok(DeployResult {
success: true,
message: format!("Deployment successful using {} strategy", strategy.as_str()),
health_check_passed: true,
backup_created,
})
}
async fn deploy_direct(&self, config: &DeployConfig) -> Result<()> {
let stop_cmd = format!("sudo systemctl stop {}", config.service_name);
self.run_remote_command(config, &stop_cmd)?;
let deploy_cmd = format!("cp -r {}/staging/* {}/", config.remote_dir, config.remote_dir);
self.run_remote_command(config, &deploy_cmd)?;
let start_cmd = format!("sudo systemctl start {}", config.service_name);
self.run_remote_command(config, &start_cmd)?;
Ok(())
}
async fn deploy_canary(&self, config: &DeployConfig) -> Result<()> {
self.deploy_direct(config).await
}
async fn deploy_rolling(&self, config: &DeployConfig) -> Result<()> {
self.deploy_direct(config).await
}
async fn deploy_blue_green(&self, config: &DeployConfig) -> Result<()> {
self.deploy_direct(config).await
}
pub async fn health_check(&self, config: &DeployConfig) -> Result<bool> {
for attempt in 1..=config.health_check_retries {
let check_cmd = format!("systemctl is-active {}", config.service_name);
match self.run_remote_command(config, &check_cmd) {
Ok(output) if output.trim() == "active" => return Ok(true),
_ => {
if attempt < config.health_check_retries {
tokio::time::sleep(tokio::time::Duration::from_secs(
config.health_check_timeout / config.health_check_retries as u64,
))
.await;
}
}
}
}
Ok(false)
}
pub async fn rollback(&self, config: &DeployConfig) -> Result<()> {
let find_backup_cmd = format!("ls -t {}/backup_* | head -1", config.backup_dir);
let latest_backup = self.run_remote_command(config, &find_backup_cmd)?.trim().to_string();
if latest_backup.is_empty() {
return Err(anyhow::anyhow!("No backup found for rollback"));
}
let stop_cmd = format!("sudo systemctl stop {}", config.service_name);
self.run_remote_command(config, &stop_cmd)?;
let restore_cmd = format!(
"rm -rf {} && cp -r {} {}",
config.remote_dir, latest_backup, config.remote_dir
);
self.run_remote_command(config, &restore_cmd)?;
let start_cmd = format!("sudo systemctl start {}", config.service_name);
self.run_remote_command(config, &start_cmd)?;
Ok(())
}
pub fn run_remote_command(&self, config: &DeployConfig, command: &str) -> Result<String> {
let mut cmd = Command::new("ssh");
cmd.arg("-p").arg(config.port.to_string());
if let Some(key_path) = &config.ssh_key {
cmd.arg("-i").arg(key_path);
}
cmd.arg("-o")
.arg("StrictHostKeyChecking=no")
.arg(format!("{}@{}", config.user, config.host))
.arg(command);
let output = cmd.output().context("Failed to execute SSH command")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow::anyhow!("Remote command failed: {}", stderr));
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
pub fn get_service_status(&self, config: &DeployConfig) -> Result<String> {
let cmd = format!("systemctl status {}", config.service_name);
self.run_remote_command(config, &cmd)
}
pub fn list_backups(&self, config: &DeployConfig) -> Result<Vec<String>> {
let cmd = format!("ls -t {}/backup_* 2>/dev/null || echo ''", config.backup_dir);
let output = self.run_remote_command(config, &cmd)?;
let backups: Vec<String> = output
.lines()
.filter(|line| !line.is_empty())
.map(|line| line.to_string())
.collect();
Ok(backups)
}
}
impl Default for DeploymentService {
fn default() -> Self {
Self::new()
}
}