use crate::proto::udb::core::control::entity::v1 as control_entity_pb;
use control_entity_pb::ResourceType;
pub const ORDERED_RESOURCE_TYPES: &[ResourceType] = &[
ResourceType::BackendTargetDefinition,
ResourceType::NativeServiceEnablement,
ResourceType::MethodSecurityPolicy,
ResourceType::RoutingPolicy,
ResourceType::RlsTenantPolicy,
];
pub fn ordered_resource_types() -> &'static [ResourceType] {
ORDERED_RESOURCE_TYPES
}
pub fn resource_type_to_db(rt: ResourceType) -> &'static str {
match rt {
ResourceType::Unspecified => "RESOURCE_TYPE_UNSPECIFIED",
ResourceType::RoutingPolicy => "RESOURCE_TYPE_ROUTING_POLICY",
ResourceType::MethodSecurityPolicy => "RESOURCE_TYPE_METHOD_SECURITY_POLICY",
ResourceType::RlsTenantPolicy => "RESOURCE_TYPE_RLS_TENANT_POLICY",
ResourceType::NativeServiceEnablement => "RESOURCE_TYPE_NATIVE_SERVICE_ENABLEMENT",
ResourceType::BackendTargetDefinition => "RESOURCE_TYPE_BACKEND_TARGET_DEFINITION",
}
}
pub fn resource_type_from_db(value: &str) -> ResourceType {
match value {
"RESOURCE_TYPE_ROUTING_POLICY" | "ROUTING_POLICY" => ResourceType::RoutingPolicy,
"RESOURCE_TYPE_METHOD_SECURITY_POLICY" | "METHOD_SECURITY_POLICY" => {
ResourceType::MethodSecurityPolicy
}
"RESOURCE_TYPE_RLS_TENANT_POLICY" | "RLS_TENANT_POLICY" => ResourceType::RlsTenantPolicy,
"RESOURCE_TYPE_NATIVE_SERVICE_ENABLEMENT" | "NATIVE_SERVICE_ENABLEMENT" => {
ResourceType::NativeServiceEnablement
}
"RESOURCE_TYPE_BACKEND_TARGET_DEFINITION" | "BACKEND_TARGET_DEFINITION" => {
ResourceType::BackendTargetDefinition
}
_ => ResourceType::Unspecified,
}
}
pub fn resource_type_from_i32(value: i32) -> ResourceType {
ResourceType::try_from(value).unwrap_or(ResourceType::Unspecified)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ResourceModel {
pub resource_id: String,
pub resource_type: String,
pub name: String,
pub tenant_id: String,
pub project_id: String,
pub version: String,
pub content_hash: String,
pub payload_json: String,
pub updated_by: String,
pub updated_at_unix: i64,
}
impl ResourceModel {
pub fn resource_type_enum(&self) -> ResourceType {
resource_type_from_db(&self.resource_type)
}
}
pub fn content_version(payload_json: &str) -> String {
use std::hash::{Hash, Hasher};
let canonical = canonical_json(payload_json);
let mut hasher = std::collections::hash_map::DefaultHasher::new();
canonical.len().hash(&mut hasher);
canonical.hash(&mut hasher);
format!("cp-{:016x}", hasher.finish())
}
pub fn aggregate_version(resources: &[ResourceModel]) -> String {
use std::hash::{Hash, Hasher};
if resources.is_empty() {
return "cp-empty".to_string();
}
let mut entries: Vec<String> = resources
.iter()
.map(|r| {
let hash = if r.content_hash.is_empty() {
content_version(&r.payload_json)
} else {
r.content_hash.clone()
};
format!("{}\u{1f}{}", r.name, hash)
})
.collect();
entries.sort();
let mut hasher = std::collections::hash_map::DefaultHasher::new();
entries.len().hash(&mut hasher);
for entry in &entries {
entry.hash(&mut hasher);
}
format!("cp-world-{}-{:016x}", entries.len(), hasher.finish())
}
fn canonical_json(raw: &str) -> String {
match serde_json::from_str::<serde_json::Value>(raw.trim()) {
Ok(value) => canonicalize(&value).to_string(),
Err(_) => raw.trim().to_string(),
}
}
fn canonicalize(value: &serde_json::Value) -> serde_json::Value {
match value {
serde_json::Value::Object(map) => {
let mut sorted = serde_json::Map::new();
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
for key in keys {
sorted.insert(key.clone(), canonicalize(&map[key]));
}
serde_json::Value::Object(sorted)
}
serde_json::Value::Array(items) => {
serde_json::Value::Array(items.iter().map(canonicalize).collect())
}
other => other.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn model(name: &str, rt: ResourceType, payload: &str) -> ResourceModel {
ResourceModel {
name: name.to_string(),
resource_type: resource_type_to_db(rt).to_string(),
content_hash: content_version(payload),
payload_json: payload.to_string(),
..Default::default()
}
}
#[test]
fn definitions_precede_referencing_policies() {
let order = ordered_resource_types();
let idx = |rt: ResourceType| order.iter().position(|t| *t == rt).unwrap();
assert!(idx(ResourceType::BackendTargetDefinition) < idx(ResourceType::RoutingPolicy));
assert!(idx(ResourceType::BackendTargetDefinition) < idx(ResourceType::RlsTenantPolicy));
assert!(idx(ResourceType::NativeServiceEnablement) < idx(ResourceType::RoutingPolicy));
assert!(idx(ResourceType::NativeServiceEnablement) < idx(ResourceType::RlsTenantPolicy));
}
#[test]
fn ordered_types_are_complete_and_unique() {
let order = ordered_resource_types();
assert_eq!(
order.len(),
5,
"all five real resource types are distributed"
);
let mut seen = std::collections::BTreeSet::new();
for rt in order {
assert!(
seen.insert(*rt),
"no duplicate resource type in the push order"
);
assert_ne!(
*rt,
ResourceType::Unspecified,
"UNSPECIFIED is not distributed"
);
}
}
#[test]
fn content_version_stable_under_reorder_and_changes_on_edit() {
let a = content_version(r#"{"cluster":"pg-primary","weight":10}"#);
let b = content_version(r#"{"weight":10,"cluster":"pg-primary"}"#);
assert_eq!(a, b, "key order must not change the content version");
assert_eq!(
a,
content_version(r#"{"cluster":"pg-primary","weight":10}"#)
);
let c = content_version(r#"{"cluster":"pg-primary","weight":11}"#);
assert_ne!(a, c, "a value edit must change the content version");
}
#[test]
fn content_version_handles_nested_objects() {
let a = content_version(r#"{"a":{"x":1,"y":2},"b":[1,2,3]}"#);
let b = content_version(r#"{"b":[1,2,3],"a":{"y":2,"x":1}}"#);
assert_eq!(a, b, "nested key reorder must hash identically");
let c = content_version(r#"{"a":{"x":1,"y":2},"b":[3,2,1]}"#);
assert_ne!(a, c, "array order is significant");
}
#[test]
fn aggregate_version_is_order_independent_and_change_sensitive() {
let r1 = model(
"pg-primary",
ResourceType::BackendTargetDefinition,
r#"{"host":"a"}"#,
);
let r2 = model(
"pg-replica",
ResourceType::BackendTargetDefinition,
r#"{"host":"b"}"#,
);
let forward = aggregate_version(&[r1.clone(), r2.clone()]);
let reversed = aggregate_version(&[r2.clone(), r1.clone()]);
assert_eq!(
forward, reversed,
"world version must not depend on row order"
);
let r2b = model(
"pg-replica",
ResourceType::BackendTargetDefinition,
r#"{"host":"c"}"#,
);
let changed = aggregate_version(&[r1, r2b]);
assert_ne!(
forward, changed,
"an edited resource must bump the world version"
);
assert_eq!(aggregate_version(&[]), "cp-empty");
}
#[test]
fn db_roundtrip_for_every_type() {
for rt in [
ResourceType::RoutingPolicy,
ResourceType::MethodSecurityPolicy,
ResourceType::RlsTenantPolicy,
ResourceType::NativeServiceEnablement,
ResourceType::BackendTargetDefinition,
] {
assert_eq!(resource_type_from_db(resource_type_to_db(rt)), rt);
}
assert_eq!(
resource_type_from_db("RESOURCE_TYPE_UNSPECIFIED"),
ResourceType::Unspecified
);
assert_eq!(
resource_type_from_db("ROUTING_POLICY"),
ResourceType::RoutingPolicy
);
}
}