Skip to main content

torrust_tracker_deployer_lib/domain/environment/state/
releasing.rs

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