torrust_tracker_deployer_lib/presentation/cli/controllers/run/
handler.rs1use std::cell::RefCell;
7use std::sync::Arc;
8
9use parking_lot::ReentrantMutex;
10
11use crate::application::command_handlers::run::RunCommandHandler;
12use crate::application::command_handlers::show::info::{GrafanaInfo, ServiceInfo};
13use crate::domain::environment::name::EnvironmentName;
14use crate::domain::environment::repository::EnvironmentRepository;
15use crate::domain::environment::state::AnyEnvironmentState;
16use crate::presentation::cli::input::cli::OutputFormat;
17use crate::presentation::cli::views::commands::run::{JsonView, RunDetailsData, TextView};
18use crate::presentation::cli::views::progress::ProgressReporter;
19use crate::presentation::cli::views::Render;
20use crate::presentation::cli::views::UserOutput;
21use crate::shared::clock::Clock;
22
23use super::errors::RunSubcommandError;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27enum RunStep {
28 ValidateEnvironment,
29 RunServices,
30}
31
32impl RunStep {
33 const ALL: &'static [Self] = &[Self::ValidateEnvironment, Self::RunServices];
35
36 const fn count() -> usize {
38 Self::ALL.len()
39 }
40
41 fn description(self) -> &'static str {
43 match self {
44 Self::ValidateEnvironment => "Validating environment",
45 Self::RunServices => "Running application services",
46 }
47 }
48}
49
50pub struct RunCommandController {
69 repository: Arc<dyn EnvironmentRepository + Send + Sync>,
70 clock: Arc<dyn Clock>,
71 progress: ProgressReporter,
72}
73
74impl RunCommandController {
75 #[allow(clippy::needless_pass_by_value)] pub fn new(
81 repository: Arc<dyn EnvironmentRepository + Send + Sync>,
82 clock: Arc<dyn Clock>,
83 user_output: Arc<ReentrantMutex<RefCell<UserOutput>>>,
84 ) -> Self {
85 let progress = ProgressReporter::new(user_output, RunStep::count());
86
87 Self {
88 repository,
89 clock,
90 progress,
91 }
92 }
93
94 #[allow(clippy::result_large_err)]
117 #[allow(clippy::unused_async)] pub async fn execute(
119 &mut self,
120 environment_name: &str,
121 output_format: OutputFormat,
122 ) -> Result<(), RunSubcommandError> {
123 let env_name = self.validate_environment_name(environment_name)?;
124
125 self.run_services(&env_name)?;
126
127 self.complete_workflow(environment_name, output_format)?;
128
129 Ok(())
130 }
131
132 #[allow(clippy::result_large_err)]
137 fn validate_environment_name(
138 &mut self,
139 name: &str,
140 ) -> Result<EnvironmentName, RunSubcommandError> {
141 self.progress
142 .start_step(RunStep::ValidateEnvironment.description())?;
143
144 let env_name = EnvironmentName::new(name.to_string()).map_err(|source| {
145 RunSubcommandError::InvalidEnvironmentName {
146 name: name.to_string(),
147 source,
148 }
149 })?;
150
151 self.progress
152 .complete_step(Some(&format!("Environment name validated: {name}")))?;
153
154 Ok(env_name)
155 }
156
157 #[allow(clippy::result_large_err)]
165 fn run_services(&mut self, env_name: &EnvironmentName) -> Result<(), RunSubcommandError> {
166 self.progress
167 .start_step(RunStep::RunServices.description())?;
168
169 let repository: Arc<dyn crate::domain::environment::repository::EnvironmentRepository> =
171 Arc::clone(&self.repository)
172 as Arc<dyn crate::domain::environment::repository::EnvironmentRepository>;
173
174 let handler = RunCommandHandler::new(repository, Arc::clone(&self.clock));
175
176 handler.execute(env_name)?;
177
178 self.progress.complete_step(Some("Services started"))?;
179
180 Ok(())
181 }
182
183 #[allow(clippy::result_large_err)]
193 fn complete_workflow(
194 &mut self,
195 name: &str,
196 output_format: OutputFormat,
197 ) -> Result<(), RunSubcommandError> {
198 let env_name = EnvironmentName::new(name.to_string()).map_err(|source| {
200 RunSubcommandError::InvalidEnvironmentName {
201 name: name.to_string(),
202 source,
203 }
204 })?;
205
206 let any_env = self.load_environment(&env_name)?;
207
208 self.progress
210 .complete(&format!("Run command completed for '{name}'"))?;
211
212 self.display_service_urls(&any_env, output_format)?;
214
215 Ok(())
216 }
217
218 #[allow(clippy::result_large_err)]
222 fn load_environment(
223 &self,
224 env_name: &EnvironmentName,
225 ) -> Result<AnyEnvironmentState, RunSubcommandError> {
226 if !self.repository.exists(env_name)? {
227 return Err(RunSubcommandError::EnvironmentNotAccessible {
228 name: env_name.to_string(),
229 data_dir: "data".to_string(),
230 });
231 }
232
233 self.repository.load(env_name)?.ok_or_else(|| {
234 RunSubcommandError::EnvironmentNotAccessible {
235 name: env_name.to_string(),
236 data_dir: "data".to_string(),
237 }
238 })
239 }
240
241 #[allow(clippy::result_large_err)]
255 fn display_service_urls(
256 &mut self,
257 any_env: &AnyEnvironmentState,
258 output_format: OutputFormat,
259 ) -> Result<(), RunSubcommandError> {
260 if let Some(instance_ip) = any_env.instance_ip() {
261 let tracker_config = any_env.tracker_config();
262 let grafana_config = any_env.grafana_config();
263
264 let services =
265 ServiceInfo::from_tracker_config(tracker_config, instance_ip, grafana_config);
266
267 let grafana =
268 grafana_config.map(|config| GrafanaInfo::from_config(config, instance_ip));
269
270 let data = RunDetailsData::new(any_env.name().to_string(), services, grafana);
271
272 let output = match output_format {
274 OutputFormat::Text => TextView::render(&data)?,
275 OutputFormat::Json => JsonView::render(&data)?,
276 };
277
278 self.progress.result(&output)?;
280 }
281
282 Ok(())
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use crate::infrastructure::persistence::file_repository_factory::FileRepositoryFactory;
290 use crate::presentation::cli::controllers::constants::DEFAULT_LOCK_TIMEOUT;
291 use crate::presentation::cli::input::cli::OutputFormat;
292 use crate::presentation::cli::views::testing::TestUserOutput;
293 use crate::presentation::cli::views::VerbosityLevel;
294 use crate::shared::SystemClock;
295 use tempfile::TempDir;
296
297 #[allow(clippy::type_complexity)]
299 fn create_test_dependencies(
300 temp_dir: &TempDir,
301 ) -> (
302 Arc<ReentrantMutex<RefCell<UserOutput>>>,
303 Arc<dyn EnvironmentRepository + Send + Sync>,
304 Arc<dyn Clock>,
305 ) {
306 let (user_output, _, _) =
307 TestUserOutput::new(VerbosityLevel::Normal).into_reentrant_wrapped();
308 let data_dir = temp_dir.path().join("data");
309 let file_repository_factory = FileRepositoryFactory::new(DEFAULT_LOCK_TIMEOUT);
310 let repository = file_repository_factory.create(data_dir);
311 let clock = Arc::new(SystemClock);
312
313 (user_output, repository, clock)
314 }
315
316 #[tokio::test]
317 async fn it_should_return_error_for_invalid_environment_name() {
318 let temp_dir = TempDir::new().unwrap();
319
320 let (user_output, repository, clock) = create_test_dependencies(&temp_dir);
321
322 let result = RunCommandController::new(repository, clock, user_output.clone())
324 .execute("invalid_name", OutputFormat::Text)
325 .await;
326
327 assert!(result.is_err());
328 match result.unwrap_err() {
329 RunSubcommandError::InvalidEnvironmentName { name, .. } => {
330 assert_eq!(name, "invalid_name");
331 }
332 other => panic!("Expected InvalidEnvironmentName, got: {other:?}"),
333 }
334 }
335
336 #[tokio::test]
337 async fn it_should_return_error_for_empty_environment_name() {
338 let temp_dir = TempDir::new().unwrap();
339
340 let (user_output, repository, clock) = create_test_dependencies(&temp_dir);
341
342 let result = RunCommandController::new(repository, clock, user_output.clone())
343 .execute("", OutputFormat::Text)
344 .await;
345
346 assert!(result.is_err());
347 match result.unwrap_err() {
348 RunSubcommandError::InvalidEnvironmentName { name, .. } => {
349 assert_eq!(name, "");
350 }
351 other => panic!("Expected InvalidEnvironmentName, got: {other:?}"),
352 }
353 }
354
355 #[tokio::test]
356 async fn it_should_return_error_when_environment_not_found() {
357 let temp_dir = TempDir::new().unwrap();
358
359 let (user_output, repository, clock) = create_test_dependencies(&temp_dir);
360
361 let result = RunCommandController::new(repository, clock, user_output.clone())
363 .execute("test-env", OutputFormat::Text)
364 .await;
365
366 assert!(result.is_err());
367 match result.unwrap_err() {
368 RunSubcommandError::EnvironmentNotAccessible { name, .. } => {
369 assert_eq!(name, "test-env");
370 }
371 other => panic!("Expected EnvironmentNotAccessible, got: {other:?}"),
372 }
373 }
374}