use std::collections::{HashMap, HashSet};
use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use super::bridge::{BridgeHandle, BridgeScope, IngestBridge};
use super::registry::{InstanceBinding, TokenRegistry};
use super::types::{INGEST_PATH, IngestError};
use crate::workload_runtime::HostContract;
pub trait ProjectDirectory: Send + Sync {
fn bound_node(&self, project_id: &str) -> Option<String>;
}
#[derive(Clone)]
pub struct IngestHooks {
own_node: String,
registry: Arc<TokenRegistry>,
bridge: Option<Arc<IngestBridge>>,
url: String,
scoped_bind: Option<IpAddr>,
reason: Option<String>,
controllers: HashMap<String, HashSet<String>>,
directory: Option<Arc<dyn ProjectDirectory>>,
}
impl IngestHooks {
pub fn new(
own_node: impl Into<String>,
registry: Arc<TokenRegistry>,
bridge: Arc<IngestBridge>,
shared: SocketAddr,
scoped_bind: Option<IpAddr>,
) -> Self {
Self {
own_node: own_node.into(),
registry,
bridge: Some(bridge),
url: format!("http://{shared}{INGEST_PATH}"),
scoped_bind,
reason: None,
controllers: HashMap::new(),
directory: None,
}
}
pub fn disabled(own_node: impl Into<String>, reason: impl Into<String>) -> Self {
Self {
own_node: own_node.into(),
registry: Arc::new(TokenRegistry::new()),
bridge: None,
url: String::new(),
scoped_bind: None,
reason: Some(reason.into()),
controllers: HashMap::new(),
directory: None,
}
}
pub fn with_project_controllers(mut self, c: HashMap<String, HashSet<String>>) -> Self {
self.controllers = c;
self
}
pub fn with_project_directory(mut self, d: Arc<dyn ProjectDirectory>) -> Self {
self.directory = Some(d);
self
}
pub fn is_enabled(&self) -> bool {
self.bridge.is_some()
}
pub fn state(&self) -> &'static str {
if self.is_enabled() { "enabled" } else { "disabled" }
}
pub fn disabled_reason(&self) -> Option<&str> {
self.reason.as_deref()
}
pub fn registry(&self) -> &Arc<TokenRegistry> {
&self.registry
}
pub fn authorize(&self, b: &InstanceBinding) -> Result<(), IngestError> {
if let Some(p) = &b.project_id {
let me = b.controller_node == self.own_node;
let listed = self
.controllers
.get(p)
.is_some_and(|s| s.contains(&b.controller_node));
let bound = self
.directory
.as_ref()
.and_then(|d| d.bound_node(p))
.is_some_and(|n| n == b.controller_node);
if !(me || listed || bound) {
return Err(IngestError::Forbidden);
}
}
let Some(bridge) = &self.bridge else {
return Ok(());
};
if !bridge.has_route(b) {
return Err(IngestError::NotRouted(match &b.project_id {
Some(p) => format!("project {p} is not routed here"),
None => format!("no store route for controller {}", b.controller_node),
}));
}
Ok(())
}
pub async fn prepare(
&self,
route: &str,
contract: HostContract,
) -> Result<(HostContract, Option<BridgeHandle>), IngestError> {
let Some(bridge) = &self.bridge else {
return Ok((contract, None));
};
let Some(ip) = self.scoped_bind.filter(|_| route != "native") else {
return Ok((contract.with_ingest_url(self.url.clone()), None));
};
let h = *blake3::hash(contract.token.expose().as_bytes()).as_bytes();
let handle = bridge
.bind(SocketAddr::new(ip, 0), BridgeScope::Token(h))
.await?;
let addr = handle.addr();
let contract = contract
.with_ingest_upstream(addr)
.with_ingest_url(format!("http://{addr}{INGEST_PATH}"));
Ok((contract, Some(handle)))
}
pub fn lease(
&self,
binding: InstanceBinding,
contract: HostContract,
listener: Option<BridgeHandle>,
) -> Result<IngestLease, IngestError> {
self.registry.register(binding.clone(), &contract)?;
Ok(IngestLease {
binding,
contract,
_listener: listener,
})
}
pub fn activate(&self, l: &IngestLease) -> Result<(), IngestError> {
if self.registry.contains(&l.binding.instance_id) {
return Ok(());
}
self.registry.register(l.binding.clone(), &l.contract)
}
pub fn deactivate(&self, l: &IngestLease) {
self.registry.revoke(&l.binding.instance_id);
if let Some(b) = &self.bridge {
b.forget(&l.binding.instance_id);
}
}
}
pub struct IngestLease {
pub binding: InstanceBinding,
contract: HostContract,
_listener: Option<BridgeHandle>,
}