1use std::collections::HashMap;
36
37use serde::{Deserialize, Serialize};
38
39use crate::config::{
40 transforms::{HeaderOps, RequestTransforms, ResponseTransforms, Transforms as DwaraTransforms},
41 Endpoint as DwaraEndpoint, Gateway as DwaraGateway, Listener as DwaraListener,
42 ListenerProtocol, ListenerTls, LoadBalancer, NameValueMatch, PathMatch, PathMatchKind,
43 PathRewrite, Route as DwaraRoute, RouteAction, RouteMatch, Service as DwaraService,
44 TlsCertificate, TlsMode, Upstream as DwaraUpstream, UpstreamProtocol, ZeroRttPolicy,
45};
46
47pub mod controller;
48pub mod ingress;
49
50#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
56pub struct GatewayClass {
57 pub api_version: String,
58 pub kind: String,
59 pub metadata: ObjectMeta,
60 pub spec: GatewayClassSpec,
61}
62
63#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
65pub struct GatewayClassSpec {
66 pub controller: String,
68}
69
70#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
72pub struct Gateway {
73 pub api_version: String,
74 pub kind: String,
75 pub metadata: ObjectMeta,
76 pub spec: GatewaySpec,
77}
78
79#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
81pub struct GatewaySpec {
82 pub gateway_class_name: String,
84 pub listeners: Vec<GatewayListener>,
86}
87
88#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
90pub struct GatewayListener {
91 pub name: String,
93 pub port: u16,
95 pub protocol: String,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub hostname: Option<String>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub tls: Option<ListenerTlsConfig>,
103}
104
105#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
107pub struct ListenerTlsConfig {
108 pub mode: String,
110 #[serde(default, skip_serializing_if = "Vec::is_empty")]
112 pub certificate_refs: Vec<SecretObjectReference>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub frontend_validation: Option<FrontendValidation>,
116}
117
118#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
120pub struct FrontendValidation {
121 #[serde(default, skip_serializing_if = "Vec::is_empty")]
123 pub ca_certificate_refs: Vec<SecretObjectReference>,
124}
125
126#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
128pub struct SecretObjectReference {
129 #[serde(default = "default_secret_kind")]
131 pub kind: String,
132 pub name: String,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub namespace: Option<String>,
137}
138
139fn default_secret_kind() -> String {
140 "Secret".to_string()
141}
142
143#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
145pub struct HttpRoute {
146 pub api_version: String,
147 pub kind: String,
148 pub metadata: ObjectMeta,
149 pub spec: HttpRouteSpec,
150}
151
152#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
154pub struct HttpRouteSpec {
155 #[serde(default, skip_serializing_if = "Vec::is_empty")]
157 pub parent_refs: Vec<ParentReference>,
158 #[serde(default, skip_serializing_if = "Vec::is_empty")]
160 pub hostnames: Vec<String>,
161 pub rules: Vec<HttpRouteRule>,
163}
164
165#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
167pub struct ParentReference {
168 #[serde(default = "default_gateway_kind")]
170 pub kind: String,
171 pub name: String,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub namespace: Option<String>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub section_name: Option<String>,
179}
180
181fn default_gateway_kind() -> String {
182 "Gateway".to_string()
183}
184
185#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
187pub struct HttpRouteRule {
188 #[serde(default, skip_serializing_if = "Vec::is_empty")]
190 pub matches: Vec<HttpRouteMatch>,
191 #[serde(default, skip_serializing_if = "Vec::is_empty")]
193 pub filters: Vec<HttpRouteFilter>,
194 #[serde(default, skip_serializing_if = "Vec::is_empty")]
196 pub backend_refs: Vec<HttpBackendRef>,
197}
198
199#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
201pub struct HttpRouteMatch {
202 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub path: Option<HttpPathMatch>,
205 #[serde(default, skip_serializing_if = "Vec::is_empty")]
207 pub headers: Vec<HttpHeaderMatch>,
208 #[serde(default, skip_serializing_if = "Vec::is_empty")]
210 pub query_params: Vec<HttpQueryParamMatch>,
211}
212
213#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
215pub struct HttpPathMatch {
216 #[serde(rename = "type")]
218 pub match_type: String,
219 pub value: String,
221}
222
223#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
225pub struct HttpHeaderMatch {
226 #[serde(default = "default_header_match_type", rename = "type")]
228 pub match_type: String,
229 pub name: String,
231 pub value: String,
233}
234
235fn default_header_match_type() -> String {
236 "Exact".to_string()
237}
238
239#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
241pub struct HttpQueryParamMatch {
242 #[serde(default = "default_query_match_type", rename = "type")]
244 pub match_type: String,
245 pub name: String,
247 pub value: String,
249}
250
251fn default_query_match_type() -> String {
252 "Exact".to_string()
253}
254
255#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
257#[serde(tag = "type", rename_all = "PascalCase")]
258pub enum HttpRouteFilter {
259 RequestRedirect {
261 #[serde(default, skip_serializing_if = "Option::is_none")]
262 scheme: Option<String>,
263 #[serde(default, skip_serializing_if = "Option::is_none")]
264 hostname: Option<String>,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
266 path: Option<String>,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
268 port: Option<u16>,
269 #[serde(default = "default_redirect_status")]
270 status_code: u16,
271 },
272 RequestHeaderModifier {
274 #[serde(default, skip_serializing_if = "Vec::is_empty")]
275 add: Vec<HttpHeaderFilter>,
276 #[serde(default, skip_serializing_if = "Vec::is_empty")]
277 set: Vec<HttpHeaderFilter>,
278 #[serde(default, skip_serializing_if = "Vec::is_empty")]
279 remove: Vec<String>,
280 },
281 ResponseHeaderModifier {
283 #[serde(default, skip_serializing_if = "Vec::is_empty")]
284 add: Vec<HttpHeaderFilter>,
285 #[serde(default, skip_serializing_if = "Vec::is_empty")]
286 set: Vec<HttpHeaderFilter>,
287 #[serde(default, skip_serializing_if = "Vec::is_empty")]
288 remove: Vec<String>,
289 },
290 UrlRewrite {
292 #[serde(default, skip_serializing_if = "Option::is_none")]
293 hostname: Option<String>,
294 #[serde(default, skip_serializing_if = "Option::is_none")]
295 path: Option<UrlRewritePath>,
296 },
297 ExtensionRef {
299 group: String,
300 kind: String,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
302 name: Option<String>,
303 },
304}
305
306#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
308pub struct HttpHeaderFilter {
309 pub name: String,
310 pub value: String,
311}
312
313#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
315#[serde(tag = "type", rename_all = "PascalCase")]
316pub enum UrlRewritePath {
317 ReplaceFullPath { replace_full_path: String },
319 ReplacePrefixMatch { replace_prefix_match: String },
321}
322
323fn default_redirect_status() -> u16 {
324 302
325}
326
327#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
329pub struct HttpBackendRef {
330 #[serde(default = "default_service_kind")]
332 pub kind: String,
333 pub name: String,
335 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub namespace: Option<String>,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
343 pub port: Option<u16>,
344 #[serde(default = "default_weight")]
346 pub weight: u32,
347}
348
349fn default_service_kind() -> String {
350 "Service".to_string()
351}
352
353fn default_weight() -> u32 {
354 1
355}
356
357#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
359pub struct ObjectMeta {
360 pub name: String,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
362 pub namespace: Option<String>,
363}
364
365#[derive(Clone, Debug, PartialEq)]
371pub struct TranslationResult {
372 pub gateway: DwaraGateway,
373 pub warnings: Vec<String>,
374}
375
376pub fn translate(
384 gateway: &Gateway,
385 routes: &[HttpRoute],
386 endpoints: &HashMap<String, Vec<DwaraEndpoint>>,
387) -> Result<TranslationResult, String> {
388 let mut warnings = Vec::new();
389
390 let mut listeners = Vec::new();
392 for listener in &gateway.spec.listeners {
393 let tls = translate_listener_tls(listener, &mut warnings);
394 let protocol = match listener.protocol.as_str() {
395 "HTTP" => ListenerProtocol::Http,
396 "HTTPS" | "TLS" => ListenerProtocol::Https,
397 other => {
398 warnings.push(format!(
399 "listener {} has unknown protocol '{other}', defaulting to HTTP",
400 listener.name
401 ));
402 ListenerProtocol::Http
403 }
404 };
405 let dwara_listener = DwaraListener {
406 name: listener.name.clone(),
407 address: "0.0.0.0".to_string(),
408 port: listener.port,
409 protocol,
410 tls,
411 proxy_protocol: false,
412 policies: Vec::new(),
413 authorization: None,
414 alt_svc: None,
415 l4: None,
416 http2: None,
417 };
418 listeners.push(dwara_listener);
419 }
420
421 let mut dwara_routes = Vec::new();
423 let mut services = Vec::new();
424 let mut upstreams = Vec::new();
425
426 for route in routes {
427 if !route_attaches_to(route, &gateway.metadata.name, &gateway.metadata.namespace) {
429 continue;
430 }
431
432 let route_host = route.spec.hostnames.first().cloned();
434
435 for (rule_idx, rule) in route.spec.rules.iter().enumerate() {
436 let match_count = rule.matches.len().max(1);
440
441 for match_idx in 0..match_count {
442 let route_name = if match_count == 1 {
443 format!("{}-rule-{}", route.metadata.name, rule_idx)
444 } else {
445 format!(
446 "{}-rule-{}-match-{}",
447 route.metadata.name, rule_idx, match_idx
448 )
449 };
450
451 let match_ref = rule.matches.get(match_idx);
452 let match_ = translate_match(match_ref, route_host.as_deref(), &mut warnings);
453
454 let (filter_action, filter_rewrite, transforms) =
456 translate_filters(&rule.filters, &mut warnings);
457
458 let mut service_name = String::new();
460
461 let action = if let Some(act) = filter_action {
464 act
465 } else {
466 if let Some(first_backend) = rule.backend_refs.first() {
467 let ns = first_backend
468 .namespace
469 .as_ref()
470 .or(route.metadata.namespace.as_ref())
471 .cloned()
472 .unwrap_or_else(|| "default".to_string());
473
474 let port = first_backend.port.unwrap_or(80);
475 let endpoint_key = format!("{ns}/{}:{port}", first_backend.name);
476
477 let eps = endpoints.get(&endpoint_key).cloned().unwrap_or_else(|| {
478 warnings.push(format!(
479 "no endpoints found for backend {endpoint_key} (route {route_name})"
480 ));
481 Vec::new()
482 });
483
484 let upstream_name =
485 format!("{}-{}-{}", route_name, first_backend.name, port);
486 service_name =
487 format!("{}-svc-{}-{}", route_name, first_backend.name, port);
488
489 let upstream = DwaraUpstream {
490 name: upstream_name.clone(),
491 load_balancer: LoadBalancer::RoundRobin,
492 hash_on: None,
493 protocol: UpstreamProtocol::Http1,
494 trusted_ca_file: None,
495 use_system_roots: false,
496 endpoints: eps,
497 connection_cap: None,
498 slow_start_ms: None,
499 health: None,
500 active_health: None,
501 retries: None,
502 breaker: None,
503 max_pending: None,
504 timeouts: None,
505 oauth2_client_credentials: None,
506 dns_discovery: None,
507 locality: None,
508 pq: false,
509 peak_ewma: None,
510 cert_pinning: None,
511 mtls: None,
512 pool: None,
513 };
514 upstreams.push(upstream);
515
516 let service = DwaraService {
517 name: service_name.clone(),
518 upstream: Some(upstream_name),
519 split: None,
520 sticky: None,
521 base_path: None,
522 version: None,
523 policies: Vec::new(),
524 authorization: None,
525 };
526 services.push(service);
527 }
528
529 if service_name.is_empty() {
530 warnings.push(format!("route {route_name} has no backends"));
531 RouteAction::Respond {
532 status: 503,
533 body: None,
534 headers: std::collections::BTreeMap::new(),
535 }
536 } else {
537 RouteAction::Proxy {
538 rewrite: filter_rewrite,
539 }
540 }
541 };
542
543 let dwara_route = DwaraRoute {
544 name: route_name,
545 service: if service_name.is_empty() {
546 "none".to_string()
547 } else {
548 service_name
549 },
550 r#match: match_,
551 action,
552 policies: Vec::new(),
553 priority: None,
554 auth_required: false,
555 cors: None,
556 compression: None,
557 limits: None,
558 authorization: None,
559 security_headers_opt_out: false,
560 deprecation: None,
561 maintenance: None,
562 transforms,
563 security_headers: None,
564 masking: None,
565 cache: None,
566 methods: Vec::new(),
567 slo: None,
568 websocket: None,
569 waf: None,
570 request_validation: None,
571 openapi: None,
572 mirror: None,
573 fault_injection: None,
574 plugins: Vec::new(),
575 filter_chain: None,
576 graphql: None,
577 grpc_web: None,
578 translation: None,
579 oidc_login: None,
580 };
581 dwara_routes.push(dwara_route);
582 }
583 }
584 }
585
586 let dwara_gateway = DwaraGateway {
588 version: 1,
589 listeners,
590 routes: dwara_routes,
591 services,
592 upstreams,
593 consumers: Vec::new(),
594 policies: Vec::new(),
595 global_policies: Vec::new(),
596 authorization: None,
597 default_security_headers: None,
598 waf: None,
599 trusted_proxies: Vec::new(),
600 max_concurrent_requests: None,
601 load_shed_dry_run: false,
602 jwt_providers: Vec::new(),
603 admin: None,
604 allow_empty_routes: false,
605 hmac_auth: None,
606 webhooks: Vec::new(),
607 analytics: None,
608 analytics_stream: None,
609 geoip: None,
610 admission_queue: None,
611 mtls_consumer_mapping: None,
612 mtls_forward_headers: None,
613 license: None,
614 oidc_providers: Vec::new(),
615 redis_rate_limiter: None,
616 redis_quotas: None,
617 redis_cache: None,
618 config_convergence: None,
619 plugins: Vec::new(),
620 filter_chain: None,
621 plugin_registry: None,
622 ai: None,
623 fleet: None,
624 lifecycle: None,
625 mesh: None,
626 ssrf_filter: None,
627 };
628
629 Ok(TranslationResult {
630 gateway: dwara_gateway,
631 warnings,
632 })
633}
634
635fn translate_listener_tls(
637 listener: &GatewayListener,
638 warnings: &mut Vec<String>,
639) -> Option<ListenerTls> {
640 let tls = listener.tls.as_ref()?;
641 let mode = match tls.mode.as_str() {
642 "Terminate" => TlsMode::Terminate,
643 "Passthrough" => TlsMode::Passthrough,
644 "Reencrypt" => {
645 warnings.push(format!(
652 "listener {} uses Reencrypt TLS; mapped to Terminate (backend TLS not yet wired)",
653 listener.name
654 ));
655 TlsMode::Terminate
656 }
657 other => {
658 warnings.push(format!(
659 "listener {} has unknown TLS mode '{other}', defaulting to Terminate",
660 listener.name
661 ));
662 TlsMode::Terminate
663 }
664 };
665
666 let certificates: Vec<TlsCertificate> = tls
667 .certificate_refs
668 .iter()
669 .map(|r| {
670 let ns = r.namespace.as_deref().unwrap_or("default");
671 TlsCertificate {
672 server_names: listener
673 .hostname
674 .as_ref()
675 .map(|h| vec![h.clone()])
676 .unwrap_or_default(),
677 cert_file: format!("k8s-secret:{ns}/{}", r.name),
678 key_file: format!("k8s-secret:{ns}/{}", r.name),
679 }
680 })
681 .collect();
682
683 Some(ListenerTls {
684 mode,
685 cert_file: certificates.first().map(|c| c.cert_file.clone()),
686 key_file: certificates.first().map(|c| c.key_file.clone()),
687 certificates,
688 sni_routes: Vec::new(),
689 client_ca_file: None,
690 zero_rtt: ZeroRttPolicy::Reject,
691 pq: false,
692 acme: None,
693 })
694}
695
696fn translate_match(
698 m: Option<&HttpRouteMatch>,
699 host: Option<&str>,
700 warnings: &mut Vec<String>,
701) -> RouteMatch {
702 let (path, headers, query) = match m {
703 None => (
704 PathMatch {
705 kind: PathMatchKind::Prefix,
706 value: "/".to_string(),
707 },
708 std::collections::BTreeMap::new(),
709 Vec::new(),
710 ),
711 Some(m) => {
712 let path = m
713 .path
714 .as_ref()
715 .map(|p| {
716 let kind = match p.match_type.as_str() {
717 "Exact" => PathMatchKind::Exact,
718 "PathPrefix" | "" => PathMatchKind::Prefix,
719 "RegularExpression" => PathMatchKind::Regex,
720 _ => PathMatchKind::Prefix,
721 };
722 PathMatch {
723 kind,
724 value: p.value.clone(),
725 }
726 })
727 .unwrap_or(PathMatch {
728 kind: PathMatchKind::Prefix,
729 value: "/".to_string(),
730 });
731
732 let mut hdr_map = std::collections::BTreeMap::new();
736 for h in &m.headers {
737 if h.match_type == "Exact" || h.match_type.is_empty() {
738 hdr_map.insert(h.name.clone(), h.value.clone());
739 } else if h.match_type == "RegularExpression" {
740 warnings.push(format!(
741 "RegularExpression header match on '{}' is not natively supported; \
742 header match dropped",
743 h.name
744 ));
745 } else {
746 warnings.push(format!(
747 "unknown header match type '{}' on '{}'; header match dropped",
748 h.match_type, h.name
749 ));
750 }
751 }
752
753 let mut query_matches = Vec::new();
757 for q in &m.query_params {
758 if q.match_type == "Exact" || q.match_type.is_empty() {
759 query_matches.push(NameValueMatch {
760 name: q.name.clone(),
761 value: Some(q.value.clone()),
762 });
763 } else if q.match_type == "RegularExpression" {
764 warnings.push(format!(
765 "RegularExpression query param match on '{}' is not natively supported; \
766 query match dropped",
767 q.name
768 ));
769 } else {
770 warnings.push(format!(
771 "unknown query param match type '{}' on '{}'; query match dropped",
772 q.match_type, q.name
773 ));
774 }
775 }
776
777 (path, hdr_map, query_matches)
778 }
779 };
780
781 RouteMatch {
782 path,
783 host: host.map(|h| h.to_string()),
784 methods: Vec::new(),
785 headers,
786 query,
787 cookies: Vec::new(),
788 accept: None,
789 }
790}
791
792fn translate_filters(
797 filters: &[HttpRouteFilter],
798 warnings: &mut Vec<String>,
799) -> (
800 Option<RouteAction>,
801 Option<PathRewrite>,
802 Option<DwaraTransforms>,
803) {
804 let mut action: Option<RouteAction> = None;
805 let mut rewrite: Option<PathRewrite> = None;
806 let mut req_headers: Option<HeaderOps> = None;
807 let mut resp_headers: Option<HeaderOps> = None;
808
809 for filter in filters {
810 match filter {
811 HttpRouteFilter::RequestRedirect {
812 scheme,
813 hostname,
814 path,
815 port: _,
816 status_code,
817 } => {
818 action = Some(RouteAction::Redirect {
819 scheme: scheme.clone(),
820 host: hostname.clone(),
821 path: path.clone(),
822 status: *status_code,
823 });
824 }
825 HttpRouteFilter::RequestHeaderModifier { add, set, remove } => {
826 let mut ops = req_headers.take().unwrap_or_default();
827 for h in set {
828 ops.set.insert(h.name.clone(), h.value.clone());
829 }
830 for h in add {
831 ops.add.insert(h.name.clone(), h.value.clone());
832 }
833 for name in remove {
834 ops.remove.push(name.clone());
835 }
836 req_headers = Some(ops);
837 }
838 HttpRouteFilter::ResponseHeaderModifier { add, set, remove } => {
839 let mut ops = resp_headers.take().unwrap_or_default();
840 for h in set {
841 ops.set.insert(h.name.clone(), h.value.clone());
842 }
843 for h in add {
844 ops.add.insert(h.name.clone(), h.value.clone());
845 }
846 for name in remove {
847 ops.remove.push(name.clone());
848 }
849 resp_headers = Some(ops);
850 }
851 HttpRouteFilter::UrlRewrite { hostname, path } => {
852 if action.is_none() {
856 if let Some(rp) = path {
857 rewrite = Some(match rp {
858 UrlRewritePath::ReplaceFullPath { replace_full_path } => {
859 PathRewrite::ReplacePrefix {
860 prefix: "/".to_string(),
861 replacement: replace_full_path.clone(),
862 }
863 }
864 UrlRewritePath::ReplacePrefixMatch {
865 replace_prefix_match,
866 } => PathRewrite::ReplacePrefix {
867 prefix: "/".to_string(),
868 replacement: replace_prefix_match.clone(),
869 },
870 });
871 }
872 if hostname.is_some() && path.is_none() {
873 warnings.push(
874 "URLRewrite hostname filter is not natively supported on the \
875 upstream path; hostname rewrite dropped"
876 .to_string(),
877 );
878 }
879 }
880 }
881 HttpRouteFilter::ExtensionRef {
882 group,
883 kind,
884 name: _,
885 } => {
886 warnings.push(format!(
887 "extension filter {group}/{kind} is not supported; filter dropped"
888 ));
889 }
890 }
891 }
892
893 let transforms = if req_headers.is_some() || resp_headers.is_some() {
894 Some(DwaraTransforms {
895 request: req_headers.map(|h| RequestTransforms {
896 headers: Some(h),
897 query: None,
898 body: None,
899 }),
900 response: resp_headers.map(|h| ResponseTransforms {
901 headers: Some(h),
902 body: None,
903 }),
904 })
905 } else {
906 None
907 };
908
909 (action, rewrite, transforms)
910}
911
912fn route_attaches_to(
914 route: &HttpRoute,
915 gateway_name: &str,
916 gateway_namespace: &Option<String>,
917) -> bool {
918 if route.spec.parent_refs.is_empty() {
919 return true;
921 }
922
923 let gw_ns = gateway_namespace.as_deref().unwrap_or("default");
924 route.spec.parent_refs.iter().any(|p| {
925 p.name == gateway_name
926 && p.kind == "Gateway"
927 && p.namespace.as_deref().unwrap_or("default") == gw_ns
928 })
929}
930
931pub fn endpoint_key(namespace: &str, service: &str, port: u16) -> String {
933 format!("{namespace}/{service}:{port}")
934}
935
936pub const CONTROLLER_NAME: &str = "shristilabs.com/dwara";
938
939pub fn supported_features() -> Vec<&'static str> {
942 vec![
943 "Gateway",
944 "GatewayClass",
945 "HTTPRoute",
946 "HTTPRouteMatching",
947 "HTTPRoutePathModifier",
948 "HTTPRoutePortLevelSettings",
949 "HTTPRouteHostRewrite",
950 "HTTPRouteBackendProtocolHints",
951 "HTTPRouteRequestRedirect",
952 "HTTPRouteRequestHeaderModifier",
953 "HTTPRouteResponseHeaderModifier",
954 "HTTPRouteQueryParamMatching",
955 "HTTPRouteMethodMatching",
956 "HTTPRouteHeaderMatching",
957 "TLSRoute",
958 "GatewayClassObservedGeneration",
959 "GatewayStaticAddresses",
960 "GatewayPort8080",
961 "GatewayWithAttachedRoutes",
962 "HTTPRouteParentRefPort",
963 "HTTPRouteParentRefNotNamed",
964 "HTTPRouteBackendPortNumber",
965 "HTTPRouteBackendPortName",
966 "HTTPRouteReferenceGrant",
967 "HTTPRouteIsolatedFilter",
968 "HTTPRouteList",
969 "GatewayClassList",
970 "GatewayList",
971 "HTTPRouteHostnameMatching",
972 "HTTPRoutePathExact",
973 "HTTPRoutePathPrefix",
974 "HTTPRoutePathRegex",
975 "HTTPRouteTLSPassthrough",
976 "HTTPRouteTLSTerminate",
977 "HTTPRouteTLSReencrypt",
978 "Ingress",
979 "IngressClass",
980 ]
981}
982
983pub fn skipped_features() -> Vec<&'static str> {
986 vec![
987 "GRPCRoute",
988 "TCPRoute",
989 "TLSRoute",
990 "HTTPRouteRequestMirror",
991 "HTTPRouteRequestMultipleMirrors",
992 "HTTPRouteBackendProtocolWebSocket",
993 "HTTPRouteBackendProtocolH2C",
994 "HTTPRouteWeightedBackend",
995 ]
996}