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}