torrust_tracker_deployer_lib/domain/environment/state/
release_failed.rs1use std::fmt;
14
15use serde::{Deserialize, Serialize};
16
17use crate::domain::environment::state::{AnyEnvironmentState, BaseFailureContext, StateTypeError};
18use crate::domain::environment::Environment;
19use crate::shared::error::ErrorKind;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum ReleaseStep {
33 CreateTrackerStorage,
35 InitTrackerDatabase,
37 RenderTrackerTemplates,
39 DeployTrackerConfigToRemote,
41 CreatePrometheusStorage,
43 RenderPrometheusTemplates,
45 DeployPrometheusConfigToRemote,
47 CreateGrafanaStorage,
49 RenderGrafanaTemplates,
51 DeployGrafanaProvisioning,
53 CreateMysqlStorage,
55 RenderBackupTemplates,
57 CreateBackupStorage,
59 DeployBackupConfigToRemote,
61 InstallBackupCrontab,
63 RenderCaddyTemplates,
65 DeployCaddyConfigToRemote,
67 RenderDockerComposeTemplates,
69 DeployComposeFilesToRemote,
71}
72
73impl fmt::Display for ReleaseStep {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 let name = match self {
76 Self::CreateTrackerStorage => "Create Tracker Storage",
77 Self::InitTrackerDatabase => "Initialize Tracker Database",
78 Self::RenderTrackerTemplates => "Render Tracker Templates",
79 Self::DeployTrackerConfigToRemote => "Deploy Tracker Config to Remote",
80 Self::CreatePrometheusStorage => "Create Prometheus Storage",
81 Self::RenderPrometheusTemplates => "Render Prometheus Templates",
82 Self::DeployPrometheusConfigToRemote => "Deploy Prometheus Config to Remote",
83 Self::CreateGrafanaStorage => "Create Grafana Storage",
84 Self::RenderGrafanaTemplates => "Render Grafana Templates",
85 Self::DeployGrafanaProvisioning => "Deploy Grafana Provisioning",
86 Self::CreateMysqlStorage => "Create MySQL Storage",
87 Self::RenderBackupTemplates => "Render Backup Templates",
88 Self::CreateBackupStorage => "Create Backup Storage",
89 Self::DeployBackupConfigToRemote => "Deploy Backup Config to Remote",
90 Self::InstallBackupCrontab => "Install Backup Crontab",
91 Self::RenderCaddyTemplates => "Render Caddy Templates",
92 Self::DeployCaddyConfigToRemote => "Deploy Caddy Config to Remote",
93 Self::RenderDockerComposeTemplates => "Render Docker Compose Templates",
94 Self::DeployComposeFilesToRemote => "Deploy Compose Files to Remote",
95 };
96 write!(f, "{name}")
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
112pub struct ReleaseFailureContext {
113 pub failed_step: ReleaseStep,
115
116 pub error_kind: ErrorKind,
118
119 pub base: BaseFailureContext,
121}
122
123#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
132pub struct ReleaseFailed {
133 pub context: ReleaseFailureContext,
135}
136
137impl Environment<ReleaseFailed> {
139 #[must_use]
141 pub fn into_any(self) -> AnyEnvironmentState {
142 AnyEnvironmentState::ReleaseFailed(self)
143 }
144}
145
146impl AnyEnvironmentState {
148 pub fn try_into_release_failed(self) -> Result<Environment<ReleaseFailed>, StateTypeError> {
154 match self {
155 Self::ReleaseFailed(env) => Ok(env),
156 other => Err(StateTypeError::UnexpectedState {
157 expected: "release_failed",
158 actual: other.state_name().to_string(),
159 }),
160 }
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use std::time::Duration;
167
168 use chrono::Utc;
169
170 use super::*;
171 use crate::domain::environment::TraceId;
172
173 fn create_test_failure_context() -> ReleaseFailureContext {
174 let now = Utc::now();
175 ReleaseFailureContext {
176 failed_step: ReleaseStep::RenderDockerComposeTemplates,
177 error_kind: ErrorKind::Configuration,
178 base: BaseFailureContext {
179 error_summary: "Test error".to_string(),
180 failed_at: now,
181 execution_started_at: now,
182 execution_duration: Duration::from_secs(10),
183 trace_id: TraceId::new(),
184 trace_file_path: None,
185 },
186 }
187 }
188
189 #[test]
190 fn it_should_create_release_failed_state_with_context() {
191 let context = create_test_failure_context();
192 let state = ReleaseFailed {
193 context: context.clone(),
194 };
195 assert_eq!(
196 state.context.failed_step,
197 ReleaseStep::RenderDockerComposeTemplates
198 );
199 assert_eq!(state.context.error_kind, ErrorKind::Configuration);
200 }
201
202 #[test]
203 fn it_should_display_release_step() {
204 assert_eq!(
205 format!("{}", ReleaseStep::RenderDockerComposeTemplates),
206 "Render Docker Compose Templates"
207 );
208 assert_eq!(
209 format!("{}", ReleaseStep::DeployComposeFilesToRemote),
210 "Deploy Compose Files to Remote"
211 );
212 }
213
214 #[test]
215 fn it_should_serialize_release_step_to_snake_case() {
216 let step = ReleaseStep::RenderDockerComposeTemplates;
217 let json = serde_json::to_string(&step).unwrap();
218 assert_eq!(json, r#""render_docker_compose_templates""#);
219
220 let step = ReleaseStep::DeployComposeFilesToRemote;
221 let json = serde_json::to_string(&step).unwrap();
222 assert_eq!(json, r#""deploy_compose_files_to_remote""#);
223 }
224
225 #[test]
226 fn it_should_deserialize_release_step_from_snake_case() {
227 let step: ReleaseStep =
228 serde_json::from_str(r#""render_docker_compose_templates""#).unwrap();
229 assert_eq!(step, ReleaseStep::RenderDockerComposeTemplates);
230
231 let step: ReleaseStep =
232 serde_json::from_str(r#""deploy_compose_files_to_remote""#).unwrap();
233 assert_eq!(step, ReleaseStep::DeployComposeFilesToRemote);
234 }
235
236 mod conversion_tests {
237 use super::*;
238 use crate::adapters::ssh::SshCredentials;
239 use crate::domain::environment::name::EnvironmentName;
240 use crate::domain::environment::runtime_outputs::ProvisionMethod;
241 use crate::domain::provider::{LxdConfig, ProviderConfig};
242 use crate::domain::ProfileName;
243 use crate::shared::Username;
244 use std::net::{IpAddr, Ipv4Addr};
245 use std::path::PathBuf;
246
247 fn default_lxd_provider_config(env_name: &EnvironmentName) -> ProviderConfig {
248 ProviderConfig::Lxd(LxdConfig {
249 profile_name: ProfileName::new(format!("lxd-{}", env_name.as_str())).unwrap(),
250 })
251 }
252
253 fn create_test_ssh_credentials() -> SshCredentials {
254 let username = Username::new("test-user".to_string()).unwrap();
255 SshCredentials::new(
256 PathBuf::from("/tmp/test_key"),
257 PathBuf::from("/tmp/test_key.pub"),
258 username,
259 )
260 }
261
262 fn create_test_environment_release_failed() -> Environment<ReleaseFailed> {
263 let name = EnvironmentName::new("test-env".to_string()).unwrap();
264 let ssh_creds = create_test_ssh_credentials();
265 let now = Utc::now();
266 let context = ReleaseFailureContext {
267 failed_step: ReleaseStep::DeployComposeFilesToRemote,
268 error_kind: ErrorKind::InfrastructureOperation,
269 base: BaseFailureContext {
270 error_summary: "SSH connection failed".to_string(),
271 failed_at: now,
272 execution_started_at: now,
273 execution_duration: Duration::from_secs(5),
274 trace_id: TraceId::new(),
275 trace_file_path: None,
276 },
277 };
278 Environment::new(
279 name.clone(),
280 default_lxd_provider_config(&name),
281 ssh_creds,
282 22,
283 chrono::Utc::now(),
284 )
285 .start_provisioning()
286 .provisioned(
287 IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
288 ProvisionMethod::Provisioned,
289 )
290 .start_configuring()
291 .configured()
292 .start_releasing()
293 .release_failed(context)
294 }
295
296 #[test]
297 fn it_should_convert_release_failed_environment_into_any() {
298 let env = create_test_environment_release_failed();
299 let any_env = env.into_any();
300 assert!(matches!(any_env, AnyEnvironmentState::ReleaseFailed(_)));
301 }
302
303 #[test]
304 fn it_should_convert_any_to_release_failed_successfully() {
305 let env = create_test_environment_release_failed();
306 let any_env = env.into_any();
307 let result = any_env.try_into_release_failed();
308 assert!(result.is_ok());
309 }
310 }
311}