Skip to content

Commit fbd235b

Browse files
committed
fix: add dual-stack support for kubelet node-ip
1 parent eed641b commit fbd235b

File tree

2 files changed

+190
-6
lines changed
  • bottlerocket-settings-models

2 files changed

+190
-6
lines changed

bottlerocket-settings-models/modeled-types/src/kubernetes.rs

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,6 +1315,191 @@ mod test_cluster_dns_ip {
13151315
}
13161316
}
13171317

1318+
// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=
1319+
1320+
/// KubernetesNodeIp represents the --node-ip setting for kubelet.
1321+
///
1322+
/// This model allows the value to be either a single IP (IPv4 or IPv6) or a
1323+
/// dual-stack format (IPv4,IPv6) for bare metal dual-stack nodes.
1324+
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
1325+
pub struct KubernetesNodeIp {
1326+
inner: String,
1327+
}
1328+
1329+
impl KubernetesNodeIp {
1330+
/// Returns the IP addresses as a comma-separated string
1331+
pub fn as_str(&self) -> &str {
1332+
&self.inner
1333+
}
1334+
1335+
/// Returns an iterator over the IP addresses
1336+
pub fn iter(&self) -> impl Iterator<Item = IpAddr> + '_ {
1337+
self.inner.split(',').filter_map(|s| s.trim().parse().ok())
1338+
}
1339+
}
1340+
1341+
impl TryFrom<&str> for KubernetesNodeIp {
1342+
type Error = error::Error;
1343+
1344+
fn try_from(input: &str) -> Result<Self, Self::Error> {
1345+
let input = input.trim();
1346+
1347+
// Split by comma to check if it's dual-stack
1348+
let parts: Vec<&str> = input.split(',').map(|s| s.trim()).collect();
1349+
1350+
match parts.as_slice() {
1351+
[single_ip] => {
1352+
// Single IP - must be valid IPv4 or IPv6
1353+
single_ip
1354+
.parse::<IpAddr>()
1355+
.map_err(|_| error::Error::BigPattern {
1356+
thing: "Kubernetes node IP".to_string(),
1357+
input: input.to_string(),
1358+
})?;
1359+
}
1360+
[ip1_str, ip2_str] => {
1361+
// Dual-stack - must be one IPv4 and one IPv6
1362+
let ip1 = ip1_str
1363+
.parse::<IpAddr>()
1364+
.map_err(|_| error::Error::BigPattern {
1365+
thing: "Kubernetes node IP".to_string(),
1366+
input: input.to_string(),
1367+
})?;
1368+
let ip2 = ip2_str
1369+
.parse::<IpAddr>()
1370+
.map_err(|_| error::Error::BigPattern {
1371+
thing: "Kubernetes node IP".to_string(),
1372+
input: input.to_string(),
1373+
})?;
1374+
1375+
// Ensure one is IPv4 and one is IPv6
1376+
let has_v4 = ip1.is_ipv4() || ip2.is_ipv4();
1377+
let has_v6 = ip1.is_ipv6() || ip2.is_ipv6();
1378+
let same_version =
1379+
(ip1.is_ipv4() && ip2.is_ipv4()) || (ip1.is_ipv6() && ip2.is_ipv6());
1380+
1381+
ensure!(
1382+
has_v4 && has_v6 && !same_version,
1383+
error::BigPatternSnafu {
1384+
thing: "Kubernetes node IP (dual-stack must have one IPv4 and one IPv6)",
1385+
input
1386+
}
1387+
);
1388+
}
1389+
_ => {
1390+
return Err(error::Error::BigPattern {
1391+
thing: "Kubernetes node IP (must be single IP or dual-stack IPv4,IPv6)"
1392+
.to_string(),
1393+
input: input.to_string(),
1394+
});
1395+
}
1396+
}
1397+
1398+
Ok(KubernetesNodeIp {
1399+
inner: input.to_string(),
1400+
})
1401+
}
1402+
}
1403+
1404+
impl Display for KubernetesNodeIp {
1405+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1406+
write!(f, "{}", self.inner)
1407+
}
1408+
}
1409+
1410+
impl Serialize for KubernetesNodeIp {
1411+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1412+
where
1413+
S: Serializer,
1414+
{
1415+
serializer.serialize_str(&self.inner)
1416+
}
1417+
}
1418+
1419+
impl<'de> Deserialize<'de> for KubernetesNodeIp {
1420+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1421+
where
1422+
D: Deserializer<'de>,
1423+
{
1424+
let s = String::deserialize(deserializer)?;
1425+
KubernetesNodeIp::try_from(s.as_str()).map_err(serde::de::Error::custom)
1426+
}
1427+
}
1428+
1429+
#[cfg(test)]
1430+
mod test_kubernetes_node_ip {
1431+
use super::KubernetesNodeIp;
1432+
use std::convert::TryFrom;
1433+
1434+
#[test]
1435+
fn test_single_ipv4() {
1436+
let node_ip = KubernetesNodeIp::try_from("192.168.1.1").unwrap();
1437+
assert_eq!(node_ip.as_str(), "192.168.1.1");
1438+
}
1439+
1440+
#[test]
1441+
fn test_single_ipv6() {
1442+
let node_ip = KubernetesNodeIp::try_from("2001:db8::1").unwrap();
1443+
assert_eq!(node_ip.as_str(), "2001:db8::1");
1444+
}
1445+
1446+
#[test]
1447+
fn test_dual_stack_ipv4_ipv6() {
1448+
let node_ip = KubernetesNodeIp::try_from("192.168.1.1,2001:db8::1").unwrap();
1449+
assert_eq!(node_ip.as_str(), "192.168.1.1,2001:db8::1");
1450+
}
1451+
1452+
#[test]
1453+
fn test_dual_stack_ipv6_ipv4() {
1454+
let node_ip = KubernetesNodeIp::try_from("2001:db8::1,192.168.1.1").unwrap();
1455+
assert_eq!(node_ip.as_str(), "2001:db8::1,192.168.1.1");
1456+
}
1457+
1458+
#[test]
1459+
fn test_dual_stack_with_spaces() {
1460+
let node_ip = KubernetesNodeIp::try_from("192.168.1.1, 2001:db8::1").unwrap();
1461+
assert_eq!(node_ip.as_str(), "192.168.1.1, 2001:db8::1");
1462+
}
1463+
1464+
#[test]
1465+
fn test_invalid_single_ip() {
1466+
assert!(KubernetesNodeIp::try_from("invalid").is_err());
1467+
}
1468+
1469+
#[test]
1470+
fn test_invalid_dual_stack_same_version_v4() {
1471+
// Both IPv4 should fail
1472+
assert!(KubernetesNodeIp::try_from("192.168.1.1,10.0.0.1").is_err());
1473+
}
1474+
1475+
#[test]
1476+
fn test_invalid_dual_stack_same_version_v6() {
1477+
// Both IPv6 should fail
1478+
assert!(KubernetesNodeIp::try_from("2001:db8::1,2001:db8::2").is_err());
1479+
}
1480+
1481+
#[test]
1482+
fn test_invalid_too_many_ips() {
1483+
assert!(KubernetesNodeIp::try_from("192.168.1.1,2001:db8::1,10.0.0.1").is_err());
1484+
}
1485+
1486+
#[test]
1487+
fn test_serde_single_ipv4() {
1488+
let json = r#""192.168.1.1""#;
1489+
let node_ip: KubernetesNodeIp = serde_json::from_str(json).unwrap();
1490+
assert_eq!(node_ip.as_str(), "192.168.1.1");
1491+
assert_eq!(serde_json::to_string(&node_ip).unwrap(), json);
1492+
}
1493+
1494+
#[test]
1495+
fn test_serde_dual_stack() {
1496+
let json = r#""192.168.1.1,2001:db8::1""#;
1497+
let node_ip: KubernetesNodeIp = serde_json::from_str(json).unwrap();
1498+
assert_eq!(node_ip.as_str(), "192.168.1.1,2001:db8::1");
1499+
assert_eq!(serde_json::to_string(&node_ip).unwrap(), json);
1500+
}
1501+
}
1502+
13181503
type EnvVarMap = HashMap<SingleLineString, SingleLineString>;
13191504

13201505
/// CredentialProvider contains the settings for a credential provider for use

bottlerocket-settings-models/settings-extensions/kubernetes/src/lib.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,16 @@ use bottlerocket_modeled_types::{
66
KubernetesCloudProvider, KubernetesClusterDnsIp, KubernetesClusterName,
77
KubernetesDurationValue, KubernetesEvictionKey, KubernetesHostnameOverrideSource,
88
KubernetesLabelKey, KubernetesLabelValue, KubernetesMemoryManagerPolicy,
9-
KubernetesMemoryReservation, KubernetesMemorySwapBehavior, KubernetesQuantityValue,
10-
KubernetesReservedResourceKey, KubernetesTaintValue, KubernetesThresholdValue,
11-
NonNegativeInteger, SingleLineString, TopologyManagerPolicy, TopologyManagerScope, Url,
12-
ValidBase64, ValidLinuxHostname,
9+
KubernetesMemoryReservation, KubernetesMemorySwapBehavior, KubernetesNodeIp,
10+
KubernetesQuantityValue, KubernetesReservedResourceKey, KubernetesTaintValue,
11+
KubernetesThresholdValue, NonNegativeInteger, SingleLineString, TopologyManagerPolicy,
12+
TopologyManagerScope, Url, ValidBase64, ValidLinuxHostname,
1313
};
1414
use bottlerocket_settings_sdk::{GenerateResult, SettingsModel};
1515

1616
use self::de::deserialize_node_taints;
1717
use std::collections::HashMap;
1818
use std::convert::Infallible;
19-
use std::net::IpAddr;
2019

2120
mod de;
2221

@@ -93,7 +92,7 @@ pub struct KubernetesSettingsV1 {
9392
max_pods: u32,
9493
cluster_dns_ip: KubernetesClusterDnsIp,
9594
cluster_domain: DNSDomain,
96-
node_ip: IpAddr,
95+
node_ip: KubernetesNodeIp,
9796
pod_infra_container_image: SingleLineString,
9897
hostname_override: ValidLinuxHostname,
9998
}

0 commit comments

Comments
 (0)