1mod 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
82macro_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 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 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#[rustfmt::skip]
216const DNS_CHARS: [u8; 256] = [
2170, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, b'-', b'.', 0, b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', b'x', b'y', b'z', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ];
245
246newtype_string! {
247 pub Hostname
252}
253
254impl Hostname {
255 const MAX_LEN: usize = 253;
259
260 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 pub Name
307}
308
309impl Name {
310 const MAX_LEN: usize = 63;
312
313 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 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#[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 #[serde(rename = "dns", alias = "DNS")]
369 Dns(DnsService),
370
371 #[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 pub fn dns(name: &str) -> Result<Self, Error> {
413 let hostname = Hostname::from_str(name)?;
414 Ok(Self::Dns(DnsService { hostname }))
415 }
416
417 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 pub fn as_backend_id(&self, port: u16) -> BackendId {
429 BackendId {
430 service: self.clone(),
431 port,
432 }
433 }
434
435 pub fn hostname(&self) -> Hostname {
437 match self {
438 Service::Dns(dns) => dns.hostname.clone(),
439 _ => {
440 let name = self.name().to_smolstr();
442 Hostname(name)
443 }
444 }
445 }
446
447 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#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
526#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
527pub struct KubeService {
528 pub name: Name,
530
531 pub namespace: Name,
534}
535
536impl KubeService {
537 const SUBDOMAIN: &'static str = ".svc.cluster.local";
538}
539
540#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
548#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
549pub struct DnsService {
550 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}