From 76824d0a714e5bd5f0b8af12b865bfc8d6dde1b2 Mon Sep 17 00:00:00 2001 From: Eric Menendez Date: Fri, 23 Jul 2021 15:53:23 -0600 Subject: [PATCH] Refresh exec-based API credentials when they expire (for Kubernetes client v11.0.0) This is a partial fix for kubernetes-client/python#741, based on the version of this repo included in `kubernetes-client` v11.0.0 (https://github.com/kubernetes-client/python/tree/v11.0.0). As described in kubernetes-client/python#741, some of the authentication schemes supported by Kubernetes require updating the client's credentials from time to time. The Kubernetes Python client currently does not support this, except for when using the `gcp` auth scheme. This is because the OpenAPI-generated code does not generally expect credentials to change after the client is configured. However, in OpenAPITools/openapi-generator#3594, the OpenAPI-generated code added a (undocumented) hook on the `Configuration` object which provides a method for the client credentials to be refreshed as needed. Unfortunately, this version of the Kubernetes client is too old to have that hook, but this patch adds it with a subclass of `Configuration`. Then the `load_kube_config()` function, used by the Kubernetes API to set up the `Configuration` object from the client's local k8s config, just needs to be updated to take advantage of this hook. This patch does this for `exec`-based authentication, which is a partial fix for kubernetes-client/python#741. The plan is to follow up to support this for all other authentication schemes which may require refreshing credentials. The follow-up patch will be based on the latest Kubernetes client and won't need the `Configuration` subclass. As noted above, `load_kube_config()` already has a special-case monkeypatch to refresh GCP tokens. I presume this functionality was added before the OpenAPI generator added support for the refresh hook. A complete fix will probably include refactoring the GCP token refreshing to use the new hook. --- config/kube_config.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/config/kube_config.py b/config/kube_config.py index 43676728..55c64edb 100644 --- a/config/kube_config.py +++ b/config/kube_config.py @@ -175,6 +175,13 @@ def token(self): expiry=parse_rfc3339(data['credential']['token_expiry'])) +class ConfigurationWithRefreshHook(Configuration): + def get_api_key_with_prefix(self, identifier): + if self.refresh_api_key_hook is not None: + self.refresh_api_key_hook(self) + return super(ConfigurationWithRefreshHook, self).get_api_key_with_prefix(identifier) + + class KubeConfigLoader(object): def __init__(self, config_dict, active_context=None, @@ -476,6 +483,8 @@ def _load_from_exec_plugin(self): logging.error('exec: missing token field in plugin output') return None self.token = "Bearer %s" % status['token'] + if 'expirationTimestamp' in status: + self.expiry = parse_rfc3339(status['expirationTimestamp']) return True except Exception as e: logging.error(str(e)) @@ -540,6 +549,13 @@ def _gcp_get_api_key(*args): # Note: this line runs for GCP auth tokens as well, but this entry # will not be updated upon GCP token refresh. client_configuration.api_key['authorization'] = self.token + + def _refresh_api_key(client_configuration): + if ('expiry' in self.__dict__ and + self.expiry < datetime.datetime.now(tz=UTC)): + self._load_authentication() + self._set_config(client_configuration) + client_configuration.refresh_api_key_hook = _refresh_api_key # copy these keys directly from self to configuration object keys = ['host', 'ssl_ca_cert', 'cert_file', 'key_file', 'verify_ssl'] for key in keys: @@ -739,7 +755,7 @@ def load_kube_config(config_file=None, context=None, persist_config=persist_config) if client_configuration is None: - config = type.__call__(Configuration) + config = type.__call__(ConfigurationWithRefreshHook) loader.load_and_set(config) Configuration.set_default(config) else: