#![cfg_attr(
not(test),
warn(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)
)]
mod artifact;
mod chain;
mod control_observability;
mod control_reload;
mod diagnostic;
mod error;
mod hash;
mod maglev;
mod manifest;
pub mod oci;
mod ratelimit;
mod reload;
mod rng;
mod route;
mod snapshot;
mod stek;
mod tls;
mod upstream;
mod weighted;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use arc_swap::ArcSwap;
use plecto_host::{LoadedFilter, SignedArtifact};
pub use artifact::{ArtifactStore, MemoryStore, ResolvedArtifact};
pub use chain::{ChainOutcome, RequestBodyOutcome, ResponseOutcome};
pub use diagnostic::{
DEV_KEY_IN_TRUST, Diagnostic, PATH_NORMALIZATION_REJECTED, QUOTA_EXCEEDED,
SIGNATURE_VERIFICATION_FAILED, diagnose, diagnosed_message,
};
pub use error::ControlError;
pub use manifest::{
Chain, CircuitBreaker, CompressionAlgorithm, FilterEntry, HealthConfig, IsolationKind,
Manifest, Observability, OutlierDetection, ProxyProtocolTrust, RateLimitKeyKind, Route,
RouteCompression, RouteRateLimit, State, StateBackendKind, TlsCert, Trust, Upstream,
};
pub use ratelimit::RateLimitDecision;
#[cfg(unix)]
pub use reload::SignalReloadSource;
pub use reload::{ReloadOutcome, ReloadSource, serve_reloads};
pub use route::{CompressionConfig, RouteInfo, UpgradeConfig, normalize_path};
pub use rustls::ClientConfig as TlsClientConfig;
pub use rustls::ServerConfig as TlsServerConfig;
pub use snapshot::ConfigSnapshot;
pub use upstream::{
Endpoints, HashInput, HashKeySource, Pick, UpstreamGroup, UpstreamInstance, UpstreamRegistry,
};
pub use plecto_host::{
FanOutSink, FilterSpan, Header, Host, HttpRequest, HttpResponse, InMemorySink, MetricsSink,
MetricsSnapshot, NoopSink, RequestTrace, SpanOutcome, TelemetrySink, TrustPolicy,
};
pub use plecto_host::{
ConformanceCheck, ConformanceReport, DEV_KEY_MARKER, DevKeyError, DevSigner, FILTER_WIT,
bound_sbom, public_key_path_for, run_conformance,
};
pub use plecto_host::otlp;
pub(crate) struct ActiveConfig {
pub(crate) filters: HashMap<String, Arc<LoadedFilter>>,
pub(crate) resolved_chain: Vec<Arc<LoadedFilter>>,
pub(crate) routes: Vec<route::CompiledRoute>,
pub(crate) tls: Option<Arc<rustls::ServerConfig>>,
pub(crate) quic_tls: Option<Arc<rustls::ServerConfig>>,
pub(crate) hash: String,
}
pub struct Control {
host: Host,
store: Box<dyn ArtifactStore>,
active: ArcSwap<ActiveConfig>,
reload_gate: parking_lot::Mutex<()>,
upstreams: Arc<UpstreamRegistry>,
manifest_path: Option<PathBuf>,
trust: Trust,
state: manifest::State,
base_dir: PathBuf,
filter_metrics: Arc<MetricsSink>,
observability: Observability,
listen: manifest::Listen,
proxy_protocol: Option<manifest::ProxyProtocolTrust>,
otlp: Option<Arc<plecto_host::otlp::OtlpBuffer>>,
}
impl Control {
fn assemble(
host: Host,
store: Box<dyn ArtifactStore>,
manifest: &Manifest,
base_dir: &Path,
manifest_path: Option<&Path>,
filter_metrics: Arc<MetricsSink>,
otlp: Option<Arc<plecto_host::otlp::OtlpBuffer>>,
) -> Result<Self, ControlError> {
let upstreams = Arc::new(UpstreamRegistry::new());
let active = build_active(&host, manifest, store.as_ref(), base_dir, &upstreams)?;
Ok(Self {
host,
store,
active: ArcSwap::from_pointee(active),
reload_gate: parking_lot::Mutex::new(()),
upstreams,
manifest_path: manifest_path.map(Path::to_path_buf),
trust: manifest.trust.clone(),
state: manifest.state.clone(),
base_dir: base_dir.to_path_buf(),
filter_metrics,
observability: manifest.observability.clone(),
listen: manifest.listen.clone(),
proxy_protocol: manifest.listen.proxy_protocol_trust()?,
otlp,
})
}
pub fn from_manifest(manifest: &Manifest, base_dir: &Path) -> Result<Self, ControlError> {
let (host, store, filter_metrics, otlp) = build_host_and_store(manifest, base_dir)?;
Self::assemble(
host,
Box::new(store),
manifest,
base_dir,
None,
filter_metrics,
otlp,
)
}
pub fn load(
host: Host,
manifest: &Manifest,
store: Box<dyn ArtifactStore>,
) -> Result<Self, ControlError> {
let (host, otlp) = add_otlp_buffer(host, manifest);
Self::assemble(
host,
store,
manifest,
Path::new("."),
None,
Arc::new(MetricsSink::new()),
otlp,
)
}
pub fn from_manifest_path(manifest_path: &Path) -> Result<Self, ControlError> {
let base_dir = manifest_path.parent().unwrap_or_else(|| Path::new("."));
let manifest = read_manifest(manifest_path)?;
let (host, store, filter_metrics, otlp) = build_host_and_store(&manifest, base_dir)?;
Self::assemble(
host,
Box::new(store),
&manifest,
base_dir,
Some(manifest_path),
filter_metrics,
otlp,
)
}
pub fn load_at(
host: Host,
manifest_path: &Path,
store: Box<dyn ArtifactStore>,
) -> Result<Self, ControlError> {
let base_dir = manifest_path.parent().unwrap_or_else(|| Path::new("."));
let manifest = read_manifest(manifest_path)?;
let (host, otlp) = add_otlp_buffer(host, &manifest);
Self::assemble(
host,
store,
&manifest,
base_dir,
Some(manifest_path),
Arc::new(MetricsSink::new()),
otlp,
)
}
pub fn loaded_ids(&self) -> Vec<String> {
self.active.load().filters.keys().cloned().collect()
}
}
fn read_manifest(path: &Path) -> Result<Manifest, ControlError> {
let toml = std::fs::read_to_string(path).map_err(|e| ControlError::IoAt {
path: path.to_path_buf(),
source: e,
})?;
Manifest::from_toml(&toml)
}
fn read_file(path: &Path) -> Result<Vec<u8>, ControlError> {
std::fs::read(path).map_err(|e| ControlError::IoAt {
path: path.to_path_buf(),
source: e,
})
}
#[derive(Debug)]
pub struct ValidateOutcome {
pub config_version: String,
pub warnings: Vec<Diagnostic>,
}
pub fn validate_manifest(
manifest: &Manifest,
base_dir: &Path,
) -> Result<ValidateOutcome, ControlError> {
let mut pems: Vec<Vec<u8>> = Vec::with_capacity(manifest.trust.keys.len());
let mut warnings = Vec::new();
for key_path in &manifest.trust.keys {
let pem = read_file(&base_dir.join(key_path))?;
if pem.starts_with(plecto_host::DEV_KEY_MARKER.as_bytes()) {
warnings.push(DEV_KEY_IN_TRUST);
}
pems.push(pem);
}
TrustPolicy::from_pem_keys(&pems).map_err(|e| ControlError::TrustKey(e.to_string()))?;
manifest.state.validate()?;
manifest.listen.validate()?;
let filter_ids = validate_filters_and_chain(manifest)?;
let upstream_names: HashSet<&str> =
manifest.upstreams.iter().map(|u| u.name.as_str()).collect();
route::validate_routes(&manifest.routes, &filter_ids, &upstream_names)?;
let client_auth_ca = manifest.read_client_auth_ca(base_dir)?;
tls::build_server_configs(
&manifest.tls,
manifest.resumption.as_ref(),
manifest
.listen
.client_auth
.as_ref()
.zip(client_auth_ca.as_deref()),
base_dir,
)?;
UpstreamRegistry::new().reconcile(&manifest.upstreams, base_dir)?;
let config_version = manifest.content_hash_with_ca(client_auth_ca.as_deref())?;
Ok(ValidateOutcome {
config_version,
warnings,
})
}
pub fn validate_manifest_path(path: &Path) -> Result<ValidateOutcome, ControlError> {
let base_dir = path.parent().unwrap_or_else(|| Path::new("."));
let manifest = read_manifest(path)?;
validate_manifest(&manifest, base_dir)
}
pub fn manifest_json_schema() -> Result<String, ControlError> {
let generator = schemars::generate::SchemaSettings::draft07().into_generator();
let schema = generator.into_root_schema_for::<Manifest>();
Ok(serde_json::to_string_pretty(&schema)?)
}
type BuiltHost = (
Host,
oci::OciLayoutStore,
Arc<MetricsSink>,
Option<Arc<plecto_host::otlp::OtlpBuffer>>,
);
fn build_host_and_store(manifest: &Manifest, base_dir: &Path) -> Result<BuiltHost, ControlError> {
let mut pems: Vec<Vec<u8>> = Vec::with_capacity(manifest.trust.keys.len());
for key_path in &manifest.trust.keys {
pems.push(read_file(&base_dir.join(key_path))?);
}
let trust =
TrustPolicy::from_pem_keys(&pems).map_err(|e| ControlError::TrustKey(e.to_string()))?;
let kv = build_state_backend(&manifest.state, base_dir)?;
let filter_metrics = Arc::new(MetricsSink::new());
let host = Host::with_backend(trust, kv)
.map_err(|e| ControlError::HostInit(e.to_string()))?
.with_telemetry_sink(filter_metrics.clone());
let (host, otlp) = add_otlp_buffer(host, manifest);
let store = oci::OciLayoutStore::new(base_dir);
Ok((host, store, filter_metrics, otlp))
}
fn build_state_backend(
state: &manifest::State,
base_dir: &Path,
) -> Result<Arc<dyn plecto_host::KvBackend>, ControlError> {
state.validate()?;
match state.backend {
StateBackendKind::Memory => Ok(Arc::new(plecto_host::MemoryBackend::default())),
StateBackendKind::Redb => {
let path = base_dir.join(state.path.as_deref().unwrap_or_default());
if !path.parent().is_some_and(Path::is_dir) {
return Err(ControlError::StateBackendInit(format!(
"parent directory of {} does not exist",
path.display()
)));
}
let backend = plecto_host::RedbBackend::open(&path)
.map_err(|e| ControlError::StateBackendInit(e.to_string()))?;
Ok(Arc::new(backend))
}
}
}
fn add_otlp_buffer(
host: Host,
manifest: &Manifest,
) -> (Host, Option<Arc<plecto_host::otlp::OtlpBuffer>>) {
if manifest.observability.otlp_endpoint.is_none() {
return (host, None);
}
let buffer = Arc::new(plecto_host::otlp::OtlpBuffer::default());
(host.with_added_telemetry_sink(buffer.clone()), Some(buffer))
}
fn validate_filters_and_chain(manifest: &Manifest) -> Result<HashSet<&str>, ControlError> {
let mut filter_ids: HashSet<&str> = HashSet::with_capacity(manifest.filters.len());
for entry in &manifest.filters {
if !filter_ids.insert(entry.id.as_str()) {
return Err(ControlError::DuplicateFilterId(entry.id.clone()));
}
entry.validate()?;
}
for id in &manifest.chain.filters {
if !filter_ids.contains(id.as_str()) {
return Err(ControlError::UnknownChainFilter(id.clone()));
}
}
Ok(filter_ids)
}
fn build_active(
host: &Host,
manifest: &Manifest,
store: &dyn ArtifactStore,
base_dir: &Path,
registry: &UpstreamRegistry,
) -> Result<ActiveConfig, ControlError> {
let filter_ids = validate_filters_and_chain(manifest)?;
let mut filters: HashMap<String, Arc<LoadedFilter>> = HashMap::new();
for entry in &manifest.filters {
let artifact = store.resolve(&entry.source, &entry.digest)?;
let signed = SignedArtifact {
component_bytes: &artifact.component,
component_signature: &artifact.component_signature,
sbom: &artifact.sbom,
sbom_signature: &artifact.sbom_signature,
};
let loaded = host
.load(&entry.id, &signed, entry.load_options())
.map_err(|err| ControlError::Load {
id: entry.id.clone(),
err,
})?;
filters.insert(entry.id.clone(), Arc::new(loaded));
}
let upstream_names: HashSet<&str> =
manifest.upstreams.iter().map(|u| u.name.as_str()).collect();
let validated_routes = route::validate_routes(&manifest.routes, &filter_ids, &upstream_names)?;
let client_auth_ca = manifest.read_client_auth_ca(base_dir)?;
let (tls, quic_tls) = match tls::build_server_configs(
&manifest.tls,
manifest.resumption.as_ref(),
manifest
.listen
.client_auth
.as_ref()
.zip(client_auth_ca.as_deref()),
base_dir,
)? {
Some(configs) => (Some(configs.tcp), Some(configs.quic)),
None => (None, None),
};
let hash = manifest.content_hash_with_ca(client_auth_ca.as_deref())?;
registry.reconcile(&manifest.upstreams, base_dir)?;
let mut routes = Vec::with_capacity(validated_routes.len());
for route::ValidatedRoute { route: r, targets } in validated_routes {
let mut resolved = Vec::with_capacity(targets.len());
for (name, weight) in targets {
let Some(group) = registry.group(name) else {
return Err(ControlError::UnknownRouteUpstream {
path_prefix: r.matcher.path_prefix.clone(),
upstream: name.to_string(),
});
};
resolved.push((group, weight));
}
let backends = weighted::WeightedBackends::new(resolved).map_err(|reason| {
ControlError::InvalidRoute {
path_prefix: r.matcher.path_prefix.clone(),
reason,
}
})?;
routes.push(route::CompiledRoute::compile(r, backends, &filters));
}
let resolved_chain = manifest
.chain
.filters
.iter()
.filter_map(|id| filters.get(id).cloned())
.collect();
Ok(ActiveConfig {
filters,
resolved_chain,
routes,
tls,
quic_tls,
hash,
})
}