Skip to content
This repository was archived by the owner on Mar 13, 2022. It is now read-only.

Commit 949c39e

Browse files
authored
Merge pull request #92 from TrevorEdwards/59_override
Refresh GCP tokens on retrieval by overriding client config method.
2 parents 4da0fcc + 8f3a69e commit 949c39e

File tree

2 files changed

+122
-15
lines changed

2 files changed

+122
-15
lines changed

config/kube_config.py

+16
Original file line numberDiff line numberDiff line change
@@ -392,8 +392,24 @@ def _load_cluster_info(self):
392392
if 'insecure-skip-tls-verify' in self._cluster:
393393
self.verify_ssl = not self._cluster['insecure-skip-tls-verify']
394394

395+
def _using_gcp_auth_provider(self):
396+
return self._user and \
397+
'auth-provider' in self._user and \
398+
'name' in self._user['auth-provider'] and \
399+
self._user['auth-provider']['name'] == 'gcp'
400+
395401
def _set_config(self, client_configuration):
402+
if self._using_gcp_auth_provider():
403+
# GCP auth tokens must be refreshed regularly, but swagger expects
404+
# a constant token. Replace the swagger-generated client config's
405+
# get_api_key_with_prefix method with our own to allow automatic
406+
# token refresh.
407+
def _gcp_get_api_key(*args):
408+
return self._load_gcp_token(self._user['auth-provider'])
409+
client_configuration.get_api_key_with_prefix = _gcp_get_api_key
396410
if 'token' in self.__dict__:
411+
# Note: this line runs for GCP auth tokens as well, but this entry
412+
# will not be updated upon GCP token refresh.
397413
client_configuration.api_key['authorization'] = self.token
398414
# copy these keys directly from self to configuration object
399415
keys = ['host', 'ssl_ca_cert', 'cert_file', 'key_file', 'verify_ssl']

config/kube_config_test.py

+106-15
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import yaml
2525
from six import PY3, next
2626

27+
from kubernetes.client import Configuration
28+
2729
from .config_exception import ConfigException
2830
from .kube_config import (ConfigNode, FileOrData, KubeConfigLoader,
2931
_cleanup_temp_files, _create_temp_file_with_content,
@@ -34,7 +36,9 @@
3436

3537
EXPIRY_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
3638
# should be less than kube_config.EXPIRY_SKEW_PREVENTION_DELAY
37-
EXPIRY_TIMEDELTA = 2
39+
PAST_EXPIRY_TIMEDELTA = 2
40+
# should be more than kube_config.EXPIRY_SKEW_PREVENTION_DELAY
41+
FUTURE_EXPIRY_TIMEDELTA = 60
3842

3943
NON_EXISTING_FILE = "zz_non_existing_file_472398324"
4044

@@ -47,9 +51,9 @@ def _format_expiry_datetime(dt):
4751
return dt.strftime(EXPIRY_DATETIME_FORMAT)
4852

4953

50-
def _get_expiry(loader):
54+
def _get_expiry(loader, active_context):
5155
expired_gcp_conf = (item for item in loader._config.value.get("users")
52-
if item.get("name") == "expired_gcp")
56+
if item.get("name") == active_context)
5357
return next(expired_gcp_conf).get("user").get("auth-provider") \
5458
.get("config").get("expiry")
5559

@@ -73,8 +77,11 @@ def _raise_exception(st):
7377
TEST_PASSWORD = "pass"
7478
# token for me:pass
7579
TEST_BASIC_TOKEN = "Basic bWU6cGFzcw=="
76-
TEST_TOKEN_EXPIRY = _format_expiry_datetime(
77-
datetime.datetime.utcnow() - datetime.timedelta(minutes=EXPIRY_TIMEDELTA))
80+
DATETIME_EXPIRY_PAST = datetime.datetime.utcnow(
81+
) - datetime.timedelta(minutes=PAST_EXPIRY_TIMEDELTA)
82+
DATETIME_EXPIRY_FUTURE = datetime.datetime.utcnow(
83+
) + datetime.timedelta(minutes=FUTURE_EXPIRY_TIMEDELTA)
84+
TEST_TOKEN_EXPIRY_PAST = _format_expiry_datetime(DATETIME_EXPIRY_PAST)
7885

7986
TEST_SSL_HOST = "https://test-host"
8087
TEST_CERTIFICATE_AUTH = "cert-auth"
@@ -371,6 +378,13 @@ class TestKubeConfigLoader(BaseTestCase):
371378
"user": "expired_gcp"
372379
}
373380
},
381+
{
382+
"name": "expired_gcp_refresh",
383+
"context": {
384+
"cluster": "default",
385+
"user": "expired_gcp_refresh"
386+
}
387+
},
374388
{
375389
"name": "oidc",
376390
"context": {
@@ -509,7 +523,24 @@ class TestKubeConfigLoader(BaseTestCase):
509523
"name": "gcp",
510524
"config": {
511525
"access-token": TEST_DATA_BASE64,
512-
"expiry": TEST_TOKEN_EXPIRY, # always in past
526+
"expiry": TEST_TOKEN_EXPIRY_PAST, # always in past
527+
}
528+
},
529+
"token": TEST_DATA_BASE64, # should be ignored
530+
"username": TEST_USERNAME, # should be ignored
531+
"password": TEST_PASSWORD, # should be ignored
532+
}
533+
},
534+
# Duplicated from "expired_gcp" so test_load_gcp_token_with_refresh
535+
# is isolated from test_gcp_get_api_key_with_prefix.
536+
{
537+
"name": "expired_gcp_refresh",
538+
"user": {
539+
"auth-provider": {
540+
"name": "gcp",
541+
"config": {
542+
"access-token": TEST_DATA_BASE64,
543+
"expiry": TEST_TOKEN_EXPIRY_PAST, # always in past
513544
}
514545
},
515546
"token": TEST_DATA_BASE64, # should be ignored
@@ -630,16 +661,20 @@ def test_load_user_token(self):
630661
self.assertEqual(BEARER_TOKEN_FORMAT % TEST_DATA_BASE64, loader.token)
631662

632663
def test_gcp_no_refresh(self):
633-
expected = FakeConfig(
634-
host=TEST_HOST,
635-
token=BEARER_TOKEN_FORMAT % TEST_DATA_BASE64)
636-
actual = FakeConfig()
664+
fake_config = FakeConfig()
665+
# swagger-generated config has this, but FakeConfig does not.
666+
self.assertFalse(hasattr(fake_config, 'get_api_key_with_prefix'))
637667
KubeConfigLoader(
638668
config_dict=self.TEST_KUBE_CONFIG,
639669
active_context="gcp",
640670
get_google_credentials=lambda: _raise_exception(
641-
"SHOULD NOT BE CALLED")).load_and_set(actual)
642-
self.assertEqual(expected, actual)
671+
"SHOULD NOT BE CALLED")).load_and_set(fake_config)
672+
# Should now be populated with a gcp token fetcher.
673+
self.assertIsNotNone(fake_config.get_api_key_with_prefix)
674+
self.assertEqual(TEST_HOST, fake_config.host)
675+
# For backwards compatibility, authorization field should still be set.
676+
self.assertEqual(BEARER_TOKEN_FORMAT % TEST_DATA_BASE64,
677+
fake_config.api_key['authorization'])
643678

644679
def test_load_gcp_token_no_refresh(self):
645680
loader = KubeConfigLoader(
@@ -654,20 +689,48 @@ def test_load_gcp_token_no_refresh(self):
654689
def test_load_gcp_token_with_refresh(self):
655690
def cred(): return None
656691
cred.token = TEST_ANOTHER_DATA_BASE64
657-
cred.expiry = datetime.datetime.now()
692+
cred.expiry = datetime.datetime.utcnow()
658693

659694
loader = KubeConfigLoader(
660695
config_dict=self.TEST_KUBE_CONFIG,
661696
active_context="expired_gcp",
662697
get_google_credentials=lambda: cred)
663-
original_expiry = _get_expiry(loader)
698+
original_expiry = _get_expiry(loader, "expired_gcp")
664699
self.assertTrue(loader._load_auth_provider_token())
665-
new_expiry = _get_expiry(loader)
700+
new_expiry = _get_expiry(loader, "expired_gcp")
666701
# assert that the configs expiry actually updates
667702
self.assertTrue(new_expiry > original_expiry)
668703
self.assertEqual(BEARER_TOKEN_FORMAT % TEST_ANOTHER_DATA_BASE64,
669704
loader.token)
670705

706+
def test_gcp_get_api_key_with_prefix(self):
707+
class cred_old:
708+
token = TEST_DATA_BASE64
709+
expiry = DATETIME_EXPIRY_PAST
710+
711+
class cred_new:
712+
token = TEST_ANOTHER_DATA_BASE64
713+
expiry = DATETIME_EXPIRY_FUTURE
714+
fake_config = FakeConfig()
715+
_get_google_credentials = mock.Mock()
716+
_get_google_credentials.side_effect = [cred_old, cred_new]
717+
718+
loader = KubeConfigLoader(
719+
config_dict=self.TEST_KUBE_CONFIG,
720+
active_context="expired_gcp_refresh",
721+
get_google_credentials=_get_google_credentials)
722+
loader.load_and_set(fake_config)
723+
original_expiry = _get_expiry(loader, "expired_gcp_refresh")
724+
# Call GCP token fetcher.
725+
token = fake_config.get_api_key_with_prefix()
726+
new_expiry = _get_expiry(loader, "expired_gcp_refresh")
727+
728+
self.assertTrue(new_expiry > original_expiry)
729+
self.assertEqual(BEARER_TOKEN_FORMAT % TEST_ANOTHER_DATA_BASE64,
730+
loader.token)
731+
self.assertEqual(BEARER_TOKEN_FORMAT % TEST_ANOTHER_DATA_BASE64,
732+
token)
733+
671734
def test_oidc_no_refresh(self):
672735
loader = KubeConfigLoader(
673736
config_dict=self.TEST_KUBE_CONFIG,
@@ -893,5 +956,33 @@ def test_user_exec_auth(self, mock):
893956
self.assertEqual(expected, actual)
894957

895958

959+
class TestKubernetesClientConfiguration(BaseTestCase):
960+
# Verifies properties of kubernetes.client.Configuration.
961+
# These tests guard against changes to the upstream configuration class,
962+
# since GCP authorization overrides get_api_key_with_prefix to refresh its
963+
# token regularly.
964+
965+
def test_get_api_key_with_prefix_exists(self):
966+
self.assertTrue(hasattr(Configuration, 'get_api_key_with_prefix'))
967+
968+
def test_get_api_key_with_prefix_returns_token(self):
969+
expected_token = 'expected_token'
970+
config = Configuration()
971+
config.api_key['authorization'] = expected_token
972+
self.assertEqual(expected_token,
973+
config.get_api_key_with_prefix('authorization'))
974+
975+
def test_auth_settings_calls_get_api_key_with_prefix(self):
976+
expected_token = 'expected_token'
977+
978+
def fake_get_api_key_with_prefix(identifier):
979+
self.assertEqual('authorization', identifier)
980+
return expected_token
981+
config = Configuration()
982+
config.get_api_key_with_prefix = fake_get_api_key_with_prefix
983+
self.assertEqual(expected_token,
984+
config.auth_settings()['BearerToken']['value'])
985+
986+
896987
if __name__ == '__main__':
897988
unittest.main()

0 commit comments

Comments
 (0)