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

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

Merged
merged 1 commit into from
Jan 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions config/kube_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
import logging
import os
import platform
import subprocess
import tempfile
import time
from collections import namedtuple

import google.auth
import google.auth.transport.requests
Expand Down Expand Up @@ -133,6 +135,46 @@ def as_data(self):
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,
Expand All @@ -156,7 +198,38 @@ def __init__(self, config_dict, active_context=None,
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()
yliaog marked this conversation as resolved.
Show resolved Hide resolved
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'
Expand Down
165 changes: 155 additions & 10 deletions config/kube_config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import shutil
import tempfile
import unittest
from collections import namedtuple

import mock
import yaml
Expand All @@ -27,9 +28,11 @@
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)

Expand Down Expand Up @@ -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": [
{
Expand Down Expand Up @@ -588,6 +612,10 @@ class TestKubeConfigLoader(BaseTestCase):
"insecure-skip-tls-verify": True,
}
},
{
"name": "clustertestcmdpath",
"cluster": {}
}
],
"users": [
{
Expand Down Expand Up @@ -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"
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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-"
yliaog marked this conversation as resolved.
Show resolved Hide resolved
"000000000000",
"environment": "AzurePublicCloud",
"expires-in": "0",
"expires-on": "-1",
Expand Down Expand Up @@ -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"
}
}
}
}
]
}

Expand Down Expand Up @@ -1279,6 +1346,48 @@ def test_user_exec_auth(self, mock):
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.
Expand Down Expand Up @@ -1421,14 +1530,46 @@ 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 = []
for part in (
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)

Expand All @@ -1439,7 +1580,11 @@ def test_list_kube_config_contexts(self):
{'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)
Expand Down