From 5e1fae28566f0b126b931c7b1e5b7ad2e124d51c Mon Sep 17 00:00:00 2001 From: Daniel Bennett Date: Mon, 16 Sep 2024 10:21:52 -0500 Subject: [PATCH] networking: set alloc NetworkStatus.AddressIPv6 (#23959) when a CNI result includes an IPv6 address, set it on the alloc's NetworkStatus for reference. e.g.: $ nomad alloc status -json 3dca | jq '.NetworkStatus' { "Address": "172.26.64.14", "AddressIPv6": "fd00:a110:c8::b", "DNS": null, "InterfaceName": "eth0" } --- api/allocations.go | 1 + client/allocrunner/networking_cni.go | 57 ++++++++++++++--------- client/allocrunner/networking_cni_test.go | 33 +++++++++++++ nomad/structs/structs.go | 4 ++ 4 files changed, 74 insertions(+), 21 deletions(-) diff --git a/api/allocations.go b/api/allocations.go index d4b91ccc63c..f8a3a0aeee9 100644 --- a/api/allocations.go +++ b/api/allocations.go @@ -435,6 +435,7 @@ type AllocDeploymentStatus struct { type AllocNetworkStatus struct { InterfaceName string Address string + AddressIPv6 string DNS *DNSConfig } diff --git a/client/allocrunner/networking_cni.go b/client/allocrunner/networking_cni.go index 6379522839f..5aada33d104 100644 --- a/client/allocrunner/networking_cni.go +++ b/client/allocrunner/networking_cni.go @@ -393,36 +393,51 @@ func (c *cniNetworkConfigurator) cniToAllocNet(res *cni.Result) (*structs.AllocN } sort.Strings(names) - // Use the first sandbox interface with an IP address - for _, name := range names { - iface := res.Interfaces[name] - if iface == nil { + // setStatus sets netStatus.Address and netStatus.InterfaceName + // if it finds a suitable interface that has IP address(es) + // (at least IPv4, possibly also IPv6) + setStatus := func(requireSandbox bool) { + for _, name := range names { + iface := res.Interfaces[name] // this should never happen but this value is coming from external // plugins so we should guard against it - delete(res.Interfaces, name) - continue - } + if iface == nil { + continue + } - if iface.Sandbox != "" && len(iface.IPConfigs) > 0 { - netStatus.Address = iface.IPConfigs[0].IP.String() - netStatus.InterfaceName = name - break + if requireSandbox && iface.Sandbox == "" { + continue + } + + for _, ipConfig := range iface.IPConfigs { + isIP4 := ipConfig.IP.To4() != nil + if netStatus.Address == "" && isIP4 { + netStatus.Address = ipConfig.IP.String() + } + if netStatus.AddressIPv6 == "" && !isIP4 { + netStatus.AddressIPv6 = ipConfig.IP.String() + } + } + + // found a good interface, so we're done + if netStatus.Address != "" { + netStatus.InterfaceName = name + return + } } } + // Use the first sandbox interface with an IP address + setStatus(true) + // If no IP address was found, use the first interface with an address // found as a fallback if netStatus.Address == "" { - for _, name := range names { - iface := res.Interfaces[name] - if len(iface.IPConfigs) > 0 { - ip := iface.IPConfigs[0].IP.String() - c.logger.Debug("no sandbox interface with an address found CNI result, using first available", "interface", name, "ip", ip) - netStatus.Address = ip - netStatus.InterfaceName = name - break - } - } + setStatus(false) + c.logger.Debug("no sandbox interface with an address found CNI result, using first available", + "interface", netStatus.InterfaceName, + "ip", netStatus.Address, + ) } // If no IP address could be found, return an error diff --git a/client/allocrunner/networking_cni_test.go b/client/allocrunner/networking_cni_test.go index 8e65461b1fe..03e9df514a7 100644 --- a/client/allocrunner/networking_cni_test.go +++ b/client/allocrunner/networking_cni_test.go @@ -426,6 +426,39 @@ func TestCNI_cniToAllocNet_Invalid(t *testing.T) { require.Nil(t, allocNet) } +func TestCNI_cniToAllocNet_Dualstack(t *testing.T) { + ci.Parallel(t) + + cniResult := &cni.Result{ + Interfaces: map[string]*cni.Config{ + "eth0": { + Sandbox: "at-the-park", + IPConfigs: []*cni.IPConfig{ + {IP: net.IPv4zero}, // 0.0.0.0 + {IP: net.IPv6zero}, // :: + }, + }, + // "a" puts this lexicographically first when sorted + "a-skippable-interface": { + // no Sandbox, so should be skipped + IPConfigs: []*cni.IPConfig{ + {IP: net.IPv4(127, 3, 2, 1)}, + }, + }, + }, + } + + c := &cniNetworkConfigurator{ + logger: testlog.HCLogger(t), + } + allocNet, err := c.cniToAllocNet(cniResult) + must.NoError(t, err) + must.NotNil(t, allocNet) + test.Eq(t, "0.0.0.0", allocNet.Address) + test.Eq(t, "::", allocNet.AddressIPv6) + test.Eq(t, "eth0", allocNet.InterfaceName) +} + func TestCNI_addCustomCNIArgs(t *testing.T) { ci.Parallel(t) cniArgs := map[string]string{ diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index 2c521be1913..2a6d43c69b7 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -12101,6 +12101,7 @@ func (s *NodeScoreMeta) Data() interface{} { type AllocNetworkStatus struct { InterfaceName string Address string + AddressIPv6 string DNS *DNSConfig } @@ -12111,6 +12112,7 @@ func (a *AllocNetworkStatus) Copy() *AllocNetworkStatus { return &AllocNetworkStatus{ InterfaceName: a.InterfaceName, Address: a.Address, + AddressIPv6: a.AddressIPv6, DNS: a.DNS.Copy(), } } @@ -12131,6 +12133,8 @@ func (a *AllocNetworkStatus) Equal(o *AllocNetworkStatus) bool { return false case a.Address != o.Address: return false + case a.AddressIPv6 != o.AddressIPv6: + return false case !a.DNS.Equal(o.DNS): return false }