junction_api/
lib.rs

1//! The core implementation for Junction - an xDS dynamically-configurable API load-balancer library.
2//!
3//! * [Getting Started](https://docs.junctionlabs.io/getting-started/rust)
4//!
5//! This crate allows you to build configuration
6//! for a dynamic HTTP client and export it to a control plane or pass it
7//! directly to an in-process client. Junction configuration is expressible as
8//! plain Rust structs, and can be serialized/deserialized with a [serde]
9//! compatible library.
10//!
11//! # Core Concepts
12//!
13//! ## Service
14//!
15//! The Junction API is built around the idea that you're always routing
16//! requests to a [Service], which is an abstract representation of a place you
17//! might want traffic to go. A [Service] can be anything, but to use one in
18//! Junction you need a way to uniquely specify it. That could be anything from
19//! a DNS name someone else has already set up to a Kubernetes Service in a
20//! cluster you've connected to Junction.
21//!
22//! ## Routes
23//!
24//! An HTTP [Route][crate::http::Route] is the client facing half of Junction,
25//! and contains most of the things you'd traditionally find in a hand-rolled
26//! HTTP client - timeouts, retries, URL rewriting and more. Routes match
27//! outgoing requests based on their method, URL, and headers. The [http]
28//! module's documentation goes into detail on how and why to configure a Route.
29//!
30//! ## Backends
31//!
32//! A [Backend][crate::backend::Backend] is a single port on a Service. Backends
33//! configuration gives you control over the things you'd normally configure in
34//! a reverse proxy or a traditional load balancer. See the [backend] module's
35//! documentation for more detail.
36//!
37//! # Crate Feature Flags
38//!
39//! The following feature flags are available:
40//!
41//! * The `kube` feature includes conversions from Junction configuration to and
42//!   from Kubernetes objects. This feature depends on the `kube` and
43//!   `k8s-openapi` crates. See the [kube] module docs for more detail.
44//!
45//! * The `xds` feature includes conversions from Junction configuration to and
46//!   from xDS types. This feature depends on the [xds-api][xds_api] crate.
47
48mod error;
49use std::{ops::Deref, str::FromStr};
50
51use backend::BackendId;
52pub use error::Error;
53
54pub mod backend;
55pub mod http;
56
57mod shared;
58pub use shared::{Duration, Fraction, Regex};
59
60#[cfg(feature = "xds")]
61mod xds;
62
63#[cfg(any(feature = "kube_v1_29", feature = "kube_v1_30", feature = "kube_v1_31"))]
64pub mod kube;
65
66#[cfg(feature = "typeinfo")]
67use junction_typeinfo::TypeInfo;
68
69use serde::{de::Visitor, Deserialize, Serialize};
70
71#[cfg(feature = "xds")]
72macro_rules! value_or_default {
73    ($value:expr, $default:expr) => {
74        $value.as_ref().map(|v| v.value).unwrap_or($default)
75    };
76}
77
78use smol_str::ToSmolStr;
79#[cfg(feature = "xds")]
80pub(crate) use value_or_default;
81
82// TODO: replace String with SmolStr
83
84macro_rules! newtype_string {
85    ($(#[$id_attr:meta])* pub $name:ident) => {
86        $(#[$id_attr])*
87        #[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
88        pub struct $name(smol_str::SmolStr);
89
90        impl Deref for $name {
91            type Target = str;
92
93            fn deref(&self) -> &Self::Target {
94                &self.0
95            }
96        }
97
98        impl AsRef<str> for $name {
99            fn as_ref(&self) -> &str {
100                &self.0
101            }
102        }
103
104        impl std::fmt::Display for $name {
105            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106                f.write_str(&self.0)
107            }
108        }
109
110        impl std::fmt::Debug for $name {
111            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112                write!(f, "{:?}", self.0)
113            }
114        }
115
116        impl Serialize for $name {
117            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
118            where
119                S: serde::Serializer,
120            {
121                serializer.serialize_str(&self.0)
122            }
123        }
124
125        impl<'de> Deserialize<'de> for $name {
126            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
127            where
128                D: serde::Deserializer<'de>,
129            {
130                // implements both visit_str and visit_string in case that moving the
131                // string into this $name instead of cloning is possible.
132                struct NameVisitor;
133
134                impl<'de> Visitor<'de> for NameVisitor {
135                    type Value = $name;
136
137                    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
138                        formatter.write_str("a valid DNS $name")
139                    }
140
141                    fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
142                    where
143                        E: serde::de::Error,
144                    {
145                        let name: Result<$name, Error> = v.try_into();
146                        name.map_err(|e: Error| E::custom(e.to_string()))
147                    }
148
149                    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
150                    where
151                        E: serde::de::Error,
152                    {
153                        $name::from_str(v).map_err(|e| E::custom(e.to_string()))
154                    }
155                }
156
157                deserializer.deserialize_string(NameVisitor)
158            }
159        }
160
161        #[cfg(feature = "typeinfo")]
162        impl junction_typeinfo::TypeInfo for $name {
163            fn kind() -> junction_typeinfo::Kind {
164                junction_typeinfo::Kind::String
165            }
166        }
167
168        impl FromStr for $name {
169            type Err = Error;
170
171            fn from_str(s: &str) -> Result<Self, Self::Err> {
172                Self::validate(s.as_bytes())?;
173                Ok($name(smol_str::SmolStr::new(s)))
174            }
175        }
176
177        impl TryFrom<String> for $name {
178            type Error = Error;
179
180            fn try_from(value: String) -> Result<$name, Self::Error> {
181                $name::validate(value.as_bytes())?;
182                Ok($name(smol_str::SmolStr::new(value)))
183            }
184        }
185
186        impl TryFrom<&[u8]> for $name {
187            type Error = Error;
188
189            fn try_from(bs: &[u8]) -> Result<$name, Self::Error> {
190                $name::validate(bs)?;
191                // safety: validate checks that the string is valid ascii
192                let value = unsafe { std::str::from_utf8_unchecked(bs) };
193                Ok($name(smol_str::SmolStr::new(value)))
194            }
195        }
196
197        impl<'a> TryFrom<&'a str> for $name {
198            type Error = Error;
199
200            fn try_from(value: &'a str) -> Result<$name, Self::Error> {
201                $name::validate(value.as_bytes())?;
202                Ok($name(smol_str::SmolStr::new(value)))
203            }
204        }
205    }
206}
207
208// A lookup table of valid RFC 1123 name characters. RFC 1035 labels use
209// this table as well but ignore the '.' character.
210//
211// Adapted from the table used in the http crate to valid URI and Authority
212// strings.
213//
214// https://github.com/hyperium/http/blob/master/src/uri/mod.rs#L146-L153
215#[rustfmt::skip]
216const DNS_CHARS: [u8; 256] = [
217//  0      1      2      3      4      5      6      7      8      9
218    0,     0,     0,     0,     0,     0,     0,     0,     0,     0, //   x
219    0,     0,     0,     0,     0,     0,     0,     0,     0,     0, //  1x
220    0,     0,     0,     0,     0,     0,     0,     0,     0,     0, //  2x
221    0,     0,     0,     0,     0,     0,     0,     0,     0,     0, //  3x
222    0,     0,     0,     0,     0,  b'-',  b'.',     0,  b'0',  b'1', //  4x
223 b'2',  b'3',  b'4',  b'5',  b'6',  b'7',  b'8',  b'9',     0,     0, //  5x
224    0,     0,     0,     0,     0,     0,     0,     0,     0,     0, //  6x
225    0,     0,     0,     0,     0,     0,     0,     0,     0,     0, //  7x
226    0,     0,     0,     0,     0,     0,     0,     0,     0,     0, //  8x
227    0,     0,     0,     0,     0,     0,     0,  b'a',  b'b',  b'c', //  9x
228 b'd',  b'e',  b'f',  b'g',  b'h',  b'i',  b'j',  b'k',  b'l',  b'm', // 10x
229 b'n',  b'o',  b'p',  b'q',  b'r',  b's',  b't',  b'u',  b'v',  b'w', // 11x
230 b'x',  b'y',  b'z',     0,     0,     0,     0,     0,     0,     0, // 12x
231    0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 13x
232    0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 14x
233    0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 15x
234    0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 16x
235    0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 17x
236    0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 18x
237    0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 19x
238    0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 20x
239    0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 21x
240    0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 22x
241    0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 23x
242    0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 24x
243    0,     0,     0,     0,     0,     0,                              // 25x
244];
245
246newtype_string! {
247    /// AN RFC 1123 DNS domain name.
248    ///
249    /// This name must be no more than 253 characters, and can only contain
250    /// lowercase ascii alphanumeric characters, `.` and `-`.
251    pub Hostname
252}
253
254impl Hostname {
255    /// The RFC 1035 max length. We don't add any extra validation to it, since
256    /// there's a variety of places Name gets used and they won't all have the
257    /// same constraints.
258    const MAX_LEN: usize = 253;
259
260    /// Create a new hostname from a static string.
261    ///
262    /// Assumes that a human being has manually validated that this is a valid
263    /// hostname and will panic if it is not.
264    pub fn from_static(src: &'static str) -> Self {
265        Self::validate(src.as_bytes()).expect("expected a static Name to be valid");
266        Self(smol_str::SmolStr::new_static(src))
267    }
268
269    fn validate(bs: &[u8]) -> Result<(), Error> {
270        if bs.is_empty() {
271            return Err(Error::new_static("Hostname must not be empty"));
272        }
273
274        if bs.len() > Self::MAX_LEN {
275            return Err(Error::new_static(
276                "Hostname must not be longer than 253 characters",
277            ));
278        }
279
280        for (i, &b) in bs.iter().enumerate() {
281            match (i, DNS_CHARS[b as usize]) {
282                (_, 0) => return Err(Error::new_static("Hostname contains an invalid character")),
283                (0, b'-' | b'.') => {
284                    return Err(Error::new_static(
285                        "Hostname must start with an alphanumeric character",
286                    ))
287                }
288                (i, b'-' | b'.') if i == bs.len() => {
289                    return Err(Error::new_static(
290                        "Hostname must end with an alphanumeric character",
291                    ))
292                }
293                _ => (),
294            }
295        }
296
297        Ok(())
298    }
299}
300
301newtype_string! {
302    /// An RFC 1035 compatible name. This name must be useable as a component of a
303    /// DNS subdomain - it must start with a lowercase ascii alphabetic character
304    /// and may only consist of ascii lowercase alphanumeric characters and the `-`
305    /// character.
306    pub Name
307}
308
309impl Name {
310    // RFC 1035 max length.
311    const MAX_LEN: usize = 63;
312
313    /// Create a new name from a static string.
314    ///
315    /// Assumes that a human being has manually validated that this is a valid name
316    /// and will panic if it is not.
317    pub fn from_static(src: &'static str) -> Self {
318        Self::validate(src.as_bytes()).expect("expected a static Name to be valid");
319        Self(smol_str::SmolStr::new_static(src))
320    }
321
322    /// Check that a `str` is a valid Name.
323    ///
324    /// Being a valid name also implies that the slice is valid utf-8.
325    fn validate(bs: &[u8]) -> Result<(), Error> {
326        if bs.is_empty() {
327            return Err(Error::new_static("Name must not be empty"));
328        }
329
330        if bs.len() > Self::MAX_LEN {
331            return Err(Error::new_static(
332                "Name must not be longer than 63 characters",
333            ));
334        }
335
336        for (i, &b) in bs.iter().enumerate() {
337            match (i, DNS_CHARS[b as usize]) {
338                (_, 0 | b'.') => {
339                    return Err(Error::new_static("Name contains an invalid character"))
340                }
341                (0, b'-' | b'0'..=b'9') => {
342                    return Err(Error::new_static("Name must start with [a-z]"))
343                }
344                (i, b'-') if i == bs.len() => {
345                    return Err(Error::new_static(
346                        "Name must end with an alphanumeric character",
347                    ))
348                }
349                _ => (),
350            }
351        }
352
353        Ok(())
354    }
355}
356
357/// A uniquely identifiable service that traffic can be routed to.
358///
359/// Services are abstract, and can have different semantics depending on where
360/// and how they're defined.
361#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
362#[serde(tag = "type")]
363#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
364pub enum Service {
365    /// A DNS hostname.
366    ///
367    /// See [DnsService] for details.
368    #[serde(rename = "dns", alias = "DNS")]
369    Dns(DnsService),
370
371    /// A Kubernetes Service.
372    ///
373    /// See [KubeService] for details.
374    #[serde(rename = "kube", alias = "k8s")]
375    Kube(KubeService),
376}
377
378impl std::fmt::Display for Service {
379    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
380        self.write_name(f)
381    }
382}
383
384impl std::str::FromStr for Service {
385    type Err = Error;
386
387    fn from_str(name: &str) -> Result<Self, Self::Err> {
388        let hostname = Hostname::from_str(name)?;
389
390        let target = match name {
391            n if n.ends_with(KubeService::SUBDOMAIN) => {
392                let parts: Vec<_> = hostname.split('.').collect();
393                if parts.len() != 5 {
394                    return Err(Error::new_static(
395                        "invalid Service target: name and namespace must be valid DNS labels",
396                    ));
397                }
398
399                let name = parts[0].parse()?;
400                let namespace = parts[1].parse()?;
401                Service::Kube(KubeService { name, namespace })
402            }
403            _ => Service::Dns(DnsService { hostname }),
404        };
405        Ok(target)
406    }
407}
408
409impl Service {
410    /// Create a new DNS target. The given name must be a valid RFC 1123 DNS
411    /// hostname.
412    pub fn dns(name: &str) -> Result<Self, Error> {
413        let hostname = Hostname::from_str(name)?;
414        Ok(Self::Dns(DnsService { hostname }))
415    }
416
417    /// Create a new Kubernetes Service target.
418    ///
419    /// `name` and `hostname` must be valid DNS subdomain labels.
420    pub fn kube(namespace: &str, name: &str) -> Result<Self, Error> {
421        let namespace = Name::from_str(namespace)?;
422        let name = Name::from_str(name)?;
423
424        Ok(Self::Kube(KubeService { name, namespace }))
425    }
426
427    /// Clone and convert this backend into a [BackendId] with the specified port.
428    pub fn as_backend_id(&self, port: u16) -> BackendId {
429        BackendId {
430            service: self.clone(),
431            port,
432        }
433    }
434
435    /// The canonical hostname for this backend.
436    pub fn hostname(&self) -> Hostname {
437        match self {
438            Service::Dns(dns) => dns.hostname.clone(),
439            _ => {
440                // safety: a name should never be an invalid hostname
441                let name = self.name().to_smolstr();
442                Hostname(name)
443            }
444        }
445    }
446
447    /// The canonical name of this backend.
448    ///
449    /// Returns the same name as [hostname][Self::hostname] but as a raw
450    /// [String] instead of a [Hostname].
451    pub fn name(&self) -> String {
452        let mut buf = String::new();
453        self.write_name(&mut buf).unwrap();
454        buf
455    }
456
457    fn write_name(&self, w: &mut impl std::fmt::Write) -> std::fmt::Result {
458        match self {
459            Service::Dns(dns) => {
460                w.write_str(&dns.hostname)?;
461            }
462            Service::Kube(svc) => {
463                write!(
464                    w,
465                    "{name}.{namespace}{subdomain}",
466                    name = svc.name,
467                    namespace = svc.namespace,
468                    subdomain = KubeService::SUBDOMAIN,
469                )?;
470            }
471        }
472
473        Ok(())
474    }
475
476    const BACKEND_SUBDOMAIN: &'static str = ".lb.jct";
477
478    #[doc(hidden)]
479    pub fn lb_config_route_name(&self) -> String {
480        let mut buf = String::new();
481        self.write_lb_config_route_name(&mut buf).unwrap();
482        buf
483    }
484
485    fn write_lb_config_route_name(&self, w: &mut impl std::fmt::Write) -> std::fmt::Result {
486        match self {
487            Service::Dns(dns) => {
488                write!(w, "{}{}", dns.hostname, Service::BACKEND_SUBDOMAIN)?;
489            }
490            Service::Kube(svc) => {
491                write!(
492                    w,
493                    "{name}.{namespace}{svc}{backend}",
494                    name = svc.name,
495                    namespace = svc.namespace,
496                    svc = KubeService::SUBDOMAIN,
497                    backend = Service::BACKEND_SUBDOMAIN,
498                )?;
499            }
500        }
501
502        Ok(())
503    }
504
505    #[doc(hidden)]
506    pub fn from_lb_config_route_name(name: &str) -> Result<Self, Error> {
507        let hostname = Hostname::from_str(name)?;
508
509        let Some(hostname) = hostname.strip_suffix(Service::BACKEND_SUBDOMAIN) else {
510            return Err(Error::new_static("expected a Junction backend name"));
511        };
512
513        Self::from_str(hostname)
514    }
515}
516
517/// A Kubernetes Service to target with traffic.
518///
519/// This Service doesn't have to exist in any particular Kubernetes cluster -
520/// Junction assumes that all connected Kubernetes clusters have adopted
521/// [namespace-sameness] instead of asking you to distinguish between individual
522/// clusters as they come and go.
523///
524/// [namespace-sameness]: https://github.com/kubernetes/community/blob/master/sig-multicluster/namespace-sameness-position-statement.md
525#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
526#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
527pub struct KubeService {
528    /// The name of the Kubernetes Service to target.
529    pub name: Name,
530
531    /// The namespace of the Kubernetes service to target. This must be explicitly
532    /// specified, and won't be inferred from context.
533    pub namespace: Name,
534}
535
536impl KubeService {
537    const SUBDOMAIN: &'static str = ".svc.cluster.local";
538}
539
540/// A DNS name to target with traffic.
541///
542/// While the whole point of Junction is to avoid DNS in the first place,
543/// sometimes you have to route traffic to an external service or something that
544/// can't (or shouldn't) be connected directly to this instance of Junction. In
545/// those cases, it's useful to treat a DNS name like a slowly changing pool of
546/// addresses and to route traffic to them appropriately.
547#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
548#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
549pub struct DnsService {
550    /// A valid RFC1123 DNS domain name.
551    pub hostname: Hostname,
552}
553
554#[inline]
555pub(crate) fn parse_port(s: &str) -> Result<(&str, Option<u16>), Error> {
556    let (name, port) = match s.split_once(':') {
557        Some((name, port)) => (name, Some(port)),
558        None => (s, None),
559    };
560
561    let port = match port {
562        Some(port) => {
563            Some(u16::from_str(port).map_err(|_| Error::new_static("invalid port number"))?)
564        }
565        None => None,
566    };
567
568    Ok((name, port))
569}
570
571#[cfg(test)]
572mod test {
573    use super::*;
574
575    #[test]
576    fn test_service_serde() {
577        let target = serde_json::json!({
578            "name": "foo",
579            "namespace": "potato",
580            "type": "kube",
581        });
582
583        assert_eq!(
584            serde_json::from_value::<Service>(target).unwrap(),
585            Service::kube("potato", "foo").unwrap(),
586        );
587
588        let target = serde_json::json!({
589            "type": "dns",
590            "hostname": "example.com"
591        });
592
593        assert_eq!(
594            serde_json::from_value::<Service>(target).unwrap(),
595            Service::dns("example.com").unwrap(),
596        );
597    }
598
599    #[test]
600    fn test_service_name() {
601        assert_name(
602            Service::kube("production", "potato").unwrap(),
603            "potato.production.svc.cluster.local",
604        );
605
606        assert_name(
607            Service::dns("cool-stuff.example.com").unwrap(),
608            "cool-stuff.example.com",
609        );
610    }
611
612    #[track_caller]
613    fn assert_name(target: Service, str: &'static str) {
614        assert_eq!(&target.name(), str);
615        let parsed = Service::from_str(str).unwrap();
616        assert_eq!(parsed, target);
617    }
618
619    #[test]
620    fn test_target_lb_config_name() {
621        assert_lb_config_name(
622            Service::kube("production", "potato").unwrap(),
623            "potato.production.svc.cluster.local.lb.jct",
624        );
625
626        assert_lb_config_name(
627            Service::dns("cool-stuff.example.com").unwrap(),
628            "cool-stuff.example.com.lb.jct",
629        );
630    }
631
632    #[track_caller]
633    fn assert_lb_config_name(target: Service, str: &'static str) {
634        assert_eq!(&target.lb_config_route_name(), str);
635        let parsed = Service::from_lb_config_route_name(str).unwrap();
636        assert_eq!(parsed, target);
637    }
638
639    #[test]
640    fn test_valid_name() {
641        let alphabetic: Vec<char> = (b'a'..=b'z').map(|b| b as char).collect();
642        let full_alphabet: Vec<char> = (b'a'..=b'z')
643            .chain(b'0'..=b'9')
644            .chain([b'-'])
645            .map(|b| b as char)
646            .collect();
647
648        arbtest::arbtest(|u| {
649            let input = arbitrary_string(u, &alphabetic, &full_alphabet, Name::MAX_LEN);
650            let res = Name::from_str(&input);
651
652            assert!(
653                res.is_ok(),
654                "string should be a valid name: {input:?} (len={}): {}",
655                input.len(),
656                res.unwrap_err(),
657            );
658            Ok(())
659        });
660    }
661
662    #[test]
663    fn test_valid_hostname() {
664        let alphanumeric: Vec<char> = (b'a'..=b'z')
665            .chain(b'0'..=b'9')
666            .map(|b| b as char)
667            .collect();
668        let full_alphabet: Vec<char> = (b'a'..=b'z')
669            .chain(b'0'..=b'9')
670            .chain([b'-', b'.'])
671            .map(|b| b as char)
672            .collect();
673
674        arbtest::arbtest(|u| {
675            let input = arbitrary_string(u, &alphanumeric, &full_alphabet, Hostname::MAX_LEN);
676            let res = Hostname::from_str(&input);
677            assert!(
678                res.is_ok(),
679                "string should be a valid hostname: {input:?} (len={}): {}",
680                input.len(),
681                res.unwrap_err(),
682            );
683            Ok(())
684        });
685    }
686
687    fn arbitrary_string(
688        u: &mut arbitrary::Unstructured,
689        first_and_last: &[char],
690        alphabet: &[char],
691        max_len: usize,
692    ) -> String {
693        let len: usize = u.choose_index(max_len - 1).unwrap() + 1;
694        let mut input = String::new();
695
696        if len > 0 {
697            input.push(*u.choose(first_and_last).unwrap());
698        }
699
700        if len > 1 {
701            for _ in 1..(len - 1) {
702                input.push(*u.choose(alphabet).unwrap());
703            }
704        }
705
706        if len > 2 {
707            input.push(*u.choose(first_and_last).unwrap());
708        }
709
710        input
711    }
712}