14
14
15
15
import atexit
16
16
import base64
17
+ import datetime
17
18
import os
18
19
import tempfile
19
20
21
+ import google .auth
22
+ import google .auth .transport .requests
20
23
import urllib3
21
24
import yaml
22
- from google .oauth2 .credentials import Credentials
23
25
24
26
from kubernetes .client import ApiClient , ConfigurationObject , configuration
25
27
26
28
from .config_exception import ConfigException
29
+ from .dateutil import UTC , format_rfc3339 , parse_rfc3339
27
30
31
+ EXPIRY_SKEW_PREVENTION_DELAY = datetime .timedelta (minutes = 5 )
28
32
KUBE_CONFIG_DEFAULT_LOCATION = os .environ .get ('KUBECONFIG' , '~/.kube/config' )
29
33
_temp_files = {}
30
34
@@ -54,6 +58,11 @@ def _create_temp_file_with_content(content):
54
58
return name
55
59
56
60
61
+ def _is_expired (expiry ):
62
+ return ((parse_rfc3339 (expiry ) + EXPIRY_SKEW_PREVENTION_DELAY ) <=
63
+ datetime .datetime .utcnow ().replace (tzinfo = UTC ))
64
+
65
+
57
66
class FileOrData (object ):
58
67
"""Utility class to read content of obj[%data_key_name] or file's
59
68
content of obj[%file_key_name] and represent it as file or data.
@@ -110,19 +119,26 @@ class KubeConfigLoader(object):
110
119
def __init__ (self , config_dict , active_context = None ,
111
120
get_google_credentials = None ,
112
121
client_configuration = configuration ,
113
- config_base_path = "" ):
122
+ config_base_path = "" ,
123
+ config_persister = None ):
114
124
self ._config = ConfigNode ('kube-config' , config_dict )
115
125
self ._current_context = None
116
126
self ._user = None
117
127
self ._cluster = None
118
128
self .set_active_context (active_context )
119
129
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
+
120
138
if get_google_credentials :
121
139
self ._get_google_credentials = get_google_credentials
122
140
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
126
142
self ._client_configuration = client_configuration
127
143
128
144
def set_active_context (self , context_name = None ):
@@ -166,16 +182,32 @@ def _load_authentication(self):
166
182
def _load_gcp_token (self ):
167
183
if 'auth-provider' not in self ._user :
168
184
return
169
- if 'name' not in self ._user ['auth-provider' ]:
185
+ provider = self ._user ['auth-provider' ]
186
+ if 'name' not in provider :
170
187
return
171
- if self . _user [ 'auth- provider' ] ['name' ] != 'gcp' :
188
+ if provider ['name' ] != 'gcp' :
172
189
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' ]
177
199
return self .token
178
200
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
+
179
211
def _load_user_token (self ):
180
212
token = FileOrData (
181
213
self ._user , 'tokenFile' , 'token' ,
@@ -299,7 +331,8 @@ def list_kube_config_contexts(config_file=None):
299
331
300
332
301
333
def load_kube_config (config_file = None , context = None ,
302
- client_configuration = configuration ):
334
+ client_configuration = configuration ,
335
+ persist_config = True ):
303
336
"""Loads authentication and cluster information from kube-config file
304
337
and stores them in kubernetes.client.configuration.
305
338
@@ -308,21 +341,35 @@ def load_kube_config(config_file=None, context=None,
308
341
from config file will be used.
309
342
:param client_configuration: The kubernetes.client.ConfigurationObject to
310
343
set configs to.
344
+ :param persist_config: If True, config file will be updated when changed
345
+ (e.g GCP token refresh).
311
346
"""
312
347
313
348
if config_file is None :
314
349
config_file = os .path .expanduser (KUBE_CONFIG_DEFAULT_LOCATION )
315
350
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
+
316
358
_get_kube_config_loader_for_yaml_file (
317
359
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 ()
319
362
320
363
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 ):
322
368
"""Loads configuration the same as load_kube_config but returns an ApiClient
323
369
to be used with any API object. This will allow the caller to concurrently
324
370
talk with multiple clusters."""
325
371
client_config = ConfigurationObject ()
326
372
load_kube_config (config_file = config_file , context = context ,
327
- client_configuration = client_config )
373
+ client_configuration = client_config ,
374
+ persist_config = persist_config )
328
375
return ApiClient (config = client_config )
0 commit comments