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