diff --git a/cmd/kubeadm/app/cmd/init.go b/cmd/kubeadm/app/cmd/init.go index 9d6ba55bc0da..eae504c15c40 100644 --- a/cmd/kubeadm/app/cmd/init.go +++ b/cmd/kubeadm/app/cmd/init.go @@ -496,21 +496,28 @@ func (d *initData) OutputWriter() io.Writer { return d.outputWriter } +// 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) { + svcSubnetCIDR, err := kubeadmconstants.GetKubernetesServiceCIDR(d.cfg.Networking.ServiceSubnet) + if err != nil { + return nil, errors.Wrapf(err, "unable to get internal Kubernetes Service IP from the given service CIDR (%s)", d.cfg.Networking.ServiceSubnet) + } + dryRunGetter := apiclient.NewInitDryRunGetter(d.cfg.NodeRegistration.Name, svcSubnetCIDR.String()) + return apiclient.NewDryRunClient(dryRunGetter, os.Stdout), nil +} + // Client returns a Kubernetes client to be used by kubeadm. // This function is implemented as a singleton, thus avoiding to recreate the client when it is used by different phases. // Important. This function must be called after the admin.conf kubeconfig file is created. func (d *initData) Client() (clientset.Interface, error) { + var err error if d.client == nil { if d.dryRun { - svcSubnetCIDR, err := kubeadmconstants.GetKubernetesServiceCIDR(d.cfg.Networking.ServiceSubnet) + d.client, err = getDryRunClient(d) if err != nil { - return nil, errors.Wrapf(err, "unable to get internal Kubernetes Service IP from the given service CIDR (%s)", d.cfg.Networking.ServiceSubnet) + return nil, err } - // If we're dry-running, we should create a faked client that answers some GETs in order to be able to do the full init flow and just logs the rest of requests - dryRunGetter := apiclient.NewInitDryRunGetter(d.cfg.NodeRegistration.Name, svcSubnetCIDR.String()) - d.client = apiclient.NewDryRunClient(dryRunGetter, os.Stdout) } else { // Use a real client - var err error if !d.adminKubeConfigBootstrapped { // Call EnsureAdminClusterRoleBinding() to obtain a working client from admin.conf. d.client, err = kubeconfigphase.EnsureAdminClusterRoleBinding(kubeadmconstants.KubernetesDir, nil) @@ -531,6 +538,28 @@ 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 + } + } + 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/data.go b/cmd/kubeadm/app/cmd/phases/init/data.go index db0f3b707e33..4560e03cbc8e 100644 --- a/cmd/kubeadm/app/cmd/phases/init/data.go +++ b/cmd/kubeadm/app/cmd/phases/init/data.go @@ -45,6 +45,7 @@ type InitData interface { ExternalCA() bool OutputWriter() io.Writer Client() (clientset.Interface, error) + ClientWithoutBootstrap() (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 9c711499e965..11c6ea9b5360 100644 --- a/cmd/kubeadm/app/cmd/phases/init/data_test.go +++ b/cmd/kubeadm/app/cmd/phases/init/data_test.go @@ -31,22 +31,23 @@ 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) 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 "" } +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) 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) ClientWithoutBootstrap() (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 a0af2810b364..bbe023ae3db5 100644 --- a/cmd/kubeadm/app/cmd/phases/init/waitcontrolplane.go +++ b/cmd/kubeadm/app/cmd/phases/init/waitcontrolplane.go @@ -82,9 +82,10 @@ func runWaitControlPlanePhase(c workflow.RunData) error { // waiter holds the apiclient.Waiter implementation of choice, responsible for querying the API server in various ways and waiting for conditions to be fulfilled klog.V(1).Infoln("[wait-control-plane] Waiting for the API server to be healthy") - client, err := data.Client() + // WaitForAPI uses the /healthz endpoint, thus a client without permissions works fine + client, err := data.ClientWithoutBootstrap() if err != nil { - return errors.Wrap(err, "cannot obtain client") + return errors.Wrap(err, "cannot obtain client without bootstrap") } timeout := data.Cfg().ClusterConfiguration.APIServer.TimeoutForControlPlane.Duration