@@ -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+
13181503type EnvVarMap = HashMap < SingleLineString , SingleLineString > ;
13191504
13201505/// CredentialProvider contains the settings for a credential provider for use
0 commit comments