Skip to main content

torrust_tracker_deployer_lib/application/steps/application/
run.rs

1//! Application run step
2//!
3//! This module provides the `RunStep` which handles starting the application
4//! stack on the remote host. The run step executes Docker Compose to bring
5//! up all application services.
6//!
7//! ## Key Features
8//!
9//! - Docker Compose stack execution
10//! - Service startup management
11//! - Container orchestration via Ansible
12//! - Integration with the step-based deployment architecture
13//!
14//! ## Run Process
15//!
16//! The step handles the run phase which typically includes:
17//! - Executing `docker compose up -d` on the remote host
18//! - Starting all application services in detached mode
19//! - Verifying service startup (future enhancement)
20//! - Managing container lifecycle
21//!
22//! This step is designed to be executed after the release step has
23//! deployed all necessary configuration and compose files.
24
25use std::sync::Arc;
26
27use thiserror::Error;
28use tracing::{info, instrument};
29
30use crate::adapters::ansible::AnsibleClient;
31use crate::shared::{ErrorKind, Traceable};
32
33/// Step that runs the application stack on a remote host
34///
35/// This step handles starting Docker Compose services on the remote instance,
36/// bringing up all application containers.
37pub 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    /// Execute the run step
48    ///
49    /// This will start the Docker Compose application stack on the remote host.
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if:
54    /// * Docker Compose execution fails
55    /// * Service startup fails
56    /// * Container creation fails
57    #[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        // TODO: Implement actual run logic
70        // This will include:
71        // 1. Execute docker compose up -d via Ansible
72        // 2. Verify services started successfully
73        // 3. Report container status
74        let _ = &self.ansible_client; // Suppress unused warning for now
75
76        info!(
77            step = "run_application",
78            status = "success",
79            "Application stack started (placeholder)"
80        );
81
82        Ok(())
83    }
84}
85
86/// Errors that can occur during the run step
87#[derive(Debug, Error)]
88pub enum RunStepError {
89    /// Failed to execute Docker Compose
90    #[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    /// Failed to start services
98    #[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    /// Failed to create containers
106    #[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    /// Returns troubleshooting help for this error
116    #[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        // These errors don't wrap Traceable sources
161        None
162    }
163
164    fn error_kind(&self) -> ErrorKind {
165        // All run step errors are infrastructure-related
166        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        // Test that the step can be created successfully
183        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        // Placeholder should succeed
195        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}