Skip to main content

torrust_tracker_deployer_lib/domain/environment/state/
created.rs

1//! Created State
2//!
3//! Initial state - Environment has been created but no operations performed
4//!
5//! This is the entry state for all new environments. From this state, the
6//! environment can transition to:
7//! - `Provisioning` when infrastructure provisioning begins (standard path)
8//! - `Provisioned` directly via `register()` (alternative path for existing infrastructure)
9
10use std::net::IpAddr;
11
12use serde::{Deserialize, Serialize};
13
14use crate::domain::environment::runtime_outputs::ProvisionMethod;
15use crate::domain::environment::state::{
16    AnyEnvironmentState, Provisioned, Provisioning, StateTypeError,
17};
18use crate::domain::environment::Environment;
19
20/// Initial state - Environment has been created but no operations performed
21///
22/// This is the entry state for all new environments. From this state, the
23/// environment can transition to:
24/// - `Provisioning` when infrastructure provisioning begins (standard path)
25/// - `Provisioned` directly via `register()` (alternative path for existing infrastructure)
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct Created;
28
29// State transition implementations
30impl Environment<Created> {
31    /// Transitions from Created to Provisioning state
32    ///
33    /// This method consumes the environment and returns a new one in the
34    /// Provisioning state, indicating that infrastructure provisioning has begun.
35    #[must_use]
36    pub fn start_provisioning(self) -> Environment<Provisioning> {
37        self.with_state(Provisioning)
38    }
39
40    /// Registers an existing instance and transitions directly to Provisioned state
41    ///
42    /// This is an alternative to `start_provisioning()` for environments that will
43    /// use existing infrastructure instead of provisioning new infrastructure.
44    /// The provision method is set to `Registered` to distinguish from infrastructure
45    /// created via `OpenTofu`.
46    ///
47    /// # Arguments
48    ///
49    /// * `instance_ip` - The IP address of the existing instance
50    ///
51    /// # Returns
52    ///
53    /// Returns the environment in `Provisioned` state with:
54    /// - Instance IP set to the provided address
55    /// - Provision method set to `Registered`
56    #[must_use]
57    pub fn register(self, instance_ip: IpAddr) -> Environment<Provisioned> {
58        self.with_instance_ip(instance_ip)
59            .with_provision_method(ProvisionMethod::Registered)
60            .with_state(Provisioned)
61    }
62}
63
64// Type Erasure: Typed → Runtime conversion (into_any)
65impl Environment<Created> {
66    /// Converts typed `Environment<Created>` into type-erased `AnyEnvironmentState`
67    #[must_use]
68    pub fn into_any(self) -> AnyEnvironmentState {
69        AnyEnvironmentState::Created(self)
70    }
71}
72
73// Type Restoration: Runtime → Typed conversion (try_into_created)
74impl AnyEnvironmentState {
75    /// Attempts to convert `AnyEnvironmentState` to `Environment<Created>`
76    ///
77    /// # Errors
78    ///
79    /// Returns `StateTypeError::UnexpectedState` if the environment is not in `Created` state.
80    pub fn try_into_created(self) -> Result<Environment<Created>, StateTypeError> {
81        match self {
82            Self::Created(env) => Ok(env),
83            other => Err(StateTypeError::UnexpectedState {
84                expected: "created",
85                actual: other.state_name().to_string(),
86            }),
87        }
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn it_should_create_created_state() {
97        let state = Created;
98        assert_eq!(state, Created);
99    }
100
101    #[test]
102    fn it_should_clone_created_state() {
103        let state = Created;
104        let cloned = state.clone();
105        assert_eq!(state, cloned);
106    }
107
108    #[test]
109    fn it_should_serialize_created_state_to_json() {
110        let state = Created;
111        let json = serde_json::to_string(&state).unwrap();
112        assert_eq!(json, "null");
113    }
114
115    #[test]
116    fn it_should_deserialize_created_state_from_json() {
117        let json = "null";
118        let state: Created = serde_json::from_str(json).unwrap();
119        assert_eq!(state, Created);
120    }
121
122    #[test]
123    fn it_should_debug_format_created_state() {
124        let created = format!("{Created:?}");
125        assert_eq!(created, "Created");
126    }
127
128    mod conversion_tests {
129        use super::*;
130        use crate::adapters::ssh::SshCredentials;
131        use crate::domain::environment::name::EnvironmentName;
132        use crate::domain::provider::{LxdConfig, ProviderConfig};
133        use crate::domain::ProfileName;
134        use crate::shared::Username;
135        use std::path::PathBuf;
136
137        fn default_lxd_provider_config(env_name: &EnvironmentName) -> ProviderConfig {
138            ProviderConfig::Lxd(LxdConfig {
139                profile_name: ProfileName::new(format!("lxd-{}", env_name.as_str())).unwrap(),
140            })
141        }
142
143        fn create_test_ssh_credentials() -> SshCredentials {
144            let username = Username::new("test-user".to_string()).unwrap();
145            SshCredentials::new(
146                PathBuf::from("/tmp/test_key"),
147                PathBuf::from("/tmp/test_key.pub"),
148                username,
149            )
150        }
151
152        fn create_test_environment_created() -> Environment<Created> {
153            let name = EnvironmentName::new("test-env".to_string()).unwrap();
154            let ssh_creds = create_test_ssh_credentials();
155            Environment::new(
156                name.clone(),
157                default_lxd_provider_config(&name),
158                ssh_creds,
159                22,
160                chrono::Utc::now(),
161            )
162        }
163
164        #[test]
165        fn it_should_convert_created_environment_into_any() {
166            let env = create_test_environment_created();
167            let any_env = env.into_any();
168            assert!(matches!(any_env, AnyEnvironmentState::Created(_)));
169        }
170
171        #[test]
172        fn it_should_convert_any_to_created_successfully() {
173            let env = create_test_environment_created();
174            let any_env = env.into_any();
175            let result = any_env.try_into_created();
176            assert!(result.is_ok());
177        }
178
179        #[test]
180        fn it_should_fail_converting_provisioning_to_created() {
181            let env = create_test_environment_created().start_provisioning();
182            let any_env = env.into_any();
183            let result = any_env.try_into_created();
184            assert!(result.is_err());
185            let err = result.unwrap_err();
186            assert!(matches!(err, StateTypeError::UnexpectedState { .. }));
187            assert!(err.to_string().contains("created"));
188            assert!(err.to_string().contains("provisioning"));
189        }
190
191        #[test]
192        fn it_should_preserve_data_in_round_trip_conversion() {
193            let original_name = "test-env";
194            let env = create_test_environment_created();
195
196            // Round-trip: typed -> erased -> typed
197            let any_env = env.into_any();
198            let env_restored = any_env.try_into_created().unwrap();
199            let name_after = env_restored.name().as_str();
200
201            assert_eq!(name_after, original_name);
202        }
203    }
204
205    mod state_transitions {
206        use super::super::*;
207        use crate::adapters::ssh::SshCredentials;
208        use crate::domain::environment::name::EnvironmentName;
209        use crate::domain::environment::state::Provisioning;
210        use crate::domain::provider::{LxdConfig, ProviderConfig};
211        use crate::domain::ProfileName;
212        use crate::shared::Username;
213        use std::path::PathBuf;
214
215        fn default_lxd_provider_config(env_name: &EnvironmentName) -> ProviderConfig {
216            ProviderConfig::Lxd(LxdConfig {
217                profile_name: ProfileName::new(format!("lxd-{}", env_name.as_str())).unwrap(),
218            })
219        }
220
221        fn create_test_ssh_credentials() -> SshCredentials {
222            let username = Username::new("test-user".to_string()).unwrap();
223            SshCredentials::new(
224                PathBuf::from("/tmp/test_key"),
225                PathBuf::from("/tmp/test_key.pub"),
226                username,
227            )
228        }
229
230        fn create_test_environment_created() -> Environment<Created> {
231            let name = EnvironmentName::new("test-env".to_string()).unwrap();
232            let ssh_creds = create_test_ssh_credentials();
233            Environment::new(
234                name.clone(),
235                default_lxd_provider_config(&name),
236                ssh_creds,
237                22,
238                chrono::Utc::now(),
239            )
240        }
241
242        #[test]
243        fn it_should_transition_from_created_to_provisioning() {
244            let env = create_test_environment_created();
245            let env = env.start_provisioning();
246
247            // Verify we can access the state
248            assert_eq!(*env.state(), Provisioning);
249            // Verify other fields are preserved
250            assert_eq!(env.name().as_str(), "test-env");
251        }
252
253        #[test]
254        fn it_should_register_existing_instance_and_transition_to_provisioned() {
255            use std::net::{IpAddr, Ipv4Addr};
256
257            use crate::domain::environment::state::Provisioned;
258
259            let env = create_test_environment_created();
260            let instance_ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100));
261            let env = env.register(instance_ip);
262
263            // Verify state transition
264            assert_eq!(*env.state(), Provisioned);
265            // Verify instance IP is set
266            assert_eq!(env.instance_ip(), Some(instance_ip));
267            // Verify other fields are preserved
268            assert_eq!(env.name().as_str(), "test-env");
269        }
270
271        #[test]
272        fn it_should_register_with_ipv6_address() {
273            use std::net::{IpAddr, Ipv6Addr};
274
275            use crate::domain::environment::state::Provisioned;
276
277            let env = create_test_environment_created();
278            let instance_ip = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1));
279            let env = env.register(instance_ip);
280
281            // Verify state transition
282            assert_eq!(*env.state(), Provisioned);
283            // Verify instance IP is set
284            assert_eq!(env.instance_ip(), Some(instance_ip));
285        }
286    }
287}