Source code for jupyterhub_traefik_proxy.consul

"""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.

import json
import os
from urllib.parse import urlparse
import string
import base64

import asyncio
import consul.aio
import escapism
from tornado.concurrent import run_on_executor
from traitlets import Any, default, Unicode

from . import traefik_utils
from jupyterhub_traefik_proxy import TKvProxy
import time


[docs]class TraefikConsulProxy(TKvProxy): """JupyterHub Proxy implementation using traefik and Consul""" # Consul doesn't accept keys containing // or starting with / so we have to escape them key_safe_chars = string.ascii_letters + string.digits + "!@#$%^&*();<>_-.+?:" kv_name = "consul" consul_client_ca_cert = Unicode( config=True, allow_none=True, default_value=None, help="""Consul client root certificates""", ) @default("kv_url") def _default_kv_url(self): return "http://127.0.0.1:8500" @default("kv_client") def _default_client(self): consul_service = urlparse(self.kv_url) if self.kv_password: client = consul.aio.Consul( host=str(consul_service.hostname), port=consul_service.port, token=self.kv_password, cert=self.consul_client_ca_cert, ) client.http._session._default_headers.update( {"X-Consul-Token": self.kv_password} ) return client return consul.aio.Consul( host=str(consul_service.hostname), port=consul_service.port, cert=self.consul_client_ca_cert, ) @default("kv_traefik_prefix") def _default_kv_traefik_prefix(self): return "traefik/" @default("kv_jupyterhub_prefix") def _default_kv_jupyterhub_prefix(self): return "jupyterhub/" def _define_kv_specific_static_config(self): self.static_config["consul"] = { "endpoint": str(urlparse(self.kv_url).hostname) + ":" + str(urlparse(self.kv_url).port), "prefix": self.kv_traefik_prefix, "watch": True, } def _launch_traefik(self, config_type): os.environ["CONSUL_HTTP_TOKEN"] = self.kv_password super()._launch_traefik(config_type) async def _kv_atomic_add_route_parts( self, jupyterhub_routespec, target, data, route_keys, rule ): escaped_target = escapism.escape(target, safe=self.key_safe_chars) escaped_jupyterhub_routespec = escapism.escape( jupyterhub_routespec, safe=self.key_safe_chars ) try: results = await self.kv_client.txn.put( payload=[ { "KV": { "Verb": "set", "Key": escaped_jupyterhub_routespec, "Value": base64.b64encode(target.encode()).decode(), } }, { "KV": { "Verb": "set", "Key": escaped_target, "Value": base64.b64encode(data.encode()).decode(), } }, { "KV": { "Verb": "set", "Key": route_keys.backend_url_path, "Value": base64.b64encode(target.encode()).decode(), } }, { "KV": { "Verb": "set", "Key": route_keys.backend_weight_path, "Value": base64.b64encode(b"1").decode(), } }, { "KV": { "Verb": "set", "Key": route_keys.frontend_backend_path, "Value": base64.b64encode( route_keys.backend_alias.encode() ).decode(), } }, { "KV": { "Verb": "set", "Key": route_keys.frontend_rule_path, "Value": base64.b64encode(rule.encode()).decode(), } }, ] ) status = 1 response = "" except Exception as e: status = 0 response = str(e) return status, response async def _kv_atomic_delete_route_parts(self, jupyterhub_routespec, route_keys): escaped_jupyterhub_routespec = escapism.escape( jupyterhub_routespec, safe=self.key_safe_chars ) index, v = await self.kv_client.kv.get(escaped_jupyterhub_routespec) if v is None: self.log.warning("Route %s doesn't exist. Nothing to delete", routespec) return target = v["Value"] escaped_target = escapism.escape(target, safe=self.key_safe_chars) try: status, response = await self.kv_client.txn.put( payload=[ {"KV": {"Verb": "delete", "Key": escaped_jupyterhub_routespec}}, {"KV": {"Verb": "delete", "Key": escaped_target}}, {"KV": {"Verb": "delete", "Key": route_keys.backend_url_path}}, {"KV": {"Verb": "delete", "Key": route_keys.backend_weight_path}}, {"KV": {"Verb": "delete", "Key": route_keys.frontend_backend_path}}, {"KV": {"Verb": "delete", "Key": route_keys.frontend_rule_path}}, ] ) status = 1 response = "" except Exception as e: status = 0 response = str(e) return status, response async def _kv_get_target(self, jupyterhub_routespec): escaped_jupyterhub_routespec = escapism.escape( jupyterhub_routespec, safe=self.key_safe_chars ) _, res = await self.kv_client.kv.get(escaped_jupyterhub_routespec) if res is None: return None return res["Value"].decode() async def _kv_get_data(self, target): escaped_target = escapism.escape(target, safe=self.key_safe_chars) _, res = await self.kv_client.kv.get(escaped_target) if res is None: return None return res["Value"].decode() async def _kv_get_route_parts(self, kv_entry): key = escapism.unescape(kv_entry["KV"]["Key"]) value = kv_entry["KV"]["Value"] # Strip the "/jupyterhub" prefix from the routespec routespec = key.replace(self.kv_jupyterhub_prefix, "") target = base64.b64decode(value.encode()).decode() data = await self._kv_get_data(target) return routespec, target, data async def _kv_get_jupyterhub_prefixed_entries(self): routes = await self.kv_client.txn.put( payload=[ { "KV": { "Verb": "get-tree", "Key": escapism.escape( self.kv_jupyterhub_prefix, safe=self.key_safe_chars ), } } ] ) return routes["Results"]
[docs] async def stop(self): await super().stop() await self.kv_client.http._session.close()