From e54a0fb5f54a1d12c743f5eb99791a5ff452d83f Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 15 Jan 2017 18:15:17 +0100 Subject: [PATCH] #72 suppport client-side SSL certs from kubeconfig file --- kube_ops_view/cluster_discovery.py | 35 +++++++++++++++++++++++------- kube_ops_view/kubernetes.py | 2 ++ kube_ops_view/main.py | 20 +++++++++++++---- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/kube_ops_view/cluster_discovery.py b/kube_ops_view/cluster_discovery.py index 0511430..cf17c3a 100644 --- a/kube_ops_view/cluster_discovery.py +++ b/kube_ops_view/cluster_discovery.py @@ -10,6 +10,7 @@ import requests import tokens from requests.auth import AuthBase +# default URL points to kubectl proxy DEFAULT_CLUSTERS = 'http://localhost:8001/' CLUSTER_ID_INVALID_CHARS = re.compile('[^a-z0-9:-]') @@ -26,16 +27,21 @@ def generate_cluster_id(url: str): return CLUSTER_ID_INVALID_CHARS.sub('-', url.lower()).strip('-') -class StaticTokenAuth(AuthBase): - def __init__(self, token): - self.token = token +class StaticAuthorizationHeaderAuth(AuthBase): + '''Static authentication with given "Authorization" header''' + + def __init__(self, authorization): + self.authorization = authorization def __call__(self, request): - request.headers['Authorization'] = 'Bearer {}'.format(self.token) + request.headers['Authorization'] = self.authorization return request class OAuthTokenAuth(AuthBase): + '''Dynamic authentication using the "tokens" library to load OAuth tokens from file + (potentially mounted from a Kubernetes secret)''' + def __init__(self, token_name): self.token_name = token_name tokens.manage(token_name) @@ -47,11 +53,13 @@ class OAuthTokenAuth(AuthBase): class Cluster: - def __init__(self, id, api_server_url, ssl_ca_cert=None, auth=None): + def __init__(self, id, api_server_url, ssl_ca_cert=None, auth=None, cert_file=None, key_file=None): self.id = id self.api_server_url = api_server_url self.ssl_ca_cert = ssl_ca_cert self.auth = auth + self.cert_file = cert_file + self.key_file = key_file class StaticClusterDiscoverer: @@ -72,7 +80,7 @@ class StaticClusterDiscoverer: generate_cluster_id(config.host), config.host, ssl_ca_cert=config.ssl_ca_cert, - auth=StaticTokenAuth(config.api_key['authorization'].split(' ', 1)[-1])) + auth=StaticAuthorizationHeaderAuth(config.api_key['authorization'])) self._clusters.append(cluster) else: for api_server_url in api_server_urls: @@ -121,21 +129,32 @@ class ClusterRegistryDiscoverer: class KubeconfigDiscoverer: - def __init__(self, kubeconfig_path: Path): + def __init__(self, kubeconfig_path: Path, contexts: set): self._path = kubeconfig_path + self._contexts = contexts def get_clusters(self): # Kubernetes Python client expects "vintage" string path config_file = str(self._path) contexts, current_context = kubernetes.config.list_kube_config_contexts(config_file) for context in contexts: + if self._contexts and context['name'] not in self._contexts: + # filter out + continue config = kubernetes.client.ConfigurationObject() kubernetes.config.load_kube_config(config_file, context=context['name'], client_configuration=config) + authorization = config.api_key.get('authorization') + if authorization: + auth = StaticAuthorizationHeaderAuth(authorization) + else: + auth = None cluster = Cluster( context['name'], config.host, ssl_ca_cert=config.ssl_ca_cert, - auth=StaticTokenAuth(config.api_key['authorization'].split(' ', 1)[-1])) + cert_file=config.cert_file, + key_file=config.key_file, + auth=auth) yield cluster diff --git a/kube_ops_view/kubernetes.py b/kube_ops_view/kubernetes.py index e07b983..90692ba 100644 --- a/kube_ops_view/kubernetes.py +++ b/kube_ops_view/kubernetes.py @@ -48,6 +48,8 @@ def request(cluster, path, **kwargs): if 'timeout' not in kwargs: # sane default timeout kwargs['timeout'] = 5 + if cluster.cert_file and cluster.key_file: + kwargs['cert'] = (cluster.cert_file, cluster.key_file) return session.get(urljoin(cluster.api_server_url, path), auth=cluster.auth, verify=cluster.ssl_ca_cert, **kwargs) diff --git a/kube_ops_view/main.py b/kube_ops_view/main.py index d985014..4ce704f 100644 --- a/kube_ops_view/main.py +++ b/kube_ops_view/main.py @@ -178,6 +178,17 @@ def print_version(ctx, param, value): ctx.exit() +class CommaSeparatedValues(click.ParamType): + name = 'comma_separated_values' + + def convert(self, value, param, ctx): + if isinstance(value, str): + values = filter(None, value.split(',')) + else: + values = value + return values + + @click.command(context_settings={'help_option_names': ['-h', '--help']}) @click.option('-V', '--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True, help='Print the current version number and exit.') @@ -186,12 +197,13 @@ def print_version(ctx, param, value): @click.option('-m', '--mock', is_flag=True, help='Mock Kubernetes clusters', envvar='MOCK') @click.option('--secret-key', help='Secret key for session cookies', envvar='SECRET_KEY', default='development') @click.option('--redis-url', help='Redis URL to use for pub/sub and job locking', envvar='REDIS_URL') -@click.option('--clusters', help='Comma separated list of Kubernetes API server URLs (default: {})'.format(DEFAULT_CLUSTERS), +@click.option('--clusters', type=CommaSeparatedValues(), help='Comma separated list of Kubernetes API server URLs (default: {})'.format(DEFAULT_CLUSTERS), envvar='CLUSTERS') @click.option('--cluster-registry-url', help='URL to cluster registry', envvar='CLUSTER_REGISTRY_URL') @click.option('--kubeconfig-path', type=click.Path(exists=True), help='Path to kubeconfig file', envvar='KUBECONFIG_PATH') +@click.option('--kubeconfig-contexts', type=CommaSeparatedValues(), help='Path to kubeconfig file', envvar='KUBECONFIG_PATH') @click.option('--query-interval', type=float, help='Interval in seconds for querying clusters (default: 5)', envvar='QUERY_INTERVAL', default=5) -def main(port, debug, mock, secret_key, redis_url, clusters, cluster_registry_url, kubeconfig_path, query_interval): +def main(port, debug, mock, secret_key, redis_url, clusters: list, cluster_registry_url, kubeconfig_path, kubeconfig_contexts: list, query_interval): logging.basicConfig(level=logging.DEBUG if debug else logging.INFO) store = RedisStore(redis_url) if redis_url else MemoryStore() @@ -208,9 +220,9 @@ def main(port, debug, mock, secret_key, redis_url, clusters, cluster_registry_ur if cluster_registry_url: discoverer = ClusterRegistryDiscoverer(cluster_registry_url) elif kubeconfig_path: - discoverer = KubeconfigDiscoverer(Path(kubeconfig_path)) + discoverer = KubeconfigDiscoverer(Path(kubeconfig_path), set(kubeconfig_contexts or [])) else: - api_server_urls = clusters.split(',') if clusters else [] + api_server_urls = clusters or [] discoverer = StaticClusterDiscoverer(api_server_urls) gevent.spawn(update_clusters, cluster_discoverer=discoverer, query_cluster=cluster_query, store=store, query_interval=query_interval, debug=debug)