use std::process::{Command, Stdio};
use std::io::{BufRead, BufReader};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use std::path::{Path, PathBuf};
use crate::utils::error::{SolarboatError, SafeOperations};
#[derive(Debug, Clone)]
pub enum TerraformStatus {
Initializing,
Planning,
Applying,
Completed { success: bool },
Failed { error: String },
}
#[derive(Debug)]
pub struct BackgroundTerraform {
thread_handle: Option<thread::JoinHandle<()>>,
status: Arc<Mutex<TerraformStatus>>,
output: Arc<Mutex<Vec<String>>>,
}
impl Default for BackgroundTerraform {
fn default() -> Self {
Self::new()
}
}
impl BackgroundTerraform {
pub fn new() -> Self {
Self {
thread_handle: None,
status: Arc::new(Mutex::new(TerraformStatus::Initializing)),
output: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn get_status(&self) -> Result<TerraformStatus, SolarboatError> {
let status = SafeOperations::lock_with_timeout(
&self.status,
Duration::from_secs(1),
"terraform_status"
)?;
Ok(status.clone())
}
pub fn get_output(&self) -> Result<Vec<String>, SolarboatError> {
let output = SafeOperations::lock_with_timeout(
&self.output,
Duration::from_secs(1),
"terraform_output"
)?;
Ok(output.clone())
}
pub fn is_running(&mut self) -> bool {
if let Some(handle) = &mut self.thread_handle {
!handle.is_finished()
} else {
false
}
}
pub fn init_background(&mut self, module_path: &str) -> Result<(), SolarboatError> {
let mut cmd = Command::new("terraform");
cmd.arg("init")
.current_dir(module_path)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn()
.map_err(|e| SolarboatError::Process {
command: "terraform init".to_string(),
args: vec!["init".to_string()],
cause: e.to_string(),
exit_code: None,
})?;
let status = Arc::clone(&self.status);
let output = Arc::clone(&self.output);
let stdout = child.stdout.take().ok_or_else(|| SolarboatError::Process {
command: "terraform init".to_string(),
args: vec!["init".to_string()],
cause: "Failed to capture stdout".to_string(),
exit_code: None,
})?;
let stderr = child.stderr.take().ok_or_else(|| SolarboatError::Process {
command: "terraform init".to_string(),
args: vec!["init".to_string()],
cause: "Failed to capture stderr".to_string(),
exit_code: None,
})?;
let child_handle = thread::spawn(move || {
let stdout_reader = BufReader::new(stdout);
let stderr_reader = BufReader::new(stderr);
for line in stdout_reader.lines() {
if let Ok(line) = line {
if let Ok(mut output) = SafeOperations::lock_with_timeout(
&output,
Duration::from_secs(1),
"output_stdout"
) {
output.push(line.clone());
}
println!(" {}", line);
}
}
for line in stderr_reader.lines() {
if let Ok(line) = line {
if let Ok(mut output) = SafeOperations::lock_with_timeout(
&output,
Duration::from_secs(1),
"output_stderr"
) {
output.push(format!("ERROR: {}", line));
}
eprintln!(" ERROR: {}", line);
}
}
let exit_status = match child.wait() {
Ok(status) => status,
Err(e) => {
eprintln!("Failed to wait for terraform init process: {}", e);
return;
}
};
if exit_status.success() {
if let Ok(mut status) = SafeOperations::lock_with_timeout(
&status,
Duration::from_secs(1),
"status_success"
) {
*status = TerraformStatus::Completed { success: true };
}
} else {
if let Ok(mut status) = SafeOperations::lock_with_timeout(
&status,
Duration::from_secs(1),
"status_failed"
) {
*status = TerraformStatus::Failed {
error: "Terraform init failed".to_string()
};
}
}
});
self.thread_handle = Some(child_handle);
Ok(())
}
pub fn plan_background(&mut self, module_path: &str, var_files: Option<&[String]>) -> Result<(), String> {
let mut cmd = Command::new("terraform");
cmd.arg("plan")
.current_dir(module_path)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if let Some(var_files) = var_files {
for var_file in var_files {
let var_file_path = if Path::new(var_file).is_absolute() {
PathBuf::from(var_file)
} else {
let current_dir = std::env::current_dir()
.map_err(|e| format!("Failed to get current directory: {}", e))?;
let absolute_var_file = current_dir.join(var_file);
let absolute_module = current_dir.join(module_path);
match absolute_var_file.strip_prefix(&absolute_module) {
Ok(relative_path) => {
relative_path.to_path_buf()
}
Err(_) => {
let mut relative_path = PathBuf::new();
let module_components: Vec<_> = absolute_module.components().collect();
let var_file_components: Vec<_> = absolute_var_file.components().collect();
let mut common_len = 0;
for (i, (m, v)) in module_components.iter().zip(var_file_components.iter()).enumerate() {
if m == v {
common_len = i + 1;
} else {
break;
}
}
for _ in common_len..module_components.len() {
relative_path.push("..");
}
for component in &var_file_components[common_len..] {
relative_path.push(component);
}
relative_path
}
}
};
cmd.arg("-var-file").arg(&var_file_path);
}
}
let mut child = cmd.spawn()
.map_err(|e| format!("Failed to start terraform plan: {}", e))?;
let status = Arc::clone(&self.status);
let output = Arc::clone(&self.output);
let stdout = child.stdout.take().unwrap();
let stderr = child.stderr.take().unwrap();
let child_handle = thread::spawn(move || {
*status.lock().unwrap() = TerraformStatus::Planning;
let stdout_reader = BufReader::new(stdout);
let stderr_reader = BufReader::new(stderr);
for line in stdout_reader.lines() {
if let Ok(line) = line {
output.lock().unwrap().push(line.clone());
println!(" {}", line);
}
}
for line in stderr_reader.lines() {
if let Ok(line) = line {
output.lock().unwrap().push(format!("ERROR: {}", line));
eprintln!(" ERROR: {}", line);
}
}
let exit_status = child.wait().unwrap();
if exit_status.success() {
*status.lock().unwrap() = TerraformStatus::Completed { success: true };
} else {
*status.lock().unwrap() = TerraformStatus::Failed {
error: "Terraform plan failed".to_string()
};
}
});
self.thread_handle = Some(child_handle);
Ok(())
}
pub fn apply_background(&mut self, module_path: &str, var_files: Option<&[String]>) -> Result<(), String> {
let mut cmd = Command::new("terraform");
cmd.arg("apply")
.arg("-auto-approve")
.arg("-input=false")
.current_dir(module_path)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if let Some(var_files) = var_files {
for var_file in var_files {
let var_file_path = if Path::new(var_file).is_absolute() {
PathBuf::from(var_file)
} else {
let current_dir = std::env::current_dir()
.map_err(|e| format!("Failed to get current directory: {}", e))?;
let absolute_var_file = current_dir.join(var_file);
let absolute_module = current_dir.join(module_path);
match absolute_var_file.strip_prefix(&absolute_module) {
Ok(relative_path) => {
relative_path.to_path_buf()
}
Err(_) => {
let mut relative_path = PathBuf::new();
let module_components: Vec<_> = absolute_module.components().collect();
let var_file_components: Vec<_> = absolute_var_file.components().collect();
let mut common_len = 0;
for (i, (m, v)) in module_components.iter().zip(var_file_components.iter()).enumerate() {
if m == v {
common_len = i + 1;
} else {
break;
}
}
for _ in common_len..module_components.len() {
relative_path.push("..");
}
for component in &var_file_components[common_len..] {
relative_path.push(component);
}
relative_path
}
}
};
cmd.arg("-var-file").arg(&var_file_path);
}
}
let mut child = cmd.spawn()
.map_err(|e| format!("Failed to start terraform apply: {}", e))?;
let status = Arc::clone(&self.status);
let output = Arc::clone(&self.output);
let stdout = child.stdout.take().unwrap();
let stderr = child.stderr.take().unwrap();
let child_handle = thread::spawn(move || {
*status.lock().unwrap() = TerraformStatus::Applying;
let stdout_reader = BufReader::new(stdout);
let stderr_reader = BufReader::new(stderr);
for line in stdout_reader.lines() {
if let Ok(line) = line {
output.lock().unwrap().push(line.clone());
println!(" {}", line);
}
}
for line in stderr_reader.lines() {
if let Ok(line) = line {
output.lock().unwrap().push(format!("ERROR: {}", line));
eprintln!(" ERROR: {}", line);
}
}
let exit_status = child.wait().unwrap();
if exit_status.success() {
*status.lock().unwrap() = TerraformStatus::Completed { success: true };
} else {
*status.lock().unwrap() = TerraformStatus::Failed {
error: "Terraform apply failed".to_string()
};
}
});
self.thread_handle = Some(child_handle);
Ok(())
}
pub fn wait_for_completion(&mut self, timeout_seconds: u64) -> Result<bool, String> {
let start_time = std::time::Instant::now();
let timeout = Duration::from_secs(timeout_seconds);
while self.is_running() {
if start_time.elapsed() > timeout {
return Err("Operation timed out".to_string());
}
thread::sleep(Duration::from_millis(100));
}
match self.get_status() {
Ok(status) => match status {
TerraformStatus::Completed { success } => Ok(success),
TerraformStatus::Failed { error } => Err(error),
_ => Err("Operation did not complete properly".to_string()),
},
Err(e) => Err(format!("Failed to get status: {}", e)),
}
}
pub fn kill(&mut self) {
if let Some(handle) = self.thread_handle.take() {
let _ = handle.join();
}
}
}
pub fn run_terraform_silent(
command: &str,
args: &[&str],
module_path: &str,
var_files: Option<&[String]>,
) -> Result<bool, String> {
let mut cmd = Command::new("terraform");
cmd.arg(command)
.args(args)
.current_dir(module_path)
.stdout(Stdio::null())
.stderr(Stdio::null());
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| format!("Failed to execute terraform {}: {}", command, e))?;
Ok(status.success())
}