From caec854e6a83c7c58987cad259e915ad479a85db Mon Sep 17 00:00:00 2001 From: Dov Reshef Date: Thu, 19 Jul 2018 16:08:00 +0300 Subject: [PATCH] Add support for getting credentials from exec plugins --- config/exec_provider.py | 79 +++++++++++++++++++++++++++++++++++++++++ config/kube_config.py | 21 ++++++++--- 2 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 config/exec_provider.py diff --git a/config/exec_provider.py b/config/exec_provider.py new file mode 100644 index 00000000..eeb8a4e5 --- /dev/null +++ b/config/exec_provider.py @@ -0,0 +1,79 @@ +# Copyright 2016 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json +import os +import subprocess +import sys + +from .config_exception import ConfigException + + +class ExecProvider(object): + def __init__(self, exec_config): + for key in ['command', 'apiVersion']: + if key not in exec_config: + raise ConfigException( + 'exec: malformed request. missing key \'%s\'' % key) + self.api_version = exec_config['apiVersion'] + self.args = [exec_config['command']] + if 'args' in exec_config: + self.args.extend(exec_config['args']) + self.env = os.environ.copy() + if 'env' in exec_config: + additional_vars = {} + for item in exec_config['env']: + name = item['name'] + value = item['value'] + additional_vars[name] = value + self.env.update(additional_vars) + + def run(self, previous_response=None): + kubernetes_exec_info = { + 'apiVersion': self.api_version, + 'kind': 'ExecCredential', + 'spec': { + 'interactive': True + } + } + if previous_response: + kubernetes_exec_info['spec']['response'] = previous_response + self.env['KUBERNETES_EXEC_INFO'] = json.dumps(kubernetes_exec_info) + process = subprocess.Popen( + self.args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=self.env, + universal_newlines=True) + (stdout, stderr) = process.communicate() + exit_code = process.wait() + if exit_code != 0: + msg = 'exec: process returned %d' % exit_code + stderr = stderr.strip() + if stderr: + msg += '. %s' % stderr + raise ConfigException(msg) + try: + data = json.loads(stdout) + except json.decoder.JSONDecodeError as de: + raise ConfigException( + 'exec: failed to decode process output: %s' % de) + for key in ('apiVersion', 'kind', 'status'): + if key not in data: + raise ConfigException( + 'exec: malformed response. missing key \'%s\'' % key) + if data['apiVersion'] != self.api_version: + raise ConfigException( + 'exec: plugin api version %s does not match %s' % + (data['apiVersion'], self.api_version)) + return data['status'] diff --git a/config/kube_config.py b/config/kube_config.py index 4d23977d..888be5a9 100644 --- a/config/kube_config.py +++ b/config/kube_config.py @@ -31,6 +31,7 @@ from .config_exception import ConfigException from .dateutil import UTC, format_rfc3339, parse_rfc3339 +from .exec_provider import ExecProvider EXPIRY_SKEW_PREVENTION_DELAY = datetime.timedelta(minutes=5) KUBE_CONFIG_DEFAULT_LOCATION = os.environ.get('KUBECONFIG', '~/.kube/config') @@ -170,11 +171,10 @@ def _load_authentication(self): section of kube-config and stops if it finds a valid authentication method. The order of authentication methods is: - 1. GCP auth-provider - 2. token_data - 3. token field (point to a token file) - 4. oidc auth-provider - 5. username/password + 1. auth-provider (gcp, azure, oidc) + 2. token field (point to a token file) + 3. exec provided plugin + 4. username/password """ if not self._user: return @@ -182,6 +182,8 @@ def _load_authentication(self): return if self._load_user_token(): return + if self._load_from_exec_plugin(): + return self._load_user_pass_token() def _load_auth_provider_token(self): @@ -319,6 +321,15 @@ def _refresh_oidc(self, provider): provider['config'].value['id-token'] = refresh['id_token'] provider['config'].value['refresh-token'] = refresh['refresh_token'] + def _load_from_exec_plugin(self): + if 'exec' not in self._user: + return + status = ExecProvider(self._user['exec']).run() + if 'token' not in status: + return + self.token = "Bearer %s" % status['token'] + return True + def _load_user_token(self): token = FileOrData( self._user, 'tokenFile', 'token',