Do not reload kubeconfig from disk

When `kubeadm init phase bootstrap-token` gets invoked, it reads
the kubeconfig from disk repeatedly. This is wasteful, but, more
importantly, it blocks the use of `/dev/stdin` and other sources
of data that cannot be read repeatedly.

This change introduces a new field that caches a parsed kubeconfig
and when a new clientset is requested, it is converted from
this pre-parsed kubeconfig, the code no longer reaches out to disk.
This commit is contained in:
Ondrej Kokes 2024-11-27 15:06:58 +01:00
parent 9d62330bfa
commit 6f06cd6e05
6 changed files with 50 additions and 35 deletions

View File

@ -28,6 +28,8 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"k8s.io/klog/v2"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
@ -87,6 +89,7 @@ type initData struct {
cfg *kubeadmapi.InitConfiguration
skipTokenPrint bool
dryRun bool
kubeconfig *clientcmdapi.Config
kubeconfigDir string
kubeconfigPath string
ignorePreflightErrors sets.Set[string]
@ -456,6 +459,21 @@ func (d *initData) CertificateDir() string {
return d.certificatesDir
}
// KubeConfig returns a kubeconfig after loading it from KubeConfigPath().
func (d *initData) KubeConfig() (*clientcmdapi.Config, error) {
if d.kubeconfig != nil {
return d.kubeconfig, nil
}
var err error
d.kubeconfig, err = clientcmd.LoadFromFile(d.KubeConfigPath())
if err != nil {
return nil, err
}
return d.kubeconfig, 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 {
@ -536,7 +554,11 @@ func (d *initData) Client() (clientset.Interface, error) {
d.adminKubeConfigBootstrapped = true
} else {
// Alternatively, just load the config pointed at the --kubeconfig path
d.client, err = kubeconfigutil.ClientSetFromFile(d.KubeConfigPath())
cfg, err := d.KubeConfig()
if err != nil {
return nil, err
}
d.client, err = kubeconfigutil.ToClientSet(cfg)
if err != nil {
return nil, err
}

View File

@ -72,6 +72,10 @@ func runBootstrapToken(c workflow.RunData) error {
if err != nil {
return err
}
kubeconfig, err := data.KubeConfig()
if err != nil {
return err
}
if !data.SkipTokenPrint() {
tokens := data.Tokens()
@ -106,7 +110,7 @@ func runBootstrapToken(c workflow.RunData) error {
}
// Create the cluster-info ConfigMap with the associated RBAC rules
if err := clusterinfophase.CreateBootstrapConfigMapIfNotExists(client, data.KubeConfigPath()); err != nil {
if err := clusterinfophase.CreateBootstrapConfigMapIfNotExists(client, kubeconfig); err != nil {
return errors.Wrap(err, "error creating bootstrap ConfigMap")
}
if err := clusterinfophase.CreateClusterInfoRBACRules(client); err != nil {

View File

@ -22,6 +22,7 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
clientset "k8s.io/client-go/kubernetes"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
)
@ -38,6 +39,7 @@ type InitData interface {
IgnorePreflightErrors() sets.Set[string]
CertificateWriteDir() string
CertificateDir() string
KubeConfig() (*clientcmdapi.Config, error)
KubeConfigDir() string
KubeConfigPath() string
ManifestDir() string

View File

@ -22,6 +22,7 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
clientset "k8s.io/client-go/kubernetes"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
)
@ -41,6 +42,7 @@ func (t *testInitData) SkipTokenPrint() bool { r
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 "" }

View File

@ -21,7 +21,7 @@ import (
"github.com/pkg/errors"
"k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
rbac "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/authentication/user"
@ -40,19 +40,20 @@ const (
)
// CreateBootstrapConfigMapIfNotExists creates the kube-public ConfigMap if it doesn't exist already
func CreateBootstrapConfigMapIfNotExists(client clientset.Interface, file string) error {
func CreateBootstrapConfigMapIfNotExists(client clientset.Interface, kubeconfig *clientcmdapi.Config) error {
fmt.Printf("[bootstrap-token] Creating the %q ConfigMap in the %q namespace\n", bootstrapapi.ConfigMapClusterInfo, metav1.NamespacePublic)
klog.V(1).Infoln("[bootstrap-token] loading admin kubeconfig")
adminConfig, err := clientcmd.LoadFromFile(file)
if err != nil {
return errors.Wrap(err, "failed to load admin kubeconfig")
}
if err = clientcmdapi.FlattenConfig(adminConfig); err != nil {
// Clone the kubeconfig so that it's not mutated.
adminConfig := kubeconfig.DeepCopy()
if err := clientcmdapi.FlattenConfig(adminConfig); err != nil {
return err
}
if adminConfig.Contexts[adminConfig.CurrentContext] == nil {
return errors.New("invalid kubeconfig")
}
adminCluster := adminConfig.Contexts[adminConfig.CurrentContext].Cluster
// Copy the cluster from admin.conf to the bootstrap kubeconfig, contains the CA cert and the server URL
klog.V(1).Infoln("[bootstrap-token] copying the cluster from admin.conf to the bootstrap kubeconfig")

View File

@ -17,8 +17,8 @@ limitations under the License.
package clusterinfo
import (
"bytes"
"context"
"os"
"testing"
"text/template"
"time"
@ -31,6 +31,7 @@ import (
"k8s.io/apiserver/pkg/authentication/user"
clientsetfake "k8s.io/client-go/kubernetes/fake"
core "k8s.io/client-go/testing"
"k8s.io/client-go/tools/clientcmd"
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
@ -55,34 +56,24 @@ users:
func TestCreateBootstrapConfigMapIfNotExists(t *testing.T) {
tests := []struct {
name string
fileExist bool
createErr error
expectErr bool
}{
{
"successful case should have no error",
true,
nil,
false,
},
{
"if configmap already exists, return error",
true,
apierrors.NewAlreadyExists(schema.GroupResource{Resource: "configmaps"}, "test"),
true,
},
{
"unexpected error should be returned",
true,
apierrors.NewUnauthorized("go away!"),
true,
},
{
"if the file does not exist, return error",
false,
nil,
true,
},
}
servers := []struct {
@ -93,20 +84,12 @@ func TestCreateBootstrapConfigMapIfNotExists(t *testing.T) {
}
for _, server := range servers {
file, err := os.CreateTemp("", "")
if err != nil {
t.Fatalf("could not create tempfile: %v", err)
}
defer os.Remove(file.Name())
var buf bytes.Buffer
if err := testConfigTempl.Execute(file, server); err != nil {
if err := testConfigTempl.Execute(&buf, server); err != nil {
t.Fatalf("could not write to tempfile: %v", err)
}
if err := file.Close(); err != nil {
t.Fatalf("could not close tempfile: %v", err)
}
// Override the default timeouts to be shorter
defaultTimeouts := kubeadmapi.GetActiveTimeouts()
defaultAPICallTimeout := defaultTimeouts.KubernetesAPICall
@ -124,11 +107,12 @@ func TestCreateBootstrapConfigMapIfNotExists(t *testing.T) {
})
}
fileName := file.Name()
if !tc.fileExist {
fileName = "notexistfile"
kubeconfig, err := clientcmd.Load(buf.Bytes())
if err != nil {
t.Fatal(err)
}
err := CreateBootstrapConfigMapIfNotExists(client, fileName)
err = CreateBootstrapConfigMapIfNotExists(client, kubeconfig)
if tc.expectErr && err == nil {
t.Errorf("CreateBootstrapConfigMapIfNotExists(%s) wanted error, got nil", tc.name)
} else if !tc.expectErr && err != nil {