Source code for jupyterhub_traefik_proxy.etcd

"""Traefik implementation

Custom proxy implementations can subclass :class:`Proxy`
and register in JupyterHub config:

.. sourcecode:: python

    from mymodule import MyProxy
    c.JupyterHub.proxy_class = MyProxy

Route Specification:

- A routespec is a URL prefix ([host]/path/), e.g.
  'host.tld/path/' for host-based routing or '/path/' for default routing.
- Route paths should be normalized to always start and end with '/'
"""

# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

from concurrent.futures import ThreadPoolExecutor
import json
import os
from urllib.parse import urlparse

import etcd3
from tornado.concurrent import run_on_executor
from traitlets import Any, default, Unicode

from jupyterhub.utils import maybe_future
from . import traefik_utils
from jupyterhub_traefik_proxy import TKvProxy


[docs]class TraefikEtcdProxy(TKvProxy): """JupyterHub Proxy implementation using traefik and etcd""" executor = Any() kv_name = "etcdv3" etcd_client_ca_cert = Unicode( config=True, allow_none=True, default_value=None, help="""Etcd client root certificates""", ) etcd_client_cert_crt = Unicode( config=True, allow_none=True, default_value=None, help="""Etcd client certificate chain (etcd_client_cert_key must also be specified)""", ) etcd_client_cert_key = Unicode( config=True, allow_none=True, default_value=None, help="""Etcd client private key (etcd_client_cert_crt must also be specified)""", ) @default("executor") def _default_executor(self): return ThreadPoolExecutor(1) @default("kv_url") def _default_kv_url(self): return "http://127.0.0.1:2379" @default("kv_client") def _default_client(self): etcd_service = urlparse(self.kv_url) if self.kv_password: return etcd3.client( host=str(etcd_service.hostname), port=etcd_service.port, user=self.kv_username, password=self.kv_password, ca_cert=self.etcd_client_ca_cert, cert_cert=self.etcd_client_cert_crt, cert_key=self.etcd_client_cert_key, ) return etcd3.client( host=str(etcd_service.hostname), port=etcd_service.port, ca_cert=self.etcd_client_ca_cert, cert_cert=self.etcd_client_cert_crt, cert_key=self.etcd_client_cert_key, ) @default("kv_traefik_prefix") def _default_kv_traefik_prefix(self): return "/traefik/" @default("kv_jupyterhub_prefix") def _default_kv_jupyterhub_prefix(self): return "/jupyterhub/" @run_on_executor def _etcd_transaction(self, success_actions): status, response = self.kv_client.transaction( compare=[], success=success_actions, failure=[] ) return status, response @run_on_executor def _etcd_get(self, key): value, _ = self.kv_client.get(key) return value @run_on_executor def _etcd_get_prefix(self, prefix): routes = self.kv_client.get_prefix(prefix) return routes def _define_kv_specific_static_config(self): self.static_config["etcd"] = { "username": self.kv_username, "password": self.kv_password, "endpoint": str(urlparse(self.kv_url).hostname) + ":" + str(urlparse(self.kv_url).port), "prefix": self.kv_traefik_prefix, "useapiv3": True, "watch": True, } async def _kv_atomic_add_route_parts( self, jupyterhub_routespec, target, data, route_keys, rule ): success = [ self.kv_client.transactions.put(jupyterhub_routespec, target), self.kv_client.transactions.put(target, data), self.kv_client.transactions.put(route_keys.backend_url_path, target), self.kv_client.transactions.put(route_keys.backend_weight_path, "1"), self.kv_client.transactions.put( route_keys.frontend_backend_path, route_keys.backend_alias ), self.kv_client.transactions.put(route_keys.frontend_rule_path, rule), ] status, response = await maybe_future(self._etcd_transaction(success)) return status, response async def _kv_atomic_delete_route_parts(self, jupyterhub_routespec, route_keys): value = await maybe_future(self._etcd_get(jupyterhub_routespec)) if value is None: self.log.warning("Route %s doesn't exist. Nothing to delete", routespec) return target = value.decode() success = [ self.kv_client.transactions.delete(jupyterhub_routespec), self.kv_client.transactions.delete(target), self.kv_client.transactions.delete(route_keys.backend_url_path), self.kv_client.transactions.delete(route_keys.backend_weight_path), self.kv_client.transactions.delete(route_keys.frontend_backend_path), self.kv_client.transactions.delete(route_keys.frontend_rule_path), ] status, response = await maybe_future(self._etcd_transaction(success)) return status, response async def _kv_get_target(self, jupyterhub_routespec): value = await maybe_future(self._etcd_get(jupyterhub_routespec)) if value == None: return None return value.decode() async def _kv_get_data(self, target): value = await maybe_future(self._etcd_get(target)) if value is None: return None return value async def _kv_get_route_parts(self, kv_entry): key = kv_entry[1].key.decode() value = kv_entry[0] # Strip the "/jupyterhub" prefix from the routespec routespec = key.replace(self.kv_jupyterhub_prefix, "") target = value.decode() data = await self._kv_get_data(target) return routespec, target, data async def _kv_get_jupyterhub_prefixed_entries(self): routes = await maybe_future(self._etcd_get_prefix(self.kv_jupyterhub_prefix)) return routes