use std::sync::Arc;
use arc_swap::ArcSwap;
use crate::core::error::OlError;
use super::config::EgressConfig;
use super::factory::{build_client_with, Consumer, Timeouts};
#[derive(Debug, Clone)]
pub struct ClientHandle(Arc<ArcSwap<Option<reqwest::Client>>>);
impl ClientHandle {
pub fn new(client: Option<reqwest::Client>) -> Self {
Self(Arc::new(ArcSwap::from_pointee(client)))
}
pub fn of(client: reqwest::Client) -> Self {
Self::new(Some(client))
}
pub fn empty() -> Self {
Self::new(None)
}
pub fn current(&self) -> Option<reqwest::Client> {
(**self.0.load()).clone()
}
pub fn if_routed(&self) -> Option<Self> {
self.current().map(|_| self.clone())
}
fn store(&self, client: Option<reqwest::Client>) {
self.0.store(Arc::new(client));
}
}
#[derive(Debug, Clone)]
pub struct EgressClients {
pub cloud: ClientHandle,
pub poller: ClientHandle,
pub alerts: ClientHandle,
pub boundary: ClientHandle,
cloud_timeouts: Timeouts,
poll_timeouts: Timeouts,
}
#[derive(Debug)]
pub struct StagedClients {
cloud: reqwest::Client,
poller: reqwest::Client,
alerts: reqwest::Client,
boundary: reqwest::Client,
}
impl StagedClients {
pub fn cloud(&self) -> &reqwest::Client {
&self.cloud
}
}
impl EgressClients {
pub fn new(cloud_timeouts: Timeouts, poll_timeouts: Timeouts) -> Self {
Self {
cloud: ClientHandle::empty(),
poller: ClientHandle::empty(),
alerts: ClientHandle::empty(),
boundary: ClientHandle::empty(),
cloud_timeouts,
poll_timeouts,
}
}
pub fn stage(&self, cfg: &EgressConfig) -> Result<StagedClients, OlError> {
Ok(StagedClients {
cloud: build_client_with(Consumer::CloudWorker, cfg, self.cloud_timeouts)?,
poller: build_client_with(Consumer::PolicyPoller, cfg, self.poll_timeouts)?,
alerts: build_client_with(Consumer::Alerts, cfg, self.poll_timeouts)?,
boundary: build_client_with(Consumer::Boundary, cfg, Timeouts::default())?,
})
}
pub fn install(&self, staged: StagedClients) {
self.cloud.store(Some(staged.cloud));
self.poller.store(Some(staged.poller));
self.alerts.store(Some(staged.alerts));
self.boundary.store(Some(staged.boundary));
}
pub fn apply(&self, cfg: &EgressConfig) -> Result<(), OlError> {
self.install(self.stage(cfg)?);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::egress::config::ProxyToml;
use crate::core::error::ERR_DIRECT_FORBIDDEN;
struct NoEnv;
impl crate::core::egress::config::EnvSource for NoEnv {
fn var(&self, _key: &str) -> Option<String> {
None
}
}
fn resolve(toml: ProxyToml) -> EgressConfig {
EgressConfig::resolve(Some(&toml), &NoEnv, 7443, 7444).expect("resolve")
}
fn proxied(url: &str) -> EgressConfig {
resolve(ProxyToml {
mode: Some("manual".into()),
url: Some(url.into()),
source: Some("windows".into()),
..Default::default()
})
}
#[test]
fn an_empty_handle_has_no_client() {
assert!(ClientHandle::empty().current().is_none());
assert!(
ClientHandle::empty().if_routed().is_none(),
"a consumer must not be started against a handle with no route"
);
}
#[test]
fn a_swap_is_visible_to_every_holder_of_the_handle() {
let clients = EgressClients::new(Timeouts::default(), Timeouts::default());
let consumer_side = clients.cloud.clone();
assert!(consumer_side.current().is_none());
clients
.apply(&proxied("http://proxy.test:8080"))
.expect("apply");
assert!(
consumer_side.current().is_some(),
"a swap must be visible through a handle cloned BEFORE the swap"
);
}
#[test]
fn every_consumer_handle_is_filled_by_one_apply() {
let clients = EgressClients::new(Timeouts::default(), Timeouts::default());
clients.apply(&EgressConfig::direct()).expect("apply");
for (name, handle) in [
("cloud", &clients.cloud),
("poller", &clients.poller),
("alerts", &clients.alerts),
("boundary", &clients.boundary),
] {
assert!(
handle.current().is_some(),
"{name} was left without a client"
);
}
}
#[test]
fn a_route_that_cannot_be_built_installs_nothing() {
let clients = EgressClients::new(Timeouts::default(), Timeouts::default());
clients.apply(&EgressConfig::direct()).expect("seed");
let before = clients.cloud.current().is_some();
let forbidden = resolve(ProxyToml {
allow_direct: Some(false),
..Default::default()
});
let err = clients.apply(&forbidden).expect_err("must refuse");
assert_eq!(err.code, ERR_DIRECT_FORBIDDEN);
assert_eq!(
clients.cloud.current().is_some(),
before,
"a refused route must leave the installed one untouched"
);
}
#[test]
fn staged_clients_are_not_installed_until_install_runs() {
let clients = EgressClients::new(Timeouts::default(), Timeouts::default());
let staged = clients
.stage(&proxied("http://proxy.test:8080"))
.expect("stage");
assert!(
clients.cloud.current().is_none(),
"staging must not touch the live handles -- the probe runs first"
);
clients.install(staged);
assert!(clients.cloud.current().is_some());
}
}