use crate::text::strip_prefix_ignore_ascii_case;
use crate::{StackLabel, labels};
pub const EXACT_IMAGE_RULES: &[(&str, StackLabel)] = &[
("mongo", labels::MONGODB),
("httpd", labels::APACHE),
("node", labels::NODE),
("python", labels::PYTHON),
("python3", labels::PYTHON),
("ruby", labels::RUBY),
("golang", labels::GO),
("go", labels::GO),
("rust", labels::RUST),
("bun", labels::BUN),
("deno", labels::DENO),
("php", labels::PHP),
("elixir", labels::ELIXIR),
];
pub const PREFIX_IMAGE_RULES: &[(&str, StackLabel)] = &[
("postgres", labels::POSTGRESQL),
("postgresql", labels::POSTGRESQL),
("postgis", labels::POSTGRESQL),
("timescaledb", labels::POSTGRESQL),
("pgvector", labels::POSTGRESQL),
("mysql", labels::MYSQL),
("mariadb", labels::MARIADB),
("mongodb", labels::MONGODB),
("redis", labels::REDIS),
("valkey", labels::VALKEY),
("memcached", labels::MEMCACHED),
("nginx", labels::NGINX),
("apache", labels::APACHE),
("rabbitmq", labels::RABBITMQ),
("kafka", labels::KAFKA),
("cp-kafka", labels::KAFKA),
("localstack", labels::LOCALSTACK),
("elasticsearch", labels::ELASTICSEARCH),
("opensearch", labels::OPENSEARCH),
("clickhouse", labels::CLICKHOUSE),
("caddy", labels::CADDY),
("traefik", labels::TRAEFIK),
("openjdk", labels::JAVA),
("eclipse-temurin", labels::JAVA),
("amazoncorretto", labels::JAVA),
("dotnet", labels::DOTNET),
];
const COMPANION_SEGMENTS: &[&str] = &[
"exporter",
"dashboard",
"dashboards",
"operator",
"admin",
"ui",
"gui",
"backup",
"client",
"cli",
"commander",
"workbench",
"shell",
"curator",
"auth",
"connect",
];
const NAMESPACE_IMAGE_RULES: &[(&str, StackLabel)] =
&[("dotnet", labels::DOTNET), ("mssql", labels::SQL_SERVER)];
#[must_use]
pub fn detect_from_image(image: &str) -> Option<StackLabel> {
let last_segment = image.rsplit('/').next().unwrap_or(image);
let base = last_segment
.split([':', '@'])
.next()
.unwrap_or(last_segment);
detect_exact_base(base)
.or_else(|| detect_prefixed_base(base))
.or_else(|| detect_namespace(image))
}
fn detect_exact_base(base: &str) -> Option<StackLabel> {
EXACT_IMAGE_RULES
.iter()
.find(|(name, _)| base.eq_ignore_ascii_case(name))
.map(|(_, label)| label.clone())
}
fn detect_prefixed_base(base: &str) -> Option<StackLabel> {
PREFIX_IMAGE_RULES
.iter()
.find(|(prefix, _)| matches_service_prefix(base, prefix))
.map(|(_, label)| label.clone())
}
fn detect_namespace(image: &str) -> Option<StackLabel> {
image.rsplit('/').skip(1).find_map(|segment| {
NAMESPACE_IMAGE_RULES
.iter()
.find(|(namespace, _)| segment.eq_ignore_ascii_case(namespace))
.map(|(_, label)| label.clone())
})
}
const NAME_SEPARATORS: [char; 3] = ['-', '_', '.'];
fn matches_service_prefix(base: &str, prefix: &str) -> bool {
let Some(rest) = strip_prefix_ignore_ascii_case(base, prefix) else {
return false;
};
if rest.is_empty() {
return true;
}
rest.strip_prefix(NAME_SEPARATORS).is_some_and(|rest| {
!rest.split(NAME_SEPARATORS).any(|segment| {
COMPANION_SEGMENTS
.iter()
.any(|companion| segment.eq_ignore_ascii_case(companion))
})
})
}