1use std::collections::HashMap;
25use std::path::PathBuf;
26
27use serde::{Deserialize, Serialize};
28
29use crate::config::Endpoint as DwaraEndpoint;
30use crate::k8s_gateway::{
31 ingress::{Ingress, IngressClass},
32 Gateway, GatewayClass, HttpRoute, TranslationResult, CONTROLLER_NAME,
33};
34
35#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
41pub struct Condition {
42 pub r#type: String,
43 pub status: String,
44 pub reason: String,
45 pub message: String,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub observed_generation: Option<i64>,
48}
49
50#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
52pub struct GatewayClassStatus {
53 #[serde(default, skip_serializing_if = "Vec::is_empty")]
54 pub conditions: Vec<Condition>,
55}
56
57#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
59pub struct GatewayStatus {
60 #[serde(default, skip_serializing_if = "Vec::is_empty")]
61 pub conditions: Vec<Condition>,
62}
63
64#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
66pub struct HttpRouteStatus {
67 #[serde(default, skip_serializing_if = "Vec::is_empty")]
68 pub conditions: Vec<Condition>,
69}
70
71#[derive(Clone, Debug, PartialEq)]
74pub struct ReconcileOutput {
75 pub config: TranslationResult,
77 pub gateway_class_statuses: HashMap<String, GatewayClassStatus>,
79 pub gateway_statuses: HashMap<String, GatewayStatus>,
81 pub httproute_statuses: HashMap<String, HttpRouteStatus>,
83}
84
85#[derive(Clone, Debug, Default)]
93pub struct Reconciler {
94 controller_name: String,
96 ingress_class: String,
98 gateway_classes: Vec<GatewayClass>,
100 gateways: Vec<Gateway>,
102 httproutes: Vec<HttpRoute>,
104 ingresses: Vec<Ingress>,
106 ingress_classes: Vec<IngressClass>,
108 endpoints: HashMap<String, Vec<DwaraEndpoint>>,
110}
111
112impl Reconciler {
113 pub fn new(controller_name: &str, ingress_class: &str) -> Self {
115 Self {
116 controller_name: controller_name.to_string(),
117 ingress_class: ingress_class.to_string(),
118 ..Default::default()
119 }
120 }
121
122 pub fn with_defaults() -> Self {
124 Self::new(CONTROLLER_NAME, "dwara")
125 }
126
127 pub fn with_gateway_classes(mut self, gcs: Vec<GatewayClass>) -> Self {
129 self.gateway_classes = gcs;
130 self
131 }
132
133 pub fn with_gateways(mut self, gws: Vec<Gateway>) -> Self {
135 self.gateways = gws;
136 self
137 }
138
139 pub fn with_httproutes(mut self, routes: Vec<HttpRoute>) -> Self {
141 self.httproutes = routes;
142 self
143 }
144
145 pub fn with_ingresses(mut self, ingresses: Vec<Ingress>) -> Self {
147 self.ingresses = ingresses;
148 self
149 }
150
151 pub fn with_ingress_classes(mut self, classes: Vec<IngressClass>) -> Self {
153 self.ingress_classes = classes;
154 self
155 }
156
157 pub fn with_endpoints(mut self, endpoints: HashMap<String, Vec<DwaraEndpoint>>) -> Self {
159 self.endpoints = endpoints;
160 self
161 }
162
163 pub fn set_gateway_classes(&mut self, gcs: Vec<GatewayClass>) {
165 self.gateway_classes = gcs;
166 }
167
168 pub fn set_gateways(&mut self, gws: Vec<Gateway>) {
170 self.gateways = gws;
171 }
172
173 pub fn set_httproutes(&mut self, routes: Vec<HttpRoute>) {
175 self.httproutes = routes;
176 }
177
178 pub fn set_ingresses(&mut self, ingresses: Vec<Ingress>) {
180 self.ingresses = ingresses;
181 }
182
183 pub fn set_endpoints(&mut self, endpoints: HashMap<String, Vec<DwaraEndpoint>>) {
185 self.endpoints = endpoints;
186 }
187
188 pub fn reconcile(&self) -> Result<ReconcileOutput, String> {
192 let mut all_warnings = Vec::new();
193 let mut gateway_class_statuses = HashMap::new();
194 let mut gateway_statuses = HashMap::new();
195 let mut httproute_statuses = HashMap::new();
196
197 let accepted_gc_names: Vec<String> = self
200 .gateway_classes
201 .iter()
202 .filter(|gc| gc.spec.controller == self.controller_name)
203 .map(|gc| {
204 let name = gc.metadata.name.clone();
205 gateway_class_statuses.insert(
206 name.clone(),
207 GatewayClassStatus {
208 conditions: vec![Condition {
209 r#type: "Accepted".to_string(),
210 status: "True".to_string(),
211 reason: "Accepted".to_string(),
212 message: format!(
213 "GatewayClass {} accepted by controller {}",
214 name, self.controller_name
215 ),
216 observed_generation: None,
217 }],
218 },
219 );
220 name
221 })
222 .collect();
223
224 for gc in &self.gateway_classes {
226 if gc.spec.controller != self.controller_name {
227 gateway_class_statuses.insert(
228 gc.metadata.name.clone(),
229 GatewayClassStatus {
230 conditions: vec![Condition {
231 r#type: "Accepted".to_string(),
232 status: "False".to_string(),
233 reason: "InvalidController".to_string(),
234 message: format!(
235 "GatewayClass {} controller '{}' does not match '{}'",
236 gc.metadata.name, gc.spec.controller, self.controller_name
237 ),
238 observed_generation: None,
239 }],
240 },
241 );
242 }
243 }
244
245 let mut merged_gateway = crate::config::Gateway {
248 version: 1,
249 listeners: Vec::new(),
250 routes: Vec::new(),
251 services: Vec::new(),
252 upstreams: Vec::new(),
253 consumers: Vec::new(),
254 policies: Vec::new(),
255 global_policies: Vec::new(),
256 authorization: None,
257 default_security_headers: None,
258 waf: None,
259 trusted_proxies: Vec::new(),
260 max_concurrent_requests: None,
261 load_shed_dry_run: false,
262 jwt_providers: Vec::new(),
263 admin: None,
264 allow_empty_routes: true,
265 hmac_auth: None,
266 webhooks: Vec::new(),
267 analytics: None,
268 analytics_stream: None,
269 geoip: None,
270 admission_queue: None,
271 mtls_consumer_mapping: None,
272 mtls_forward_headers: None,
273 license: None,
274 oidc_providers: Vec::new(),
275 redis_rate_limiter: None,
276 redis_quotas: None,
277 redis_cache: None,
278 config_convergence: None,
279 plugins: Vec::new(),
280 filter_chain: None,
281 plugin_registry: None,
282 ai: None,
283 fleet: None,
284 lifecycle: None,
285 mesh: None,
286 ssrf_filter: None,
287 };
288
289 for gw in &self.gateways {
290 let gw_key = format!(
291 "{}/{}",
292 gw.metadata.namespace.as_deref().unwrap_or("default"),
293 gw.metadata.name
294 );
295
296 if !accepted_gc_names.contains(&gw.spec.gateway_class_name) {
297 gateway_statuses.insert(
299 gw_key.clone(),
300 GatewayStatus {
301 conditions: vec![Condition {
302 r#type: "Accepted".to_string(),
303 status: "False".to_string(),
304 reason: "InvalidGatewayClass".to_string(),
305 message: format!(
306 "Gateway references GatewayClass '{}' not controlled by {}",
307 gw.spec.gateway_class_name, self.controller_name
308 ),
309 observed_generation: None,
310 }],
311 },
312 );
313 continue;
314 }
315
316 let result = crate::k8s_gateway::translate(gw, &self.httproutes, &self.endpoints)
318 .map_err(|e| format!("translation failed for Gateway {gw_key}: {e}"))?;
319
320 all_warnings.extend(result.warnings);
321
322 merged_gateway.listeners.extend(result.gateway.listeners);
324 merged_gateway.routes.extend(result.gateway.routes);
325 merged_gateway.services.extend(result.gateway.services);
326 merged_gateway.upstreams.extend(result.gateway.upstreams);
327
328 gateway_statuses.insert(
330 gw_key,
331 GatewayStatus {
332 conditions: vec![
333 Condition {
334 r#type: "Accepted".to_string(),
335 status: "True".to_string(),
336 reason: "Accepted".to_string(),
337 message: "Gateway accepted and programmed".to_string(),
338 observed_generation: None,
339 },
340 Condition {
341 r#type: "Programmed".to_string(),
342 status: "True".to_string(),
343 reason: "Programmed".to_string(),
344 message: "Gateway config published".to_string(),
345 observed_generation: None,
346 },
347 ],
348 },
349 );
350 }
351
352 for route in &self.httproutes {
354 let route_key = format!(
355 "{}/{}",
356 route.metadata.namespace.as_deref().unwrap_or("default"),
357 route.metadata.name
358 );
359
360 let attached = self.gateways.iter().any(|gw| {
362 if !accepted_gc_names.contains(&gw.spec.gateway_class_name) {
363 return false;
364 }
365 route_attaches_to_any(route, &gw.metadata.name, &gw.metadata.namespace)
366 });
367
368 let resolved_refs = route.spec.rules.iter().all(|rule| {
369 rule.backend_refs.iter().all(|br| {
370 let port = br.port.unwrap_or(80);
371 let ns = br
372 .namespace
373 .as_deref()
374 .or(route.metadata.namespace.as_deref())
375 .unwrap_or("default");
376 let key = format!("{ns}/{}:{port}", br.name);
377 self.endpoints.contains_key(&key)
378 })
379 });
380
381 httproute_statuses.insert(
382 route_key,
383 HttpRouteStatus {
384 conditions: vec![
385 Condition {
386 r#type: "Accepted".to_string(),
387 status: if attached { "True" } else { "False" }.to_string(),
388 reason: if attached {
389 "Accepted".to_string()
390 } else {
391 "NoMatchingGateway".to_string()
392 },
393 message: if attached {
394 "Route accepted by a Gateway".to_string()
395 } else {
396 "Route does not attach to any Gateway".to_string()
397 },
398 observed_generation: None,
399 },
400 Condition {
401 r#type: "ResolvedRefs".to_string(),
402 status: if resolved_refs { "True" } else { "False" }.to_string(),
403 reason: if resolved_refs {
404 "ResolvedRefs".to_string()
405 } else {
406 "BackendNotFound".to_string()
407 },
408 message: if resolved_refs {
409 "All backend references resolved".to_string()
410 } else {
411 "One or more backend references could not be resolved".to_string()
412 },
413 observed_generation: None,
414 },
415 ],
416 },
417 );
418 }
419
420 let ingress_result = crate::k8s_gateway::ingress::translate_ingress(
422 &self.ingresses,
423 &self.ingress_class,
424 &self.endpoints,
425 )
426 .map_err(|e| format!("Ingress translation failed: {e}"))?;
427
428 all_warnings.extend(ingress_result.warnings);
429 merged_gateway
430 .listeners
431 .extend(ingress_result.gateway.listeners);
432 merged_gateway.routes.extend(ingress_result.gateway.routes);
433 merged_gateway
434 .services
435 .extend(ingress_result.gateway.services);
436 merged_gateway
437 .upstreams
438 .extend(ingress_result.gateway.upstreams);
439
440 Ok(ReconcileOutput {
441 config: TranslationResult {
442 gateway: merged_gateway,
443 warnings: all_warnings,
444 },
445 gateway_class_statuses,
446 gateway_statuses,
447 httproute_statuses,
448 })
449 }
450}
451
452fn route_attaches_to_any(
454 route: &HttpRoute,
455 gateway_name: &str,
456 gateway_namespace: &Option<String>,
457) -> bool {
458 if route.spec.parent_refs.is_empty() {
459 return true;
460 }
461 let gw_ns = gateway_namespace.as_deref().unwrap_or("default");
462 route.spec.parent_refs.iter().any(|p| {
463 p.name == gateway_name
464 && p.kind == "Gateway"
465 && p.namespace.as_deref().unwrap_or("default") == gw_ns
466 })
467}
468
469#[derive(Clone, Debug)]
475pub struct ControllerConfig {
476 pub controller_name: String,
478 pub ingress_class: String,
480 pub output_config_path: PathBuf,
483 pub namespace: String,
485}
486
487impl Default for ControllerConfig {
488 fn default() -> Self {
489 Self {
490 controller_name: CONTROLLER_NAME.to_string(),
491 ingress_class: "dwara".to_string(),
492 output_config_path: PathBuf::from("/etc/dwara/dwara.yaml"),
493 namespace: String::new(),
494 }
495 }
496}
497
498pub fn publish_config(
502 config: &crate::config::Gateway,
503 output_path: &std::path::Path,
504) -> Result<(), String> {
505 let yaml =
506 serde_yaml_ng::to_string(config).map_err(|e| format!("cannot serialize config: {e}"))?;
507 std::fs::write(output_path, yaml)
508 .map_err(|e| format!("cannot write config to {}: {e}", output_path.display()))
509}
510
511pub struct Controller {
519 config: ControllerConfig,
520 reconciler: Reconciler,
521}
522
523impl Controller {
524 pub fn new(config: ControllerConfig) -> Self {
526 let reconciler = Reconciler::new(&config.controller_name, &config.ingress_class);
527 Self { config, reconciler }
528 }
529
530 pub async fn run(&mut self) -> Result<(), String> {
538 use kube::Client;
539
540 let client = Client::try_default()
541 .await
542 .map_err(|e| format!("cannot create kube client: {e}"))?;
543
544 tracing::info!(
568 controller = %self.config.controller_name,
569 ingress_class = %self.config.ingress_class,
570 output = %self.config.output_config_path.display(),
571 "dwara k8s controller started"
572 );
573
574 let _ = (client, &self.reconciler);
579
580 Ok(())
581 }
582}