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 base64
import string
from urllib.parse import urlparse

from traitlets import Any, Dict, Unicode, default

from .kv_proxy import TKvProxy
from .traefik_utils import deep_merge


[docs]class TraefikConsulProxy(TKvProxy): """JupyterHub Proxy implementation using traefik and Consul""" provider_name = "consul" # Consul doesn't accept keys containing // or starting with / so we have to escape them key_safe_chars = string.ascii_letters + string.digits + "!@#$%^&*();<>-.+?:" consul_client_kwargs = Dict( config=True, help="Extra consul client constructor arguments", ) consul_url = Unicode( "http://127.0.0.1:8500", config=True, help="URL for the consul endpoint.", ) consul_username = Unicode( "", config=True, help="Usrname for accessing consul.", ) consul_password = Unicode( "", config=True, help="Password or token for accessing consul.", ) kv_url = Unicode("DEPRECATED", config=True).tag( deprecated_in="1.0", deprecated_for="consul_url", ) kv_username = Unicode("DEPRECATED", config=True).tag( deprecated_in="1.0", deprecated_for="consul_username", ) kv_password = Unicode("DEPRECATED", config=True).tag( deprecated_in="1.0", deprecated_for="consul_password", ) consul = Any() @default("consul") def _default_client(self): try: import consul.aio except ImportError: raise ImportError( "Please install python-consul2 package to use traefik-proxy with consul" ) consul_service = urlparse(self.consul_url) kwargs = { "host": consul_service.hostname, "port": consul_service.port, } if self.consul_password: kwargs.update({"token": self.consul_password}) if self.consul_client_kwargs: kwargs.update(self.consul_client_kwargs) return consul.aio.Consul(**kwargs) def __init__(self, **kwargs): super().__init__(**kwargs) self.log.warning( "Using traefik with consul is deprecated in jupyterhub-traefik-proxy 1.0 due to lack of support for the python-consul2 API client. Use etcd instead." ) def _setup_traefik_static_config(self): provider_config = { "consul": { "rootKey": self.kv_traefik_prefix, "endpoints": [urlparse(self.consul_url).netloc], } } self.static_config = deep_merge( self.static_config, {"providers": provider_config} ) return super()._setup_traefik_static_config() def _start_traefik(self): if self.consul_password: if self.consul_username: self.traefik_env.setdefault( "CONSUL_HTTP_AUTH", f"{self.consul_username}:{self.consul_password}" ) else: self.traefik_env.setdefault("CONSUL_HTTP_TOKEN", self.consul_password) super()._start_traefik() async def _kv_atomic_set(self, to_set): payload = [] def append_payload(key, val): payload.append( { "KV": { "Verb": "set", "Key": key, "Value": base64.b64encode(val.encode()).decode(), } } ) for k, v in to_set.items(): append_payload(k, v) try: await self.consul.txn.put(payload=payload) except Exception: self.log.exception("Error uploading payload to KV store!") else: self.log.debug("Successfully uploaded payload to KV store") async def _kv_atomic_delete(self, *to_delete): payload = [] for key in to_delete: if key.endswith(self.kv_separator): verb = "delete-tree" else: verb = "delete" payload.append( { "KV": {"Verb": verb, "Key": key}, } ) status, response = await self.consul.txn.put(payload=payload) # check response? async def _kv_get_tree(self, prefix): response = await self.consul.txn.put( payload=[ { "KV": { "Verb": "get-tree", "Key": prefix, } } ] ) kv_list = [ ( item["KV"]["Key"], base64.b64decode(item["KV"]["Value"] or '').decode("utf8"), ) for item in response["Results"] ] return self.unflatten_dict_from_kv(kv_list, root_key=prefix)