use manta_backend_dispatcher::error::Error;
use manta_backend_dispatcher::interfaces::bss::BootParametersTrait;
use manta_backend_dispatcher::interfaces::ims::ImsTrait;
use manta_backend_dispatcher::types::Group;
use manta_backend_dispatcher::types::bss::BootParameters;
use manta_backend_dispatcher::types::ims::Image;
use crate::server::common::app_context::InfraContext;
use crate::service::boot_parameters::get_restricted_boot_parameters;
pub use manta_shared::types::api::image::GetImagesParams;
pub async fn get_images(
infra: &InfraContext<'_>,
token: &str,
params: &GetImagesParams,
) -> Result<Vec<Image>, Error> {
let mut image_vec = infra
.backend
.get_images(token, params.id.as_deref())
.await?;
image_vec = apply_pattern_filter(image_vec, params.pattern.as_deref())?;
if let Some(limit) = params.limit {
image_vec.truncate(limit as usize);
}
image_vec.sort_by_key(|image| image.created.clone());
Ok(image_vec)
}
fn apply_pattern_filter(
image_vec: Vec<Image>,
pattern: Option<&str>,
) -> Result<Vec<Image>, Error> {
let Some(pattern) = pattern else {
return Ok(image_vec);
};
let matcher = globset::Glob::new(pattern)
.map_err(|e| {
Error::BadRequest(format!("invalid glob pattern '{pattern}': {e}"))
})?
.compile_matcher();
Ok(
image_vec
.into_iter()
.filter(|img| matcher.is_match(&img.name))
.collect(),
)
}
pub async fn validate_image_deletion(
infra: &InfraContext<'_>,
token: &str,
image_id_vec: &[&str],
settings_group_name_opt: Option<&str>,
) -> Result<(), Error> {
let (group_available_vec, _target_group_vec) =
crate::service::group::resolve_target_and_available_groups(
infra,
token,
settings_group_name_opt,
)
.await?;
let boot_parameter_vec = infra.backend.get_all_bootparameters(token).await?;
let image_used_to_boot_nodes: Vec<String> = boot_parameter_vec
.iter()
.map(manta_backend_dispatcher::types::bss::BootParameters::try_get_boot_image_id)
.collect::<Option<Vec<String>>>()
.ok_or_else(|| {
Error::MissingField(
"Could not get image ids used to boot nodes".to_string(),
)
})?;
let image_used_to_boot_nodes_set: std::collections::HashSet<&str> =
image_used_to_boot_nodes
.iter()
.map(String::as_str)
.collect();
let image_xnames_boot_map: Vec<&&str> = image_id_vec
.iter()
.filter(|id| image_used_to_boot_nodes_set.contains(**id))
.collect();
if !image_xnames_boot_map.is_empty() {
return Err(Error::BadRequest(format!(
"The following images could not be deleted \
since they boot nodes.\n{}",
image_xnames_boot_map
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)));
}
let image_restricted_vec =
get_restricted_image_ids(&group_available_vec, &boot_parameter_vec)
.ok_or_else(|| {
Error::MissingField(
"Could not get restricted image ids used by boot parameters"
.to_string(),
)
})?;
if !image_restricted_vec.is_empty() {
return Err(Error::BadRequest(format!(
"The following image ids can't be deleted \
because they are used by hosts that are not part \
of the groups available to the user:\n{}",
image_restricted_vec.join(", ")
)));
}
Ok(())
}
pub async fn delete_images(
infra: &InfraContext<'_>,
token: &str,
image_id_vec: &[&str],
settings_hsm_group_name_opt: Option<&str>,
) -> Result<Vec<String>, Error> {
validate_image_deletion(
infra,
token,
image_id_vec,
settings_hsm_group_name_opt,
)
.await?;
let mut deleted = Vec::new();
for image_id in image_id_vec {
match infra.backend.delete_image(token, image_id).await {
Ok(()) => {
tracing::info!("Image {} deleted successfully", image_id);
deleted.push((*image_id).to_string());
}
Err(e) => tracing::error!(
"Failed to delete image {}: {}. Continuing",
image_id,
e
),
}
}
Ok(deleted)
}
fn get_restricted_image_ids(
group_available_vec: &[Group],
boot_parameter_vec: &[BootParameters],
) -> Option<Vec<String>> {
get_restricted_boot_parameters(group_available_vec, boot_parameter_vec)
.iter()
.map(manta_backend_dispatcher::types::bss::BootParameters::try_get_boot_image_id)
.collect()
}
#[cfg(test)]
mod tests {
use super::apply_pattern_filter;
use manta_backend_dispatcher::error::Error;
use manta_backend_dispatcher::types::ims::Image;
fn image(name: &str) -> Image {
Image {
name: name.to_string(),
..Default::default()
}
}
#[test]
fn no_pattern_returns_all_images_unchanged() {
let input = vec![image("a"), image("b"), image("c")];
let out = apply_pattern_filter(input.clone(), None).expect("None is no-op");
assert_eq!(out.len(), 3);
assert_eq!(out[0].name, "a");
assert_eq!(out[2].name, "c");
}
#[test]
fn star_glob_matches_everything() {
let input = vec![image("compute-a"), image("login-b")];
let out = apply_pattern_filter(input, Some("*")).expect("'*' is valid");
assert_eq!(out.len(), 2);
}
#[test]
fn prefix_star_keeps_only_matching_subset() {
let input = vec![
image("compute-a"),
image("compute-b"),
image("login-a"),
image("storage-3"),
];
let out = apply_pattern_filter(input, Some("compute-*"))
.expect("'compute-*' valid");
assert_eq!(out.len(), 2);
assert!(out.iter().all(|i| i.name.starts_with("compute-")));
}
#[test]
fn pattern_with_no_matches_returns_empty() {
let input = vec![image("compute-a"), image("login-b")];
let out = apply_pattern_filter(input, Some("nomatch-*"))
.expect("'nomatch-*' is valid even when nothing matches");
assert!(out.is_empty());
}
#[test]
fn invalid_glob_returns_bad_request() {
let input = vec![image("anything")];
let err = apply_pattern_filter(input, Some("[unclosed"))
.expect_err("'[unclosed' is malformed");
match err {
Error::BadRequest(msg) => {
assert!(
msg.contains("invalid glob pattern"),
"error message should explain the glob is bad; got: {msg}"
);
assert!(
msg.contains("'[unclosed'"),
"error should quote the offending pattern; got: {msg}"
);
}
other => panic!("expected BadRequest, got {other:?}"),
}
}
#[test]
fn question_mark_matches_single_char() {
let input = vec![
image("a"), image("ab"), image("abc"), image("abcd"), ];
let out = apply_pattern_filter(input, Some("a??")).expect("'a??' is valid");
assert_eq!(out.len(), 1);
assert_eq!(out[0].name, "abc");
}
#[test]
fn character_class_matches_any_listed_char() {
let input = vec![
image("compute-a"),
image("compute-b"),
image("compute-c"),
image("compute-d"),
];
let out =
apply_pattern_filter(input, Some("compute-[abc]")).expect("class valid");
assert_eq!(out.len(), 3);
assert!(!out.iter().any(|i| i.name == "compute-d"));
}
}