Repository: kubernetes-client/python

Issue:
KUBECONFIG loading does not match kubectl behavior
**What happened (please include outputs or screenshots)**:

Calling `config.load_kube_config()` with a KUBECONFIG that uses colon delimited files throws an error:

```shell
.venv/lib64/python3.11/site-packages/kubernetes/config/kube_config.py:815: in load_kube_config
    loader = _get_kube_config_loader(
.venv/lib64/python3.11/site-packages/kubernetes/config/kube_config.py:775: in _get_kube_config_loader
    return KubeConfigLoader(
.venv/lib64/python3.11/site-packages/kubernetes/config/kube_config.py:206: in __init__
    self.set_active_context(active_context)
.venv/lib64/python3.11/site-packages/kubernetes/config/kube_config.py:258: in set_active_context
    context_name = self._config['current-context']
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <kubernetes.config.kube_config.ConfigNode object at 0x7fb7e4108f90>, key = 'current-context'

    def __getitem__(self, key):
        v = self.safe_get(key)
        if v is None:
>           raise ConfigException(
                'Invalid kube-config file. Expected key %s in %s'
                % (key, self.name))
E           kubernetes.config.config_exception.ConfigException: Invalid kube-config file. Expected key current-context in /var/home/seb/.config/kube/clusters/kube.example.com.yaml

.venv/lib64/python3.11/site-packages/kubernetes/config/kube_config.py:625: ConfigException
```

The code that causes this is just:

```python
    @pytest.fixture
    def k8s_client():
        config.load_kube_config()
```

`KUBECONFIG` is defined as follows:

```shell
echo $KUBECONFIG
/var/home/seb/.config/kube/clusters/kube.example.com.yaml:/var/home/seb/.config/kube/contexts/kube.example.com.yaml:/var/home/seb/.config/kube/users/kube.example.com.yaml
```

These files define clusters, contexts, and users each on their own.

**What you expected to happen**:

The `load_kube_config` call should behave just like kubectl which merges all files first and then attempts to load them and thus does not care about the order of these files.

**How to reproduce it (as minimally and precisely as possible)**:

Create 3 files for cluster, context, and user:

```yaml
# cluster
apiVersion: v1
kind: Config
clusters:
  - name: kube.example.com
    cluster:
      certificate-authority-data: doesnotmatter
      server: https://kube.example.com:6443
```

```yaml
# context
apiVersion: v1
kind: Config
contexts:
  - name: kube.example.com
    context:
      cluster: kube.example.com
      user: kube.example.com
      namespace: kube-system
current-context: kube.example.com
```

```yaml
# user
apiVersion: v1
kind: Config
users:
  - name: kube.example.com
```

Then define `KUBECONFIG` to something like `cluster.yaml:context.yaml:user.yaml`.

**Anything else we need to know?**:

Changing the order of these files so that the context file is at the first position fixes this!

Support for colon separated files was introduced with https://github.com/kubernetes-client/python/issues/636 (?)

**Environment**:
- Kubernetes version (`kubectl version`): v1.29.0
- OS (e.g., MacOS 10.13.6): Fedora 39
- Python version (`python --version`): 3.11.7
- Python client version (`pip list | grep kubernetes`): 29.0.0



Discussion:
@sebhoss Thanks for the detailed issue, I've prepared fix for it.



Patch:
diff --git a/kubernetes/base/config/kube_config.py b/kubernetes/base/config/kube_config.py
index d8c63a8261..96e5bd633e 100644
--- a/kubernetes/base/config/kube_config.py
+++ b/kubernetes/base/config/kube_config.py
@@ -727,6 +727,10 @@ def load_config(self, path):
             self.config_merged = ConfigNode(path, config_merged, path)
         for item in ('clusters', 'contexts', 'users'):
             self._merge(item, config.get(item, []) or [], path)
+
+        if 'current-context' in config:
+            self.config_merged.value['current-context'] = config['current-context']
+
         self.config_files[path] = config
 
     def _merge(self, item, add_cfg, path):


Test patch:
diff --git a/kubernetes/base/config/kube_config_test.py b/kubernetes/base/config/kube_config_test.py
index b415492032..9b12b2d69f 100644
--- a/kubernetes/base/config/kube_config_test.py
+++ b/kubernetes/base/config/kube_config_test.py
@@ -1653,7 +1653,7 @@ def refresh_api_key_hook(client_config):
 
 
 class TestKubeConfigMerger(BaseTestCase):
-    TEST_KUBE_CONFIG_PART1 = {
+    TEST_KUBE_CONFIG_SET1 = [{
         "current-context": "no_user",
         "contexts": [
             {
@@ -1672,9 +1672,7 @@ class TestKubeConfigMerger(BaseTestCase):
             },
         ],
         "users": []
-    }
-
-    TEST_KUBE_CONFIG_PART2 = {
+    }, {
         "current-context": "",
         "contexts": [
             {
@@ -1712,9 +1710,7 @@ class TestKubeConfigMerger(BaseTestCase):
                 }
             },
         ]
-    }
-
-    TEST_KUBE_CONFIG_PART3 = {
+    }, {
         "current-context": "no_user",
         "contexts": [
             {
@@ -1761,12 +1757,10 @@ class TestKubeConfigMerger(BaseTestCase):
                 }
             },
         ]
-    }
-    TEST_KUBE_CONFIG_PART4 = {
+    }, {
         "current-context": "no_user",
-    }
-    # Config with user having cmd-path
-    TEST_KUBE_CONFIG_PART5 = {
+    }, {
+        # Config with user having cmd-path
         "contexts": [
             {
                 "name": "contexttestcmdpath",
@@ -1795,8 +1789,7 @@ class TestKubeConfigMerger(BaseTestCase):
                 }
             }
         ]
-    }
-    TEST_KUBE_CONFIG_PART6 = {
+    }, {
         "current-context": "no_user",
         "contexts": [
             {
@@ -1815,22 +1808,49 @@ class TestKubeConfigMerger(BaseTestCase):
             },
         ],
         "users": None
-    }
+    }]
+    # 3 parts with different keys/data to merge
+    TEST_KUBE_CONFIG_SET2 = [{
+        "clusters": [
+            {
+                "name": "default",
+                "cluster": {
+                    "server": TEST_HOST
+                }
+            },
+        ],
+    }, {
+        "current-context": "simple_token",
+        "contexts": [
+            {
+                "name": "simple_token",
+                "context": {
+                    "cluster": "default",
+                    "user": "simple_token"
+                }
+            },
+        ],
+    }, {
+        "users": [
+            {
+                "name": "simple_token",
+                "user": {
+                    "token": TEST_DATA_BASE64,
+                    "username": TEST_USERNAME,
+                    "password": TEST_PASSWORD,
+                }
+            },
+        ]
+    }]
 
-    def _create_multi_config(self):
+    def _create_multi_config(self, parts):
         files = []
-        for part in (
-                self.TEST_KUBE_CONFIG_PART1,
-                self.TEST_KUBE_CONFIG_PART2,
-                self.TEST_KUBE_CONFIG_PART3,
-                self.TEST_KUBE_CONFIG_PART4,
-                self.TEST_KUBE_CONFIG_PART5,
-                self.TEST_KUBE_CONFIG_PART6):
+        for part in parts:
             files.append(self._create_temp_file(yaml.safe_dump(part)))
         return ENV_KUBECONFIG_PATH_SEPARATOR.join(files)
 
     def test_list_kube_config_contexts(self):
-        kubeconfigs = self._create_multi_config()
+        kubeconfigs = self._create_multi_config(self.TEST_KUBE_CONFIG_SET1)
         expected_contexts = [
             {'context': {'cluster': 'default'}, 'name': 'no_user'},
             {'context': {'cluster': 'ssl', 'user': 'ssl'}, 'name': 'ssl'},
@@ -1849,15 +1869,31 @@ def test_list_kube_config_contexts(self):
         self.assertEqual(active_context, expected_contexts[0])
 
     def test_new_client_from_config(self):
-        kubeconfigs = self._create_multi_config()
+        kubeconfigs = self._create_multi_config(self.TEST_KUBE_CONFIG_SET1)
         client = new_client_from_config(
             config_file=kubeconfigs, context="simple_token")
         self.assertEqual(TEST_HOST, client.configuration.host)
         self.assertEqual(BEARER_TOKEN_FORMAT % TEST_DATA_BASE64,
                          client.configuration.api_key['authorization'])
 
+    def test_merge_with_context_in_different_file(self):
+        kubeconfigs = self._create_multi_config(self.TEST_KUBE_CONFIG_SET2)
+        client = new_client_from_config(config_file=kubeconfigs)
+
+        expected_contexts = [
+            {'context': {'cluster': 'default', 'user': 'simple_token'},
+             'name': 'simple_token'}
+        ]
+        contexts, active_context = list_kube_config_contexts(
+            config_file=kubeconfigs)
+        self.assertEqual(contexts, expected_contexts)
+        self.assertEqual(active_context, expected_contexts[0])
+        self.assertEqual(TEST_HOST, client.configuration.host)
+        self.assertEqual(BEARER_TOKEN_FORMAT % TEST_DATA_BASE64,
+                         client.configuration.api_key['authorization'])
+
     def test_save_changes(self):
-        kubeconfigs = self._create_multi_config()
+        kubeconfigs = self._create_multi_config(self.TEST_KUBE_CONFIG_SET1)
 
         # load configuration, update token, save config
         kconf = KubeConfigMerger(kubeconfigs)