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

Commit eeb8b54

Browse files
committed
Add option to refresh gcp token when config is cmd-path
1 parent a2d1024 commit eeb8b54

File tree

2 files changed

+227
-10
lines changed

2 files changed

+227
-10
lines changed

Diff for: config/kube_config.py

+73
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
import logging
2121
import os
2222
import platform
23+
import subprocess
2324
import tempfile
2425
import time
26+
from collections import namedtuple
2527

2628
import google.auth
2729
import google.auth.transport.requests
@@ -133,6 +135,46 @@ def as_data(self):
133135
return self._data
134136

135137

138+
class CommandTokenSource(object):
139+
def __init__(self, cmd, args, tokenKey, expiryKey):
140+
self._cmd = cmd
141+
self._args = args
142+
if not tokenKey:
143+
self._tokenKey = '{.access_token}'
144+
else:
145+
self._tokenKey = tokenKey
146+
if not expiryKey:
147+
self._expiryKey = '{.token_expiry}'
148+
else:
149+
self._expiryKey = expiryKey
150+
151+
def token(self):
152+
fullCmd = self._cmd + (" ") + " ".join(self._args)
153+
process = subprocess.Popen(
154+
[self._cmd] + self._args,
155+
stdout=subprocess.PIPE,
156+
stderr=subprocess.PIPE,
157+
universal_newlines=True)
158+
(stdout, stderr) = process.communicate()
159+
exit_code = process.wait()
160+
if exit_code != 0:
161+
msg = 'cmd-path: process returned %d' % exit_code
162+
msg += "\nCmd: %s" % fullCmd
163+
stderr = stderr.strip()
164+
if stderr:
165+
msg += '\nStderr: %s' % stderr
166+
raise ConfigException(msg)
167+
try:
168+
data = json.loads(stdout)
169+
except ValueError as de:
170+
raise ConfigException(
171+
'exec: failed to decode process output: %s' % de)
172+
A = namedtuple('A', ['token', 'expiry'])
173+
return A(
174+
token=data['credential']['access_token'],
175+
expiry=parse_rfc3339(data['credential']['token_expiry']))
176+
177+
136178
class KubeConfigLoader(object):
137179

138180
def __init__(self, config_dict, active_context=None,
@@ -156,7 +198,38 @@ def __init__(self, config_dict, active_context=None,
156198
self._config_base_path = config_base_path
157199
self._config_persister = config_persister
158200

201+
def _refresh_credentials_with_cmd_path():
202+
config = self._user['auth-provider']['config']
203+
cmd = config['cmd-path']
204+
if len(cmd) == 0:
205+
raise ConfigException(
206+
'missing access token cmd '
207+
'(cmd-path is an empty string in your kubeconfig file)')
208+
if 'scopes' in config and config['scopes'] != "":
209+
raise ConfigException(
210+
'scopes can only be used '
211+
'when kubectl is using a gcp service account key')
212+
args = []
213+
if 'cmd-args' in config:
214+
args = config['cmd-args'].split()
215+
else:
216+
fields = config['cmd-path'].split()
217+
cmd = fields[0]
218+
args = fields[1:]
219+
220+
commandTokenSource = CommandTokenSource(
221+
cmd, args,
222+
config.safe_get('token-key'),
223+
config.safe_get('expiry-key'))
224+
return commandTokenSource.token()
225+
159226
def _refresh_credentials():
227+
# Refresh credentials using cmd-path
228+
if ('auth-provider' in self._user and
229+
'config' in self._user['auth-provider'] and
230+
'cmd-path' in self._user['auth-provider']['config']):
231+
return _refresh_credentials_with_cmd_path()
232+
160233
credentials, project_id = google.auth.default(scopes=[
161234
'https://www.googleapis.com/auth/cloud-platform',
162235
'https://www.googleapis.com/auth/userinfo.email'

Diff for: config/kube_config_test.py

+154-10
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import shutil
2020
import tempfile
2121
import unittest
22+
from collections import namedtuple
2223

2324
import mock
2425
import yaml
@@ -27,9 +28,11 @@
2728
from kubernetes.client import Configuration
2829

2930
from .config_exception import ConfigException
30-
from .kube_config import (ENV_KUBECONFIG_PATH_SEPARATOR, ConfigNode,
31-
FileOrData, KubeConfigLoader, KubeConfigMerger,
32-
_cleanup_temp_files, _create_temp_file_with_content,
31+
from .dateutil import parse_rfc3339
32+
from .kube_config import (ENV_KUBECONFIG_PATH_SEPARATOR, CommandTokenSource,
33+
ConfigNode, FileOrData, KubeConfigLoader,
34+
KubeConfigMerger, _cleanup_temp_files,
35+
_create_temp_file_with_content,
3336
list_kube_config_contexts, load_kube_config,
3437
new_client_from_config)
3538

@@ -550,6 +553,27 @@ class TestKubeConfigLoader(BaseTestCase):
550553
"user": "exec_cred_user"
551554
}
552555
},
556+
{
557+
"name": "contexttestcmdpath",
558+
"context": {
559+
"cluster": "clustertestcmdpath",
560+
"user": "usertestcmdpath"
561+
}
562+
},
563+
{
564+
"name": "contexttestcmdpathempty",
565+
"context": {
566+
"cluster": "clustertestcmdpath",
567+
"user": "usertestcmdpathempty"
568+
}
569+
},
570+
{
571+
"name": "contexttestcmdpathscope",
572+
"context": {
573+
"cluster": "clustertestcmdpath",
574+
"user": "usertestcmdpathscope"
575+
}
576+
}
553577
],
554578
"clusters": [
555579
{
@@ -588,6 +612,10 @@ class TestKubeConfigLoader(BaseTestCase):
588612
"insecure-skip-tls-verify": True,
589613
}
590614
},
615+
{
616+
"name": "clustertestcmdpath",
617+
"cluster": {}
618+
}
591619
],
592620
"users": [
593621
{
@@ -661,7 +689,8 @@ class TestKubeConfigLoader(BaseTestCase):
661689
"auth-provider": {
662690
"config": {
663691
"access-token": TEST_AZURE_TOKEN,
664-
"apiserver-id": "00000002-0000-0000-c000-000000000000",
692+
"apiserver-id": "00000002-0000-0000-c000-"
693+
"000000000000",
665694
"environment": "AzurePublicCloud",
666695
"refresh-token": "refreshToken",
667696
"tenant-id": "9d2ac018-e843-4e14-9e2b-4e0ddac75433"
@@ -676,7 +705,8 @@ class TestKubeConfigLoader(BaseTestCase):
676705
"auth-provider": {
677706
"config": {
678707
"access-token": TEST_AZURE_TOKEN,
679-
"apiserver-id": "00000002-0000-0000-c000-000000000000",
708+
"apiserver-id": "00000002-0000-0000-c000-"
709+
"000000000000",
680710
"environment": "AzurePublicCloud",
681711
"expires-in": "0",
682712
"expires-on": "156207275",
@@ -693,7 +723,8 @@ class TestKubeConfigLoader(BaseTestCase):
693723
"auth-provider": {
694724
"config": {
695725
"access-token": TEST_AZURE_TOKEN,
696-
"apiserver-id": "00000002-0000-0000-c000-000000000000",
726+
"apiserver-id": "00000002-0000-0000-c000-"
727+
"000000000000",
697728
"environment": "AzurePublicCloud",
698729
"expires-in": "0",
699730
"expires-on": "2018-10-18 00:52:29.044727",
@@ -710,7 +741,8 @@ class TestKubeConfigLoader(BaseTestCase):
710741
"auth-provider": {
711742
"config": {
712743
"access-token": TEST_AZURE_TOKEN,
713-
"apiserver-id": "00000002-0000-0000-c000-000000000000",
744+
"apiserver-id": "00000002-0000-0000-c000-"
745+
"000000000000",
714746
"environment": "AzurePublicCloud",
715747
"expires-in": "0",
716748
"expires-on": "2018-10-18 00:52",
@@ -727,7 +759,8 @@ class TestKubeConfigLoader(BaseTestCase):
727759
"auth-provider": {
728760
"config": {
729761
"access-token": TEST_AZURE_TOKEN,
730-
"apiserver-id": "00000002-0000-0000-c000-000000000000",
762+
"apiserver-id": "00000002-0000-0000-c000-"
763+
"000000000000",
731764
"environment": "AzurePublicCloud",
732765
"expires-in": "0",
733766
"expires-on": "-1",
@@ -877,6 +910,40 @@ class TestKubeConfigLoader(BaseTestCase):
877910
}
878911
}
879912
},
913+
{
914+
"name": "usertestcmdpath",
915+
"user": {
916+
"auth-provider": {
917+
"name": "gcp",
918+
"config": {
919+
"cmd-path": "cmdtorun"
920+
}
921+
}
922+
}
923+
},
924+
{
925+
"name": "usertestcmdpathempty",
926+
"user": {
927+
"auth-provider": {
928+
"name": "gcp",
929+
"config": {
930+
"cmd-path": ""
931+
}
932+
}
933+
}
934+
},
935+
{
936+
"name": "usertestcmdpathscope",
937+
"user": {
938+
"auth-provider": {
939+
"name": "gcp",
940+
"config": {
941+
"cmd-path": "cmd",
942+
"scopes": "scope"
943+
}
944+
}
945+
}
946+
}
880947
]
881948
}
882949

@@ -1279,6 +1346,46 @@ def test_user_exec_auth(self, mock):
12791346
active_context="exec_cred_user").load_and_set(actual)
12801347
self.assertEqual(expected, actual)
12811348

1349+
def test_user_cmd_path(self):
1350+
A = namedtuple('A', ['token', 'expiry'])
1351+
token = "dummy"
1352+
return_value = A(token, parse_rfc3339(datetime.datetime.now()))
1353+
CommandTokenSource.token = mock.Mock(return_value=return_value)
1354+
expected = FakeConfig(api_key={
1355+
"authorization": BEARER_TOKEN_FORMAT % token})
1356+
actual = FakeConfig()
1357+
KubeConfigLoader(
1358+
config_dict=self.TEST_KUBE_CONFIG,
1359+
active_context="contexttestcmdpath").load_and_set(actual)
1360+
expected.get_api_key_with_prefix = actual.get_api_key_with_prefix
1361+
self.assertEqual(expected, actual)
1362+
1363+
def test_user_cmd_path_empty(self):
1364+
A = namedtuple('A', ['token', 'expiry'])
1365+
token = "dummy"
1366+
return_value = A(token, parse_rfc3339(datetime.datetime.now()))
1367+
CommandTokenSource.token = mock.Mock(return_value=return_value)
1368+
expected = FakeConfig(api_key={
1369+
"authorization": BEARER_TOKEN_FORMAT % token})
1370+
actual = FakeConfig()
1371+
with self.assertRaises(ConfigException):
1372+
KubeConfigLoader(
1373+
config_dict=self.TEST_KUBE_CONFIG,
1374+
active_context="contexttestcmdpathempty").load_and_set(actual)
1375+
1376+
def test_user_cmd_path_with_scope(self):
1377+
A = namedtuple('A', ['token', 'expiry'])
1378+
token = "dummy"
1379+
return_value = A(token, parse_rfc3339(datetime.datetime.now()))
1380+
CommandTokenSource.token = mock.Mock(return_value=return_value)
1381+
expected = FakeConfig(api_key={
1382+
"authorization": BEARER_TOKEN_FORMAT % token})
1383+
actual = FakeConfig()
1384+
with self.assertRaises(ConfigException):
1385+
KubeConfigLoader(
1386+
config_dict=self.TEST_KUBE_CONFIG,
1387+
active_context="contexttestcmdpathscope").load_and_set(actual)
1388+
12821389

12831390
class TestKubernetesClientConfiguration(BaseTestCase):
12841391
# Verifies properties of kubernetes.client.Configuration.
@@ -1394,6 +1501,7 @@ class TestKubeConfigMerger(BaseTestCase):
13941501
{
13951502
"name": "expired_oidc",
13961503
"user": {
1504+
"name": "gcp",
13971505
"auth-provider": {
13981506
"name": "oidc",
13991507
"config": {
@@ -1421,14 +1529,46 @@ class TestKubeConfigMerger(BaseTestCase):
14211529
TEST_KUBE_CONFIG_PART4 = {
14221530
"current-context": "no_user",
14231531
}
1532+
# Config with user having cmd-path
1533+
TEST_KUBE_CONFIG_PART5 = {
1534+
"contexts": [
1535+
{
1536+
"name": "contexttestcmdpath",
1537+
"context": {
1538+
"cluster": "clustertestcmdpath",
1539+
"user": "usertestcmdpath"
1540+
}
1541+
}
1542+
],
1543+
"clusters": [
1544+
{
1545+
"name": "clustertestcmdpath",
1546+
"cluster": {}
1547+
}
1548+
],
1549+
"users": [
1550+
{
1551+
"name": "usertestcmdpath",
1552+
"user": {
1553+
"auth-provider": {
1554+
"name": "gcp",
1555+
"config": {
1556+
"cmd-path": "cmdtorun"
1557+
}
1558+
}
1559+
}
1560+
}
1561+
]
1562+
}
14241563

14251564
def _create_multi_config(self):
14261565
files = []
14271566
for part in (
14281567
self.TEST_KUBE_CONFIG_PART1,
14291568
self.TEST_KUBE_CONFIG_PART2,
14301569
self.TEST_KUBE_CONFIG_PART3,
1431-
self.TEST_KUBE_CONFIG_PART4):
1570+
self.TEST_KUBE_CONFIG_PART4,
1571+
self.TEST_KUBE_CONFIG_PART5):
14321572
files.append(self._create_temp_file(yaml.safe_dump(part)))
14331573
return ENV_KUBECONFIG_PATH_SEPARATOR.join(files)
14341574

@@ -1439,7 +1579,11 @@ def test_list_kube_config_contexts(self):
14391579
{'context': {'cluster': 'ssl', 'user': 'ssl'}, 'name': 'ssl'},
14401580
{'context': {'cluster': 'default', 'user': 'simple_token'},
14411581
'name': 'simple_token'},
1442-
{'context': {'cluster': 'default', 'user': 'expired_oidc'}, 'name': 'expired_oidc'}]
1582+
{'context': {'cluster': 'default', 'user': 'expired_oidc'},
1583+
'name': 'expired_oidc'},
1584+
{'context': {'cluster': 'clustertestcmdpath',
1585+
'user': 'usertestcmdpath'},
1586+
'name': 'contexttestcmdpath'}]
14431587

14441588
contexts, active_context = list_kube_config_contexts(
14451589
config_file=kubeconfigs)

0 commit comments

Comments
 (0)