torrust_tracker_deployer_lib/domain/environment/state/
provision_failed.rs1use serde::{Deserialize, Serialize};
15
16use crate::domain::environment::state::{AnyEnvironmentState, BaseFailureContext, StateTypeError};
17use crate::domain::environment::Environment;
18use crate::shared::ErrorKind;
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
29pub struct ProvisionFailureContext {
30 pub failed_step: ProvisionStep,
32
33 pub error_kind: ErrorKind,
35
36 #[serde(flatten)]
38 pub base: BaseFailureContext,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43pub enum ProvisionStep {
44 RenderOpenTofuTemplates,
46 OpenTofuInit,
48 OpenTofuValidate,
50 OpenTofuPlan,
52 OpenTofuApply,
54 GetInstanceInfo,
56 RenderAnsibleTemplates,
58 WaitSshConnectivity,
60 CloudInitWait,
62}
63
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub struct ProvisionFailed {
76 pub context: ProvisionFailureContext,
78}
79
80impl Environment<ProvisionFailed> {
82 #[must_use]
84 pub fn into_any(self) -> AnyEnvironmentState {
85 AnyEnvironmentState::ProvisionFailed(self)
86 }
87}
88
89impl AnyEnvironmentState {
91 pub fn try_into_provision_failed(self) -> Result<Environment<ProvisionFailed>, StateTypeError> {
97 match self {
98 Self::ProvisionFailed(env) => Ok(env),
99 other => Err(StateTypeError::UnexpectedState {
100 expected: "provision_failed",
101 actual: other.state_name().to_string(),
102 }),
103 }
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::domain::environment::TraceId;
111 use chrono::Utc;
112 use std::path::PathBuf;
113 use std::time::Duration;
114
115 fn create_test_context() -> ProvisionFailureContext {
116 ProvisionFailureContext {
117 failed_step: ProvisionStep::CloudInitWait,
118 error_kind: ErrorKind::Timeout,
119 base: BaseFailureContext {
120 error_summary: "cloud_init_timeout".to_string(),
121 failed_at: Utc::now(),
122 execution_started_at: Utc::now(),
123 execution_duration: Duration::from_secs(30),
124 trace_id: TraceId::new(),
125 trace_file_path: None,
126 },
127 }
128 }
129
130 #[test]
131 fn it_should_create_provision_failed_state_with_context() {
132 let context = create_test_context();
133 let state = ProvisionFailed {
134 context: context.clone(),
135 };
136 assert_eq!(state.context.failed_step, ProvisionStep::CloudInitWait);
137 assert_eq!(state.context.error_kind, ErrorKind::Timeout);
138 }
139
140 #[test]
141 fn it_should_clone_provision_failed_state() {
142 let state = ProvisionFailed {
143 context: create_test_context(),
144 };
145 let cloned = state.clone();
146 assert_eq!(state.context.failed_step, cloned.context.failed_step);
147 }
148
149 #[test]
150 fn it_should_serialize_provision_failed_state_to_json() {
151 let state = ProvisionFailed {
152 context: create_test_context(),
153 };
154 let json = serde_json::to_string(&state).unwrap();
155 assert!(json.contains("CloudInitWait"));
156 assert!(json.contains("Timeout"));
157 }
158
159 #[test]
160 fn it_should_deserialize_provision_failed_state_from_json() {
161 let state = ProvisionFailed {
162 context: create_test_context(),
163 };
164 let json = serde_json::to_string(&state).unwrap();
165 let deserialized: ProvisionFailed = serde_json::from_str(&json).unwrap();
166 assert_eq!(
167 deserialized.context.failed_step,
168 ProvisionStep::CloudInitWait
169 );
170 }
171
172 mod conversion_tests {
173 use super::*;
174 use crate::adapters::ssh::SshCredentials;
175 use crate::domain::environment::name::EnvironmentName;
176 use crate::domain::provider::{LxdConfig, ProviderConfig};
177 use crate::domain::ProfileName;
178 use crate::shared::Username;
179 use std::path::PathBuf;
180
181 fn default_lxd_provider_config(env_name: &EnvironmentName) -> ProviderConfig {
182 ProviderConfig::Lxd(LxdConfig {
183 profile_name: ProfileName::new(format!("lxd-{}", env_name.as_str())).unwrap(),
184 })
185 }
186
187 fn create_test_ssh_credentials() -> SshCredentials {
188 let username = Username::new("test-user".to_string()).unwrap();
189 SshCredentials::new(
190 PathBuf::from("/tmp/test_key"),
191 PathBuf::from("/tmp/test_key.pub"),
192 username,
193 )
194 }
195
196 fn create_test_environment_provision_failed() -> Environment<ProvisionFailed> {
197 let name = EnvironmentName::new("test-env".to_string()).unwrap();
198 let ssh_creds = create_test_ssh_credentials();
199 Environment::new(
200 name.clone(),
201 default_lxd_provider_config(&name),
202 ssh_creds,
203 22,
204 chrono::Utc::now(),
205 )
206 .start_provisioning()
207 .provision_failed(super::create_test_context())
208 }
209
210 #[test]
211 fn it_should_convert_provision_failed_environment_into_any() {
212 let env = create_test_environment_provision_failed();
213 let any_env = env.into_any();
214 assert!(matches!(any_env, AnyEnvironmentState::ProvisionFailed(_)));
215 }
216
217 #[test]
218 fn it_should_convert_any_to_provision_failed_successfully() {
219 let env = create_test_environment_provision_failed();
220 let any_env = env.into_any();
221 let result = any_env.try_into_provision_failed();
222 assert!(result.is_ok());
223 }
224
225 #[test]
226 fn it_should_preserve_error_details_in_failed_states() {
227 let name = EnvironmentName::new("test-env".to_string()).unwrap();
228 let ssh_creds = create_test_ssh_credentials();
229 let context = super::create_test_context();
230 let env = Environment::new(
231 name.clone(),
232 default_lxd_provider_config(&name),
233 ssh_creds,
234 22,
235 chrono::Utc::now(),
236 )
237 .start_provisioning()
238 .provision_failed(context.clone());
239
240 let any_env = env.into_any();
242 let env_restored = any_env.try_into_provision_failed().unwrap();
243
244 assert_eq!(
245 env_restored.state().context.failed_step,
246 context.failed_step
247 );
248 assert_eq!(
249 env_restored.state().context.base.error_summary,
250 context.base.error_summary
251 );
252 }
253 }
254
255 mod context_tests {
256 use super::*;
257
258 #[test]
259 fn it_should_serialize_provision_failure_context() {
260 let context = ProvisionFailureContext {
261 failed_step: ProvisionStep::OpenTofuApply,
262 error_kind: ErrorKind::InfrastructureOperation,
263 base: BaseFailureContext {
264 error_summary: "Infrastructure provisioning failed".to_string(),
265 failed_at: Utc::now(),
266 execution_started_at: Utc::now(),
267 execution_duration: Duration::from_secs(30),
268 trace_id: TraceId::new(),
269 trace_file_path: Some(PathBuf::from("/data/env/traces/trace.log")),
270 },
271 };
272
273 let json = serde_json::to_string(&context).unwrap();
274 assert!(json.contains("OpenTofuApply"));
275 assert!(json.contains("InfrastructureOperation"));
276 }
277
278 #[test]
279 fn it_should_deserialize_provision_failure_context() {
280 let trace_id = TraceId::new();
281 let json = format!(
282 r#"{{
283 "failed_step": "RenderOpenTofuTemplates",
284 "error_kind": "TemplateRendering",
285 "error_summary": "Template rendering failed",
286 "failed_at": "2025-10-06T10:00:00Z",
287 "execution_started_at": "2025-10-06T09:59:00Z",
288 "execution_duration": {{"secs": 60, "nanos": 0}},
289 "trace_id": "{trace_id}",
290 "trace_file_path": null
291 }}"#
292 );
293
294 let context: ProvisionFailureContext = serde_json::from_str(&json).unwrap();
295 assert_eq!(context.failed_step, ProvisionStep::RenderOpenTofuTemplates);
296 assert_eq!(context.error_kind, ErrorKind::TemplateRendering);
297 }
298 }
299}