2021-12-07 17:50:58 +00:00
|
|
|
use std::collections::HashMap;
|
2021-12-06 22:08:22 +00:00
|
|
|
use std::net::SocketAddr;
|
2021-12-07 15:37:22 +00:00
|
|
|
use std::sync::{atomic, Arc};
|
2023-08-27 13:58:47 +00:00
|
|
|
use std::time::Duration;
|
2021-12-06 22:08:22 +00:00
|
|
|
|
2021-12-07 16:56:15 +00:00
|
|
|
use anyhow::Result;
|
2022-12-05 17:43:48 +00:00
|
|
|
use opentelemetry::{metrics, KeyValue};
|
2021-12-07 16:56:15 +00:00
|
|
|
|
2023-08-27 13:58:47 +00:00
|
|
|
use tokio::{select, sync::watch};
|
2023-08-27 14:13:29 +00:00
|
|
|
use tracing::*;
|
2021-12-06 22:08:22 +00:00
|
|
|
|
2023-06-13 09:40:13 +00:00
|
|
|
use crate::consul;
|
2021-12-06 22:08:22 +00:00
|
|
|
|
|
|
|
// ---- Extract proxy config from Consul catalog ----
|
|
|
|
|
2023-11-29 11:49:55 +00:00
|
|
|
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
2021-12-08 10:11:22 +00:00
|
|
|
pub enum HostDescription {
|
|
|
|
Hostname(String),
|
|
|
|
Pattern(glob::Pattern),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl HostDescription {
|
|
|
|
fn new(desc: &str) -> Result<Self> {
|
|
|
|
if desc.chars().any(|x| matches!(x, '*' | '?' | '[' | ']')) {
|
|
|
|
Ok(Self::Pattern(glob::Pattern::new(desc)?))
|
|
|
|
} else {
|
|
|
|
Ok(Self::Hostname(desc.to_string()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn matches(&self, v: &str) -> bool {
|
|
|
|
match self {
|
|
|
|
Self::Pattern(p) => p.matches(v),
|
|
|
|
Self::Hostname(s) => s == v,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-05 17:43:48 +00:00
|
|
|
impl std::fmt::Display for HostDescription {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
match self {
|
|
|
|
HostDescription::Hostname(h) => write!(f, "{}", h),
|
2023-08-27 13:26:19 +00:00
|
|
|
HostDescription::Pattern(p) => write!(f, "[{}]", p.as_str()),
|
2022-12-05 17:43:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-29 11:49:55 +00:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
pub struct UrlPrefix {
|
2022-01-13 10:31:08 +00:00
|
|
|
/// Publicly exposed TLS hostnames for matching this rule
|
2021-12-08 10:11:22 +00:00
|
|
|
pub host: HostDescription,
|
2023-11-29 11:49:55 +00:00
|
|
|
|
2022-01-13 10:31:08 +00:00
|
|
|
/// Path prefix for matching this rule
|
2021-12-06 22:08:22 +00:00
|
|
|
pub path_prefix: Option<String>,
|
2023-11-29 11:49:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2022-01-13 10:31:08 +00:00
|
|
|
/// Priority with which this rule is considered (highest first)
|
2021-12-06 22:08:22 +00:00
|
|
|
pub priority: u32,
|
2022-01-13 10:31:08 +00:00
|
|
|
|
2022-12-05 17:43:48 +00:00
|
|
|
/// Consul service name
|
|
|
|
pub service_name: String,
|
2022-01-13 10:31:08 +00:00
|
|
|
/// Node address (ip+port) to handle requests that match this entry
|
|
|
|
pub target_addr: SocketAddr,
|
|
|
|
/// Is the target serving HTTPS instead of HTTP?
|
|
|
|
pub https_target: bool,
|
|
|
|
|
2023-01-02 16:33:05 +00:00
|
|
|
/// Flags for target selection
|
|
|
|
pub flags: ProxyEntryFlags,
|
2022-01-13 10:31:08 +00:00
|
|
|
|
|
|
|
/// Add the following headers to all responses returned
|
|
|
|
/// when matching this rule
|
2021-12-07 17:19:51 +00:00
|
|
|
pub add_headers: Vec<(String, String)>,
|
2021-12-07 15:37:22 +00:00
|
|
|
|
2023-11-29 11:49:55 +00:00
|
|
|
/// Try to match all these redirection before forwarding to the backend
|
|
|
|
/// when matching this rule
|
2023-11-29 14:50:25 +00:00
|
|
|
pub redirects: Vec<(UrlPrefix, UrlPrefix, u16)>,
|
2023-11-29 11:49:55 +00:00
|
|
|
|
2023-11-30 15:53:04 +00:00
|
|
|
/// Wether or not the domain must be validated before asking a certificate
|
|
|
|
/// to let's encrypt (only for Glob patterns)
|
|
|
|
pub on_demand_tls_ask: Option<String>,
|
|
|
|
|
2022-12-06 13:02:32 +00:00
|
|
|
/// Number of calls in progress, used to deprioritize slow back-ends
|
|
|
|
pub calls_in_progress: atomic::AtomicI64,
|
|
|
|
/// Time of last call, used for round-robin selection
|
|
|
|
pub last_call: atomic::AtomicI64,
|
2021-12-06 22:08:22 +00:00
|
|
|
}
|
|
|
|
|
2023-08-27 13:26:19 +00:00
|
|
|
impl PartialEq for ProxyEntry {
|
|
|
|
fn eq(&self, other: &Self) -> bool {
|
2023-11-29 11:49:55 +00:00
|
|
|
self.url_prefix == other.url_prefix
|
2023-08-27 13:26:19 +00:00
|
|
|
&& self.priority == other.priority
|
|
|
|
&& self.service_name == other.service_name
|
|
|
|
&& self.target_addr == other.target_addr
|
|
|
|
&& self.https_target == other.https_target
|
|
|
|
&& self.flags == other.flags
|
|
|
|
&& self.add_headers == other.add_headers
|
|
|
|
}
|
|
|
|
}
|
|
|
|
impl Eq for ProxyEntry {}
|
|
|
|
|
2023-11-29 11:49:55 +00:00
|
|
|
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![];
|
2023-11-30 15:53:04 +00:00
|
|
|
let mut on_demand_tls_ask: Option<String> = None;
|
2023-11-29 11:49:55 +00:00
|
|
|
for mid in middleware.into_iter() {
|
2023-11-30 15:53:04 +00:00
|
|
|
// LocalLb and GlobalLb are handled in the parent function
|
2023-11-29 11:49:55 +00:00
|
|
|
match mid {
|
2023-11-29 12:06:32 +00:00
|
|
|
ConfigTag::AddHeader(k, v) => add_headers.push((k.to_string(), v.clone())),
|
2023-11-29 11:49:55 +00:00
|
|
|
ConfigTag::AddRedirect(m, r, c) => redirects.push(((*m).clone(), (*r).clone(), *c)),
|
2023-11-30 15:53:04 +00:00
|
|
|
ConfigTag::OnDemandTlsAsk(url) => on_demand_tls_ask = Some(url.to_string()),
|
|
|
|
ConfigTag::LocalLb | ConfigTag::GlobalLb => (),
|
2023-11-29 11:49:55 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
ProxyEntry {
|
|
|
|
// id
|
|
|
|
service_name,
|
|
|
|
// frontend
|
|
|
|
url_prefix,
|
|
|
|
priority,
|
|
|
|
// backend
|
|
|
|
target_addr,
|
|
|
|
https_target,
|
|
|
|
// middleware
|
|
|
|
flags,
|
|
|
|
add_headers,
|
|
|
|
redirects,
|
2023-11-30 15:53:04 +00:00
|
|
|
on_demand_tls_ask,
|
2023-11-29 11:49:55 +00:00
|
|
|
// internal
|
|
|
|
last_call: atomic::AtomicI64::from(0),
|
|
|
|
calls_in_progress: atomic::AtomicI64::from(0),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-27 13:26:19 +00:00
|
|
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
2023-01-02 16:33:05 +00:00
|
|
|
pub struct ProxyEntryFlags {
|
2023-08-27 13:58:47 +00:00
|
|
|
/// Is the target healthy?
|
|
|
|
pub healthy: bool,
|
|
|
|
|
2023-01-02 16:33:05 +00:00
|
|
|
/// Is the target the same node as we are running on?
|
|
|
|
/// (if yes priorize it over other matching targets)
|
|
|
|
pub same_node: bool,
|
|
|
|
/// Is the target the same site as this node?
|
|
|
|
/// (if yes priorize it over other matching targets)
|
|
|
|
pub same_site: bool,
|
|
|
|
|
|
|
|
/// Is site-wide load balancing enabled for this service?
|
|
|
|
pub site_lb: bool,
|
|
|
|
/// Is global load balancing enabled for this service?
|
|
|
|
pub global_lb: bool,
|
|
|
|
}
|
|
|
|
|
2021-12-08 10:24:25 +00:00
|
|
|
impl std::fmt::Display for ProxyEntry {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
2021-12-08 21:58:19 +00:00
|
|
|
if self.https_target {
|
|
|
|
write!(f, "https://")?;
|
|
|
|
}
|
2021-12-08 10:24:25 +00:00
|
|
|
write!(f, "{} ", self.target_addr)?;
|
2021-12-08 11:02:39 +00:00
|
|
|
write!(
|
|
|
|
f,
|
2022-12-05 17:43:48 +00:00
|
|
|
"{}{} {}",
|
2023-11-29 11:49:55 +00:00
|
|
|
self.url_prefix.host,
|
|
|
|
self.url_prefix.path_prefix.as_deref().unwrap_or_default(),
|
2021-12-08 11:02:39 +00:00
|
|
|
self.priority
|
|
|
|
)?;
|
2023-08-27 13:58:47 +00:00
|
|
|
if !self.flags.healthy {
|
|
|
|
write!(f, " UNHEALTHY")?;
|
|
|
|
}
|
2023-01-02 16:33:05 +00:00
|
|
|
if self.flags.same_node {
|
2022-01-13 10:31:08 +00:00
|
|
|
write!(f, " OURSELF")?;
|
2023-01-02 16:33:05 +00:00
|
|
|
} else if self.flags.same_site {
|
2022-01-13 10:31:08 +00:00
|
|
|
write!(f, " SAME_SITE")?;
|
|
|
|
}
|
2023-01-02 16:33:05 +00:00
|
|
|
if self.flags.global_lb {
|
|
|
|
write!(f, " GLOBAL-LB")?;
|
|
|
|
} else if self.flags.site_lb {
|
|
|
|
write!(f, " SITE-LB")?;
|
|
|
|
}
|
2021-12-08 10:24:25 +00:00
|
|
|
if !self.add_headers.is_empty() {
|
2021-12-08 11:27:47 +00:00
|
|
|
write!(f, " +Headers: {:?}", self.add_headers)?;
|
2021-12-08 10:24:25 +00:00
|
|
|
}
|
2022-12-06 13:02:32 +00:00
|
|
|
Ok(())
|
2021-12-08 10:24:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-27 13:26:19 +00:00
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
2021-12-06 22:08:22 +00:00
|
|
|
pub struct ProxyConfig {
|
|
|
|
pub entries: Vec<ProxyEntry>,
|
|
|
|
}
|
|
|
|
|
2023-11-29 11:49:55 +00:00
|
|
|
#[derive(Debug)]
|
|
|
|
enum ParsedTag<'a> {
|
|
|
|
Frontend(MatchTag),
|
|
|
|
Middleware(ConfigTag<'a>),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
enum MatchTag {
|
|
|
|
/// HTTP backend (plain text)
|
|
|
|
Http(UrlPrefix, u32),
|
|
|
|
/// HTTPS backend (TLS encrypted)
|
|
|
|
HttpWithTls(UrlPrefix, u32),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
enum ConfigTag<'a> {
|
2023-11-29 12:06:32 +00:00
|
|
|
AddHeader(&'a str, String),
|
2023-11-29 14:50:25 +00:00
|
|
|
AddRedirect(UrlPrefix, UrlPrefix, u16),
|
2023-11-30 15:53:04 +00:00
|
|
|
OnDemandTlsAsk(&'a str),
|
2023-11-29 11:49:55 +00:00
|
|
|
GlobalLb,
|
|
|
|
LocalLb,
|
|
|
|
}
|
2021-12-06 22:08:22 +00:00
|
|
|
|
2023-11-29 11:49:55 +00:00
|
|
|
fn parse_tricot_tags(tag: &str) -> Option<ParsedTag> {
|
|
|
|
let splits = tag.splitn(4, ' ').collect::<Vec<_>>();
|
|
|
|
let parsed_tag = match splits.as_slice() {
|
|
|
|
["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)))
|
2021-12-08 16:50:40 +00:00
|
|
|
}
|
2023-11-29 11:49:55 +00:00
|
|
|
["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)))
|
|
|
|
}
|
2023-11-29 12:06:32 +00:00
|
|
|
["tricot-add-header", header_key, header_values @ ..] => Some(ParsedTag::Middleware(
|
|
|
|
ConfigTag::AddHeader(header_key, header_values.join(" ")),
|
2023-11-29 11:49:55 +00:00
|
|
|
)),
|
|
|
|
["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;
|
|
|
|
}
|
|
|
|
};
|
2021-12-06 22:08:22 +00:00
|
|
|
|
2023-11-29 11:49:55 +00:00
|
|
|
if matches!(p_replace.host, HostDescription::Pattern(_)) {
|
|
|
|
debug!(
|
|
|
|
"tag {} ignored as redirect to a glob pattern is not supported",
|
|
|
|
tag
|
|
|
|
);
|
|
|
|
return None;
|
|
|
|
}
|
2021-12-06 22:08:22 +00:00
|
|
|
|
2023-11-29 11:49:55 +00:00
|
|
|
let maybe_parsed_code = maybe_raw_code
|
|
|
|
.iter()
|
|
|
|
.next()
|
2023-11-29 14:50:25 +00:00
|
|
|
.map(|c| c.parse::<u16>().ok())
|
2023-11-29 11:49:55 +00:00
|
|
|
.flatten();
|
|
|
|
let http_code = match maybe_parsed_code {
|
|
|
|
Some(301) => 301,
|
|
|
|
Some(302) => 302,
|
2023-11-29 14:50:25 +00:00
|
|
|
Some(303) => 303,
|
|
|
|
Some(307) => 307,
|
2023-11-29 11:49:55 +00:00
|
|
|
_ => {
|
|
|
|
debug!(
|
2023-11-29 14:50:25 +00:00
|
|
|
"tag {} has a missing or invalid http code, setting it to 302",
|
2023-11-29 11:49:55 +00:00
|
|
|
tag
|
|
|
|
);
|
|
|
|
302
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
Some(ParsedTag::Middleware(ConfigTag::AddRedirect(
|
|
|
|
p_match, p_replace, http_code,
|
|
|
|
)))
|
2021-12-08 10:11:22 +00:00
|
|
|
}
|
2023-11-30 15:53:04 +00:00
|
|
|
["tricot-on-demand-tls-ask", url, ..] => {
|
|
|
|
Some(ParsedTag::Middleware(ConfigTag::OnDemandTlsAsk(url)))
|
|
|
|
}
|
2023-11-29 11:49:55 +00:00
|
|
|
["tricot-global-lb", ..] => Some(ParsedTag::Middleware(ConfigTag::GlobalLb)),
|
|
|
|
["tricot-local-lb", ..] => Some(ParsedTag::Middleware(ConfigTag::LocalLb)),
|
|
|
|
_ => None,
|
2021-12-08 10:11:22 +00:00
|
|
|
};
|
|
|
|
|
2023-11-29 11:49:55 +00:00
|
|
|
trace!("tag {} parsed as {:?}", tag, parsed_tag);
|
|
|
|
parsed_tag
|
2021-12-07 17:19:51 +00:00
|
|
|
}
|
|
|
|
|
2023-08-27 13:58:47 +00:00
|
|
|
fn parse_consul_service(
|
|
|
|
s: &consul::catalog::HealthServiceNode,
|
|
|
|
mut flags: ProxyEntryFlags,
|
2022-01-13 10:31:08 +00:00
|
|
|
) -> Vec<ProxyEntry> {
|
2023-08-27 13:58:47 +00:00
|
|
|
trace!("Parsing service: {:#?}", s);
|
2021-12-08 12:28:07 +00:00
|
|
|
|
2023-08-27 13:58:47 +00:00
|
|
|
let ip_addr = match s.service.address.parse() {
|
|
|
|
Ok(ip) => ip,
|
|
|
|
_ => match s.node.address.parse() {
|
2021-12-06 22:08:22 +00:00
|
|
|
Ok(ip) => ip,
|
2023-08-27 13:58:47 +00:00
|
|
|
_ => {
|
|
|
|
warn!(
|
|
|
|
"Could not get address for service {} at node {}",
|
|
|
|
s.service.service, s.node.node
|
|
|
|
);
|
|
|
|
return vec![];
|
2021-12-07 17:19:51 +00:00
|
|
|
}
|
2023-08-27 13:58:47 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
let addr = SocketAddr::new(ip_addr, s.service.port);
|
|
|
|
|
2023-11-29 11:49:55 +00:00
|
|
|
// tag parsing
|
|
|
|
let mut collected_middleware = vec![];
|
|
|
|
let mut collected_frontends = vec![];
|
2023-08-27 13:58:47 +00:00
|
|
|
for tag in s.service.tags.iter() {
|
2023-11-29 11:49:55 +00:00
|
|
|
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
|
|
|
|
),
|
2021-12-07 17:19:51 +00:00
|
|
|
}
|
2023-08-27 13:58:47 +00:00
|
|
|
}
|
2021-12-07 17:19:51 +00:00
|
|
|
|
2023-11-29 11:49:55 +00:00
|
|
|
// some legacy processing that would need a refactor later
|
|
|
|
for mid in collected_middleware.iter() {
|
|
|
|
match mid {
|
|
|
|
ConfigTag::GlobalLb => flags.global_lb = true,
|
|
|
|
ConfigTag::LocalLb => flags.site_lb = true,
|
2023-11-30 15:53:04 +00:00
|
|
|
_ => (),
|
2023-11-29 11:49:55 +00:00
|
|
|
};
|
2021-12-06 22:08:22 +00:00
|
|
|
}
|
|
|
|
|
2023-11-29 11:49:55 +00:00
|
|
|
// build proxy entries
|
|
|
|
let entries = collected_frontends
|
|
|
|
.into_iter()
|
|
|
|
.map(|frt| {
|
|
|
|
ProxyEntry::new(
|
|
|
|
s.service.service.clone(),
|
|
|
|
frt,
|
|
|
|
addr,
|
|
|
|
collected_middleware.as_ref(),
|
|
|
|
flags,
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
2023-08-27 13:58:47 +00:00
|
|
|
trace!("Result of parsing service:");
|
2021-12-08 12:28:07 +00:00
|
|
|
for ent in entries.iter() {
|
|
|
|
trace!(" {}", ent);
|
|
|
|
}
|
|
|
|
|
2021-12-07 16:56:15 +00:00
|
|
|
entries
|
2021-12-06 22:08:22 +00:00
|
|
|
}
|
|
|
|
|
2022-01-24 18:28:18 +00:00
|
|
|
pub fn spawn_proxy_config_task(
|
2023-06-13 09:40:13 +00:00
|
|
|
consul: consul::Consul,
|
2022-12-07 13:28:29 +00:00
|
|
|
local_node: String,
|
2022-01-24 18:28:18 +00:00
|
|
|
mut must_exit: watch::Receiver<bool>,
|
|
|
|
) -> watch::Receiver<Arc<ProxyConfig>> {
|
2021-12-07 12:50:44 +00:00
|
|
|
let (tx, rx) = watch::channel(Arc::new(ProxyConfig {
|
2021-12-06 22:08:22 +00:00
|
|
|
entries: Vec::new(),
|
2021-12-07 12:50:44 +00:00
|
|
|
}));
|
2021-12-07 17:50:58 +00:00
|
|
|
|
2022-12-05 17:43:48 +00:00
|
|
|
let metrics = ProxyConfigMetrics::new(rx.clone());
|
2021-12-07 16:56:15 +00:00
|
|
|
let consul = Arc::new(consul);
|
2021-12-06 22:08:22 +00:00
|
|
|
|
|
|
|
tokio::spawn(async move {
|
2023-08-27 13:58:47 +00:00
|
|
|
let mut catalog_rx = consul.watch_all_service_health(Duration::from_secs(300));
|
|
|
|
let mut local_node_site = None;
|
2022-01-13 10:31:08 +00:00
|
|
|
|
2022-01-24 18:28:18 +00:00
|
|
|
while !*must_exit.borrow() {
|
2023-08-27 13:58:47 +00:00
|
|
|
select! {
|
|
|
|
_ = catalog_rx.changed() => (),
|
2022-01-24 18:28:18 +00:00
|
|
|
_ = must_exit.changed() => continue,
|
|
|
|
};
|
|
|
|
|
2023-08-27 13:58:47 +00:00
|
|
|
let services = catalog_rx.borrow_and_update().clone();
|
|
|
|
if local_node_site.is_none() {
|
|
|
|
for (_, svcnodes) in services.iter() {
|
|
|
|
for svcnode in svcnodes.iter() {
|
|
|
|
if svcnode.node.node == local_node {
|
|
|
|
if let Some(site) = svcnode.node.meta.get("site") {
|
|
|
|
local_node_site = Some(site.to_string());
|
|
|
|
}
|
2022-01-13 10:31:08 +00:00
|
|
|
}
|
2021-12-07 16:56:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-27 13:58:47 +00:00
|
|
|
let mut entries = vec![];
|
2021-12-07 16:56:15 +00:00
|
|
|
|
2023-08-27 13:58:47 +00:00
|
|
|
for (_service, svcnodes) in services.iter() {
|
|
|
|
for svcnode in svcnodes.iter() {
|
|
|
|
let healthy = !svcnode.checks.iter().any(|x| x.status == "critical");
|
2021-12-06 22:08:22 +00:00
|
|
|
|
2023-08-27 13:58:47 +00:00
|
|
|
let same_node = svcnode.node.node == local_node;
|
|
|
|
let same_site = match (svcnode.node.meta.get("site"), local_node_site.as_ref())
|
|
|
|
{
|
2022-12-07 13:28:29 +00:00
|
|
|
(Some(s1), Some(s2)) => s1 == s2,
|
|
|
|
_ => false,
|
|
|
|
};
|
2022-01-13 10:31:08 +00:00
|
|
|
|
2023-08-27 13:58:47 +00:00
|
|
|
let flags = ProxyEntryFlags {
|
|
|
|
healthy,
|
|
|
|
same_node,
|
|
|
|
same_site,
|
|
|
|
site_lb: false,
|
|
|
|
global_lb: false,
|
|
|
|
};
|
|
|
|
|
|
|
|
entries.extend(parse_consul_service(&svcnode, flags));
|
2021-12-07 16:56:15 +00:00
|
|
|
}
|
|
|
|
}
|
2023-08-27 13:26:19 +00:00
|
|
|
|
|
|
|
entries.sort_by_cached_key(|ent| ent.to_string());
|
|
|
|
|
2021-12-07 16:56:15 +00:00
|
|
|
let config = ProxyConfig { entries };
|
2021-12-06 22:08:22 +00:00
|
|
|
|
2021-12-07 12:50:44 +00:00
|
|
|
tx.send(Arc::new(config)).expect("Internal error");
|
2021-12-06 22:08:22 +00:00
|
|
|
}
|
2022-12-05 17:43:48 +00:00
|
|
|
|
|
|
|
drop(metrics); // ensure Metrics lives up to here
|
2021-12-06 22:08:22 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
rx
|
|
|
|
}
|
2022-05-04 07:14:39 +00:00
|
|
|
|
2022-12-05 17:43:48 +00:00
|
|
|
// ----
|
|
|
|
|
|
|
|
struct ProxyConfigMetrics {
|
2023-10-02 11:11:42 +00:00
|
|
|
_proxy_config_entries: metrics::ObservableGauge<u64>,
|
2022-12-05 17:43:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl ProxyConfigMetrics {
|
|
|
|
fn new(rx: watch::Receiver<Arc<ProxyConfig>>) -> Self {
|
|
|
|
let meter = opentelemetry::global::meter("tricot");
|
|
|
|
Self {
|
|
|
|
_proxy_config_entries: meter
|
2023-10-02 11:11:42 +00:00
|
|
|
.u64_observable_gauge("proxy_config_entries")
|
|
|
|
.with_callback(move |observer| {
|
2022-12-05 17:43:48 +00:00
|
|
|
let mut patterns = HashMap::new();
|
|
|
|
for ent in rx.borrow().entries.iter() {
|
2022-12-05 18:10:15 +00:00
|
|
|
let attrs = (
|
2023-11-29 11:49:55 +00:00
|
|
|
ent.url_prefix.host.to_string(),
|
|
|
|
ent.url_prefix.path_prefix.clone().unwrap_or_default(),
|
2022-12-05 18:10:15 +00:00
|
|
|
ent.service_name.clone(),
|
2022-12-05 17:43:48 +00:00
|
|
|
);
|
2022-12-05 18:10:15 +00:00
|
|
|
*patterns.entry(attrs).or_default() += 1;
|
2022-12-05 17:43:48 +00:00
|
|
|
}
|
2022-12-05 18:10:15 +00:00
|
|
|
for ((host, prefix, svc), num) in patterns {
|
|
|
|
observer.observe(
|
|
|
|
num,
|
|
|
|
&[
|
|
|
|
KeyValue::new("host", host),
|
|
|
|
KeyValue::new("path_prefix", prefix),
|
|
|
|
KeyValue::new("service", svc),
|
|
|
|
],
|
|
|
|
);
|
2022-12-05 17:43:48 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
.with_description("Number of proxy entries (back-ends) configured in Tricot")
|
|
|
|
.init(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ----
|
|
|
|
|
2022-05-04 07:14:39 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2022-05-06 10:21:15 +00:00
|
|
|
use super::*;
|
2022-05-04 07:14:39 +00:00
|
|
|
|
2022-05-06 10:21:15 +00:00
|
|
|
#[test]
|
|
|
|
fn test_parse_tricot_add_header_tag() {
|
2023-11-29 12:06:32 +00:00
|
|
|
match parse_tricot_tags("tricot-add-header Content-Security-Policy default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'") {
|
|
|
|
Some(ParsedTag::Middleware(ConfigTag::AddHeader(name, value))) => {
|
2022-05-04 07:14:39 +00:00
|
|
|
assert_eq!(name, "Content-Security-Policy");
|
|
|
|
assert_eq!(value, "default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'");
|
|
|
|
}
|
|
|
|
_ => panic!("Passed a valid tag but the function says it is not valid")
|
|
|
|
}
|
2022-05-06 10:21:15 +00:00
|
|
|
}
|
2022-05-04 07:14:39 +00:00
|
|
|
}
|