From 90b89224d880eaa4e584b6e8ac1f590be5b190d5 Mon Sep 17 00:00:00 2001 From: Willem Date: Mon, 17 Nov 2025 15:20:51 -0500 Subject: [PATCH] fix: added K8sName type for strict naming of Kubernetes resources --- harmony_types/src/k8s_name.rs | 96 +++++++++++++++++++++++++++++++++++ harmony_types/src/lib.rs | 1 + 2 files changed, 97 insertions(+) create mode 100644 harmony_types/src/k8s_name.rs diff --git a/harmony_types/src/k8s_name.rs b/harmony_types/src/k8s_name.rs new file mode 100644 index 0000000..9cb92ae --- /dev/null +++ b/harmony_types/src/k8s_name.rs @@ -0,0 +1,96 @@ +use std::str::FromStr; + +use serde::Serialize; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +pub struct K8sName(pub String); + +impl K8sName { + #[cfg(test)] + pub fn dummy() -> Self { + K8sName("example".to_string()) + } + + fn is_valid(name: &str) -> bool { + if name.is_empty() || name.len() > 63 { + return false; + } + + let b = name.as_bytes(); + + if !b[0].is_ascii_alphanumeric() || !b[b.len() - 1].is_ascii_alphanumeric() { + return false; + } + + b.iter() + .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || *c == b'-') + } +} + +impl FromStr for K8sName { + type Err = K8sNameError; + + fn from_str(s: &str) -> Result { + if !Self::is_valid(s) { + return Err(K8sNameError::InvalidFormat(format!( + "Invalid Kubernetes resource name '{s}': \ + must match DNS-1123 (lowercase alphanumeric, hyphens, <=63 chars)" + ))); + }; + + Ok(K8sName(s.to_string())) + } +} + +#[derive(Debug)] +pub enum K8sNameError { + InvalidFormat(String), +} + +impl From<&K8sName> for String { + fn from(value: &K8sName) -> Self { + value.0.clone() + } +} + +impl std::fmt::Display for K8sName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_valid_name() { + assert!(K8sName::from_str("k8s-name-test").is_ok()); + assert!(K8sName::from_str("n").is_ok()); + assert!(K8sName::from_str("node1").is_ok()); + assert!(K8sName::from_str("my-app-v2").is_ok()); + assert!(K8sName::from_str("service123").is_ok()); + assert!(K8sName::from_str("abcdefghijklmnopqrstuvwxyz-1234567890").is_ok()); + } + + #[test] + fn test_invalid_name() { + assert!(K8sName::from_str("").is_err()); + assert!(K8sName::from_str(".config").is_err()); + assert!(K8sName::from_str("_hidden").is_err()); + assert!(K8sName::from_str("UPPER-CASE").is_err()); + assert!(K8sName::from_str("123-$$$").is_err()); + assert!(K8sName::from_str("app!name").is_err()); + assert!(K8sName::from_str("my..app").is_err()); + assert!(K8sName::from_str("backend-").is_err()); + assert!(K8sName::from_str("-frontend").is_err()); + assert!(K8sName::from_str("InvalidName").is_err()); + assert!( + K8sName::from_str("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + .is_err() + ); + assert!(K8sName::from_str("k8s name").is_err()); + assert!(K8sName::from_str("k8s_name").is_err()); + assert!(K8sName::from_str("k8s@name").is_err()); + } +} diff --git a/harmony_types/src/lib.rs b/harmony_types/src/lib.rs index 098379a..d5c7db0 100644 --- a/harmony_types/src/lib.rs +++ b/harmony_types/src/lib.rs @@ -1,3 +1,4 @@ pub mod id; +pub mod k8s_name; pub mod net; pub mod switch;