Skip to content

Commit 75242fc

Browse files
committed
kubelet: allow specifying dual-stack node IPs on bare metal
Discussion is ongoing about how to best handle dual-stack with clouds and autodetected IPs, but there is at least agreement that people on bare metal ought to be able to specify two explicit IPs on dual-stack hosts, so allow that.
1 parent 2680095 commit 75242fc

File tree

10 files changed

+136
-23
lines changed

10 files changed

+136
-23
lines changed

cmd/kubelet/app/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ go_library(
124124
"//vendor/github.com/spf13/pflag:go_default_library",
125125
"//vendor/k8s.io/klog/v2:go_default_library",
126126
"//vendor/k8s.io/utils/exec:go_default_library",
127+
"//vendor/k8s.io/utils/net:go_default_library",
127128
] + select({
128129
"@io_bazel_rules_go//go/platform:android": [
129130
"//vendor/k8s.io/utils/inotify:go_default_library",

cmd/kubelet/app/options/options.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ func (f *KubeletFlags) AddFlags(mainfs *pflag.FlagSet) {
325325

326326
fs.StringVar(&f.HostnameOverride, "hostname-override", f.HostnameOverride, "If non-empty, will use this string as identification instead of the actual hostname. If --cloud-provider is set, the cloud provider determines the name of the node (consult cloud provider documentation to determine if and how the hostname is used).")
327327

328-
fs.StringVar(&f.NodeIP, "node-ip", f.NodeIP, "IP address of the node. If set, kubelet will use this IP address for the node. If unset, kubelet will use the node's default IPv4 address, if any, or its default IPv6 address if it has no IPv4 addresses. You can pass '::' to make it prefer the default IPv6 address rather than the default IPv4 address.")
328+
fs.StringVar(&f.NodeIP, "node-ip", f.NodeIP, "IP address (or comma-separated dual-stack IP addresses) of the node. If unset, kubelet will use the node's default IPv4 address, if any, or its default IPv6 address if it has no IPv4 addresses. You can pass '::' to make it prefer the default IPv6 address rather than the default IPv4 address.")
329329

330330
fs.StringVar(&f.CertDirectory, "cert-dir", f.CertDirectory, "The directory where the TLS certs are located. "+
331331
"If --tls-cert-file and --tls-private-key-file are provided, this flag will be ignored.")

cmd/kubelet/app/server.go

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ import (
102102
"k8s.io/kubernetes/pkg/volume/util/hostutil"
103103
"k8s.io/kubernetes/pkg/volume/util/subpath"
104104
"k8s.io/utils/exec"
105+
utilnet "k8s.io/utils/net"
105106
)
106107

107108
const (
@@ -1086,6 +1087,27 @@ func RunKubelet(kubeServer *options.KubeletServer, kubeDeps *kubelet.Dependencie
10861087
// Setup event recorder if required.
10871088
makeEventRecorder(kubeDeps, nodeName)
10881089

1090+
var nodeIPs []net.IP
1091+
if kubeServer.NodeIP != "" {
1092+
for _, ip := range strings.Split(kubeServer.NodeIP, ",") {
1093+
parsedNodeIP := net.ParseIP(strings.TrimSpace(ip))
1094+
if parsedNodeIP == nil {
1095+
klog.Warningf("Could not parse --node-ip value %q; ignoring", ip)
1096+
} else {
1097+
nodeIPs = append(nodeIPs, parsedNodeIP)
1098+
}
1099+
}
1100+
}
1101+
if !utilfeature.DefaultFeatureGate.Enabled(features.IPv6DualStack) && len(nodeIPs) > 1 {
1102+
return fmt.Errorf("dual-stack --node-ip %q not supported in a single-stack cluster", kubeServer.NodeIP)
1103+
} else if len(nodeIPs) > 2 || (len(nodeIPs) == 2 && utilnet.IsIPv6(nodeIPs[0]) == utilnet.IsIPv6(nodeIPs[1])) {
1104+
return fmt.Errorf("bad --node-ip %q; must contain either a single IP or a dual-stack pair of IPs", kubeServer.NodeIP)
1105+
} else if len(nodeIPs) == 2 && kubeServer.CloudProvider != "" {
1106+
return fmt.Errorf("dual-stack --node-ip %q not supported when using a cloud provider", kubeServer.NodeIP)
1107+
} else if len(nodeIPs) == 2 && (nodeIPs[0].IsUnspecified() || nodeIPs[1].IsUnspecified()) {
1108+
return fmt.Errorf("dual-stack --node-ip %q cannot include '0.0.0.0' or '::'", kubeServer.NodeIP)
1109+
}
1110+
10891111
capabilities.Initialize(capabilities.Capabilities{
10901112
AllowPrivileged: true,
10911113
})
@@ -1104,7 +1126,7 @@ func RunKubelet(kubeServer *options.KubeletServer, kubeDeps *kubelet.Dependencie
11041126
hostname,
11051127
hostnameOverridden,
11061128
nodeName,
1107-
kubeServer.NodeIP,
1129+
nodeIPs,
11081130
kubeServer.ProviderID,
11091131
kubeServer.CloudProvider,
11101132
kubeServer.CertDirectory,
@@ -1178,7 +1200,7 @@ func createAndInitKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
11781200
hostname string,
11791201
hostnameOverridden bool,
11801202
nodeName types.NodeName,
1181-
nodeIP string,
1203+
nodeIPs []net.IP,
11821204
providerID string,
11831205
cloudProvider string,
11841206
certDirectory string,
@@ -1209,7 +1231,7 @@ func createAndInitKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
12091231
hostname,
12101232
hostnameOverridden,
12111233
nodeName,
1212-
nodeIP,
1234+
nodeIPs,
12131235
providerID,
12141236
cloudProvider,
12151237
certDirectory,

pkg/kubelet/kubelet.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
334334
hostname string,
335335
hostnameOverridden bool,
336336
nodeName types.NodeName,
337-
nodeIP string,
337+
nodeIPs []net.IP,
338338
providerID string,
339339
cloudProvider string,
340340
certDirectory string,
@@ -462,7 +462,6 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
462462
}
463463
}
464464
httpClient := &http.Client{}
465-
parsedNodeIP := net.ParseIP(nodeIP)
466465

467466
klet := &Kubelet{
468467
hostname: hostname,
@@ -477,7 +476,7 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
477476
registerNode: registerNode,
478477
registerWithTaints: registerWithTaints,
479478
registerSchedulable: registerSchedulable,
480-
dnsConfigurer: dns.NewConfigurer(kubeDeps.Recorder, nodeRef, parsedNodeIP, clusterDNS, kubeCfg.ClusterDomain, kubeCfg.ResolverConfig),
479+
dnsConfigurer: dns.NewConfigurer(kubeDeps.Recorder, nodeRef, nodeIPs, clusterDNS, kubeCfg.ClusterDomain, kubeCfg.ResolverConfig),
481480
serviceLister: serviceLister,
482481
serviceHasSynced: serviceHasSynced,
483482
nodeLister: nodeLister,
@@ -506,7 +505,7 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
506505
containerManager: kubeDeps.ContainerManager,
507506
containerRuntimeName: containerRuntime,
508507
redirectContainerStreaming: crOptions.RedirectContainerStreaming,
509-
nodeIP: parsedNodeIP,
508+
nodeIPs: nodeIPs,
510509
nodeIPValidator: validateNodeIP,
511510
clock: clock.RealClock{},
512511
enableControllerAttachDetach: kubeCfg.EnableControllerAttachDetach,
@@ -1042,8 +1041,8 @@ type Kubelet struct {
10421041
// oneTimeInitializer is used to initialize modules that are dependent on the runtime to be up.
10431042
oneTimeInitializer sync.Once
10441043

1045-
// If non-nil, use this IP address for the node
1046-
nodeIP net.IP
1044+
// If set, use this IP address or addresses for the node
1045+
nodeIPs []net.IP
10471046

10481047
// use this function to validate the kubelet nodeIP
10491048
nodeIPValidator func(net.IP) error

pkg/kubelet/kubelet_network_linux.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ func (kl *Kubelet) initNetworkUtil() {
3535
exec := utilexec.New()
3636

3737
// At this point in startup we don't know the actual node IPs, so we configure dual stack iptables
38-
// rules if the node _might_ be dual-stack, and single-stack based on requested nodeIP otherwise.
38+
// rules if the node _might_ be dual-stack, and single-stack based on requested nodeIPs[0] otherwise.
3939
maybeDualStack := utilfeature.DefaultFeatureGate.Enabled(features.IPv6DualStack)
40-
ipv6Primary := kl.nodeIP != nil && utilnet.IsIPv6(kl.nodeIP)
40+
ipv6Primary := kl.nodeIPs != nil && utilnet.IsIPv6(kl.nodeIPs[0])
4141

4242
var iptClients []utiliptables.Interface
4343
if maybeDualStack || !ipv6Primary {

pkg/kubelet/kubelet_node_status.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,7 @@ func (kl *Kubelet) defaultNodeStatusFuncs() []func(*v1.Node) error {
587587
}
588588
var setters []func(n *v1.Node) error
589589
setters = append(setters,
590-
nodestatus.NodeAddress(kl.nodeIP, kl.nodeIPValidator, kl.hostname, kl.hostnameOverridden, kl.externalCloudProvider, kl.cloud, nodeAddressesFunc),
590+
nodestatus.NodeAddress(kl.nodeIPs, kl.nodeIPValidator, kl.hostname, kl.hostnameOverridden, kl.externalCloudProvider, kl.cloud, nodeAddressesFunc),
591591
nodestatus.MachineInfo(string(kl.nodeName), kl.maxPods, kl.podsPerCore, kl.GetCachedMachineInfo, kl.containerManager.GetCapacity,
592592
kl.containerManager.GetDevicePluginResourceCapacity, kl.containerManager.GetNodeAllocatableReservation, kl.recordEvent),
593593
nodestatus.VersionInfo(kl.cadvisor.VersionInfo, kl.containerRuntime.Type, kl.containerRuntime.Version),

pkg/kubelet/network/dns/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ go_library(
1515
"//staging/src/k8s.io/cri-api/pkg/apis/runtime/v1alpha2:go_default_library",
1616
"//vendor/k8s.io/klog/v2:go_default_library",
1717
"//vendor/k8s.io/utils/io:go_default_library",
18+
"//vendor/k8s.io/utils/net:go_default_library",
1819
],
1920
)
2021

pkg/kubelet/network/dns/dns.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535

3636
"k8s.io/klog/v2"
3737
utilio "k8s.io/utils/io"
38+
utilnet "k8s.io/utils/net"
3839
)
3940

4041
var (
@@ -58,7 +59,7 @@ const (
5859
type Configurer struct {
5960
recorder record.EventRecorder
6061
nodeRef *v1.ObjectReference
61-
nodeIP net.IP
62+
nodeIPs []net.IP
6263

6364
// If non-nil, use this for container DNS server.
6465
clusterDNS []net.IP
@@ -71,11 +72,11 @@ type Configurer struct {
7172
}
7273

7374
// NewConfigurer returns a DNS configurer for launching pods.
74-
func NewConfigurer(recorder record.EventRecorder, nodeRef *v1.ObjectReference, nodeIP net.IP, clusterDNS []net.IP, clusterDomain, resolverConfig string) *Configurer {
75+
func NewConfigurer(recorder record.EventRecorder, nodeRef *v1.ObjectReference, nodeIPs []net.IP, clusterDNS []net.IP, clusterDomain, resolverConfig string) *Configurer {
7576
return &Configurer{
7677
recorder: recorder,
7778
nodeRef: nodeRef,
78-
nodeIP: nodeIP,
79+
nodeIPs: nodeIPs,
7980
clusterDNS: clusterDNS,
8081
ClusterDomain: clusterDomain,
8182
ResolverConfig: resolverConfig,
@@ -373,11 +374,15 @@ func (c *Configurer) GetPodDNS(pod *v1.Pod) (*runtimeapi.DNSConfig, error) {
373374
// local machine". A nameserver setting of localhost is equivalent to
374375
// this documented behavior.
375376
if c.ResolverConfig == "" {
376-
switch {
377-
case c.nodeIP == nil || c.nodeIP.To4() != nil:
378-
dnsConfig.Servers = []string{"127.0.0.1"}
379-
case c.nodeIP.To16() != nil:
380-
dnsConfig.Servers = []string{"::1"}
377+
for _, nodeIP := range c.nodeIPs {
378+
if utilnet.IsIPv6(nodeIP) {
379+
dnsConfig.Servers = append(dnsConfig.Servers, "::1")
380+
} else {
381+
dnsConfig.Servers = append(dnsConfig.Servers, "127.0.0.1")
382+
}
383+
}
384+
if len(dnsConfig.Servers) == 0 {
385+
dnsConfig.Servers = append(dnsConfig.Servers, "127.0.0.1")
381386
}
382387
dnsConfig.Searches = []string{"."}
383388
}

pkg/kubelet/nodestatus/setters.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,25 +57,40 @@ const (
5757
type Setter func(node *v1.Node) error
5858

5959
// NodeAddress returns a Setter that updates address-related information on the node.
60-
func NodeAddress(nodeIP net.IP, // typically Kubelet.nodeIP
60+
func NodeAddress(nodeIPs []net.IP, // typically Kubelet.nodeIPs
6161
validateNodeIPFunc func(net.IP) error, // typically Kubelet.nodeIPValidator
6262
hostname string, // typically Kubelet.hostname
6363
hostnameOverridden bool, // was the hostname force set?
6464
externalCloudProvider bool, // typically Kubelet.externalCloudProvider
6565
cloud cloudprovider.Interface, // typically Kubelet.cloud
6666
nodeAddressesFunc func() ([]v1.NodeAddress, error), // typically Kubelet.cloudResourceSyncManager.NodeAddresses
6767
) Setter {
68+
var nodeIP, secondaryNodeIP net.IP
69+
if len(nodeIPs) > 0 {
70+
nodeIP = nodeIPs[0]
71+
}
6872
preferIPv4 := nodeIP == nil || nodeIP.To4() != nil
6973
isPreferredIPFamily := func(ip net.IP) bool { return (ip.To4() != nil) == preferIPv4 }
7074
nodeIPSpecified := nodeIP != nil && !nodeIP.IsUnspecified()
7175

76+
if len(nodeIPs) > 1 {
77+
secondaryNodeIP = nodeIPs[1]
78+
}
79+
secondaryNodeIPSpecified := secondaryNodeIP != nil && !secondaryNodeIP.IsUnspecified()
80+
7281
return func(node *v1.Node) error {
7382
if nodeIPSpecified {
7483
if err := validateNodeIPFunc(nodeIP); err != nil {
7584
return fmt.Errorf("failed to validate nodeIP: %v", err)
7685
}
7786
klog.V(2).Infof("Using node IP: %q", nodeIP.String())
7887
}
88+
if secondaryNodeIPSpecified {
89+
if err := validateNodeIPFunc(secondaryNodeIP); err != nil {
90+
return fmt.Errorf("failed to validate secondaryNodeIP: %v", err)
91+
}
92+
klog.V(2).Infof("Using secondary node IP: %q", secondaryNodeIP.String())
93+
}
7994

8095
if externalCloudProvider {
8196
if nodeIPSpecified {
@@ -185,6 +200,12 @@ func NodeAddress(nodeIP net.IP, // typically Kubelet.nodeIP
185200
}
186201
}
187202
node.Status.Addresses = nodeAddresses
203+
} else if nodeIPSpecified && secondaryNodeIPSpecified {
204+
node.Status.Addresses = []v1.NodeAddress{
205+
{Type: v1.NodeInternalIP, Address: nodeIP.String()},
206+
{Type: v1.NodeInternalIP, Address: secondaryNodeIP.String()},
207+
{Type: v1.NodeHostName, Address: hostname},
208+
}
188209
} else {
189210
var ipAddr net.IP
190211
var err error

pkg/kubelet/nodestatus/setters_test.go

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ func TestNodeAddress(t *testing.T) {
416416
}
417417

418418
// construct setter
419-
setter := NodeAddress(nodeIP,
419+
setter := NodeAddress([]net.IP{nodeIP},
420420
nodeIPValidator,
421421
hostname,
422422
testCase.hostnameOverride,
@@ -439,6 +439,70 @@ func TestNodeAddress(t *testing.T) {
439439
}
440440
}
441441

442+
// We can't test failure or autodetection cases here because the relevant code isn't mockable
443+
func TestNodeAddress_NoCloudProvider(t *testing.T) {
444+
cases := []struct {
445+
name string
446+
nodeIPs []net.IP
447+
expectedAddresses []v1.NodeAddress
448+
}{
449+
{
450+
name: "Single --node-ip",
451+
nodeIPs: []net.IP{net.ParseIP("10.1.1.1")},
452+
expectedAddresses: []v1.NodeAddress{
453+
{Type: v1.NodeInternalIP, Address: "10.1.1.1"},
454+
{Type: v1.NodeHostName, Address: testKubeletHostname},
455+
},
456+
},
457+
{
458+
name: "Dual --node-ips",
459+
nodeIPs: []net.IP{net.ParseIP("10.1.1.1"), net.ParseIP("fd01::1234")},
460+
expectedAddresses: []v1.NodeAddress{
461+
{Type: v1.NodeInternalIP, Address: "10.1.1.1"},
462+
{Type: v1.NodeInternalIP, Address: "fd01::1234"},
463+
{Type: v1.NodeHostName, Address: testKubeletHostname},
464+
},
465+
},
466+
}
467+
for _, testCase := range cases {
468+
t.Run(testCase.name, func(t *testing.T) {
469+
// testCase setup
470+
existingNode := &v1.Node{
471+
ObjectMeta: metav1.ObjectMeta{Name: testKubeletHostname, Annotations: make(map[string]string)},
472+
Spec: v1.NodeSpec{},
473+
Status: v1.NodeStatus{
474+
Addresses: []v1.NodeAddress{},
475+
},
476+
}
477+
478+
nodeIPValidator := func(nodeIP net.IP) error {
479+
return nil
480+
}
481+
nodeAddressesFunc := func() ([]v1.NodeAddress, error) {
482+
return nil, fmt.Errorf("not reached")
483+
}
484+
485+
// construct setter
486+
setter := NodeAddress(testCase.nodeIPs,
487+
nodeIPValidator,
488+
testKubeletHostname,
489+
false, // hostnameOverridden
490+
false, // externalCloudProvider
491+
nil, // cloud
492+
nodeAddressesFunc)
493+
494+
// call setter on existing node
495+
err := setter(existingNode)
496+
if err != nil {
497+
t.Fatalf("unexpected error: %v", err)
498+
}
499+
500+
assert.True(t, apiequality.Semantic.DeepEqual(testCase.expectedAddresses, existingNode.Status.Addresses),
501+
"Diff: %s", diff.ObjectDiff(testCase.expectedAddresses, existingNode.Status.Addresses))
502+
})
503+
}
504+
}
505+
442506
func TestMachineInfo(t *testing.T) {
443507
const nodeName = "test-node"
444508

0 commit comments

Comments
 (0)