#72 suppport client-side SSL certs from kubeconfig file
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user