Skip to main content

torrust_tracker_deployer_lib/domain/environment/state/
configure_failed.rs

1//! `ConfigureFailed` State
2//!
3//! Error state - Application configuration failed
4//!
5//! The configuration command failed during execution. The `context` field
6//! contains structured error information including the failed step, error kind,
7//! timing information, and a reference to the detailed trace file.
8//!
9//! **Recovery Options:**
10//! - Destroy and recreate the environment
11//! - Manual configuration correction (advanced users)
12//! - Review trace file for detailed error information
13
14use serde::{Deserialize, Serialize};
15
16use crate::domain::environment::state::{AnyEnvironmentState, BaseFailureContext, StateTypeError};
17use crate::domain::environment::Environment;
18use crate::shared::ErrorKind;
19
20// ============================================================================
21// Configure Command Error Context
22// ============================================================================
23
24/// Error context for configure command failures
25///
26/// Captures comprehensive information about configuration failures including
27/// the specific step that failed, error classification, timing, and trace details.
28#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
29pub struct ConfigureFailureContext {
30    /// Which step failed during configuration
31    pub failed_step: ConfigureStep,
32
33    /// Error category for type-safe handling
34    pub error_kind: ErrorKind,
35
36    /// Base failure context with common fields
37    #[serde(flatten)]
38    pub base: BaseFailureContext,
39}
40
41/// Steps in the configure workflow
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43pub enum ConfigureStep {
44    /// Installing Docker
45    InstallDocker,
46    /// Installing Docker Compose
47    InstallDockerCompose,
48    /// Configuring automatic security updates
49    ConfigureSecurityUpdates,
50    /// Configuring UFW firewall (SSH access only)
51    ConfigureFirewall,
52}
53
54/// Error state - Application configuration failed
55///
56/// The configuration command failed during execution. The `context` field
57/// contains structured error information including the failed step, error kind,
58/// timing information, and a reference to the detailed trace file.
59///
60/// **Recovery Options:**
61/// - Destroy and recreate the environment
62/// - Manual configuration correction (advanced users)
63/// - Review trace file for detailed error information
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65pub struct ConfigureFailed {
66    /// Structured error context with detailed failure information
67    pub context: ConfigureFailureContext,
68}
69
70// Type Erasure: Typed → Runtime conversion (into_any)
71impl Environment<ConfigureFailed> {
72    /// Converts typed `Environment<ConfigureFailed>` into type-erased `AnyEnvironmentState`
73    #[must_use]
74    pub fn into_any(self) -> AnyEnvironmentState {
75        AnyEnvironmentState::ConfigureFailed(self)
76    }
77}
78
79// Type Restoration: Runtime → Typed conversion (try_into_configure_failed)
80impl AnyEnvironmentState {
81    /// Attempts to convert `AnyEnvironmentState` to `Environment<ConfigureFailed>`
82    ///
83    /// # Errors
84    ///
85    /// Returns `StateTypeError::UnexpectedState` if the environment is not in `ConfigureFailed` state.
86    pub fn try_into_configure_failed(self) -> Result<Environment<ConfigureFailed>, StateTypeError> {
87        match self {
88            Self::ConfigureFailed(env) => Ok(env),
89            other => Err(StateTypeError::UnexpectedState {
90                expected: "configure_failed",
91                actual: other.state_name().to_string(),
92            }),
93        }
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::domain::environment::TraceId;
101    use chrono::Utc;
102    use std::time::Duration;
103
104    fn create_test_context() -> ConfigureFailureContext {
105        ConfigureFailureContext {
106            failed_step: ConfigureStep::InstallDocker,
107            error_kind: ErrorKind::CommandExecution,
108            base: BaseFailureContext {
109                error_summary: "Docker installation failed".to_string(),
110                failed_at: Utc::now(),
111                execution_started_at: Utc::now(),
112                execution_duration: Duration::from_secs(15),
113                trace_id: TraceId::new(),
114                trace_file_path: None,
115            },
116        }
117    }
118
119    #[test]
120    fn it_should_create_configure_failed_state_with_context() {
121        let context = create_test_context();
122        let state = ConfigureFailed {
123            context: context.clone(),
124        };
125        assert_eq!(state.context.failed_step, ConfigureStep::InstallDocker);
126        assert_eq!(state.context.error_kind, ErrorKind::CommandExecution);
127    }
128
129    #[test]
130    fn it_should_serialize_configure_failed_state_to_json() {
131        let state = ConfigureFailed {
132            context: create_test_context(),
133        };
134        let json = serde_json::to_string(&state).unwrap();
135        assert!(json.contains("InstallDocker"));
136        assert!(json.contains("CommandExecution"));
137    }
138
139    #[test]
140    fn it_should_deserialize_configure_failed_state_from_json() {
141        let state = ConfigureFailed {
142            context: create_test_context(),
143        };
144        let json = serde_json::to_string(&state).unwrap();
145        let deserialized: ConfigureFailed = serde_json::from_str(&json).unwrap();
146        assert_eq!(
147            deserialized.context.failed_step,
148            ConfigureStep::InstallDocker
149        );
150    }
151
152    mod conversion_tests {
153        use super::*;
154        use crate::adapters::ssh::SshCredentials;
155        use crate::domain::environment::name::EnvironmentName;
156        use crate::domain::environment::runtime_outputs::ProvisionMethod;
157        use crate::domain::provider::{LxdConfig, ProviderConfig};
158        use crate::domain::ProfileName;
159        use crate::shared::Username;
160        use std::net::{IpAddr, Ipv4Addr};
161        use std::path::PathBuf;
162
163        fn default_lxd_provider_config(env_name: &EnvironmentName) -> ProviderConfig {
164            ProviderConfig::Lxd(LxdConfig {
165                profile_name: ProfileName::new(format!("lxd-{}", env_name.as_str())).unwrap(),
166            })
167        }
168
169        fn create_test_ssh_credentials() -> SshCredentials {
170            let username = Username::new("test-user".to_string()).unwrap();
171            SshCredentials::new(
172                PathBuf::from("/tmp/test_key"),
173                PathBuf::from("/tmp/test_key.pub"),
174                username,
175            )
176        }
177
178        fn create_test_environment_configure_failed() -> Environment<ConfigureFailed> {
179            let name = EnvironmentName::new("test-env".to_string()).unwrap();
180            let ssh_creds = create_test_ssh_credentials();
181            Environment::new(
182                name.clone(),
183                default_lxd_provider_config(&name),
184                ssh_creds,
185                22,
186                chrono::Utc::now(),
187            )
188            .start_provisioning()
189            .provisioned(
190                IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
191                ProvisionMethod::Provisioned,
192            )
193            .start_configuring()
194            .configure_failed(super::create_test_context())
195        }
196
197        #[test]
198        fn it_should_convert_configure_failed_environment_into_any() {
199            let env = create_test_environment_configure_failed();
200            let any_env = env.into_any();
201            assert!(matches!(any_env, AnyEnvironmentState::ConfigureFailed(_)));
202        }
203
204        #[test]
205        fn it_should_convert_any_to_configure_failed_successfully() {
206            let env = create_test_environment_configure_failed();
207            let any_env = env.into_any();
208            let result = any_env.try_into_configure_failed();
209            assert!(result.is_ok());
210        }
211    }
212
213    mod context_tests {
214        use super::*;
215
216        #[test]
217        fn it_should_serialize_configure_failure_context() {
218            let context = ConfigureFailureContext {
219                failed_step: ConfigureStep::InstallDocker,
220                error_kind: ErrorKind::CommandExecution,
221                base: BaseFailureContext {
222                    error_summary: "Docker installation failed".to_string(),
223                    failed_at: Utc::now(),
224                    execution_started_at: Utc::now(),
225                    execution_duration: Duration::from_secs(15),
226                    trace_id: TraceId::new(),
227                    trace_file_path: None,
228                },
229            };
230
231            let json = serde_json::to_string(&context).unwrap();
232            assert!(json.contains("InstallDocker"));
233            assert!(json.contains("CommandExecution"));
234        }
235
236        #[test]
237        fn it_should_deserialize_configure_failure_context() {
238            let trace_id = TraceId::new();
239            let json = format!(
240                r#"{{
241                    "failed_step": "InstallDockerCompose",
242                    "error_kind": "CommandExecution",
243                    "error_summary": "Command execution failed",
244                    "failed_at": "2025-10-06T10:00:00Z",
245                    "execution_started_at": "2025-10-06T09:59:30Z",
246                    "execution_duration": {{"secs": 30, "nanos": 0}},
247                    "trace_id": "{trace_id}",
248                    "trace_file_path": null
249                }}"#
250            );
251
252            let context: ConfigureFailureContext = serde_json::from_str(&json).unwrap();
253            assert_eq!(context.failed_step, ConfigureStep::InstallDockerCompose);
254            assert_eq!(context.error_kind, ErrorKind::CommandExecution);
255        }
256    }
257}