1use std::{
2 collections::{BTreeMap, HashMap},
3 fmt::Debug,
4 str::FromStr,
5};
6
7use crate::{
8 error::{Error, ErrorContext},
9 http::{
10 BackendRef, HeaderMatch, Method, PathMatch, QueryParamMatch, Route, RouteMatch, RouteRetry,
11 RouteRule, RouteTimeouts,
12 },
13 shared::{Duration, Regex},
14 Name,
15};
16use xds_api::pb::{
17 envoy::{
18 config::{
19 core::v3 as xds_core,
20 route::v3::{self as xds_route, query_parameter_matcher::QueryParameterMatchSpecifier},
21 },
22 r#type::matcher::v3::{string_matcher::MatchPattern, StringMatcher},
23 },
24 google::{
25 self,
26 protobuf::{self, UInt32Value},
27 },
28};
29
30use crate::xds::shared::{parse_xds_regex, regex_matcher};
31
32impl TryInto<Route> for &xds_route::RouteConfiguration {
33 type Error = Error;
34
35 fn try_into(self) -> Result<Route, Self::Error> {
36 Route::from_xds(self)
37 }
38}
39
40impl From<&Route> for xds_route::RouteConfiguration {
41 fn from(route: &Route) -> Self {
42 route.to_xds()
43 }
44}
45
46impl Route {
47 pub fn from_xds(xds: &xds_route::RouteConfiguration) -> Result<Self, Error> {
48 let id = Name::from_str(&xds.name).with_field("name")?;
49 let tags = tags_from_xds(&xds.metadata)?;
50
51 let [vhost] = &xds.virtual_hosts[..] else {
52 return Err(Error::new_static(
53 "RouteConfiguration must have exactly one VirtualHost",
54 ));
55 };
56
57 let mut hostnames = Vec::new();
58 let mut ports = Vec::new();
59 for (i, domain) in vhost.domains.iter().enumerate() {
60 let (name, port) = crate::parse_port(domain).with_field_index("domains", i)?;
61
62 let name = crate::http::HostnameMatch::from_str(name)
63 .with_field_index("domains", i)
64 .with_field_index("virtual_hosts", 0)?;
65
66 hostnames.push(name);
67 if let Some(port) = port {
68 ports.push(port);
69 }
70 }
71 hostnames.sort();
72 hostnames.dedup();
73 ports.sort();
74 ports.dedup();
75
76 let mut rules = vec![];
77 let actions_and_matches = vhost.routes.iter().enumerate().map(|(route_idx, route)| {
78 let key = (route.name.as_str(), route.action.as_ref());
79 (key, (route_idx, route))
80 });
81
82 for ((name, action), matches) in group_by(actions_and_matches) {
83 let action_idx = matches
85 .first()
86 .map(|(idx, _)| *idx)
87 .expect("missing route index");
88
89 let Some(action) = &action else {
90 return Err(Error::new_static("route has no route action"))
91 .with_field_index("routes", action_idx)
92 .with_field_index("virtual_hosts", 0);
93 };
94
95 rules.push(
96 RouteRule::from_xds_grouped(name, action, &matches)
97 .with_field_index("virtual_hosts", 0)?,
98 );
99 }
100
101 Ok(Route {
102 id,
103 hostnames,
104 ports,
105 tags,
106 rules,
107 })
108 }
109
110 pub fn to_xds(&self) -> xds_route::RouteConfiguration {
111 let routes = self.rules.iter().flat_map(|rule| rule.to_xds()).collect();
112
113 let mut domains = Vec::with_capacity(self.hostnames.len() * self.ports.len().min(1));
114 for hostname in &self.hostnames {
115 if self.ports.is_empty() {
116 domains.push(hostname.to_string());
117 } else {
118 for port in &self.ports {
119 domains.push(format!("{hostname}:{port}"));
120 }
121 }
122 }
123
124 let virtual_hosts = vec![xds_route::VirtualHost {
125 domains,
126 routes,
127 ..Default::default()
128 }];
129
130 let name = self.id.to_string();
131 let metadata = tags_to_xds(&self.tags);
132
133 xds_route::RouteConfiguration {
134 name,
135 metadata,
136 virtual_hosts,
137 ..Default::default()
138 }
139 }
140}
141
142const JUNCTION_ROUTE_TAGS: &str = "io.junctionlabs.route.tags";
143
144fn tags_to_xds(tags: &BTreeMap<String, String>) -> Option<xds_core::Metadata> {
145 if tags.is_empty() {
146 return None;
147 }
148
149 let fields: HashMap<_, _> = tags
150 .iter()
151 .map(|(k, v)| {
152 let v = protobuf::Value {
153 kind: Some(protobuf::value::Kind::StringValue(v.clone())),
154 };
155 (k.clone(), v)
156 })
157 .collect();
158
159 let mut metadata = xds_core::Metadata::default();
160 metadata
161 .filter_metadata
162 .insert(JUNCTION_ROUTE_TAGS.to_string(), protobuf::Struct { fields });
163
164 Some(metadata)
165}
166
167fn tags_from_xds(metadata: &Option<xds_core::Metadata>) -> Result<BTreeMap<String, String>, Error> {
168 let Some(metadata) = metadata else {
169 return Ok(Default::default());
170 };
171
172 let Some(route_tags) = metadata.filter_metadata.get(JUNCTION_ROUTE_TAGS) else {
173 return Ok(Default::default());
174 };
175
176 let mut tags = BTreeMap::new();
177 for (k, v) in route_tags.fields.iter() {
178 let v = match &v.kind {
179 Some(protobuf::value::Kind::StringValue(v)) => v.clone(),
180 _ => {
181 return Err(Error::new_static("invalid tag"))
182 .with_fields("filter_metadata", JUNCTION_ROUTE_TAGS)
183 }
184 };
185
186 tags.insert(k.clone(), v);
187 }
188
189 Ok(tags)
190}
191
192impl RouteRule {
193 fn from_xds_grouped(
194 name: &str,
195 action: &xds_route::route::Action,
196 routes: &[(usize, &xds_route::Route)],
197 ) -> Result<Self, Error> {
198 let mut matches = vec![];
199 for (route_idx, route) in routes {
200 if let Some(route_match) = &route.r#match {
201 let m = RouteMatch::from_xds(route_match)
202 .with_field("match")
203 .with_field_index("route", *route_idx)?;
204 matches.push(m);
205 }
206 }
207
208 let first_route_idx = routes
212 .first()
213 .map(|(idx, _)| *idx)
214 .expect("no Routes grouped with action. this is a bug in Junction");
215
216 let name = if !name.is_empty() {
218 Some(
219 Name::from_str(name)
220 .with_field("name")
221 .with_field_index("route", first_route_idx)?,
222 )
223 } else {
224 None
225 };
226
227 let action = match action {
228 xds_route::route::Action::Route(action) => action,
229 _ => {
230 return Err(Error::new_static("unsupported route action").with_field("action"))
231 .with_field_index("route", first_route_idx)
232 }
233 };
234
235 let timeouts = RouteTimeouts::from_xds(action)?;
236 let retry = action.retry_policy.as_ref().and_then(RouteRetry::from_xds);
237 let backends = BackendRef::from_xds(action.cluster_specifier.as_ref())
240 .with_field("action")
241 .with_field_index("route", first_route_idx)?;
242
243 Ok(RouteRule {
244 name,
245 matches,
246 retry,
247 filters: vec![],
248 timeouts,
249 backends,
250 })
251 }
252
253 pub fn to_xds(&self) -> Vec<xds_route::Route> {
254 let mut retry_policy = self.retry.as_ref().map(RouteRetry::to_xds);
256
257 let (timeout, per_try_timeout) = self
263 .timeouts
264 .as_ref()
265 .map(RouteTimeouts::to_xds)
266 .unwrap_or((None, None));
267
268 if let Some(per_try_timeout) = per_try_timeout {
269 retry_policy
270 .get_or_insert_with(Default::default)
271 .per_try_timeout = Some(per_try_timeout);
272 }
273
274 let cluster_specifier = BackendRef::to_xds(&self.backends);
275
276 let route_action = xds_route::route::Action::Route(xds_route::RouteAction {
278 timeout,
279 retry_policy,
280 cluster_specifier,
281 ..Default::default()
282 });
283
284 let name = self
285 .name
286 .as_ref()
287 .map(|name| name.to_string())
288 .unwrap_or_default();
289
290 if self.matches.is_empty() {
291 vec![xds_route::Route {
292 name,
293 r#match: Some(xds_route::RouteMatch {
294 path_specifier: Some(xds_route::route_match::PathSpecifier::Prefix(
295 "".to_string(),
296 )),
297 ..Default::default()
298 }),
299 action: Some(route_action),
300 ..Default::default()
301 }]
302 } else {
303 self.matches
304 .iter()
305 .map(|route_match| {
306 let r#match = Some(route_match.to_xds());
307 xds_route::Route {
308 name: name.clone(),
309 r#match,
310 action: Some(route_action.clone()),
311 ..Default::default()
312 }
313 })
314 .collect()
315 }
316 }
317}
318
319impl RouteTimeouts {
320 pub fn from_xds(r: &xds_route::RouteAction) -> Result<Option<Self>, Error> {
321 let request = r.timeout.map(Duration::try_from).transpose()?;
322 let backend_request = r
323 .retry_policy
324 .as_ref()
325 .and_then(|retry_policy| retry_policy.per_try_timeout.map(Duration::try_from))
326 .transpose()?;
327
328 if request.is_some() || backend_request.is_some() {
329 Ok(Some(RouteTimeouts {
330 request,
331 backend_request,
332 }))
333 } else {
334 Ok(None)
335 }
336 }
337
338 pub fn to_xds(
339 &self,
340 ) -> (
341 Option<google::protobuf::Duration>,
342 Option<google::protobuf::Duration>,
343 ) {
344 let request_timeout = self.request.map(|d| d.try_into().unwrap());
345 let per_try_timeout = self.backend_request.map(|d| d.try_into().unwrap());
346 (request_timeout, per_try_timeout)
347 }
348}
349
350impl RouteMatch {
351 pub fn from_xds(r: &xds_route::RouteMatch) -> Result<Self, Error> {
352 let path = r
355 .path_specifier
356 .as_ref()
357 .map(PathMatch::from_xds)
358 .transpose()?;
359
360 let mut method: Option<Method> = None;
365 let mut headers = vec![];
366 for (i, header) in r.headers.iter().enumerate() {
367 let header_match = HeaderMatch::from_xds(header).with_field_index("headers", i)?;
368
369 match header_match {
370 HeaderMatch::Exact { name, value } if name == ":method" => {
371 method = Some(value);
372 }
373 _ => {
374 headers.push(header_match);
375 }
376 }
377 }
378
379 let query_params = r
382 .query_parameters
383 .iter()
384 .enumerate()
385 .map(|(i, e)| QueryParamMatch::from_xds(e).with_field_index("query_parameters", i))
386 .collect::<Result<Vec<_>, _>>()?;
387
388 Ok(RouteMatch {
389 headers,
390 method,
391 path,
392 query_params,
393 })
394 }
395
396 fn to_xds(&self) -> xds_route::RouteMatch {
397 let path_specifier = self.path.as_ref().map(|p| p.to_xds());
399
400 let mut headers = vec![];
401 if let Some(method) = &self.method {
403 headers.push(xds_route::HeaderMatcher {
404 name: ":method".to_string(),
405 header_match_specifier: Some(
406 xds_route::header_matcher::HeaderMatchSpecifier::ExactMatch(method.to_string()),
407 ),
408 ..Default::default()
409 })
410 }
411 for header_match in &self.headers {
413 headers.push(header_match.to_xds());
414 }
415
416 let query_parameters = self
418 .query_params
419 .iter()
420 .map(QueryParamMatch::to_xds)
421 .collect();
422
423 xds_route::RouteMatch {
424 headers,
425 path_specifier,
426 query_parameters,
427 ..Default::default()
428 }
429 }
430}
431
432impl QueryParamMatch {
433 pub fn from_xds(matcher: &xds_route::QueryParameterMatcher) -> Result<Self, Error> {
434 let name = matcher.name.clone();
435 match matcher.query_parameter_match_specifier.as_ref() {
436 Some(QueryParameterMatchSpecifier::StringMatch(s)) => {
437 let match_pattern = match s.match_pattern.as_ref() {
438 Some(MatchPattern::Exact(s)) => Ok(QueryParamMatch::Exact {
439 name,
440 value: s.clone(),
441 }),
442 Some(MatchPattern::SafeRegex(pfx)) => Ok(QueryParamMatch::RegularExpression {
443 name,
444 value: parse_xds_regex(pfx)?,
445 }),
446 Some(_) => Err(Error::new_static("unsupported string match type")),
447 None => Err(Error::new_static("missing string match")),
448 };
449 match_pattern.with_field("string_match")
450 }
451 Some(QueryParameterMatchSpecifier::PresentMatch(true)) => {
452 Ok(QueryParamMatch::RegularExpression {
453 name,
454 value: Regex::from_str(".*").unwrap(),
455 })
456 }
457 Some(QueryParameterMatchSpecifier::PresentMatch(false)) => {
458 Err(Error::new_static("absent matches are not supported")
459 .with_field("present_match"))
460 }
461 None => Ok(QueryParamMatch::RegularExpression {
466 name,
467 value: Regex::from_str(".*").unwrap(),
468 }),
469 }
470 }
471
472 pub fn to_xds(&self) -> xds_route::QueryParameterMatcher {
473 let (name, matcher) = match self {
474 QueryParamMatch::RegularExpression { name, value } => {
475 let name = name.clone();
476 let matcher = MatchPattern::SafeRegex(regex_matcher(value));
477 (name, matcher)
478 }
479 QueryParamMatch::Exact { name, value } => {
480 let name = name.clone();
481 let matcher = MatchPattern::Exact(value.to_string());
482 (name, matcher)
483 }
484 };
485
486 xds_route::QueryParameterMatcher {
487 name,
488 query_parameter_match_specifier: Some(QueryParameterMatchSpecifier::StringMatch(
489 StringMatcher {
490 match_pattern: Some(matcher),
491 ignore_case: false,
492 },
493 )),
494 }
495 }
496}
497
498impl HeaderMatch {
499 fn from_xds(header_matcher: &xds_route::HeaderMatcher) -> Result<Self, Error> {
500 use xds_route::header_matcher::HeaderMatchSpecifier;
501
502 let name = header_matcher.name.clone();
503 match header_matcher.header_match_specifier.as_ref() {
504 Some(HeaderMatchSpecifier::ExactMatch(value)) => Ok(HeaderMatch::Exact {
505 name,
506 value: value.clone(),
507 }),
508 Some(HeaderMatchSpecifier::SafeRegexMatch(regex)) => {
509 Ok(HeaderMatch::RegularExpression {
510 name,
511 value: parse_xds_regex(regex)?,
512 })
513 }
514 Some(HeaderMatchSpecifier::PresentMatch(true)) => Ok(HeaderMatch::RegularExpression {
516 name,
517 value: Regex::from_str(".*").unwrap(),
518 }),
519 Some(_) => Err(Error::new_static("unsupported matcher")),
520 None => Ok(HeaderMatch::RegularExpression {
521 name,
522 value: Regex::from_str(".*").unwrap(),
523 }),
524 }
525 }
526
527 fn to_xds(&self) -> xds_route::HeaderMatcher {
528 match self {
529 HeaderMatch::RegularExpression { name, value } => xds_route::HeaderMatcher {
530 name: name.clone(),
531 header_match_specifier: Some(
532 xds_route::header_matcher::HeaderMatchSpecifier::SafeRegexMatch(regex_matcher(
533 value,
534 )),
535 ),
536 ..Default::default()
537 },
538 HeaderMatch::Exact { name, value } => xds_route::HeaderMatcher {
539 name: name.clone(),
540 header_match_specifier: Some(
541 xds_route::header_matcher::HeaderMatchSpecifier::ExactMatch(value.to_string()),
542 ),
543 ..Default::default()
544 },
545 }
546 }
547}
548
549impl PathMatch {
550 fn from_xds(path_spec: &xds_route::route_match::PathSpecifier) -> Result<Self, Error> {
551 match path_spec {
552 xds_route::route_match::PathSpecifier::Prefix(p) => {
553 Ok(PathMatch::Prefix { value: p.clone() })
554 }
555 xds_route::route_match::PathSpecifier::Path(p) => {
556 Ok(PathMatch::Exact { value: p.clone() })
557 }
558 xds_route::route_match::PathSpecifier::SafeRegex(p) => {
559 Ok(PathMatch::RegularExpression {
560 value: parse_xds_regex(p).with_field("safe_regex")?,
561 })
562 }
563 _ => Err(Error::new_static("unsupported path specifier")),
564 }
565 }
566
567 pub fn to_xds(&self) -> xds_route::route_match::PathSpecifier {
568 match self {
569 PathMatch::Prefix { value } => {
570 xds_route::route_match::PathSpecifier::Prefix(value.to_string())
571 }
572 PathMatch::RegularExpression { value } => {
573 xds_route::route_match::PathSpecifier::SafeRegex(regex_matcher(value))
574 }
575 PathMatch::Exact { value } => {
576 xds_route::route_match::PathSpecifier::Path(value.clone())
577 }
578 }
579 }
580}
581
582impl RouteRetry {
583 pub fn from_xds(r: &xds_route::RetryPolicy) -> Option<Self> {
584 if r.retriable_status_codes.is_empty()
585 && r.num_retries.is_none()
586 && r.retry_back_off.is_none()
587 {
588 return None;
590 }
591 let codes = r
592 .retriable_status_codes
593 .iter()
594 .map(|code| *code as u16)
595 .collect();
596 let attempts = r.num_retries.map(|v| u32::from(v) + 1);
597 let backoff = r
598 .retry_back_off
599 .as_ref()
600 .and_then(|r2| r2.base_interval.map(|x| x.try_into().unwrap()));
601 Some(Self {
602 codes,
603 attempts,
604 backoff,
605 })
606 }
607
608 pub fn to_xds(&self) -> xds_route::RetryPolicy {
609 let retriable_status_codes = self.codes.iter().map(|&code| code as u32).collect();
610 let num_retries = self
611 .attempts
612 .map(|attempts| UInt32Value::from(attempts.saturating_sub(1)));
613
614 let retry_back_off = self.backoff.map(|b| xds_route::retry_policy::RetryBackOff {
615 base_interval: Some(b.try_into().unwrap()),
616 max_interval: None,
617 });
618
619 xds_route::RetryPolicy {
620 retriable_status_codes,
621 num_retries,
622 retry_back_off,
623 ..Default::default()
624 }
625 }
626}
627
628fn group_by<I, K, V>(iter: I) -> GroupBy<<I as IntoIterator>::IntoIter, K, V>
636where
637 I: IntoIterator<Item = (K, V)>,
638 K: PartialEq,
639{
640 GroupBy {
641 iter: iter.into_iter(),
642 current_key: None,
643 current_values: Vec::new(),
644 }
645}
646
647struct GroupBy<I, K, V> {
648 iter: I,
649 current_key: Option<K>,
650 current_values: Vec<V>,
651}
652
653impl<I, K, V> Iterator for GroupBy<I, K, V>
654where
655 I: Iterator<Item = (K, V)>,
656 K: PartialEq + Debug,
657 V: Debug,
658{
659 type Item = (K, Vec<V>);
660
661 fn next(&mut self) -> Option<Self::Item> {
662 loop {
663 match (self.current_key.take(), self.iter.next()) {
664 (None, Some((k, v))) => {
666 self.current_key = Some(k);
667 self.current_values.push(v);
668 }
669 (Some(current_key), Some((next_key, v))) if next_key == current_key => {
671 self.current_key = Some(current_key);
672 self.current_values.push(v)
673 }
674 (Some(current_key), Some((next_key, v))) => {
676 let values = std::mem::take(&mut self.current_values);
677
678 self.current_key = Some(next_key);
680 self.current_values.push(v);
681
682 return Some((current_key, values));
683 }
684 (Some(key), None) => {
686 let values = std::mem::take(&mut self.current_values);
687 return Some((key, values));
688 }
689 (None, None) => return None,
691 }
692 }
693 }
694}
695
696impl BackendRef {
697 pub(crate) fn to_xds(wbs: &[Self]) -> Option<xds_route::route_action::ClusterSpecifier> {
698 match wbs {
699 [] => None,
700 [backend] => Some(xds_route::route_action::ClusterSpecifier::Cluster(
701 backend.name(),
702 )),
703 targets => {
704 let clusters = targets
705 .iter()
706 .map(|wb| xds_route::weighted_cluster::ClusterWeight {
707 name: wb.name(),
708 weight: Some(wb.weight.into()),
709 ..Default::default()
710 })
711 .collect();
712 Some(xds_route::route_action::ClusterSpecifier::WeightedClusters(
713 xds_route::WeightedCluster {
714 clusters,
715 ..Default::default()
716 },
717 ))
718 }
719 }
720 }
721
722 pub(crate) fn from_xds(
723 xds: Option<&xds_route::route_action::ClusterSpecifier>,
724 ) -> Result<Vec<Self>, Error> {
725 match xds {
726 Some(xds_route::route_action::ClusterSpecifier::Cluster(name)) => {
727 BackendRef::from_str(name)
728 .map(|br| vec![br])
729 .with_field("cluster")
730 }
731 Some(xds_route::route_action::ClusterSpecifier::WeightedClusters(
732 weighted_clusters,
733 )) => {
734 let clusters = weighted_clusters.clusters.iter().enumerate().map(|(i, w)| {
735 let backend_ref = BackendRef::from_str(&w.name).with_field_index("name", i)?;
736 let weight = crate::value_or_default!(w.weight, 1);
737 Ok(Self {
738 weight,
739 ..backend_ref
740 })
741 });
742
743 clusters
744 .collect::<Result<Vec<_>, _>>()
745 .with_fields("weighted_clusters", "clusters")
746 }
747 Some(_) => Err(Error::new_static("unsupported cluster specifier")),
748 None => Ok(Vec::new()),
749 }
750 }
751}
752
753#[cfg(test)]
754mod test {
755 use super::*;
756 use crate::{http::HostnameMatch, Hostname, Service};
757
758 #[test]
759 fn test_group_by() {
760 let groups: Vec<_> = group_by([(1, "a"), (2, "b"), (3, "c")]).collect();
761 assert_eq!(vec![(1, vec!["a"]), (2, vec!["b"]), (3, vec!["c"])], groups);
762
763 let groups: Vec<_> = group_by([
764 (1, "a"),
765 (1, "a"),
766 (2, "b"),
767 (3, "c"),
768 (3, "c"),
769 (3, "c"),
770 (1, "a"),
771 ])
772 .collect();
773 assert_eq!(
774 vec![
775 (1, vec!["a", "a"]),
776 (2, vec!["b"]),
777 (3, vec!["c", "c", "c"]),
778 (1, vec!["a"]),
779 ],
780 groups
781 );
782 }
783
784 #[test]
785 fn test_simple_route() {
786 let web = Service::kube("prod", "web").unwrap();
787
788 let original = Route {
789 id: Name::from_static("my-route"),
790 hostnames: vec![web.hostname().into()],
791 ports: vec![],
792 tags: Default::default(),
793 rules: vec![RouteRule {
794 backends: vec![BackendRef {
795 weight: 1,
796 service: web.clone(),
797 port: None,
798 }],
799 ..Default::default()
800 }],
801 };
802
803 let round_tripped = Route::from_xds(&original.to_xds()).unwrap();
804 let expected = Route {
805 id: Name::from_static("my-route"),
806 hostnames: vec![web.hostname().into()],
807 ports: vec![],
808 tags: Default::default(),
809 rules: vec![RouteRule {
810 matches: vec![RouteMatch {
811 path: Some(PathMatch::empty_prefix()),
812 ..Default::default()
813 }],
814 backends: vec![BackendRef {
815 weight: 1,
816 service: web.clone(),
817 port: None,
818 }],
819 ..Default::default()
820 }],
821 };
822 assert_eq!(round_tripped, expected)
823 }
824
825 #[test]
826 fn test_wildcard_hostname() {
827 let web = Service::kube("prod", "web").unwrap();
828
829 let original = Route {
830 id: Name::from_static("my-route"),
831 hostnames: vec![
832 HostnameMatch::from_str("*.prod.web.svc.cluster.local").unwrap(),
833 HostnameMatch::from_str("*.staging.web.svc.cluster.local").unwrap(),
834 ],
835 ports: vec![80, 443],
836 tags: Default::default(),
837 rules: vec![RouteRule {
838 matches: vec![RouteMatch {
839 path: Some(PathMatch::empty_prefix()),
840 ..Default::default()
841 }],
842 backends: vec![BackendRef {
843 weight: 1,
844 service: web.clone(),
845 port: None,
846 }],
847 ..Default::default()
848 }],
849 };
850
851 let round_tripped = Route::from_xds(&original.to_xds()).unwrap();
852 assert_eq!(round_tripped, original)
853 }
854
855 #[test]
856 fn test_route_no_rules() {
857 let original = Route {
858 id: Name::from_static("no-rules"),
859 hostnames: vec![Hostname::from_static("web.internal").into()],
860 ports: vec![],
861 tags: Default::default(),
862 rules: vec![],
863 };
864
865 let round_tripped = Route::from_xds(&original.to_xds()).unwrap();
866 assert_eq!(round_tripped, original)
867 }
868
869 #[test]
870 fn test_route_rule_no_backend() {
871 let original = Route {
872 id: Name::from_static("no-backends"),
873 hostnames: vec![Hostname::from_static("web.internal").into()],
874 ports: vec![],
875 tags: Default::default(),
876 rules: vec![RouteRule::default()],
877 };
878 let normalized = Route {
879 id: Name::from_static("no-backends"),
880 hostnames: vec![Hostname::from_static("web.internal").into()],
881 ports: vec![],
882 tags: Default::default(),
883 rules: vec![RouteRule {
884 matches: vec![RouteMatch {
885 path: Some(PathMatch::Prefix {
886 value: "".to_string(),
887 }),
888 ..Default::default()
889 }],
890 ..Default::default()
891 }],
892 };
893
894 let round_tripped = Route::from_xds(&original.to_xds()).unwrap();
895 assert_eq!(round_tripped, normalized)
896 }
897
898 #[test]
899 fn test_metadata_roundtrip() {
900 let web = Service::kube("prod", "web").unwrap();
901
902 assert_roundtrip::<_, xds_route::RouteConfiguration>(Route {
903 id: Name::from_static("metadata"),
904 hostnames: vec![Hostname::from_static("web.internal").into()],
905 ports: vec![],
906 tags: BTreeMap::from_iter([("foo".to_string(), "bar".to_string())]),
907 rules: vec![RouteRule {
908 matches: vec![RouteMatch {
909 path: Some(PathMatch::Prefix {
910 value: "".to_string(),
911 }),
912 ..Default::default()
913 }],
914 backends: vec![BackendRef {
915 weight: 1,
916 service: web.clone(),
917 port: Some(8778),
918 }],
919 ..Default::default()
920 }],
921 });
922 }
923
924 #[test]
925 fn test_multiple_rules_roundtrip() {
926 let web = Service::kube("prod", "web").unwrap();
927 let staging = Service::kube("staging", "web").unwrap();
928
929 assert_roundtrip::<_, xds_route::RouteConfiguration>(Route {
931 id: Name::from_static("multiple-targets"),
932 hostnames: vec![Hostname::from_static("web.internal").into()],
933 ports: vec![],
934 tags: Default::default(),
935 rules: vec![
936 RouteRule {
937 name: Some(Name::from_static("split-web")),
938 matches: vec![RouteMatch {
939 path: Some(PathMatch::Exact {
940 value: "/foo/feature-test".to_string(),
941 }),
942 ..Default::default()
943 }],
944 backends: vec![
945 BackendRef {
946 weight: 3,
947 service: staging.clone(),
948 port: Some(80),
949 },
950 BackendRef {
951 weight: 1,
952 service: web.clone(),
953 port: Some(80),
954 },
955 ],
956 ..Default::default()
957 },
958 RouteRule {
959 name: Some(Name::from_static("one-web")),
960 matches: vec![RouteMatch {
961 path: Some(PathMatch::Prefix {
962 value: "/foo".to_string(),
963 }),
964 ..Default::default()
965 }],
966 backends: vec![BackendRef {
967 weight: 1,
968 service: web.clone(),
969 port: Some(80),
970 }],
971 ..Default::default()
972 },
973 ],
974 });
975
976 assert_roundtrip::<_, xds_route::RouteConfiguration>(Route {
978 id: Name::from_static("same-target-multiple-timeouts"),
979 hostnames: vec![Hostname::from_static("web.internal").into()],
980 ports: vec![],
981 tags: Default::default(),
982 rules: vec![
983 RouteRule {
984 name: Some(Name::from_static("no-timeouts")),
985 matches: vec![RouteMatch {
986 path: Some(PathMatch::Exact {
987 value: "/foo/feature-test".to_string(),
988 }),
989 ..Default::default()
990 }],
991 backends: vec![BackendRef {
992 weight: 1,
993 service: web.clone(),
994 port: None,
995 }],
996 ..Default::default()
997 },
998 RouteRule {
999 name: Some(Name::from_static("with-timeouts")),
1000 matches: vec![RouteMatch {
1001 path: Some(PathMatch::Prefix {
1002 value: "/foo".to_string(),
1003 }),
1004 ..Default::default()
1005 }],
1006 timeouts: Some(RouteTimeouts {
1007 request: Some(Duration::from_secs(123)),
1008 backend_request: None,
1009 }),
1010 backends: vec![BackendRef {
1011 weight: 1,
1012 service: web.clone(),
1013 port: None,
1014 }],
1015 ..Default::default()
1016 },
1017 ],
1018 });
1019
1020 assert_roundtrip::<_, xds_route::RouteConfiguration>(Route {
1022 id: Name::from_static("same-target-multiple-retries"),
1023 hostnames: vec![Hostname::from_static("web.internal").into()],
1024 ports: vec![],
1025 tags: Default::default(),
1026 rules: vec![
1027 RouteRule {
1028 matches: vec![RouteMatch {
1029 path: Some(PathMatch::Exact {
1030 value: "/foo/feature-test".to_string(),
1031 }),
1032 ..Default::default()
1033 }],
1034 retry: Some(RouteRetry {
1035 codes: vec![500, 503],
1036 attempts: Some(123),
1037 backoff: Some(Duration::from_secs(1)),
1038 }),
1039 backends: vec![BackendRef {
1040 weight: 1,
1041 service: web.clone(),
1042 port: None,
1043 }],
1044 ..Default::default()
1045 },
1046 RouteRule {
1047 matches: vec![RouteMatch {
1048 path: Some(PathMatch::Prefix {
1049 value: "/foo".to_string(),
1050 }),
1051 ..Default::default()
1052 }],
1053 backends: vec![BackendRef {
1054 weight: 1,
1055 service: web.clone(),
1056 port: None,
1057 }],
1058 ..Default::default()
1059 },
1060 ],
1061 });
1062
1063 assert_roundtrip::<_, xds_route::RouteConfiguration>(Route {
1065 id: Name::from_static("different-names"),
1066 hostnames: vec![Hostname::from_static("web.internal").into()],
1067 ports: vec![],
1068 tags: Default::default(),
1069 rules: vec![
1070 RouteRule {
1071 name: Some(Name::from_static("rule-1")),
1072 matches: vec![RouteMatch {
1073 path: Some(PathMatch::Prefix {
1074 value: "/foo".to_string(),
1075 }),
1076 ..Default::default()
1077 }],
1078 backends: vec![BackendRef {
1079 weight: 1,
1080 service: web.clone(),
1081 port: None,
1082 }],
1083 ..Default::default()
1084 },
1085 RouteRule {
1086 name: Some(Name::from_static("rule-2")),
1087 matches: vec![RouteMatch {
1088 path: Some(PathMatch::Prefix {
1089 value: "/foo".to_string(),
1090 }),
1091 ..Default::default()
1092 }],
1093 backends: vec![BackendRef {
1094 weight: 1,
1095 service: web.clone(),
1096 port: None,
1097 }],
1098 ..Default::default()
1099 },
1100 ],
1101 });
1102 }
1103
1104 #[test]
1105 fn test_condense_rules() {
1106 let web = Service::kube("prod", "web").unwrap();
1107
1108 let original = Route {
1110 id: Name::from_static("will-be-condensed"),
1111 hostnames: vec![Hostname::from_static("web.internal").into()],
1112 ports: vec![],
1113 tags: Default::default(),
1114 rules: vec![
1115 RouteRule {
1116 matches: vec![RouteMatch {
1117 path: Some(PathMatch::Exact {
1118 value: "/foo/feature-test".to_string(),
1119 }),
1120 ..Default::default()
1121 }],
1122 backends: vec![BackendRef {
1123 weight: 1,
1124 service: web.clone(),
1125 port: Some(80),
1126 }],
1127 ..Default::default()
1128 },
1129 RouteRule {
1130 matches: vec![RouteMatch {
1131 path: Some(PathMatch::Prefix {
1132 value: "/foo".to_string(),
1133 }),
1134 ..Default::default()
1135 }],
1136 backends: vec![BackendRef {
1137 weight: 1,
1138 service: web.clone(),
1139 port: Some(80),
1140 }],
1141 ..Default::default()
1142 },
1143 ],
1144 };
1145
1146 let converted = Route::from_xds(&original.to_xds()).unwrap();
1148 assert_eq!(
1149 converted,
1150 Route {
1151 id: Name::from_static("will-be-condensed"),
1152 hostnames: vec![Hostname::from_static("web.internal").into()],
1153 ports: vec![],
1154 tags: Default::default(),
1155 rules: vec![RouteRule {
1156 matches: vec![
1157 RouteMatch {
1158 path: Some(PathMatch::Exact {
1159 value: "/foo/feature-test".to_string(),
1160 }),
1161 ..Default::default()
1162 },
1163 RouteMatch {
1164 path: Some(PathMatch::Prefix {
1165 value: "/foo".to_string(),
1166 }),
1167 ..Default::default()
1168 }
1169 ],
1170 backends: vec![BackendRef {
1171 weight: 1,
1172 service: web.clone(),
1173 port: Some(80),
1174 }],
1175 ..Default::default()
1176 },],
1177 }
1178 )
1179 }
1180
1181 #[test]
1182 fn test_multiple_matches_roundtrip() {
1183 let web = Service::kube("prod", "web").unwrap();
1184
1185 assert_roundtrip::<_, xds_route::RouteConfiguration>(Route {
1186 id: Name::from_static("multiple-matches"),
1187 hostnames: vec![Hostname::from_static("web.internal").into()],
1188 ports: vec![],
1189 tags: Default::default(),
1190 rules: vec![RouteRule {
1191 matches: vec![
1192 RouteMatch {
1193 path: Some(PathMatch::Prefix {
1194 value: "/foo".to_string(),
1195 }),
1196 ..Default::default()
1197 },
1198 RouteMatch {
1199 path: Some(PathMatch::Prefix {
1200 value: "/bar".to_string(),
1201 }),
1202 ..Default::default()
1203 },
1204 RouteMatch {
1205 query_params: vec![QueryParamMatch::Exact {
1206 name: "param".to_string(),
1207 value: "an_value".to_string(),
1208 }],
1209 ..Default::default()
1210 },
1211 ],
1212 backends: vec![BackendRef {
1213 weight: 1,
1214 service: web.clone(),
1215 port: Some(80),
1216 }],
1217 ..Default::default()
1218 }],
1219 });
1220 }
1221
1222 #[test]
1223 fn test_full_route_match_roundtrips() {
1224 let web = Service::kube("prod", "web").unwrap();
1225
1226 assert_roundtrip::<_, xds_route::RouteConfiguration>(Route {
1227 id: Name::from_static("full-send"),
1228 hostnames: vec![
1229 HostnameMatch::from_str("*.web.internal").unwrap(),
1230 Hostname::from_static("potato.tomato").into(),
1231 Hostname::from_static("web.internal").into(),
1232 ],
1233 ports: vec![80, 443, 8080],
1234 tags: [("foo".to_string(), "bar".to_string())]
1235 .into_iter()
1236 .collect(),
1237 rules: vec![RouteRule {
1238 matches: vec![RouteMatch {
1239 path: Some(PathMatch::Prefix {
1240 value: "/potato".to_string(),
1241 }),
1242 headers: vec![HeaderMatch::RegularExpression {
1243 name: "x-one".to_string(),
1244 value: ".*".parse().unwrap(),
1245 }],
1246 query_params: vec![
1247 QueryParamMatch::RegularExpression {
1248 name: "foo".to_string(),
1249 value: r"\w+".parse().unwrap(),
1250 },
1251 QueryParamMatch::Exact {
1252 name: "bar".to_string(),
1253 value: "baz".to_string(),
1254 },
1255 ],
1256 method: Some("CONNECT".to_string()),
1257 }],
1258 backends: vec![
1259 BackendRef {
1260 weight: 1,
1261 service: web.clone(),
1262 port: Some(8080),
1263 },
1264 BackendRef {
1265 weight: 0,
1266 service: web.clone(),
1267 port: None,
1268 },
1269 ],
1270 ..Default::default()
1271 }],
1272 });
1273 }
1274
1275 #[track_caller]
1276 fn assert_roundtrip<T, Xds>(v: T)
1277 where
1278 T: PartialEq + std::fmt::Debug,
1279 for<'a> &'a T: Into<Xds>,
1280 for<'a> &'a Xds: TryInto<T, Error = Error>,
1281 {
1282 let xds: Xds = (&v).into();
1283 let back: T = (&xds).try_into().unwrap();
1284 assert_eq!(v, back);
1285 }
1286}