use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use anyhow::{bail, Result};
use serde::Serialize;
use workload_spec::{
EnvValue, EnvVar, ExposeSpec, HealthProbe, Healthcheck, ImageRef, InlineFile,
LifecycleArchetype, MeshExpose, MeshIdent, Millis, NamespaceId, ResourceLimits, RestartPolicy,
StopPolicy, TenantId, TierTag, Workload, WorkloadSpec,
};
use crate::config::{
domain_serving_service, normalize_mount, DeployTier, DomainConfig, ServiceConfig,
};
pub const ROUTES_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum DeployedUnit {
Bundle,
Component(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InnerDoorMount {
pub mount: String,
pub unit: DeployedUnit,
pub headers: BTreeMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InnerDoorPlan {
pub service: String,
pub mounts: Vec<InnerDoorMount>,
}
pub fn passway_mount(raw: Option<&str>) -> String {
match raw.map(normalize_mount) {
Some(m) if !m.is_empty() => format!("/{m}"),
_ => String::new(),
}
}
pub fn plan(
service: &ServiceConfig,
domains: &BTreeMap<String, DomainConfig>,
) -> Result<Option<InnerDoorPlan>> {
let domain = domain_serving_service(domains, &service.name);
let mut mounts: Vec<InnerDoorMount> = Vec::new();
for component in &service.components {
let unit = match component.deploy {
DeployTier::Bundle => DeployedUnit::Bundle,
DeployTier::Workload => DeployedUnit::Component(component.id.clone()),
};
let mount = passway_mount(component.mount.as_deref());
mounts.push(InnerDoorMount {
headers: headers_for(domain, &mount),
mount,
unit,
});
}
let units: std::collections::BTreeSet<&DeployedUnit> = mounts.iter().map(|m| &m.unit).collect();
if units.len() < 2 {
return Ok(None);
}
if !mounts.iter().any(|m| m.mount.is_empty()) {
bail!(
"services/{}/service.toml declares {} independently-deployed units but no component \
at the service root — every component sets a `mount`. An inner door's table needs a \
root (\"\") mount as its catch-all; without one every path outside the declared \
mounts 503s, which is indistinguishable from an outage. Drop the `mount` from \
whichever component serves `/`.",
service.name,
units.len(),
);
}
Ok(Some(InnerDoorPlan {
service: service.name.clone(),
mounts,
}))
}
fn headers_for(domain: Option<&DomainConfig>, mount: &str) -> BTreeMap<String, String> {
let path = if mount.is_empty() { "/" } else { mount };
domain
.and_then(|d| d.route_for_path(path))
.map(|r| r.headers.clone())
.unwrap_or_default()
}
#[derive(Debug, Serialize)]
struct RoutesFile<'a> {
schema_version: u32,
routes: Vec<RouteEntry<'a>>,
}
#[derive(Debug, Serialize)]
struct RouteEntry<'a> {
mount: &'a str,
upstreams: Vec<String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
headers: &'a BTreeMap<String, String>,
}
impl InnerDoorPlan {
pub fn units(&self) -> Vec<DeployedUnit> {
let set: std::collections::BTreeSet<DeployedUnit> =
self.mounts.iter().map(|m| m.unit.clone()).collect();
set.into_iter().collect()
}
pub fn routes_file(
&self,
address: impl Fn(&DeployedUnit) -> Option<String>,
) -> Result<String> {
let mut routes = Vec::with_capacity(self.mounts.len());
for m in &self.mounts {
let Some(addr) = address(&m.unit) else {
bail!(
"service {}: mount {:?} is served by {:?}, which has no resolved address yet. \
Refusing to write a partial route table — a missing mount does not 503, it \
falls through to the root mount and serves the wrong component with a 200.",
self.service,
m.mount,
m.unit,
);
};
routes.push(RouteEntry {
mount: &m.mount,
upstreams: vec![addr],
headers: &m.headers,
});
}
Ok(serde_json::to_string(&RoutesFile {
schema_version: ROUTES_SCHEMA_VERSION,
routes,
})?)
}
}
pub const INNER_DOOR_BINARY: &str = "/usr/local/bin/passway";
pub const ROUTES_DIR: &str = "/var/lib/passway/routes";
pub const INNER_DOOR_HOST: &str = "127.0.0.1";
pub const INNER_DOOR_PORT_LOW: u16 = 10_000;
pub const INNER_DOOR_PORT_HIGH: u16 = 19_999;
pub fn listen_port(service: &str) -> u16 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for byte in service.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
let span = u64::from(INNER_DOOR_PORT_HIGH - INNER_DOOR_PORT_LOW) + 1;
INNER_DOOR_PORT_LOW + (hash % span) as u16
}
pub fn component_workload_ident(service: &str, component_id: &str) -> String {
crate::reconciler::native_support::sanitize_ident(&format!("{service}-{component_id}"))
}
impl InnerDoorPlan {
pub fn workload_name(&self) -> String {
format!("passway-inner-{}", self.service)
}
pub fn routes_path(&self) -> PathBuf {
Path::new(ROUTES_DIR).join(format!("{}.routes.json", self.service))
}
pub fn listen_port(&self) -> u16 {
listen_port(&self.service)
}
pub fn unit_ident(&self, unit: &DeployedUnit, bundle_ident: &str) -> String {
match unit {
DeployedUnit::Bundle => bundle_ident.to_string(),
DeployedUnit::Component(id) => component_workload_ident(&self.service, id),
}
}
pub fn resolve_addresses(
&self,
bundle_ident: &str,
lookup: impl Fn(&str) -> Option<String>,
) -> BTreeMap<DeployedUnit, String> {
self.units()
.into_iter()
.filter_map(|unit| {
let addr = lookup(&self.unit_ident(&unit, bundle_ident))?;
Some((unit, addr))
})
.collect()
}
pub fn workload(
&self,
listen_port: u16,
address: impl Fn(&DeployedUnit) -> Option<String>,
) -> Result<Workload> {
let routes_path = self.routes_path();
let name = self.workload_name();
let listen = format!("127.0.0.1:{listen_port}");
let env = vec![
literal_env("PASSWAY_TLS_MODE", "plaintext".to_string()),
literal_env("PASSWAY_LISTEN", listen),
literal_env(
"PASSWAY_PATH_ROUTES_FILE",
routes_path.display().to_string(),
),
];
let spec = WorkloadSpec {
name: name.clone(),
image: ImageRef {
registry: "local".into(),
repository: "passway".into(),
tag: "inner-door".into(),
digest: String::new(),
},
tier: TierTag("infra".into()),
tenant: TenantId::singleton(),
namespace: NamespaceId::singleton(),
replicas: 1,
command: Some(vec![INNER_DOOR_BINARY.to_string()]),
entrypoint: None,
workdir: None,
user: None,
env,
secrets: vec![],
volumes: vec![],
resources: ResourceLimits {
memory_mb: 128,
cpu_millis: 256,
memory_request_mb: None,
cpu_limit_millis: None,
pids_max: None,
scratch_floor_mb: None,
},
depends_on: vec![],
requires: vec![],
healthcheck: Some(Healthcheck {
probe: HealthProbe::TcpConnect { port: listen_port },
interval: Millis::from_secs(10),
timeout: Millis::from_secs(2),
initial_delay: Millis::from_secs(5),
failure_threshold: 3,
}),
restart_policy: RestartPolicy::Always,
archetype: Some(LifecycleArchetype::Appliance),
stop_policy: StopPolicy {
signal: 15,
grace_period: Millis::from_secs(5),
},
expose: ExposeSpec {
mesh: MeshExpose {
identity: MeshIdent(name),
ports: MeshExpose::anonymous_ports([listen_port]),
allow_from: vec![],
},
public: None,
operator: None,
},
labels: HashMap::new(),
durability: None,
annotations: HashMap::new(),
files: vec![InlineFile {
path: routes_path,
content: self.routes_file(address)?,
mode: Some(0o600),
}],
};
Ok(Workload::container(spec))
}
}
fn literal_env(name: &str, value: String) -> EnvVar {
EnvVar {
name: name.into(),
value: EnvValue::Literal { value },
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{DomainRoute, FrontDoor, RouteMode, ServiceComponent};
use crate::route_table::{CdnPlacement, RouteTable};
fn component(id: &str, mount: Option<&str>, deploy: DeployTier) -> ServiceComponent {
ServiceComponent {
id: id.to_string(),
kind: "mesofact-spa".to_string(),
path: format!("app/{id}"),
git: None,
role: "static".to_string(),
publishes: Some("static".to_string()),
mount: mount.map(str::to_string),
wave: 0,
deploy,
}
}
fn service(name: &str, components: Vec<ServiceComponent>) -> ServiceConfig {
ServiceConfig {
schema_version: 1,
name: name.to_string(),
domain: format!("{name}.test"),
health_path: None,
components,
db: Default::default(),
}
}
fn domains(service: &str, routes: &[(&str, &[(&str, &str)])]) -> BTreeMap<String, DomainConfig> {
let mut map = BTreeMap::new();
map.insert(
"test".to_string(),
DomainConfig {
schema_version: 1,
name: "test".to_string(),
domain: format!("{service}.test"),
front_door: FrontDoor::Passway,
cdn_bucket: "cdn".to_string(),
worker_bundle_path: None,
routes: routes
.iter()
.map(|(path, headers)| DomainRoute {
path: path.to_string(),
headers: headers
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
mode: RouteMode::Static {
component: format!("{service}/root"),
},
})
.collect(),
},
);
map
}
#[test]
fn a_single_unit_service_gets_no_inner_door() {
let svc = service(
"yah-marketing",
vec![component("site", None, DeployTier::Bundle)],
);
assert_eq!(plan(&svc, &BTreeMap::new()).unwrap(), None);
}
#[test]
fn several_bundle_components_are_one_unit_and_still_get_no_door() {
let svc = service(
"noisetable",
vec![
component("site", None, DeployTier::Bundle),
component("app", Some("app"), DeployTier::Bundle),
component("docs", Some("docs"), DeployTier::Bundle),
],
);
assert_eq!(plan(&svc, &BTreeMap::new()).unwrap(), None);
}
#[test]
fn one_bundle_component_plus_one_workload_component_is_two_units() {
let svc = service(
"noisetable",
vec![
component("site", None, DeployTier::Bundle),
component("account", Some("app"), DeployTier::Workload),
],
);
let plan = plan(&svc, &BTreeMap::new()).unwrap().expect("two units");
assert_eq!(
plan.mounts.iter().map(|m| m.mount.as_str()).collect::<Vec<_>>(),
["", "/app"]
);
assert_eq!(
plan.units(),
vec![
DeployedUnit::Bundle,
DeployedUnit::Component("account".into())
]
);
}
#[test]
fn each_mount_carries_only_its_own_routes_headers() {
let svc = service(
"noisetable",
vec![
component("site", None, DeployTier::Bundle),
component("account", Some("app"), DeployTier::Workload),
],
);
let domains = domains(
"noisetable",
&[
(
"/app/*",
&[
("cross-origin-opener-policy", "same-origin"),
("cross-origin-embedder-policy", "require-corp"),
],
),
("/*", &[]),
],
);
let plan = plan(&svc, &domains).unwrap().expect("two units");
let root = &plan.mounts[0];
assert_eq!(root.mount, "");
assert!(root.headers.is_empty(), "{:?}", root.headers);
let app = &plan.mounts[1];
assert_eq!(app.mount, "/app");
assert_eq!(
app.headers.get("cross-origin-opener-policy").map(String::as_str),
Some("same-origin")
);
assert_eq!(
app.headers
.get("cross-origin-embedder-policy")
.map(String::as_str),
Some("require-corp")
);
}
#[test]
fn a_bundle_components_sub_mount_keeps_its_headers_and_the_bundle_upstream() {
let svc = service(
"noisetable",
vec![
component("site", None, DeployTier::Bundle),
component("docs", Some("docs"), DeployTier::Bundle),
component("account", Some("app"), DeployTier::Workload),
],
);
let domains = domains(
"noisetable",
&[("/docs/*", &[("x-frame-options", "DENY")])],
);
let plan = plan(&svc, &domains).unwrap().expect("two units");
let docs = &plan.mounts[1];
assert_eq!(docs.mount, "/docs");
assert_eq!(docs.unit, DeployedUnit::Bundle);
assert_eq!(docs.headers.get("x-frame-options").map(String::as_str), Some("DENY"));
assert_eq!(plan.units().len(), 2);
assert_eq!(plan.mounts.len(), 3);
}
#[test]
fn a_table_with_no_root_mount_is_refused_rather_than_written() {
let svc = service(
"noisetable",
vec![
component("app", Some("app"), DeployTier::Bundle),
component("account", Some("account"), DeployTier::Workload),
],
);
let err = plan(&svc, &BTreeMap::new()).unwrap_err().to_string();
assert!(err.contains("no component at the service root"), "{err}");
}
#[test]
fn rendering_produces_the_exact_shape_passway_reads() {
let svc = service(
"noisetable",
vec![
component("site", None, DeployTier::Bundle),
component("account", Some("app"), DeployTier::Workload),
],
);
let domains = domains(
"noisetable",
&[("/app/*", &[("cross-origin-opener-policy", "same-origin")])],
);
let plan = plan(&svc, &domains).unwrap().unwrap();
let json = plan
.routes_file(|unit| match unit {
DeployedUnit::Bundle => Some("127.0.0.1:8081".to_string()),
DeployedUnit::Component(id) if id == "account" => {
Some("127.0.0.1:8082".to_string())
}
DeployedUnit::Component(_) => None,
})
.unwrap();
assert_eq!(
json,
r#"{"schema_version":1,"routes":[{"mount":"","upstreams":["127.0.0.1:8081"]},{"mount":"/app","upstreams":["127.0.0.1:8082"],"headers":{"cross-origin-opener-policy":"same-origin"}}]}"#
);
}
#[test]
fn an_unresolved_upstream_refuses_the_whole_table() {
let svc = service(
"noisetable",
vec![
component("site", None, DeployTier::Bundle),
component("account", Some("app"), DeployTier::Workload),
],
);
let plan = plan(&svc, &BTreeMap::new()).unwrap().unwrap();
let err = plan
.routes_file(|unit| match unit {
DeployedUnit::Bundle => Some("127.0.0.1:8081".to_string()),
DeployedUnit::Component(_) => None,
})
.unwrap_err()
.to_string();
assert!(err.contains("no resolved address"), "{err}");
assert!(err.contains("account"), "{err}");
}
#[test]
fn the_rendered_door_is_cleartext_on_loopback_and_carries_its_own_table() {
let svc = service(
"noisetable",
vec![
component("site", None, DeployTier::Bundle),
component("account", Some("app"), DeployTier::Workload),
],
);
let plan = plan(&svc, &BTreeMap::new()).unwrap().unwrap();
let workload = plan
.workload(8443, |unit| match unit {
DeployedUnit::Bundle => Some("127.0.0.1:8081".to_string()),
DeployedUnit::Component(_) => Some("127.0.0.1:8082".to_string()),
})
.unwrap();
let spec = workload.container_spec().expect("container-shaped");
let env: BTreeMap<&str, &str> = spec
.env
.iter()
.filter_map(|e| match &e.value {
EnvValue::Literal { value } => Some((e.name.as_str(), value.as_str())),
_ => None,
})
.collect();
assert_eq!(env.get("PASSWAY_TLS_MODE"), Some(&"plaintext"));
assert_eq!(env.get("PASSWAY_LISTEN"), Some(&"127.0.0.1:8443"));
assert!(spec.expose.public.is_none(), "an inner door is never public");
assert_eq!(spec.files.len(), 1);
let file = &spec.files[0];
assert_eq!(
env.get("PASSWAY_PATH_ROUTES_FILE").map(|s| s.to_string()),
Some(file.path.display().to_string())
);
assert!(file.content.contains("\"schema_version\":1"), "{}", file.content);
assert!(file.content.contains("127.0.0.1:8082"), "{}", file.content);
assert_eq!(spec.command.as_deref(), Some(&[INNER_DOOR_BINARY.to_string()][..]));
}
#[test]
fn an_unresolvable_unit_stops_the_workload_being_built() {
let svc = service(
"noisetable",
vec![
component("site", None, DeployTier::Bundle),
component("account", Some("app"), DeployTier::Workload),
],
);
let plan = plan(&svc, &BTreeMap::new()).unwrap().unwrap();
assert!(plan.workload(8443, |_| None).is_err());
}
#[test]
fn the_mount_spelling_matches_passways_convention_in_both_directions() {
assert_eq!(passway_mount(None), "");
assert_eq!(passway_mount(Some("")), "");
assert_eq!(passway_mount(Some("/")), "");
for raw in ["app", "/app", "app/", "/app/"] {
assert_eq!(passway_mount(Some(raw)), "/app", "{raw}");
}
assert_eq!(passway_mount(Some("/a/b/")), "/a/b");
}
#[test]
fn the_derived_port_is_stable_and_inside_its_declared_window() {
assert_eq!(listen_port("noisetable"), listen_port("noisetable"));
for service in ["noisetable", "yah-marketing", "", "a", "a-very-long-service-name"] {
let port = listen_port(service);
assert!(
(INNER_DOOR_PORT_LOW..=INNER_DOOR_PORT_HIGH).contains(&port),
"{service} -> {port}"
);
assert!(port < 32_768, "{service} -> {port}");
}
}
#[test]
fn two_services_do_not_share_a_door() {
assert_ne!(listen_port("noisetable"), listen_port("yah-marketing"));
}
#[test]
fn each_unit_resolves_through_its_own_identity_rule() {
let svc = service(
"noisetable",
vec![
component("site", None, DeployTier::Bundle),
component("account", Some("app"), DeployTier::Workload),
],
);
let plan = plan(&svc, &BTreeMap::new()).unwrap().unwrap();
assert_eq!(
plan.unit_ident(&DeployedUnit::Bundle, "renamed-bundle"),
"renamed-bundle",
"a slot's `name = \"…\"` override has to win — it is what the record carries"
);
assert_eq!(
plan.unit_ident(&DeployedUnit::Component("account".into()), "renamed-bundle"),
"noisetable-account",
);
}
#[test]
fn a_derived_component_ident_is_folded_like_every_other_mesh_ident() {
assert_eq!(
component_workload_ident("Noise_Table", "Account.API"),
"noise-table-account-api"
);
}
#[test]
fn resolved_units_render_a_door_on_the_derived_port() {
let svc = service(
"noisetable",
vec![
component("site", None, DeployTier::Bundle),
component("account", Some("app"), DeployTier::Workload),
],
);
let domains = domains(
"noisetable",
&[(
"/app/*",
&[
("cross-origin-opener-policy", "same-origin"),
("cross-origin-embedder-policy", "require-corp"),
],
)],
);
let plan = plan(&svc, &domains).unwrap().unwrap();
let addresses = plan.resolve_addresses("noisetable", |ident| match ident {
"noisetable" => Some("100.64.0.3:8080".to_string()),
"noisetable-account" => Some("100.64.0.3:14001".to_string()),
_ => None,
});
assert_eq!(addresses.len(), 2);
let workload = plan
.workload(plan.listen_port(), |unit| addresses.get(unit).cloned())
.unwrap();
let spec = workload.container_spec().expect("container-shaped");
let listen = spec
.env
.iter()
.find(|e| e.name == "PASSWAY_LISTEN")
.and_then(|e| match &e.value {
EnvValue::Literal { value } => Some(value.clone()),
_ => None,
})
.expect("a door always declares its listener");
assert_eq!(listen, format!("127.0.0.1:{}", listen_port("noisetable")));
let table = &spec.files[0].content;
assert!(table.contains("100.64.0.3:8080"), "{table}");
assert!(table.contains("100.64.0.3:14001"), "{table}");
assert!(table.contains("cross-origin-embedder-policy"), "{table}");
}
#[test]
fn an_unresolvable_unit_is_absent_rather_than_defaulted() {
let svc = service(
"noisetable",
vec![
component("site", None, DeployTier::Bundle),
component("account", Some("app"), DeployTier::Workload),
],
);
let plan = plan(&svc, &BTreeMap::new()).unwrap().unwrap();
let addresses = plan.resolve_addresses("noisetable", |ident| {
(ident == "noisetable").then(|| "100.64.0.3:8080".to_string())
});
assert_eq!(addresses.len(), 1);
assert!(!addresses.contains_key(&DeployedUnit::Component("account".into())));
assert!(plan
.workload(plan.listen_port(), |unit| addresses.get(unit).cloned())
.is_err());
}
fn placement() -> CdnPlacement {
CdnPlacement {
cdn_base: "https://cdn.test".to_string(),
env: "prod".to_string(),
..Default::default()
}
}
fn compiled(domains: &BTreeMap<String, DomainConfig>) -> RouteTable {
domains["test"]
.route_table(&placement())
.expect("the fixture's routes all resolve")
}
fn mount_path(mount: &str) -> &str {
if mount.is_empty() {
"/"
} else {
mount
}
}
fn seam_fixture() -> (ServiceConfig, BTreeMap<String, DomainConfig>) {
let svc = service(
"noisetable",
vec![
component("site", None, DeployTier::Bundle),
component("account", Some("app"), DeployTier::Workload),
],
);
let domains = domains(
"noisetable",
&[
(
"/app/*",
&[("cross-origin-opener-policy", "same-origin")],
),
("/*", &[("x-frame-options", "DENY")]),
],
);
(svc, domains)
}
#[test]
fn every_mounts_headers_are_the_compiled_route_tables_answer() {
let (svc, domains) = seam_fixture();
let table = compiled(&domains);
let plan = plan(&svc, &domains).unwrap().expect("two units");
for m in &plan.mounts {
let expected = table
.match_path(mount_path(&m.mount))
.map(|e| e.headers.clone())
.unwrap_or_default();
assert_eq!(m.headers, expected, "mount {:?}", m.mount);
}
assert_eq!(
plan.mounts
.iter()
.map(|m| m.headers.keys().cloned().collect::<Vec<_>>())
.collect::<Vec<_>>(),
vec![
vec!["x-frame-options".to_string()],
vec!["cross-origin-opener-policy".to_string()],
]
);
}
#[test]
fn a_header_changed_in_the_manifest_moves_both_tiers_at_once() {
let (svc, mut domains) = seam_fixture();
let before = plan(&svc, &domains).unwrap().unwrap();
assert_eq!(
before.mounts[1]
.headers
.get("cross-origin-opener-policy")
.map(String::as_str),
Some("same-origin")
);
let route = domains
.get_mut("test")
.unwrap()
.routes
.iter_mut()
.find(|r| r.path == "/app/*")
.unwrap();
route.headers.insert(
"cross-origin-embedder-policy".to_string(),
"require-corp".to_string(),
);
let after = plan(&svc, &domains).unwrap().unwrap();
let table = compiled(&domains);
assert_eq!(
after.mounts[1].headers,
table.match_path("/app").unwrap().headers,
"the door's mount and the compiled entry must move together"
);
assert_eq!(
after.mounts[1]
.headers
.get("cross-origin-embedder-policy")
.map(String::as_str),
Some("require-corp")
);
assert_eq!(before.mounts[0].headers, after.mounts[0].headers);
}
#[test]
fn a_catch_all_declared_first_claims_every_mount_at_both_tiers() {
let svc = service(
"noisetable",
vec![
component("site", None, DeployTier::Bundle),
component("account", Some("app"), DeployTier::Workload),
],
);
let domains = domains(
"noisetable",
&[
("/*", &[("x-frame-options", "DENY")]),
("/app/*", &[("cross-origin-opener-policy", "same-origin")]),
],
);
let table = compiled(&domains);
let plan = plan(&svc, &domains).unwrap().unwrap();
assert_eq!(
table.match_path("/app").unwrap().path,
"/*",
"first match wins over the compiled table"
);
assert_eq!(plan.mounts[1].mount, "/app");
assert_eq!(
plan.mounts[1].headers.keys().cloned().collect::<Vec<_>>(),
vec!["x-frame-options".to_string()],
"and the inner door reports the same route's headers, not a second join's"
);
}
}