Skip to main content

torrust_tracker_deployer_lib/domain/environment/state/
provisioned.rs

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