Skip to main content

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                &params.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                &params.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}