Add option to refresh gcp token when config is cmd-path

This commit is contained in:
Fabrice Rabaute 2019-10-03 15:04:30 -07:00
parent a2d1024524
commit 39113de2aa
No known key found for this signature in database
GPG Key ID: 230B0BA8E423D606
2 changed files with 228 additions and 10 deletions

View File

@ -20,8 +20,10 @@ import json
import logging
import os
import platform
import subprocess
import tempfile
import time
from collections import namedtuple
import google.auth
import google.auth.transport.requests
@ -133,6 +135,46 @@ class FileOrData(object):
return self._data
class CommandTokenSource(object):
def __init__(self, cmd, args, tokenKey, expiryKey):
self._cmd = cmd
self._args = args
if not tokenKey:
self._tokenKey = '{.access_token}'
else:
self._tokenKey = tokenKey
if not expiryKey:
self._expiryKey = '{.token_expiry}'
else:
self._expiryKey = expiryKey
def token(self):
fullCmd = self._cmd + (" ") + " ".join(self._args)
process = subprocess.Popen(
[self._cmd] + self._args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
(stdout, stderr) = process.communicate()
exit_code = process.wait()
if exit_code != 0:
msg = 'cmd-path: process returned %d' % exit_code
msg += "\nCmd: %s" % fullCmd
stderr = stderr.strip()
if stderr:
msg += '\nStderr: %s' % stderr
raise ConfigException(msg)
try:
data = json.loads(stdout)
except ValueError as de:
raise ConfigException(
'exec: failed to decode process output: %s' % de)
A = namedtuple('A', ['token', 'expiry'])
return A(
token=data['credential']['access_token'],
expiry=parse_rfc3339(data['credential']['token_expiry']))
class KubeConfigLoader(object):
def __init__(self, config_dict, active_context=None,
@ -156,7 +198,38 @@ class KubeConfigLoader(object):
self._config_base_path = config_base_path
self._config_persister = config_persister
def _refresh_credentials_with_cmd_path():
config = self._user['auth-provider']['config']
cmd = config['cmd-path']
if len(cmd) == 0:
raise ConfigException(
'missing access token cmd '
'(cmd-path is an empty string in your kubeconfig file)')
if 'scopes' in config and config['scopes'] != "":
raise ConfigException(
'scopes can only be used '
'when kubectl is using a gcp service account key')
args = []
if 'cmd-args' in config:
args = config['cmd-args'].split()
else:
fields = config['cmd-path'].split()
cmd = fields[0]
args = fields[1:]
commandTokenSource = CommandTokenSource(
cmd, args,
config.safe_get('token-key'),
config.safe_get('expiry-key'))
return commandTokenSource.token()
def _refresh_credentials():
# Refresh credentials using cmd-path
if ('auth-provider' in self._user and
'config' in self._user['auth-provider'] and
'cmd-path' in self._user['auth-provider']['config']):
return _refresh_credentials_with_cmd_path()
credentials, project_id = google.auth.default(scopes=[
'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/userinfo.email'

View File

@ -19,6 +19,7 @@ import os
import shutil
import tempfile
import unittest
from collections import namedtuple
import mock
import yaml
@ -27,9 +28,11 @@ from six import PY3, next
from kubernetes.client import Configuration
from .config_exception import ConfigException
from .kube_config import (ENV_KUBECONFIG_PATH_SEPARATOR, ConfigNode,
FileOrData, KubeConfigLoader, KubeConfigMerger,
_cleanup_temp_files, _create_temp_file_with_content,
from .dateutil import parse_rfc3339
from .kube_config import (ENV_KUBECONFIG_PATH_SEPARATOR, CommandTokenSource,
ConfigNode, FileOrData, KubeConfigLoader,
KubeConfigMerger, _cleanup_temp_files,
_create_temp_file_with_content,
list_kube_config_contexts, load_kube_config,
new_client_from_config)
@ -550,6 +553,27 @@ class TestKubeConfigLoader(BaseTestCase):
"user": "exec_cred_user"
}
},
{
"name": "contexttestcmdpath",
"context": {
"cluster": "clustertestcmdpath",
"user": "usertestcmdpath"
}
},
{
"name": "contexttestcmdpathempty",
"context": {
"cluster": "clustertestcmdpath",
"user": "usertestcmdpathempty"
}
},
{
"name": "contexttestcmdpathscope",
"context": {
"cluster": "clustertestcmdpath",
"user": "usertestcmdpathscope"
}
}
],
"clusters": [
{
@ -588,6 +612,10 @@ class TestKubeConfigLoader(BaseTestCase):
"insecure-skip-tls-verify": True,
}
},
{
"name": "clustertestcmdpath",
"cluster": {}
}
],
"users": [
{
@ -661,7 +689,8 @@ class TestKubeConfigLoader(BaseTestCase):
"auth-provider": {
"config": {
"access-token": TEST_AZURE_TOKEN,
"apiserver-id": "00000002-0000-0000-c000-000000000000",
"apiserver-id": "00000002-0000-0000-c000-"
"000000000000",
"environment": "AzurePublicCloud",
"refresh-token": "refreshToken",
"tenant-id": "9d2ac018-e843-4e14-9e2b-4e0ddac75433"
@ -676,7 +705,8 @@ class TestKubeConfigLoader(BaseTestCase):
"auth-provider": {
"config": {
"access-token": TEST_AZURE_TOKEN,
"apiserver-id": "00000002-0000-0000-c000-000000000000",
"apiserver-id": "00000002-0000-0000-c000-"
"000000000000",
"environment": "AzurePublicCloud",
"expires-in": "0",
"expires-on": "156207275",
@ -693,7 +723,8 @@ class TestKubeConfigLoader(BaseTestCase):
"auth-provider": {
"config": {
"access-token": TEST_AZURE_TOKEN,
"apiserver-id": "00000002-0000-0000-c000-000000000000",
"apiserver-id": "00000002-0000-0000-c000-"
"000000000000",
"environment": "AzurePublicCloud",
"expires-in": "0",
"expires-on": "2018-10-18 00:52:29.044727",
@ -710,7 +741,8 @@ class TestKubeConfigLoader(BaseTestCase):
"auth-provider": {
"config": {
"access-token": TEST_AZURE_TOKEN,
"apiserver-id": "00000002-0000-0000-c000-000000000000",
"apiserver-id": "00000002-0000-0000-c000-"
"000000000000",
"environment": "AzurePublicCloud",
"expires-in": "0",
"expires-on": "2018-10-18 00:52",
@ -727,7 +759,8 @@ class TestKubeConfigLoader(BaseTestCase):
"auth-provider": {
"config": {
"access-token": TEST_AZURE_TOKEN,
"apiserver-id": "00000002-0000-0000-c000-000000000000",
"apiserver-id": "00000002-0000-0000-c000-"
"000000000000",
"environment": "AzurePublicCloud",
"expires-in": "0",
"expires-on": "-1",
@ -877,6 +910,40 @@ class TestKubeConfigLoader(BaseTestCase):
}
}
},
{
"name": "usertestcmdpath",
"user": {
"auth-provider": {
"name": "gcp",
"config": {
"cmd-path": "cmdtorun"
}
}
}
},
{
"name": "usertestcmdpathempty",
"user": {
"auth-provider": {
"name": "gcp",
"config": {
"cmd-path": ""
}
}
}
},
{
"name": "usertestcmdpathscope",
"user": {
"auth-provider": {
"name": "gcp",
"config": {
"cmd-path": "cmd",
"scopes": "scope"
}
}
}
}
]
}
@ -1279,6 +1346,48 @@ class TestKubeConfigLoader(BaseTestCase):
active_context="exec_cred_user").load_and_set(actual)
self.assertEqual(expected, actual)
def test_user_cmd_path(self):
A = namedtuple('A', ['token', 'expiry'])
token = "dummy"
return_value = A(token, parse_rfc3339(datetime.datetime.now()))
CommandTokenSource.token = mock.Mock(return_value=return_value)
expected = FakeConfig(api_key={
"authorization": BEARER_TOKEN_FORMAT % token})
actual = FakeConfig()
KubeConfigLoader(
config_dict=self.TEST_KUBE_CONFIG,
active_context="contexttestcmdpath").load_and_set(actual)
del actual.get_api_key_with_prefix
self.assertEqual(expected, actual)
def test_user_cmd_path_empty(self):
A = namedtuple('A', ['token', 'expiry'])
token = "dummy"
return_value = A(token, parse_rfc3339(datetime.datetime.now()))
CommandTokenSource.token = mock.Mock(return_value=return_value)
expected = FakeConfig(api_key={
"authorization": BEARER_TOKEN_FORMAT % token})
actual = FakeConfig()
self.expect_exception(lambda: KubeConfigLoader(
config_dict=self.TEST_KUBE_CONFIG,
active_context="contexttestcmdpathempty").load_and_set(actual),
"missing access token cmd "
"(cmd-path is an empty string in your kubeconfig file)")
def test_user_cmd_path_with_scope(self):
A = namedtuple('A', ['token', 'expiry'])
token = "dummy"
return_value = A(token, parse_rfc3339(datetime.datetime.now()))
CommandTokenSource.token = mock.Mock(return_value=return_value)
expected = FakeConfig(api_key={
"authorization": BEARER_TOKEN_FORMAT % token})
actual = FakeConfig()
self.expect_exception(lambda: KubeConfigLoader(
config_dict=self.TEST_KUBE_CONFIG,
active_context="contexttestcmdpathscope").load_and_set(actual),
"scopes can only be used when kubectl is using "
"a gcp service account key")
class TestKubernetesClientConfiguration(BaseTestCase):
# Verifies properties of kubernetes.client.Configuration.
@ -1421,6 +1530,37 @@ class TestKubeConfigMerger(BaseTestCase):
TEST_KUBE_CONFIG_PART4 = {
"current-context": "no_user",
}
# Config with user having cmd-path
TEST_KUBE_CONFIG_PART5 = {
"contexts": [
{
"name": "contexttestcmdpath",
"context": {
"cluster": "clustertestcmdpath",
"user": "usertestcmdpath"
}
}
],
"clusters": [
{
"name": "clustertestcmdpath",
"cluster": {}
}
],
"users": [
{
"name": "usertestcmdpath",
"user": {
"auth-provider": {
"name": "gcp",
"config": {
"cmd-path": "cmdtorun"
}
}
}
}
]
}
def _create_multi_config(self):
files = []
@ -1428,7 +1568,8 @@ class TestKubeConfigMerger(BaseTestCase):
self.TEST_KUBE_CONFIG_PART1,
self.TEST_KUBE_CONFIG_PART2,
self.TEST_KUBE_CONFIG_PART3,
self.TEST_KUBE_CONFIG_PART4):
self.TEST_KUBE_CONFIG_PART4,
self.TEST_KUBE_CONFIG_PART5):
files.append(self._create_temp_file(yaml.safe_dump(part)))
return ENV_KUBECONFIG_PATH_SEPARATOR.join(files)
@ -1439,7 +1580,11 @@ class TestKubeConfigMerger(BaseTestCase):
{'context': {'cluster': 'ssl', 'user': 'ssl'}, 'name': 'ssl'},
{'context': {'cluster': 'default', 'user': 'simple_token'},
'name': 'simple_token'},
{'context': {'cluster': 'default', 'user': 'expired_oidc'}, 'name': 'expired_oidc'}]
{'context': {'cluster': 'default', 'user': 'expired_oidc'},
'name': 'expired_oidc'},
{'context': {'cluster': 'clustertestcmdpath',
'user': 'usertestcmdpath'},
'name': 'contexttestcmdpath'}]
contexts, active_context = list_kube_config_contexts(
config_file=kubeconfigs)