Skip to main content

torrust_tracker_deployer_lib/domain/environment/state/
running.rs

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