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,
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 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 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 #[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 #[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 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 #[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 #[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}