From 0010d3992ac5feaa754b462bd8c44e1ae5585654 Mon Sep 17 00:00:00 2001 From: "Lubomir I. Ivanov" Date: Thu, 25 Sep 2025 13:45:24 +0200 Subject: [PATCH] kubeadm: ensure waiting for apiserver uses a local client When waiting for the kube-apiserver to report 'ok' in the 'init' and 'join' phase 'wait-control-plane', a client constructed from the 'admin.conf' is used. In the case of the kube-apiserver, the discovery client is used so that anonymous-auth works. But if 'admin.conf' is used as is, it would point to the CPE and not the LAE. Implement a new method WaitControlPlaneClient() for both init.go and join.go that patches the 'Server' field in the loaded v1.Config to point to the LAE, before constructing a client set and using it in the kube-apiserver waiter. --- cmd/kubeadm/app/cmd/init.go | 35 +++++++++---------- cmd/kubeadm/app/cmd/join.go | 22 ++++++++++++ cmd/kubeadm/app/cmd/phases/init/data.go | 2 +- cmd/kubeadm/app/cmd/phases/init/data_test.go | 2 +- .../app/cmd/phases/init/waitcontrolplane.go | 3 +- cmd/kubeadm/app/cmd/phases/join/data.go | 1 + cmd/kubeadm/app/cmd/phases/join/data_test.go | 27 +++++++------- .../app/cmd/phases/join/waitcontrolplane.go | 2 +- 8 files changed, 58 insertions(+), 36 deletions(-) diff --git a/cmd/kubeadm/app/cmd/init.go b/cmd/kubeadm/app/cmd/init.go index 7927c0f0195..b5db93d71f9 100644 --- a/cmd/kubeadm/app/cmd/init.go +++ b/cmd/kubeadm/app/cmd/init.go @@ -569,24 +569,23 @@ func (d *initData) Client() (clientset.Interface, error) { return d.client, nil } -// ClientWithoutBootstrap returns a dry-run client or a regular client from admin.conf. -// Unlike Client(), it does not call EnsureAdminClusterRoleBinding() or sets d.client. -// This means the client only has anonymous permissions and does not persist in initData. -func (d *initData) ClientWithoutBootstrap() (clientset.Interface, error) { - var ( - client clientset.Interface - err error - ) - if d.dryRun { - client, err = getDryRunClient(d) - if err != nil { - return nil, err - } - } else { // Use a real client - client, err = kubeconfigutil.ClientSetFromFile(d.KubeConfigPath()) - if err != nil { - return nil, err - } +// WaitControlPlaneClient returns a basic client used for the purpose of waiting +// for control plane components to report 'ok' on their respective health check endpoints. +// It uses the admin.conf as the base, but modifies it to point at the local API server instead +// of the control plane endpoint. +func (d *initData) WaitControlPlaneClient() (clientset.Interface, error) { + config, err := clientcmd.LoadFromFile(d.KubeConfigPath()) + if err != nil { + return nil, err + } + for _, v := range config.Clusters { + v.Server = fmt.Sprintf("https://%s:%d", + d.Cfg().LocalAPIEndpoint.AdvertiseAddress, + d.Cfg().LocalAPIEndpoint.BindPort) + } + client, err := kubeconfigutil.ToClientSet(config) + if err != nil { + return nil, err } return client, nil } diff --git a/cmd/kubeadm/app/cmd/join.go b/cmd/kubeadm/app/cmd/join.go index 1762fc98284..0d47065d23e 100644 --- a/cmd/kubeadm/app/cmd/join.go +++ b/cmd/kubeadm/app/cmd/join.go @@ -625,6 +625,28 @@ func (j *joinData) Client() (clientset.Interface, error) { return client, nil } +// WaitControlPlaneClient returns a basic client used for the purpose of waiting +// for control plane components to report 'ok' on their respective health check endpoints. +// It uses the admin.conf as the base, but modifies it to point at the local API server instead +// of the control plane endpoint. +func (j *joinData) WaitControlPlaneClient() (clientset.Interface, error) { + pathAdmin := filepath.Join(j.KubeConfigDir(), kubeadmconstants.AdminKubeConfigFileName) + config, err := clientcmd.LoadFromFile(pathAdmin) + if err != nil { + return nil, err + } + for _, v := range config.Clusters { + v.Server = fmt.Sprintf("https://%s:%d", + j.Cfg().ControlPlane.LocalAPIEndpoint.AdvertiseAddress, + j.Cfg().ControlPlane.LocalAPIEndpoint.BindPort) + } + client, err := kubeconfigutil.ToClientSet(config) + if err != nil { + return nil, err + } + return client, nil +} + // IgnorePreflightErrors returns the list of preflight errors to ignore. func (j *joinData) IgnorePreflightErrors() sets.Set[string] { return j.ignorePreflightErrors diff --git a/cmd/kubeadm/app/cmd/phases/init/data.go b/cmd/kubeadm/app/cmd/phases/init/data.go index 21bf0778039..6e7b8ba3a6b 100644 --- a/cmd/kubeadm/app/cmd/phases/init/data.go +++ b/cmd/kubeadm/app/cmd/phases/init/data.go @@ -47,7 +47,7 @@ type InitData interface { ExternalCA() bool OutputWriter() io.Writer Client() (clientset.Interface, error) - ClientWithoutBootstrap() (clientset.Interface, error) + WaitControlPlaneClient() (clientset.Interface, error) Tokens() []string PatchesDir() string } diff --git a/cmd/kubeadm/app/cmd/phases/init/data_test.go b/cmd/kubeadm/app/cmd/phases/init/data_test.go index 8229cfc76c8..8465021446a 100644 --- a/cmd/kubeadm/app/cmd/phases/init/data_test.go +++ b/cmd/kubeadm/app/cmd/phases/init/data_test.go @@ -50,6 +50,6 @@ func (t *testInitData) KubeletDir() string { r func (t *testInitData) ExternalCA() bool { return false } func (t *testInitData) OutputWriter() io.Writer { return nil } func (t *testInitData) Client() (clientset.Interface, error) { return nil, nil } -func (t *testInitData) ClientWithoutBootstrap() (clientset.Interface, error) { return nil, nil } +func (t *testInitData) WaitControlPlaneClient() (clientset.Interface, error) { return nil, nil } func (t *testInitData) Tokens() []string { return nil } func (t *testInitData) PatchesDir() string { return "" } diff --git a/cmd/kubeadm/app/cmd/phases/init/waitcontrolplane.go b/cmd/kubeadm/app/cmd/phases/init/waitcontrolplane.go index 51308db2896..c93d02aef1c 100644 --- a/cmd/kubeadm/app/cmd/phases/init/waitcontrolplane.go +++ b/cmd/kubeadm/app/cmd/phases/init/waitcontrolplane.go @@ -58,8 +58,7 @@ func runWaitControlPlanePhase(c workflow.RunData) error { } } - // Both Wait* calls below use a /healthz endpoint, thus a client without permissions works fine - client, err := data.ClientWithoutBootstrap() + client, err := data.WaitControlPlaneClient() if err != nil { return errors.Wrap(err, "cannot obtain client without bootstrap") } diff --git a/cmd/kubeadm/app/cmd/phases/join/data.go b/cmd/kubeadm/app/cmd/phases/join/data.go index bbfb8730cbe..9e04d9032f9 100644 --- a/cmd/kubeadm/app/cmd/phases/join/data.go +++ b/cmd/kubeadm/app/cmd/phases/join/data.go @@ -35,6 +35,7 @@ type JoinData interface { TLSBootstrapCfg() (*clientcmdapi.Config, error) InitCfg() (*kubeadmapi.InitConfiguration, error) Client() (clientset.Interface, error) + WaitControlPlaneClient() (clientset.Interface, error) IgnorePreflightErrors() sets.Set[string] OutputWriter() io.Writer PatchesDir() string diff --git a/cmd/kubeadm/app/cmd/phases/join/data_test.go b/cmd/kubeadm/app/cmd/phases/join/data_test.go index c03e06bea1b..5995acf7d73 100644 --- a/cmd/kubeadm/app/cmd/phases/join/data_test.go +++ b/cmd/kubeadm/app/cmd/phases/join/data_test.go @@ -32,16 +32,17 @@ type testJoinData struct{} // testJoinData must satisfy JoinData. var _ JoinData = &testJoinData{} -func (j *testJoinData) CertificateKey() string { return "" } -func (j *testJoinData) Cfg() *kubeadmapi.JoinConfiguration { return nil } -func (j *testJoinData) TLSBootstrapCfg() (*clientcmdapi.Config, error) { return nil, nil } -func (j *testJoinData) InitCfg() (*kubeadmapi.InitConfiguration, error) { return nil, nil } -func (j *testJoinData) Client() (clientset.Interface, error) { return nil, nil } -func (j *testJoinData) IgnorePreflightErrors() sets.Set[string] { return nil } -func (j *testJoinData) OutputWriter() io.Writer { return nil } -func (j *testJoinData) PatchesDir() string { return "" } -func (j *testJoinData) DryRun() bool { return false } -func (j *testJoinData) KubeConfigDir() string { return "" } -func (j *testJoinData) KubeletDir() string { return "" } -func (j *testJoinData) ManifestDir() string { return "" } -func (j *testJoinData) CertificateWriteDir() string { return "" } +func (j *testJoinData) CertificateKey() string { return "" } +func (j *testJoinData) Cfg() *kubeadmapi.JoinConfiguration { return nil } +func (j *testJoinData) TLSBootstrapCfg() (*clientcmdapi.Config, error) { return nil, nil } +func (j *testJoinData) InitCfg() (*kubeadmapi.InitConfiguration, error) { return nil, nil } +func (j *testJoinData) Client() (clientset.Interface, error) { return nil, nil } +func (j *testJoinData) WaitControlPlaneClient() (clientset.Interface, error) { return nil, nil } +func (j *testJoinData) IgnorePreflightErrors() sets.Set[string] { return nil } +func (j *testJoinData) OutputWriter() io.Writer { return nil } +func (j *testJoinData) PatchesDir() string { return "" } +func (j *testJoinData) DryRun() bool { return false } +func (j *testJoinData) KubeConfigDir() string { return "" } +func (j *testJoinData) KubeletDir() string { return "" } +func (j *testJoinData) ManifestDir() string { return "" } +func (j *testJoinData) CertificateWriteDir() string { return "" } diff --git a/cmd/kubeadm/app/cmd/phases/join/waitcontrolplane.go b/cmd/kubeadm/app/cmd/phases/join/waitcontrolplane.go index ba4bb82b346..bd29eabbf60 100644 --- a/cmd/kubeadm/app/cmd/phases/join/waitcontrolplane.go +++ b/cmd/kubeadm/app/cmd/phases/join/waitcontrolplane.go @@ -50,7 +50,7 @@ func runWaitControlPlanePhase(c workflow.RunData) error { return nil } - client, err := data.Client() + client, err := data.WaitControlPlaneClient() if err != nil { return err }