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

Commit

Permalink
Add option to refresh gcp token when config is cmd-path
Browse files Browse the repository at this point in the history
  • Loading branch information
jfrabaute committed Jan 8, 2020
1 parent a2d1024 commit d3bc0a9
Show file tree
Hide file tree
Showing 2 changed files with 228 additions and 10 deletions.
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()
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-"
"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)
expected.get_api_key_with_prefix = 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

0 comments on commit d3bc0a9

Please sign in to comment.