Skip to main content

torrust_tracker_deployer_lib/domain/environment/state/
run_failed.rs

1//! `RunFailed` State
2//!
3//! Error state - Application runtime failed
4//!
5//! The run command failed during execution. The `context` field
6//! contains detailed information about the failure, including which step
7//! failed, error classification, and trace file location.
8//!
9//! **Recovery Options:**
10//! - Retry the run command
11//! - Destroy and recreate the environment
12
13use std::fmt;
14
15use serde::{Deserialize, Serialize};
16
17use crate::domain::environment::state::{AnyEnvironmentState, BaseFailureContext, StateTypeError};
18use crate::domain::environment::Environment;
19use crate::shared::error::ErrorKind;
20
21/// Steps in the run workflow
22///
23/// Each variant represents a distinct phase in the run process.
24/// This allows precise tracking of which step failed during run.
25///
26/// The run workflow follows the three-level architecture:
27/// - **Command** (Level 1): `RunCommandHandler` orchestrates the workflow
28/// - **Step** (Level 2): Individual steps like `StartServicesStep`
29/// - **Remote Action** (Level 3): Ansible playbooks execute on remote hosts
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum RunStep {
33    /// Starting Docker Compose services on the remote host
34    StartServices,
35}
36
37impl fmt::Display for RunStep {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        let name = match self {
40            Self::StartServices => "Start Services",
41        };
42        write!(f, "{name}")
43    }
44}
45
46/// Structured failure context for run command errors
47///
48/// Contains comprehensive information about a run failure:
49/// - Which step failed
50/// - Error classification for recovery guidance
51/// - Base failure metadata (timing, trace ID, error summary)
52///
53/// This enables:
54/// - Accurate error reporting
55/// - Recovery suggestions based on the specific failure
56/// - Post-mortem analysis via trace files
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
58pub struct RunFailureContext {
59    /// The step that was executing when the failure occurred
60    pub failed_step: RunStep,
61
62    /// Classification of the error for recovery guidance
63    pub error_kind: ErrorKind,
64
65    /// Common failure metadata (timing, trace, error summary)
66    pub base: BaseFailureContext,
67}
68
69/// Error state - Application runtime failed
70///
71/// The run command failed during execution. The `context` field
72/// contains detailed information about the failure.
73///
74/// **Recovery Options:**
75/// - Retry the run command
76/// - Destroy and recreate the environment
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct RunFailed {
79    /// Structured failure context with step info, error classification, and trace
80    pub context: RunFailureContext,
81}
82
83// Type Erasure: Typed → Runtime conversion (into_any)
84impl Environment<RunFailed> {
85    /// Converts typed `Environment<RunFailed>` into type-erased `AnyEnvironmentState`
86    #[must_use]
87    pub fn into_any(self) -> AnyEnvironmentState {
88        AnyEnvironmentState::RunFailed(self)
89    }
90}
91
92// Type Restoration: Runtime → Typed conversion (try_into_run_failed)
93impl AnyEnvironmentState {
94    /// Attempts to convert `AnyEnvironmentState` to `Environment<RunFailed>`
95    ///
96    /// # Errors
97    ///
98    /// Returns `StateTypeError::UnexpectedState` if the environment is not in `RunFailed` state.
99    pub fn try_into_run_failed(self) -> Result<Environment<RunFailed>, StateTypeError> {
100        match self {
101            Self::RunFailed(env) => Ok(env),
102            other => Err(StateTypeError::UnexpectedState {
103                expected: "run_failed",
104                actual: other.state_name().to_string(),
105            }),
106        }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use std::time::Duration;
113
114    use chrono::Utc;
115
116    use super::*;
117    use crate::domain::environment::TraceId;
118
119    fn create_test_failure_context() -> RunFailureContext {
120        let now = Utc::now();
121        RunFailureContext {
122            failed_step: RunStep::StartServices,
123            error_kind: ErrorKind::InfrastructureOperation,
124            base: BaseFailureContext {
125                error_summary: "Test error".to_string(),
126                failed_at: now,
127                execution_started_at: now,
128                execution_duration: Duration::from_secs(10),
129                trace_id: TraceId::new(),
130                trace_file_path: None,
131            },
132        }
133    }
134
135    #[test]
136    fn it_should_create_run_failed_state_with_context() {
137        let context = create_test_failure_context();
138        let state = RunFailed {
139            context: context.clone(),
140        };
141        assert_eq!(state.context.failed_step, RunStep::StartServices);
142        assert_eq!(state.context.error_kind, ErrorKind::InfrastructureOperation);
143    }
144
145    #[test]
146    fn it_should_display_run_step() {
147        assert_eq!(format!("{}", RunStep::StartServices), "Start Services");
148    }
149
150    #[test]
151    fn it_should_serialize_run_step_to_snake_case() {
152        let step = RunStep::StartServices;
153        let json = serde_json::to_string(&step).unwrap();
154        assert_eq!(json, r#""start_services""#);
155    }
156
157    #[test]
158    fn it_should_deserialize_run_step_from_snake_case() {
159        let step: RunStep = serde_json::from_str(r#""start_services""#).unwrap();
160        assert_eq!(step, RunStep::StartServices);
161    }
162
163    mod conversion_tests {
164        use super::*;
165        use crate::adapters::ssh::SshCredentials;
166        use crate::domain::environment::name::EnvironmentName;
167        use crate::domain::environment::runtime_outputs::ProvisionMethod;
168        use crate::domain::provider::{LxdConfig, ProviderConfig};
169        use crate::domain::ProfileName;
170        use crate::shared::Username;
171        use std::net::{IpAddr, Ipv4Addr};
172        use std::path::PathBuf;
173
174        fn default_lxd_provider_config(env_name: &EnvironmentName) -> ProviderConfig {
175            ProviderConfig::Lxd(LxdConfig {
176                profile_name: ProfileName::new(format!("lxd-{}", env_name.as_str())).unwrap(),
177            })
178        }
179
180        fn create_test_ssh_credentials() -> SshCredentials {
181            let username = Username::new("test-user".to_string()).unwrap();
182            SshCredentials::new(
183                PathBuf::from("/tmp/test_key"),
184                PathBuf::from("/tmp/test_key.pub"),
185                username,
186            )
187        }
188
189        fn create_test_environment_run_failed() -> Environment<RunFailed> {
190            let name = EnvironmentName::new("test-env".to_string()).unwrap();
191            let ssh_creds = create_test_ssh_credentials();
192            let now = Utc::now();
193            let context = RunFailureContext {
194                failed_step: RunStep::StartServices,
195                error_kind: ErrorKind::InfrastructureOperation,
196                base: BaseFailureContext {
197                    error_summary: "Docker compose failed".to_string(),
198                    failed_at: now,
199                    execution_started_at: now,
200                    execution_duration: Duration::from_secs(5),
201                    trace_id: TraceId::new(),
202                    trace_file_path: None,
203                },
204            };
205            Environment::new(
206                name.clone(),
207                default_lxd_provider_config(&name),
208                ssh_creds,
209                22,
210                chrono::Utc::now(),
211            )
212            .start_provisioning()
213            .provisioned(
214                IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
215                ProvisionMethod::Provisioned,
216            )
217            .start_configuring()
218            .configured()
219            .start_releasing()
220            .released()
221            .start_running()
222            .run_failed(context)
223        }
224
225        #[test]
226        fn it_should_convert_run_failed_environment_into_any() {
227            let env = create_test_environment_run_failed();
228            let any_env = env.into_any();
229            assert!(matches!(any_env, AnyEnvironmentState::RunFailed(_)));
230        }
231
232        #[test]
233        fn it_should_convert_any_to_run_failed_successfully() {
234            let env = create_test_environment_run_failed();
235            let any_env = env.into_any();
236            let result = any_env.try_into_run_failed();
237            assert!(result.is_ok());
238        }
239    }
240}