Skip to main content

torrust_tracker_deployer_lib/domain/environment/state/
configuring.rs

1//! Configuring State
2//!
3//! Intermediate state - Application configuration in progress
4//!
5//! The environment is being configured with application-specific settings
6//! (Ansible playbooks, configuration files, etc.).
7//!
8//! **Valid Transitions:**
9//! - Success: `Configured`
10//! - Failure: `ConfigureFailed`
11
12use serde::{Deserialize, Serialize};
13
14use crate::domain::environment::state::{
15    AnyEnvironmentState, ConfigureFailed, Configured, StateTypeError,
16};
17use crate::domain::environment::Environment;
18
19/// Intermediate state - Application configuration in progress
20///
21/// The environment is being configured with application-specific settings
22/// (Ansible playbooks, configuration files, etc.).
23///
24/// **Valid Transitions:**
25/// - Success: `Configured`
26/// - Failure: `ConfigureFailed`
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct Configuring;
29
30// State transition implementations
31impl Environment<Configuring> {
32    /// Transitions from Configuring to Configured state
33    ///
34    /// This method indicates that application configuration completed successfully.
35    #[must_use]
36    pub fn configured(self) -> Environment<Configured> {
37        self.with_state(Configured)
38    }
39
40    /// Transitions from Configuring to `ConfigureFailed` state
41    ///
42    /// This method indicates that application configuration 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 configure_failed(
47        self,
48        context: crate::domain::environment::state::ConfigureFailureContext,
49    ) -> Environment<ConfigureFailed> {
50        self.with_state(ConfigureFailed { context })
51    }
52}
53
54// Type Erasure: Typed → Runtime conversion (into_any)
55impl Environment<Configuring> {
56    /// Converts typed `Environment<Configuring>` into type-erased `AnyEnvironmentState`
57    #[must_use]
58    pub fn into_any(self) -> AnyEnvironmentState {
59        AnyEnvironmentState::Configuring(self)
60    }
61}
62
63// Type Restoration: Runtime → Typed conversion (try_into_configuring)
64impl AnyEnvironmentState {
65    /// Attempts to convert `AnyEnvironmentState` to `Environment<Configuring>`
66    ///
67    /// # Errors
68    ///
69    /// Returns `StateTypeError::UnexpectedState` if the environment is not in `Configuring` state.
70    pub fn try_into_configuring(self) -> Result<Environment<Configuring>, StateTypeError> {
71        match self {
72            Self::Configuring(env) => Ok(env),
73            other => Err(StateTypeError::UnexpectedState {
74                expected: "configuring",
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_configuring_state() {
87        let state = Configuring;
88        assert_eq!(state, Configuring);
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_configuring() -> Environment<Configuring> {
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_configuring()
133        }
134
135        #[test]
136        fn it_should_convert_configuring_environment_into_any() {
137            let env = create_test_environment_configuring();
138            let any_env = env.into_any();
139            assert!(matches!(any_env, AnyEnvironmentState::Configuring(_)));
140        }
141
142        #[test]
143        fn it_should_convert_any_to_configuring_successfully() {
144            let env = create_test_environment_configuring();
145            let any_env = env.into_any();
146            let result = any_env.try_into_configuring();
147            assert!(result.is_ok());
148        }
149    }
150
151    mod transition_tests {
152        use super::*;
153        use crate::adapters::ssh::SshCredentials;
154        use crate::domain::environment::name::EnvironmentName;
155        use crate::domain::environment::runtime_outputs::ProvisionMethod;
156        use crate::domain::environment::state::Configured;
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_environment() -> Environment<Configuring> {
170            let env_name = EnvironmentName::new("test-state".to_string()).unwrap();
171            let ssh_username = Username::new("torrust".to_string()).unwrap();
172            let ssh_credentials = SshCredentials::new(
173                PathBuf::from("test_key"),
174                PathBuf::from("test_key.pub"),
175                ssh_username,
176            );
177            Environment::new(
178                env_name.clone(),
179                default_lxd_provider_config(&env_name),
180                ssh_credentials,
181                22,
182                chrono::Utc::now(),
183            )
184            .start_provisioning()
185            .provisioned(
186                IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
187                ProvisionMethod::Provisioned,
188            )
189            .start_configuring()
190        }
191
192        #[test]
193        fn it_should_transition_from_configuring_to_configured() {
194            let env = create_test_environment();
195            let env = env.configured();
196
197            assert_eq!(*env.state(), Configured);
198            assert_eq!(env.name().as_str(), "test-state");
199        }
200
201        #[test]
202        fn it_should_transition_from_configuring_to_configure_failed() {
203            use crate::domain::environment::state::{
204                BaseFailureContext, ConfigureFailureContext, ConfigureStep,
205            };
206            use crate::domain::environment::TraceId;
207            use crate::shared::ErrorKind;
208            use chrono::Utc;
209            use std::time::Duration;
210
211            let env = create_test_environment();
212            let context = ConfigureFailureContext {
213                failed_step: ConfigureStep::InstallDocker,
214                error_kind: ErrorKind::CommandExecution,
215                base: BaseFailureContext {
216                    error_summary: "ansible_playbook_error".to_string(),
217                    failed_at: Utc::now(),
218                    execution_started_at: Utc::now(),
219                    execution_duration: Duration::from_secs(15),
220                    trace_id: TraceId::new(),
221                    trace_file_path: None,
222                },
223            };
224            let env = env.configure_failed(context.clone());
225
226            assert_eq!(
227                env.state().context.failed_step,
228                ConfigureStep::InstallDocker
229            );
230            assert_eq!(env.name().as_str(), "test-state");
231        }
232    }
233}