From ba3e22a7ac8c1874f12a6a5f6577558ad21bbb4a Mon Sep 17 00:00:00 2001 From: mbohlool Date: Tue, 22 Nov 2016 02:41:37 -0800 Subject: [PATCH] Add incluster config support --- examples/example3.py | 2 +- kubernetes/config/__init__.py | 2 + kubernetes/config/incluster_config.py | 86 ++++++++++++++++ kubernetes/config/incluster_config_test.py | 113 +++++++++++++++++++++ 4 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 kubernetes/config/incluster_config.py create mode 100644 kubernetes/config/incluster_config_test.py diff --git a/examples/example3.py b/examples/example3.py index 25aab6962b..deb1abcfff 100644 --- a/examples/example3.py +++ b/examples/example3.py @@ -34,7 +34,7 @@ def main(): name += "*" name += v.version versions.append(name) - print("%-20s %s" % (api.name, ",".join(versions))) + print("%-40s %s" % (api.name, ",".join(versions))) if __name__ == '__main__': diff --git a/kubernetes/config/__init__.py b/kubernetes/config/__init__.py index bceafecbf0..9254c408be 100644 --- a/kubernetes/config/__init__.py +++ b/kubernetes/config/__init__.py @@ -13,3 +13,5 @@ # limitations under the License. from .kube_config import load_kube_config +from .incluster_config import load_incluster_config +from .incluster_config import ConfigException diff --git a/kubernetes/config/incluster_config.py b/kubernetes/config/incluster_config.py new file mode 100644 index 0000000000..d0b21afd3d --- /dev/null +++ b/kubernetes/config/incluster_config.py @@ -0,0 +1,86 @@ +# 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 os + +from kubernetes.client import configuration + +_SERVICE_HOST_ENV_NAME = "KUBERNETES_SERVICE_HOST" +_SERVICE_PORT_ENV_NAME = "KUBERNETES_SERVICE_PORT" +_SERVICE_TOKEN_FILENAME = "/var/run/secrets/kubernetes.io/serviceaccount/token" +_SERVICE_CERT_FILENAME = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + + +def _join_host_port(host, port): + """Adapted golang's net.JoinHostPort""" + template = "%s:%s" + host_requires_bracketing = ':' in host or '%' in host + if host_requires_bracketing: + template = "[%s]:%s" + return template % (host, port) + + +class ConfigException(Exception): + pass + + +class InClusterConfigLoader(object): + def __init__(self, host_env_name, port_env_name, token_filename, + cert_filename, environ=os.environ): + self._host_env_name = host_env_name + self._port_env_name = port_env_name + self._token_filename = token_filename + self._cert_filename = cert_filename + self._environ = environ + + def load_and_set(self): + self._load_config() + self._set_config() + + def _load_config(self): + if (self._host_env_name not in self._environ or + self._port_env_name not in self._environ): + raise ConfigException("Service host/port is not set.") + + self.host = ( + "https://" + _join_host_port(self._environ[self._host_env_name], + self._environ[self._port_env_name])) + + if not os.path.isfile(self._token_filename): + raise ConfigException("Service token file does not exists.") + + with open(self._token_filename) as f: + self.token = f.read() + + if not os.path.isfile(self._cert_filename): + raise ConfigException( + "Service certification file does not exists.") + + self.ssl_ca_cert = self._cert_filename + + def _set_config(self): + configuration.host = self.host + configuration.ssl_ca_cert = self.ssl_ca_cert + configuration.api_key['authorization'] = "bearer " + self.token + + +def load_incluster_config(): + """Use the service account kubernetes gives to pods to connect to kubernetes + cluster. It's intended for clients that expect to be running inside a pod + running on kubernetes. It will raise an exception if called from a process + not running in a kubernetes environment.""" + InClusterConfigLoader(host_env_name=_SERVICE_HOST_ENV_NAME, + port_env_name=_SERVICE_PORT_ENV_NAME, + token_filename=_SERVICE_TOKEN_FILENAME, + cert_filename=_SERVICE_CERT_FILENAME).load_and_set() diff --git a/kubernetes/config/incluster_config_test.py b/kubernetes/config/incluster_config_test.py new file mode 100644 index 0000000000..e69f7d4d83 --- /dev/null +++ b/kubernetes/config/incluster_config_test.py @@ -0,0 +1,113 @@ +# 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 os +import tempfile +import unittest + +from kubernetes.client import configuration + +from .incluster_config import (ConfigException, InClusterConfigLoader, + _SERVICE_HOST_ENV_NAME, _SERVICE_PORT_ENV_NAME) + +_TEST_TOKEN = "temp_token" +_TEST_HOST = "127.0.0.1" +_TEST_IPV6_HOST = "::1" +_TEST_PORT = "80" +_TEST_ENVIRON = {_SERVICE_HOST_ENV_NAME: _TEST_HOST, + _SERVICE_PORT_ENV_NAME: _TEST_PORT} +_TEST_IPV6_ENVIRON = {_SERVICE_HOST_ENV_NAME: _TEST_IPV6_HOST, + _SERVICE_PORT_ENV_NAME: _TEST_PORT} + + +class InClusterConfigTest(unittest.TestCase): + + def setUp(self): + self._temp_files = [] + + def tearDown(self): + for f in self._temp_files: + os.remove(f) + + def _create_file_with_temp_content(self, content=""): + handler, name = tempfile.mkstemp() + self._temp_files.append(name) + os.write(handler, str.encode(content)) + os.close(handler) + return name + + def get_test_loader( + self, + host_env_name=_SERVICE_HOST_ENV_NAME, + port_env_name=_SERVICE_PORT_ENV_NAME, + token_filename=None, + cert_filename=None, + environ=_TEST_ENVIRON): + if not token_filename: + token_filename = self._create_file_with_temp_content(_TEST_TOKEN) + if not cert_filename: + cert_filename = self._create_file_with_temp_content() + return InClusterConfigLoader( + host_env_name=host_env_name, + port_env_name=port_env_name, + token_filename=token_filename, + cert_filename=cert_filename, + environ=environ) + + def test_load_config(self): + cert_filename = self._create_file_with_temp_content() + loader = self.get_test_loader(cert_filename=cert_filename) + loader._load_config() + self.assertEqual("https://%s:%s" % (_TEST_HOST, _TEST_PORT), + loader.host) + self.assertEqual(cert_filename, loader.ssl_ca_cert) + self.assertEqual(_TEST_TOKEN, loader.token) + + def test_load_config_with_bracketed_hostname(self): + cert_filename = self._create_file_with_temp_content() + loader = self.get_test_loader(cert_filename=cert_filename, + environ=_TEST_IPV6_ENVIRON) + loader._load_config() + self.assertEqual("https://[%s]:%s" % (_TEST_IPV6_HOST, _TEST_PORT), + loader.host) + self.assertEqual(cert_filename, loader.ssl_ca_cert) + self.assertEqual(_TEST_TOKEN, loader.token) + + def _should_fail_load(self, config_loader, reason): + try: + config_loader.load_and_set() + self.fail("Should fail because %s" % reason) + except ConfigException: + # expected + pass + + def test_no_port(self): + loader = self.get_test_loader(port_env_name="not_exists_port") + self._should_fail_load(loader, "no port specified") + + def test_no_host(self): + loader = self.get_test_loader(host_env_name="not_exists_host") + self._should_fail_load(loader, "no host specified") + + def test_no_cert_file(self): + loader = self.get_test_loader(cert_filename="not_exists_file_1123") + self._should_fail_load(loader, "cert file does not exists") + + def test_no_token_file(self): + loader = self.get_test_loader(token_filename="not_exists_file_1123") + self._should_fail_load(loader, "token file does not exists") + + +if __name__ == '__main__': + unittest.main()