#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemberKind {
Lakespec,
Manifest,
DbDump,
S3Blob,
Image,
ImageRef,
InfraTerraform,
InfraRedfish,
InfraKvm,
TalosIso,
TalosMachineConfig,
Config,
HelmChart,
HelmValues,
Secret,
}
impl MemberKind {
pub fn as_str(self) -> &'static str {
match self {
MemberKind::Lakespec => "lakespec",
MemberKind::Manifest => "manifest",
MemberKind::DbDump => "dbdump",
MemberKind::S3Blob => "s3-blob",
MemberKind::Image => "image",
MemberKind::ImageRef => "image-ref",
MemberKind::InfraTerraform => "infra-terraform",
MemberKind::InfraRedfish => "infra-redfish",
MemberKind::InfraKvm => "infra-kvm",
MemberKind::TalosIso => "talos-iso",
MemberKind::TalosMachineConfig => "talos-machineconfig",
MemberKind::Config => "config",
MemberKind::HelmChart => "helm-chart",
MemberKind::HelmValues => "helm-values",
MemberKind::Secret => "secret",
}
}
}
fn segments(path: &str) -> Vec<&str> {
path.split(['/', '\\']).filter(|s| !s.is_empty() && *s != ".").collect()
}
pub fn classify(path: &str) -> Option<MemberKind> {
if path.ends_with(".age") {
return Some(MemberKind::Secret);
}
let segs = segments(path);
let top = *segs.first()?;
match top {
"lakespec.json" => Some(MemberKind::Lakespec),
"manifest" => Some(MemberKind::Manifest),
"dbdump" => Some(MemberKind::DbDump),
"s3" => Some(MemberKind::S3Blob),
"app" => {
if path.ends_with(".imageref") || path.ends_with(".ref") {
Some(MemberKind::ImageRef)
} else {
Some(MemberKind::Image)
}
}
"njordconf" => {
let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
if path.ends_with(".tgz") {
Some(MemberKind::HelmChart)
} else if name.starts_with("values")
&& (name.ends_with(".yaml") || name.ends_with(".yml"))
{
Some(MemberKind::HelmValues)
} else {
Some(MemberKind::Config)
}
}
"secrets" => Some(MemberKind::Secret),
"infra" => match segs.get(1).copied() {
Some("terraform") => Some(MemberKind::InfraTerraform),
Some("redfish") => Some(MemberKind::InfraRedfish),
Some("kvm") => Some(MemberKind::InfraKvm),
Some("talos") => {
if path.ends_with(".iso") {
Some(MemberKind::TalosIso)
} else {
Some(MemberKind::TalosMachineConfig)
}
}
_ => None,
},
_ => None,
}
}
pub fn component_of(kind: MemberKind, path: &str) -> Option<&'static str> {
match kind {
MemberKind::DbDump => Some("postgres"),
MemberKind::S3Blob => Some("minio"),
MemberKind::Image | MemberKind::ImageRef => {
let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
if name.contains("mail-server") {
Some("mail-server")
} else if name.contains("web") {
Some("web")
} else {
Some("app")
}
}
MemberKind::InfraTerraform
| MemberKind::InfraRedfish
| MemberKind::InfraKvm
| MemberKind::TalosIso => Some("infra"),
MemberKind::TalosMachineConfig => Some("cluster"),
MemberKind::Config => Some("config"),
MemberKind::HelmChart | MemberKind::HelmValues => Some("platform"),
MemberKind::Secret => Some("secrets"),
MemberKind::Lakespec | MemberKind::Manifest => None,
}
}
pub fn is_encrypted(kind: MemberKind, path: &str) -> bool {
kind == MemberKind::Secret || path.ends_with(".age")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifies_every_top_level_dir() {
let cases = [
("lakespec.json", MemberKind::Lakespec),
("manifest/bundle.json", MemberKind::Manifest),
("manifest/consistency.json", MemberKind::Manifest),
("dbdump/njord.dump", MemberKind::DbDump),
("s3/2026/07/protokoll-123.pdf", MemberKind::S3Blob),
("app/njord-web.oci.tar", MemberKind::Image),
("app/njord-mail-server.oci.tar", MemberKind::Image),
("app/njord-web.imageref", MemberKind::ImageRef),
("app/njord-mail-server.ref", MemberKind::ImageRef),
("infra/terraform/main.tf", MemberKind::InfraTerraform),
("infra/redfish/bmc.json", MemberKind::InfraRedfish),
("infra/kvm/domain.xml", MemberKind::InfraKvm),
("infra/talos/talos-1.13.iso", MemberKind::TalosIso),
("infra/talos/machineconfig.yaml", MemberKind::TalosMachineConfig),
("njordconf/serverspec.json", MemberKind::Config),
("njordconf/chart/njord-0.1.0.tgz", MemberKind::HelmChart),
("njordconf/values-lab.yaml", MemberKind::HelmValues),
("secrets/secrets.age", MemberKind::Secret),
];
for (p, want) in cases {
assert_eq!(classify(p), Some(want), "classify({p})");
}
}
#[test]
fn age_anywhere_is_secret() {
assert_eq!(classify("njordconf/values.age"), Some(MemberKind::Secret));
assert!(is_encrypted(MemberKind::Secret, "secrets/x"));
assert!(is_encrypted(MemberKind::Config, "njordconf/v.age"));
assert!(!is_encrypted(MemberKind::Config, "njordconf/serverspec.json"));
}
#[test]
fn unknown_paths_are_not_claimed() {
assert_eq!(classify("README.md"), None);
assert_eq!(classify("infra/unknown/x"), None);
assert_eq!(classify(""), None);
assert_eq!(classify("./s3/./doc.pdf"), Some(MemberKind::S3Blob));
}
#[test]
fn component_mapping() {
assert_eq!(component_of(MemberKind::DbDump, "dbdump/x.dump"), Some("postgres"));
assert_eq!(component_of(MemberKind::S3Blob, "s3/x.pdf"), Some("minio"));
assert_eq!(component_of(MemberKind::Image, "app/njord-web.oci.tar"), Some("web"));
assert_eq!(component_of(MemberKind::ImageRef, "app/njord-web.imageref"), Some("web"));
assert_eq!(
component_of(MemberKind::Image, "app/njord-mail-server.oci.tar"),
Some("mail-server")
);
assert_eq!(component_of(MemberKind::TalosMachineConfig, "infra/talos/mc.yaml"), Some("cluster"));
assert_eq!(component_of(MemberKind::Lakespec, "lakespec.json"), None);
}
}