use std::process::{Command, Stdio};
use std::path::Path;
use regex::Regex;
use std::time::{SystemTime, UNIX_EPOCH};
use std::time::Duration;
use std::thread;
#[derive(Debug, Clone)]
pub struct TerraformOperation {
pub module_path: String,
pub workspace: Option<String>,
pub var_files: Vec<String>,
pub operation_type: OperationType,
pub watch: bool,
pub skip_init: bool, }
#[derive(Debug, Clone)]
pub enum OperationType {
Init,
Plan { plan_dir: Option<String> },
Apply,
}
#[derive(Debug, Clone)]
pub struct OperationResult {
pub module_path: String,
pub workspace: Option<String>,
pub operation_type: OperationType,
pub success: bool,
pub error: Option<String>,
pub output: Vec<String>,
}
pub fn ensure_module_initialized(module_path: &str) -> Result<(), String> {
let terraform_dir = std::path::Path::new(module_path).join(".terraform");
if terraform_dir.exists() {
let workspace_check = Command::new("terraform")
.arg("workspace")
.arg("list")
.current_dir(module_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
if workspace_check.is_ok() && workspace_check.unwrap().success() {
return Ok(()); }
}
let output = Command::new("terraform")
.arg("init")
.current_dir(module_path)
.output()
.map_err(|e| format!("Failed to run terraform init: {}", e))?;
if !output.status.success() {
let error_msg = String::from_utf8_lossy(&output.stderr);
return Err(format!("Terraform init failed: {}", error_msg));
}
Ok(())
}
pub fn select_workspace(module_path: &str, workspace: &str) -> Result<(), String> {
let current_workspace = Command::new("terraform")
.arg("workspace")
.arg("show")
.current_dir(module_path)
.output()
.map_err(|e| format!("Failed to get current workspace: {}", e))?;
if current_workspace.status.success() {
let current = String::from_utf8_lossy(¤t_workspace.stdout).trim().to_string();
if current == workspace {
return Ok(()); }
}
let mut cmd = Command::new("terraform");
cmd.arg("workspace")
.arg("select")
.arg(workspace)
.current_dir(module_path)
.stdout(Stdio::null())
.stderr(Stdio::null());
let status = cmd.status()
.map_err(|e| format!("Failed to select workspace {}: {}", workspace, e))?;
if status.success() {
Ok(())
} else {
Err(format!("Failed to select workspace {}", workspace))
}
}
pub fn save_plan_output(module_path: &str, plan_dir: &str, workspace: Option<&str>, output_lines: &[String]) -> Result<(), String> {
std::fs::create_dir_all(plan_dir)
.map_err(|e| format!("Failed to create plan directory: {}", e))?;
if let Some(module_name) = Path::new(module_path).file_name().and_then(|n| n.to_str()) {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| format!("Failed to get timestamp: {}", e))?
.as_secs();
let workspace_name = workspace.unwrap_or("default");
let filename = format!("{}-{}-{}.tfplan.md", module_name, workspace_name, timestamp);
let plan_file = Path::new(plan_dir).join(filename);
let mut content = format!("# Terraform Plan Output for {} (workspace: {})\n\n", module_name, workspace_name);
content.push_str("```\n");
for line in output_lines {
content.push_str(&clean_terraform_output(line));
content.push('\n');
}
content.push_str("```\n");
std::fs::write(&plan_file, content)
.map_err(|e| format!("Failed to write plan file: {}", e))?;
}
Ok(())
}
pub fn clean_terraform_output(input: &str) -> String {
let re = Regex::new(r"\x1b\[[0-9;]*m").unwrap();
re.replace_all(input, "").to_string()
}
pub fn run_single_plan(module_path: &str, plan_dir: Option<&str>, workspace: Option<&str>, var_files: Option<&[String]>) -> Result<bool, String> {
ensure_module_initialized(module_path)?;
let mut cmd = Command::new("terraform");
cmd.arg("plan").current_dir(module_path);
if let Some(var_files) = var_files {
for var_file in var_files {
cmd.arg("-var-file").arg(var_file);
}
}
let output = cmd.output()
.map_err(|e| e.to_string())?;
if !output.status.success() {
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
return Ok(false);
}
if let Some(plan_dir) = plan_dir {
let plan_output = String::from_utf8_lossy(&output.stdout).to_string();
let output_lines: Vec<String> = plan_output.lines().map(|s| s.to_string()).collect();
if let Err(e) = save_plan_output(module_path, plan_dir, workspace, &output_lines) {
eprintln!("Warning: Failed to save plan output: {}", e);
}
}
Ok(true)
}
pub fn run_single_apply(module_path: &str, var_files: Option<&[String]>) -> Result<bool, String> {
ensure_module_initialized(module_path)?;
let mut cmd = Command::new("terraform");
cmd.arg("apply")
.arg("-auto-approve")
.arg("-input=false") .current_dir(module_path);
if let Some(var_files) = var_files {
for var_file in var_files {
cmd.arg("-var-file").arg(var_file);
}
}
let status = cmd.status()
.map_err(|e| e.to_string())?;
Ok(status.success())
}
pub fn check_state_lock_available(module_path: &str, workspace: Option<&str>) -> bool {
if let Some(ws) = workspace {
if let Err(_) = select_workspace(module_path, ws) {
return false;
}
}
let result = Command::new("terraform")
.arg("state")
.arg("list")
.current_dir(module_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
result.is_ok() && result.unwrap().success()
}
pub fn wait_for_state_lock_release(module_path: &str, workspace: Option<&str>, max_wait: Duration) -> bool {
use std::time::Instant;
let start = Instant::now();
let mut attempt = 0;
let max_attempts = 10;
while start.elapsed() < max_wait && attempt < max_attempts {
if check_state_lock_available(module_path, workspace) {
return true; }
attempt += 1;
let delay = std::cmp::min(2u64.pow(attempt), 30); thread::sleep(Duration::from_secs(delay));
}
false }