use crate::StackLabel;
pub const EXACT_IMAGE_RULES: &[(&str, StackLabel)] = &[
("mongo", StackLabel::database("MongoDB")),
("httpd", StackLabel::service("Apache")),
("node", StackLabel::runtime("Node.js")),
("python", StackLabel::runtime("Python")),
("python3", StackLabel::runtime("Python")),
("ruby", StackLabel::runtime("Ruby")),
("golang", StackLabel::runtime("Go")),
("go", StackLabel::runtime("Go")),
("rust", StackLabel::runtime("Rust")),
("bun", StackLabel::runtime("Bun")),
("deno", StackLabel::runtime("Deno")),
("php", StackLabel::runtime("PHP")),
];
pub const PREFIX_IMAGE_RULES: &[(&str, StackLabel)] = &[
("postgres", StackLabel::database("PostgreSQL")),
("postgresql", StackLabel::database("PostgreSQL")),
("postgis", StackLabel::database("PostgreSQL")),
("timescaledb", StackLabel::database("PostgreSQL")),
("mysql", StackLabel::database("MySQL")),
("mariadb", StackLabel::database("MariaDB")),
("mongodb", StackLabel::database("MongoDB")),
("redis", StackLabel::database("Redis")),
("valkey", StackLabel::database("Valkey")),
("memcached", StackLabel::database("Memcached")),
("nginx", StackLabel::service("Nginx")),
("apache", StackLabel::service("Apache")),
("rabbitmq", StackLabel::service("RabbitMQ")),
("kafka", StackLabel::service("Kafka")),
("localstack", StackLabel::service("LocalStack")),
("elasticsearch", StackLabel::database("Elasticsearch")),
("opensearch", StackLabel::database("OpenSearch")),
("clickhouse", StackLabel::database("ClickHouse")),
("caddy", StackLabel::service("Caddy")),
("traefik", StackLabel::service("Traefik")),
("openjdk", StackLabel::runtime("Java")),
("eclipse-temurin", StackLabel::runtime("Java")),
("dotnet", StackLabel::runtime(".NET")),
];
const COMPANION_SEGMENTS: &[&str] = &[
"exporter",
"dashboard",
"dashboards",
"operator",
"admin",
"ui",
"gui",
"backup",
"client",
"cli",
"commander",
"workbench",
"shell",
"curator",
"auth",
];
pub const DOTNET_NAMESPACE_LABEL: StackLabel = StackLabel::runtime(".NET");
#[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(|| image_has_dotnet_namespace(image).then_some(DOTNET_NAMESPACE_LABEL))
}
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 image_has_dotnet_namespace(image: &str) -> bool {
image
.split('/')
.any(|segment| segment.eq_ignore_ascii_case("dotnet"))
}
const NAME_SEPARATORS: [char; 3] = ['-', '_', '.'];
fn matches_service_prefix(base: &str, prefix: &str) -> bool {
let Some(rest) = strip_prefix_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))
})
})
}
fn strip_prefix_ascii_case<'a>(value: &'a str, prefix: &str) -> Option<&'a str> {
value
.get(..prefix.len())
.filter(|head| head.eq_ignore_ascii_case(prefix))
.and_then(|_| value.get(prefix.len()..))
}