torrust_tracker_deployer_lib/presentation/cli/controllers/destroy/
handler.rs1use std::cell::RefCell;
7use std::sync::Arc;
8
9use parking_lot::ReentrantMutex;
10
11use crate::application::command_handlers::DestroyCommandHandler;
12use crate::domain::environment::name::EnvironmentName;
13use crate::domain::environment::repository::EnvironmentRepository;
14use crate::domain::environment::state::Destroyed;
15use crate::domain::environment::Environment;
16use crate::presentation::cli::input::cli::OutputFormat;
17use crate::presentation::cli::views::commands::destroy::{DestroyDetailsData, JsonView, 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::DestroySubcommandError;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27enum DestroyStep {
28 ValidateEnvironment,
29 CreateCommandHandler,
30 TearDownInfrastructure,
31}
32
33impl DestroyStep {
34 const ALL: &'static [Self] = &[
36 Self::ValidateEnvironment,
37 Self::CreateCommandHandler,
38 Self::TearDownInfrastructure,
39 ];
40
41 const fn count() -> usize {
43 Self::ALL.len()
44 }
45
46 fn description(self) -> &'static str {
48 match self {
49 Self::ValidateEnvironment => "Validating environment",
50 Self::CreateCommandHandler => "Creating command handler",
51 Self::TearDownInfrastructure => "Tearing down infrastructure",
52 }
53 }
54}
55
56pub struct DestroyCommandController {
74 repository: Arc<dyn EnvironmentRepository + Send + Sync>,
75 clock: Arc<dyn Clock>,
76 progress: ProgressReporter,
77}
78
79impl DestroyCommandController {
80 #[allow(clippy::needless_pass_by_value)] pub fn new(
86 repository: Arc<dyn EnvironmentRepository + Send + Sync>,
87 clock: Arc<dyn Clock>,
88 user_output: Arc<ReentrantMutex<RefCell<UserOutput>>>,
89 ) -> Self {
90 let progress = ProgressReporter::new(user_output, DestroyStep::count());
91
92 Self {
93 repository,
94 clock,
95 progress,
96 }
97 }
98
99 #[allow(clippy::result_large_err)]
123 #[allow(clippy::unused_async)] pub async fn execute(
125 &mut self,
126 environment_name: &str,
127 output_format: OutputFormat,
128 ) -> Result<(), DestroySubcommandError> {
129 let env_name = self.validate_environment_name(environment_name)?;
130
131 let handler = self.create_command_handler()?;
132
133 let destroyed = self.tear_down_infrastructure(&handler, &env_name)?;
134
135 self.complete_workflow(environment_name, &destroyed, output_format)?;
136
137 Ok(())
138 }
139
140 #[allow(clippy::result_large_err)]
145 fn validate_environment_name(
146 &mut self,
147 name: &str,
148 ) -> Result<EnvironmentName, DestroySubcommandError> {
149 self.progress
150 .start_step(DestroyStep::ValidateEnvironment.description())?;
151
152 let env_name = EnvironmentName::new(name.to_string()).map_err(|source| {
153 DestroySubcommandError::InvalidEnvironmentName {
154 name: name.to_string(),
155 source,
156 }
157 })?;
158
159 self.progress
160 .complete_step(Some(&format!("Environment name validated: {name}")))?;
161
162 Ok(env_name)
163 }
164
165 #[allow(clippy::result_large_err)]
170 fn create_command_handler(&mut self) -> Result<DestroyCommandHandler, DestroySubcommandError> {
171 self.progress
172 .start_step(DestroyStep::CreateCommandHandler.description())?;
173 let handler = DestroyCommandHandler::new(self.repository.clone(), self.clock.clone());
174 self.progress.complete_step(None)?;
175
176 Ok(handler)
177 }
178
179 #[allow(clippy::result_large_err)]
184 fn tear_down_infrastructure(
185 &mut self,
186 handler: &DestroyCommandHandler,
187 env_name: &EnvironmentName,
188 ) -> Result<Environment<Destroyed>, DestroySubcommandError> {
189 self.progress
190 .start_step(DestroyStep::TearDownInfrastructure.description())?;
191
192 let destroyed = handler.execute(env_name).map_err(|source| {
193 DestroySubcommandError::DestroyOperationFailed {
194 name: env_name.to_string(),
195 source,
196 }
197 })?;
198
199 self.progress
200 .complete_step(Some("Infrastructure torn down"))?;
201 Ok(destroyed)
202 }
203
204 #[allow(clippy::result_large_err)]
210 fn complete_workflow(
211 &mut self,
212 name: &str,
213 destroyed: &Environment<Destroyed>,
214 output_format: OutputFormat,
215 ) -> Result<(), DestroySubcommandError> {
216 let details = DestroyDetailsData::from(destroyed);
217
218 let output = match output_format {
219 OutputFormat::Text => TextView::render(&details)?,
220 OutputFormat::Json => JsonView::render(&details)?,
221 };
222
223 self.progress.result(&output)?;
224
225 if matches!(output_format, OutputFormat::Text) {
227 self.progress.blank_line()?;
228 self.progress.output().lock().borrow_mut().result(&format!(
229 "💡 Local data preserved for debugging. To completely remove and reuse the name:\n torrust-tracker-deployer purge {name} --force"
230 ));
231 }
232
233 Ok(())
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use crate::infrastructure::persistence::file_repository_factory::FileRepositoryFactory;
241 use crate::presentation::cli::controllers::constants::DEFAULT_LOCK_TIMEOUT;
242 use crate::presentation::cli::views::testing::TestUserOutput;
243 use crate::presentation::cli::views::VerbosityLevel;
244 use crate::shared::SystemClock;
245 use std::fs;
246 use tempfile::TempDir;
247
248 #[allow(clippy::type_complexity)] fn create_test_dependencies(
256 temp_dir: &TempDir,
257 ) -> (
258 Arc<ReentrantMutex<RefCell<UserOutput>>>,
259 Arc<dyn EnvironmentRepository + Send + Sync>,
260 Arc<dyn Clock>,
261 ) {
262 let (user_output, _, _) =
263 TestUserOutput::new(VerbosityLevel::Normal).into_reentrant_wrapped();
264 let data_dir = temp_dir.path().join("data");
265 let file_repository_factory = FileRepositoryFactory::new(DEFAULT_LOCK_TIMEOUT);
266 let repository = file_repository_factory.create(data_dir);
267 let clock = Arc::new(SystemClock);
268
269 (user_output, repository, clock)
270 }
271
272 #[tokio::test]
273 async fn it_should_return_error_for_invalid_environment_name() {
274 let temp_dir = TempDir::new().unwrap();
275
276 let (user_output, repository, clock) = create_test_dependencies(&temp_dir);
277
278 let result = DestroyCommandController::new(repository, clock, user_output.clone())
280 .execute("invalid_name", OutputFormat::Text)
281 .await;
282
283 assert!(result.is_err());
284 match result.unwrap_err() {
285 DestroySubcommandError::InvalidEnvironmentName { name, .. } => {
286 assert_eq!(name, "invalid_name");
287 }
288 other => panic!("Expected InvalidEnvironmentName, got: {other:?}"),
289 }
290 }
291
292 #[tokio::test]
293 async fn it_should_return_error_for_empty_environment_name() {
294 let temp_dir = TempDir::new().unwrap();
295
296 let (user_output, repository, clock) = create_test_dependencies(&temp_dir);
297
298 let result = DestroyCommandController::new(repository, clock, user_output.clone())
299 .execute("", OutputFormat::Text)
300 .await;
301
302 assert!(result.is_err());
303 match result.unwrap_err() {
304 DestroySubcommandError::InvalidEnvironmentName { name, .. } => {
305 assert_eq!(name, "");
306 }
307 other => panic!("Expected InvalidEnvironmentName, got: {other:?}"),
308 }
309 }
310
311 #[tokio::test]
312 async fn it_should_return_error_for_nonexistent_environment() {
313 let temp_dir = TempDir::new().unwrap();
314
315 let (user_output, repository, clock) = create_test_dependencies(&temp_dir);
316
317 let result = DestroyCommandController::new(repository, clock, user_output.clone())
319 .execute("nonexistent-env", OutputFormat::Text)
320 .await;
321
322 assert!(result.is_err());
323 match result.unwrap_err() {
325 DestroySubcommandError::DestroyOperationFailed { name, .. } => {
326 assert_eq!(name, "nonexistent-env");
327 }
328 other => panic!("Expected DestroyOperationFailed, got: {other:?}"),
329 }
330 }
331
332 #[tokio::test]
333 async fn it_should_accept_valid_environment_name() {
334 let temp_dir = TempDir::new().unwrap();
335 let working_dir = temp_dir.path();
336
337 let (user_output, repository, clock) = create_test_dependencies(&temp_dir);
338
339 let env_dir = working_dir.join("test-env");
341 fs::create_dir_all(&env_dir).unwrap();
342
343 let result = DestroyCommandController::new(repository, clock, user_output.clone())
346 .execute("test-env", OutputFormat::Text)
347 .await;
348
349 if let Err(DestroySubcommandError::InvalidEnvironmentName { .. }) = result {
351 panic!("Should not fail at name validation for 'test-env'");
352 }
353 }
355}