14
14
15
15
import atexit
16
16
import base64
17
+ import datetime
17
18
import os
18
19
import tempfile
20
+ import time
19
21
22
+ import google .auth
23
+ import google .auth .transport .requests
20
24
import urllib3
21
25
import yaml
22
- from oauth2client .client import GoogleCredentials
23
-
24
26
from kubernetes .client import ApiClient , ConfigurationObject , configuration
25
27
26
28
from .config_exception import ConfigException
29
+ from .rfc3339 import tf_from_timestamp , timestamp_from_tf
27
30
31
+ EXPIRY_SKEW_PREVENTION_DELAY_S = 600
28
32
KUBE_CONFIG_DEFAULT_LOCATION = '~/.kube/config'
29
33
_temp_files = {}
30
34
@@ -54,6 +58,17 @@ def _create_temp_file_with_content(content):
54
58
return name
55
59
56
60
61
+ def _is_expired (expiry ):
62
+ tf = tf_from_timestamp (expiry )
63
+ n = time .time ()
64
+ return tf + EXPIRY_SKEW_PREVENTION_DELAY_S <= n
65
+
66
+
67
+ def _datetime_to_rfc3339 (dt ):
68
+ tf = (dt - datetime .datetime .utcfromtimestamp (0 )).total_seconds ()
69
+ return timestamp_from_tf (tf , time_offset = "Z" )
70
+
71
+
57
72
class FileOrData (object ):
58
73
"""Utility class to read content of obj[%data_key_name] or file's
59
74
content of obj[%file_key_name] and represent it as file or data.
@@ -110,19 +125,26 @@ class KubeConfigLoader(object):
110
125
def __init__ (self , config_dict , active_context = None ,
111
126
get_google_credentials = None ,
112
127
client_configuration = configuration ,
113
- config_base_path = "" ):
128
+ config_base_path = "" ,
129
+ config_persister = None ):
114
130
self ._config = ConfigNode ('kube-config' , config_dict )
115
131
self ._current_context = None
116
132
self ._user = None
117
133
self ._cluster = None
118
134
self .set_active_context (active_context )
119
135
self ._config_base_path = config_base_path
136
+ self ._config_persister = config_persister
137
+
138
+ def _refresh_credentials ():
139
+ credentials , project_id = google .auth .default ()
140
+ request = google .auth .transport .requests .Request ()
141
+ credentials .refresh (request )
142
+ return credentials
143
+
120
144
if get_google_credentials :
121
145
self ._get_google_credentials = get_google_credentials
122
146
else :
123
- self ._get_google_credentials = lambda : (
124
- GoogleCredentials .get_application_default ()
125
- .get_access_token ().access_token )
147
+ self ._get_google_credentials = _refresh_credentials
126
148
self ._client_configuration = client_configuration
127
149
128
150
def set_active_context (self , context_name = None ):
@@ -161,16 +183,32 @@ def _load_authentication(self):
161
183
def _load_gcp_token (self ):
162
184
if 'auth-provider' not in self ._user :
163
185
return
164
- if 'name' not in self ._user ['auth-provider' ]:
186
+ provider = self ._user ['auth-provider' ]
187
+ if 'name' not in provider :
165
188
return
166
- if self . _user [ 'auth- provider' ] ['name' ] != 'gcp' :
189
+ if provider ['name' ] != 'gcp' :
167
190
return
168
- # Ignore configs in auth-provider and rely on GoogleCredentials
169
- # caching and refresh mechanism.
170
- # TODO: support gcp command based token ("cmd-path" config).
171
- self .token = "Bearer %s" % self ._get_google_credentials ()
191
+
192
+ if (('config' not in provider ) or
193
+ ('access-token' not in provider ['config' ]) or
194
+ ('expiry' in provider ['config' ] and
195
+ _is_expired (provider ['config' ]['expiry' ]))):
196
+ # token is not available or expired, refresh it
197
+ self ._refresh_gcp_token ()
198
+
199
+ self .token = "Bearer %s" % provider ['config' ]['access-token' ]
172
200
return self .token
173
201
202
+ def _refresh_gcp_token (self ):
203
+ if 'config' not in self ._user ['auth-provider' ]:
204
+ self ._user ['auth-provider' ].value ['config' ] = {}
205
+ provider = self ._user ['auth-provider' ]['config' ]
206
+ credentials = self ._get_google_credentials ()
207
+ provider .value ['access-token' ] = credentials .token
208
+ provider .value ['expiry' ] = _datetime_to_rfc3339 (credentials .expiry )
209
+ if self ._config_persister :
210
+ self ._config_persister (self ._config .value )
211
+
174
212
def _load_user_token (self ):
175
213
token = FileOrData (
176
214
self ._user , 'tokenFile' , 'token' ,
@@ -282,6 +320,11 @@ def _get_kube_config_loader_for_yaml_file(filename, **kwargs):
282
320
** kwargs )
283
321
284
322
323
+ def _save_kube_config (filename , config_map ):
324
+ with open (filename , 'w' ) as f :
325
+ yaml .safe_dump (config_map , f , default_flow_style = False )
326
+
327
+
285
328
def list_kube_config_contexts (config_file = None ):
286
329
287
330
if config_file is None :
@@ -292,7 +335,8 @@ def list_kube_config_contexts(config_file=None):
292
335
293
336
294
337
def load_kube_config (config_file = None , context = None ,
295
- client_configuration = configuration ):
338
+ client_configuration = configuration ,
339
+ persist_config = True ):
296
340
"""Loads authentication and cluster information from kube-config file
297
341
and stores them in kubernetes.client.configuration.
298
342
@@ -301,21 +345,32 @@ def load_kube_config(config_file=None, context=None,
301
345
from config file will be used.
302
346
:param client_configuration: The kubernetes.client.ConfigurationObject to
303
347
set configs to.
348
+ :param persist_config: If True and config changed (e.g. GCP token refresh)
349
+ the provided config file will be updated.
304
350
"""
305
351
306
352
if config_file is None :
307
353
config_file = os .path .expanduser (KUBE_CONFIG_DEFAULT_LOCATION )
308
354
355
+ config_persister = None
356
+ if persist_config :
357
+ config_persister = lambda config_map , config_file = config_file : (
358
+ _save_kube_config (config_file , config_map ))
309
359
_get_kube_config_loader_for_yaml_file (
310
360
config_file , active_context = context ,
311
- client_configuration = client_configuration ).load_and_set ()
361
+ client_configuration = client_configuration ,
362
+ config_persister = config_persister ).load_and_set ()
312
363
313
364
314
- def new_client_from_config (config_file = None , context = None ):
365
+ def new_client_from_config (
366
+ config_file = None ,
367
+ context = None ,
368
+ persist_config = True ):
315
369
"""Loads configuration the same as load_kube_config but returns an ApiClient
316
370
to be used with any API object. This will allow the caller to concurrently
317
371
talk with multiple clusters."""
318
372
client_config = ConfigurationObject ()
319
373
load_kube_config (config_file = config_file , context = context ,
320
- client_configuration = client_config )
374
+ client_configuration = client_config ,
375
+ persist_config = persist_config )
321
376
return ApiClient (config = client_config )
0 commit comments