use rustlavel_core::Json;
use crate::instance::{Instance, Status, now};
pub fn read_instance(body: &Json) -> Option<Instance> {
let body = body.get("instance").unwrap_or(body);
let app = body.get("app").and_then(Json::as_str)?.to_ascii_uppercase();
let host = body
.get("hostName")
.and_then(Json::as_str)
.or_else(|| body.get("ipAddr").and_then(Json::as_str))?
.to_string();
let port = port_of(body, "port").unwrap_or(80);
let secure_port = port_of(body, "securePort");
let secure = secure_port.is_some();
let mut instance = Instance::new(app, host, secure_port.unwrap_or(port)).secure(secure);
if let Some(id) = body.get("instanceId").and_then(Json::as_str) {
instance = instance.id(id);
}
instance.status = body
.get("status")
.and_then(Json::as_str)
.map(Status::parse)
.unwrap_or(Status::Starting);
if let Some(lease) = body.get("leaseInfo")
&& let Some(interval) = lease.get("renewalIntervalInSecs").and_then(Json::as_f64)
{
instance = instance.renew_every(interval as u64);
}
if let Some(Json::Object(entries)) = body.get("metadata") {
for (key, value) in entries {
if let Some(text) = value.as_str() {
instance = instance.meta(key.clone(), text);
}
}
}
Some(instance)
}
fn port_of(body: &Json, field: &str) -> Option<u16> {
let value = body.get(field)?;
if let Some(number) = value.as_f64() {
return Some(number as u16);
}
let enabled = value
.get("@enabled")
.and_then(|flag| flag.as_str().map(|text| text == "true").or_else(|| flag.as_bool()))
.unwrap_or(true);
if !enabled {
return None;
}
value.get("$").and_then(Json::as_f64).map(|number| number as u16)
}
pub fn instance_json(instance: &Instance) -> Json {
let (port, secure_port) = match instance.secure {
true => (80, instance.port),
false => (instance.port, 443),
};
Json::object([
("instanceId", Json::from(instance.id.as_str())),
("hostName", Json::from(instance.host.as_str())),
("app", Json::from(instance.service.as_str())),
("ipAddr", Json::from(instance.host.as_str())),
("status", Json::from(instance.status.as_str())),
("overriddenStatus", Json::from("UNKNOWN")),
("port", enabled_port(port, !instance.secure)),
("securePort", enabled_port(secure_port, instance.secure)),
("countryId", Json::from(1.0)),
(
"dataCenterInfo",
Json::object([
("@class", Json::from("com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo")),
("name", Json::from("MyOwn")),
]),
),
(
"leaseInfo",
Json::object([
("renewalIntervalInSecs", Json::from(instance.renew_interval as f64)),
("durationInSecs", Json::from((instance.renew_interval * 3) as f64)),
("lastRenewalTimestamp", Json::from((instance.last_seen * 1000) as f64)),
]),
),
(
"metadata",
Json::object(
instance
.metadata
.iter()
.map(|(key, value)| (key.as_str(), Json::from(value.as_str())))
.collect::<Vec<_>>(),
),
),
("homePageUrl", Json::from(format!("{}/", instance.url()).as_str())),
("statusPageUrl", Json::from(format!("{}/info", instance.url()).as_str())),
("healthCheckUrl", Json::from(format!("{}/health", instance.url()).as_str())),
("vipAddress", Json::from(instance.service.as_str())),
("secureVipAddress", Json::from(instance.service.as_str())),
("lastUpdatedTimestamp", Json::from((now() * 1000) as f64)),
])
}
fn enabled_port(port: u16, enabled: bool) -> Json {
Json::object([
("$", Json::from(port as f64)),
("@enabled", Json::from(if enabled { "true" } else { "false" })),
])
}
pub fn applications_json(instances: &[Instance]) -> Json {
let mut names: Vec<&str> = instances.iter().map(|i| i.service.as_str()).collect();
names.sort_unstable();
names.dedup();
let applications: Vec<Json> = names
.iter()
.map(|name| {
let mine: Vec<Json> = instances
.iter()
.filter(|instance| instance.service == *name)
.map(instance_json)
.collect();
Json::object([("name", Json::from(*name)), ("instance", Json::Array(mine))])
})
.collect();
Json::object([(
"applications",
Json::object([
("versions__delta", Json::from("1")),
("apps__hashcode", Json::from(hashcode(instances).as_str())),
("application", Json::Array(applications)),
]),
)])
}
pub fn application_json(name: &str, instances: &[Instance]) -> Json {
Json::object([(
"application",
Json::object([
("name", Json::from(name)),
("instance", Json::Array(instances.iter().map(instance_json).collect())),
]),
)])
}
fn hashcode(instances: &[Instance]) -> String {
let mut counts: Vec<(&str, usize)> = Vec::new();
for instance in instances {
let status = instance.status.as_str();
match counts.iter_mut().find(|(held, _)| *held == status) {
Some((_, count)) => *count += 1,
None => counts.push((status, 1)),
}
}
counts.sort_unstable();
counts.iter().map(|(status, count)| format!("{status}_{count}_")).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn spring_registration() -> Json {
Json::parse(
r#"{"instance":{
"instanceId":"orders:8761",
"hostName":"10.0.0.7",
"app":"ORDERS",
"ipAddr":"10.0.0.7",
"status":"UP",
"port":{"$":8080,"@enabled":"true"},
"securePort":{"$":443,"@enabled":"false"},
"dataCenterInfo":{"@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo","name":"MyOwn"},
"leaseInfo":{"renewalIntervalInSecs":10,"durationInSecs":30},
"metadata":{"zone":"jakarta-a"}
}}"#,
)
.unwrap()
}
#[test]
fn a_spring_registration_is_read() {
let instance = read_instance(&spring_registration()).expect("a readable registration");
assert_eq!(instance.service, "ORDERS");
assert_eq!(instance.id, "orders:8761");
assert_eq!(instance.host, "10.0.0.7");
assert_eq!(instance.port, 8080);
assert_eq!(instance.status, Status::Up);
assert_eq!(instance.renew_interval, 10);
assert_eq!(instance.zone(), Some("jakarta-a"));
assert!(!instance.secure, "`@enabled: false` was read as a usable TLS port");
}
#[test]
fn the_wrapper_is_optional() {
let inner = spring_registration().get("instance").cloned().unwrap();
assert_eq!(read_instance(&inner).unwrap().id, "orders:8761");
}
#[test]
fn a_registration_with_no_status_is_starting_rather_than_up() {
let body = Json::parse(r#"{"app":"orders","hostName":"h","port":{"$":80,"@enabled":"true"}}"#).unwrap();
assert_eq!(read_instance(&body).unwrap().status, Status::Starting);
}
#[test]
fn a_disabled_port_is_absent_rather_than_usable() {
let body = Json::parse(
r#"{"app":"a","hostName":"h","port":{"$":8080,"@enabled":"true"},"securePort":{"$":8443,"@enabled":"false"}}"#,
)
.unwrap();
let instance = read_instance(&body).unwrap();
assert!(!instance.secure);
assert_eq!(instance.port, 8080);
}
#[test]
fn a_registration_without_a_name_or_a_host_is_refused() {
for body in [r#"{"hostName":"h"}"#, r#"{"app":"a"}"#, "{}"] {
assert!(read_instance(&Json::parse(body).unwrap()).is_none(), "{body}");
}
}
#[test]
fn the_rendered_instance_carries_the_fields_clients_check() {
let rendered = instance_json(&Instance::new("orders", "10.0.0.7", 8080));
let text = rendered.to_string();
assert!(text.contains("com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo"), "{text}");
assert!(text.contains(r#""@enabled":"true""#), "@enabled must be a string: {text}");
assert!(text.contains(r#""app":"ORDERS""#), "{text}");
assert!(text.contains(r#""vipAddress":"ORDERS""#), "{text}");
}
#[test]
fn a_registration_survives_a_round_trip() {
let original = read_instance(&spring_registration()).unwrap();
let round_tripped = read_instance(&instance_json(&original)).unwrap();
assert_eq!(round_tripped.service, original.service);
assert_eq!(round_tripped.id, original.id);
assert_eq!(round_tripped.host, original.host);
assert_eq!(round_tripped.port, original.port);
assert_eq!(round_tripped.status, original.status);
assert_eq!(round_tripped.renew_interval, original.renew_interval);
assert_eq!(round_tripped.zone(), original.zone());
}
#[test]
fn the_applications_document_groups_instances_by_name() {
let instances = vec![
Instance::new("orders", "10.0.0.1", 8080),
Instance::new("orders", "10.0.0.2", 8080),
Instance::new("billing", "10.0.0.3", 8080),
];
let text = applications_json(&instances).to_string();
assert!(text.contains(r#""name":"ORDERS""#), "{text}");
assert!(text.contains(r#""name":"BILLING""#), "{text}");
assert!(text.contains("apps__hashcode"), "{text}");
}
#[test]
fn the_hashcode_follows_the_registry() {
let up = vec![Instance::new("orders", "10.0.0.1", 8080)];
let mut down = up.clone();
down[0].status = Status::Down;
assert_eq!(hashcode(&up), "UP_1_");
assert_ne!(hashcode(&up), hashcode(&down));
assert_eq!(hashcode(&[]), "");
}
}