Skip to main content

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}