use crate::utils::scan_utils;
use crate::commands::plan::helpers as plan_helpers;
use crate::utils::parallel_processor::ParallelProcessor;
use crate::utils::terraform_operations::{TerraformOperation, OperationType};
use crate::config::ConfigResolver;
use crate::utils::logger;
use colored::*;
#[derive(Debug)]
pub struct ModuleError {
path: String,
error: String,
}
pub fn get_changed_modules(root_dir: &str, force: bool, default_branch: &str, recent_commits: u32) -> Result<Vec<String>, String> {
scan_utils::get_changed_modules_clean(root_dir, force, default_branch, recent_commits)
}
pub fn run_terraform_apply(
modules: &[String],
dry_run: bool,
ignore_workspaces: Option<&[String]>,
var_files: Option<&[String]>,
config_resolver: &ConfigResolver,
watch: bool,
parallel: u32,
) -> Result<(), String> {
if dry_run {
println!("🔍 Running in dry-run mode - executing plan instead of apply");
return plan_helpers::run_terraform_plan(modules, None, ignore_workspaces, var_files, config_resolver, watch, parallel);
}
let effective_parallel = if watch {
println!("🔄 Watch mode enabled - forcing parallel processing to 1 for real-time output");
1
} else {
parallel
};
let parallel_limit = effective_parallel.min(4) as usize;
let mut processor = ParallelProcessor::new(parallel_limit);
for module in modules {
logger::module_header(module);
validate_module_configuration(module)?;
logger::module_init_status(true);
let workspaces = plan_helpers::get_workspaces(module)?;
if workspaces.len() <= 1 {
let default_var_files = config_resolver.get_workspace_var_files(module, "default", var_files);
logger::workspace_discovery(&workspaces);
let operation = TerraformOperation {
module_path: module.clone(),
workspace: None, var_files: default_var_files,
operation_type: OperationType::Apply,
watch,
skip_init: false, };
processor.add_operation(operation).map_err(|e| format!("Failed to add operation: {}", e))?;
} else {
logger::workspace_discovery(&workspaces);
for workspace in workspaces {
if config_resolver.should_ignore_workspace(module, &workspace, ignore_workspaces) {
if workspace == "default" {
logger::workspace_skip(&workspace, "auto-ignored");
continue;
} else {
logger::workspace_skip(&workspace, "configured");
continue;
}
}
let workspace_var_files = config_resolver.get_workspace_var_files(module, &workspace, var_files);
logger::workspace_processing(&workspace, workspace_var_files.len());
let operation = TerraformOperation {
module_path: module.clone(),
workspace: Some(workspace.clone()),
var_files: workspace_var_files,
operation_type: OperationType::Apply,
watch,
skip_init: false, };
processor.add_operation(operation).map_err(|e| format!("Failed to add operation: {}", e))?;
}
}
}
logger::parallel_processing_start(parallel_limit);
processor.start().map_err(|e| format!("Failed to start processor: {}", e))?;
let results = processor.wait_for_completion().map_err(|e| format!("Failed to wait for completion: {}", e))?;
let total_count = results.len();
let mut failed_modules = Vec::new();
let mut successful_count = 0;
for result in results {
if !result.success {
let module_path = match &result.workspace {
Some(workspace) => format!("{}:{}", result.module_path, workspace),
None => result.module_path.clone(),
};
failed_modules.push(ModuleError {
path: module_path,
error: result.error.unwrap_or_else(|| "Unknown error".to_string()),
});
} else {
successful_count += 1;
}
}
logger::processing_summary(total_count, successful_count, failed_modules.len());
if !failed_modules.is_empty() {
use crate::utils::logger;
logger::error_summary("Apply Results", failed_modules.len(), total_count);
println!("\n❌ Failed modules:");
for failure in &failed_modules {
let module_name = failure.path.split('/').last().unwrap_or(&failure.path);
let friendly_error = if failure.error.len() > 80 {
format!("{}...", &failure.error[..80])
} else {
failure.error.clone()
};
println!(" • {}: {}", module_name.cyan(), friendly_error.dimmed());
}
return Err(format!("Failed to process {} module(s)", failed_modules.len()));
}
println!("\n✅ All modules processed successfully!");
Ok(())
}
fn validate_module_configuration(module_path: &str) -> Result<(), String> {
let tf_files = ["main.tf", "variables.tf", "terraform.tfvars"];
let mut has_tf_files = false;
for file in &tf_files {
if std::path::Path::new(module_path).join(file).exists() {
has_tf_files = true;
break;
}
}
if !has_tf_files {
return Err(format!("No Terraform files found in module: {}", module_path));
}
Ok(())
}