-
Notifications
You must be signed in to change notification settings - Fork 182
Refresh GCP tokens on retrieval by overriding client config method. #92
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -392,8 +392,24 @@ def _load_cluster_info(self): | |
if 'insecure-skip-tls-verify' in self._cluster: | ||
self.verify_ssl = not self._cluster['insecure-skip-tls-verify'] | ||
|
||
def _using_gcp_auth_provider(self): | ||
return self._user and \ | ||
'auth-provider' in self._user and \ | ||
'name' in self._user['auth-provider'] and \ | ||
self._user['auth-provider']['name'] == 'gcp' | ||
|
||
def _set_config(self, client_configuration): | ||
if self._using_gcp_auth_provider(): | ||
# GCP auth tokens must be refreshed regularly, but swagger expects | ||
# a constant token. Replace the swagger-generated client config's | ||
# get_api_key_with_prefix method with our own to allow automatic | ||
# token refresh. | ||
def _gcp_get_api_key(*args): | ||
return self._load_gcp_token(self._user['auth-provider']) | ||
client_configuration.get_api_key_with_prefix = _gcp_get_api_key | ||
if 'token' in self.__dict__: | ||
# Note: this line runs for GCP auth tokens as well, but this entry | ||
# will not be updated upon GCP token refresh. | ||
client_configuration.api_key['authorization'] = self.token | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added comments to make that behavior more clear, unless you mean that you want this documented somewhere else. The only existing workaround I know of will catch exceptions when an API call fails (via a wrapper) and then reload the config. I believe with this change, they will never hit that exception. |
||
# copy these keys directly from self to configuration object | ||
keys = ['host', 'ssl_ca_cert', 'cert_file', 'key_file', 'verify_ssl'] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,8 @@ | |
import yaml | ||
from six import PY3, next | ||
|
||
from kubernetes.client import Configuration | ||
|
||
from .config_exception import ConfigException | ||
from .kube_config import (ConfigNode, FileOrData, KubeConfigLoader, | ||
_cleanup_temp_files, _create_temp_file_with_content, | ||
|
@@ -34,7 +36,9 @@ | |
|
||
EXPIRY_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" | ||
# should be less than kube_config.EXPIRY_SKEW_PREVENTION_DELAY | ||
EXPIRY_TIMEDELTA = 2 | ||
PAST_EXPIRY_TIMEDELTA = 2 | ||
# should be more than kube_config.EXPIRY_SKEW_PREVENTION_DELAY | ||
FUTURE_EXPIRY_TIMEDELTA = 60 | ||
|
||
NON_EXISTING_FILE = "zz_non_existing_file_472398324" | ||
|
||
|
@@ -47,9 +51,9 @@ def _format_expiry_datetime(dt): | |
return dt.strftime(EXPIRY_DATETIME_FORMAT) | ||
|
||
|
||
def _get_expiry(loader): | ||
def _get_expiry(loader, active_context): | ||
expired_gcp_conf = (item for item in loader._config.value.get("users") | ||
if item.get("name") == "expired_gcp") | ||
if item.get("name") == active_context) | ||
return next(expired_gcp_conf).get("user").get("auth-provider") \ | ||
.get("config").get("expiry") | ||
|
||
|
@@ -73,8 +77,11 @@ def _raise_exception(st): | |
TEST_PASSWORD = "pass" | ||
# token for me:pass | ||
TEST_BASIC_TOKEN = "Basic bWU6cGFzcw==" | ||
TEST_TOKEN_EXPIRY = _format_expiry_datetime( | ||
datetime.datetime.utcnow() - datetime.timedelta(minutes=EXPIRY_TIMEDELTA)) | ||
DATETIME_EXPIRY_PAST = datetime.datetime.utcnow( | ||
) - datetime.timedelta(minutes=PAST_EXPIRY_TIMEDELTA) | ||
DATETIME_EXPIRY_FUTURE = datetime.datetime.utcnow( | ||
) + datetime.timedelta(minutes=FUTURE_EXPIRY_TIMEDELTA) | ||
TEST_TOKEN_EXPIRY_PAST = _format_expiry_datetime(DATETIME_EXPIRY_PAST) | ||
|
||
TEST_SSL_HOST = "https://test-host" | ||
TEST_CERTIFICATE_AUTH = "cert-auth" | ||
|
@@ -371,6 +378,13 @@ class TestKubeConfigLoader(BaseTestCase): | |
"user": "expired_gcp" | ||
} | ||
}, | ||
{ | ||
"name": "expired_gcp_refresh", | ||
"context": { | ||
"cluster": "default", | ||
"user": "expired_gcp_refresh" | ||
} | ||
}, | ||
{ | ||
"name": "oidc", | ||
"context": { | ||
|
@@ -509,7 +523,24 @@ class TestKubeConfigLoader(BaseTestCase): | |
"name": "gcp", | ||
"config": { | ||
"access-token": TEST_DATA_BASE64, | ||
"expiry": TEST_TOKEN_EXPIRY, # always in past | ||
"expiry": TEST_TOKEN_EXPIRY_PAST, # always in past | ||
} | ||
}, | ||
"token": TEST_DATA_BASE64, # should be ignored | ||
"username": TEST_USERNAME, # should be ignored | ||
"password": TEST_PASSWORD, # should be ignored | ||
} | ||
}, | ||
# Duplicated from "expired_gcp" so test_load_gcp_token_with_refresh | ||
# is isolated from test_gcp_get_api_key_with_prefix. | ||
{ | ||
"name": "expired_gcp_refresh", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Added a comment to note that. |
||
"user": { | ||
"auth-provider": { | ||
"name": "gcp", | ||
"config": { | ||
"access-token": TEST_DATA_BASE64, | ||
"expiry": TEST_TOKEN_EXPIRY_PAST, # always in past | ||
} | ||
}, | ||
"token": TEST_DATA_BASE64, # should be ignored | ||
|
@@ -630,16 +661,20 @@ def test_load_user_token(self): | |
self.assertEqual(BEARER_TOKEN_FORMAT % TEST_DATA_BASE64, loader.token) | ||
|
||
def test_gcp_no_refresh(self): | ||
expected = FakeConfig( | ||
host=TEST_HOST, | ||
token=BEARER_TOKEN_FORMAT % TEST_DATA_BASE64) | ||
actual = FakeConfig() | ||
fake_config = FakeConfig() | ||
# swagger-generated config has this, but FakeConfig does not. | ||
self.assertFalse(hasattr(fake_config, 'get_api_key_with_prefix')) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we add a test to make sure swagger-generated config has this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
KubeConfigLoader( | ||
config_dict=self.TEST_KUBE_CONFIG, | ||
active_context="gcp", | ||
get_google_credentials=lambda: _raise_exception( | ||
"SHOULD NOT BE CALLED")).load_and_set(actual) | ||
self.assertEqual(expected, actual) | ||
"SHOULD NOT BE CALLED")).load_and_set(fake_config) | ||
# Should now be populated with a gcp token fetcher. | ||
self.assertIsNotNone(fake_config.get_api_key_with_prefix) | ||
self.assertEqual(TEST_HOST, fake_config.host) | ||
# For backwards compatibility, authorization field should still be set. | ||
self.assertEqual(BEARER_TOKEN_FORMAT % TEST_DATA_BASE64, | ||
fake_config.api_key['authorization']) | ||
|
||
def test_load_gcp_token_no_refresh(self): | ||
loader = KubeConfigLoader( | ||
|
@@ -654,20 +689,48 @@ def test_load_gcp_token_no_refresh(self): | |
def test_load_gcp_token_with_refresh(self): | ||
def cred(): return None | ||
cred.token = TEST_ANOTHER_DATA_BASE64 | ||
cred.expiry = datetime.datetime.now() | ||
cred.expiry = datetime.datetime.utcnow() | ||
|
||
loader = KubeConfigLoader( | ||
config_dict=self.TEST_KUBE_CONFIG, | ||
active_context="expired_gcp", | ||
get_google_credentials=lambda: cred) | ||
original_expiry = _get_expiry(loader) | ||
original_expiry = _get_expiry(loader, "expired_gcp") | ||
self.assertTrue(loader._load_auth_provider_token()) | ||
new_expiry = _get_expiry(loader) | ||
new_expiry = _get_expiry(loader, "expired_gcp") | ||
# assert that the configs expiry actually updates | ||
self.assertTrue(new_expiry > original_expiry) | ||
self.assertEqual(BEARER_TOKEN_FORMAT % TEST_ANOTHER_DATA_BASE64, | ||
loader.token) | ||
|
||
def test_gcp_get_api_key_with_prefix(self): | ||
class cred_old: | ||
token = TEST_DATA_BASE64 | ||
expiry = DATETIME_EXPIRY_PAST | ||
|
||
class cred_new: | ||
token = TEST_ANOTHER_DATA_BASE64 | ||
expiry = DATETIME_EXPIRY_FUTURE | ||
fake_config = FakeConfig() | ||
_get_google_credentials = mock.Mock() | ||
_get_google_credentials.side_effect = [cred_old, cred_new] | ||
|
||
loader = KubeConfigLoader( | ||
config_dict=self.TEST_KUBE_CONFIG, | ||
active_context="expired_gcp_refresh", | ||
get_google_credentials=_get_google_credentials) | ||
loader.load_and_set(fake_config) | ||
original_expiry = _get_expiry(loader, "expired_gcp_refresh") | ||
# Call GCP token fetcher. | ||
token = fake_config.get_api_key_with_prefix() | ||
new_expiry = _get_expiry(loader, "expired_gcp_refresh") | ||
|
||
self.assertTrue(new_expiry > original_expiry) | ||
self.assertEqual(BEARER_TOKEN_FORMAT % TEST_ANOTHER_DATA_BASE64, | ||
loader.token) | ||
self.assertEqual(BEARER_TOKEN_FORMAT % TEST_ANOTHER_DATA_BASE64, | ||
token) | ||
|
||
def test_oidc_no_refresh(self): | ||
loader = KubeConfigLoader( | ||
config_dict=self.TEST_KUBE_CONFIG, | ||
|
@@ -893,5 +956,33 @@ def test_user_exec_auth(self, mock): | |
self.assertEqual(expected, actual) | ||
|
||
|
||
class TestKubernetesClientConfiguration(BaseTestCase): | ||
# Verifies properties of kubernetes.client.Configuration. | ||
# These tests guard against changes to the upstream configuration class, | ||
# since GCP authorization overrides get_api_key_with_prefix to refresh its | ||
# token regularly. | ||
|
||
def test_get_api_key_with_prefix_exists(self): | ||
self.assertTrue(hasattr(Configuration, 'get_api_key_with_prefix')) | ||
|
||
def test_get_api_key_with_prefix_returns_token(self): | ||
expected_token = 'expected_token' | ||
config = Configuration() | ||
config.api_key['authorization'] = expected_token | ||
self.assertEqual(expected_token, | ||
config.get_api_key_with_prefix('authorization')) | ||
|
||
def test_auth_settings_calls_get_api_key_with_prefix(self): | ||
expected_token = 'expected_token' | ||
|
||
def fake_get_api_key_with_prefix(identifier): | ||
self.assertEqual('authorization', identifier) | ||
return expected_token | ||
config = Configuration() | ||
config.get_api_key_with_prefix = fake_get_api_key_with_prefix | ||
self.assertEqual(expected_token, | ||
config.auth_settings()['BearerToken']['value']) | ||
|
||
|
||
if __name__ == '__main__': | ||
unittest.main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am ok with this hack. just to make sure we are protected when things change in configuration class of generated client, can we add proper tests to make sure get_api_key_with_prefix exists and being called the way we expected it.
I can't tell from tests that we are protected against changes in configuration class. please add them if they are missing. e.g. if get_api_key_with_prefix got rename, or if it is being called but it returns something else. I see a whitebox test for get_api_key_with_prefix, but it won't protect us against get_api_key_with_prefix being removed or renamed in configuration class).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. I added three new test cases which I believe cover all of the concerns that you list.