Skip to content

Commit 55dfa94

Browse files
committed
refactor: [#272] Replace tls with domain+use_tls_proxy for Health Check API
This commit migrates the Health Check API from the old `tls: Option<TlsSection>` pattern to the new `domain: Option<String>` + `use_tls_proxy: Option<bool>` pattern, following the same approach used for HTTP trackers and HTTP API. Changes: - Update HealthCheckApiSection DTO with new fields - Update HealthCheckApiConfig domain type with domain and use_tls_proxy - Add validation that use_tls_proxy requires a domain - Update TrackerConfig localhost+TLS validation to use use_tls_proxy - Update show command to use tls_domain() method - Update Caddy template context documentation - Update all test fixtures and doc examples - Update envs/manual-https-test.json with new format This is part of the incremental migration to remove the TlsSection type and standardize on the simpler domain + use_tls_proxy pattern across all services. Next: Grafana (Step 7.5.4).
1 parent 3662aff commit 55dfa94

File tree

10 files changed

+221
-105
lines changed

10 files changed

+221
-105
lines changed

docs/issues/272-add-https-support-with-caddy.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1455,15 +1455,15 @@ The implementation is split into incremental steps, one service type at a time,
14551455

14561456
##### Step 7.5.3: Tracker Health Check API
14571457

1458-
- [ ] Add `domain: Option<String>` and `use_tls_proxy: Option<bool>` to `HealthCheckApiSection` DTO
1459-
- [ ] Update `HealthCheckApiConfig` domain type
1460-
- [ ] Add validation rules
1461-
- [ ] Update Caddy template for health check
1462-
- [ ] Update show command `ServiceInfo` for health check
1463-
- [ ] Update `envs/manual-https-test.json` for health check
1464-
- [ ] Remove `TlsSection` from health check
1465-
- [ ] Add unit tests
1466-
- [ ] Run E2E tests
1458+
- [x] Add `domain: Option<String>` and `use_tls_proxy: Option<bool>` to `HealthCheckApiSection` DTO
1459+
- [x] Update `HealthCheckApiConfig` domain type
1460+
- [x] Add validation rules
1461+
- [x] Update Caddy template for health check
1462+
- [x] Update show command `ServiceInfo` for health check
1463+
- [x] Update `envs/manual-https-test.json` for health check
1464+
- [x] Remove `TlsSection` from health check
1465+
- [x] Add unit tests
1466+
- [x] Run E2E tests
14671467

14681468
##### Step 7.5.4: Grafana
14691469

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

Lines changed: 92 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,28 @@ use schemars::JsonSchema;
44
use serde::{Deserialize, Serialize};
55

66
use crate::application::command_handlers::create::config::errors::CreateConfigError;
7-
use crate::application::command_handlers::create::config::https::TlsSection;
8-
use crate::domain::tls::TlsConfig;
97
use crate::domain::tracker::HealthCheckApiConfig;
108
use crate::shared::DomainName;
119

1210
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
1311
pub struct HealthCheckApiSection {
1412
pub bind_address: String,
1513

16-
/// Optional TLS configuration for HTTPS
14+
/// Domain name for HTTPS access via Caddy reverse proxy
1715
///
18-
/// When present, this service will be proxied through Caddy with HTTPS enabled.
19-
/// The domain specified will be used for Let's Encrypt certificate acquisition.
16+
/// When present with `use_tls_proxy: true`, this service will be accessible
17+
/// via HTTPS at this domain. The domain will be used for Let's Encrypt
18+
/// certificate acquisition.
19+
#[serde(default, skip_serializing_if = "Option::is_none")]
20+
pub domain: Option<String>,
21+
22+
/// Whether to proxy this service through Caddy with TLS termination
23+
///
24+
/// When `true`, the service will be accessible via HTTPS through Caddy.
25+
/// Requires `domain` to be set.
2026
/// This is useful for exposing health checks to external monitoring systems.
2127
#[serde(default, skip_serializing_if = "Option::is_none")]
22-
pub tls: Option<TlsSection>,
28+
pub use_tls_proxy: Option<bool>,
2329
}
2430

2531
impl HealthCheckApiSection {
@@ -29,7 +35,8 @@ impl HealthCheckApiSection {
2935
///
3036
/// Returns `CreateConfigError::InvalidBindAddress` if the bind address cannot be parsed as a valid IP:PORT combination.
3137
/// Returns `CreateConfigError::DynamicPortNotSupported` if port 0 (dynamic port assignment) is specified.
32-
/// Returns `CreateConfigError::InvalidDomain` if the TLS domain is invalid.
38+
/// Returns `CreateConfigError::InvalidDomain` if the domain is invalid.
39+
/// Returns `CreateConfigError::TlsProxyWithoutDomain` if `use_tls_proxy` is true but domain is missing.
3340
///
3441
/// Note: Localhost + TLS validation is performed at the domain layer
3542
/// (see `TrackerConfig::validate()`) to avoid duplicating business rules.
@@ -49,30 +56,42 @@ impl HealthCheckApiSection {
4956
});
5057
}
5158

52-
// Convert TLS section to domain type with validation
53-
let tls = match &self.tls {
54-
Some(tls_section) => {
55-
tls_section.validate()?;
56-
let domain = DomainName::new(&tls_section.domain).map_err(|e| {
59+
let use_tls_proxy = self.use_tls_proxy.unwrap_or(false);
60+
61+
// Validate: use_tls_proxy requires domain
62+
if use_tls_proxy && self.domain.is_none() {
63+
return Err(CreateConfigError::TlsProxyWithoutDomain {
64+
service_type: "Health Check API".to_string(),
65+
bind_address: self.bind_address.clone(),
66+
});
67+
}
68+
69+
// Parse domain if present
70+
let domain =
71+
match &self.domain {
72+
Some(domain_str) => Some(DomainName::new(domain_str).map_err(|e| {
5773
CreateConfigError::InvalidDomain {
58-
domain: tls_section.domain.clone(),
74+
domain: domain_str.clone(),
5975
reason: e.to_string(),
6076
}
61-
})?;
62-
Some(TlsConfig::new(domain))
63-
}
64-
None => None,
65-
};
66-
67-
Ok(HealthCheckApiConfig { bind_address, tls })
77+
})?),
78+
None => None,
79+
};
80+
81+
Ok(HealthCheckApiConfig {
82+
bind_address,
83+
domain,
84+
use_tls_proxy,
85+
})
6886
}
6987
}
7088

7189
impl Default for HealthCheckApiSection {
7290
fn default() -> Self {
7391
Self {
7492
bind_address: "127.0.0.1:1313".to_string(),
75-
tls: None,
93+
domain: None,
94+
use_tls_proxy: None,
7695
}
7796
}
7897
}
@@ -85,7 +104,8 @@ mod tests {
85104
fn it_should_convert_to_domain_config_when_bind_address_is_valid() {
86105
let section = HealthCheckApiSection {
87106
bind_address: "127.0.0.1:1313".to_string(),
88-
tls: None,
107+
domain: None,
108+
use_tls_proxy: None,
89109
};
90110

91111
let config = section.to_health_check_api_config().unwrap();
@@ -94,16 +114,16 @@ mod tests {
94114
config.bind_address,
95115
"127.0.0.1:1313".parse::<SocketAddr>().unwrap()
96116
);
97-
assert!(config.tls.is_none());
117+
assert!(!config.use_tls_proxy);
118+
assert!(config.domain.is_none());
98119
}
99120

100121
#[test]
101-
fn it_should_convert_to_domain_config_with_tls() {
122+
fn it_should_convert_to_domain_config_with_tls_proxy() {
102123
let section = HealthCheckApiSection {
103124
bind_address: "0.0.0.0:1313".to_string(),
104-
tls: Some(TlsSection {
105-
domain: "health.tracker.local".to_string(),
106-
}),
125+
domain: Some("health.tracker.local".to_string()),
126+
use_tls_proxy: Some(true),
107127
};
108128

109129
let config = section.to_health_check_api_config().unwrap();
@@ -112,15 +132,16 @@ mod tests {
112132
config.bind_address,
113133
"0.0.0.0:1313".parse::<SocketAddr>().unwrap()
114134
);
115-
assert!(config.tls.is_some());
135+
assert!(config.use_tls_proxy);
116136
assert_eq!(config.tls_domain(), Some("health.tracker.local"));
117137
}
118138

119139
#[test]
120140
fn it_should_fail_when_bind_address_is_invalid() {
121141
let section = HealthCheckApiSection {
122142
bind_address: "invalid".to_string(),
123-
tls: None,
143+
domain: None,
144+
use_tls_proxy: None,
124145
};
125146

126147
let result = section.to_health_check_api_config();
@@ -136,7 +157,8 @@ mod tests {
136157
fn it_should_reject_dynamic_port_assignment() {
137158
let section = HealthCheckApiSection {
138159
bind_address: "0.0.0.0:0".to_string(),
139-
tls: None,
160+
domain: None,
161+
use_tls_proxy: None,
140162
};
141163

142164
let result = section.to_health_check_api_config();
@@ -152,7 +174,8 @@ mod tests {
152174
fn it_should_allow_ipv6_addresses() {
153175
let section = HealthCheckApiSection {
154176
bind_address: "[::1]:1313".to_string(),
155-
tls: None,
177+
domain: None,
178+
use_tls_proxy: None,
156179
};
157180

158181
let result = section.to_health_check_api_config();
@@ -164,7 +187,8 @@ mod tests {
164187
fn it_should_allow_any_port_except_zero() {
165188
let section = HealthCheckApiSection {
166189
bind_address: "127.0.0.1:8080".to_string(),
167-
tls: None,
190+
domain: None,
191+
use_tls_proxy: None,
168192
};
169193

170194
let result = section.to_health_check_api_config();
@@ -177,16 +201,16 @@ mod tests {
177201
let section = HealthCheckApiSection::default();
178202

179203
assert_eq!(section.bind_address, "127.0.0.1:1313");
180-
assert!(section.tls.is_none());
204+
assert!(section.domain.is_none());
205+
assert!(section.use_tls_proxy.is_none());
181206
}
182207

183208
#[test]
184-
fn it_should_fail_when_tls_domain_is_invalid() {
209+
fn it_should_fail_when_domain_is_invalid() {
185210
let section = HealthCheckApiSection {
186211
bind_address: "0.0.0.0:1313".to_string(),
187-
tls: Some(TlsSection {
188-
domain: "invalid domain with spaces".to_string(),
189-
}),
212+
domain: Some("invalid domain with spaces".to_string()),
213+
use_tls_proxy: Some(true),
190214
};
191215

192216
let result = section.to_health_check_api_config();
@@ -197,4 +221,35 @@ mod tests {
197221
CreateConfigError::InvalidDomain { .. }
198222
));
199223
}
224+
225+
#[test]
226+
fn it_should_fail_when_use_tls_proxy_without_domain() {
227+
let section = HealthCheckApiSection {
228+
bind_address: "0.0.0.0:1313".to_string(),
229+
domain: None,
230+
use_tls_proxy: Some(true),
231+
};
232+
233+
let result = section.to_health_check_api_config();
234+
235+
assert!(result.is_err());
236+
assert!(matches!(
237+
result.unwrap_err(),
238+
CreateConfigError::TlsProxyWithoutDomain { .. }
239+
));
240+
}
241+
242+
#[test]
243+
fn it_should_allow_domain_without_tls_proxy() {
244+
let section = HealthCheckApiSection {
245+
bind_address: "0.0.0.0:1313".to_string(),
246+
domain: Some("health.tracker.local".to_string()),
247+
use_tls_proxy: None,
248+
};
249+
250+
let config = section.to_health_check_api_config().unwrap();
251+
252+
assert!(!config.use_tls_proxy);
253+
assert!(config.domain.is_some());
254+
}
200255
}

src/application/command_handlers/show/info/tracker.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -214,12 +214,12 @@ impl ServiceInfo {
214214
let health_check_is_localhost_only =
215215
is_localhost(&tracker_config.health_check_api.bind_address);
216216
let (health_check_url, health_check_uses_https) =
217-
if let Some(tls) = &tracker_config.health_check_api.tls {
217+
if let Some(domain) = tracker_config.health_check_api.tls_domain() {
218218
tls_domains.push(TlsDomainInfo {
219-
domain: tls.domain().to_string(),
219+
domain: domain.to_string(),
220220
internal_port: tracker_config.health_check_api.bind_address.port(),
221221
});
222-
(format!("https://{}/health_check", tls.domain()), true)
222+
(format!("https://{domain}/health_check"), true)
223223
} else {
224224
(
225225
format!(

0 commit comments

Comments
 (0)