Skip to main content

torrust_tracker_deployer_lib/domain/environment/
runtime_outputs.rs

1//! Runtime Outputs Module
2//!
3//! This module contains the `RuntimeOutputs` struct which holds data generated
4//! during deployment operations.
5//!
6//! ## Purpose
7//!
8//! Runtime outputs represent data that is produced as deployment operations
9//! execute. These fields are mutable and grow as the deployment progresses.
10//!
11//! ## Semantic Category
12//!
13//! **Runtime Outputs** are:
14//! - Generated during deployment operations
15//! - Mutable as operations progress
16//! - Examples: IP addresses, container IDs, service URLs
17//!
18//! Add new fields here when: Operations produce new data about the deployed infrastructure.
19//!
20//! ## Future Extensions
21//!
22//! This struct is expected to grow with fields like:
23//! - `container_id: Option<String>` - Container/VM identifier
24//! - `resource_metrics: Option<ResourceMetrics>` - CPU, memory, disk usage
25
26use serde::{Deserialize, Serialize};
27use std::net::IpAddr;
28use url::Url;
29
30/// How the infrastructure instance was provisioned
31///
32/// This enum tracks the method used to provision the infrastructure, which
33/// affects how the environment can be destroyed and other lifecycle operations.
34#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35pub enum ProvisionMethod {
36    /// Instance was provisioned using `OpenTofu` (infrastructure as code)
37    ///
38    /// This method creates new infrastructure that can be destroyed using
39    /// `tofu destroy`. The infrastructure lifecycle is fully managed.
40    #[default]
41    Provisioned,
42
43    /// Instance was registered from existing infrastructure
44    ///
45    /// This method connects to existing infrastructure (VMs, containers, physical servers)
46    /// that was created externally. The infrastructure cannot be destroyed by this tool;
47    /// the `destroy` command will only clean up local state, not the actual instance.
48    Registered,
49}
50
51impl std::fmt::Display for ProvisionMethod {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        match self {
54            Self::Provisioned => write!(f, "provisioned"),
55            Self::Registered => write!(f, "registered"),
56        }
57    }
58}
59
60/// Service endpoints for deployed tracker services
61///
62/// This struct stores the URLs for all deployed tracker services. These URLs
63/// are computed from the tracker configuration and instance IP after the
64/// `run` command successfully starts the services.
65///
66/// # Purpose
67///
68/// Having service endpoints as first-class data allows:
69/// - Displaying service URLs without recomputation
70/// - Sharing URLs with external tools/integrations
71/// - Validating service availability against stored endpoints
72///
73/// # Examples
74///
75/// ```rust
76/// use torrust_tracker_deployer_lib::domain::environment::runtime_outputs::ServiceEndpoints;
77/// use url::Url;
78///
79/// let endpoints = ServiceEndpoints {
80///     udp_trackers: vec![
81///         Url::parse("udp://10.0.0.1:6969/announce").unwrap(),
82///     ],
83///     http_trackers: vec![
84///         Url::parse("http://10.0.0.1:7070/announce").unwrap(),
85///     ],
86///     api_endpoint: Some(Url::parse("http://10.0.0.1:1212/api").unwrap()),
87///     health_check_url: Some(Url::parse("http://10.0.0.1:1313/health_check").unwrap()),
88/// };
89/// ```
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct ServiceEndpoints {
92    /// UDP tracker announce URLs (e.g., `udp://10.0.0.1:6969/announce`)
93    #[serde(default)]
94    pub udp_trackers: Vec<Url>,
95
96    /// HTTP tracker announce URLs (e.g., `http://10.0.0.1:7070/announce`)
97    #[serde(default)]
98    pub http_trackers: Vec<Url>,
99
100    /// HTTP API endpoint URL (e.g., `http://10.0.0.1:1212/api`)
101    pub api_endpoint: Option<Url>,
102
103    /// Health check API URL (e.g., `http://10.0.0.1:1313/health_check`)
104    pub health_check_url: Option<Url>,
105}
106
107impl ServiceEndpoints {
108    /// Create new `ServiceEndpoints` from the provided URLs
109    #[must_use]
110    pub fn new(
111        udp_trackers: Vec<Url>,
112        http_trackers: Vec<Url>,
113        api_endpoint: Option<Url>,
114        health_check_url: Option<Url>,
115    ) -> Self {
116        Self {
117            udp_trackers,
118            http_trackers,
119            api_endpoint,
120            health_check_url,
121        }
122    }
123
124    /// Build `ServiceEndpoints` from tracker configuration and instance IP
125    ///
126    /// Constructs service URLs by combining the configured bind addresses
127    /// with the actual instance IP address.
128    ///
129    /// # Examples
130    ///
131    /// ```rust
132    /// use torrust_tracker_deployer_lib::domain::environment::runtime_outputs::ServiceEndpoints;
133    /// use torrust_tracker_deployer_lib::domain::tracker::TrackerConfig;
134    /// use std::net::{IpAddr, Ipv4Addr};
135    ///
136    /// let tracker_config = TrackerConfig::default();
137    /// let instance_ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
138    ///
139    /// let endpoints = ServiceEndpoints::from_tracker_config(&tracker_config, instance_ip);
140    /// ```
141    #[must_use]
142    pub fn from_tracker_config(
143        tracker_config: &crate::domain::tracker::TrackerConfig,
144        instance_ip: IpAddr,
145    ) -> Self {
146        let udp_trackers = Self::build_udp_tracker_urls(tracker_config.udp_trackers(), instance_ip);
147        let http_trackers =
148            Self::build_http_tracker_urls(tracker_config.http_trackers(), instance_ip);
149        let api_endpoint =
150            Self::build_api_endpoint_url(tracker_config.http_api().bind_address(), instance_ip);
151        let health_check_url = Self::build_health_check_url(
152            tracker_config.health_check_api().bind_address(),
153            instance_ip,
154        );
155
156        Self::new(udp_trackers, http_trackers, api_endpoint, health_check_url)
157    }
158
159    fn build_udp_tracker_urls(
160        udp_trackers: &[crate::domain::tracker::UdpTrackerConfig],
161        instance_ip: IpAddr,
162    ) -> Vec<Url> {
163        udp_trackers
164            .iter()
165            .filter_map(|udp| {
166                Url::parse(&format!(
167                    "udp://{}:{}/announce",
168                    instance_ip,
169                    udp.bind_address().port()
170                ))
171                .ok()
172            })
173            .collect()
174    }
175
176    fn build_http_tracker_urls(
177        http_trackers: &[crate::domain::tracker::HttpTrackerConfig],
178        instance_ip: IpAddr,
179    ) -> Vec<Url> {
180        http_trackers
181            .iter()
182            .filter_map(|http| {
183                Url::parse(&format!(
184                    "http://{}:{}/announce", // DevSkim: ignore DS137138
185                    instance_ip,
186                    http.bind_address().port()
187                ))
188                .ok()
189            })
190            .collect()
191    }
192
193    fn build_api_endpoint_url(
194        bind_address: std::net::SocketAddr,
195        instance_ip: IpAddr,
196    ) -> Option<Url> {
197        Url::parse(&format!(
198            "http://{}:{}/api", // DevSkim: ignore DS137138
199            instance_ip,
200            bind_address.port()
201        ))
202        .ok()
203    }
204
205    fn build_health_check_url(
206        bind_address: std::net::SocketAddr,
207        instance_ip: IpAddr,
208    ) -> Option<Url> {
209        Url::parse(&format!(
210            "http://{}:{}/health_check", // DevSkim: ignore DS137138
211            instance_ip,
212            bind_address.port()
213        ))
214        .ok()
215    }
216}
217
218/// Runtime outputs generated during deployment operations
219///
220/// This struct contains fields that are generated during deployment operations
221/// and represent the runtime state of deployed infrastructure. Fields are
222/// private to protect invariants and provide semantic clarity through setters.
223///
224/// # Lifecycle
225///
226/// Fields are populated at different stages of the deployment lifecycle:
227/// - **Creation**: All fields are `None` (use `RuntimeOutputs::new()`)
228/// - **After Provisioning**: `instance_ip` and `provision_method` are set
229///   (use `record_provisioning()` or `record_registration()`)
230/// - **After Run Command**: `service_endpoints` is set
231///   (use `record_services_started()`)
232///
233/// # Future Fields
234///
235/// This struct is expected to grow as deployment operations become more complex:
236/// - `container_id: Option<String>` - Container/VM identifier
237/// - `resource_metrics: Option<ResourceMetrics>` - CPU, memory, disk usage
238///
239/// # Examples
240///
241/// ```rust
242/// use torrust_tracker_deployer_lib::domain::environment::runtime_outputs::{RuntimeOutputs, ProvisionMethod};
243/// use std::net::{IpAddr, Ipv4Addr};
244///
245/// // Create empty runtime outputs
246/// let mut runtime_outputs = RuntimeOutputs::new();
247/// assert!(runtime_outputs.instance_ip().is_none());
248///
249/// // After provisioning, record the IP and method
250/// let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100));
251/// runtime_outputs.record_provisioning(ip);
252/// assert_eq!(runtime_outputs.instance_ip(), Some(ip));
253/// assert_eq!(runtime_outputs.provision_method(), Some(ProvisionMethod::Provisioned));
254/// ```
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct RuntimeOutputs {
257    /// Instance IP address (populated after provisioning)
258    ///
259    /// This field stores the IP address of the provisioned instance and is
260    /// `None` until the environment has been successfully provisioned.
261    instance_ip: Option<IpAddr>,
262
263    /// How the instance was provisioned
264    ///
265    /// This field tracks whether the instance was created via `OpenTofu` (`Provisioned`)
266    /// or registered from existing infrastructure (`Registered`). This affects
267    /// lifecycle operations like `destroy`.
268    ///
269    /// - `None`: Unknown or legacy state (before this field was added)
270    /// - `Some(Provisioned)`: Instance was created via `provision` command
271    /// - `Some(Registered)`: Instance was connected via `register` command
272    #[serde(default)]
273    provision_method: Option<ProvisionMethod>,
274
275    /// Service endpoints populated after services are started
276    ///
277    /// This field stores the URLs for all deployed tracker services. It is
278    /// populated by the `run` command after services start successfully.
279    ///
280    /// - `None`: Services not yet started or legacy state
281    /// - `Some(endpoints)`: URLs for all running services
282    #[serde(default)]
283    service_endpoints: Option<ServiceEndpoints>,
284}
285
286impl RuntimeOutputs {
287    /// Creates new empty runtime outputs
288    ///
289    /// All fields are initialized to `None`, representing an environment
290    /// that has not yet been provisioned or run.
291    ///
292    /// # Examples
293    ///
294    /// ```rust
295    /// use torrust_tracker_deployer_lib::domain::environment::runtime_outputs::RuntimeOutputs;
296    ///
297    /// let outputs = RuntimeOutputs::new();
298    /// assert!(outputs.instance_ip().is_none());
299    /// assert!(outputs.provision_method().is_none());
300    /// assert!(outputs.service_endpoints().is_none());
301    /// ```
302    #[must_use]
303    pub fn new() -> Self {
304        Self {
305            instance_ip: None,
306            provision_method: None,
307            service_endpoints: None,
308        }
309    }
310
311    // =========================================================================
312    // Getters - Access runtime output values
313    // =========================================================================
314
315    /// Returns the instance IP address if available
316    ///
317    /// This is `None` until the environment has been provisioned or registered.
318    #[must_use]
319    pub fn instance_ip(&self) -> Option<IpAddr> {
320        self.instance_ip
321    }
322
323    /// Returns how the instance was provisioned
324    ///
325    /// - `None`: Unknown or legacy state
326    /// - `Some(Provisioned)`: Created via `provision` command
327    /// - `Some(Registered)`: Connected via `register` command
328    #[must_use]
329    pub fn provision_method(&self) -> Option<ProvisionMethod> {
330        self.provision_method
331    }
332
333    /// Returns the service endpoints if available
334    ///
335    /// This is `None` until the `run` command has started services successfully.
336    #[must_use]
337    pub fn service_endpoints(&self) -> Option<&ServiceEndpoints> {
338        self.service_endpoints.as_ref()
339    }
340
341    // =========================================================================
342    // Semantic Setters - Record deployment lifecycle events
343    // =========================================================================
344
345    /// Records that provisioning has completed with the given instance IP
346    ///
347    /// Call this after the `provision` command successfully creates infrastructure.
348    /// Sets both `instance_ip` and `provision_method` to `Provisioned`.
349    ///
350    /// # Arguments
351    ///
352    /// * `ip` - The IP address of the newly provisioned instance
353    pub fn record_provisioning(&mut self, ip: IpAddr) {
354        self.instance_ip = Some(ip);
355        self.provision_method = Some(ProvisionMethod::Provisioned);
356    }
357
358    /// Records that an existing instance has been registered
359    ///
360    /// Call this after the `register` command connects to existing infrastructure.
361    /// Sets both `instance_ip` and `provision_method` to `Registered`.
362    ///
363    /// # Arguments
364    ///
365    /// * `ip` - The IP address of the registered instance
366    pub fn record_registration(&mut self, ip: IpAddr) {
367        self.instance_ip = Some(ip);
368        self.provision_method = Some(ProvisionMethod::Registered);
369    }
370
371    /// Records that services have been started with the given endpoints
372    ///
373    /// Call this after the `run` command successfully starts all services.
374    /// The endpoints can then be displayed to users or used for health checks.
375    ///
376    /// # Arguments
377    ///
378    /// * `endpoints` - The URLs for all running services
379    pub fn record_services_started(&mut self, endpoints: ServiceEndpoints) {
380        self.service_endpoints = Some(endpoints);
381    }
382
383    // =========================================================================
384    // Low-level setters - For backward compatibility and state restoration
385    // =========================================================================
386
387    /// Sets the instance IP directly
388    ///
389    /// Prefer `record_provisioning()` or `record_registration()` which also
390    /// set the provision method. This method is provided for cases where
391    /// only the IP needs to be updated (e.g., deserialization workarounds).
392    pub fn set_instance_ip(&mut self, ip: IpAddr) {
393        self.instance_ip = Some(ip);
394    }
395
396    /// Sets the provision method directly
397    ///
398    /// Prefer `record_provisioning()` or `record_registration()` which also
399    /// set the instance IP. This method is provided for backward compatibility.
400    pub fn set_provision_method(&mut self, method: ProvisionMethod) {
401        self.provision_method = Some(method);
402    }
403}
404
405impl Default for RuntimeOutputs {
406    fn default() -> Self {
407        Self::new()
408    }
409}