refactor configuration to support redirects

This commit is contained in:
Quentin 2023-11-29 12:49:55 +01:00
parent 14325395f6
commit de72d6037f
Signed by untrusted user: quentin
GPG key ID: E9602264D639FF68
4 changed files with 238 additions and 89 deletions

View file

@ -78,7 +78,7 @@ impl CertStore {
let proxy_config: Arc<ProxyConfig> = rx_proxy_config.borrow().clone(); let proxy_config: Arc<ProxyConfig> = rx_proxy_config.borrow().clone();
for ent in proxy_config.entries.iter() { for ent in proxy_config.entries.iter() {
if let HostDescription::Hostname(domain) = &ent.host { if let HostDescription::Hostname(domain) = &ent.url_prefix.host {
if let Some((host, _port)) = domain.split_once(':') { if let Some((host, _port)) = domain.split_once(':') {
domains.insert(host.to_string()); domains.insert(host.to_string());
} else { } else {
@ -121,7 +121,7 @@ impl CertStore {
.borrow() .borrow()
.entries .entries
.iter() .iter()
.any(|ent| ent.host.matches(domain)) .any(|ent| ent.url_prefix.host.matches(domain))
{ {
bail!("Domain {} should not have a TLS certificate.", domain); bail!("Domain {} should not have a TLS certificate.", domain);
} }

View file

@ -234,8 +234,9 @@ async fn select_target_and_proxy(
.iter() .iter()
.filter(|ent| { .filter(|ent| {
ent.flags.healthy ent.flags.healthy
&& ent.host.matches(host) && ent.url_prefix.host.matches(host)
&& ent && ent
.url_prefix
.path_prefix .path_prefix
.as_ref() .as_ref()
.map(|prefix| path.starts_with(prefix)) .map(|prefix| path.starts_with(prefix))
@ -244,7 +245,8 @@ async fn select_target_and_proxy(
.max_by_key(|ent| { .max_by_key(|ent| {
( (
ent.priority, ent.priority,
ent.path_prefix ent.url_prefix
.path_prefix
.as_ref() .as_ref()
.map(|x| x.len() as i32) .map(|x| x.len() as i32)
.unwrap_or(0), .unwrap_or(0),

View file

@ -239,7 +239,7 @@ async fn dump_config_on_change(
let mut cfg_map = BTreeMap::<_, Vec<_>>::new(); let mut cfg_map = BTreeMap::<_, Vec<_>>::new();
for ent in cfg.entries.iter() { for ent in cfg.entries.iter() {
cfg_map cfg_map
.entry((&ent.host, &ent.path_prefix)) .entry((&ent.url_prefix.host, &ent.url_prefix.path_prefix))
.or_default() .or_default()
.push(ent); .push(ent);
} }

View file

@ -13,7 +13,7 @@ use crate::consul;
// ---- Extract proxy config from Consul catalog ---- // ---- Extract proxy config from Consul catalog ----
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum HostDescription { pub enum HostDescription {
Hostname(String), Hostname(String),
Pattern(glob::Pattern), Pattern(glob::Pattern),
@ -45,12 +45,48 @@ impl std::fmt::Display for HostDescription {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct ProxyEntry { pub struct UrlPrefix {
/// Publicly exposed TLS hostnames for matching this rule /// Publicly exposed TLS hostnames for matching this rule
pub host: HostDescription, pub host: HostDescription,
/// Path prefix for matching this rule /// Path prefix for matching this rule
pub path_prefix: Option<String>, pub path_prefix: Option<String>,
}
impl PartialEq for UrlPrefix {
fn eq(&self, other: &Self) -> bool {
self.host == other.host && self.path_prefix == other.path_prefix
}
}
impl Eq for UrlPrefix {}
impl UrlPrefix {
fn new(raw_prefix: &str) -> Option<Self> {
let (raw_host, path_prefix) = match raw_prefix.find('/') {
Some(i) => {
let (host, pp) = raw_prefix.split_at(i);
(host, Some(pp.to_string()))
}
None => (raw_prefix, None),
};
let host = match HostDescription::new(raw_host) {
Ok(h) => h,
Err(e) => {
warn!("Invalid hostname pattern {}: {}", raw_host, e);
return None;
}
};
Some(Self { host, path_prefix })
}
}
#[derive(Debug)]
pub struct ProxyEntry {
/// An Url prefix is made of a host and maybe a path prefix
pub url_prefix: UrlPrefix,
/// Priority with which this rule is considered (highest first) /// Priority with which this rule is considered (highest first)
pub priority: u32, pub priority: u32,
@ -68,6 +104,10 @@ pub struct ProxyEntry {
/// when matching this rule /// when matching this rule
pub add_headers: Vec<(String, String)>, pub add_headers: Vec<(String, String)>,
/// Try to match all these redirection before forwarding to the backend
/// when matching this rule
pub redirects: Vec<(UrlPrefix, UrlPrefix, u32)>,
/// Number of calls in progress, used to deprioritize slow back-ends /// Number of calls in progress, used to deprioritize slow back-ends
pub calls_in_progress: atomic::AtomicI64, pub calls_in_progress: atomic::AtomicI64,
/// Time of last call, used for round-robin selection /// Time of last call, used for round-robin selection
@ -76,8 +116,7 @@ pub struct ProxyEntry {
impl PartialEq for ProxyEntry { impl PartialEq for ProxyEntry {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.host == other.host self.url_prefix == other.url_prefix
&& self.path_prefix == other.path_prefix
&& self.priority == other.priority && self.priority == other.priority
&& self.service_name == other.service_name && self.service_name == other.service_name
&& self.target_addr == other.target_addr && self.target_addr == other.target_addr
@ -88,6 +127,58 @@ impl PartialEq for ProxyEntry {
} }
impl Eq for ProxyEntry {} impl Eq for ProxyEntry {}
impl ProxyEntry {
fn new(
service_name: String,
frontend: MatchTag,
target_addr: SocketAddr,
middleware: &[ConfigTag],
flags: ProxyEntryFlags,
) -> Self {
let (url_prefix, priority, https_target) = match frontend {
MatchTag::Http(u, p) => (u, p, false),
MatchTag::HttpWithTls(u, p) => (u, p, true),
};
let mut add_headers = vec![];
let mut redirects = vec![];
for mid in middleware.into_iter() {
match mid {
ConfigTag::AddHeader(k, v) => add_headers.push((k.to_string(), v.to_string())),
ConfigTag::AddRedirect(m, r, c) => redirects.push(((*m).clone(), (*r).clone(), *c)),
ConfigTag::LocalLb =>
/* handled in parent fx */
{
()
}
ConfigTag::GlobalLb =>
/* handled in parent fx */
{
()
}
};
}
ProxyEntry {
// id
service_name,
// frontend
url_prefix,
priority,
// backend
target_addr,
https_target,
// middleware
flags,
add_headers,
redirects,
// internal
last_call: atomic::AtomicI64::from(0),
calls_in_progress: atomic::AtomicI64::from(0),
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct ProxyEntryFlags { pub struct ProxyEntryFlags {
/// Is the target healthy? /// Is the target healthy?
@ -115,8 +206,8 @@ impl std::fmt::Display for ProxyEntry {
write!( write!(
f, f,
"{}{} {}", "{}{} {}",
self.host, self.url_prefix.host,
self.path_prefix.as_deref().unwrap_or_default(), self.url_prefix.path_prefix.as_deref().unwrap_or_default(),
self.priority self.priority
)?; )?;
if !self.flags.healthy { if !self.flags.healthy {
@ -144,62 +235,103 @@ pub struct ProxyConfig {
pub entries: Vec<ProxyEntry>, pub entries: Vec<ProxyEntry>,
} }
fn parse_tricot_tag( #[derive(Debug)]
service_name: String, enum ParsedTag<'a> {
tag: &str, Frontend(MatchTag),
target_addr: SocketAddr, Middleware(ConfigTag<'a>),
add_headers: &[(String, String)],
flags: ProxyEntryFlags,
) -> Option<ProxyEntry> {
let splits = tag.split(' ').collect::<Vec<_>>();
if (splits.len() != 2 && splits.len() != 3)
|| (splits[0] != "tricot" && splits[0] != "tricot-https")
{
return None;
} }
let (host, path_prefix) = match splits[1].find('/') { #[derive(Debug)]
Some(i) => { enum MatchTag {
let (host, pp) = splits[1].split_at(i); /// HTTP backend (plain text)
(host, Some(pp.to_string())) Http(UrlPrefix, u32),
/// HTTPS backend (TLS encrypted)
HttpWithTls(UrlPrefix, u32),
} }
None => (splits[1], None),
};
let priority = match splits.len() { #[derive(Debug)]
3 => splits[2].parse().ok()?, enum ConfigTag<'a> {
_ => 100, AddHeader(&'a str, &'a str),
}; AddRedirect(UrlPrefix, UrlPrefix, u32),
GlobalLb,
LocalLb,
}
let host = match HostDescription::new(host) { fn parse_tricot_tags(tag: &str) -> Option<ParsedTag> {
Ok(h) => h, let splits = tag.splitn(4, ' ').collect::<Vec<_>>();
Err(e) => { let parsed_tag = match splits.as_slice() {
warn!("Invalid hostname pattern {}: {}", host, e); ["tricot", raw_prefix, maybe_priority @ ..] => {
// priority is set to 100 when value is invalid or missing
let priority: u32 = maybe_priority
.iter()
.next()
.map_or(Ok(100), |x| x.parse::<u32>())
.unwrap_or(100);
UrlPrefix::new(raw_prefix)
.map(|prefix| ParsedTag::Frontend(MatchTag::Http(prefix, priority)))
}
["tricot-https", raw_prefix, maybe_priority @ ..] => {
// priority is set to 100 when value is invalid or missing
let priority: u32 = maybe_priority
.iter()
.next()
.map_or(Ok(100), |x| x.parse::<u32>())
.unwrap_or(100);
UrlPrefix::new(raw_prefix)
.map(|prefix| ParsedTag::Frontend(MatchTag::HttpWithTls(prefix, priority)))
}
["tricot-add-header", header_key, header_value] => Some(ParsedTag::Middleware(
ConfigTag::AddHeader(header_key, header_value),
)),
["tricot-add-redirect", raw_match, raw_replace, maybe_raw_code @ ..] => {
let (p_match, p_replace) =
match (UrlPrefix::new(raw_match), UrlPrefix::new(raw_replace)) {
(Some(m), Some(r)) => (m, r),
_ => {
debug!(
"tag {} is ignored, one of the url prefix can't be parsed",
tag
);
return None; return None;
} }
}; };
Some(ProxyEntry { if matches!(p_replace.host, HostDescription::Pattern(_)) {
service_name, debug!(
target_addr, "tag {} ignored as redirect to a glob pattern is not supported",
https_target: (splits[0] == "tricot-https"), tag
host, );
flags, return None;
path_prefix,
priority,
add_headers: add_headers.to_vec(),
last_call: atomic::AtomicI64::from(0),
calls_in_progress: atomic::AtomicI64::from(0),
})
} }
fn parse_tricot_add_header_tag(tag: &str) -> Option<(String, String)> { let maybe_parsed_code = maybe_raw_code
let splits = tag.splitn(3, ' ').collect::<Vec<_>>(); .iter()
if splits.len() == 3 && splits[0] == "tricot-add-header" { .next()
Some((splits[1].to_string(), splits[2].to_string())) .map(|c| c.parse::<u32>().ok())
} else { .flatten();
None let http_code = match maybe_parsed_code {
Some(301) => 301,
Some(302) => 302,
_ => {
debug!(
"tag {} has a missing or invalid http code, set it to 302",
tag
);
302
} }
};
Some(ParsedTag::Middleware(ConfigTag::AddRedirect(
p_match, p_replace, http_code,
)))
}
["tricot-global-lb", ..] => Some(ParsedTag::Middleware(ConfigTag::GlobalLb)),
["tricot-local-lb", ..] => Some(ParsedTag::Middleware(ConfigTag::LocalLb)),
_ => None,
};
trace!("tag {} parsed as {:?}", tag, parsed_tag);
parsed_tag
} }
fn parse_consul_service( fn parse_consul_service(
@ -208,8 +340,6 @@ fn parse_consul_service(
) -> Vec<ProxyEntry> { ) -> Vec<ProxyEntry> {
trace!("Parsing service: {:#?}", s); trace!("Parsing service: {:#?}", s);
let mut entries = vec![];
let ip_addr = match s.service.address.parse() { let ip_addr = match s.service.address.parse() {
Ok(ip) => ip, Ok(ip) => ip,
_ => match s.node.address.parse() { _ => match s.node.address.parse() {
@ -225,30 +355,47 @@ fn parse_consul_service(
}; };
let addr = SocketAddr::new(ip_addr, s.service.port); let addr = SocketAddr::new(ip_addr, s.service.port);
if s.service.tags.contains(&"tricot-global-lb".into()) { // tag parsing
flags.global_lb = true; let mut collected_middleware = vec![];
} else if s.service.tags.contains(&"tricot-site-lb".into()) { let mut collected_frontends = vec![];
flags.site_lb = true; for tag in s.service.tags.iter() {
match parse_tricot_tags(tag) {
Some(ParsedTag::Frontend(x)) => collected_frontends.push(x),
Some(ParsedTag::Middleware(y)) => collected_middleware.push(y),
_ => trace!(
"service {}: tag '{}' could not be parsed",
s.service.service,
tag
),
}
}
// some legacy processing that would need a refactor later
for mid in collected_middleware.iter() {
match mid {
ConfigTag::AddHeader(_, _) | ConfigTag::AddRedirect(_, _, _) =>
/* not handled here */
{
()
}
ConfigTag::GlobalLb => flags.global_lb = true,
ConfigTag::LocalLb => flags.site_lb = true,
}; };
let mut add_headers = vec![];
for tag in s.service.tags.iter() {
if let Some(pair) = parse_tricot_add_header_tag(tag) {
add_headers.push(pair);
}
} }
for tag in s.service.tags.iter() { // build proxy entries
if let Some(ent) = parse_tricot_tag( let entries = collected_frontends
.into_iter()
.map(|frt| {
ProxyEntry::new(
s.service.service.clone(), s.service.service.clone(),
tag, frt,
addr, addr,
&add_headers[..], collected_middleware.as_ref(),
flags, flags,
) { )
entries.push(ent); })
} .collect::<Vec<_>>();
}
trace!("Result of parsing service:"); trace!("Result of parsing service:");
for ent in entries.iter() { for ent in entries.iter() {
@ -347,8 +494,8 @@ impl ProxyConfigMetrics {
let mut patterns = HashMap::new(); let mut patterns = HashMap::new();
for ent in rx.borrow().entries.iter() { for ent in rx.borrow().entries.iter() {
let attrs = ( let attrs = (
ent.host.to_string(), ent.url_prefix.host.to_string(),
ent.path_prefix.clone().unwrap_or_default(), ent.url_prefix.path_prefix.clone().unwrap_or_default(),
ent.service_name.clone(), ent.service_name.clone(),
); );
*patterns.entry(attrs).or_default() += 1; *patterns.entry(attrs).or_default() += 1;