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