torrust_tracker_deployer_lib/application/steps/application/
run.rs1use std::sync::Arc;
26
27use thiserror::Error;
28use tracing::{info, instrument};
29
30use crate::adapters::ansible::AnsibleClient;
31use crate::shared::{ErrorKind, Traceable};
32
33pub struct RunStep {
38 ansible_client: Arc<AnsibleClient>,
39}
40
41impl RunStep {
42 #[must_use]
43 pub fn new(ansible_client: Arc<AnsibleClient>) -> Self {
44 Self { ansible_client }
45 }
46
47 #[instrument(
58 name = "run_application",
59 skip_all,
60 fields(step_type = "application", operation = "run")
61 )]
62 pub fn execute(&self) -> Result<(), RunStepError> {
63 info!(
64 step = "run_application",
65 status = "starting",
66 "Starting application stack"
67 );
68
69 let _ = &self.ansible_client; info!(
77 step = "run_application",
78 status = "success",
79 "Application stack started (placeholder)"
80 );
81
82 Ok(())
83 }
84}
85
86#[derive(Debug, Error)]
88pub enum RunStepError {
89 #[error("Failed to execute Docker Compose: {message}")]
91 DockerComposeExecutionFailed {
92 message: String,
93 #[source]
94 source: Option<Box<dyn std::error::Error + Send + Sync>>,
95 },
96
97 #[error("Failed to start services: {message}")]
99 ServiceStartupFailed {
100 message: String,
101 #[source]
102 source: Option<Box<dyn std::error::Error + Send + Sync>>,
103 },
104
105 #[error("Failed to create containers: {message}")]
107 ContainerCreationFailed {
108 message: String,
109 #[source]
110 source: Option<Box<dyn std::error::Error + Send + Sync>>,
111 },
112}
113
114impl RunStepError {
115 #[must_use]
117 pub fn help(&self) -> &'static str {
118 match self {
119 Self::DockerComposeExecutionFailed { .. } => {
120 "Docker Compose execution failed. Please check:\n\
121 1. Docker Compose files are present on the remote host\n\
122 2. Docker Compose syntax is valid\n\
123 3. Docker daemon is running on the remote host\n\
124 4. User has permissions to run Docker commands"
125 }
126 Self::ServiceStartupFailed { .. } => {
127 "Service startup failed. Please check:\n\
128 1. Container images are available or can be pulled\n\
129 2. Port conflicts with existing services\n\
130 3. Volume mounts are accessible\n\
131 4. Environment variables are properly configured"
132 }
133 Self::ContainerCreationFailed { .. } => {
134 "Container creation failed. Please check:\n\
135 1. Docker daemon is running and healthy\n\
136 2. Sufficient disk space for containers\n\
137 3. Network configuration is valid\n\
138 4. Container images exist and are valid"
139 }
140 }
141 }
142}
143
144impl Traceable for RunStepError {
145 fn trace_format(&self) -> String {
146 match self {
147 Self::DockerComposeExecutionFailed { message, .. } => {
148 format!("RunStep::DockerComposeExecutionFailed - {message}")
149 }
150 Self::ServiceStartupFailed { message, .. } => {
151 format!("RunStep::ServiceStartupFailed - {message}")
152 }
153 Self::ContainerCreationFailed { message, .. } => {
154 format!("RunStep::ContainerCreationFailed - {message}")
155 }
156 }
157 }
158
159 fn trace_source(&self) -> Option<&dyn Traceable> {
160 None
162 }
163
164 fn error_kind(&self) -> ErrorKind {
165 ErrorKind::InfrastructureOperation
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use std::path::PathBuf;
173 use std::sync::Arc;
174
175 use super::*;
176
177 #[test]
178 fn it_should_create_run_step() {
179 let ansible_client = Arc::new(AnsibleClient::new(PathBuf::from("test_inventory.yml")));
180 let step = RunStep::new(ansible_client);
181
182 assert_eq!(
184 std::mem::size_of_val(&step),
185 std::mem::size_of::<Arc<AnsibleClient>>()
186 );
187 }
188
189 #[test]
190 fn it_should_execute_run_step_placeholder() {
191 let ansible_client = Arc::new(AnsibleClient::new(PathBuf::from("test_inventory.yml")));
192 let step = RunStep::new(ansible_client);
193
194 let result = step.execute();
196 assert!(result.is_ok());
197 }
198
199 #[test]
200 fn docker_compose_error_should_provide_help() {
201 let error = RunStepError::DockerComposeExecutionFailed {
202 message: "Command not found".to_string(),
203 source: None,
204 };
205
206 let help = error.help();
207 assert!(help.contains("Docker Compose"));
208 assert!(help.contains("Docker daemon"));
209 }
210
211 #[test]
212 fn service_startup_error_should_provide_help() {
213 let error = RunStepError::ServiceStartupFailed {
214 message: "Port already in use".to_string(),
215 source: None,
216 };
217
218 let help = error.help();
219 assert!(help.contains("Port conflicts"));
220 assert!(help.contains("Container images"));
221 }
222
223 #[test]
224 fn container_creation_error_should_provide_help() {
225 let error = RunStepError::ContainerCreationFailed {
226 message: "No space left on device".to_string(),
227 source: None,
228 };
229
230 let help = error.help();
231 assert!(help.contains("disk space"));
232 assert!(help.contains("Docker daemon"));
233 }
234
235 #[test]
236 fn errors_should_implement_traceable() {
237 let error = RunStepError::DockerComposeExecutionFailed {
238 message: "test error".to_string(),
239 source: None,
240 };
241
242 assert!(error
243 .trace_format()
244 .contains("DockerComposeExecutionFailed"));
245 assert!(error.trace_source().is_none());
246 assert!(matches!(
247 error.error_kind(),
248 ErrorKind::InfrastructureOperation
249 ));
250 }
251}