use std::sync::Arc;
use rustlavel_core::Json;
use rustlavel_http::{BoxFuture, Plugin, Request, Response, Setup, Status};
use crate::eureka;
use crate::instance::Status as InstanceStatus;
use crate::registry::{Mode, Registry};
use crate::replicate::{Change, Peers, REPLICATION_HEADER, is_replication};
pub struct RegistryServer {
registry: Registry,
peers: Peers,
dashboard: Option<String>,
}
impl Default for RegistryServer {
fn default() -> Self {
RegistryServer::new()
}
}
impl RegistryServer {
pub fn new() -> RegistryServer {
RegistryServer {
registry: Registry::new(),
peers: Peers::new(Vec::new()),
dashboard: Some("/discovery".to_string()),
}
}
pub fn using(mut self, registry: Registry) -> RegistryServer {
self.registry = registry;
self
}
pub fn registry(&self) -> &Registry {
&self.registry
}
pub fn peers(mut self, peers: Peers) -> RegistryServer {
self.peers = peers;
self
}
pub fn dashboard(mut self, at: Option<&str>) -> RegistryServer {
self.dashboard = at.map(str::to_string);
self
}
}
fn arrived_from_a_client(request: &Request) -> bool {
!is_replication(request.query("isReplication"), request.header(REPLICATION_HEADER))
}
impl Plugin for RegistryServer {
fn name(&self) -> &'static str {
"discovery"
}
fn register(self: Box<Self>, setup: &mut Setup<'_>) {
let this = Arc::new(*self);
setup.state(this.registry.clone());
let owner = Arc::clone(&this);
setup.router.post("/eureka/apps/{app}", move |mut request: Request| {
let owner = Arc::clone(&owner);
Box::pin(async move {
let Some(body) = request.json().cloned() else {
return Response::new(Status::BAD_REQUEST)
.with_json(problem("a registration must be a JSON body"));
};
let Some(instance) = eureka::read_instance(&body) else {
return Response::new(Status::BAD_REQUEST)
.with_json(problem("a registration needs an app and a host"));
};
owner.registry.register(instance.clone());
if arrived_from_a_client(&request) {
owner.peers.spread(Change::Registered(instance));
}
Response::no_content()
}) as BoxFuture<Response>
});
let owner = Arc::clone(&this);
setup.router.put("/eureka/apps/{app}/{id}", move |request: Request| {
let owner = Arc::clone(&owner);
Box::pin(async move {
let (app, id) = named(&request);
if !owner.registry.renew(&app, &id) {
return Response::not_found();
}
if arrived_from_a_client(&request) {
owner.peers.spread(Change::Renewed { service: app, id });
}
Response::ok()
}) as BoxFuture<Response>
});
let owner = Arc::clone(&this);
setup.router.put("/eureka/apps/{app}/{id}/status", move |request: Request| {
let owner = Arc::clone(&owner);
Box::pin(async move {
let (app, id) = named(&request);
let status = InstanceStatus::parse(request.query("value").unwrap_or(""));
if !owner.registry.set_status(&app, &id, status) {
return Response::not_found();
}
if arrived_from_a_client(&request) {
owner.peers.spread(Change::StatusChanged { service: app, id, status });
}
Response::ok()
}) as BoxFuture<Response>
});
let owner = Arc::clone(&this);
setup.router.delete("/eureka/apps/{app}/{id}", move |request: Request| {
let owner = Arc::clone(&owner);
Box::pin(async move {
let (app, id) = named(&request);
let removed = owner.registry.cancel(&app, &id);
if arrived_from_a_client(&request) {
owner.peers.spread(Change::Cancelled { service: app, id });
}
match removed {
true => Response::ok(),
false => Response::not_found(),
}
}) as BoxFuture<Response>
});
let owner = Arc::clone(&this);
setup.router.get("/eureka/apps", move |_request: Request| {
let owner = Arc::clone(&owner);
Box::pin(async move {
Response::json(eureka::applications_json(&owner.registry.all()))
}) as BoxFuture<Response>
});
let owner = Arc::clone(&this);
setup.router.get("/eureka/apps/{app}", move |request: Request| {
let owner = Arc::clone(&owner);
Box::pin(async move {
let app = request.param("app").unwrap_or("").to_ascii_uppercase();
let (instances, mode) = owner.registry.alive(&app);
if instances.is_empty() {
return Response::not_found();
}
let mut response = Response::json(eureka::application_json(&app, &instances));
if mode == Mode::SelfPreservation {
response = response.with_header("x-discovery-self-preservation", "true");
}
response
}) as BoxFuture<Response>
});
let owner = Arc::clone(&this);
setup.router.get("/discovery/registry.json", move |_request: Request| {
let owner = Arc::clone(&owner);
Box::pin(async move { Response::json(crate::ui::registry_json(&owner.registry)) })
as BoxFuture<Response>
});
if let Some(at) = this.dashboard.clone() {
let owner = Arc::clone(&this);
setup.router.get(&at, move |_request: Request| {
let owner = Arc::clone(&owner);
Box::pin(async move { Response::html(crate::ui::page(&owner.registry)) })
as BoxFuture<Response>
});
}
}
}
fn named(request: &Request) -> (String, String) {
(
request.param("app").unwrap_or("").to_ascii_uppercase(),
request.param("id").unwrap_or("").to_string(),
)
}
fn problem(message: &str) -> Json {
Json::object([("message", Json::from(message))])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::instance::Instance;
#[test]
fn the_dashboard_can_be_taken_away() {
assert_eq!(RegistryServer::new().dashboard.as_deref(), Some("/discovery"));
assert!(RegistryServer::new().dashboard(None).dashboard.is_none());
assert_eq!(
RegistryServer::new().dashboard(Some("/ops/discovery")).dashboard.as_deref(),
Some("/ops/discovery")
);
}
#[test]
fn a_registry_can_be_shared_with_the_rest_of_the_application() {
let registry = Registry::new();
let server = RegistryServer::new().using(registry.clone());
registry.register(Instance::new("orders", "10.0.0.1", 8080));
assert_eq!(server.registry().all().len(), 1);
}
}