mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-08-08 23:37:11 +00:00
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:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user