Skip to main content

koi_embedded/
config.rs

1use std::net::IpAddr;
2use std::path::PathBuf;
3
4use koi_common::firewall::{FirewallPort, FirewallProtocol};
5use koi_dns::DnsConfig;
6use koi_runtime::RuntimeBackendKind;
7
8#[derive(Debug, Clone)]
9pub struct KoiConfig {
10    pub data_dir: Option<PathBuf>,
11    pub service_endpoint: String,
12    pub service_mode: ServiceMode,
13    pub http_enabled: bool,
14    pub mdns_enabled: bool,
15    pub dns_enabled: bool,
16    pub health_enabled: bool,
17    pub certmesh_enabled: bool,
18    pub proxy_enabled: bool,
19    pub udp_enabled: bool,
20    pub runtime_enabled: bool,
21    pub runtime_backend: RuntimeBackendKind,
22    /// Translate runtime (container) lifecycle events into mDNS/DNS/health/proxy entries.
23    /// Opt-in (default false): a leaf embedded host usually only wants the event stream.
24    pub orchestrator_enabled: bool,
25    /// Run the certmesh role-driven background loops (renewal / roster sync / heartbeat /
26    /// failover). Opt-in (default false): only a clustered embedded CA host needs them.
27    pub certmesh_background_enabled: bool,
28    pub http_port: u16,
29    pub dashboard_enabled: bool,
30    pub api_docs_enabled: bool,
31    pub mdns_browser_enabled: bool,
32    pub announce_http: bool,
33    pub dns_config: DnsConfig,
34    pub dns_auto_start: bool,
35    pub health_auto_start: bool,
36    pub proxy_auto_start: bool,
37}
38
39impl KoiConfig {
40    /// Collect firewall ports required by the currently-enabled capabilities.
41    ///
42    /// This mirrors the logic in the standalone Koi daemon's
43    /// `firewall_ports_for_config`, but derives from the embedded config.
44    pub fn firewall_ports(&self) -> Vec<FirewallPort> {
45        use std::collections::HashSet;
46
47        let mut ports = Vec::new();
48        if self.mdns_enabled {
49            ports.extend(koi_mdns::firewall_ports());
50        }
51        if self.http_enabled {
52            ports.push(FirewallPort::new(
53                "HTTP",
54                FirewallProtocol::Tcp,
55                self.http_port,
56            ));
57        }
58        if self.dns_enabled {
59            ports.extend(koi_dns::firewall_ports(&self.dns_config));
60        }
61
62        // Deduplicate by (protocol, port)
63        let mut seen = HashSet::new();
64        ports
65            .into_iter()
66            .filter(|p| seen.insert((p.protocol, p.port)))
67            .collect()
68    }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum ServiceMode {
73    Auto,
74    EmbeddedOnly,
75    ClientOnly,
76}
77
78impl Default for KoiConfig {
79    fn default() -> Self {
80        Self {
81            data_dir: None,
82            service_endpoint: "http://127.0.0.1:5641".to_string(),
83            service_mode: ServiceMode::Auto,
84            http_enabled: false,
85            mdns_enabled: true,
86            dns_enabled: true,
87            health_enabled: false,
88            certmesh_enabled: false,
89            proxy_enabled: false,
90            udp_enabled: false,
91            runtime_enabled: false,
92            runtime_backend: RuntimeBackendKind::Auto,
93            orchestrator_enabled: false,
94            certmesh_background_enabled: false,
95            http_port: 5641,
96            dashboard_enabled: false,
97            api_docs_enabled: false,
98            mdns_browser_enabled: false,
99            announce_http: false,
100            dns_config: DnsConfig::default(),
101            dns_auto_start: false,
102            health_auto_start: false,
103            proxy_auto_start: false,
104        }
105    }
106}
107
108pub struct DnsConfigBuilder {
109    config: DnsConfig,
110}
111
112impl DnsConfigBuilder {
113    pub fn new(config: DnsConfig) -> Self {
114        Self { config }
115    }
116
117    pub fn bind_addr(mut self, addr: IpAddr) -> Self {
118        self.config.bind_addr = addr;
119        self
120    }
121
122    pub fn port(mut self, port: u16) -> Self {
123        self.config.port = port;
124        self
125    }
126
127    pub fn zone(mut self, zone: impl Into<String>) -> Self {
128        self.config.zone = zone.into();
129        self
130    }
131
132    pub fn local_ttl(mut self, ttl: u32) -> Self {
133        self.config.local_ttl = ttl;
134        self
135    }
136
137    pub fn allow_public_clients(mut self, allow: bool) -> Self {
138        self.config.allow_public_clients = allow;
139        self
140    }
141
142    pub fn max_qps(mut self, max_qps: u32) -> Self {
143        self.config.max_qps = max_qps;
144        self
145    }
146
147    pub fn local_zone(mut self, enabled: bool) -> Self {
148        self.config.local_zone = enabled;
149        self
150    }
151
152    pub fn build(self) -> DnsConfig {
153        self.config
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use std::net::{IpAddr, Ipv4Addr};
161
162    // ── KoiConfig defaults ─────────────────────────────────────────
163
164    #[test]
165    fn default_config_has_expected_values() {
166        let cfg = KoiConfig::default();
167        assert_eq!(cfg.service_endpoint, "http://127.0.0.1:5641");
168        assert_eq!(cfg.service_mode, ServiceMode::Auto);
169        assert!(!cfg.http_enabled);
170        assert!(cfg.mdns_enabled);
171        assert!(cfg.dns_enabled);
172        assert!(!cfg.health_enabled);
173        assert!(!cfg.certmesh_enabled);
174        assert!(!cfg.proxy_enabled);
175        assert!(!cfg.udp_enabled);
176        assert!(!cfg.runtime_enabled);
177        assert_eq!(cfg.runtime_backend, RuntimeBackendKind::Auto);
178        assert_eq!(cfg.http_port, 5641);
179        assert!(!cfg.dashboard_enabled);
180        assert!(!cfg.api_docs_enabled);
181        assert!(!cfg.mdns_browser_enabled);
182        assert!(!cfg.announce_http);
183        assert!(!cfg.dns_auto_start);
184        assert!(!cfg.health_auto_start);
185        assert!(!cfg.proxy_auto_start);
186        assert!(cfg.data_dir.is_none());
187    }
188
189    #[test]
190    fn default_config_clone_is_equal() {
191        let cfg = KoiConfig::default();
192        let cloned = cfg.clone();
193        assert_eq!(cfg.http_port, cloned.http_port);
194        assert_eq!(cfg.mdns_enabled, cloned.mdns_enabled);
195        assert_eq!(cfg.service_endpoint, cloned.service_endpoint);
196    }
197
198    #[test]
199    fn default_config_debug_does_not_panic() {
200        let cfg = KoiConfig::default();
201        let debug = format!("{cfg:?}");
202        assert!(debug.contains("KoiConfig"));
203    }
204
205    // ── Firewall ports ─────────────────────────────────────────────
206
207    #[test]
208    fn firewall_ports_includes_http_when_enabled() {
209        let cfg = KoiConfig {
210            http_enabled: true,
211            mdns_enabled: false,
212            dns_enabled: false,
213            ..Default::default()
214        };
215        let ports = cfg.firewall_ports();
216        assert!(
217            ports.iter().any(|p| p.port == 5641),
218            "expected HTTP port 5641 in firewall ports"
219        );
220    }
221
222    #[test]
223    fn firewall_ports_respects_custom_http_port() {
224        let cfg = KoiConfig {
225            http_enabled: true,
226            http_port: 9999,
227            mdns_enabled: false,
228            dns_enabled: false,
229            ..Default::default()
230        };
231        let ports = cfg.firewall_ports();
232        assert!(
233            ports.iter().any(|p| p.port == 9999),
234            "expected custom HTTP port 9999"
235        );
236        assert!(
237            !ports.iter().any(|p| p.port == 5641),
238            "should not have default port when overridden"
239        );
240    }
241
242    #[test]
243    fn firewall_ports_empty_when_all_disabled() {
244        let cfg = KoiConfig {
245            http_enabled: false,
246            mdns_enabled: false,
247            dns_enabled: false,
248            ..Default::default()
249        };
250        let ports = cfg.firewall_ports();
251        assert!(ports.is_empty(), "all disabled should yield no ports");
252    }
253
254    #[test]
255    fn firewall_ports_deduplicates() {
256        // DNS default port is 53 (TCP+UDP), mDNS is 5353 (UDP).
257        // With both enabled we should not have duplicate (protocol, port) pairs.
258        let cfg = KoiConfig {
259            http_enabled: false,
260            mdns_enabled: true,
261            dns_enabled: true,
262            ..Default::default()
263        };
264        let ports = cfg.firewall_ports();
265        let mut seen = std::collections::HashSet::new();
266        for p in &ports {
267            assert!(
268                seen.insert((p.protocol, p.port)),
269                "duplicate firewall port: {:?} {}",
270                p.protocol,
271                p.port
272            );
273        }
274    }
275
276    // ── ServiceMode ────────────────────────────────────────────────
277
278    #[test]
279    fn service_mode_equality() {
280        assert_eq!(ServiceMode::Auto, ServiceMode::Auto);
281        assert_eq!(ServiceMode::EmbeddedOnly, ServiceMode::EmbeddedOnly);
282        assert_eq!(ServiceMode::ClientOnly, ServiceMode::ClientOnly);
283        assert_ne!(ServiceMode::Auto, ServiceMode::EmbeddedOnly);
284        assert_ne!(ServiceMode::Auto, ServiceMode::ClientOnly);
285    }
286
287    #[test]
288    fn service_mode_is_copy() {
289        let mode = ServiceMode::Auto;
290        let copy = mode;
291        assert_eq!(mode, copy);
292    }
293
294    #[test]
295    fn service_mode_debug() {
296        let debug = format!("{:?}", ServiceMode::EmbeddedOnly);
297        assert!(debug.contains("EmbeddedOnly"));
298    }
299
300    // ── DnsConfigBuilder ───────────────────────────────────────────
301
302    #[test]
303    fn dns_config_builder_defaults_match_dns_config() {
304        let dns_default = DnsConfig::default();
305        let built = DnsConfigBuilder::new(DnsConfig::default()).build();
306        assert_eq!(built.port, dns_default.port);
307        assert_eq!(built.zone, dns_default.zone);
308        assert_eq!(built.local_ttl, dns_default.local_ttl);
309        assert_eq!(built.allow_public_clients, dns_default.allow_public_clients);
310        assert_eq!(built.max_qps, dns_default.max_qps);
311        assert_eq!(built.local_zone, dns_default.local_zone);
312    }
313
314    #[test]
315    fn dns_config_builder_port() {
316        let cfg = DnsConfigBuilder::new(DnsConfig::default())
317            .port(5353)
318            .build();
319        assert_eq!(cfg.port, 5353);
320    }
321
322    #[test]
323    fn dns_config_builder_bind_addr() {
324        let addr = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
325        let cfg = DnsConfigBuilder::new(DnsConfig::default())
326            .bind_addr(addr)
327            .build();
328        assert_eq!(cfg.bind_addr, addr);
329    }
330
331    #[test]
332    fn dns_config_builder_zone() {
333        let cfg = DnsConfigBuilder::new(DnsConfig::default())
334            .zone("home")
335            .build();
336        assert_eq!(cfg.zone, "home");
337    }
338
339    #[test]
340    fn dns_config_builder_local_ttl() {
341        let cfg = DnsConfigBuilder::new(DnsConfig::default())
342            .local_ttl(120)
343            .build();
344        assert_eq!(cfg.local_ttl, 120);
345    }
346
347    #[test]
348    fn dns_config_builder_allow_public_clients() {
349        let cfg = DnsConfigBuilder::new(DnsConfig::default())
350            .allow_public_clients(true)
351            .build();
352        assert!(cfg.allow_public_clients);
353    }
354
355    #[test]
356    fn dns_config_builder_max_qps() {
357        let cfg = DnsConfigBuilder::new(DnsConfig::default())
358            .max_qps(500)
359            .build();
360        assert_eq!(cfg.max_qps, 500);
361    }
362
363    #[test]
364    fn dns_config_builder_local_zone() {
365        let cfg = DnsConfigBuilder::new(DnsConfig::default())
366            .local_zone(false)
367            .build();
368        assert!(!cfg.local_zone);
369    }
370
371    #[test]
372    fn dns_config_builder_chaining() {
373        let cfg = DnsConfigBuilder::new(DnsConfig::default())
374            .port(5353)
375            .zone("office")
376            .local_ttl(300)
377            .allow_public_clients(true)
378            .max_qps(1000)
379            .local_zone(false)
380            .build();
381        assert_eq!(cfg.port, 5353);
382        assert_eq!(cfg.zone, "office");
383        assert_eq!(cfg.local_ttl, 300);
384        assert!(cfg.allow_public_clients);
385        assert_eq!(cfg.max_qps, 1000);
386        assert!(!cfg.local_zone);
387    }
388}