torrust_tracker_deployer_lib/domain/environment/context.rs
1//! Environment Context Module
2//!
3//! This module contains the `EnvironmentContext` struct which composes three
4//! semantic types to organize state-independent environment data.
5//!
6//! ## Purpose
7//!
8//! The `EnvironmentContext` separates immutable environment configuration from
9//! the mutable state machine, and further organizes that configuration into
10//! three distinct semantic categories:
11//!
12//! 1. **User Inputs** - Configuration provided by users
13//! 2. **Internal Config** - Derived paths for organizing artifacts
14//! 3. **Runtime Outputs** - Data generated during deployment
15//!
16//! ## Benefits
17//!
18//! - **Reduced pattern matching**: Access common fields without matching on state (83% reduction)
19//! - **Clear semantic boundaries**: Types document the purpose of each field
20//! - **Developer guidance**: Clear where to add new fields based on their purpose
21//! - **Simplified state transitions**: Only the state changes, context remains constant
22//! - **Easier extension**: Adding fields is straightforward with clear categorization
23//!
24//! ## Three-Way Semantic Split
25//!
26//! ### When to Add Fields
27//!
28//! - **`UserInputs`**: User needs to configure something at environment creation time
29//! - **`InternalConfig`**: Need internal paths or derived configuration
30//! - **`RuntimeOutputs`**: Operations produce new data about deployed infrastructure
31//!
32//! ### Design Rationale
33//!
34//! By organizing fields into three semantic categories, we make it immediately
35//! clear where each piece of information comes from and guide developers on
36//! where to add new fields as the application evolves.
37
38use crate::adapters::ssh::SshCredentials;
39use crate::domain::backup::BackupConfig;
40use crate::domain::environment::{
41 EnvironmentName, EnvironmentParams, InternalConfig, RuntimeOutputs, UserInputs,
42};
43use crate::domain::grafana::GrafanaConfig;
44use crate::domain::prometheus::PrometheusConfig;
45use crate::domain::provider::ProviderConfig;
46use chrono::{DateTime, TimeZone, Utc};
47use serde::{Deserialize, Serialize};
48use std::path::PathBuf;
49
50/// Default value for `created_at` field for backward compatibility
51///
52/// Returns Unix epoch (1970-01-01 00:00:00 UTC) for environments created
53/// before the `created_at` field was added.
54fn default_created_at() -> DateTime<Utc> {
55 Utc.timestamp_opt(0, 0).unwrap()
56}
57
58/// Complete environment context composed of three semantic types
59///
60/// The context is split into three logical categories:
61/// 1. **User Inputs** (`user_inputs`): Configuration provided by users
62/// 2. **Internal Config** (`internal_config`): Derived paths for organizing artifacts
63/// 3. **Runtime Outputs** (`runtime_outputs`): Data generated during deployment
64///
65/// This separation makes it clear where each piece of information comes from
66/// and helps developers understand where to add new fields.
67///
68/// # Design Rationale
69///
70/// By separating state-independent data from the state machine and organizing
71/// it into three semantic categories, we:
72/// - Eliminate repetitive pattern matching in `AnyEnvironmentState`
73/// - Make it clear which data is constant vs. state-dependent
74/// - Provide semantic clarity about the purpose of each field
75/// - Guide developers where to add new fields based on their purpose
76/// - Simplify state transitions (only the state field changes)
77/// - Enable easier extension of environment configuration
78///
79/// # Three Semantic Categories
80///
81/// - **User Inputs**: Immutable user configuration (name, SSH credentials, port)
82/// - **Internal Config**: Derived paths (`build_dir`, `data_dir`)
83/// - **Runtime Outputs**: Generated during deployment (`instance_ip`, future metrics)
84///
85/// # Examples
86///
87/// `EnvironmentContext` is typically created internally by `Environment::new()`:
88///
89/// ```rust
90/// use torrust_tracker_deployer_lib::domain::environment::{Environment, EnvironmentName};
91/// use torrust_tracker_deployer_lib::domain::provider::{LxdConfig, ProviderConfig};
92/// use torrust_tracker_deployer_lib::domain::ProfileName;
93/// use torrust_tracker_deployer_lib::shared::Username;
94/// use torrust_tracker_deployer_lib::adapters::ssh::SshCredentials;
95/// use std::path::PathBuf;
96/// use chrono::{TimeZone, Utc};
97///
98/// let env_name = EnvironmentName::new("production".to_string())?;
99/// let ssh_username = Username::new("torrust".to_string())?;
100/// let ssh_credentials = SshCredentials::new(
101/// PathBuf::from("keys/prod_rsa"),
102/// PathBuf::from("keys/prod_rsa.pub"),
103/// ssh_username,
104/// );
105/// let provider_config = ProviderConfig::Lxd(LxdConfig {
106/// profile_name: ProfileName::new(format!("lxd-{}", env_name.as_str())).unwrap(),
107/// });
108///
109/// // Environment::new() creates the EnvironmentContext internally
110/// let created_at = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
111/// let environment = Environment::new(env_name, provider_config, ssh_credentials, 22, created_at);
112///
113/// // Access the context through the environment
114/// let context = environment.context();
115/// // Context holds all state-independent data for the environment
116///
117/// # Ok::<(), Box<dyn std::error::Error>>(())
118/// ```
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct EnvironmentContext {
121 /// Timestamp when the environment was created
122 ///
123 /// This field records the exact moment when the environment was first created
124 /// using the `create environment` command. It never changes throughout the
125 /// environment lifecycle.
126 #[serde(default = "default_created_at")]
127 pub created_at: DateTime<Utc>,
128
129 /// User-provided configuration
130 pub user_inputs: UserInputs,
131
132 /// Internal paths and derived configuration
133 pub internal_config: InternalConfig,
134
135 /// Runtime outputs from deployment operations
136 pub runtime_outputs: RuntimeOutputs,
137}
138
139impl EnvironmentContext {
140 /// Creates a new `EnvironmentContext` with auto-generated names and paths
141 ///
142 /// # Arguments
143 ///
144 /// * `name` - The validated environment name
145 /// * `provider_config` - Provider-specific configuration (LXD, Hetzner, etc.)
146 /// * `ssh_credentials` - SSH credentials for connecting to instances
147 /// * `ssh_port` - SSH port for connecting to instances
148 ///
149 /// # Returns
150 ///
151 /// A new `EnvironmentContext` with:
152 /// - Auto-generated instance name: `torrust-tracker-vm-{env_name}`
153 /// - Provider configuration with validated settings
154 /// - Auto-generated data and build directories
155 /// - Empty runtime outputs
156 ///
157 /// # Examples
158 ///
159 /// ```rust
160 /// use torrust_tracker_deployer_lib::domain::environment::{EnvironmentContext, EnvironmentName};
161 /// use torrust_tracker_deployer_lib::domain::provider::{ProviderConfig, LxdConfig};
162 /// use torrust_tracker_deployer_lib::domain::ProfileName;
163 /// use torrust_tracker_deployer_lib::shared::Username;
164 /// use torrust_tracker_deployer_lib::adapters::ssh::SshCredentials;
165 /// use std::path::PathBuf;
166 /// use chrono::{TimeZone, Utc};
167 ///
168 /// let env_name = EnvironmentName::new("production".to_string())?;
169 /// let ssh_username = Username::new("torrust".to_string())?;
170 /// let ssh_credentials = SshCredentials::new(
171 /// PathBuf::from("keys/prod_rsa"),
172 /// PathBuf::from("keys/prod_rsa.pub"),
173 /// ssh_username,
174 /// );
175 /// let provider_config = ProviderConfig::Lxd(LxdConfig {
176 /// profile_name: ProfileName::new("torrust-profile-production".to_string())?,
177 /// });
178 ///
179 /// let created_at = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
180 /// let context = EnvironmentContext::new(&env_name, provider_config, ssh_credentials, 22, created_at);
181 ///
182 /// assert_eq!(context.user_inputs.instance_name().as_str(), "torrust-tracker-vm-production");
183 /// let lxd_config = context.user_inputs.provider_config().as_lxd().unwrap();
184 /// assert_eq!(lxd_config.profile_name.as_str(), "torrust-profile-production");
185 /// assert_eq!(context.internal_config.data_dir, PathBuf::from("./data/production"));
186 /// assert_eq!(context.internal_config.build_dir, PathBuf::from("./build/production"));
187 ///
188 /// # Ok::<(), Box<dyn std::error::Error>>(())
189 /// ```
190 ///
191 /// # Panics
192 ///
193 /// This function does not panic. All name generation is guaranteed to succeed
194 /// for valid environment names.
195 #[must_use]
196 pub fn new(
197 name: &EnvironmentName,
198 provider_config: ProviderConfig,
199 ssh_credentials: SshCredentials,
200 ssh_port: u16,
201 created_at: DateTime<Utc>,
202 ) -> Self {
203 Self {
204 created_at,
205 user_inputs: UserInputs::new(name, provider_config, ssh_credentials, ssh_port)
206 .expect("UserInputs::new with defaults should never fail - default config always passes validation"),
207 internal_config: InternalConfig::new(name),
208 runtime_outputs: RuntimeOutputs::new(),
209 }
210 }
211
212 /// Creates a new environment context from validated parameters
213 ///
214 /// This creates absolute paths for data and build directories by using the
215 /// provided working directory as the base.
216 ///
217 /// # Arguments
218 ///
219 /// * `params` - Validated environment parameters (domain value object)
220 /// * `working_dir` - Base directory for data and build directories
221 /// * `created_at` - Timestamp for context creation
222 ///
223 /// # Errors
224 ///
225 /// Returns `UserInputsError` if cross-service invariant validation fails:
226 /// - `GrafanaRequiresPrometheus` if Grafana is configured without Prometheus
227 /// - `HttpsSectionWithoutTlsServices` if HTTPS section exists but no service uses TLS
228 /// - `TlsServicesWithoutHttpsSection` if a service uses TLS but HTTPS section is missing
229 pub fn create(
230 params: EnvironmentParams,
231 working_dir: &std::path::Path,
232 created_at: DateTime<Utc>,
233 ) -> Result<Self, crate::domain::environment::UserInputsError> {
234 Ok(Self {
235 created_at,
236 user_inputs: UserInputs::with_tracker(
237 ¶ms.environment_name,
238 params.provider_config,
239 params.ssh_credentials,
240 params.ssh_port,
241 params.tracker_config,
242 params.prometheus_config,
243 params.grafana_config,
244 params.https_config,
245 params.backup_config,
246 )?,
247 internal_config: InternalConfig::with_working_dir(
248 ¶ms.environment_name,
249 working_dir,
250 ),
251 runtime_outputs: RuntimeOutputs::new(),
252 })
253 }
254
255 /// Returns the SSH username for this environment
256 #[must_use]
257 pub fn ssh_username(&self) -> &crate::shared::Username {
258 &self.user_inputs.ssh_credentials().ssh_username
259 }
260
261 /// Returns the SSH private key path for this environment
262 #[must_use]
263 pub fn ssh_private_key_path(&self) -> &PathBuf {
264 &self.user_inputs.ssh_credentials().ssh_priv_key_path
265 }
266
267 /// Returns the SSH public key path for this environment
268 #[must_use]
269 pub fn ssh_public_key_path(&self) -> &PathBuf {
270 &self.user_inputs.ssh_credentials().ssh_pub_key_path
271 }
272
273 /// Returns the templates directory for this environment
274 ///
275 /// Path: `data/{env_name}/templates/`
276 #[must_use]
277 pub fn templates_dir(&self) -> PathBuf {
278 self.internal_config.templates_dir()
279 }
280
281 /// Returns the traces directory for this environment
282 ///
283 /// Path: `data/{env_name}/traces/`
284 #[must_use]
285 pub fn traces_dir(&self) -> PathBuf {
286 self.internal_config.traces_dir()
287 }
288
289 /// Returns the ansible build directory
290 ///
291 /// Path: `build/{env_name}/ansible`
292 #[must_use]
293 pub fn ansible_build_dir(&self) -> PathBuf {
294 self.internal_config.ansible_build_dir()
295 }
296
297 /// Returns the tofu build directory for the environment's provider
298 ///
299 /// Path: `build/{env_name}/tofu/{provider_name}`
300 ///
301 /// The provider is determined from the environment's provider
302 /// configuration (e.g., LXD, Hetzner).
303 #[must_use]
304 pub fn tofu_build_dir(&self) -> PathBuf {
305 let provider = self.user_inputs.provider_config().provider();
306 self.internal_config.tofu_build_dir_for_provider(provider)
307 }
308
309 /// Returns the ansible templates directory
310 ///
311 /// Path: `data/{env_name}/templates/ansible`
312 #[must_use]
313 pub fn ansible_templates_dir(&self) -> PathBuf {
314 self.internal_config.ansible_templates_dir()
315 }
316
317 /// Returns the tofu templates directory
318 ///
319 /// Path: `data/{env_name}/templates/tofu`
320 #[must_use]
321 pub fn tofu_templates_dir(&self) -> PathBuf {
322 self.internal_config.tofu_templates_dir()
323 }
324
325 /// Returns the environment name
326 #[must_use]
327 pub fn name(&self) -> &EnvironmentName {
328 self.user_inputs.name()
329 }
330
331 /// Returns the instance name
332 #[must_use]
333 pub fn instance_name(&self) -> &crate::domain::InstanceName {
334 self.user_inputs.instance_name()
335 }
336
337 /// Returns the provider configuration
338 #[must_use]
339 pub fn provider_config(&self) -> &ProviderConfig {
340 self.user_inputs.provider_config()
341 }
342
343 /// Returns the SSH credentials
344 #[must_use]
345 pub fn ssh_credentials(&self) -> &SshCredentials {
346 self.user_inputs.ssh_credentials()
347 }
348
349 /// Returns the SSH port
350 #[must_use]
351 pub fn ssh_port(&self) -> u16 {
352 self.user_inputs.ssh_port()
353 }
354
355 /// Returns the database configuration
356 #[must_use]
357 pub fn database_config(&self) -> &crate::domain::tracker::DatabaseConfig {
358 self.user_inputs.tracker().core().database()
359 }
360
361 /// Returns the tracker configuration
362 #[must_use]
363 pub fn tracker_config(&self) -> &crate::domain::tracker::TrackerConfig {
364 self.user_inputs.tracker()
365 }
366
367 /// Returns the admin token
368 #[must_use]
369 pub fn admin_token(&self) -> &str {
370 self.user_inputs
371 .tracker()
372 .http_api()
373 .admin_token()
374 .expose_secret()
375 }
376
377 /// Returns the Prometheus configuration if enabled
378 #[must_use]
379 pub fn prometheus_config(&self) -> Option<&PrometheusConfig> {
380 self.user_inputs.prometheus()
381 }
382
383 /// Returns the Grafana configuration if enabled
384 #[must_use]
385 pub fn grafana_config(&self) -> Option<&GrafanaConfig> {
386 self.user_inputs.grafana()
387 }
388
389 /// Returns the Backup configuration if enabled
390 #[must_use]
391 pub fn backup_config(&self) -> Option<&BackupConfig> {
392 self.user_inputs.backup()
393 }
394
395 /// Returns the build directory
396 #[must_use]
397 pub fn build_dir(&self) -> &PathBuf {
398 &self.internal_config.build_dir
399 }
400
401 /// Returns the data directory
402 #[must_use]
403 pub fn data_dir(&self) -> &PathBuf {
404 &self.internal_config.data_dir
405 }
406
407 /// Returns the instance IP address if available
408 #[must_use]
409 pub fn instance_ip(&self) -> Option<std::net::IpAddr> {
410 self.runtime_outputs.instance_ip()
411 }
412
413 /// Returns the provision method
414 #[must_use]
415 pub fn provision_method(&self) -> Option<crate::domain::environment::ProvisionMethod> {
416 self.runtime_outputs.provision_method()
417 }
418
419 /// Returns the creation timestamp
420 #[must_use]
421 pub fn created_at(&self) -> DateTime<Utc> {
422 self.created_at
423 }
424}