diff --git a/src/application/commands/destroy.rs b/src/application/commands/destroy.rs new file mode 100644 index 00000000..4fa49961 --- /dev/null +++ b/src/application/commands/destroy.rs @@ -0,0 +1,319 @@ +//! Infrastructure destruction command +//! +//! This module contains the `DestroyCommand` which orchestrates the complete infrastructure +//! teardown workflow including: +//! +//! - Infrastructure destruction via `OpenTofu` +//! - State cleanup and resource verification +//! +//! The command handles the complex interaction with deployment tools and ensures +//! proper sequencing of destruction steps. + +use std::path::PathBuf; +use std::sync::Arc; + +use tracing::{info, instrument}; + +use crate::adapters::tofu::client::OpenTofuError; +use crate::application::steps::DestroyInfrastructureStep; +use crate::domain::environment::repository::EnvironmentRepository; +use crate::domain::environment::{Destroyed, Environment}; +use crate::shared::command::CommandError; + +/// Comprehensive error type for the `DestroyCommand` +#[derive(Debug, thiserror::Error)] +pub enum DestroyCommandError { + #[error("OpenTofu command failed: {0}")] + OpenTofu(#[from] OpenTofuError), + + #[error("Command execution failed: {0}")] + Command(#[from] CommandError), + + #[error("Failed to persist environment state: {0}")] + StatePersistence(#[from] crate::domain::environment::repository::RepositoryError), + + #[error("Failed to clean up state files at '{path}': {source}")] + StateCleanupFailed { + path: PathBuf, + #[source] + source: std::io::Error, + }, +} + +impl crate::shared::Traceable for DestroyCommandError { + fn trace_format(&self) -> String { + match self { + Self::OpenTofu(e) => { + format!("DestroyCommandError: OpenTofu command failed - {e}") + } + Self::Command(e) => { + format!("DestroyCommandError: Command execution failed - {e}") + } + Self::StatePersistence(e) => { + format!("DestroyCommandError: Failed to persist environment state - {e}") + } + Self::StateCleanupFailed { path, source } => { + format!( + "DestroyCommandError: Failed to clean up state files at '{}' - {source}", + path.display() + ) + } + } + } + + fn trace_source(&self) -> Option<&dyn crate::shared::Traceable> { + match self { + Self::OpenTofu(e) => Some(e), + Self::Command(e) => Some(e), + Self::StatePersistence(_) | Self::StateCleanupFailed { .. } => None, + } + } + + fn error_kind(&self) -> crate::shared::ErrorKind { + match self { + Self::OpenTofu(_) => crate::shared::ErrorKind::InfrastructureOperation, + Self::Command(_) => crate::shared::ErrorKind::CommandExecution, + Self::StatePersistence(_) | Self::StateCleanupFailed { .. } => { + crate::shared::ErrorKind::StatePersistence + } + } + } +} + +/// `DestroyCommand` orchestrates the complete infrastructure destruction workflow +/// +/// The `DestroyCommand` orchestrates the complete infrastructure teardown workflow. +/// +/// This command handles all steps required to destroy infrastructure: +/// 1. Destroy infrastructure via `OpenTofu` +/// 2. Transition environment to `Destroyed` state +/// +/// # State Management +/// +/// The command integrates with the type-state pattern for environment lifecycle: +/// - Accepts `Environment` (any state) as input +/// - Returns `Environment` on success +/// +/// State is persisted after destruction using the injected repository. +/// +/// # Idempotency +/// +/// The destroy operation is idempotent. Running it multiple times on the same +/// environment will: +/// - Succeed if the infrastructure is already destroyed +/// - Report appropriate status to the user +/// - Not fail due to missing resources +pub struct DestroyCommand { + opentofu_client: Arc, + repository: Arc, +} + +impl DestroyCommand { + /// Create a new `DestroyCommand` + #[must_use] + pub fn new( + opentofu_client: Arc, + repository: Arc, + ) -> Self { + Self { + opentofu_client, + repository, + } + } + + /// Execute the complete destruction workflow + /// + /// # Arguments + /// + /// * `environment` - The environment to destroy (can be in any state) + /// + /// # Returns + /// + /// Returns the destroyed environment + /// + /// # Errors + /// + /// Returns an error if any step in the destruction workflow fails: + /// * `OpenTofu` destroy fails + /// * Unable to persist the destroyed state + /// + /// On error, cleanup may be partial. The user should manually verify + /// and complete the cleanup if necessary. + #[instrument( + name = "destroy_command", + skip_all, + fields( + command_type = "destroy", + environment = %environment.name() + ) + )] + pub fn execute( + &self, + environment: Environment, + ) -> Result, DestroyCommandError> { + info!( + command = "destroy", + environment = %environment.name(), + "Starting complete infrastructure destruction workflow" + ); + + // Execute infrastructure destruction + // OpenTofu destroy is idempotent - it will succeed even if infrastructure doesn't exist + self.destroy_infrastructure()?; + + // Transition to Destroyed state + let destroyed = environment.destroy(); + + // Clean up state files only after successful infrastructure destruction + Self::cleanup_state_files(&destroyed)?; + + // Persist final state + self.repository.save(&destroyed.clone().into_any())?; + + info!( + command = "destroy", + environment = %destroyed.name(), + "Infrastructure destruction completed successfully" + ); + + Ok(destroyed) + } + + // Private helper methods + + /// Destroy the infrastructure using `OpenTofu` + /// + /// Executes the `OpenTofu` destroy workflow to remove all managed infrastructure. + /// + /// # Errors + /// + /// Returns an error if `OpenTofu` destroy fails + fn destroy_infrastructure(&self) -> Result<(), DestroyCommandError> { + DestroyInfrastructureStep::new(Arc::clone(&self.opentofu_client)).execute()?; + Ok(()) + } + + /// Clean up state files after successful infrastructure destruction + /// + /// Removes the data and build directories for the environment. + /// This is only called after infrastructure destruction succeeds. + /// + /// # Arguments + /// + /// * `env` - The destroyed environment + /// + /// # Errors + /// + /// Returns an error if state file cleanup fails + fn cleanup_state_files(env: &Environment) -> Result<(), DestroyCommandError> { + let data_dir = env.data_dir(); + let build_dir = env.build_dir(); + + // Remove data directory if it exists + if data_dir.exists() { + std::fs::remove_dir_all(data_dir).map_err(|source| { + DestroyCommandError::StateCleanupFailed { + path: data_dir.clone(), + source, + } + })?; + info!( + command = "destroy", + path = %data_dir.display(), + "Removed state directory" + ); + } + + // Remove build directory if it exists + if build_dir.exists() { + std::fs::remove_dir_all(build_dir).map_err(|source| { + DestroyCommandError::StateCleanupFailed { + path: build_dir.clone(), + source, + } + })?; + info!( + command = "destroy", + path = %build_dir.display(), + "Removed build directory" + ); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + /// Test builder for `DestroyCommand` that manages dependencies and lifecycle + /// + /// This builder simplifies test setup by: + /// - Managing `TempDir` lifecycle + /// - Providing sensible defaults for all dependencies + /// - Allowing selective customization of dependencies + /// - Returning only the command and necessary test artifacts + pub struct DestroyCommandTestBuilder { + temp_dir: TempDir, + } + + impl DestroyCommandTestBuilder { + /// Create a new test builder with default configuration + pub fn new() -> Self { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + Self { temp_dir } + } + + /// Build the `DestroyCommand` with all dependencies + /// + /// Returns: (`command`, `temp_dir`) + /// The `temp_dir` must be kept alive for the duration of the test. + pub fn build(self) -> (DestroyCommand, TempDir) { + let opentofu_client = Arc::new(crate::adapters::tofu::client::OpenTofuClient::new( + self.temp_dir.path(), + )); + + let repository_factory = + crate::infrastructure::persistence::repository_factory::RepositoryFactory::new( + std::time::Duration::from_secs(30), + ); + let repository = repository_factory.create(self.temp_dir.path().to_path_buf()); + + let command = DestroyCommand::new(opentofu_client, repository); + + (command, self.temp_dir) + } + } + + #[test] + fn it_should_create_destroy_command_with_all_dependencies() { + let (command, _temp_dir) = DestroyCommandTestBuilder::new().build(); + + // Verify the command was created (basic structure test) + // This test just verifies that the command can be created with the dependencies + assert_eq!(Arc::strong_count(&command.opentofu_client), 1); + } + + #[test] + fn it_should_have_correct_error_type_conversions() { + // Test that all error types can convert to DestroyCommandError + let command_error = CommandError::StartupFailed { + command: "test".to_string(), + source: std::io::Error::new(std::io::ErrorKind::NotFound, "test"), + }; + let opentofu_error = OpenTofuError::CommandError(command_error); + let destroy_error: DestroyCommandError = opentofu_error.into(); + drop(destroy_error); + + let command_error_direct = CommandError::ExecutionFailed { + command: "test".to_string(), + exit_code: "1".to_string(), + stdout: String::new(), + stderr: "test error".to_string(), + }; + let destroy_error: DestroyCommandError = command_error_direct.into(); + drop(destroy_error); + } +} diff --git a/src/application/commands/mod.rs b/src/application/commands/mod.rs index 21f4af08..78fc4a96 100644 --- a/src/application/commands/mod.rs +++ b/src/application/commands/mod.rs @@ -10,6 +10,7 @@ //! ## Available Commands //! //! - `configure` - Infrastructure configuration and software installation +//! - `destroy` - Infrastructure destruction and teardown //! - `provision` - Infrastructure provisioning using `OpenTofu` //! - `test` - Deployment testing and validation //! @@ -17,9 +18,11 @@ //! error management, and coordination across multiple infrastructure services. pub mod configure; +pub mod destroy; pub mod provision; pub mod test; pub use configure::ConfigureCommand; +pub use destroy::DestroyCommand; pub use provision::ProvisionCommand; pub use test::TestCommand; diff --git a/src/application/steps/infrastructure/destroy.rs b/src/application/steps/infrastructure/destroy.rs new file mode 100644 index 00000000..2d898f65 --- /dev/null +++ b/src/application/steps/infrastructure/destroy.rs @@ -0,0 +1,128 @@ +//! `OpenTofu` infrastructure destruction step +//! +//! This module provides the `DestroyInfrastructureStep` which handles `OpenTofu` +//! destroy operations by executing `tofu destroy`. This step destroys all +//! infrastructure resources managed by the `OpenTofu` configuration. +//! +//! ## Key Features +//! +//! - Infrastructure teardown and resource destruction +//! - Configurable auto-approval for automation scenarios +//! - Progress tracking and status reporting +//! - Integration with `OpenTofuClient` for command execution +//! +//! ## Destroy Process +//! +//! The step executes `tofu destroy` which: +//! - Destroys all resources managed by the `OpenTofu` state +//! - Manages resource dependencies and proper destruction order +//! - Removes infrastructure state after successful destruction +//! - Provides detailed progress and completion status +//! +//! This step is where actual infrastructure destruction occurs. + +use std::sync::Arc; + +use tracing::{info, instrument}; + +use crate::adapters::tofu::client::OpenTofuClient; +use crate::shared::command::CommandError; + +/// Simple step that destroys `OpenTofu` infrastructure by executing `tofu destroy` +pub struct DestroyInfrastructureStep { + opentofu_client: Arc, + auto_approve: bool, +} + +impl DestroyInfrastructureStep { + #[must_use] + pub fn new(opentofu_client: Arc) -> Self { + Self { + opentofu_client, + auto_approve: true, // Default to auto-approve for automation + } + } + + #[must_use] + pub fn with_auto_approve(mut self, auto_approve: bool) -> Self { + self.auto_approve = auto_approve; + self + } + + /// Execute the `OpenTofu` destroy step + /// + /// # Errors + /// + /// Returns an error if: + /// * The `OpenTofu` destroy fails + /// * The working directory does not exist or is not accessible + /// * The `OpenTofu` command execution fails + #[instrument( + name = "destroy_infrastructure", + skip_all, + fields( + step_type = "infrastructure", + operation = "destroy", + auto_approve = %self.auto_approve + ) + )] + pub fn execute(&self) -> Result<(), CommandError> { + info!( + step = "destroy_infrastructure", + auto_approve = self.auto_approve, + "Destroying OpenTofu infrastructure" + ); + + // Execute tofu destroy command with variables file + let output = self + .opentofu_client + .destroy(self.auto_approve, &["-var-file=variables.tfvars"])?; + + info!( + step = "destroy_infrastructure", + status = "success", + "OpenTofu infrastructure destroyed successfully" + ); + + // Log output for debugging if needed + tracing::debug!(output = %output, "OpenTofu destroy output"); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use crate::adapters::tofu::client::OpenTofuClient; + + use super::*; + + #[test] + fn it_should_create_destroy_infrastructure_step() { + let opentofu_client = Arc::new(OpenTofuClient::new("/tmp")); + + let _step = DestroyInfrastructureStep::new(opentofu_client); + + // If we reach this point, the step was created successfully + } + + #[test] + fn it_should_create_step_with_custom_auto_approve() { + let opentofu_client = Arc::new(OpenTofuClient::new("/tmp")); + + let step = DestroyInfrastructureStep::new(opentofu_client).with_auto_approve(false); + + assert!(!step.auto_approve); + } + + #[test] + fn it_should_default_to_auto_approve() { + let opentofu_client = Arc::new(OpenTofuClient::new("/tmp")); + + let step = DestroyInfrastructureStep::new(opentofu_client); + + assert!(step.auto_approve); + } +} diff --git a/src/application/steps/infrastructure/mod.rs b/src/application/steps/infrastructure/mod.rs index e0e25a99..7b0483af 100644 --- a/src/application/steps/infrastructure/mod.rs +++ b/src/application/steps/infrastructure/mod.rs @@ -1,14 +1,16 @@ //! Infrastructure lifecycle management steps //! //! This module contains steps that manage infrastructure lifecycle using `OpenTofu` -//! (Terraform). These steps handle the complete infrastructure provisioning workflow -//! including initialization, planning, application, and information retrieval. +//! (Terraform). These steps handle the complete infrastructure provisioning and +//! destruction workflow including initialization, planning, application, destruction, +//! and information retrieval. //! //! ## Available Steps //! //! - `initialize` - `OpenTofu` initialization (tofu init) //! - `plan` - Infrastructure planning and change preview (tofu plan) //! - `apply` - Infrastructure provisioning and application (tofu apply) +//! - `destroy` - Infrastructure destruction and teardown (tofu destroy) //! - `get_instance_info` - Instance information retrieval from state //! //! ## Key Features @@ -19,15 +21,17 @@ //! - Integration with template rendering and configuration systems //! //! These steps provide the core infrastructure management capabilities for -//! provisioning and managing deployment environments. +//! provisioning, destroying, and managing deployment environments. pub mod apply; +pub mod destroy; pub mod get_instance_info; pub mod initialize; pub mod plan; pub mod validate; pub use apply::ApplyInfrastructureStep; +pub use destroy::DestroyInfrastructureStep; pub use get_instance_info::GetInstanceInfoStep; pub use initialize::InitializeInfrastructureStep; pub use plan::PlanInfrastructureStep; diff --git a/src/application/steps/mod.rs b/src/application/steps/mod.rs index b63bb7db..9cd8cf17 100644 --- a/src/application/steps/mod.rs +++ b/src/application/steps/mod.rs @@ -29,8 +29,8 @@ pub mod validation; // Re-export all steps for easy access pub use connectivity::WaitForSSHConnectivityStep; pub use infrastructure::{ - ApplyInfrastructureStep, GetInstanceInfoStep, InitializeInfrastructureStep, - PlanInfrastructureStep, ValidateInfrastructureStep, + ApplyInfrastructureStep, DestroyInfrastructureStep, GetInstanceInfoStep, + InitializeInfrastructureStep, PlanInfrastructureStep, ValidateInfrastructureStep, }; pub use rendering::{ RenderAnsibleTemplatesError, RenderAnsibleTemplatesStep, RenderOpenTofuTemplatesStep, diff --git a/src/bin/e2e_tests_full.rs b/src/bin/e2e_tests_full.rs index 51db23d8..9a3b79e8 100644 --- a/src/bin/e2e_tests_full.rs +++ b/src/bin/e2e_tests_full.rs @@ -68,9 +68,8 @@ use torrust_tracker_deployer_lib::testing::e2e::tasks::{ run_configure_command::run_configure_command, run_test_command::run_test_command, virtual_machine::{ - cleanup_infrastructure::cleanup_test_infrastructure, preflight_cleanup::preflight_cleanup_previous_resources, - run_provision_command::run_provision_command, + run_destroy_command::run_destroy_command, run_provision_command::run_provision_command, }, }; @@ -164,9 +163,10 @@ pub async fn main() -> Result<()> { Err(_) => Ok(()), // Skip validation if deployment failed }; - // Always cleanup test infrastructure created during this test run + // Always cleanup test infrastructure created during this test run using DestroyCommand // This ensures proper resource cleanup regardless of test success or failure - cleanup_test_infrastructure(&test_context); + // The keep_env flag is handled inside run_full_destroy_test + let destroy_result = run_full_destroy_test(&mut test_context); let test_duration = test_start.elapsed(); @@ -177,7 +177,28 @@ pub async fn main() -> Result<()> { "Test execution completed" ); - // Handle all 4 combinations of deployment and validation results + // Handle all combinations of deployment, validation, and destroy results + // Destroy failures are logged but don't override test results + match destroy_result { + Ok(()) => { + info!( + operation = "destroy", + status = "success", + "Infrastructure cleanup completed successfully" + ); + } + Err(destroy_err) => { + error!( + operation = "destroy", + status = "failed", + error = %destroy_err, + "Infrastructure cleanup failed" + ); + // Note: We don't fail the overall test just because cleanup failed + // The test results are more important than cleanup results + } + } + match (deployment_result, validation_result) { (Ok(()), Ok(())) => { info!( @@ -243,3 +264,20 @@ async fn run_full_deployment_test(test_context: &mut TestContext) -> Result<()> Ok(()) } + +fn run_full_destroy_test(test_context: &mut TestContext) -> Result<()> { + info!( + test_type = "full_destroy", + workflow = "template_based", + "Starting full destroy E2E test" + ); + + // Call the new run_destroy_command function + run_destroy_command(test_context).map_err(|e| anyhow::anyhow!("{e}"))?; + + info!( + status = "success", + "Infrastructure destruction completed successfully" + ); + Ok(()) +} diff --git a/src/testing/e2e/context.rs b/src/testing/e2e/context.rs index 6cf74eea..3bcc0859 100644 --- a/src/testing/e2e/context.rs +++ b/src/testing/e2e/context.rs @@ -391,6 +391,34 @@ impl TestContext { self.environment = configured_env.into_any(); } + /// Updates the test context environment from a destroyed environment + /// + /// This method updates the internal environment state after destruction + /// completes, ensuring the `TestContext` maintains the latest and accurate environment state. + /// + /// # Arguments + /// + /// * `destroyed_env` - The destroyed environment returned by `DestroyCommand` + /// + /// # Examples + /// + /// ```rust,no_run + /// # use torrust_tracker_deployer_lib::testing::e2e::context::TestContext; + /// # use torrust_tracker_deployer_lib::domain::Environment; + /// # fn example(test_context: &mut TestContext, destroyed_env: Environment) { + /// // After destruction succeeds, update the test context + /// test_context.update_from_destroyed(destroyed_env); + /// # } + /// ``` + pub fn update_from_destroyed( + &mut self, + destroyed_env: crate::domain::Environment, + ) { + // Replace the environment with the destroyed state using type erasure + // This properly represents the actual state (Destroyed) rather than keeping it in previous state + self.environment = destroyed_env.into_any(); + } + /// Creates a repository for the current environment /// /// This is a convenience method that creates an `EnvironmentRepository` diff --git a/src/testing/e2e/tasks/virtual_machine/mod.rs b/src/testing/e2e/tasks/virtual_machine/mod.rs index 39c085da..b56c1f74 100644 --- a/src/testing/e2e/tasks/virtual_machine/mod.rs +++ b/src/testing/e2e/tasks/virtual_machine/mod.rs @@ -7,4 +7,5 @@ pub mod cleanup_infrastructure; pub mod preflight_cleanup; +pub mod run_destroy_command; pub mod run_provision_command; diff --git a/src/testing/e2e/tasks/virtual_machine/run_destroy_command.rs b/src/testing/e2e/tasks/virtual_machine/run_destroy_command.rs new file mode 100644 index 00000000..bcf9c95e --- /dev/null +++ b/src/testing/e2e/tasks/virtual_machine/run_destroy_command.rs @@ -0,0 +1,170 @@ +//! Infrastructure destruction task for E2E testing +//! +//! This module provides the E2E testing task for destroying infrastructure using +//! the `DestroyCommand`. It orchestrates the complete infrastructure teardown workflow +//! through the application layer command. +//! +//! ## Key Operations +//! +//! - Execute infrastructure destruction via `DestroyCommand` +//! - Destroy infrastructure using `OpenTofu` operations +//! - Transition environment to `Destroyed` state +//! - Update test context with final state +//! +//! ## Integration +//! +//! This task is typically the final step in E2E testing workflows, cleaning up +//! all provisioned infrastructure after tests complete. + +use std::sync::Arc; +use thiserror::Error; +use tracing::info; + +use crate::application::commands::destroy::DestroyCommandError; +use crate::application::commands::DestroyCommand; +use crate::testing::e2e::context::TestContext; + +/// Errors that can occur during the destroy task +#[derive(Debug, Error)] +pub enum DestroyTaskError { + /// Destruction command execution failed + #[error( + "Failed to destroy infrastructure: {source} +Tip: Check OpenTofu logs in the build directory for detailed error information" + )] + DestructionFailed { + #[source] + source: DestroyCommandError, + }, +} + +impl DestroyTaskError { + /// Get detailed troubleshooting guidance for this error + /// + /// This method provides comprehensive troubleshooting steps that can be + /// displayed to users when they need more help resolving the error. + /// + /// # Example + /// + /// ```rust,no_run + /// # use torrust_tracker_deployer_lib::testing::e2e::tasks::virtual_machine::run_destroy_command::DestroyTaskError; + /// # use torrust_tracker_deployer_lib::application::commands::destroy::DestroyCommandError; + /// # use torrust_tracker_deployer_lib::shared::command::CommandError; + /// let error = DestroyTaskError::DestructionFailed { + /// source: DestroyCommandError::Command(CommandError::StartupFailed { + /// command: "tofu".to_string(), + /// source: std::io::Error::new(std::io::ErrorKind::NotFound, "test"), + /// }), + /// }; + /// println!("{}", error.help()); + /// ``` + #[must_use] + pub fn help(&self) -> &'static str { + match self { + Self::DestructionFailed { .. } => { + "Destruction Failed - Detailed Troubleshooting: + +1. Check OpenTofu logs in the build directory: + - Review terraform.log for detailed error messages + - Look for resource deletion conflicts or permission errors + +2. Verify infrastructure state: + - Ensure infrastructure resources still exist + - Check that OpenTofu state files are intact + - Verify network connectivity to infrastructure providers + +3. Check for resource locks: + - Ensure no other processes are accessing the resources + - Verify that no manual holds exist on resources + - Check for dependency issues preventing deletion + +4. Manual cleanup may be required if destroy fails: + - Review OpenTofu state to identify remaining resources + - Use provider-specific tools (e.g., lxc commands) for manual cleanup + - Remove state files after manual cleanup is complete + +For more information, see docs/e2e-testing.md and docs/vm-providers.md." + } + } + } +} + +/// Destroy infrastructure using `DestroyCommand` +/// +/// This function updates the `TestContext`'s internal environment to reflect the +/// destroyed state, ensuring consistency throughout the test lifecycle. Callers +/// can access the destroyed environment through the `TestContext`. +/// +/// If the `keep_env` flag is set in the test context, this function will skip +/// destruction and preserve the environment for debugging purposes. +/// +/// # Errors +/// +/// Returns an error if: +/// - `DestroyCommand` execution fails +/// - Infrastructure destruction fails +/// - `OpenTofu` destroy operations fail +pub fn run_destroy_command(test_context: &mut TestContext) -> Result<(), DestroyTaskError> { + use crate::domain::environment::state::AnyEnvironmentState; + + // If keep_env is set, skip destruction and preserve the environment + if test_context.keep_env { + let instance_name = &test_context.environment.instance_name(); + info!( + operation = "destroy", + action = "keep_environment", + instance = %instance_name, + connect_command = format!("lxc exec {} -- /bin/bash", instance_name), + "Keeping test environment as requested (destruction skipped)" + ); + return Ok(()); + } + + info!("Destroying test infrastructure"); + + // Create repository for this environment + let repository = test_context.create_repository(); + + // Use the new DestroyCommand to handle all infrastructure destruction steps + let destroy_command = DestroyCommand::new( + Arc::clone(&test_context.services.opentofu_client), + repository, + ); + + // Execute destruction with environment (can be in any state) + // The DestroyCommand accepts Environment generically, so we need to extract + // the environment from AnyEnvironmentState. Since destroy works on any state, + // we handle the special case of already-destroyed environments. + + let destroyed_env = match test_context.environment.clone() { + AnyEnvironmentState::Destroyed(env) => { + // Already destroyed, just return it + info!("Environment is already in Destroyed state"); + Ok(env) + } + AnyEnvironmentState::Created(env) => destroy_command.execute(env), + AnyEnvironmentState::Provisioning(env) => destroy_command.execute(env), + AnyEnvironmentState::Provisioned(env) => destroy_command.execute(env), + AnyEnvironmentState::Configuring(env) => destroy_command.execute(env), + AnyEnvironmentState::Configured(env) => destroy_command.execute(env), + AnyEnvironmentState::Releasing(env) => destroy_command.execute(env), + AnyEnvironmentState::Released(env) => destroy_command.execute(env), + AnyEnvironmentState::Running(env) => destroy_command.execute(env), + AnyEnvironmentState::ProvisionFailed(env) => destroy_command.execute(env), + AnyEnvironmentState::ConfigureFailed(env) => destroy_command.execute(env), + AnyEnvironmentState::ReleaseFailed(env) => destroy_command.execute(env), + AnyEnvironmentState::RunFailed(env) => destroy_command.execute(env), + } + .map_err(|source| DestroyTaskError::DestructionFailed { source })?; + + info!( + status = "complete", + environment = %destroyed_env.name(), + "Infrastructure destroyed successfully" + ); + + // Update the test context with the destroyed environment state + test_context.update_from_destroyed(destroyed_env); + + Ok(()) +}