torrust-tracker-deployer 0.1.0-beta.2

Torrust Tracker Deployer - Deployment Infrastructure with Ansible and OpenTofu
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
//! Environment Context Module
//!
//! This module contains the `EnvironmentContext` struct which composes three
//! semantic types to organize state-independent environment data.
//!
//! ## Purpose
//!
//! The `EnvironmentContext` separates immutable environment configuration from
//! the mutable state machine, and further organizes that configuration into
//! three distinct semantic categories:
//!
//! 1. **User Inputs** - Configuration provided by users
//! 2. **Internal Config** - Derived paths for organizing artifacts
//! 3. **Runtime Outputs** - Data generated during deployment
//!
//! ## Benefits
//!
//! - **Reduced pattern matching**: Access common fields without matching on state (83% reduction)
//! - **Clear semantic boundaries**: Types document the purpose of each field
//! - **Developer guidance**: Clear where to add new fields based on their purpose
//! - **Simplified state transitions**: Only the state changes, context remains constant
//! - **Easier extension**: Adding fields is straightforward with clear categorization
//!
//! ## Three-Way Semantic Split
//!
//! ### When to Add Fields
//!
//! - **`UserInputs`**: User needs to configure something at environment creation time
//! - **`InternalConfig`**: Need internal paths or derived configuration
//! - **`RuntimeOutputs`**: Operations produce new data about deployed infrastructure
//!
//! ### Design Rationale
//!
//! By organizing fields into three semantic categories, we make it immediately
//! clear where each piece of information comes from and guide developers on
//! where to add new fields as the application evolves.

use crate::adapters::ssh::SshCredentials;
use crate::domain::backup::BackupConfig;
use crate::domain::environment::{
    EnvironmentName, EnvironmentParams, InternalConfig, RuntimeOutputs, UserInputs,
};
use crate::domain::grafana::GrafanaConfig;
use crate::domain::prometheus::PrometheusConfig;
use crate::domain::provider::ProviderConfig;
use chrono::{DateTime, TimeZone, Utc};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

/// Default value for `created_at` field for backward compatibility
///
/// Returns Unix epoch (1970-01-01 00:00:00 UTC) for environments created
/// before the `created_at` field was added.
fn default_created_at() -> DateTime<Utc> {
    Utc.timestamp_opt(0, 0).unwrap()
}

/// Complete environment context composed of three semantic types
///
/// The context is split into three logical categories:
/// 1. **User Inputs** (`user_inputs`): Configuration provided by users
/// 2. **Internal Config** (`internal_config`): Derived paths for organizing artifacts
/// 3. **Runtime Outputs** (`runtime_outputs`): Data generated during deployment
///
/// This separation makes it clear where each piece of information comes from
/// and helps developers understand where to add new fields.
///
/// # Design Rationale
///
/// By separating state-independent data from the state machine and organizing
/// it into three semantic categories, we:
/// - Eliminate repetitive pattern matching in `AnyEnvironmentState`
/// - Make it clear which data is constant vs. state-dependent
/// - Provide semantic clarity about the purpose of each field
/// - Guide developers where to add new fields based on their purpose
/// - Simplify state transitions (only the state field changes)
/// - Enable easier extension of environment configuration
///
/// # Three Semantic Categories
///
/// - **User Inputs**: Immutable user configuration (name, SSH credentials, port)
/// - **Internal Config**: Derived paths (`build_dir`, `data_dir`)
/// - **Runtime Outputs**: Generated during deployment (`instance_ip`, future metrics)
///
/// # Examples
///
/// `EnvironmentContext` is typically created internally by `Environment::new()`:
///
/// ```rust
/// use torrust_tracker_deployer_lib::domain::environment::{Environment, EnvironmentName};
/// use torrust_tracker_deployer_lib::domain::provider::{LxdConfig, ProviderConfig};
/// use torrust_tracker_deployer_lib::domain::ProfileName;
/// use torrust_tracker_deployer_lib::shared::Username;
/// use torrust_tracker_deployer_lib::adapters::ssh::SshCredentials;
/// use std::path::PathBuf;
/// use chrono::{TimeZone, Utc};
///
/// let env_name = EnvironmentName::new("production".to_string())?;
/// let ssh_username = Username::new("torrust".to_string())?;
/// let ssh_credentials = SshCredentials::new(
///     PathBuf::from("keys/prod_rsa"),
///     PathBuf::from("keys/prod_rsa.pub"),
///     ssh_username,
/// );
/// let provider_config = ProviderConfig::Lxd(LxdConfig {
///     profile_name: ProfileName::new(format!("lxd-{}", env_name.as_str())).unwrap(),
/// });
///
/// // Environment::new() creates the EnvironmentContext internally
/// let created_at = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
/// let environment = Environment::new(env_name, provider_config, ssh_credentials, 22, created_at);
///
/// // Access the context through the environment
/// let context = environment.context();
/// // Context holds all state-independent data for the environment
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnvironmentContext {
    /// Timestamp when the environment was created
    ///
    /// This field records the exact moment when the environment was first created
    /// using the `create environment` command. It never changes throughout the
    /// environment lifecycle.
    #[serde(default = "default_created_at")]
    pub created_at: DateTime<Utc>,

    /// User-provided configuration
    pub user_inputs: UserInputs,

    /// Internal paths and derived configuration
    pub internal_config: InternalConfig,

    /// Runtime outputs from deployment operations
    pub runtime_outputs: RuntimeOutputs,
}

impl EnvironmentContext {
    /// Creates a new `EnvironmentContext` with auto-generated names and paths
    ///
    /// # Arguments
    ///
    /// * `name` - The validated environment name
    /// * `provider_config` - Provider-specific configuration (LXD, Hetzner, etc.)
    /// * `ssh_credentials` - SSH credentials for connecting to instances
    /// * `ssh_port` - SSH port for connecting to instances
    ///
    /// # Returns
    ///
    /// A new `EnvironmentContext` with:
    /// - Auto-generated instance name: `torrust-tracker-vm-{env_name}`
    /// - Provider configuration with validated settings
    /// - Auto-generated data and build directories
    /// - Empty runtime outputs
    ///
    /// # Examples
    ///
    /// ```rust
    /// use torrust_tracker_deployer_lib::domain::environment::{EnvironmentContext, EnvironmentName};
    /// use torrust_tracker_deployer_lib::domain::provider::{ProviderConfig, LxdConfig};
    /// use torrust_tracker_deployer_lib::domain::ProfileName;
    /// use torrust_tracker_deployer_lib::shared::Username;
    /// use torrust_tracker_deployer_lib::adapters::ssh::SshCredentials;
    /// use std::path::PathBuf;
    /// use chrono::{TimeZone, Utc};
    ///
    /// let env_name = EnvironmentName::new("production".to_string())?;
    /// let ssh_username = Username::new("torrust".to_string())?;
    /// let ssh_credentials = SshCredentials::new(
    ///     PathBuf::from("keys/prod_rsa"),
    ///     PathBuf::from("keys/prod_rsa.pub"),
    ///     ssh_username,
    /// );
    /// let provider_config = ProviderConfig::Lxd(LxdConfig {
    ///     profile_name: ProfileName::new("torrust-profile-production".to_string())?,
    /// });
    ///
    /// let created_at = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
    /// let context = EnvironmentContext::new(&env_name, provider_config, ssh_credentials, 22, created_at);
    ///
    /// assert_eq!(context.user_inputs.instance_name().as_str(), "torrust-tracker-vm-production");
    /// let lxd_config = context.user_inputs.provider_config().as_lxd().unwrap();
    /// assert_eq!(lxd_config.profile_name.as_str(), "torrust-profile-production");
    /// assert_eq!(context.internal_config.data_dir, PathBuf::from("./data/production"));
    /// assert_eq!(context.internal_config.build_dir, PathBuf::from("./build/production"));
    ///
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    ///
    /// # Panics
    ///
    /// This function does not panic. All name generation is guaranteed to succeed
    /// for valid environment names.
    #[must_use]
    pub fn new(
        name: &EnvironmentName,
        provider_config: ProviderConfig,
        ssh_credentials: SshCredentials,
        ssh_port: u16,
        created_at: DateTime<Utc>,
    ) -> Self {
        Self {
            created_at,
            user_inputs: UserInputs::new(name, provider_config, ssh_credentials, ssh_port)
                .expect("UserInputs::new with defaults should never fail - default config always passes validation"),
            internal_config: InternalConfig::new(name),
            runtime_outputs: RuntimeOutputs::new(),
        }
    }

    /// Creates a new environment context from validated parameters
    ///
    /// This creates absolute paths for data and build directories by using the
    /// provided working directory as the base.
    ///
    /// # Arguments
    ///
    /// * `params` - Validated environment parameters (domain value object)
    /// * `working_dir` - Base directory for data and build directories
    /// * `created_at` - Timestamp for context creation
    ///
    /// # Errors
    ///
    /// Returns `UserInputsError` if cross-service invariant validation fails:
    /// - `GrafanaRequiresPrometheus` if Grafana is configured without Prometheus
    /// - `HttpsSectionWithoutTlsServices` if HTTPS section exists but no service uses TLS
    /// - `TlsServicesWithoutHttpsSection` if a service uses TLS but HTTPS section is missing
    pub fn create(
        params: EnvironmentParams,
        working_dir: &std::path::Path,
        created_at: DateTime<Utc>,
    ) -> Result<Self, crate::domain::environment::UserInputsError> {
        Ok(Self {
            created_at,
            user_inputs: UserInputs::with_tracker(
                &params.environment_name,
                params.provider_config,
                params.ssh_credentials,
                params.ssh_port,
                params.tracker_config,
                params.prometheus_config,
                params.grafana_config,
                params.https_config,
                params.backup_config,
            )?,
            internal_config: InternalConfig::with_working_dir(
                &params.environment_name,
                working_dir,
            ),
            runtime_outputs: RuntimeOutputs::new(),
        })
    }

    /// Returns the SSH username for this environment
    #[must_use]
    pub fn ssh_username(&self) -> &crate::shared::Username {
        &self.user_inputs.ssh_credentials().ssh_username
    }

    /// Returns the SSH private key path for this environment
    #[must_use]
    pub fn ssh_private_key_path(&self) -> &PathBuf {
        &self.user_inputs.ssh_credentials().ssh_priv_key_path
    }

    /// Returns the SSH public key path for this environment
    #[must_use]
    pub fn ssh_public_key_path(&self) -> &PathBuf {
        &self.user_inputs.ssh_credentials().ssh_pub_key_path
    }

    /// Returns the templates directory for this environment
    ///
    /// Path: `data/{env_name}/templates/`
    #[must_use]
    pub fn templates_dir(&self) -> PathBuf {
        self.internal_config.templates_dir()
    }

    /// Returns the traces directory for this environment
    ///
    /// Path: `data/{env_name}/traces/`
    #[must_use]
    pub fn traces_dir(&self) -> PathBuf {
        self.internal_config.traces_dir()
    }

    /// Returns the ansible build directory
    ///
    /// Path: `build/{env_name}/ansible`
    #[must_use]
    pub fn ansible_build_dir(&self) -> PathBuf {
        self.internal_config.ansible_build_dir()
    }

    /// Returns the tofu build directory for the environment's provider
    ///
    /// Path: `build/{env_name}/tofu/{provider_name}`
    ///
    /// The provider is determined from the environment's provider
    /// configuration (e.g., LXD, Hetzner).
    #[must_use]
    pub fn tofu_build_dir(&self) -> PathBuf {
        let provider = self.user_inputs.provider_config().provider();
        self.internal_config.tofu_build_dir_for_provider(provider)
    }

    /// Returns the ansible templates directory
    ///
    /// Path: `data/{env_name}/templates/ansible`
    #[must_use]
    pub fn ansible_templates_dir(&self) -> PathBuf {
        self.internal_config.ansible_templates_dir()
    }

    /// Returns the tofu templates directory
    ///
    /// Path: `data/{env_name}/templates/tofu`
    #[must_use]
    pub fn tofu_templates_dir(&self) -> PathBuf {
        self.internal_config.tofu_templates_dir()
    }

    /// Returns the environment name
    #[must_use]
    pub fn name(&self) -> &EnvironmentName {
        self.user_inputs.name()
    }

    /// Returns the instance name
    #[must_use]
    pub fn instance_name(&self) -> &crate::domain::InstanceName {
        self.user_inputs.instance_name()
    }

    /// Returns the provider configuration
    #[must_use]
    pub fn provider_config(&self) -> &ProviderConfig {
        self.user_inputs.provider_config()
    }

    /// Returns the SSH credentials
    #[must_use]
    pub fn ssh_credentials(&self) -> &SshCredentials {
        self.user_inputs.ssh_credentials()
    }

    /// Returns the SSH port
    #[must_use]
    pub fn ssh_port(&self) -> u16 {
        self.user_inputs.ssh_port()
    }

    /// Returns the database configuration
    #[must_use]
    pub fn database_config(&self) -> &crate::domain::tracker::DatabaseConfig {
        self.user_inputs.tracker().core().database()
    }

    /// Returns the tracker configuration
    #[must_use]
    pub fn tracker_config(&self) -> &crate::domain::tracker::TrackerConfig {
        self.user_inputs.tracker()
    }

    /// Returns the admin token
    #[must_use]
    pub fn admin_token(&self) -> &str {
        self.user_inputs
            .tracker()
            .http_api()
            .admin_token()
            .expose_secret()
    }

    /// Returns the Prometheus configuration if enabled
    #[must_use]
    pub fn prometheus_config(&self) -> Option<&PrometheusConfig> {
        self.user_inputs.prometheus()
    }

    /// Returns the Grafana configuration if enabled
    #[must_use]
    pub fn grafana_config(&self) -> Option<&GrafanaConfig> {
        self.user_inputs.grafana()
    }

    /// Returns the Backup configuration if enabled
    #[must_use]
    pub fn backup_config(&self) -> Option<&BackupConfig> {
        self.user_inputs.backup()
    }

    /// Returns the build directory
    #[must_use]
    pub fn build_dir(&self) -> &PathBuf {
        &self.internal_config.build_dir
    }

    /// Returns the data directory
    #[must_use]
    pub fn data_dir(&self) -> &PathBuf {
        &self.internal_config.data_dir
    }

    /// Returns the instance IP address if available
    #[must_use]
    pub fn instance_ip(&self) -> Option<std::net::IpAddr> {
        self.runtime_outputs.instance_ip()
    }

    /// Returns the provision method
    #[must_use]
    pub fn provision_method(&self) -> Option<crate::domain::environment::ProvisionMethod> {
        self.runtime_outputs.provision_method()
    }

    /// Returns the creation timestamp
    #[must_use]
    pub fn created_at(&self) -> DateTime<Utc> {
        self.created_at
    }
}