1mod error;
6mod url;
7pub use crate::error::{Error, Result};
8pub use crate::url::Url;
9
10pub(crate) mod hash;
11pub(crate) mod rand;
12
13mod endpoints;
14pub use endpoints::Endpoint;
15use endpoints::EndpointGroup;
16
17mod client;
18mod dns;
19mod load_balancer;
20mod xds;
21
22pub use client::{
23 Client, HttpRequest, HttpResult, LbContext, ResolvedRoute, SearchConfig, SelectedEndpoint,
24};
25use error::Trace;
26use futures::FutureExt;
27use junction_api::Name;
28pub use xds::{ResourceVersion, XdsConfig};
29
30use junction_api::backend::BackendId;
31use junction_api::http::Route;
32use std::collections::{HashMap, HashSet};
33use std::future::Future;
34use std::sync::Arc;
35
36pub use crate::load_balancer::{BackendLb, LoadBalancer};
37use junction_api::backend::{Backend, LbPolicy};
38
39pub fn check_route(
49 routes: Vec<Route>,
50 method: &http::Method,
51 url: &crate::Url,
52 headers: &http::HeaderMap,
53 search_config: Option<&SearchConfig>,
54) -> Result<ResolvedRoute> {
55 let request = client::HttpRequest::from_parts(method, url, headers)?;
56 let config = StaticConfig::new(routes, Vec::new());
62 let search_config = search_config.cloned().unwrap_or_default();
63
64 client::resolve_routes(&config, Trace::new(), request, None, &search_config)
68 .now_or_never()
69 .expect("check_route yielded unexpectedly. this is a bug in Junction, please file an issue")
70}
71
72pub(crate) trait ConfigCache {
73 async fn get_route<S: AsRef<str>>(&self, authority: S) -> Option<Arc<Route>>;
74 async fn get_backend(&self, target: &BackendId) -> Option<Arc<BackendLb>>;
75 async fn get_endpoints(&self, backend: &BackendId) -> Option<Arc<EndpointGroup>>;
76}
77
78#[derive(Clone, Debug, Default)]
79pub(crate) struct StaticConfig {
80 pub routes: Vec<Arc<Route>>,
81 pub backends: HashMap<BackendId, Arc<BackendLb>>,
82}
83
84impl StaticConfig {
85 pub(crate) fn new(routes: Vec<Route>, backends: Vec<Backend>) -> Self {
86 let routes = routes.into_iter().map(Arc::new).collect();
87
88 let backends: HashMap<_, _> = backends
89 .into_iter()
90 .map(|config| {
91 let load_balancer = LoadBalancer::from_config(&config.lb);
92 let backend_id = config.id.clone();
93 let backend_lb = Arc::new(BackendLb {
94 config,
95 load_balancer,
96 });
97 (backend_id, backend_lb)
98 })
99 .collect();
100
101 Self { routes, backends }
102 }
103
104 pub(crate) fn with_inferred(routes: Vec<Route>, backends: Vec<Backend>) -> Self {
105 let mut routes: Vec<_> = routes.into_iter().map(Arc::new).collect();
106 let mut backends: HashMap<_, _> = backends
107 .into_iter()
108 .map(|config| {
109 let load_balancer = LoadBalancer::from_config(&config.lb);
110 let backend_id = config.id.clone();
111 let backend_lb = Arc::new(BackendLb {
112 config,
113 load_balancer,
114 });
115 (backend_id, backend_lb)
116 })
117 .collect();
118
119 let mut inferred_backends = vec![];
122 for route in &routes {
123 for rule in &route.rules {
124 for backend_ref in &rule.backends {
125 let Some(backend_id) = backend_ref.as_backend_id() else {
126 continue;
127 };
128
129 if backends.contains_key(&backend_id) {
130 continue;
131 }
132
133 let config = Backend {
134 id: backend_id.clone(),
135 lb: LbPolicy::default(),
136 };
137 let load_balancer = LoadBalancer::from_config(&config.lb);
138
139 inferred_backends.push((
140 backend_id,
141 Arc::new(BackendLb {
142 config,
143 load_balancer,
144 }),
145 ))
146 }
147 }
148 }
149
150 let mut inferred_routes = vec![];
154 let mut route_refs = HashSet::new();
155 for route in &routes {
156 for rule in &route.rules {
157 for backend_ref in &rule.backends {
158 route_refs.insert(backend_ref.service.clone());
159 }
160 }
161 }
162 for backend in backends.values() {
163 if !route_refs.contains(&backend.config.id.service) {
164 let route = Route::passthrough_route(
165 Name::from_static("inferred"),
166 backend.config.id.service.clone(),
167 );
168 inferred_routes.push(Arc::new(route));
169 route_refs.insert(backend.config.id.service.clone());
170 }
171 }
172
173 routes.extend(inferred_routes);
174 backends.extend(inferred_backends);
175
176 Self { routes, backends }
177 }
178}
179
180impl ConfigCache for StaticConfig {
181 async fn get_route<S: AsRef<str>>(&self, authority: S) -> Option<Arc<Route>> {
182 let (host, port) = authority.as_ref().split_once(":")?;
183 let port = port.parse().ok()?;
184
185 self.routes
186 .iter()
187 .find(|r| route_matches(r, host, port))
188 .map(Arc::clone)
189 }
190
191 fn get_backend(&self, target: &BackendId) -> impl Future<Output = Option<Arc<BackendLb>>> {
192 std::future::ready(self.backends.get(target).cloned())
193 }
194
195 fn get_endpoints(&self, _: &BackendId) -> impl Future<Output = Option<Arc<EndpointGroup>>> {
196 std::future::ready(None)
197 }
198}
199
200fn route_matches(route: &Route, host: &str, port: u16) -> bool {
201 if !route.hostnames.iter().any(|h| h.matches_str(host)) {
202 return false;
203 }
204
205 if !(route.ports.is_empty() || route.ports.contains(&port)) {
206 return false;
207 }
208
209 true
210}
211
212#[cfg(test)]
213mod test {
214 use super::*;
215 use junction_api::{
216 http::{BackendRef, RouteRule},
217 Hostname, Service,
218 };
219 use std::str::FromStr;
220
221 #[test]
222 fn test_check_routes_resolves_ndots_no_match() {
223 let backend = Service::kube("web", "svc1").unwrap();
224
225 let wont_match = ["http://not.example.com", "http://notexample.com"];
226
227 let route = Route {
228 id: Name::from_static("ndots-match"),
229 hostnames: vec![Hostname::from_static("example.foo.bar.com").into()],
230 ports: vec![],
231 tags: Default::default(),
232 rules: vec![RouteRule {
233 matches: vec![],
234 backends: vec![BackendRef {
235 weight: 1,
236 service: backend.clone(),
237 port: Some(8910),
238 }],
239 ..Default::default()
240 }],
241 };
242
243 let routes = vec![route];
244
245 for url in wont_match {
246 let url = crate::Url::from_str(url).unwrap();
247 let headers = &http::HeaderMap::default();
248
249 let resolved_route = check_route(
250 routes.clone(),
251 &http::Method::GET,
252 &url,
253 headers,
254 Some(&SearchConfig::new(3, vec![])),
255 );
256
257 match resolved_route {
258 Ok(_) => panic!("succeeded for {} should have failed.", url.authority()),
259 Err(e) => assert_eq!(
260 format!("{}", e),
261 format!("no route matched: '{}'", url.authority())
262 ),
263 }
264 }
265 }
266
267 #[test]
268 fn test_check_routes_resolves_ndots_match_without_search() {
269 let backend = Service::kube("web", "svc1").unwrap();
270
271 let route = Route {
272 id: Name::from_static("ndots-match"),
273 hostnames: vec![
274 Hostname::from_static("example.com").into(),
275 Hostname::from_static("example.foo.com").into(),
276 Hostname::from_static("example.foo.bar.com").into(),
277 ],
278 ports: vec![],
279 tags: Default::default(),
280 rules: vec![RouteRule {
281 matches: vec![],
282 backends: vec![BackendRef {
283 weight: 1,
284 service: backend.clone(),
285 port: Some(8910),
286 }],
287 ..Default::default()
288 }],
289 };
290
291 let routes = vec![route];
292
293 let will_match = [
294 "http://example.com",
295 "http://example.foo.com",
296 "http://example.foo.bar.com",
297 ];
298
299 for url in will_match {
300 let url = crate::Url::from_str(url).unwrap();
301 let headers = &http::HeaderMap::default();
302
303 let resolved = check_route(
304 routes.clone(),
305 &http::Method::GET,
306 &url,
307 headers,
308 Some(&SearchConfig::new(3, vec![])),
309 )
310 .unwrap();
311 assert_eq!(
313 (resolved.rule, &resolved.backend),
314 (0, &backend.as_backend_id(8910)),
315 "should match the first rule: {url}"
316 );
317 }
318 }
319
320 #[test]
321 fn test_check_routes_resolves_ndots() {
322 let backend = Service::kube("web", "svc1").unwrap();
323
324 let route = Route {
325 id: Name::from_static("ndots-match"),
326 hostnames: vec![Hostname::from_static("example.foo.bar.com").into()],
327 ports: vec![],
328 tags: Default::default(),
329 rules: vec![RouteRule {
330 matches: vec![],
331 backends: vec![BackendRef {
332 weight: 1,
333 service: backend.clone(),
334 port: Some(8910),
335 }],
336 ..Default::default()
337 }],
338 };
339
340 let routes = vec![route];
341
342 let will_match = [
343 "http://example",
344 "http://example.foo",
345 "http://example.foo.bar",
346 "http://example.foo.bar.com",
347 ];
348 let will_match_hostnames = vec![
349 Hostname::from_static("foo.bar.com"),
350 Hostname::from_static("bar.com"),
351 Hostname::from_static("com"),
352 ];
353
354 for url in will_match {
355 let url = crate::Url::from_str(url).unwrap();
356 let headers = &http::HeaderMap::default();
357
358 let resolved = check_route(
359 routes.clone(),
360 &http::Method::GET,
361 &url,
362 headers,
363 Some(&SearchConfig::new(3, will_match_hostnames.clone())),
364 )
365 .unwrap();
366 assert_eq!(
368 (resolved.rule, &resolved.backend),
369 (0, &backend.as_backend_id(8910)),
370 "should match the first rule: {url}"
371 );
372 }
373 }
374}