torrust_tracker_deployer_lib/domain/caddy/config.rs
1//! Caddy TLS reverse proxy configuration domain type
2//!
3//! This module defines the Caddy configuration domain type which implements
4//! the `PortDerivation` and `NetworkDerivation` traits following the same
5//! pattern as other services.
6//!
7//! ## Port Rules Reference
8//!
9//! | Rule | Description |
10//! |---------|-------------------------------------------|
11//! | PORT-09 | Caddy always exposes 80, 443, 443/udp |
12//!
13//! ## Network Rules Reference
14//!
15//! | Rule | Description |
16//! |--------|-------------------------------------------|
17//! | NET-09 | Caddy always connects to Proxy network |
18
19use serde::{Deserialize, Serialize};
20
21use crate::domain::topology::{
22 EnabledServices, Network, NetworkDerivation, PortBinding, PortDerivation,
23};
24
25/// Caddy TLS reverse proxy configuration
26///
27/// Caddy is a special service with fixed behavior:
28/// - Always exposes ports 80 (ACME), 443 (HTTPS), 443/udp (QUIC)
29/// - Always connects to the Proxy network
30///
31/// Unlike other services, Caddy doesn't have user-configurable port behavior,
32/// but it still implements `PortDerivation` for consistency.
33///
34/// # Example
35///
36/// ```rust
37/// use torrust_tracker_deployer_lib::domain::caddy::CaddyConfig;
38/// use torrust_tracker_deployer_lib::domain::topology::PortDerivation;
39///
40/// let config = CaddyConfig::new();
41/// let ports = config.derive_ports();
42/// assert_eq!(ports.len(), 3);
43/// ```
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
45pub struct CaddyConfig {
46 // Caddy has no configurable fields for port/network derivation.
47 // This is intentionally empty - the behavior is fixed.
48 // Future: Could add ACME email, custom certificates, etc.
49}
50
51impl CaddyConfig {
52 /// Creates a new `CaddyConfig`
53 ///
54 /// # Examples
55 ///
56 /// ```rust
57 /// use torrust_tracker_deployer_lib::domain::caddy::CaddyConfig;
58 ///
59 /// let config = CaddyConfig::new();
60 /// ```
61 #[must_use]
62 pub const fn new() -> Self {
63 Self {}
64 }
65}
66
67impl PortDerivation for CaddyConfig {
68 /// Derives port bindings for the Caddy TLS proxy service
69 ///
70 /// Implements PORT-09: Caddy always exposes 80, 443, 443/udp
71 ///
72 /// These ports are required for:
73 /// - **Port 80/tcp**: ACME HTTP-01 challenge for Let's Encrypt certificate renewal
74 /// - **Port 443/tcp**: HTTPS traffic for all proxied services
75 /// - **Port 443/udp**: HTTP/3 (QUIC) support for modern browsers
76 fn derive_ports(&self) -> Vec<PortBinding> {
77 vec![
78 PortBinding::tcp(80, "HTTP (ACME HTTP-01 challenge)"),
79 PortBinding::tcp(443, "HTTPS"),
80 PortBinding::udp(443, "HTTP/3 (QUIC)"),
81 ]
82 }
83}
84
85impl NetworkDerivation for CaddyConfig {
86 /// Derives network assignments for the Caddy service
87 ///
88 /// Implements NET-09: Caddy always connects to Proxy network only
89 fn derive_networks(&self, _enabled_services: &EnabledServices) -> Vec<Network> {
90 vec![Network::Proxy]
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::domain::topology::Service;
98 use crate::domain::tracker::Protocol;
99
100 // =========================================================================
101 // Constructor tests
102 // =========================================================================
103
104 mod constructor {
105 use super::*;
106
107 #[test]
108 fn it_should_create_caddy_config() {
109 let config = CaddyConfig::new();
110 assert_eq!(config, CaddyConfig::default());
111 }
112
113 #[test]
114 fn it_should_implement_default() {
115 let config = CaddyConfig::default();
116 assert_eq!(config, CaddyConfig::new());
117 }
118 }
119
120 // =========================================================================
121 // PortDerivation tests (PORT-09)
122 // =========================================================================
123
124 mod port_derivation {
125 use super::*;
126
127 #[test]
128 fn it_should_expose_port_80_for_acme_challenge() {
129 let config = CaddyConfig::new();
130 let ports = config.derive_ports();
131
132 let port_80 = ports
133 .iter()
134 .find(|p| p.host_port() == 80 && p.protocol() == Protocol::Tcp);
135
136 assert!(port_80.is_some());
137 assert!(port_80.unwrap().description().contains("ACME"));
138 }
139
140 #[test]
141 fn it_should_expose_port_443_tcp_for_https() {
142 let config = CaddyConfig::new();
143 let ports = config.derive_ports();
144
145 let port_443_tcp = ports
146 .iter()
147 .find(|p| p.host_port() == 443 && p.protocol() == Protocol::Tcp);
148
149 assert!(port_443_tcp.is_some());
150 assert!(port_443_tcp.unwrap().description().contains("HTTPS"));
151 }
152
153 #[test]
154 fn it_should_expose_port_443_udp_for_quic() {
155 let config = CaddyConfig::new();
156 let ports = config.derive_ports();
157
158 let port_443_udp = ports
159 .iter()
160 .find(|p| p.host_port() == 443 && p.protocol() == Protocol::Udp);
161
162 assert!(port_443_udp.is_some());
163 assert!(port_443_udp.unwrap().description().contains("QUIC"));
164 }
165
166 #[test]
167 fn it_should_expose_exactly_three_ports() {
168 let config = CaddyConfig::new();
169 let ports = config.derive_ports();
170
171 assert_eq!(ports.len(), 3);
172 }
173 }
174
175 // =========================================================================
176 // NetworkDerivation tests (NET-09)
177 // =========================================================================
178
179 mod network_derivation {
180 use super::*;
181
182 #[test]
183 fn it_should_connect_to_proxy_network() {
184 let config = CaddyConfig::new();
185 let enabled = EnabledServices::from(&[]);
186 let networks = config.derive_networks(&enabled);
187
188 assert_eq!(networks, vec![Network::Proxy]);
189 }
190
191 #[test]
192 fn it_should_connect_only_to_proxy_network_regardless_of_enabled_services() {
193 let config = CaddyConfig::new();
194 let enabled = EnabledServices::from(&[
195 Service::Tracker,
196 Service::Prometheus,
197 Service::Grafana,
198 Service::MySQL,
199 ]);
200 let networks = config.derive_networks(&enabled);
201
202 // NET-09: Caddy only connects to Proxy network
203 assert_eq!(networks, vec![Network::Proxy]);
204 }
205 }
206}