diff --git a/bottlerocket-settings-models/modeled-types/src/kubernetes.rs b/bottlerocket-settings-models/modeled-types/src/kubernetes.rs index b347fefa..8dd9e9d7 100644 --- a/bottlerocket-settings-models/modeled-types/src/kubernetes.rs +++ b/bottlerocket-settings-models/modeled-types/src/kubernetes.rs @@ -1315,6 +1315,128 @@ mod test_cluster_dns_ip { } } +// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= + +/// KubernetesNodeIp represents the --node-ip setting for kubelet. +/// +/// This model allows the value to be either a single IP (IPv4 or IPv6) or a +/// list of IPs for dual-stack configurations (one IPv4 and one IPv6). +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[serde(untagged)] +pub enum KubernetesNodeIp { + Scalar(IpAddr), + Vector(Vec), +} + +impl KubernetesNodeIp { + pub fn iter<'a>(&'a self) -> Box + 'a> { + match self { + Self::Scalar(inner) => Box::new(std::iter::once(inner)), + Self::Vector(inner) => Box::new(inner.iter()), + } + } +} + +impl IntoIterator for KubernetesNodeIp { + type Item = IpAddr; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + match self { + Self::Scalar(inner) => vec![inner], + Self::Vector(inner) => inner, + } + .into_iter() + } +} + +#[cfg(test)] +mod test_kubernetes_node_ip { + use super::KubernetesNodeIp; + use std::net::IpAddr; + use std::str::FromStr; + + #[test] + fn test_parse_single_ipv4() { + assert_eq!( + serde_json::from_str::(r#""192.168.1.1""#).unwrap(), + KubernetesNodeIp::Scalar(IpAddr::from_str("192.168.1.1").unwrap()) + ); + } + + #[test] + fn test_parse_single_ipv6() { + assert_eq!( + serde_json::from_str::(r#""2001:db8::1""#).unwrap(), + KubernetesNodeIp::Scalar(IpAddr::from_str("2001:db8::1").unwrap()) + ); + } + + #[test] + fn test_parse_dual_stack_list() { + let node_ip = + serde_json::from_str::(r#"["192.168.1.1", "2001:db8::1"]"#).unwrap(); + assert_eq!( + node_ip, + KubernetesNodeIp::Vector(vec![ + IpAddr::from_str("192.168.1.1").unwrap(), + IpAddr::from_str("2001:db8::1").unwrap() + ]) + ); + } + + #[test] + fn test_parse_dual_stack_reverse_order() { + let node_ip = + serde_json::from_str::(r#"["2001:db8::1", "192.168.1.1"]"#).unwrap(); + assert_eq!( + node_ip, + KubernetesNodeIp::Vector(vec![ + IpAddr::from_str("2001:db8::1").unwrap(), + IpAddr::from_str("192.168.1.1").unwrap() + ]) + ); + } + + #[test] + fn test_iter_scalar() { + let node_ip = KubernetesNodeIp::Scalar(IpAddr::from_str("192.168.1.1").unwrap()); + assert_eq!( + node_ip.iter().collect::>(), + vec![&IpAddr::from_str("192.168.1.1").unwrap()] + ); + } + + #[test] + fn test_iter_vector() { + let node_ip = KubernetesNodeIp::Vector(vec![ + IpAddr::from_str("192.168.1.1").unwrap(), + IpAddr::from_str("2001:db8::1").unwrap(), + ]); + assert_eq!( + node_ip.iter().collect::>(), + vec![ + &IpAddr::from_str("192.168.1.1").unwrap(), + &IpAddr::from_str("2001:db8::1").unwrap() + ] + ); + } + + #[test] + fn test_serde_round_trip_scalar() { + let json = r#""192.168.1.1""#; + let node_ip: KubernetesNodeIp = serde_json::from_str(json).unwrap(); + assert_eq!(serde_json::to_string(&node_ip).unwrap(), json); + } + + #[test] + fn test_serde_round_trip_vector() { + let json = r#"["192.168.1.1","2001:db8::1"]"#; + let node_ip: KubernetesNodeIp = serde_json::from_str(json).unwrap(); + assert_eq!(serde_json::to_string(&node_ip).unwrap(), json); + } +} + type EnvVarMap = HashMap; /// CredentialProvider contains the settings for a credential provider for use diff --git a/bottlerocket-settings-models/settings-extensions/kubernetes/src/lib.rs b/bottlerocket-settings-models/settings-extensions/kubernetes/src/lib.rs index 35474f4a..d309eebf 100644 --- a/bottlerocket-settings-models/settings-extensions/kubernetes/src/lib.rs +++ b/bottlerocket-settings-models/settings-extensions/kubernetes/src/lib.rs @@ -6,17 +6,16 @@ use bottlerocket_modeled_types::{ KubernetesCloudProvider, KubernetesClusterDnsIp, KubernetesClusterName, KubernetesDurationValue, KubernetesEvictionKey, KubernetesHostnameOverrideSource, KubernetesLabelKey, KubernetesLabelValue, KubernetesMemoryManagerPolicy, - KubernetesMemoryReservation, KubernetesMemorySwapBehavior, KubernetesQuantityValue, - KubernetesReservedResourceKey, KubernetesTaintValue, KubernetesThresholdValue, - NonNegativeInteger, SingleLineString, TopologyManagerPolicy, TopologyManagerScope, Url, - ValidBase64, ValidLinuxHostname, + KubernetesMemoryReservation, KubernetesMemorySwapBehavior, KubernetesNodeIp, + KubernetesQuantityValue, KubernetesReservedResourceKey, KubernetesTaintValue, + KubernetesThresholdValue, NonNegativeInteger, SingleLineString, TopologyManagerPolicy, + TopologyManagerScope, Url, ValidBase64, ValidLinuxHostname, }; use bottlerocket_settings_sdk::{GenerateResult, SettingsModel}; use self::de::deserialize_node_taints; use std::collections::HashMap; use std::convert::Infallible; -use std::net::IpAddr; mod de; @@ -93,7 +92,7 @@ pub struct KubernetesSettingsV1 { max_pods: u32, cluster_dns_ip: KubernetesClusterDnsIp, cluster_domain: DNSDomain, - node_ip: IpAddr, + node_ip: KubernetesNodeIp, pod_infra_container_image: SingleLineString, hostname_override: ValidLinuxHostname, }