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 google .oauth2 .credentials import Credentials
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 = os .environ .get ('KUBECONFIG' , '~/.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 ):
@@ -166,16 +188,32 @@ def _load_authentication(self):
166
188
def _load_gcp_token (self ):
167
189
if 'auth-provider' not in self ._user :
168
190
return
169
- if 'name' not in self ._user ['auth-provider' ]:
191
+ provider = self ._user ['auth-provider' ]
192
+ if 'name' not in provider :
170
193
return
171
- if self . _user [ 'auth- provider' ] ['name' ] != 'gcp' :
194
+ if provider ['name' ] != 'gcp' :
172
195
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 ()
196
+
197
+ if (('config' not in provider ) or
198
+ ('access-token' not in provider ['config' ]) or
199
+ ('expiry' in provider ['config' ] and
200
+ _is_expired (provider ['config' ]['expiry' ]))):
201
+ # token is not available or expired, refresh it
202
+ self ._refresh_gcp_token ()
203
+
204
+ self .token = "Bearer %s" % provider ['config' ]['access-token' ]
177
205
return self .token
178
206
207
+ def _refresh_gcp_token (self ):
208
+ if 'config' not in self ._user ['auth-provider' ]:
209
+ self ._user ['auth-provider' ].value ['config' ] = {}
210
+ provider = self ._user ['auth-provider' ]['config' ]
211
+ credentials = self ._get_google_credentials ()
212
+ provider .value ['access-token' ] = credentials .token
213
+ provider .value ['expiry' ] = _datetime_to_rfc3339 (credentials .expiry )
214
+ if self ._config_persister :
215
+ self ._config_persister (self ._config .value )
216
+
179
217
def _load_user_token (self ):
180
218
token = FileOrData (
181
219
self ._user , 'tokenFile' , 'token' ,
@@ -289,6 +327,11 @@ def _get_kube_config_loader_for_yaml_file(filename, **kwargs):
289
327
** kwargs )
290
328
291
329
330
+ def _save_kube_config (filename , config_map ):
331
+ with open (filename , 'w' ) as f :
332
+ yaml .safe_dump (config_map , f , default_flow_style = False )
333
+
334
+
292
335
def list_kube_config_contexts (config_file = None ):
293
336
294
337
if config_file is None :
@@ -299,7 +342,8 @@ def list_kube_config_contexts(config_file=None):
299
342
300
343
301
344
def load_kube_config (config_file = None , context = None ,
302
- client_configuration = configuration ):
345
+ client_configuration = configuration ,
346
+ persist_config = True ):
303
347
"""Loads authentication and cluster information from kube-config file
304
348
and stores them in kubernetes.client.configuration.
305
349
@@ -308,21 +352,32 @@ def load_kube_config(config_file=None, context=None,
308
352
from config file will be used.
309
353
:param client_configuration: The kubernetes.client.ConfigurationObject to
310
354
set configs to.
355
+ :param persist_config: If True and config changed (e.g. GCP token refresh)
356
+ the provided config file will be updated.
311
357
"""
312
358
313
359
if config_file is None :
314
360
config_file = os .path .expanduser (KUBE_CONFIG_DEFAULT_LOCATION )
315
361
362
+ config_persister = None
363
+ if persist_config :
364
+ config_persister = lambda config_map , config_file = config_file : (
365
+ _save_kube_config (config_file , config_map ))
316
366
_get_kube_config_loader_for_yaml_file (
317
367
config_file , active_context = context ,
318
- client_configuration = client_configuration ).load_and_set ()
368
+ client_configuration = client_configuration ,
369
+ config_persister = config_persister ).load_and_set ()
319
370
320
371
321
- def new_client_from_config (config_file = None , context = None ):
372
+ def new_client_from_config (
373
+ config_file = None ,
374
+ context = None ,
375
+ persist_config = True ):
322
376
"""Loads configuration the same as load_kube_config but returns an ApiClient
323
377
to be used with any API object. This will allow the caller to concurrently
324
378
talk with multiple clusters."""
325
379
client_config = ConfigurationObject ()
326
380
load_kube_config (config_file = config_file , context = context ,
327
- client_configuration = client_config )
381
+ client_configuration = client_config ,
382
+ persist_config = persist_config )
328
383
return ApiClient (config = client_config )
0 commit comments