From be0a49b221e10f23c787e4c15859b2e781f373b9 Mon Sep 17 00:00:00 2001 From: "Lubomir I. Ivanov" Date: Fri, 17 Apr 2026 18:47:35 +0200 Subject: [PATCH] kubeadm: use the localAPIEndpoint for all API calls in 'init' Historicaly the kubeadm clients have used the 'admin.conf' and 'super-admin.conf' directly, which makes all API calls go trough the CPE (control plane endpoint). This can create problems for scenarios when the LB is provisioned only after 'init' starts the kube-apiserver. Instead of using the '.conf' as they are, modify them in memory to point to the LAE (localAPIEndpoint). This was already done by the WaitControlPlaneClient for the WaitControlPlane phase, which required it. This separate client is no longer needed. However, do use a unmodified kubeconfig to the init phase bootstrap-token since this is the phase that creates the cluster-info CM and for that we need the original CPE server address. --- cmd/kubeadm/app/cmd/init.go | 58 ++++++++++--------- .../app/cmd/phases/init/bootstraptoken.go | 2 +- cmd/kubeadm/app/cmd/phases/init/data.go | 2 +- cmd/kubeadm/app/cmd/phases/init/data_test.go | 42 +++++++------- .../app/cmd/phases/init/waitcontrolplane.go | 2 +- .../app/phases/kubeconfig/kubeconfig.go | 16 ++++- .../app/phases/kubeconfig/kubeconfig_test.go | 2 +- cmd/kubeadm/app/util/kubeconfig/kubeconfig.go | 16 +++++ 8 files changed, 84 insertions(+), 56 deletions(-) diff --git a/cmd/kubeadm/app/cmd/init.go b/cmd/kubeadm/app/cmd/init.go index 0c548ba4000..85a9f87e156 100644 --- a/cmd/kubeadm/app/cmd/init.go +++ b/cmd/kubeadm/app/cmd/init.go @@ -19,10 +19,8 @@ package cmd import ( "fmt" "io" - "net" "os" "path/filepath" - "strconv" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -92,6 +90,7 @@ type initData struct { skipTokenPrint bool dryRun bool kubeconfig *clientcmdapi.Config + kubeconfigOriginal *clientcmdapi.Config kubeconfigDir string kubeconfigPath string ignorePreflightErrors sets.Set[string] @@ -462,6 +461,9 @@ func (d *initData) CertificateDir() string { } // KubeConfig returns a kubeconfig after loading it from KubeConfigPath(). +// If the default kubeconfig path is used (admin.conf), instead of constructing +// a kubeconfig that points to the control plane endpoint, make it point to the localAPIEndpoint. +// This would allow 'kubeadm init' to only talk to the local kube-apiserver instance. func (d *initData) KubeConfig() (*clientcmdapi.Config, error) { if d.kubeconfig != nil { return d.kubeconfig, nil @@ -472,10 +474,26 @@ func (d *initData) KubeConfig() (*clientcmdapi.Config, error) { if err != nil { return nil, err } + d.kubeconfigOriginal = d.kubeconfig.DeepCopy() + + if d.kubeconfigPath == kubeadmconstants.GetAdminKubeConfigPath() { + kubeconfigutil.PointKubeConfigToLocalAPIEndpoint(d.kubeconfig, &d.Cfg().LocalAPIEndpoint) + } return d.kubeconfig, nil } +// KubeConfigOriginal returns the original kubeconfig loaded from file, without any modifications. +func (d *initData) KubeConfigOriginal() (*clientcmdapi.Config, error) { + if d.kubeconfigOriginal == nil { + if _, err := d.KubeConfig(); err != nil { + return nil, err + } + } + + return d.kubeconfigOriginal, nil +} + // KubeConfigDir returns the path of the Kubernetes configuration folder or the temporary folder path in case of DryRun. func (d *initData) KubeConfigDir() string { if d.dryRun { @@ -520,8 +538,12 @@ func (d *initData) OutputWriter() io.Writer { // getDryRunClient creates a fake client that answers some GET calls in order to be able to do the full init flow in dry-run mode. func getDryRunClient(d *initData) (clientset.Interface, error) { + kubeconfig, err := d.KubeConfig() + if err != nil { + return nil, err + } dryRun := apiclient.NewDryRun() - if err := dryRun.WithKubeConfigFile(d.KubeConfigPath()); err != nil { + if err := dryRun.WithKubeConfig(kubeconfig); err != nil { return nil, err } dryRun.WithDefaultMarshalFunction(). @@ -549,7 +571,11 @@ func (d *initData) Client() (clientset.Interface, error) { // and if the bootstrapping was not already done if !d.adminKubeConfigBootstrapped && isDefaultKubeConfigPath { // Call EnsureAdminClusterRoleBinding() to obtain a working client from admin.conf. - d.client, err = kubeconfigphase.EnsureAdminClusterRoleBinding(kubeadmconstants.KubernetesDir, nil) + d.client, err = kubeconfigphase.EnsureAdminClusterRoleBinding( + kubeadmconstants.KubernetesDir, + &d.Cfg().LocalAPIEndpoint, + nil, + ) if err != nil { return nil, errors.Wrapf(err, "could not bootstrap the admin user in file %s", kubeadmconstants.AdminKubeConfigFileName) } @@ -570,30 +596,6 @@ func (d *initData) Client() (clientset.Interface, error) { return d.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 (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", - net.JoinHostPort( - d.Cfg().LocalAPIEndpoint.AdvertiseAddress, - strconv.Itoa(int(d.Cfg().LocalAPIEndpoint.BindPort)), - ), - ) - } - client, err := kubeconfigutil.ToClientSet(config) - if err != nil { - return nil, err - } - return client, nil -} - // Tokens returns an array of token strings. func (d *initData) Tokens() []string { tokens := []string{} diff --git a/cmd/kubeadm/app/cmd/phases/init/bootstraptoken.go b/cmd/kubeadm/app/cmd/phases/init/bootstraptoken.go index 21f89e081aa..33296e6fda4 100644 --- a/cmd/kubeadm/app/cmd/phases/init/bootstraptoken.go +++ b/cmd/kubeadm/app/cmd/phases/init/bootstraptoken.go @@ -72,7 +72,7 @@ func runBootstrapToken(c workflow.RunData) error { if err != nil { return err } - kubeconfig, err := data.KubeConfig() + kubeconfig, err := data.KubeConfigOriginal() if err != nil { return err } diff --git a/cmd/kubeadm/app/cmd/phases/init/data.go b/cmd/kubeadm/app/cmd/phases/init/data.go index feaf95fd299..3af216f7474 100644 --- a/cmd/kubeadm/app/cmd/phases/init/data.go +++ b/cmd/kubeadm/app/cmd/phases/init/data.go @@ -40,6 +40,7 @@ type InitData interface { CertificateWriteDir() string CertificateDir() string KubeConfig() (*clientcmdapi.Config, error) + KubeConfigOriginal() (*clientcmdapi.Config, error) KubeConfigDir() string KubeConfigPath() string ManifestDir() string @@ -47,7 +48,6 @@ type InitData interface { ExternalCA() bool OutputWriter() io.Writer Client() (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 afdbc100f25..b64eb4de8b8 100644 --- a/cmd/kubeadm/app/cmd/phases/init/data_test.go +++ b/cmd/kubeadm/app/cmd/phases/init/data_test.go @@ -32,24 +32,24 @@ type testInitData struct{} // testInitData must satisfy InitData. var _ InitData = &testInitData{} -func (t *testInitData) UploadCerts() bool { return false } -func (t *testInitData) CertificateKey() string { return "" } -func (t *testInitData) SetCertificateKey(key string) {} -func (t *testInitData) SkipCertificateKeyPrint() bool { return false } -func (t *testInitData) Cfg() *kubeadmapi.InitConfiguration { return nil } -func (t *testInitData) DryRun() bool { return false } -func (t *testInitData) SkipTokenPrint() bool { return false } -func (t *testInitData) IgnorePreflightErrors() sets.Set[string] { return nil } -func (t *testInitData) CertificateWriteDir() string { return "" } -func (t *testInitData) CertificateDir() string { return "" } -func (t *testInitData) KubeConfig() (*clientcmdapi.Config, error) { return nil, nil } -func (t *testInitData) KubeConfigDir() string { return "" } -func (t *testInitData) KubeConfigPath() string { return "" } -func (t *testInitData) ManifestDir() string { return "" } -func (t *testInitData) KubeletDir() string { return "" } -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) WaitControlPlaneClient() (clientset.Interface, error) { return nil, nil } -func (t *testInitData) Tokens() []string { return nil } -func (t *testInitData) PatchesDir() string { return "" } +func (t *testInitData) UploadCerts() bool { return false } +func (t *testInitData) CertificateKey() string { return "" } +func (t *testInitData) SetCertificateKey(key string) {} +func (t *testInitData) SkipCertificateKeyPrint() bool { return false } +func (t *testInitData) Cfg() *kubeadmapi.InitConfiguration { return nil } +func (t *testInitData) DryRun() bool { return false } +func (t *testInitData) SkipTokenPrint() bool { return false } +func (t *testInitData) IgnorePreflightErrors() sets.Set[string] { return nil } +func (t *testInitData) CertificateWriteDir() string { return "" } +func (t *testInitData) CertificateDir() string { return "" } +func (t *testInitData) KubeConfig() (*clientcmdapi.Config, error) { return nil, nil } +func (t *testInitData) KubeConfigOriginal() (*clientcmdapi.Config, error) { return nil, nil } +func (t *testInitData) KubeConfigDir() string { return "" } +func (t *testInitData) KubeConfigPath() string { return "" } +func (t *testInitData) ManifestDir() string { return "" } +func (t *testInitData) KubeletDir() string { return "" } +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) 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 50b89d3ac1b..7bb691bfd73 100644 --- a/cmd/kubeadm/app/cmd/phases/init/waitcontrolplane.go +++ b/cmd/kubeadm/app/cmd/phases/init/waitcontrolplane.go @@ -63,7 +63,7 @@ func runWaitControlPlanePhase(c workflow.RunData) error { } } - client, err := data.WaitControlPlaneClient() + client, err := data.Client() if err != nil { return errors.Wrap(err, "cannot obtain client without bootstrap") } diff --git a/cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go b/cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go index c1fcca242b7..7af15be2dea 100644 --- a/cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go +++ b/cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go @@ -623,14 +623,19 @@ type EnsureRBACFunc func(context.Context, clientset.Interface, clientset.Interfa // constructs a client from super-admin.conf if the file exists. It then proceeds // to pass the clients to EnsureAdminClusterRoleBindingImpl. The function returns a // usable client from admin.conf with RBAC properly constructed or an error. -func EnsureAdminClusterRoleBinding(outDir string, ensureRBACFunc EnsureRBACFunc) (clientset.Interface, error) { +func EnsureAdminClusterRoleBinding(outDir string, lae *kubeadmapi.APIEndpoint, ensureRBACFunc EnsureRBACFunc) (clientset.Interface, error) { var ( err error adminClient, superAdminClient clientset.Interface ) // Create a client from admin.conf. - adminClient, err = kubeconfigutil.ClientSetFromFile(filepath.Join(outDir, kubeadmconstants.AdminKubeConfigFileName)) + kubeconfig, err := clientcmd.LoadFromFile(filepath.Join(outDir, kubeadmconstants.AdminKubeConfigFileName)) + if err != nil { + return nil, err + } + kubeconfigutil.PointKubeConfigToLocalAPIEndpoint(kubeconfig, lae) + adminClient, err = kubeconfigutil.ToClientSet(kubeconfig) if err != nil { return nil, err } @@ -638,7 +643,12 @@ func EnsureAdminClusterRoleBinding(outDir string, ensureRBACFunc EnsureRBACFunc) // Create a client from super-admin.conf. superAdminPath := filepath.Join(outDir, kubeadmconstants.SuperAdminKubeConfigFileName) if _, err := os.Stat(superAdminPath); err == nil { - superAdminClient, err = kubeconfigutil.ClientSetFromFile(superAdminPath) + kubeconfig, err := clientcmd.LoadFromFile(superAdminPath) + if err != nil { + return nil, err + } + kubeconfigutil.PointKubeConfigToLocalAPIEndpoint(kubeconfig, lae) + superAdminClient, err = kubeconfigutil.ToClientSet(kubeconfig) if err != nil { return nil, err } diff --git a/cmd/kubeadm/app/phases/kubeconfig/kubeconfig_test.go b/cmd/kubeadm/app/phases/kubeconfig/kubeconfig_test.go index 0418beca5b5..17a6fc600e1 100644 --- a/cmd/kubeadm/app/phases/kubeconfig/kubeconfig_test.go +++ b/cmd/kubeadm/app/phases/kubeconfig/kubeconfig_test.go @@ -991,7 +991,7 @@ func TestEnsureAdminClusterRoleBinding(t *testing.T) { } } - client, err := EnsureAdminClusterRoleBinding(dir, ensureRBACFunc) + client, err := EnsureAdminClusterRoleBinding(dir, &kubeadmapi.APIEndpoint{}, ensureRBACFunc) if (err != nil) != tc.expectedError { t.Fatalf("expected error: %v, got: %v, error: %v", err != nil, tc.expectedError, err) } diff --git a/cmd/kubeadm/app/util/kubeconfig/kubeconfig.go b/cmd/kubeadm/app/util/kubeconfig/kubeconfig.go index f7b465ed7da..518c187962f 100644 --- a/cmd/kubeadm/app/util/kubeconfig/kubeconfig.go +++ b/cmd/kubeadm/app/util/kubeconfig/kubeconfig.go @@ -18,13 +18,17 @@ package kubeconfig import ( "fmt" + "net" "os" + "strconv" "github.com/pkg/errors" clientset "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + + kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" ) // CreateBasic creates a basic, general KubeConfig object that then can be extended @@ -121,6 +125,18 @@ func GetClusterFromKubeConfig(config *clientcmdapi.Config) (string, *clientcmdap return "", nil, errors.Errorf("the current context is invalid: %s", config.CurrentContext) } +// PointKubeConfigToLocalAPIEndpoint modifies the provided kubeconfig to point to the given APIEndpoint. +func PointKubeConfigToLocalAPIEndpoint(config *clientcmdapi.Config, lae *kubeadmapi.APIEndpoint) { + for _, v := range config.Clusters { + v.Server = fmt.Sprintf("https://%s", + net.JoinHostPort( + lae.AdvertiseAddress, + strconv.Itoa(int(lae.BindPort)), + ), + ) + } +} + // HasAuthenticationCredentials returns true if the current user has valid authentication credentials for // token authentication, basic authentication or X509 authentication func HasAuthenticationCredentials(config *clientcmdapi.Config) bool {