use std::collections::HashSet;
use kanade_shared::manifest::{Manifest, Target, Tier};
use crate::api::AppState;
pub fn requires_controller(m: &Manifest) -> bool {
matches!(m.tier, Some(Tier::Controller)) || !m.feed.is_empty()
}
pub async fn member_set(s: &AppState) -> anyhow::Result<Option<HashSet<String>>> {
let settings = crate::api::server_settings::load(s).await?;
let Some(group) = settings.effective_controller_group() else {
return Ok(None);
};
let members = crate::scheduler::resolve_roster(
s,
&Target {
all: false,
groups: vec![group.to_string()],
pcs: Vec::new(),
},
true,
)
.await?;
Ok(Some(members.into_iter().collect()))
}
#[derive(Debug, PartialEq, Eq)]
pub enum Constrained {
Pcs(Vec<String>),
GroupUnset,
NoMember,
}
pub fn constrain(requested: Vec<String>, members: &Option<HashSet<String>>) -> Constrained {
match members {
None => Constrained::GroupUnset,
Some(m) => {
let pcs: Vec<String> = requested.into_iter().filter(|p| m.contains(p)).collect();
if pcs.is_empty() {
Constrained::NoMember
} else {
Constrained::Pcs(pcs)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn set(items: &[&str]) -> HashSet<String> {
items.iter().map(|s| s.to_string()).collect()
}
fn manifest(extra: &str) -> Manifest {
serde_yaml::from_str(&format!(
"id: j\nversion: 0.0.1\nexecute:\n shell: powershell\n script: x\n timeout: 30s\n{extra}"
))
.expect("manifest parse")
}
#[test]
fn requires_controller_for_tier_and_feed() {
assert!(!requires_controller(&manifest("")));
assert!(requires_controller(&manifest("tier: controller\n")));
assert!(requires_controller(&manifest(
"feed:\n - id: cisa-kev\n field: vulnerabilities\n primary_key: [cveID]\n"
)));
}
#[test]
fn unset_group_fails_closed() {
assert_eq!(
constrain(vec!["minipc".into()], &None),
Constrained::GroupUnset
);
}
#[test]
fn intersects_requested_with_members() {
let members = Some(set(&["minipc", "dmz1"]));
let out = constrain(
vec!["minipc".into(), "laptop7".into(), "dmz1".into()],
&members,
);
match out {
Constrained::Pcs(mut pcs) => {
pcs.sort();
assert_eq!(pcs, vec!["dmz1".to_string(), "minipc".to_string()]);
}
other => panic!("expected Pcs, got {other:?}"),
}
}
#[test]
fn no_overlap_is_no_member() {
let members = Some(set(&["minipc"]));
assert_eq!(
constrain(vec!["laptop7".into()], &members),
Constrained::NoMember
);
}
#[test]
fn empty_member_set_is_no_member() {
assert_eq!(
constrain(vec!["minipc".into()], &Some(HashSet::new())),
Constrained::NoMember
);
}
}