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 pub orchestrator_enabled: bool,
25 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 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 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 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 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 #[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 #[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 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 #[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 #[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}