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