Skip to main content

torrust_tracker_deployer_lib/domain/environment/state/
destroying.rs

1//! Destroying State
2//!
3//! Intermediate state - Infrastructure destruction in progress
4//!
5//! The environment is actively being destroyed (VM deletion, resource cleanup, etc.).
6//! This state indicates that the destroy command has started but not yet completed.
7//!
8//! **Valid Transitions:**
9//! - Success: `Destroyed`
10//! - Failure: `DestroyFailed`
11
12use serde::{Deserialize, Serialize};
13
14use crate::domain::environment::state::{
15    AnyEnvironmentState, DestroyFailed, Destroyed, StateTypeError,
16};
17use crate::domain::environment::Environment;
18
19/// Intermediate state - Infrastructure destruction in progress
20///
21/// The environment is actively being destroyed (VM deletion, resource cleanup, etc.).
22/// This state indicates that the destroy command has started but not yet completed.
23///
24/// **Valid Transitions:**
25/// - Success: `Destroyed`
26/// - Failure: `DestroyFailed`
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct Destroying;
29
30// State transition implementations
31impl Environment<Destroying> {
32    /// Transitions from Destroying to Destroyed state
33    ///
34    /// This method indicates that infrastructure destruction completed successfully.
35    #[must_use]
36    pub fn destroyed(self) -> Environment<Destroyed> {
37        self.with_state(Destroyed)
38    }
39
40    /// Transitions from Destroying to `DestroyFailed` state
41    ///
42    /// This method indicates that infrastructure destruction failed.
43    /// The context parameter provides structured error information including
44    /// the failed step, error classification, and trace reference.
45    #[must_use]
46    pub fn destroy_failed(
47        self,
48        context: crate::domain::environment::state::DestroyFailureContext,
49    ) -> Environment<DestroyFailed> {
50        self.with_state(DestroyFailed { context })
51    }
52}
53
54// Type Erasure: Typed → Runtime conversion (into_any)
55impl Environment<Destroying> {
56    /// Converts typed `Environment<Destroying>` into type-erased `AnyEnvironmentState`
57    #[must_use]
58    pub fn into_any(self) -> AnyEnvironmentState {
59        AnyEnvironmentState::Destroying(self)
60    }
61}
62
63// Type Restoration: Runtime → Typed conversion (try_into_destroying)
64impl AnyEnvironmentState {
65    /// Attempts to convert `AnyEnvironmentState` to `Environment<Destroying>`
66    ///
67    /// # Errors
68    ///
69    /// Returns `StateTypeError::UnexpectedState` if the environment is not in `Destroying` state.
70    pub fn try_into_destroying(self) -> Result<Environment<Destroying>, StateTypeError> {
71        match self {
72            Self::Destroying(env) => Ok(env),
73            other => Err(StateTypeError::UnexpectedState {
74                expected: "destroying",
75                actual: other.state_name().to_string(),
76            }),
77        }
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn it_should_create_destroying_state() {
87        let state = Destroying;
88        assert_eq!(state, Destroying);
89    }
90
91    mod conversion_tests {
92        use super::*;
93        use crate::adapters::ssh::SshCredentials;
94        use crate::domain::environment::name::EnvironmentName;
95        use crate::domain::environment::runtime_outputs::ProvisionMethod;
96        use crate::domain::provider::{LxdConfig, ProviderConfig};
97        use crate::domain::ProfileName;
98        use crate::shared::Username;
99        use std::net::{IpAddr, Ipv4Addr};
100        use std::path::PathBuf;
101
102        fn default_lxd_provider_config(env_name: &EnvironmentName) -> ProviderConfig {
103            ProviderConfig::Lxd(LxdConfig {
104                profile_name: ProfileName::new(format!("lxd-{}", env_name.as_str())).unwrap(),
105            })
106        }
107
108        fn create_test_ssh_credentials() -> SshCredentials {
109            let username = Username::new("test-user".to_string()).unwrap();
110            SshCredentials::new(
111                PathBuf::from("/tmp/test_key"),
112                PathBuf::from("/tmp/test_key.pub"),
113                username,
114            )
115        }
116
117        fn create_test_environment_destroying() -> Environment<Destroying> {
118            let name = EnvironmentName::new("test-env".to_string()).unwrap();
119            let ssh_creds = create_test_ssh_credentials();
120            Environment::new(
121                name.clone(),
122                default_lxd_provider_config(&name),
123                ssh_creds,
124                22,
125                chrono::Utc::now(),
126            )
127            .start_provisioning()
128            .provisioned(
129                IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
130                ProvisionMethod::Provisioned,
131            )
132            .start_destroying()
133        }
134
135        #[test]
136        fn it_should_convert_destroying_environment_into_any() {
137            let env = create_test_environment_destroying();
138            let any_env = env.into_any();
139            assert!(matches!(any_env, AnyEnvironmentState::Destroying(_)));
140        }
141
142        #[test]
143        fn it_should_convert_any_to_destroying_successfully() {
144            let env = create_test_environment_destroying();
145            let any_env = env.into_any();
146            let result = any_env.try_into_destroying();
147            assert!(result.is_ok());
148        }
149
150        #[test]
151        fn it_should_fail_converting_created_to_destroying() {
152            let name = EnvironmentName::new("test-env".to_string()).unwrap();
153            let ssh_creds = create_test_ssh_credentials();
154            let env = Environment::new(
155                name.clone(),
156                default_lxd_provider_config(&name),
157                ssh_creds,
158                22,
159                chrono::Utc::now(),
160            );
161            let any_env = env.into_any();
162            let result = any_env.try_into_destroying();
163            assert!(result.is_err());
164            let err = result.unwrap_err();
165            assert!(err.to_string().contains("destroying"));
166            assert!(err.to_string().contains("created"));
167        }
168    }
169
170    mod state_transitions {
171        use super::super::*;
172        use crate::adapters::ssh::SshCredentials;
173        use crate::domain::environment::name::EnvironmentName;
174        use crate::domain::environment::runtime_outputs::ProvisionMethod;
175        use crate::domain::environment::state::Destroyed;
176        use crate::domain::provider::{LxdConfig, ProviderConfig};
177        use crate::domain::ProfileName;
178        use crate::shared::Username;
179        use std::net::{IpAddr, Ipv4Addr};
180        use std::path::PathBuf;
181
182        fn default_lxd_provider_config(env_name: &EnvironmentName) -> ProviderConfig {
183            ProviderConfig::Lxd(LxdConfig {
184                profile_name: ProfileName::new(format!("lxd-{}", env_name.as_str())).unwrap(),
185            })
186        }
187
188        fn create_test_ssh_credentials() -> SshCredentials {
189            let username = Username::new("test-user".to_string()).unwrap();
190            SshCredentials::new(
191                PathBuf::from("/tmp/test_key"),
192                PathBuf::from("/tmp/test_key.pub"),
193                username,
194            )
195        }
196
197        fn create_test_environment_destroying() -> Environment<Destroying> {
198            let name = EnvironmentName::new("test-env".to_string()).unwrap();
199            let ssh_creds = create_test_ssh_credentials();
200            Environment::new(
201                name.clone(),
202                default_lxd_provider_config(&name),
203                ssh_creds,
204                22,
205                chrono::Utc::now(),
206            )
207            .start_provisioning()
208            .provisioned(
209                IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
210                ProvisionMethod::Provisioned,
211            )
212            .start_destroying()
213        }
214
215        #[test]
216        fn it_should_transition_from_destroying_to_destroyed() {
217            let env = create_test_environment_destroying();
218            let env = env.destroyed();
219
220            assert_eq!(*env.state(), Destroyed);
221            assert_eq!(env.name().as_str(), "test-env");
222        }
223
224        #[test]
225        fn it_should_transition_from_destroying_to_destroy_failed() {
226            use crate::domain::environment::state::{
227                BaseFailureContext, DestroyFailureContext, DestroyStep,
228            };
229            use crate::domain::environment::TraceId;
230            use crate::shared::ErrorKind;
231            use chrono::Utc;
232            use std::time::Duration;
233
234            let env = create_test_environment_destroying();
235            let context = DestroyFailureContext {
236                failed_step: DestroyStep::DestroyInfrastructure,
237                error_kind: ErrorKind::InfrastructureOperation,
238                base: BaseFailureContext {
239                    error_summary: "infrastructure_destroy_failed".to_string(),
240                    failed_at: Utc::now(),
241                    execution_started_at: Utc::now(),
242                    execution_duration: Duration::from_secs(30),
243                    trace_id: TraceId::new(),
244                    trace_file_path: None,
245                },
246            };
247            let env = env.destroy_failed(context.clone());
248
249            assert_eq!(
250                env.state().context.failed_step,
251                DestroyStep::DestroyInfrastructure
252            );
253            assert_eq!(env.name().as_str(), "test-env");
254        }
255    }
256}