Skip to content

Commit 1962386

Browse files
committed
refactor(infrastructure): [#301] extract ServiceTopology shared type
Extract common ports/networks fields into ServiceTopology type. All Docker Compose ServiceContext types now use this shared topology via #[serde(flatten)] to maintain template compatibility. Makes the architectural pattern explicit: - Topology (ports/networks) → docker-compose.yml template - Configuration (credentials) → .env template Changes: - Add service_topology.rs with shared ServiceTopology struct - Update TrackerServiceContext to embed ServiceTopology - Update GrafanaServiceContext to embed ServiceTopology - Update PrometheusServiceContext to embed ServiceTopology - Update CaddyServiceContext to embed ServiceTopology - Update MysqlServiceContext to embed ServiceTopology - Add ports() and networks() accessor methods - Update builder.rs to use accessor methods - Update all tests to use accessor methods
1 parent 195c80e commit 1962386

8 files changed

Lines changed: 306 additions & 120 deletions

File tree

src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/builder.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -218,20 +218,20 @@ impl DockerComposeContextBuilder {
218218
let mut networks: HashSet<Network> = HashSet::new();
219219

220220
// Collect from tracker (always present)
221-
networks.extend(tracker.networks.iter().copied());
221+
networks.extend(tracker.networks().iter().copied());
222222

223223
// Collect from optional services
224224
if let Some(prom) = prometheus {
225-
networks.extend(prom.networks.iter().copied());
225+
networks.extend(prom.networks().iter().copied());
226226
}
227227
if let Some(graf) = grafana {
228-
networks.extend(graf.networks.iter().copied());
228+
networks.extend(graf.networks().iter().copied());
229229
}
230230
if let Some(cad) = caddy {
231-
networks.extend(cad.networks.iter().copied());
231+
networks.extend(cad.networks().iter().copied());
232232
}
233233
if let Some(my) = mysql {
234-
networks.extend(my.networks.iter().copied());
234+
networks.extend(my.networks().iter().copied());
235235
}
236236

237237
// Sort for deterministic output (alphabetically by name)
@@ -263,22 +263,22 @@ impl DockerComposeContextBuilder {
263263

264264
// Collect ports with service names
265265
let service_ports: Vec<(&'static str, &[PortDefinition])> = vec![
266-
("tracker", &context.tracker.ports),
266+
("tracker", context.tracker.ports()),
267267
(
268268
"prometheus",
269-
context.prometheus.as_ref().map_or(&[][..], |p| &p.ports),
269+
context.prometheus.as_ref().map_or(&[][..], |p| p.ports()),
270270
),
271271
(
272272
"grafana",
273-
context.grafana.as_ref().map_or(&[][..], |g| &g.ports),
273+
context.grafana.as_ref().map_or(&[][..], |g| g.ports()),
274274
),
275275
(
276276
"caddy",
277-
context.caddy.as_ref().map_or(&[][..], |c| &c.ports),
277+
context.caddy.as_ref().map_or(&[][..], |c| c.ports()),
278278
),
279279
(
280280
"mysql",
281-
context.mysql.as_ref().map_or(&[][..], |m| &m.ports),
281+
context.mysql.as_ref().map_or(&[][..], |m| m.ports()),
282282
),
283283
];
284284

src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/caddy.rs

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@ use crate::domain::caddy::CaddyConfig;
2020
use crate::domain::topology::{EnabledServices, Network, NetworkDerivation, PortDerivation};
2121

2222
use super::port_definition::PortDefinition;
23+
use super::service_topology::ServiceTopology;
2324

2425
/// Caddy reverse proxy service configuration for Docker Compose
2526
///
2627
/// Contains configuration for the Caddy service definition in docker-compose.yml.
2728
/// This is intentionally minimal - the actual Caddy configuration (domains, ports)
2829
/// is in the Caddyfile, rendered separately.
2930
///
31+
/// Uses `ServiceTopology` to share the common topology structure with other services.
32+
///
3033
/// # Example
3134
///
3235
/// ```rust
@@ -35,20 +38,16 @@ use super::port_definition::PortDefinition;
3538
/// use torrust_tracker_deployer_lib::domain::topology::{EnabledServices, Network};
3639
///
3740
/// let caddy = CaddyServiceContext::from_domain_config(&CaddyConfig::new(), &EnabledServices::default());
38-
/// assert_eq!(caddy.networks, vec![Network::Proxy]);
39-
/// assert_eq!(caddy.ports.len(), 3); // 80, 443, 443/udp
41+
/// assert_eq!(caddy.networks(), &[Network::Proxy]);
42+
/// assert_eq!(caddy.ports().len(), 3); // 80, 443, 443/udp
4043
/// ```
4144
#[derive(Debug, Clone, Serialize, PartialEq)]
4245
pub struct CaddyServiceContext {
43-
/// Port bindings for Docker Compose
44-
///
45-
/// Caddy exposes ports 80 (HTTP for ACME), 443 (HTTPS), and 443/udp (QUIC).
46-
pub ports: Vec<PortDefinition>,
47-
/// Networks this service connects to
46+
/// Service topology (ports and networks)
4847
///
49-
/// Caddy always connects to `proxy_network` for reverse proxying
50-
/// to backend services (tracker API, HTTP trackers, Grafana).
51-
pub networks: Vec<Network>,
48+
/// Flattened for template compatibility - serializes ports/networks at top level.
49+
#[serde(flatten)]
50+
pub topology: ServiceTopology,
5251
}
5352

5453
impl CaddyServiceContext {
@@ -67,7 +66,9 @@ impl CaddyServiceContext {
6766
let ports = port_bindings.iter().map(PortDefinition::from).collect();
6867
let networks = config.derive_networks(enabled_services);
6968

70-
Self { ports, networks }
69+
Self {
70+
topology: ServiceTopology::new(ports, networks),
71+
}
7172
}
7273

7374
/// Creates a new `CaddyServiceContext` with default configuration
@@ -77,6 +78,18 @@ impl CaddyServiceContext {
7778
pub fn new() -> Self {
7879
Self::from_domain_config(&CaddyConfig::new(), &EnabledServices::default())
7980
}
81+
82+
/// Returns a reference to the port bindings
83+
#[must_use]
84+
pub fn ports(&self) -> &[PortDefinition] {
85+
&self.topology.ports
86+
}
87+
88+
/// Returns a reference to the networks
89+
#[must_use]
90+
pub fn networks(&self) -> &[Network] {
91+
&self.topology.networks
92+
}
8093
}
8194

8295
impl Default for CaddyServiceContext {
@@ -93,14 +106,14 @@ mod tests {
93106
fn it_should_connect_caddy_to_proxy_network() {
94107
let caddy = CaddyServiceContext::new();
95108

96-
assert_eq!(caddy.networks, vec![Network::Proxy]);
109+
assert_eq!(caddy.networks(), &[Network::Proxy]);
97110
}
98111

99112
#[test]
100113
fn it_should_implement_default() {
101114
let caddy = CaddyServiceContext::default();
102115

103-
assert_eq!(caddy.networks, vec![Network::Proxy]);
116+
assert_eq!(caddy.networks(), &[Network::Proxy]);
104117
}
105118

106119
#[test]
@@ -117,7 +130,7 @@ mod tests {
117130
fn it_should_expose_three_ports() {
118131
let caddy = CaddyServiceContext::new();
119132

120-
assert_eq!(caddy.ports.len(), 3);
133+
assert_eq!(caddy.ports().len(), 3);
121134
}
122135

123136
#[test]
@@ -138,7 +151,7 @@ mod tests {
138151
let caddy = CaddyServiceContext::from_domain_config(&config, &enabled_services);
139152

140153
// Verify ports come from domain trait
141-
assert_eq!(caddy.ports.len(), 3);
154+
assert_eq!(caddy.ports().len(), 3);
142155
}
143156

144157
#[test]
@@ -148,6 +161,6 @@ mod tests {
148161
let caddy = CaddyServiceContext::from_domain_config(&config, &enabled_services);
149162

150163
// Verify networks come from domain trait
151-
assert_eq!(caddy.networks, vec![Network::Proxy]);
164+
assert_eq!(caddy.networks(), &[Network::Proxy]);
152165
}
153166
}

src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/grafana.rs

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,22 @@ use crate::domain::grafana::GrafanaConfig;
77
use crate::domain::topology::{EnabledServices, Network, NetworkDerivation, PortDerivation};
88

99
use super::port_definition::PortDefinition;
10+
use super::service_topology::ServiceTopology;
1011

1112
/// Grafana service configuration for Docker Compose
1213
///
1314
/// Contains configuration needed for the Grafana service definition in docker-compose.yml.
1415
/// Only includes fields actually used by the template (ports and networks).
1516
/// Credentials are handled separately by the env context for .env template.
17+
///
18+
/// Uses `ServiceTopology` to share the common topology structure with other services.
1619
#[derive(Serialize, Debug, Clone)]
1720
pub struct GrafanaServiceContext {
18-
/// Port bindings for Docker Compose
19-
///
20-
/// When TLS is disabled, Grafana exposes port 3000 directly.
21-
/// When TLS is enabled, Caddy handles the port and this is empty.
22-
pub ports: Vec<PortDefinition>,
23-
/// Networks the Grafana service should connect to
21+
/// Service topology (ports and networks)
2422
///
25-
/// Pre-computed list based on enabled features:
26-
/// - Always includes `visualization_network` (queries Prometheus)
27-
/// - Includes `proxy_network` if Caddy TLS proxy is enabled
28-
pub networks: Vec<Network>,
23+
/// Flattened for template compatibility - serializes ports/networks at top level.
24+
#[serde(flatten)]
25+
pub topology: ServiceTopology,
2926
}
3027

3128
impl GrafanaServiceContext {
@@ -46,7 +43,21 @@ impl GrafanaServiceContext {
4643
.iter()
4744
.map(PortDefinition::from)
4845
.collect();
49-
Self { ports, networks }
46+
Self {
47+
topology: ServiceTopology::new(ports, networks),
48+
}
49+
}
50+
51+
/// Returns a reference to the port bindings
52+
#[must_use]
53+
pub fn ports(&self) -> &[PortDefinition] {
54+
&self.topology.ports
55+
}
56+
57+
/// Returns a reference to the networks
58+
#[must_use]
59+
pub fn networks(&self) -> &[Network] {
60+
&self.topology.networks
5061
}
5162
}
5263

@@ -81,27 +92,24 @@ mod tests {
8192
let context = make_context(false);
8293
let config = GrafanaServiceContext::from_domain_config(&make_config(false), &context);
8394

84-
assert!(config.networks.contains(&Network::Visualization));
95+
assert!(config.networks().contains(&Network::Visualization));
8596
}
8697

8798
#[test]
8899
fn it_should_not_connect_grafana_to_proxy_network_when_caddy_disabled() {
89100
let context = make_context(false);
90101
let config = GrafanaServiceContext::from_domain_config(&make_config(false), &context);
91102

92-
assert_eq!(config.networks, vec![Network::Visualization]);
93-
assert!(!config.networks.contains(&Network::Proxy));
103+
assert_eq!(config.networks(), &[Network::Visualization]);
104+
assert!(!config.networks().contains(&Network::Proxy));
94105
}
95106

96107
#[test]
97108
fn it_should_connect_grafana_to_proxy_network_when_caddy_enabled() {
98109
let context = make_context(true);
99110
let config = GrafanaServiceContext::from_domain_config(&make_config(true), &context);
100111

101-
assert_eq!(
102-
config.networks,
103-
vec![Network::Visualization, Network::Proxy]
104-
);
112+
assert_eq!(config.networks(), &[Network::Visualization, Network::Proxy]);
105113
}
106114

107115
#[test]
@@ -121,15 +129,15 @@ mod tests {
121129
let context = make_context(false);
122130
let config = GrafanaServiceContext::from_domain_config(&make_config(false), &context);
123131

124-
assert_eq!(config.ports.len(), 1);
125-
assert_eq!(config.ports[0].binding(), "3000:3000");
132+
assert_eq!(config.ports().len(), 1);
133+
assert_eq!(config.ports()[0].binding(), "3000:3000");
126134
}
127135

128136
#[test]
129137
fn it_should_not_expose_ports_when_tls_enabled() {
130138
let context = make_context(true);
131139
let config = GrafanaServiceContext::from_domain_config(&make_config(true), &context);
132140

133-
assert!(config.ports.is_empty());
141+
assert!(config.ports().is_empty());
134142
}
135143
}

src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context/mod.rs

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mod mysql;
1515
mod network_definition;
1616
mod port_definition;
1717
mod prometheus;
18+
mod service_topology;
1819
mod tracker;
1920

2021
// Re-exports - service contexts
@@ -29,6 +30,7 @@ pub use builder::{DockerComposeContextBuilder, PortConflictError};
2930
pub use database::{DatabaseConfig, MysqlSetupConfig};
3031
pub use network_definition::NetworkDefinition;
3132
pub use port_definition::PortDefinition;
33+
pub use service_topology::ServiceTopology;
3234

3335
/// Context for rendering the docker-compose.yml template
3436
///
@@ -288,11 +290,11 @@ mod tests {
288290
assert_eq!(context.database().driver(), "sqlite3");
289291
assert!(context.database().mysql().is_none());
290292
// Verify ports are derived correctly (2 UDP + 1 HTTP + 1 API = 4 ports)
291-
assert_eq!(context.tracker().ports.len(), 4);
292-
assert_eq!(context.tracker().ports[0].binding(), "6868:6868/udp");
293-
assert_eq!(context.tracker().ports[1].binding(), "6969:6969/udp");
294-
assert_eq!(context.tracker().ports[2].binding(), "7070:7070");
295-
assert_eq!(context.tracker().ports[3].binding(), "1212:1212");
293+
assert_eq!(context.tracker().ports().len(), 4);
294+
assert_eq!(context.tracker().ports()[0].binding(), "6868:6868/udp");
295+
assert_eq!(context.tracker().ports()[1].binding(), "6969:6969/udp");
296+
assert_eq!(context.tracker().ports()[2].binding(), "7070:7070");
297+
assert_eq!(context.tracker().ports()[3].binding(), "1212:1212");
296298
}
297299

298300
#[test]
@@ -320,9 +322,9 @@ mod tests {
320322
assert_eq!(mysql.port, 3306);
321323

322324
// Verify ports are derived correctly (2 UDP + 1 HTTP + 1 API = 4 ports)
323-
assert_eq!(context.tracker().ports.len(), 4);
324-
assert_eq!(context.tracker().ports[0].binding(), "6868:6868/udp");
325-
assert_eq!(context.tracker().ports[1].binding(), "6969:6969/udp");
325+
assert_eq!(context.tracker().ports().len(), 4);
326+
assert_eq!(context.tracker().ports()[0].binding(), "6868:6868/udp");
327+
assert_eq!(context.tracker().ports()[1].binding(), "6969:6969/udp");
326328
}
327329

328330
#[test]
@@ -394,7 +396,7 @@ mod tests {
394396
.build();
395397

396398
assert!(context.prometheus().is_some());
397-
assert!(!context.prometheus().unwrap().ports.is_empty());
399+
assert!(!context.prometheus().unwrap().ports().is_empty());
398400
}
399401

400402
#[test]
@@ -432,7 +434,7 @@ mod tests {
432434
.build();
433435

434436
let prometheus = context.prometheus().unwrap();
435-
assert_eq!(prometheus.networks, vec![Network::Metrics]);
437+
assert_eq!(prometheus.networks(), &[Network::Metrics]);
436438
}
437439

438440
#[test]
@@ -452,8 +454,8 @@ mod tests {
452454

453455
let prometheus = context.prometheus().unwrap();
454456
assert_eq!(
455-
prometheus.networks,
456-
vec![Network::Metrics, Network::Visualization]
457+
prometheus.networks(),
458+
&[Network::Metrics, Network::Visualization]
457459
);
458460
}
459461

@@ -470,7 +472,7 @@ mod tests {
470472
.build();
471473

472474
let grafana = context.grafana().unwrap();
473-
assert_eq!(grafana.networks, vec![Network::Visualization]);
475+
assert_eq!(grafana.networks(), &[Network::Visualization]);
474476
}
475477

476478
#[test]
@@ -482,14 +484,14 @@ mod tests {
482484
let grafana_config =
483485
GrafanaConfig::new("admin".to_string(), "password".to_string(), None, false);
484486
let context = DockerComposeContext::builder(tracker)
485-
.with_grafana(grafana_config)
486487
.with_caddy()
488+
.with_grafana(grafana_config)
487489
.build();
488490

489491
let grafana = context.grafana().unwrap();
490492
assert_eq!(
491-
grafana.networks,
492-
vec![Network::Visualization, Network::Proxy]
493+
grafana.networks(),
494+
&[Network::Visualization, Network::Proxy]
493495
);
494496
}
495497

0 commit comments

Comments
 (0)