kubeadm: add missing cluster-info context validation

When retrieving the cluster-info CM, ensure the cluster pointed
out by the current context in the kubeconfig is validated.

Add unit test for the above.

Make the function GetClusterFromKubeConfig() to return various
errors. Handle the errors on call sites. Add unit tests
for the update.

The above changes prevent panics when the users has manually
edited and malformed the kubeconfig in the cluster-info CM.
This commit is contained in:
Lubomir I. Ivanov
2025-10-19 17:03:46 +02:00
parent e34f01402f
commit 0613fdeccb
9 changed files with 104 additions and 37 deletions

View File

@@ -692,7 +692,10 @@ func fetchInitConfigurationFromJoinConfiguration(cfg *kubeadmapi.JoinConfigurati
}
// Create the final KubeConfig file with the cluster name discovered after fetching the cluster configuration
_, clusterinfo := kubeconfigutil.GetClusterFromKubeConfig(tlsBootstrapCfg)
_, clusterinfo, err := kubeconfigutil.GetClusterFromKubeConfig(tlsBootstrapCfg)
if err != nil {
return nil, errors.Wrap(err, "the TLS bootstrap kubeconfig is malformed")
}
tlsBootstrapCfg.Clusters = map[string]*clientcmdapi.Cluster{
initConfiguration.ClusterName: clusterinfo,
}

View File

@@ -56,9 +56,9 @@ func getJoinCommand(kubeConfigFile, token, key string, controlPlane, skipTokenPr
}
// load the default cluster config
_, clusterConfig := kubeconfigutil.GetClusterFromKubeConfig(config)
if clusterConfig == nil {
return "", errors.New("failed to get default cluster config")
_, clusterConfig, err := kubeconfigutil.GetClusterFromKubeConfig(config)
if err != nil {
return "", errors.Wrapf(err, "malformed kubeconfig file: %s", kubeConfigFile)
}
// load CA certificates from the kubeconfig (either from PEM data or by file path)

View File

@@ -133,7 +133,7 @@ func TestGetJoinCommand(t *testing.T) {
kubeConfig: &clientcmdapi.Config{},
token: "test-token",
expectError: true,
errorMessage: "failed to get default cluster config",
errorMessage: "the current context is invalid",
},
{
name: "Error when CA certificate is invalid",

View File

@@ -21,6 +21,7 @@ import (
clientset "k8s.io/client-go/kubernetes"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
"k8s.io/klog/v2"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
@@ -51,7 +52,10 @@ func For(client clientset.Interface, cfg *kubeadmapi.JoinConfiguration) (*client
if len(cfg.Discovery.TLSBootstrapToken) != 0 {
klog.V(1).Info("[discovery] Using provided TLSBootstrapToken as authentication credentials for the join process")
_, clusterinfo := kubeconfigutil.GetClusterFromKubeConfig(config)
_, clusterinfo, err := kubeconfigutil.GetClusterFromKubeConfig(config)
if err != nil {
return nil, errors.Wrapf(err, "malformed kubeconfig in the %s ConfigMap", bootstrapapi.ConfigMapClusterInfo)
}
return kubeconfigutil.CreateWithToken(
clusterinfo.Server,
kubeadmapiv1.DefaultClusterName,

View File

@@ -52,9 +52,9 @@ func ValidateConfigInfo(config *clientcmdapi.Config, discoveryTimeout time.Durat
if len(config.Clusters) < 1 {
return nil, errors.New("the provided kubeconfig file must have at least one Cluster defined")
}
currentClusterName, currentCluster := kubeconfigutil.GetClusterFromKubeConfig(config)
if currentCluster == nil {
return nil, errors.New("the provided kubeconfig file must have a unnamed Cluster or a CurrentContext that specifies a non-nil Cluster")
currentClusterName, currentCluster, err := kubeconfigutil.GetClusterFromKubeConfig(config)
if err != nil {
return nil, errors.Wrap(err, "the provided kubeconfig file is malformed")
}
if err := clientcmd.Validate(*config); err != nil {
return nil, err
@@ -124,7 +124,10 @@ func ValidateConfigInfo(config *clientcmdapi.Config, discoveryTimeout time.Durat
return config, nil
}
_, refreshedCluster := kubeconfigutil.GetClusterFromKubeConfig(refreshedBaseKubeConfig)
_, refreshedCluster, err := kubeconfigutil.GetClusterFromKubeConfig(refreshedBaseKubeConfig)
if err != nil {
return nil, errors.Wrapf(err, "malformed kubeconfig in the %s ConfigMap", bootstrapapi.ConfigMapClusterInfo)
}
if currentCluster.Server != refreshedCluster.Server {
klog.Warningf("[discovery] the API Server endpoint %q in use is different from the endpoint %q which defined in the %s ConfigMap", currentCluster.Server, refreshedCluster.Server, bootstrapapi.ConfigMapClusterInfo)
}

View File

@@ -103,9 +103,9 @@ func retrieveValidatedConfigInfo(client clientset.Interface, cfg *kubeadmapi.Dis
return nil, errors.Wrapf(err, "couldn't parse the kubeconfig file in the %s ConfigMap", bootstrapapi.ConfigMapClusterInfo)
}
// The ConfigMap should contain a single cluster
if len(insecureConfig.Clusters) != 1 {
return nil, errors.Errorf("expected the kubeconfig file in the %s ConfigMap to have a single cluster, but it had %d", bootstrapapi.ConfigMapClusterInfo, len(insecureConfig.Clusters))
_, _, err = kubeconfigutil.GetClusterFromKubeConfig(insecureConfig)
if err != nil {
return nil, errors.Wrapf(err, "malformed kubeconfig in the %s ConfigMap", bootstrapapi.ConfigMapClusterInfo)
}
// If no TLS root CA pinning was specified, we're done

View File

@@ -77,10 +77,12 @@ users: null
name string
tokenID string
tokenSecret string
currentContextCluster string
cfg *kubeadmapi.Discovery
configMap *fakeConfigMap
delayedJWSSignaturePatch bool
expectedError bool
expectedErrorString string
}{
{
// This is the default behavior. The JWS signature is patched after the cluster-info ConfigMap is created
@@ -130,6 +132,24 @@ users: null
data: nil,
},
},
{
name: "invalid: the kubeconfig in the configmap has the wrong current context",
tokenID: "123456",
tokenSecret: "abcdef1234567890",
cfg: &kubeadmapi.Discovery{
BootstrapToken: &kubeadmapi.BootstrapTokenDiscovery{
Token: "123456.abcdef1234567890",
CACertHashes: []string{caCertHash},
},
},
configMap: &fakeConfigMap{
name: bootstrapapi.ConfigMapClusterInfo,
data: nil,
},
currentContextCluster: "foo",
expectedError: true,
expectedErrorString: `malformed kubeconfig in the cluster-info ConfigMap: no matching cluster for the current context: token-bootstrap-client@somecluster`,
},
{
name: "invalid: token format is invalid",
tokenID: "foo",
@@ -216,6 +236,13 @@ users: null
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
kubeconfig := buildSecureBootstrapKubeConfig("127.0.0.1", []byte(caCert), "somecluster")
if len(test.currentContextCluster) > 0 {
currentContext := kubeconfig.Contexts[kubeconfig.CurrentContext]
if currentContext == nil {
t.Fatal("unexpected nil current context")
}
currentContext.Cluster = test.currentContextCluster
}
kubeconfigBytes, err := clientcmd.Write(*kubeconfig)
if err != nil {
t.Fatalf("cannot marshal kubeconfig %v", err)
@@ -267,8 +294,13 @@ users: null
t.Errorf("expected error %v, got %v, error: %v", test.expectedError, err != nil, err)
}
// Return if an error is expected
if err != nil {
if len(test.expectedErrorString) > 0 && test.expectedErrorString != err.Error() {
t.Fatalf("expected error string: %s, got: %s",
test.expectedErrorString, err.Error())
}
// Return if an error is expected
return
}

View File

@@ -104,17 +104,21 @@ func WriteToDisk(filename string, kubeconfig *clientcmdapi.Config) error {
}
// GetClusterFromKubeConfig returns the default Cluster of the specified KubeConfig
func GetClusterFromKubeConfig(config *clientcmdapi.Config) (string, *clientcmdapi.Cluster) {
func GetClusterFromKubeConfig(config *clientcmdapi.Config) (string, *clientcmdapi.Cluster, error) {
// If there is an unnamed cluster object, use it
if config.Clusters[""] != nil {
return "", config.Clusters[""]
return "", config.Clusters[""], nil
}
currentContext := config.Contexts[config.CurrentContext]
if currentContext != nil {
return currentContext.Cluster, config.Clusters[currentContext.Cluster]
if config.Clusters[currentContext.Cluster] != nil {
return currentContext.Cluster, config.Clusters[currentContext.Cluster], nil
}
return "", nil, errors.Errorf("no matching cluster for the current context: %s", config.CurrentContext)
}
return "", nil
return "", nil, errors.Errorf("the current context is invalid: %s", config.CurrentContext)
}
// HasAuthenticationCredentials returns true if the current user has valid authentication credentials for

View File

@@ -351,17 +351,49 @@ func TestGetClusterFromKubeConfig(t *testing.T) {
config *clientcmdapi.Config
expectedClusterName string
expectedCluster *clientcmdapi.Cluster
expectedError bool
}{
{
name: "cluster is empty",
name: "an existing cluster with an empty name is returned directly",
config: &clientcmdapi.Config{
CurrentContext: "kubernetes",
Clusters: map[string]*clientcmdapi.Cluster{
"": {Server: "http://foo:8080"},
},
},
expectedClusterName: "",
expectedCluster: &clientcmdapi.Cluster{
Server: "http://foo:8080",
},
},
{
name: "the current context is invalid",
config: &clientcmdapi.Config{
CurrentContext: "foo",
Contexts: map[string]*clientcmdapi.Context{
"bar": {AuthInfo: "bar", Cluster: "bar"},
},
},
expectedClusterName: "",
expectedCluster: nil,
expectedError: true,
},
{
name: "cluster and currentContext are not empty",
name: "no matching cluster for the current context",
config: &clientcmdapi.Config{
CurrentContext: "foo",
Contexts: map[string]*clientcmdapi.Context{
"foo": {AuthInfo: "bar", Cluster: "bar"},
},
Clusters: map[string]*clientcmdapi.Cluster{
"baz": {Server: "https://bar:16443"},
},
},
expectedClusterName: "",
expectedCluster: nil,
expectedError: true,
},
{
name: "valid current context and cluster",
config: &clientcmdapi.Config{
CurrentContext: "foo",
Contexts: map[string]*clientcmdapi.Context{
@@ -378,31 +410,20 @@ func TestGetClusterFromKubeConfig(t *testing.T) {
Server: "http://foo:8080",
},
},
{
name: "cluster is not empty and currentContext is not in contexts",
config: &clientcmdapi.Config{
CurrentContext: "foo",
Contexts: map[string]*clientcmdapi.Context{
"bar": {AuthInfo: "bar", Cluster: "bar"},
},
Clusters: map[string]*clientcmdapi.Cluster{
"foo": {Server: "http://foo:8080"},
"bar": {Server: "https://bar:16443"},
},
},
expectedClusterName: "",
expectedCluster: nil,
},
}
for _, rt := range tests {
t.Run(rt.name, func(t *testing.T) {
clusterName, cluster := GetClusterFromKubeConfig(rt.config)
clusterName, cluster, err := GetClusterFromKubeConfig(rt.config)
if clusterName != rt.expectedClusterName {
t.Errorf("got cluster name = %s, expected %s", clusterName, rt.expectedClusterName)
}
if !reflect.DeepEqual(cluster, rt.expectedCluster) {
t.Errorf("got cluster = %+v, expected %+v", cluster, rt.expectedCluster)
}
if (err != nil) != rt.expectedError {
t.Errorf("expected error: %v, got: %v, error: %v",
rt.expectedError, err != nil, err)
}
})
}
}