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
26
24
27
from kubernetes .client import ApiClient , ConfigurationObject , configuration
25
28
26
29
from .config_exception import ConfigException
30
+ from .rfc3339 import tf_from_timestamp , timestamp_from_tf
27
31
32
+ EXPIRY_SKEW_PREVENTION_DELAY_S = 600
28
33
KUBE_CONFIG_DEFAULT_LOCATION = os .environ .get ('KUBECONFIG' , '~/.kube/config' )
29
34
_temp_files = {}
30
35
@@ -54,6 +59,17 @@ def _create_temp_file_with_content(content):
54
59
return name
55
60
56
61
62
+ def _is_expired (expiry ):
63
+ tf = tf_from_timestamp (expiry )
64
+ n = time .time ()
65
+ return tf + EXPIRY_SKEW_PREVENTION_DELAY_S <= n
66
+
67
+
68
+ def _datetime_to_rfc3339 (dt ):
69
+ tf = (dt - datetime .datetime .utcfromtimestamp (0 )).total_seconds ()
70
+ return timestamp_from_tf (tf , time_offset = "Z" )
71
+
72
+
57
73
class FileOrData (object ):
58
74
"""Utility class to read content of obj[%data_key_name] or file's
59
75
content of obj[%file_key_name] and represent it as file or data.
@@ -110,19 +126,26 @@ class KubeConfigLoader(object):
110
126
def __init__ (self , config_dict , active_context = None ,
111
127
get_google_credentials = None ,
112
128
client_configuration = configuration ,
113
- config_base_path = "" ):
129
+ config_base_path = "" ,
130
+ config_persister = None ):
114
131
self ._config = ConfigNode ('kube-config' , config_dict )
115
132
self ._current_context = None
116
133
self ._user = None
117
134
self ._cluster = None
118
135
self .set_active_context (active_context )
119
136
self ._config_base_path = config_base_path
137
+ self ._config_persister = config_persister
138
+
139
+ def _refresh_credentials ():
140
+ credentials , project_id = google .auth .default ()
141
+ request = google .auth .transport .requests .Request ()
142
+ credentials .refresh (request )
143
+ return credentials
144
+
120
145
if get_google_credentials :
121
146
self ._get_google_credentials = get_google_credentials
122
147
else :
123
- self ._get_google_credentials = lambda : (
124
- GoogleCredentials .get_application_default ()
125
- .get_access_token ().access_token )
148
+ self ._get_google_credentials = _refresh_credentials
126
149
self ._client_configuration = client_configuration
127
150
128
151
def set_active_context (self , context_name = None ):
@@ -166,16 +189,32 @@ def _load_authentication(self):
166
189
def _load_gcp_token (self ):
167
190
if 'auth-provider' not in self ._user :
168
191
return
169
- if 'name' not in self ._user ['auth-provider' ]:
192
+ provider = self ._user ['auth-provider' ]
193
+ if 'name' not in provider :
170
194
return
171
- if self . _user [ 'auth- provider' ] ['name' ] != 'gcp' :
195
+ if provider ['name' ] != 'gcp' :
172
196
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 ()
197
+
198
+ if (('config' not in provider ) or
199
+ ('access-token' not in provider ['config' ]) or
200
+ ('expiry' in provider ['config' ] and
201
+ _is_expired (provider ['config' ]['expiry' ]))):
202
+ # token is not available or expired, refresh it
203
+ self ._refresh_gcp_token ()
204
+
205
+ self .token = "Bearer %s" % provider ['config' ]['access-token' ]
177
206
return self .token
178
207
208
+ def _refresh_gcp_token (self ):
209
+ if 'config' not in self ._user ['auth-provider' ]:
210
+ self ._user ['auth-provider' ].value ['config' ] = {}
211
+ provider = self ._user ['auth-provider' ]['config' ]
212
+ credentials = self ._get_google_credentials ()
213
+ provider .value ['access-token' ] = credentials .token
214
+ provider .value ['expiry' ] = _datetime_to_rfc3339 (credentials .expiry )
215
+ if self ._config_persister :
216
+ self ._config_persister (self ._config .value )
217
+
179
218
def _load_user_token (self ):
180
219
token = FileOrData (
181
220
self ._user , 'tokenFile' , 'token' ,
@@ -289,6 +328,11 @@ def _get_kube_config_loader_for_yaml_file(filename, **kwargs):
289
328
** kwargs )
290
329
291
330
331
+ def _save_kube_config (filename , config_map ):
332
+ with open (filename , 'w' ) as f :
333
+ yaml .safe_dump (config_map , f , default_flow_style = False )
334
+
335
+
292
336
def list_kube_config_contexts (config_file = None ):
293
337
294
338
if config_file is None :
@@ -299,7 +343,8 @@ def list_kube_config_contexts(config_file=None):
299
343
300
344
301
345
def load_kube_config (config_file = None , context = None ,
302
- client_configuration = configuration ):
346
+ client_configuration = configuration ,
347
+ persist_config = True ):
303
348
"""Loads authentication and cluster information from kube-config file
304
349
and stores them in kubernetes.client.configuration.
305
350
@@ -308,21 +353,32 @@ def load_kube_config(config_file=None, context=None,
308
353
from config file will be used.
309
354
:param client_configuration: The kubernetes.client.ConfigurationObject to
310
355
set configs to.
356
+ :param persist_config: If True and config changed (e.g. GCP token refresh)
357
+ the provided config file will be updated.
311
358
"""
312
359
313
360
if config_file is None :
314
361
config_file = os .path .expanduser (KUBE_CONFIG_DEFAULT_LOCATION )
315
362
363
+ config_persister = None
364
+ if persist_config :
365
+ config_persister = lambda config_map , config_file = config_file : (
366
+ _save_kube_config (config_file , config_map ))
316
367
_get_kube_config_loader_for_yaml_file (
317
368
config_file , active_context = context ,
318
- client_configuration = client_configuration ).load_and_set ()
369
+ client_configuration = client_configuration ,
370
+ config_persister = config_persister ).load_and_set ()
319
371
320
372
321
- def new_client_from_config (config_file = None , context = None ):
373
+ def new_client_from_config (
374
+ config_file = None ,
375
+ context = None ,
376
+ persist_config = True ):
322
377
"""Loads configuration the same as load_kube_config but returns an ApiClient
323
378
to be used with any API object. This will allow the caller to concurrently
324
379
talk with multiple clusters."""
325
380
client_config = ConfigurationObject ()
326
381
load_kube_config (config_file = config_file , context = context ,
327
- client_configuration = client_config )
382
+ client_configuration = client_config ,
383
+ persist_config = persist_config )
328
384
return ApiClient (config = client_config )
0 commit comments