Skip to main content

torrust_tracker_deployer_lib/application/command_handlers/create/
handler.rs

1//! Create Command Implementation
2//!
3//! This module implements the `CreateCommandHandler` that orchestrates environment
4//! creation business logic. It follows the Command Pattern with dependency
5//! injection and is delivery-agnostic.
6
7use std::convert::TryInto;
8use std::sync::Arc;
9use tracing::{info, instrument};
10
11use crate::application::command_handlers::create::config::EnvironmentCreationConfig;
12use crate::domain::environment::repository::EnvironmentRepository;
13use crate::domain::environment::{Created, Environment, EnvironmentParams};
14use crate::shared::Clock;
15
16use super::errors::CreateCommandHandlerError;
17
18/// Command to create a new deployment environment
19///
20/// This command is delivery-agnostic and can be used from CLI, REST API,
21/// GraphQL, or any other delivery mechanism. It orchestrates the business
22/// logic for environment creation without knowledge of how the configuration
23/// was obtained.
24///
25/// # Architecture
26///
27/// The command follows these design principles:
28///
29/// - **Synchronous**: No async/await, following existing patterns
30/// - **Dependency Injection**: Uses `Arc<dyn Trait>` for testability
31/// - **Repository Pattern**: Delegates persistence to repository
32/// - **Explicit Errors**: All failures return structured errors with `.help()`
33///
34/// # Business Logic Flow
35///
36/// 1. Convert configuration to domain objects
37/// 2. Check if environment already exists (prevent duplicates)
38/// 3. Create environment entity using `Environment::new()`
39/// 4. Persist via repository (repository handles directory creation)
40///
41/// # Examples
42///
43/// ```rust,no_run
44/// use std::sync::Arc;
45/// use torrust_tracker_deployer_lib::application::command_handlers::create::CreateCommandHandler;
46/// use torrust_tracker_deployer_lib::application::command_handlers::create::config::{
47///     EnvironmentCreationConfig, EnvironmentSection, LxdProviderSection, ProviderSection,
48///     SshCredentialsConfig,
49/// };
50/// use torrust_tracker_deployer_lib::application::command_handlers::create::config::tracker::TrackerSection;
51/// use torrust_tracker_deployer_lib::infrastructure::persistence::file_repository_factory::FileRepositoryFactory;
52/// use torrust_tracker_deployer_lib::shared::{SystemClock, Clock};
53///
54/// // Setup dependencies
55/// let file_repository_factory = FileRepositoryFactory::new(std::time::Duration::from_secs(30));
56/// let repository = file_repository_factory.create(std::path::PathBuf::from("."));
57/// let clock: Arc<dyn Clock> = Arc::new(SystemClock);
58///
59/// // Create command
60/// let command = CreateCommandHandler::new(repository, clock);
61///
62/// // Prepare configuration
63/// let config = EnvironmentCreationConfig::new(
64///     EnvironmentSection {
65///         name: "dev".to_string(),
66///         description: None,
67///         instance_name: None, // Auto-generate from environment name
68///     },
69///     SshCredentialsConfig::new(
70///         "fixtures/testing_rsa".to_string(),
71///         "fixtures/testing_rsa.pub".to_string(),
72///         "torrust".to_string(),
73///         22,
74///     ),
75///     ProviderSection::Lxd(LxdProviderSection {
76///         profile_name: "lxd-dev".to_string(),
77///     }),
78///     TrackerSection::default(),
79///     None, // prometheus
80///     None, // grafana
81///     None, // https
82///     None, // backup
83/// );
84///
85/// // Execute command with working directory
86/// let working_dir = std::path::Path::new(".");
87/// let environment = command.execute(config, working_dir)?;
88/// println!("Created environment: {}", environment.name());
89/// # Ok::<(), Box<dyn std::error::Error>>(())
90/// ```
91pub struct CreateCommandHandler {
92    /// Repository for persisting environment state
93    pub(crate) environment_repository: Arc<dyn EnvironmentRepository>,
94
95    /// Clock for timestamp generation (injected for testability)
96    pub(crate) clock: Arc<dyn Clock>,
97}
98
99impl CreateCommandHandler {
100    /// Create a new `CreateCommandHandler` with required dependencies
101    ///
102    /// # Arguments
103    ///
104    /// * `environment_repository` - Repository for persisting environment state
105    /// * `clock` - Clock for timestamp generation (for future use)
106    ///
107    /// # Examples
108    ///
109    /// ```rust,no_run
110    /// use std::sync::Arc;
111    /// use torrust_tracker_deployer_lib::application::command_handlers::create::CreateCommandHandler;
112    /// use torrust_tracker_deployer_lib::infrastructure::persistence::file_repository_factory::FileRepositoryFactory;
113    /// use torrust_tracker_deployer_lib::shared::{SystemClock, Clock};
114    ///
115    /// let file_repository_factory = FileRepositoryFactory::new(std::time::Duration::from_secs(30));
116    /// let repository = file_repository_factory.create(std::path::PathBuf::from("."));
117    /// let clock: Arc<dyn Clock> = Arc::new(SystemClock);
118    ///
119    /// let command = CreateCommandHandler::new(repository, clock);
120    /// ```
121    #[must_use]
122    pub fn new(
123        environment_repository: Arc<dyn EnvironmentRepository>,
124        clock: Arc<dyn Clock>,
125    ) -> Self {
126        Self {
127            environment_repository,
128            clock,
129        }
130    }
131
132    /// Execute the create command with validated configuration
133    ///
134    /// This method orchestrates the complete environment creation workflow:
135    /// 1. Converts configuration to domain objects
136    /// 2. Validates environment uniqueness
137    /// 3. Creates the environment entity
138    /// 4. Persists the environment state
139    ///
140    /// # Arguments
141    ///
142    /// * `config` - Validated environment configuration from domain layer
143    ///
144    /// # Returns
145    ///
146    /// * `Ok(Environment<Created>)` - Successfully created environment
147    /// * `Err(CreateCommandHandlerError)` - Business logic or persistence failure
148    ///
149    /// # Business Rules
150    ///
151    /// 1. Configuration must convert to valid domain objects
152    /// 2. Environment name must be unique (no duplicates)
153    /// 3. Repository handles directory creation atomically during save
154    /// 4. Environment state must be persisted successfully
155    ///
156    /// # Errors
157    ///
158    /// Returns an error if:
159    /// - Configuration validation fails
160    /// - Environment with the same name already exists
161    /// - Repository persistence fails
162    ///
163    /// All errors implement `.help()` with detailed troubleshooting guidance.
164    ///
165    /// # Panics
166    ///
167    /// This function does not panic in practice. The internal `.expect()` call
168    /// when generating the profile name is theoretically unreachable because
169    /// valid environment names always produce valid profile names.
170    ///
171    /// # Examples
172    ///
173    /// ```rust,no_run
174    /// use torrust_tracker_deployer_lib::application::command_handlers::create::CreateCommandHandler;
175    /// use torrust_tracker_deployer_lib::application::command_handlers::create::config::{
176    ///     EnvironmentCreationConfig, EnvironmentSection, LxdProviderSection, ProviderSection,
177    ///     SshCredentialsConfig,
178    /// };
179    /// use torrust_tracker_deployer_lib::application::command_handlers::create::config::tracker::TrackerSection;
180    ///
181    /// # fn example(command: CreateCommandHandler) -> Result<(), Box<dyn std::error::Error>> {
182    /// let config = EnvironmentCreationConfig::new(
183    ///     EnvironmentSection {
184    ///         name: "staging".to_string(),
185    ///         description: None,
186    ///         instance_name: None, // Auto-generate from environment name
187    ///     },
188    ///     SshCredentialsConfig::new(
189    ///         "keys/stage_key".to_string(),
190    ///         "keys/stage_key.pub".to_string(),
191    ///         "torrust".to_string(),
192    ///         22,
193    ///     ),
194    ///     ProviderSection::Lxd(LxdProviderSection {
195    ///         profile_name: "lxd-staging".to_string(),
196    ///     }),
197    ///     TrackerSection::default(),
198    ///     None, // prometheus
199    ///     None, // grafana
200    ///     None, // https
201    ///     None, // backup
202    /// );
203    ///
204    /// let working_dir = std::path::Path::new(".");
205    /// let environment = command.execute(config, working_dir)?;
206    /// println!("Created: {}", environment.name());
207    /// # Ok(())
208    /// # }
209    /// ```
210    #[instrument(
211        name = "create_command",
212        skip_all,
213        fields(
214            command_type = "create",
215            environment = %config.environment.name
216        )
217    )]
218    pub fn execute(
219        &self,
220        config: EnvironmentCreationConfig,
221        working_dir: &std::path::Path,
222    ) -> Result<Environment<Created>, CreateCommandHandlerError> {
223        // Convert DTO to validated domain parameters
224        let params: EnvironmentParams = config
225            .try_into()
226            .map_err(CreateCommandHandlerError::InvalidConfiguration)?;
227
228        // Check for duplicate environment
229        if self
230            .environment_repository
231            .exists(&params.environment_name)
232            .map_err(|e| CreateCommandHandlerError::RepositoryError(e.into()))?
233        {
234            return Err(CreateCommandHandlerError::EnvironmentAlreadyExists {
235                name: params.environment_name.as_str().to_string(),
236            });
237        }
238
239        // Create environment aggregate from validated params
240        let environment = Environment::create(params, working_dir, self.clock.now())
241            .map_err(|e| CreateCommandHandlerError::InvalidConfiguration(e.into()))?;
242
243        self.environment_repository
244            .save(&environment.clone().into_any())
245            .map_err(|e| CreateCommandHandlerError::RepositoryError(e.into()))?;
246
247        info!(
248            command = "create",
249            environment = %environment.name(),
250            "Environment created successfully"
251        );
252
253        Ok(environment)
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn it_should_create_create_command_with_dependencies() {
263        use crate::infrastructure::persistence::file_repository_factory::FileRepositoryFactory;
264        use crate::shared::SystemClock;
265        use tempfile::TempDir;
266
267        let temp_dir = TempDir::new().unwrap();
268        let file_repository_factory =
269            FileRepositoryFactory::new(std::time::Duration::from_secs(30));
270        let repository = file_repository_factory.create(temp_dir.path().to_path_buf());
271        let clock: Arc<dyn Clock> = Arc::new(SystemClock);
272
273        let command = CreateCommandHandler::new(repository, clock);
274
275        // Verify the command was created (basic structure test)
276        assert_eq!(Arc::strong_count(&command.environment_repository), 1);
277        assert_eq!(Arc::strong_count(&command.clock), 1);
278    }
279}