From 37c27fe654d2c7643df304ec6e8a97a844357f81 Mon Sep 17 00:00:00 2001 From: Lennart Jern Date: Tue, 7 Apr 2026 13:16:55 +0300 Subject: [PATCH] Add address support to PortOpenCheck - PortOpenCheck now accepts an address to check port availability on a specific interface. - InitNodeChecks uses the bind-address knowledge for the port checks. - For ETCD we check the advertise address instead Note: This works for bindAddress set through extraArgs. It will not work for patches that are applied later. Signed-off-by: Lennart Jern --- cmd/kubeadm/app/preflight/checks.go | 48 ++++++++++--- cmd/kubeadm/app/preflight/checks_test.go | 89 ++++++++++++++++++++---- 2 files changed, 111 insertions(+), 26 deletions(-) diff --git a/cmd/kubeadm/app/preflight/checks.go b/cmd/kubeadm/app/preflight/checks.go index c071293f862..d3109ec6553 100644 --- a/cmd/kubeadm/app/preflight/checks.go +++ b/cmd/kubeadm/app/preflight/checks.go @@ -31,6 +31,7 @@ import ( "os" "path/filepath" "runtime" + "strconv" "strings" "time" @@ -222,11 +223,13 @@ func (fc FirewalldCheck) Check() (warnings, errorList []error) { // PortOpenCheck ensures the given port is available for use. type PortOpenCheck struct { - port int - label string + address string + port int + label string + listenFunc func(string, string) (net.Listener, error) } -// Name returns name for PortOpenCheck. If not known, will return "PortXXXX" based on port number +// Name returns name for PortOpenCheck. If not known, will return "Port-XXXX" based on port number func (poc PortOpenCheck) Name() string { if poc.label != "" { return poc.label @@ -236,9 +239,19 @@ func (poc PortOpenCheck) Name() string { // Check validates if the particular port is available. func (poc PortOpenCheck) Check() (warnings, errorList []error) { - klog.V(1).Infof("validating availability of port %d", poc.port) + klog.V(1).Infof("validating availability of port %d on address %q", poc.port, poc.address) - ln, err := net.Listen("tcp", fmt.Sprintf(":%d", poc.port)) + listenAddress := ":" + strconv.Itoa(poc.port) + if poc.address != "" { + listenAddress = net.JoinHostPort(poc.address, strconv.Itoa(poc.port)) + } + + listen := poc.listenFunc + if listen == nil { + listen = net.Listen + } + + ln, err := listen("tcp", listenAddress) if err != nil { errorList = []error{errors.Errorf("Port %d is in use", poc.port)} } @@ -968,6 +981,13 @@ func (MemCheck) Name() string { // InitNodeChecks returns checks specific to "kubeadm init" func InitNodeChecks(execer utilsexec.Interface, cfg *kubeadmapi.InitConfiguration, ignorePreflightErrors sets.Set[string], isSecondaryControlPlane bool, downloadCerts bool) ([]Checker, error) { manifestsDir := filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.ManifestsSubDirName) + + // Check if the user has configured a bind address for control plane components + // in extraArgs. If so, use it for the PortOpenCheck to avoid false positives. + apiServerBindAddress, _ := kubeadmapi.GetArgValue(cfg.APIServer.ExtraArgs, "bind-address", -1) + schedulerBindAddress, _ := kubeadmapi.GetArgValue(cfg.Scheduler.ExtraArgs, "bind-address", -1) + controllerManagerBindAddress, _ := kubeadmapi.GetArgValue(cfg.ControllerManager.ExtraArgs, "bind-address", -1) + checks := []Checker{ NumCPUCheck{NumCPU: kubeadmconstants.ControlPlaneNumCPU}, // Linux only @@ -975,9 +995,9 @@ func InitNodeChecks(execer utilsexec.Interface, cfg *kubeadmapi.InitConfiguratio MemCheck{Mem: kubeadmconstants.ControlPlaneMem}, KubernetesVersionCheck{KubernetesVersion: cfg.KubernetesVersion, KubeadmVersion: kubeadmversion.Get().GitVersion}, FirewalldCheck{ports: []int{int(cfg.LocalAPIEndpoint.BindPort), kubeadmconstants.KubeletPort}}, - PortOpenCheck{port: int(cfg.LocalAPIEndpoint.BindPort)}, - PortOpenCheck{port: kubeadmconstants.KubeSchedulerPort}, - PortOpenCheck{port: kubeadmconstants.KubeControllerManagerPort}, + PortOpenCheck{port: int(cfg.LocalAPIEndpoint.BindPort), address: apiServerBindAddress}, + PortOpenCheck{port: kubeadmconstants.KubeSchedulerPort, address: schedulerBindAddress}, + PortOpenCheck{port: kubeadmconstants.KubeControllerManagerPort, address: controllerManagerBindAddress}, FileAvailableCheck{Path: kubeadmconstants.GetStaticPodFilepath(kubeadmconstants.KubeAPIServer, manifestsDir)}, FileAvailableCheck{Path: kubeadmconstants.GetStaticPodFilepath(kubeadmconstants.KubeControllerManager, manifestsDir)}, FileAvailableCheck{Path: kubeadmconstants.GetStaticPodFilepath(kubeadmconstants.KubeScheduler, manifestsDir)}, @@ -1044,10 +1064,16 @@ func InitNodeChecks(execer utilsexec.Interface, cfg *kubeadmapi.InitConfiguratio } if cfg.Etcd.Local != nil { - // Only do etcd related checks when required to install a local etcd + // Only do etcd related checks when required to install a local etcd. + // Etcd uses the same IP as the API server advertise address, not the bind address. + // Use the --advertise-address from extraArgs if defined, otherwise fall back to LocalAPIEndpoint.AdvertiseAddress. + etcdAddress := cfg.LocalAPIEndpoint.AdvertiseAddress + if advertiseAddress, _ := kubeadmapi.GetArgValue(cfg.APIServer.ExtraArgs, "advertise-address", -1); advertiseAddress != "" { + etcdAddress = advertiseAddress + } checks = append(checks, - PortOpenCheck{port: kubeadmconstants.EtcdListenClientPort}, - PortOpenCheck{port: kubeadmconstants.EtcdListenPeerPort}, + PortOpenCheck{port: kubeadmconstants.EtcdListenClientPort, address: etcdAddress}, + PortOpenCheck{port: kubeadmconstants.EtcdListenPeerPort, address: etcdAddress}, DirAvailableCheck{Path: cfg.Etcd.Local.DataDir}, ) } diff --git a/cmd/kubeadm/app/preflight/checks_test.go b/cmd/kubeadm/app/preflight/checks_test.go index 1f61802617b..726c63bdae1 100644 --- a/cmd/kubeadm/app/preflight/checks_test.go +++ b/cmd/kubeadm/app/preflight/checks_test.go @@ -383,12 +383,22 @@ func TestDirAvailableCheck(t *testing.T) { } } +// mockNetListener is a minimal implementation of net.Listener used for testing +// PortOpenCheck without relying on real network sockets. +type mockNetListener struct{} + +func (m *mockNetListener) Accept() (net.Conn, error) { return nil, nil } +func (m *mockNetListener) Close() error { return nil } +func (m *mockNetListener) Addr() net.Addr { return nil } + func TestPortOpenCheck(t *testing.T) { - ln, err := net.Listen("tcp", ":0") - if err != nil { - t.Fatalf("could not listen on local network: %v", err) + mockListenSuccess := func(string, string) (net.Listener, error) { + return &mockNetListener{}, nil } - defer ln.Close() + mockListenFail := func(string, string) (net.Listener, error) { + return nil, fmt.Errorf("address already in use") + } + var tests = []struct { name string check PortOpenCheck @@ -396,25 +406,74 @@ func TestPortOpenCheck(t *testing.T) { }{ { name: "Port is available", - check: PortOpenCheck{port: 0}, + check: PortOpenCheck{port: 6443, listenFunc: mockListenSuccess}, expectedError: false, }, { name: "Port is not available", - check: PortOpenCheck{port: ln.Addr().(*net.TCPAddr).Port}, + check: PortOpenCheck{port: 6443, listenFunc: mockListenFail}, + expectedError: true, + }, + { + name: "Port bound on 127.0.0.1 is available on 127.0.0.2", + check: PortOpenCheck{port: 6443, address: "127.0.0.2", listenFunc: mockListenSuccess}, + expectedError: false, + }, + { + name: "Port bound on 127.0.0.1 is not available on 127.0.0.1", + check: PortOpenCheck{port: 6443, address: "127.0.0.1", listenFunc: mockListenFail}, expectedError: true, }, } + for _, rt := range tests { - _, output := rt.check.Check() - if (output != nil) != rt.expectedError { - t.Errorf( - "Failed PortOpenCheck:%v\n\texpectedError: %t\n\t actual: %t", - rt.name, - rt.expectedError, - (output != nil), - ) - } + t.Run(rt.name, func(t *testing.T) { + _, output := rt.check.Check() + if (output != nil) != rt.expectedError { + t.Errorf( + "Failed PortOpenCheck:%v\n\texpectedError: %t\n\t actual: %t", + rt.name, + rt.expectedError, + (output != nil), + ) + } + }) + } +} + +func TestPortOpenCheckName(t *testing.T) { + var tests = []struct { + name string + check PortOpenCheck + expected string + }{ + { + name: "Port only", + check: PortOpenCheck{port: 6443}, + expected: "Port-6443", + }, + { + name: "Port with label", + check: PortOpenCheck{port: 6443, label: "MyLabel"}, + expected: "MyLabel", + }, + { + name: "Port with address", + check: PortOpenCheck{port: 6443, address: "10.0.0.1"}, + expected: "Port-6443", + }, + { + name: "Port with address and label", + check: PortOpenCheck{port: 6443, address: "10.0.0.1", label: "MyLabel"}, + expected: "MyLabel", + }, + } + for _, rt := range tests { + t.Run(rt.name, func(t *testing.T) { + if rt.check.Name() != rt.expected { + t.Errorf("expected name %q, got %q", rt.expected, rt.check.Name()) + } + }) } }