Skip to content

Commit ab84bbe

Browse files
committed
feat: add configurable Health Check API to tracker configuration
- Add HealthCheckApiConfig domain type with bind_address field - Add HealthCheckApiSection DTO with validation (rejects port 0) - Default bind address: 127.0.0.1:1313 - Update TrackerConfig and TrackerSection with health_check_api field - Add comprehensive unit tests for validation logic - Update all test fixtures and E2E config generators - Regenerate environment-config.json schema - Fix all doc test examples The Health Check API is now fully configurable throughout the application with proper validation and comprehensive test coverage.
1 parent 69ae424 commit ab84bbe

18 files changed

Lines changed: 471 additions & 22 deletions

File tree

project-words.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,3 +303,5 @@ zeroize
303303
ключ
304304
конфиг
305305
файл
306+
Datagram
307+
connectionless

schemas/environment-config.json

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,17 @@
155155
"admin_password"
156156
]
157157
},
158+
"HealthCheckApiSection": {
159+
"type": "object",
160+
"properties": {
161+
"bind_address": {
162+
"type": "string"
163+
}
164+
},
165+
"required": [
166+
"bind_address"
167+
]
168+
},
158169
"HetznerProviderSection": {
159170
"description": "Hetzner-specific configuration section\n\nUses raw `String` fields for JSON deserialization. Convert to domain\n`HetznerConfig` via `ProviderSection::to_provider_config()`.\n\n# Examples\n\n```rust\nuse torrust_tracker_deployer_lib::application::command_handlers::create::config::HetznerProviderSection;\n\nlet section = HetznerProviderSection {\n api_token: \"your-api-token\".to_string(),\n server_type: \"cx22\".to_string(),\n location: \"nbg1\".to_string(),\n image: \"ubuntu-24.04\".to_string(),\n};\n```",
160171
"type": "object",
@@ -320,13 +331,17 @@
320331
]
321332
},
322333
"TrackerSection": {
323-
"description": "Tracker configuration section (application DTO)\n\nAggregates all tracker configuration sections: core, UDP trackers,\nHTTP trackers, and HTTP API.\n\n# Examples\n\n```json\n{\n \"core\": {\n \"database\": {\n \"driver\": \"sqlite3\",\n \"database_name\": \"tracker.db\"\n },\n \"private\": false\n },\n \"udp_trackers\": [\n { \"bind_address\": \"0.0.0.0:6969\" }\n ],\n \"http_trackers\": [\n { \"bind_address\": \"0.0.0.0:7070\" }\n ],\n \"http_api\": {\n \"bind_address\": \"0.0.0.0:1212\",\n \"admin_token\": \"MyAccessToken\"\n }\n}\n```",
334+
"description": "Tracker configuration section (application DTO)\n\nAggregates all tracker configuration sections: core, UDP trackers,\nHTTP trackers, and HTTP API.\n\n# Examples\n\n```json\n{\n \"core\": {\n \"database\": {\n \"driver\": \"sqlite3\",\n \"database_name\": \"tracker.db\"\n },\n \"private\": false\n },\n \"udp_trackers\": [\n { \"bind_address\": \"0.0.0.0:6969\" }\n ],\n \"http_trackers\": [\n { \"bind_address\": \"0.0.0.0:7070\" }\n ],\n \"http_api\": {\n \"bind_address\": \"0.0.0.0:1212\",\n \"admin_token\": \"MyAccessToken\"\n },\n \"health_check_api\": {\n \"bind_address\": \"127.0.0.1:1313\"\n }\n}\n```",
324335
"type": "object",
325336
"properties": {
326337
"core": {
327338
"description": "Core tracker configuration (database, privacy mode)",
328339
"$ref": "#/$defs/TrackerCoreSection"
329340
},
341+
"health_check_api": {
342+
"description": "Health Check API configuration",
343+
"$ref": "#/$defs/HealthCheckApiSection"
344+
},
330345
"http_api": {
331346
"description": "HTTP API configuration",
332347
"$ref": "#/$defs/HttpApiSection"
@@ -350,7 +365,8 @@
350365
"core",
351366
"udp_trackers",
352367
"http_trackers",
353-
"http_api"
368+
"http_api",
369+
"health_check_api"
354370
]
355371
},
356372
"UdpTrackerSection": {
@@ -365,4 +381,4 @@
365381
]
366382
}
367383
}
368-
}
384+
}

src/application/command_handlers/create/config/environment_config.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ use super::tracker::TrackerSection;
6767
/// "http_api": {
6868
/// "bind_address": "0.0.0.0:1212",
6969
/// "admin_token": "MyAccessToken"
70+
/// },
71+
/// "health_check_api": {
72+
/// "bind_address": "127.0.0.1:1313"
7073
/// }
7174
/// },
7275
/// "prometheus": {
@@ -417,6 +420,7 @@ impl EnvironmentCreationConfig {
417420
bind_address: "0.0.0.0:1212".to_string(),
418421
admin_token: "MyAccessToken".to_string(),
419422
},
423+
health_check_api: super::tracker::HealthCheckApiSection::default(),
420424
},
421425
prometheus: Some(PrometheusSection::default()),
422426
grafana: Some(GrafanaSection::default()),
@@ -572,6 +576,9 @@ mod tests {
572576
"http_api": {
573577
"bind_address": "0.0.0.0:1212",
574578
"admin_token": "MyAccessToken"
579+
},
580+
"health_check_api": {
581+
"bind_address": "127.0.0.1:1313"
575582
}
576583
}
577584
}"#;
@@ -633,6 +640,9 @@ mod tests {
633640
"http_api": {
634641
"bind_address": "0.0.0.0:1212",
635642
"admin_token": "MyAccessToken"
643+
},
644+
"health_check_api": {
645+
"bind_address": "127.0.0.1:1313"
636646
}
637647
}
638648
}"#;
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
use std::net::SocketAddr;
2+
3+
use schemars::JsonSchema;
4+
use serde::{Deserialize, Serialize};
5+
6+
use crate::application::command_handlers::create::config::errors::CreateConfigError;
7+
use crate::domain::tracker::HealthCheckApiConfig;
8+
9+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
10+
pub struct HealthCheckApiSection {
11+
pub bind_address: String,
12+
}
13+
14+
impl HealthCheckApiSection {
15+
/// Converts this DTO to a domain `HealthCheckApiConfig`
16+
///
17+
/// # Errors
18+
///
19+
/// Returns `CreateConfigError::InvalidBindAddress` if the bind address cannot be parsed as a valid IP:PORT combination.
20+
/// Returns `CreateConfigError::DynamicPortNotSupported` if port 0 (dynamic port assignment) is specified.
21+
pub fn to_health_check_api_config(&self) -> Result<HealthCheckApiConfig, CreateConfigError> {
22+
// Validate that the bind address can be parsed as SocketAddr
23+
let bind_address = self.bind_address.parse::<SocketAddr>().map_err(|e| {
24+
CreateConfigError::InvalidBindAddress {
25+
address: self.bind_address.clone(),
26+
source: e,
27+
}
28+
})?;
29+
30+
// Reject port 0 (dynamic port assignment)
31+
if bind_address.port() == 0 {
32+
return Err(CreateConfigError::DynamicPortNotSupported {
33+
bind_address: self.bind_address.clone(),
34+
});
35+
}
36+
37+
Ok(HealthCheckApiConfig { bind_address })
38+
}
39+
}
40+
41+
impl Default for HealthCheckApiSection {
42+
fn default() -> Self {
43+
Self {
44+
bind_address: "127.0.0.1:1313".to_string(),
45+
}
46+
}
47+
}
48+
49+
#[cfg(test)]
50+
mod tests {
51+
use super::*;
52+
53+
#[test]
54+
fn it_should_convert_to_domain_config_when_bind_address_is_valid() {
55+
let section = HealthCheckApiSection {
56+
bind_address: "127.0.0.1:1313".to_string(),
57+
};
58+
59+
let config = section.to_health_check_api_config().unwrap();
60+
61+
assert_eq!(
62+
config.bind_address,
63+
"127.0.0.1:1313".parse::<SocketAddr>().unwrap()
64+
);
65+
}
66+
67+
#[test]
68+
fn it_should_fail_when_bind_address_is_invalid() {
69+
let section = HealthCheckApiSection {
70+
bind_address: "invalid".to_string(),
71+
};
72+
73+
let result = section.to_health_check_api_config();
74+
75+
assert!(result.is_err());
76+
assert!(matches!(
77+
result.unwrap_err(),
78+
CreateConfigError::InvalidBindAddress { .. }
79+
));
80+
}
81+
82+
#[test]
83+
fn it_should_reject_dynamic_port_assignment() {
84+
let section = HealthCheckApiSection {
85+
bind_address: "0.0.0.0:0".to_string(),
86+
};
87+
88+
let result = section.to_health_check_api_config();
89+
90+
assert!(result.is_err());
91+
assert!(matches!(
92+
result.unwrap_err(),
93+
CreateConfigError::DynamicPortNotSupported { .. }
94+
));
95+
}
96+
97+
#[test]
98+
fn it_should_allow_ipv6_addresses() {
99+
let section = HealthCheckApiSection {
100+
bind_address: "[::1]:1313".to_string(),
101+
};
102+
103+
let result = section.to_health_check_api_config();
104+
105+
assert!(result.is_ok());
106+
}
107+
108+
#[test]
109+
fn it_should_allow_any_port_except_zero() {
110+
let section = HealthCheckApiSection {
111+
bind_address: "127.0.0.1:8080".to_string(),
112+
};
113+
114+
let result = section.to_health_check_api_config();
115+
116+
assert!(result.is_ok());
117+
}
118+
119+
#[test]
120+
fn it_should_provide_default_localhost_1313() {
121+
let section = HealthCheckApiSection::default();
122+
123+
assert_eq!(section.bind_address, "127.0.0.1:1313");
124+
}
125+
}

src/application/command_handlers/create/config/tracker/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
//! environment creation. These types use raw primitives (String) for
55
//! JSON deserialization and convert to rich domain types (`SocketAddr`).
66
7+
mod health_check_api_section;
78
mod http_api_section;
89
mod http_tracker_section;
910
mod tracker_core_section;
1011
mod tracker_section;
1112
mod udp_tracker_section;
1213

14+
pub use health_check_api_section::HealthCheckApiSection;
1315
pub use http_api_section::HttpApiSection;
1416
pub use http_tracker_section::HttpTrackerSection;
1517
pub use tracker_core_section::{DatabaseSection, TrackerCoreSection};

src/application/command_handlers/create/config/tracker/tracker_section.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@
66
use schemars::JsonSchema;
77
use serde::{Deserialize, Serialize};
88

9-
use super::{HttpApiSection, HttpTrackerSection, TrackerCoreSection, UdpTrackerSection};
9+
use super::{
10+
HealthCheckApiSection, HttpApiSection, HttpTrackerSection, TrackerCoreSection,
11+
UdpTrackerSection,
12+
};
1013
use crate::application::command_handlers::create::config::errors::CreateConfigError;
11-
use crate::domain::tracker::{HttpApiConfig, HttpTrackerConfig, TrackerConfig, UdpTrackerConfig};
14+
use crate::domain::tracker::{
15+
HealthCheckApiConfig, HttpApiConfig, HttpTrackerConfig, TrackerConfig, UdpTrackerConfig,
16+
};
1217

1318
/// Tracker configuration section (application DTO)
1419
///
@@ -35,6 +40,9 @@ use crate::domain::tracker::{HttpApiConfig, HttpTrackerConfig, TrackerConfig, Ud
3540
/// "http_api": {
3641
/// "bind_address": "0.0.0.0:1212",
3742
/// "admin_token": "MyAccessToken"
43+
/// },
44+
/// "health_check_api": {
45+
/// "bind_address": "127.0.0.1:1313"
3846
/// }
3947
/// }
4048
/// ```
@@ -48,6 +56,8 @@ pub struct TrackerSection {
4856
pub http_trackers: Vec<HttpTrackerSection>,
4957
/// HTTP API configuration
5058
pub http_api: HttpApiSection,
59+
/// Health Check API configuration
60+
pub health_check_api: HealthCheckApiSection,
5161
}
5262

5363
impl TrackerSection {
@@ -75,11 +85,15 @@ impl TrackerSection {
7585

7686
let http_api: HttpApiConfig = self.http_api.to_http_api_config()?;
7787

88+
let health_check_api: HealthCheckApiConfig =
89+
self.health_check_api.to_health_check_api_config()?;
90+
7891
Ok(TrackerConfig {
7992
core,
8093
udp_trackers: udp_trackers?,
8194
http_trackers: http_trackers?,
8295
http_api,
96+
health_check_api,
8397
})
8498
}
8599
}
@@ -113,6 +127,7 @@ impl Default for TrackerSection {
113127
bind_address: "0.0.0.0:1212".to_string(),
114128
admin_token: "MyAccessToken".to_string(),
115129
},
130+
health_check_api: HealthCheckApiSection::default(),
116131
}
117132
}
118133
}
@@ -144,6 +159,7 @@ mod tests {
144159
bind_address: "0.0.0.0:1212".to_string(),
145160
admin_token: "MyAccessToken".to_string(),
146161
},
162+
health_check_api: HealthCheckApiSection::default(),
147163
};
148164

149165
let config = section.to_tracker_config().unwrap();
@@ -192,6 +208,7 @@ mod tests {
192208
bind_address: "0.0.0.0:1212".to_string(),
193209
admin_token: "MyAccessToken".to_string(),
194210
},
211+
health_check_api: HealthCheckApiSection::default(),
195212
};
196213

197214
let config = section.to_tracker_config().unwrap();
@@ -217,6 +234,7 @@ mod tests {
217234
bind_address: "0.0.0.0:1212".to_string(),
218235
admin_token: "MyAccessToken".to_string(),
219236
},
237+
health_check_api: HealthCheckApiSection::default(),
220238
};
221239

222240
let result = section.to_tracker_config();
@@ -247,6 +265,7 @@ mod tests {
247265
bind_address: "0.0.0.0:1212".to_string(),
248266
admin_token: "MyAccessToken".to_string(),
249267
},
268+
health_check_api: HealthCheckApiSection::default(),
250269
};
251270

252271
let json = serde_json::to_string(&section).unwrap();
@@ -275,6 +294,9 @@ mod tests {
275294
"http_api": {
276295
"bind_address": "0.0.0.0:1212",
277296
"admin_token": "MyAccessToken"
297+
},
298+
"health_check_api": {
299+
"bind_address": "127.0.0.1:1313"
278300
}
279301
}"#;
280302

src/domain/environment/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ pub use user_inputs::UserInputs;
127127

128128
// Re-export tracker types for convenience
129129
pub use crate::domain::tracker::{
130-
DatabaseConfig, HttpApiConfig, HttpTrackerConfig, MysqlConfig, SqliteConfig, TrackerConfig,
131-
TrackerCoreConfig, UdpTrackerConfig,
130+
DatabaseConfig, HealthCheckApiConfig, HttpApiConfig, HttpTrackerConfig, MysqlConfig,
131+
SqliteConfig, TrackerConfig, TrackerCoreConfig, UdpTrackerConfig,
132132
};
133133

134134
// Re-export Prometheus types for convenience

0 commit comments

Comments
 (0)