1414
1515import atexit
1616import base64
17+ import datetime
1718import os
1819import tempfile
1920
21+ import google .auth
22+ import google .auth .transport .requests
2023import urllib3
2124import yaml
22- from google .oauth2 .credentials import Credentials
2325
2426from kubernetes .client import ApiClient , ConfigurationObject , configuration
2527
2628from .config_exception import ConfigException
29+ from .dateutil import UTC , format_rfc3339 , parse_rfc3339
2730
31+ EXPIRY_SKEW_PREVENTION_DELAY = datetime .timedelta (minutes = 5 )
2832KUBE_CONFIG_DEFAULT_LOCATION = os .environ .get ('KUBECONFIG' , '~/.kube/config' )
2933_temp_files = {}
3034
@@ -54,6 +58,11 @@ def _create_temp_file_with_content(content):
5458 return name
5559
5660
61+ def _is_expired (expiry ):
62+ return ((parse_rfc3339 (expiry ) + EXPIRY_SKEW_PREVENTION_DELAY ) <=
63+ datetime .datetime .utcnow ().replace (tzinfo = UTC ))
64+
65+
5766class FileOrData (object ):
5867 """Utility class to read content of obj[%data_key_name] or file's
5968 content of obj[%file_key_name] and represent it as file or data.
@@ -110,19 +119,26 @@ class KubeConfigLoader(object):
110119 def __init__ (self , config_dict , active_context = None ,
111120 get_google_credentials = None ,
112121 client_configuration = configuration ,
113- config_base_path = "" ):
122+ config_base_path = "" ,
123+ config_persister = None ):
114124 self ._config = ConfigNode ('kube-config' , config_dict )
115125 self ._current_context = None
116126 self ._user = None
117127 self ._cluster = None
118128 self .set_active_context (active_context )
119129 self ._config_base_path = config_base_path
130+ self ._config_persister = config_persister
131+
132+ def _refresh_credentials ():
133+ credentials , project_id = google .auth .default ()
134+ request = google .auth .transport .requests .Request ()
135+ credentials .refresh (request )
136+ return credentials
137+
120138 if get_google_credentials :
121139 self ._get_google_credentials = get_google_credentials
122140 else :
123- self ._get_google_credentials = lambda : (
124- GoogleCredentials .get_application_default ()
125- .get_access_token ().access_token )
141+ self ._get_google_credentials = _refresh_credentials
126142 self ._client_configuration = client_configuration
127143
128144 def set_active_context (self , context_name = None ):
@@ -166,16 +182,32 @@ def _load_authentication(self):
166182 def _load_gcp_token (self ):
167183 if 'auth-provider' not in self ._user :
168184 return
169- if 'name' not in self ._user ['auth-provider' ]:
185+ provider = self ._user ['auth-provider' ]
186+ if 'name' not in provider :
170187 return
171- if self . _user [ 'auth- provider' ] ['name' ] != 'gcp' :
188+ if provider ['name' ] != 'gcp' :
172189 return
173- # Ignore configs in auth-provider and rely on GoogleCredentials
174- # caching and refresh mechanism.
175- # TODO: support gcp command based token ("cmd-path" config).
176- self .token = "Bearer %s" % self ._get_google_credentials ()
190+
191+ if (('config' not in provider ) or
192+ ('access-token' not in provider ['config' ]) or
193+ ('expiry' in provider ['config' ] and
194+ _is_expired (provider ['config' ]['expiry' ]))):
195+ # token is not available or expired, refresh it
196+ self ._refresh_gcp_token ()
197+
198+ self .token = "Bearer %s" % provider ['config' ]['access-token' ]
177199 return self .token
178200
201+ def _refresh_gcp_token (self ):
202+ if 'config' not in self ._user ['auth-provider' ]:
203+ self ._user ['auth-provider' ].value ['config' ] = {}
204+ provider = self ._user ['auth-provider' ]['config' ]
205+ credentials = self ._get_google_credentials ()
206+ provider .value ['access-token' ] = credentials .token
207+ provider .value ['expiry' ] = format_rfc3339 (credentials .expiry )
208+ if self ._config_persister :
209+ self ._config_persister (self ._config .value )
210+
179211 def _load_user_token (self ):
180212 token = FileOrData (
181213 self ._user , 'tokenFile' , 'token' ,
@@ -299,7 +331,8 @@ def list_kube_config_contexts(config_file=None):
299331
300332
301333def load_kube_config (config_file = None , context = None ,
302- client_configuration = configuration ):
334+ client_configuration = configuration ,
335+ persist_config = True ):
303336 """Loads authentication and cluster information from kube-config file
304337 and stores them in kubernetes.client.configuration.
305338
@@ -308,21 +341,35 @@ def load_kube_config(config_file=None, context=None,
308341 from config file will be used.
309342 :param client_configuration: The kubernetes.client.ConfigurationObject to
310343 set configs to.
344+ :param persist_config: If True, config file will be updated when changed
345+ (e.g GCP token refresh).
311346 """
312347
313348 if config_file is None :
314349 config_file = os .path .expanduser (KUBE_CONFIG_DEFAULT_LOCATION )
315350
351+ config_persister = None
352+ if persist_config :
353+ def _save_kube_config (config_map ):
354+ with open (config_file , 'w' ) as f :
355+ yaml .safe_dump (config_map , f , default_flow_style = False )
356+ config_persister = _save_kube_config
357+
316358 _get_kube_config_loader_for_yaml_file (
317359 config_file , active_context = context ,
318- client_configuration = client_configuration ).load_and_set ()
360+ client_configuration = client_configuration ,
361+ config_persister = config_persister ).load_and_set ()
319362
320363
321- def new_client_from_config (config_file = None , context = None ):
364+ def new_client_from_config (
365+ config_file = None ,
366+ context = None ,
367+ persist_config = True ):
322368 """Loads configuration the same as load_kube_config but returns an ApiClient
323369 to be used with any API object. This will allow the caller to concurrently
324370 talk with multiple clusters."""
325371 client_config = ConfigurationObject ()
326372 load_kube_config (config_file = config_file , context = context ,
327- client_configuration = client_config )
373+ client_configuration = client_config ,
374+ persist_config = persist_config )
328375 return ApiClient (config = client_config )
0 commit comments