use std::collections::BTreeMap;
use engenho_apiserver::{defaulting, schema_validation, validation};
use engenho_controllers::node_lease::LEASE_NAMESPACE;
use engenho_controllers::{
CrdController, CrdHandlerSpec, NormalizedTemplate, ObjectMeta as _, REPLICAS,
current_replicaset, is_owned_by,
};
use engenho_scheduler::{
FilterPlugin, Headroom, NodeLedger, ObservedNode, holds_capacity, pod_requests,
};
use engenho_store::{ImageInconsistency, ImageTripwire, ResourceKey};
use serde_json::Value;
use super::{Builtin, Finding, Gvk, Match, Subject, text_at};
pub(super) fn node_overcommitted(nodes: &[Value], pods: &[Value]) -> Vec<Match> {
let gvk = Builtin::Node.gvk();
NodeLedger::seed(nodes, pods)
.headroom()
.filter(Headroom::is_overcommitted)
.map(|row| Match {
subject: Subject::Object {
gvk: gvk.clone(),
namespace: None,
name: Some(row.node.to_owned()),
},
finding: Finding::Overcommitted {
cpu_milli: row.cpu_milli,
mem_milli: row.mem_milli,
},
})
.collect()
}
pub(super) fn bound_pod_rejected(
nodes: &[Value],
pods: &[Value],
leases: &[Value],
now: &str,
) -> Vec<Match> {
let pod_gvk = Builtin::Pod.gvk();
let mut found = Vec::new();
for (i, pod) in pods.iter().enumerate() {
let Some(node_name) = text_at(pod, "/spec/nodeName").filter(|n| !n.is_empty()) else {
continue;
};
if !holds_capacity(pod).holds() {
continue;
}
let subject = Subject::object(&pod_gvk, pod);
let Some(node) = nodes
.iter()
.find(|n| text_at(n, "/metadata/name") == Some(node_name))
else {
found.push(Match {
subject,
finding: Finding::NodeNotObserved {
node: node_name.to_owned(),
},
});
continue;
};
let lease = leases.iter().find(|l| {
text_at(l, "/metadata/namespace") == Some(LEASE_NAMESPACE)
&& text_at(l, "/metadata/name") == Some(node_name)
});
let observed = ObservedNode::project(node.clone(), lease, now);
let others = pods
.iter()
.enumerate()
.filter(|(j, _)| *j != i)
.map(|(_, p)| p);
let ledger = NodeLedger::seed(std::iter::once(node), others);
let req = pod_requests(pod);
for plugin in FilterPlugin::ALL {
if let Err(rejection) = plugin.check(pod, &req, &observed, &ledger) {
found.push(Match {
subject: subject.clone(),
finding: Finding::Rejected(rejection),
});
}
}
}
found
}
pub(super) fn deployment_would_roll(deployments: &[Value], replicasets: &[Value]) -> Vec<Match> {
let gvk = Builtin::Deployment.gvk();
let mut found = Vec::new();
for d in deployments {
let Some(uid) = d.uid() else { continue };
if REPLICAS.read(d).is_err() {
continue;
}
let Some(template) = NormalizedTemplate::of_spec_template(d) else {
continue;
};
let namespace = d.namespace();
let owned: Vec<(ResourceKey, Value)> = replicasets
.iter()
.filter(|rs| rs.namespace() == namespace && is_owned_by(rs, uid))
.map(|rs| {
(
ResourceKey::namespaced(
"apps",
"v1",
"ReplicaSet",
namespace.unwrap_or_default(),
rs.name().unwrap_or_default(),
),
rs.clone(),
)
})
.collect();
if current_replicaset(&template, &owned).is_none() {
found.push(Match {
subject: Subject::object(&gvk, d),
finding: Finding::NoReplicaSetRunsTemplate,
});
}
}
found
}
pub(super) fn crd_schemas(crds: &[Value]) -> BTreeMap<Gvk, Value> {
crds.iter()
.flat_map(CrdController::extract_entries)
.map(|entry| CrdHandlerSpec::from(&entry))
.filter(|spec| spec.schema.is_object())
.map(|spec| {
(
Gvk::new(&spec.group, &spec.version, &spec.kind),
spec.schema,
)
})
.collect()
}
pub(super) fn would_reject(gvk: &Gvk, objects: &[Value], schema: Option<&Value>) -> Vec<Match> {
let mut found = Vec::new();
for object in objects {
let mut body = object.clone();
defaulting::apply(&gvk.group, &gvk.version, &gvk.kind, &mut body);
for violation in validation::validate(&gvk.group, &gvk.version, &gvk.kind, &body) {
found.push(Match {
subject: Subject::object(gvk, object),
finding: Finding::Invalid(violation),
});
}
if let Some(schema) = schema {
schema_validation::apply_defaults(schema, &mut body);
for violation in schema_validation::validate(schema, &body) {
found.push(Match {
subject: Subject::object(gvk, object),
finding: Finding::SchemaInvalid(violation),
});
}
}
}
found
}
pub(super) fn image_inconsistent(tripwire: &ImageTripwire) -> Vec<Match> {
ImageInconsistency::ALL
.iter()
.filter_map(|&kind| {
let hits = tripwire.count(kind);
(hits > 0).then_some(Match {
subject: Subject::StoreImage,
finding: Finding::ImageInconsistent { kind, hits },
})
})
.collect()
}