Support connecting to multiple clusters
This commit is contained in:
parent
729d543f6d
commit
8c26b4b75b
@ -2,6 +2,7 @@
|
||||
|
||||
- Add context switch to kube config loader #46
|
||||
- Add default kube config location #64
|
||||
- Add suport for accessing multiple clusters #7
|
||||
- Bugfix: Python client does not resolve relative paths in kubeconfig #68
|
||||
- Bugfix: `read_namespaced_pod_log` get None response #57
|
||||
- Improved test coverage #54
|
||||
|
||||
@ -14,6 +14,9 @@
|
||||
|
||||
from kubernetes import client, config
|
||||
from kubernetes.client import configuration
|
||||
# install pick using "pip install pick". It is not included
|
||||
# as a dependency because it only used in examples
|
||||
from pick import pick
|
||||
|
||||
|
||||
def main():
|
||||
@ -22,25 +25,12 @@ def main():
|
||||
print("Cannot find any context in kube-config file.")
|
||||
return
|
||||
contexts = [context['name'] for context in contexts]
|
||||
active_context = active_context['name']
|
||||
for i, context in enumerate(contexts):
|
||||
format_str = "%d. %s"
|
||||
if context == active_context:
|
||||
format_str = "* " + format_str
|
||||
print(format_str % (i, context))
|
||||
context = input("Enter context number: ")
|
||||
context = int(context)
|
||||
if context not in range(len(contexts)):
|
||||
print(
|
||||
"Number out of range. Using default context %s." %
|
||||
active_context)
|
||||
context_name = active_context
|
||||
else:
|
||||
context_name = contexts[context]
|
||||
|
||||
active_index = contexts.index(active_context['name'])
|
||||
option, _ = pick(contexts, title="Pick the context to load",
|
||||
default_index=active_index)
|
||||
# Configs can be set in Configuration class directly or using helper
|
||||
# utility
|
||||
config.load_kube_config(context=context_name)
|
||||
config.load_kube_config(context=option)
|
||||
|
||||
print("Active host is %s" % configuration.host)
|
||||
|
||||
|
||||
51
examples/multiple_clusters.py
Normal file
51
examples/multiple_clusters.py
Normal file
@ -0,0 +1,51 @@
|
||||
# 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.
|
||||
|
||||
from kubernetes import client, config
|
||||
# install pick using "pip install pick". It is not included
|
||||
# as a dependency because it only used in examples
|
||||
from pick import pick
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
contexts, active_context = config.list_kube_config_contexts()
|
||||
if not contexts:
|
||||
print("Cannot find any context in kube-config file.")
|
||||
return
|
||||
contexts = [context['name'] for context in contexts]
|
||||
active_index = contexts.index(active_context['name'])
|
||||
cluster1, first_index = pick(contexts, title="Pick the first context",
|
||||
default_index=active_index)
|
||||
cluster2, _ = pick(contexts, title="Pick the second context",
|
||||
default_index=first_index)
|
||||
|
||||
client1 = client.CoreV1Api(
|
||||
api_client=config.new_client_from_config(context=cluster1))
|
||||
client2 = client.CoreV1Api(
|
||||
api_client=config.new_client_from_config(context=cluster2))
|
||||
|
||||
print("\nList of pods on %s:" % cluster1)
|
||||
for i in client1.list_pod_for_all_namespaces().items:
|
||||
print("%s\t%s\t%s" %
|
||||
(i.status.pod_ip, i.metadata.namespace, i.metadata.name))
|
||||
|
||||
print("\n\nList of pods on %s:" % cluster2)
|
||||
for i in client2.list_pod_for_all_namespaces().items:
|
||||
print("%s\t%s\t%s" %
|
||||
(i.status.pod_ip, i.metadata.namespace, i.metadata.name))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -5,3 +5,7 @@ test-requirements.txt
|
||||
setup.py
|
||||
.travis.yml
|
||||
tox.ini
|
||||
client/__init__.py
|
||||
client/api_client.py
|
||||
client/configuration.py
|
||||
client/rest.py
|
||||
|
||||
@ -288,6 +288,4 @@ from .apis.version_api import VersionApi
|
||||
# import ApiClient
|
||||
from .api_client import ApiClient
|
||||
|
||||
from .configuration import Configuration
|
||||
|
||||
configuration = Configuration()
|
||||
from .configuration import Configuration, ConfigurationObject, configuration
|
||||
|
||||
@ -38,7 +38,7 @@ from datetime import date
|
||||
from six import PY3, integer_types, iteritems, text_type
|
||||
from six.moves.urllib.parse import quote
|
||||
|
||||
from .configuration import Configuration
|
||||
from .configuration import configuration
|
||||
|
||||
|
||||
class ApiClient(object):
|
||||
@ -58,17 +58,19 @@ class ApiClient(object):
|
||||
:param header_name: a header to pass when making calls to the API.
|
||||
:param header_value: a header value to pass when making calls to the API.
|
||||
"""
|
||||
def __init__(self, host=None, header_name=None, header_value=None, cookie=None):
|
||||
def __init__(self, host=None, header_name=None, header_value=None,
|
||||
cookie=None, config=configuration):
|
||||
|
||||
"""
|
||||
Constructor of the class.
|
||||
"""
|
||||
self.rest_client = RESTClientObject()
|
||||
self.config = config
|
||||
self.rest_client = RESTClientObject(config=self.config)
|
||||
self.default_headers = {}
|
||||
if header_name is not None:
|
||||
self.default_headers[header_name] = header_value
|
||||
if host is None:
|
||||
self.host = Configuration().host
|
||||
self.host = self.config.host
|
||||
else:
|
||||
self.host = host
|
||||
self.cookie = cookie
|
||||
@ -499,13 +501,12 @@ class ApiClient(object):
|
||||
:param querys: Query parameters tuple list to be updated.
|
||||
:param auth_settings: Authentication setting identifiers list.
|
||||
"""
|
||||
config = Configuration()
|
||||
|
||||
if not auth_settings:
|
||||
return
|
||||
|
||||
for auth in auth_settings:
|
||||
auth_setting = config.auth_settings().get(auth)
|
||||
auth_setting = self.config.auth_settings().get(auth)
|
||||
if auth_setting:
|
||||
if not auth_setting['value']:
|
||||
continue
|
||||
@ -526,9 +527,7 @@ class ApiClient(object):
|
||||
:param response: RESTResponse.
|
||||
:return: file path.
|
||||
"""
|
||||
config = Configuration()
|
||||
|
||||
fd, path = tempfile.mkstemp(dir=config.temp_folder_path)
|
||||
fd, path = tempfile.mkstemp(dir=self.config.temp_folder_path)
|
||||
os.close(fd)
|
||||
os.remove(path)
|
||||
|
||||
|
||||
@ -33,18 +33,7 @@ from six import iteritems
|
||||
from six.moves import http_client as httplib
|
||||
|
||||
|
||||
def singleton(cls, *args, **kw):
|
||||
instances = {}
|
||||
|
||||
def _singleton():
|
||||
if cls not in instances:
|
||||
instances[cls] = cls(*args, **kw)
|
||||
return instances[cls]
|
||||
return _singleton
|
||||
|
||||
|
||||
@singleton
|
||||
class Configuration(object):
|
||||
class ConfigurationObject(object):
|
||||
"""
|
||||
NOTE: This class is auto generated by the swagger code generator program.
|
||||
Ref: https://github.com/swagger-api/swagger-codegen
|
||||
@ -235,3 +224,11 @@ class Configuration(object):
|
||||
"Version of the API: v1.5.0-snapshot\n"\
|
||||
"SDK Package Version: 1.0.0-snapshot".\
|
||||
format(env=sys.platform, pyversion=sys.version)
|
||||
|
||||
|
||||
configuration = ConfigurationObject()
|
||||
|
||||
|
||||
def Configuration():
|
||||
"""Simulate a singelton Configuration object for backward compatibility."""
|
||||
return configuration
|
||||
|
||||
@ -35,7 +35,7 @@ import re
|
||||
from six import PY3
|
||||
from six.moves.urllib.parse import urlencode
|
||||
|
||||
from .configuration import Configuration
|
||||
from .configuration import configuration
|
||||
|
||||
try:
|
||||
import urllib3
|
||||
@ -69,7 +69,7 @@ class RESTResponse(io.IOBase):
|
||||
|
||||
class RESTClientObject(object):
|
||||
|
||||
def __init__(self, pools_size=4):
|
||||
def __init__(self, pools_size=4, config=configuration):
|
||||
# urllib3.PoolManager will pass all kw parameters to connectionpool
|
||||
# https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/poolmanager.py#L75
|
||||
# https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/connectionpool.py#L680
|
||||
@ -77,23 +77,23 @@ class RESTClientObject(object):
|
||||
# http://stackoverflow.com/a/23957365/2985775
|
||||
|
||||
# cert_reqs
|
||||
if Configuration().verify_ssl:
|
||||
if config.verify_ssl:
|
||||
cert_reqs = ssl.CERT_REQUIRED
|
||||
else:
|
||||
cert_reqs = ssl.CERT_NONE
|
||||
|
||||
# ca_certs
|
||||
if Configuration().ssl_ca_cert:
|
||||
ca_certs = Configuration().ssl_ca_cert
|
||||
if config.ssl_ca_cert:
|
||||
ca_certs = config.ssl_ca_cert
|
||||
else:
|
||||
# if not set certificate file, use Mozilla's root certificates.
|
||||
ca_certs = certifi.where()
|
||||
|
||||
# cert_file
|
||||
cert_file = Configuration().cert_file
|
||||
cert_file = config.cert_file
|
||||
|
||||
# key file
|
||||
key_file = Configuration().key_file
|
||||
key_file = config.key_file
|
||||
|
||||
# https pool manager
|
||||
self.pool_manager = urllib3.PoolManager(
|
||||
|
||||
@ -14,5 +14,5 @@
|
||||
|
||||
from .config_exception import ConfigException
|
||||
from .incluster_config import load_incluster_config
|
||||
from .kube_config import list_kube_config_contexts
|
||||
from .kube_config import load_kube_config
|
||||
from .kube_config import (list_kube_config_contexts, load_kube_config,
|
||||
new_client_from_config)
|
||||
|
||||
@ -19,7 +19,7 @@ import tempfile
|
||||
|
||||
import urllib3
|
||||
import yaml
|
||||
from kubernetes.client import configuration
|
||||
from kubernetes.client import ApiClient, ConfigurationObject, configuration
|
||||
from oauth2client.client import GoogleCredentials
|
||||
|
||||
from .config_exception import ConfigException
|
||||
@ -97,7 +97,8 @@ class FileOrData(object):
|
||||
class KubeConfigLoader(object):
|
||||
|
||||
def __init__(self, config_dict, active_context=None,
|
||||
get_google_credentials=None, client_configuration=None,
|
||||
get_google_credentials=None,
|
||||
client_configuration=configuration,
|
||||
config_base_path=""):
|
||||
self._config = ConfigNode('kube-config', config_dict)
|
||||
self._current_context = None
|
||||
@ -111,10 +112,7 @@ class KubeConfigLoader(object):
|
||||
self._get_google_credentials = lambda: (
|
||||
GoogleCredentials.get_application_default()
|
||||
.get_access_token().access_token)
|
||||
if client_configuration:
|
||||
self._client_configuration = client_configuration
|
||||
else:
|
||||
self._client_configuration = configuration
|
||||
self._client_configuration = client_configuration
|
||||
|
||||
def set_active_context(self, context_name=None):
|
||||
if context_name is None:
|
||||
@ -279,17 +277,31 @@ def list_kube_config_contexts(config_file=None):
|
||||
return loader.list_contexts(), loader.current_context
|
||||
|
||||
|
||||
def load_kube_config(config_file=None, context=None):
|
||||
def load_kube_config(config_file=None, context=None,
|
||||
client_configuration=configuration):
|
||||
"""Loads authentication and cluster information from kube-config file
|
||||
and stores them in kubernetes.client.configuration.
|
||||
|
||||
:param config_file: Name of the kube-config file.
|
||||
:param context: set the active context. If is set to None, current_context
|
||||
from config file will be used.
|
||||
from config file will be used.
|
||||
:param client_configuration: The kubernetes.client.ConfigurationObject to
|
||||
set configs to.
|
||||
"""
|
||||
|
||||
if config_file is None:
|
||||
config_file = os.path.expanduser(KUBE_CONFIG_DEFAULT_LOCATION)
|
||||
|
||||
_get_kube_config_loader_for_yaml_file(
|
||||
config_file, active_context=context).load_and_set()
|
||||
config_file, active_context=context,
|
||||
client_configuration=client_configuration).load_and_set()
|
||||
|
||||
|
||||
def new_client_from_config(config_file=None, context=None):
|
||||
"""Loads configuration the same as load_kube_config but returns an ApiClient
|
||||
to be used with any API object. This will allow the caller to concurrently
|
||||
talk with multiple clusters."""
|
||||
client_config = ConfigurationObject()
|
||||
load_kube_config(config_file=config_file, context=context,
|
||||
client_configuration=client_config)
|
||||
return ApiClient(config=client_config)
|
||||
|
||||
@ -17,10 +17,14 @@ import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
import yaml
|
||||
from six import PY3
|
||||
|
||||
from .config_exception import ConfigException
|
||||
from .kube_config import (ConfigNode, FileOrData, KubeConfigLoader,
|
||||
_cleanup_temp_files, _create_temp_file_with_content)
|
||||
_cleanup_temp_files, _create_temp_file_with_content,
|
||||
load_kube_config, list_kube_config_contexts,
|
||||
new_client_from_config)
|
||||
|
||||
NON_EXISTING_FILE = "zz_non_existing_file_472398324"
|
||||
|
||||
@ -534,6 +538,35 @@ class TestKubeConfigLoader(BaseTestCase):
|
||||
finally:
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
def test_load_kube_config(self):
|
||||
expected = FakeConfig(host=TEST_HOST, token=TEST_DATA_BASE64)
|
||||
config_file = self._create_temp_file(yaml.dump(self.TEST_KUBE_CONFIG))
|
||||
actual = FakeConfig()
|
||||
load_kube_config(config_file=config_file,context="simple_token",
|
||||
client_configuration=actual)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_list_kube_config_contexts(self):
|
||||
config_file = self._create_temp_file(yaml.dump(self.TEST_KUBE_CONFIG))
|
||||
contexts, active_context = list_kube_config_contexts(
|
||||
config_file=config_file)
|
||||
self.assertDictEqual(self.TEST_KUBE_CONFIG['contexts'][0],
|
||||
active_context)
|
||||
if PY3:
|
||||
self.assertCountEqual(self.TEST_KUBE_CONFIG['contexts'],
|
||||
contexts)
|
||||
else:
|
||||
self.assertItemsEqual(self.TEST_KUBE_CONFIG['contexts'],
|
||||
contexts)
|
||||
|
||||
def test_new_client_from_config(self):
|
||||
config_file = self._create_temp_file(yaml.dump(self.TEST_KUBE_CONFIG))
|
||||
client = new_client_from_config(
|
||||
config_file=config_file, context="simple_token")
|
||||
self.assertEqual(TEST_HOST, client.config.host)
|
||||
self.assertEqual(TEST_DATA_BASE64,
|
||||
client.config.api_key['authorization'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@ -43,7 +43,8 @@ echo "--- Downloading and processing OpenAPI spec"
|
||||
python "${SCRIPT_ROOT}/preprocess_spec.py"
|
||||
|
||||
echo "--- Cleaning up previously generated folders"
|
||||
rm -rf "${CLIENT_ROOT}/${PACKAGE_NAME}"
|
||||
rm -rf "${CLIENT_ROOT}/${PACKAGE_NAME}/apis"
|
||||
rm -rf "${CLIENT_ROOT}/${PACKAGE_NAME}/models"
|
||||
rm -rf "${CLIENT_ROOT}/docs"
|
||||
rm -rf "${CLIENT_ROOT}/test"
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user