Repository: tomplus/kubernetes_asyncio (python)

Issue:
Service account token is not refreshed when ApiClient is created with default configuration causing 401 (Unauthorized) error
I am facing an issue with service account token periodic refresh that was originally implemented in https://github.com/tomplus/kubernetes_asyncio/pull/205/files

**Problem**:

Service account token is getting stale when ApiClient is created with default configuration

**Use case:**

1) Create in-cluster config without parameters:
load_incluster_config()
2) Use default configuration with ApiClient
async with ApiClient() as api:
   v1 = CoreV1Api(api)
   ...
3) Run a job that periodically is making k8s calls 
Expected result: once token is refreshed, all new calls will happen with the new token
Actual result: when token is refreshed, only the current call is using the new token but subsequent calls are using the old token

Analysis:

ApiClient is using a **copy** of default configuration https://github.com/tomplus/kubernetes_asyncio/blob/master/kubernetes_asyncio/client/api_client.py#L71

When token is refreshed, that same copy is getting updated:
https://github.com/tomplus/kubernetes_asyncio/blob/ad5590c1f7026ddec939c2c0d9a2281cbc05a059/kubernetes_asyncio/client/configuration.py#L359
https://github.com/tomplus/kubernetes_asyncio/blob/master/kubernetes_asyncio/config/incluster_config.py#L99

Due to the original "default" configuration is not being updated, and that ApiClient is creating a copy of configuration from the default one (that still has the old token), the old original tokens are being sent to kube api server.

Proposed solution:

It is necessary to update the default configuration as well when the token is refreshed (at least in the case when ApiClient uses default configuration) 
A small improvement to the original code can resolve the issue https://github.com/tomplus/kubernetes_asyncio/blob/master/kubernetes_asyncio/config/incluster_config.py#L96

`def load_token_from_file(configuration, *args):
      if self.token_expires_at <= datetime.datetime.now():
        # this will reload the token from disk and update default configuration                
        self.load_and_set()
        configuration.api_key['BearerToken'] = self.token`

Note: this problem might be also actual for https://github.com/kubernetes-client/python

Pull request description:
[fix] refreshing incluster configuration
Fixes #253 

Patch:
diff --git a/kubernetes_asyncio/config/incluster_config.py b/kubernetes_asyncio/config/incluster_config.py
index 78093d3c..62704085 100644
--- a/kubernetes_asyncio/config/incluster_config.py
+++ b/kubernetes_asyncio/config/incluster_config.py
@@ -96,6 +96,10 @@ class InClusterConfigLoader(object):
         def load_token_from_file(configuration, *args):
             if self.token_expires_at <= datetime.datetime.now():
                 self._read_token_file()
+
+            # expiration time is stored InClusterConfigLoader,
+            # thus some copies of Configuration can be outdated.
+            if configuration.api_key['BearerToken'] != self.token:
                 configuration.api_key['BearerToken'] = self.token
 
         configuration.refresh_api_key_hook = load_token_from_file


Test patch:
diff --git a/kubernetes_asyncio/config/incluster_config_test.py b/kubernetes_asyncio/config/incluster_config_test.py
index d4437821..b1ccbdda 100644
--- a/kubernetes_asyncio/config/incluster_config_test.py
+++ b/kubernetes_asyncio/config/incluster_config_test.py
@@ -48,6 +48,7 @@ class InClusterConfigTest(unittest.TestCase):
     def tearDown(self):
         for f in self._temp_files:
             os.remove(f)
+        Configuration.set_default(None)
 
     def _create_file_with_temp_content(self, content=""):
         handler, name = tempfile.mkstemp()
@@ -107,6 +108,38 @@ class InClusterConfigTest(unittest.TestCase):
         self.assertEqual('Bearer ' + _TEST_NEW_TOKEN, loader.token)
         self.assertGreater(loader.token_expires_at, old_token_expires_at)
 
+    def test_refresh_token_default_config_with_copies(self):
+        loader = self.get_test_loader()
+        loader.load_and_set()
+
+        configs = [
+            Configuration.get_default_copy(),
+            Configuration.get_default_copy(),
+        ]
+
+        for config in configs:
+            self.assertEqual('Bearer ' + _TEST_TOKEN,
+                             config.get_api_key_with_prefix('BearerToken'))
+        self.assertEqual('Bearer ' + _TEST_TOKEN, loader.token)
+        self.assertIsNotNone(loader.token_expires_at)
+
+        old_token_expires_at = loader.token_expires_at
+        loader._token_filename = self._create_file_with_temp_content(
+            _TEST_NEW_TOKEN)
+
+        for config in configs:
+            self.assertEqual('Bearer ' + _TEST_TOKEN,
+                             config.get_api_key_with_prefix('BearerToken'))
+
+        loader.token_expires_at = datetime.datetime.now()
+
+        for config in configs:
+            self.assertEqual('Bearer ' + _TEST_NEW_TOKEN,
+                             config.get_api_key_with_prefix('BearerToken'))
+
+        self.assertEqual('Bearer ' + _TEST_NEW_TOKEN, loader.token)
+        self.assertGreater(loader.token_expires_at, old_token_expires_at)
+
     def _should_fail_load(self, config_loader, reason):
         try:
             config_loader.load_and_set()