Skip to main content

torrust_tracker_deployer_lib/domain/environment/state/
provisioning.rs

1//! Provisioning State
2//!
3//! Intermediate state - Infrastructure provisioning in progress
4//!
5//! The environment is actively being provisioned (VM creation, network setup, etc.).
6//! This state indicates that the provision command has started but not yet completed.
7//!
8//! **Valid Transitions:**
9//! - Success: `Provisioned`
10//! - Failure: `ProvisionFailed`
11
12use std::net::IpAddr;
13
14use serde::{Deserialize, Serialize};
15
16use crate::domain::environment::runtime_outputs::ProvisionMethod;
17use crate::domain::environment::state::{
18    AnyEnvironmentState, ProvisionFailed, Provisioned, StateTypeError,
19};
20use crate::domain::environment::Environment;
21
22/// Intermediate state - Infrastructure provisioning in progress
23///
24/// The environment is actively being provisioned (VM creation, network setup, etc.).
25/// This state indicates that the provision command has started but not yet completed.
26///
27/// **Valid Transitions:**
28/// - Success: `Provisioned`
29/// - Failure: `ProvisionFailed`
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct Provisioning;
32
33// State transition implementations
34impl Environment<Provisioning> {
35    /// Transitions from Provisioning to Provisioned state
36    ///
37    /// This method indicates that infrastructure provisioning completed successfully.
38    /// It requires the instance IP address and provision method as proof of successful
39    /// provisioning, ensuring that an `Environment<Provisioned>` always has this data.
40    ///
41    /// # Arguments
42    ///
43    /// * `instance_ip` - The IP address of the provisioned instance
44    /// * `provision_method` - How the instance was provisioned (always `Provisioned` for this transition)
45    ///
46    /// # Returns
47    ///
48    /// Returns the environment in `Provisioned` state with instance IP and provision method set
49    #[must_use]
50    pub fn provisioned(
51        self,
52        instance_ip: IpAddr,
53        provision_method: ProvisionMethod,
54    ) -> Environment<Provisioned> {
55        self.with_instance_ip(instance_ip)
56            .with_provision_method(provision_method)
57            .with_state(Provisioned)
58    }
59
60    /// Transitions from Provisioning to `ProvisionFailed` state
61    ///
62    /// This method indicates that infrastructure provisioning failed.
63    /// The context parameter provides structured error information including
64    /// the failed step, error classification, and trace reference.
65    #[must_use]
66    pub fn provision_failed(
67        self,
68        context: crate::domain::environment::state::ProvisionFailureContext,
69    ) -> Environment<ProvisionFailed> {
70        self.with_state(ProvisionFailed { context })
71    }
72}
73
74// Type Erasure: Typed → Runtime conversion (into_any)
75impl Environment<Provisioning> {
76    /// Converts typed `Environment<Provisioning>` into type-erased `AnyEnvironmentState`
77    #[must_use]
78    pub fn into_any(self) -> AnyEnvironmentState {
79        AnyEnvironmentState::Provisioning(self)
80    }
81}
82
83// Type Restoration: Runtime → Typed conversion (try_into_provisioning)
84impl AnyEnvironmentState {
85    /// Attempts to convert `AnyEnvironmentState` to `Environment<Provisioning>`
86    ///
87    /// # Errors
88    ///
89    /// Returns `StateTypeError::UnexpectedState` if the environment is not in `Provisioning` state.
90    pub fn try_into_provisioning(self) -> Result<Environment<Provisioning>, StateTypeError> {
91        match self {
92            Self::Provisioning(env) => Ok(env),
93            other => Err(StateTypeError::UnexpectedState {
94                expected: "provisioning",
95                actual: other.state_name().to_string(),
96            }),
97        }
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn it_should_create_provisioning_state() {
107        let state = Provisioning;
108        assert_eq!(state, Provisioning);
109    }
110
111    mod conversion_tests {
112        use super::*;
113        use crate::adapters::ssh::SshCredentials;
114        use crate::domain::environment::name::EnvironmentName;
115        use crate::domain::provider::{LxdConfig, ProviderConfig};
116        use crate::domain::ProfileName;
117        use crate::shared::Username;
118        use std::path::PathBuf;
119
120        fn default_lxd_provider_config(env_name: &EnvironmentName) -> ProviderConfig {
121            ProviderConfig::Lxd(LxdConfig {
122                profile_name: ProfileName::new(format!("lxd-{}", env_name.as_str())).unwrap(),
123            })
124        }
125
126        fn create_test_ssh_credentials() -> SshCredentials {
127            let username = Username::new("test-user".to_string()).unwrap();
128            SshCredentials::new(
129                PathBuf::from("/tmp/test_key"),
130                PathBuf::from("/tmp/test_key.pub"),
131                username,
132            )
133        }
134
135        fn create_test_environment_provisioning() -> Environment<Provisioning> {
136            let name = EnvironmentName::new("test-env".to_string()).unwrap();
137            let ssh_creds = create_test_ssh_credentials();
138            Environment::new(
139                name.clone(),
140                default_lxd_provider_config(&name),
141                ssh_creds,
142                22,
143                chrono::Utc::now(),
144            )
145            .start_provisioning()
146        }
147
148        #[test]
149        fn it_should_convert_provisioning_environment_into_any() {
150            let env = create_test_environment_provisioning();
151            let any_env = env.into_any();
152            assert!(matches!(any_env, AnyEnvironmentState::Provisioning(_)));
153        }
154
155        #[test]
156        fn it_should_convert_any_to_provisioning_successfully() {
157            let env = create_test_environment_provisioning();
158            let any_env = env.into_any();
159            let result = any_env.try_into_provisioning();
160            assert!(result.is_ok());
161        }
162
163        #[test]
164        fn it_should_fail_converting_created_to_provisioning() {
165            let name = EnvironmentName::new("test-env".to_string()).unwrap();
166            let ssh_creds = create_test_ssh_credentials();
167            let env = Environment::new(
168                name.clone(),
169                default_lxd_provider_config(&name),
170                ssh_creds,
171                22,
172                chrono::Utc::now(),
173            );
174            let any_env = env.into_any();
175            let result = any_env.try_into_provisioning();
176            assert!(result.is_err());
177            let err = result.unwrap_err();
178            assert!(err.to_string().contains("provisioning"));
179            assert!(err.to_string().contains("created"));
180        }
181    }
182
183    mod state_transitions {
184        use super::super::*;
185        use crate::adapters::ssh::SshCredentials;
186        use crate::domain::environment::name::EnvironmentName;
187        use crate::domain::environment::runtime_outputs::ProvisionMethod;
188        use crate::domain::environment::state::Provisioned;
189        use crate::domain::provider::{LxdConfig, ProviderConfig};
190        use crate::domain::ProfileName;
191        use crate::shared::Username;
192        use std::net::{IpAddr, Ipv4Addr};
193        use std::path::PathBuf;
194
195        fn default_lxd_provider_config(env_name: &EnvironmentName) -> ProviderConfig {
196            ProviderConfig::Lxd(LxdConfig {
197                profile_name: ProfileName::new(format!("lxd-{}", env_name.as_str())).unwrap(),
198            })
199        }
200
201        fn create_test_ssh_credentials() -> SshCredentials {
202            let username = Username::new("test-user".to_string()).unwrap();
203            SshCredentials::new(
204                PathBuf::from("/tmp/test_key"),
205                PathBuf::from("/tmp/test_key.pub"),
206                username,
207            )
208        }
209
210        fn create_test_environment_provisioning() -> Environment<Provisioning> {
211            let name = EnvironmentName::new("test-env".to_string()).unwrap();
212            let ssh_creds = create_test_ssh_credentials();
213            Environment::new(
214                name.clone(),
215                default_lxd_provider_config(&name),
216                ssh_creds,
217                22,
218                chrono::Utc::now(),
219            )
220            .start_provisioning()
221        }
222
223        #[test]
224        fn it_should_transition_from_provisioning_to_provisioned() {
225            let env = create_test_environment_provisioning();
226            let env = env.provisioned(
227                IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
228                ProvisionMethod::Provisioned,
229            );
230
231            assert_eq!(*env.state(), Provisioned);
232            assert_eq!(env.name().as_str(), "test-env");
233        }
234
235        #[test]
236        fn it_should_transition_from_provisioning_to_provision_failed() {
237            use crate::domain::environment::state::{
238                BaseFailureContext, ProvisionFailureContext, ProvisionStep,
239            };
240            use crate::domain::environment::TraceId;
241            use crate::shared::ErrorKind;
242            use chrono::Utc;
243            use std::time::Duration;
244
245            let env = create_test_environment_provisioning();
246            let context = ProvisionFailureContext {
247                failed_step: ProvisionStep::CloudInitWait,
248                error_kind: ErrorKind::Timeout,
249                base: BaseFailureContext {
250                    error_summary: "cloud_init_timeout".to_string(),
251                    failed_at: Utc::now(),
252                    execution_started_at: Utc::now(),
253                    execution_duration: Duration::from_secs(30),
254                    trace_id: TraceId::new(),
255                    trace_file_path: None,
256                },
257            };
258            let env = env.provision_failed(context.clone());
259
260            assert_eq!(
261                env.state().context.failed_step,
262                ProvisionStep::CloudInitWait
263            );
264            assert_eq!(env.name().as_str(), "test-env");
265        }
266    }
267}