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.
This commit is contained in:
Lubomir I. Ivanov
2025-09-25 13:45:24 +02:00
parent fcaf057eb2
commit 9f99111f42
8 changed files with 58 additions and 36 deletions

View File

@@ -568,24 +568,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
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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 "" }

View File

@@ -63,8 +63,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")
}

View File

@@ -38,6 +38,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

View File

@@ -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 "" }

View File

@@ -67,7 +67,7 @@ func runWaitControlPlanePhase(c workflow.RunData) error {
return nil
}
client, err := data.Client()
client, err := data.WaitControlPlaneClient()
if err != nil {
return err
}