diff --git a/README.md b/README.md
index bcf22e45..354ca42c 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ depending on how traefik store its routing configuration.
For **smaller**, single-node deployments:
-* TraefikTomlProxy
+* TraefikFileProxy
For **distributed** setups:
@@ -32,7 +32,7 @@ The [documentation](https://jupyterhub-traefik-proxy.readthedocs.io) contains a
guide](https://jupyterhub-traefik-proxy.readthedocs.io/en/latest/install.html)
with examples for the three different implementations.
-* [For TraefikTomlProxy](https://jupyterhub-traefik-proxy.readthedocs.io/en/latest/toml.html#example-setup)
+* [For TraefikFileProxy](https://jupyterhub-traefik-proxy.readthedocs.io/en/latest/file.html#example-setup)
* [For TraefikEtcdProxy](https://jupyterhub-traefik-proxy.readthedocs.io/en/latest/etcd.html#example-setup)
* [For TraefikConsulProxy](https://jupyterhub-traefik-proxy.readthedocs.io/en/latest/consul.html#example-setup)
diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst
index 95481165..3cc18cd7 100644
--- a/docs/source/api/index.rst
+++ b/docs/source/api/index.rst
@@ -15,10 +15,10 @@ Module: :mod:`jupyterhub_traefik_proxy`
.. autoconfigurable:: TraefikProxy
:members:
-:class:`TraefikTomlProxy`
+:class:`TraefikFileProxy`
-------------------------
-.. autoconfigurable:: TraefikTomlProxy
+.. autoconfigurable:: TraefikFileProxy
:members:
:class:`TKvProxy`
diff --git a/docs/source/file.md b/docs/source/file.md
new file mode 100644
index 00000000..e49eabbd
--- /dev/null
+++ b/docs/source/file.md
@@ -0,0 +1,194 @@
+# Using TraefikFileProxy
+
+**jupyterhub-traefik-proxy** can be used with simple toml or yaml configuration files, for smaller, single-node deployments such as
+[The Littlest JupyterHub](https://tljh.jupyter.org).
+
+## How-To install TraefikFileProxy
+
+1. Install **jupyterhub**
+2. Install **jupyterhub-traefik-proxy**
+3. Install **traefik**
+
+* You can find the full installation guide and examples in the [Introduction section](install.html#traefik-proxy-installation)
+
+## How-To enable TraefikFileProxy
+
+You can enable JupyterHub to work with `TraefikFileProxy` in jupyterhub_config.py, using the `proxy_class` configuration option.
+
+You can choose to:
+
+* use the `traefik_file` entrypoint, new in JupyterHub 1.0, e.g.:
+
+ ```python
+ c.JupyterHub.proxy_class = "traefik_file"
+ ```
+
+* use the TraefikFileProxy object, in which case, you have to import the module, e.g.:
+
+ ```python
+ from jupyterhub_traefik_proxy import TraefikFileProxy
+ c.JupyterHub.proxy_class = TraefikFileProxy
+ ```
+
+
+## Traefik configuration
+
+Traefik's configuration is divided into two parts:
+
+* The **static** configuration (loaded only at the beginning)
+* The **dynamic** configuration (can be hot-reloaded, without restarting the proxy),
+where the routing table will be updated continuously.
+
+Traefik allows us to have one file for the static configuration file (`traefik.toml` or `traefik.yaml`) and one or several files for the routes, that traefik would watch.
+
+```{note}
+ **TraefikFileProxy**, uses two configuration files: one file for the routes (**rules.toml** or **rules.yaml**), and one for the static configuration (**traefik.toml** or **traefik.yaml**).
+```
+
+
+By **default**, Traefik will search for `traefik.toml` and `rules.toml` in the following places:
+
+* /etc/traefik/
+* $HOME/.traefik/
+* . the working directory
+
+You can override this in TraefikFileProxy, by modifying the **toml_static_config_file** argument:
+
+```python
+c.TraefikFileProxy.static_config_file="/path/to/static_config_filename.toml"
+```
+
+Similarly, you can override the dynamic configuration file by modifying the **dynamic_config_file** argument:
+
+```python
+c.TraefikFileProxy.dynamic_config_file="/path/to/dynamic_config_filename.toml"
+```
+
+```{note}
+
+* **When JupyterHub starts the proxy**, it writes the static config once, then only edits the dynamic config file.
+
+* **When JupyterHub does not start the proxy**, the user is totally responsible for the static config and
+JupyterHub is responsible exclusively for the routes.
+
+* **When JupyterHub does not start the proxy**, the user should tell `traefik` to get its dynamic configuration
+from a directory. Then, one (or more) dynamic configuration file(s) can be managed externally, and `dynamic_config_file`
+will be managed by JupyterHub. This allows e.g., the administrator to configure traefik's API outside of JupyterHub.
+
+```
+
+## Externally managed TraefikFileProxy
+
+When TraefikFileProxy is externally managed, service managers like [systemd](https://www.freedesktop.org/wiki/Software/systemd/)
+or [docker](https://www.docker.com/) will be responsible for starting and stopping the proxy.
+
+If TraefikFileProxy is used as an externally managed service, then make sure you follow the steps enumerated below:
+
+1. Let JupyterHub know that the proxy being used is TraefikFileProxy, using the *proxy_class* configuration option:
+ ```python
+ from jupyterhub_traefik_proxy import TraefikFileProxy
+ c.JupyterHub.proxy_class = TraefikFileProxy
+ ```
+
+2. Configure `TraefikFileProxy` in **jupyterhub_config.py**
+
+ JupyterHub configuration file, *jupyterhub_config.py* must specify at least:
+ * That the proxy is externally managed
+ * The traefik api credentials
+ * The dynamic configuration file,
+ if different from *rules.toml* or if this file is located
+ in another place than traefik's default search directories (etc/traefik/, $HOME/.traefik/, the working directory)
+
+ Example configuration:
+ ```python
+ # JupyterHub shouldn't start the proxy, it's already running
+ c.TraefikFileProxy.should_start = False
+
+ # if not the default:
+ c.TraefikFileProxy.dynamic_config_file = "/path/to/somefile.toml"
+
+ # traefik api credentials
+ c.TraefikFileProxy.traefik_api_username = "abc"
+ c.TraefikFileProxy.traefik_api_password = "xxx"
+ ```
+
+3. Ensure **traefik.toml** / **traefik.yaml**
+
+ The static configuration file, *traefik.toml* (or **traefik.yaml**) must configure at least:
+ * The default entrypoint
+ * The api entrypoint (*and authenticate it in a user-managed dynamic configuration file*)
+ * The websockets protocol
+ * The dynamic configuration directory to watch
+ (*make sure this configuration directory exists, even if empty before the proxy is launched*)
+ * Check `tests/config_files/traefik.toml` for an example.
+
+## Example setup
+
+This is an example setup for using JupyterHub and TraefikFileProxy managed by another service than JupyterHub.
+
+1. Configure the proxy through the JupyterHub configuration file, *jupyterhub_config.py*, e.g.:
+
+ ```python
+ from jupyterhub_traefik_proxy import TraefikFileProxy
+
+ # mark the proxy as externally managed
+ c.TraefikFileProxy.should_start = False
+
+ # traefik api endpoint login password
+ c.TraefikFileProxy.traefik_api_password = "admin"
+
+ # traefik api endpoint login username
+ c.TraefikFileProxy.traefik_api_username = "api_admin"
+
+ # traefik's dynamic configuration file, which will be managed by JupyterHub
+ c.TraefikFileProxy.dynamic_config_file = "/var/run/traefik/rules.toml"
+
+ # configure JupyterHub to use TraefikFileProxy
+ c.JupyterHub.proxy_class = TraefikFileProxy
+ ```
+
+2. Create a traefik static configuration file, *traefik.toml*, e.g.:
+
+ ```
+ # the api entrypoint
+ [api]
+ dashboard = true
+
+ # websockets protocol
+ [wss]
+ protocol = "http"
+
+ # the port on localhost where traefik accepts http requests
+ [entryPoints.web]
+ address = ":8000"
+
+ # the port on localhost where the traefik api and dashboard can be found
+ [entryPoints.enter_api]
+ address = ":8099"
+
+ # the dynamic configuration directory
+ # This must match the directory provided in Step 1. above.
+ [providers.file]
+ directory = "/var/run/traefik"
+ watch = true
+ ```
+
+3. Create a traefik dynamic configuration file in the directory provided in the dynamic configuration above, to provide the api authentication parameters, e.g.
+
+ ```
+ # Router configuration for the api service
+ [http.routers.router-api]
+ rule = "Host(`localhost`) && PathPrefix(`/api`)"
+ entryPoints = ["enter_api"]
+ service = "api@internal"
+ middlewares = ["auth_api"]
+
+ # authenticate the traefik api entrypoint
+ [http.middlewares.auth_api.basicAuth]
+ users = [ "api_admin:$apr1$eS/j3kum$q/X2khsIEG/bBGsteP.x./",]
+ ```
+
+4. Start traefik with the configuration specified above, e.g.:
+ ```bash
+ $ traefik --configfile traefik.toml
+ ```
diff --git a/docs/source/index.rst b/docs/source/index.rst
index d4750855..1b3ad8ce 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -20,7 +20,7 @@ Moreover it offers *HTTPS* support through a straight-forward `ACME (Let's Encry
There are three versions for the proxy, depending on how traefik stores the routes:
* *for* **smaller**, *single-node deployments*:
- * TraefikTomlProxy
+ * TraefikFileProxy
* *for* **distributed** *setups*:
* TraefikEtcdProxy
* TraefikConsulProxy
@@ -39,7 +39,7 @@ Getting Started
.. toctree::
:maxdepth: 1
- toml
+ fileprovider
etcd
consul
diff --git a/docs/source/install.md b/docs/source/install.md
index af1dd013..f8369713 100644
--- a/docs/source/install.md
+++ b/docs/source/install.md
@@ -69,7 +69,7 @@
## Enabling traefik-proxy in JupyterHub
-[TraefikTomlProxy](https://github.com/jupyterhub/traefik-proxy/blob/master/jupyterhub_traefik_proxy/toml.py), [TraefikEtcdProxy](https://github.com/jupyterhub/traefik-proxy/blob/master/jupyterhub_traefik_proxy/etcd.py) and [TraefikConsulProxy](https://github.com/jupyterhub/traefik-proxy/blob/master/jupyterhub_traefik_proxy/consul.py) are custom proxy implementations that subclass [Proxy](https://github.com/jupyterhub/jupyterhub/blob/master/jupyterhub/proxy.py) and can register in JupyterHub config using `c.JupyterHub.proxy_class` entrypoint.
+[TraefikFileProxy](https://github.com/jupyterhub/traefik-proxy/blob/traefik-v2/jupyterhub_traefik_proxy/fileprovider.py), [TraefikEtcdProxy](https://github.com/jupyterhub/traefik-proxy/blob/traefik-v2/jupyterhub_traefik_proxy/etcd.py) and [TraefikConsulProxy](https://github.com/jupyterhub/traefik-proxy/blob/traefik-v2/jupyterhub_traefik_proxy/consul.py) are custom proxy implementations that subclass [Proxy](https://github.com/jupyterhub/jupyterhub/blob/traefik-v2/jupyterhub/proxy.py) and can register in JupyterHub config using `c.JupyterHub.proxy_class` entrypoint.
On startup, JupyterHub will look by default for a configuration file, *jupyterhub_config.py*, in the current working directory. If the configuration file is not in the current working directory,
you can load a specific config file and start JupyterHub using:
@@ -78,13 +78,13 @@ you can load a specific config file and start JupyterHub using:
$ jupyterhub -f /path/to/jupyterhub_config.py
```
-There is an example configuration file [here](https://github.com/jupyterhub/traefik-proxy/blob/master/examples/jupyterhub_config.py) that configures JupyterHub to run with *TraefikEtcdProxy* as the proxy and uses dummyauthenticator and simplespawner to enable testing without administrative privileges.
+There is an example configuration file [here](https://github.com/jupyterhub/traefik-proxy/blob/traefik-v2/examples/jupyterhub_config.py) that configures JupyterHub to run with *TraefikEtcdProxy* as the proxy and uses dummyauthenticator and simplespawner to enable testing without administrative privileges.
In *jupyterhub_config.py*:
```
-c.JupyterHub.proxy_class = "traefik_toml"
-# will configure JupyterHub to run with TraefikTomlProxy
+c.JupyterHub.proxy_class = "traefik_file"
+# will configure JupyterHub to run with TraefikFileProxy
```
```
@@ -110,9 +110,9 @@ c.JupyterHub.proxy_class = "traefik_consul"
The port on which traefik-proxy's api will run, as well as the username and password used for authenticating, can be passed to the proxy through `jupyterhub_config.py`, e.g.:
```
- c.TraefikTomlProxy.traefik_api_url = "http://127.0.0.1:8099"
- c.TraefikTomlProxy.traefik_api_password = "admin"
- c.TraefikTomlProxy.traefik_api_username = "admin"
+ c.TraefikFileProxy.traefik_api_url = "http://127.0.0.1:8099"
+ c.TraefikFileProxy.traefik_api_password = "admin"
+ c.TraefikFileProxy.traefik_api_username = "admin"
```
Check out TraefikProxy's **API Reference** for more configuration options.
diff --git a/docs/source/toml.md b/docs/source/toml.md
deleted file mode 100644
index 7c88503f..00000000
--- a/docs/source/toml.md
+++ /dev/null
@@ -1,179 +0,0 @@
-# Using TraefikTomlProxy
-
-**jupyterhub-traefik-proxy** can be used with simple toml configuration files, for smaller, single-node deployments such as
-[The Littlest JupyterHub](https://tljh.jupyter.org).
-
-## How-To install TraefikTomlProxy
-
-1. Install **jupyterhub**
-2. Install **jupyterhub-traefik-proxy**
-3. Install **traefik**
-
-* You can find the full installation guide and examples in the [Introduction section](install.html#traefik-proxy-installation)
-
-## How-To enable TraefikTomlProxy
-
-You can enable JupyterHub to work with `TraefikTomlProxy` in jupyterhub_config.py, using the `proxy_class` configuration option.
-
-You can choose to:
-
-* use the `traefik_toml` entrypoint, new in JupyterHub 1.0, e.g.:
-
- ```python
- c.JupyterHub.proxy_class = "traefik_toml"
- ```
-
-* use the TraefikTomlProxy object, in which case, you have to import the module, e.g.:
-
- ```python
- from jupyterhub_traefik_proxy import TraefikTomlProxy
- c.JupyterHub.proxy_class = TraefikTomlProxy
- ```
-
-
-## Traefik configuration
-
-Traefik's configuration is divided into two parts:
-
-* The **static** configuration (loaded only at the beginning)
-* The **dynamic** configuration (can be hot-reloaded, without restarting the proxy),
-where the routing table will be updated continuously.
-
-Traefik allows us to have one file for the static configuration (the `traefik.toml`) and one or several files for the routes, that traefik would watch.
-
-```{note}
- **TraefikTomlProxy**, uses two configuration files: one file for the routes (**rules.toml**), and one for the static configuration (**traefik.toml**).
-```
-
-
-By **default**, Traefik will search for `traefik.toml` and `rules.toml` in the following places:
-
-* /etc/traefik/
-* $HOME/.traefik/
-* . the working directory
-
-You can override this in TraefikTomlProxy, by modifying the **toml_static_config_file** argument:
-
-```python
-c.TraefikTomlProxy.toml_static_config_file="/path/to/static_config_filename.toml"
-```
-
-Similarly, you can override the dynamic configuration file by modifying the **toml_dynamic_config_file** argument:
-
-```python
-c.TraefikTomlProxy.toml_dynamic_config_file="/path/to/dynamic_config_filename.toml"
-```
-
-```{note}
-
-* **When JupyterHub starts the proxy**, it writes the static config once, then only edits the routes config file.
-
-* **When JupyterHub does not start the proxy**, the user is totally responsible for the static config and
-JupyterHub is responsible exclusively for the routes.
-```
-
-## Externally managed TraefikTomlProxy
-
-When TraefikTomlProxy is externally managed, service managers like [systemd](https://www.freedesktop.org/wiki/Software/systemd/)
-or [docker](https://www.docker.com/) will be responsible for starting and stopping the proxy.
-
-If TraefikTomlProxy is used as an externally managed service, then make sure you follow the steps enumerated below:
-
-1. Let JupyterHub know that the proxy being used is TraefikTomlProxy, using the *proxy_class* configuration option:
- ```python
- c.JupyterHub.proxy_class = "traefik_toml"
- ```
-
-2. Configure `TraefikTomlProxy` in **jupyterhub_config.py**
-
- JupyterHub configuration file, *jupyterhub_config.py* must specify at least:
- * That the proxy is externally managed
- * The traefik api credentials
- * The dynamic configuration file,
- if different from *rules.toml* or if this file is located
- in another place than traefik's default search directories (etc/traefik/, $HOME/.traefik/, the working directory)
-
- Example configuration:
- ```python
- # JupyterHub shouldn't start the proxy, it's already running
- c.TraefikTomlProxy.should_start = False
-
- # if not the default:
- c.TraefikTomlProxy.toml_dynamic_config_file = "somefile.toml"
-
- # traefik api credentials
- c.TraefikTomlProxy.traefik_api_username = "abc"
- c.TraefikTomlProxy.traefik_api_password = "xxx"
- ```
-
-3. Ensure **traefik.toml**
-
- The static configuration file, *traefik.toml* must configure at least:
- * The default entrypoint
- * The api entrypoint (*and authenticate it*)
- * The websockets protocol
- * The dynamic configuration file to watch
- (*make sure this configuration file exists, even if empty before the proxy is launched*)
-
-## Example setup
-
-This is an example setup for using JupyterHub and TraefikTomlProxy managed by another service than JupyterHub.
-
-1. Configure the proxy through the JupyterHub configuration file, *jupyterhub_config.py*, e.g.:
-
- ```python
- from jupyterhub_traefik_proxy import TraefikTomlProxy
-
- # mark the proxy as externally managed
- c.TraefikTomlProxy.should_start = False
-
- # traefik api endpoint login password
- c.TraefikTomlProxy.traefik_api_password = "admin"
-
- # traefik api endpoint login username
- c.TraefikTomlProxy.traefik_api_username = "api_admin"
-
- # traefik's dynamic configuration file
- c.TraefikTomlProxy.toml_dynamic_config_file = "path/to/rules.toml"
-
- # configure JupyterHub to use TraefikTomlProxy
- c.JupyterHub.proxy_class = TraefikTomlProxy
- ```
-
-2. Create a traefik static configuration file, *traefik.toml*, e.g.:
-
- ```
- # the default entrypoint
- defaultentrypoints = ["http"]
-
- # the api entrypoint
- [api]
- dashboard = true
- entrypoint = "auth_api"
-
- # websockets protocol
- [wss]
- protocol = "http"
-
- # the port on localhost where traefik accepts http requests
- [entryPoints.http]
- address = ":8000"
-
- # the port on localhost where the traefik api and dashboard can be found
- [entryPoints.auth_api]
- address = ":8099"
-
- # authenticate the traefik api entrypoint
- [entryPoints.auth_api.auth.basic]
- users = [ "api_admin:$apr1$eS/j3kum$q/X2khsIEG/bBGsteP.x./",]
-
- # the dynamic configuration file
- [file]
- filename = "rules.toml"
- watch = true
- ```
-
-3. Start traefik with the configuration specified above, e.g.:
- ```bash
- $ traefik -c traefik.toml
- ```
diff --git a/examples/jupyterhub_config_toml.py b/examples/jupyterhub_config_file.py
similarity index 58%
rename from examples/jupyterhub_config_toml.py
rename to examples/jupyterhub_config_file.py
index b17ca1f3..236a6bf7 100644
--- a/examples/jupyterhub_config_toml.py
+++ b/examples/jupyterhub_config_file.py
@@ -1,6 +1,6 @@
"""sample jupyterhub config file for testing
-configures jupyterhub to run with traefik_toml.
+configures jupyterhub to run with traefik_file.
configures jupyterhub with dummyauthenticator and simplespawner
to enable testing without administrative privileges.
@@ -8,10 +8,10 @@
requires jupyterhub 1.0.dev
"""
-c.JupyterHub.proxy_class = "traefik_toml"
-c.TraefikTomlProxy.traefik_api_username = "admin"
-c.TraefikTomlProxy.traefik_api_password = "admin"
-c.TraefikTomlProxy.traefik_log_level = "INFO"
+c.JupyterHub.proxy_class = "traefik_file"
+c.TraefikFileProxy.traefik_api_username = "admin"
+c.TraefikFileProxy.traefik_api_password = "admin"
+c.TraefikFileProxy.traefik_log_level = "INFO"
# use dummy and simple auth/spawner for testing
c.JupyterHub.authenticator_class = "dummy"
diff --git a/jupyterhub_traefik_proxy/__init__.py b/jupyterhub_traefik_proxy/__init__.py
index 45346b81..39dda657 100644
--- a/jupyterhub_traefik_proxy/__init__.py
+++ b/jupyterhub_traefik_proxy/__init__.py
@@ -1,8 +1,7 @@
"""Traefik implementation of the JupyterHub proxy API"""
from .proxy import TraefikProxy # noqa
-from .kv_proxy import TKvProxy # noqa
-from .toml import TraefikTomlProxy
+from .fileprovider import TraefikFileProxy
from ._version import get_versions
diff --git a/jupyterhub_traefik_proxy/consul.py b/jupyterhub_traefik_proxy/consul.py
deleted file mode 100644
index 3459aab0..00000000
--- a/jupyterhub_traefik_proxy/consul.py
+++ /dev/null
@@ -1,242 +0,0 @@
-"""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 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
-
-
-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):
- try:
- import consul.aio
- except ImportError:
- raise ImportError("Please install python-consul2 package to use traefik-proxy with consul")
- 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,
- )
- 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", jupyterhub_routespec
- )
- return True, None
- 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"]
-
- async def stop(self):
- await super().stop()
diff --git a/jupyterhub_traefik_proxy/etcd.py b/jupyterhub_traefik_proxy/etcd.py
deleted file mode 100644
index 9b0e0d15..00000000
--- a/jupyterhub_traefik_proxy/etcd.py
+++ /dev/null
@@ -1,196 +0,0 @@
-"""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
-
-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
-
-
-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)
- try:
- import etcd3
- except ImportError:
- raise ImportError("Please install etcd3 package to use traefik-proxy with etcd3")
- 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", jupyterhub_routespec
- )
- return True, None
-
- 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
diff --git a/jupyterhub_traefik_proxy/install.py b/jupyterhub_traefik_proxy/install.py
index ef2b4dcc..e636a91f 100644
--- a/jupyterhub_traefik_proxy/install.py
+++ b/jupyterhub_traefik_proxy/install.py
@@ -3,44 +3,22 @@
from urllib.request import urlretrieve
import tarfile
import zipfile
-import shutil
import argparse
import textwrap
import hashlib
import warnings
checksums_traefik = {
- "https://github.com/traefik/traefik/releases/download/v1.7.29/traefik_linux-arm64": "d27c220bdcc8bae33436adce309fd856c2ee295bd3dd5416428d3b4a173b8310",
- "https://github.com/traefik/traefik/releases/download/v1.7.29/traefik_linux-amd64": "70cd8847354326fb17acd10251c44450cf5d6c4fd8df130f2c6f86dd7b1b52d1",
- "https://github.com/traefik/traefik/releases/download/v1.7.29/traefik_darwin-amd64": "bbe30c8e7aa5e76442187be409c07e6b798e7ba67d7d3d60856e0a7664654c46",
- "https://github.com/containous/traefik/releases/download/v1.7.28/traefik_linux-amd64": "b70284ac72b4f9a119be92f206fc0c6dbc0db18ff7295d4df6701c0b292ecbf0",
- "https://github.com/containous/traefik/releases/download/v1.7.28/traefik_darwin-amd64": "3e4bb0146bed06c842ae7a91e711e5ba98339f529b84aa80c766a01dd39d9731",
- "https://github.com/containous/traefik/releases/download/v1.7.18/traefik_linux-amd64": "3c2d153d80890b6fc8875af9f8ced32c4d684e1eb5a46d9815337cb343dfd92e",
- "https://github.com/containous/traefik/releases/download/v1.7.18/traefik_darwin-amd64": "84e07a184c31b7fb86417ba3a237ad334a26bcb1ed53fd56f0774afaa34074d9",
- "https://github.com/containous/traefik/releases/download/v1.7.5/traefik_linux-amd64": "4417a9d83753e1ad6bdd64bbbeaeb4b279bcc71542e779b7bcb3b027c6e3356e",
- "https://github.com/containous/traefik/releases/download/v1.7.5/traefik_darwin-amd64": "379d4af242743a3fe44b44a1ee6df68ea8332578d85de35f264e062c19fd20a0",
- "https://github.com/containous/traefik/releases/download/v1.7.0/traefik_linux-amd64": "b84cb03e8a175b8b7d1a30246d19705f607c6ae5ee89f2dca7a1adccab919135",
- "https://github.com/containous/traefik/releases/download/v1.7.0/traefik_darwin-amd64": "3000cb9f8ed567e9bc567cce33107f6877f2017c69fae8ac235b51a7a94229bf",
-}
-
-checksums_etcd = {
- "https://github.com/etcd-io/etcd/releases/download/v3.4.15/etcd-v3.4.15-linux-arm64.tar.gz": "fcc522275300cf90d42377106d47a2e384d1d2083af205cbb7833a79ef5a49d1",
- "https://github.com/etcd-io/etcd/releases/download/v3.4.15/etcd-v3.4.15-linux-amd64.tar.gz": "3bd00836ea328db89ecba3ed2155293934c0d09e64b53d6c9dfc0a256e724b81",
- "https://github.com/etcd-io/etcd/releases/download/v3.4.15/etcd-v3.4.15-darwin-amd64.tar.gz": "c596709069193bffc639a22558bdea4d801128e635909ea01a6fd5b5c85da729",
- "https://github.com/etcd-io/etcd/releases/download/v3.3.10/etcd-v3.3.10-linux-amd64.tar.gz": "1620a59150ec0a0124a65540e23891243feb2d9a628092fb1edcc23974724a45",
- "https://github.com/etcd-io/etcd/releases/download/v3.3.10/etcd-v3.3.10-darwin-amd64.tar.gz": "fac4091c7ba6f032830fad7809a115909d0f0cae5cbf5b34044540def743577b",
- "https://github.com/etcd-io/etcd/releases/download/v3.2.25/etcd-v3.3.10-linux-amd64.tar.gz": "8a509ffb1443088d501f19e339a0d9c0058ce20599752c3baef83c1c68790ef7",
- "https://github.com/etcd-io/etcd/releases/download/v3.2.25/etcd-v3.3.10-darwin-amd64.tar.gz": "9950684a01d7431bc12c3dba014f222d55a862c6f8af64c09c42d7a59ed6790d",
-}
-
-checksums_consul = {
- "https://releases.hashicorp.com/consul/1.9.4/consul_1.9.4_darwin.zip": "c168240d52f67c71b30ef51b3594673cad77d0dbbf38c412b2ee30b39ef30843",
- "https://releases.hashicorp.com/consul/1.9.4/consul_1.9.4_linux_amd64.zip": "da3919197ef33c4205bb7df3cc5992ccaae01d46753a72fe029778d7f52fb610",
- "https://releases.hashicorp.com/consul/1.9.4/consul_1.9.4_linux_arm64.zip": "012c552aff502f907416c9a119d2dfed88b92e981f9b160eb4fe292676afdaeb",
- "https://releases.hashicorp.com/consul/1.6.1/consul_1.6.1_linux_amd64.zip": "a8568ca7b6797030b2c32615b4786d4cc75ce7aee2ed9025996fe92b07b31f7e",
- "https://releases.hashicorp.com/consul/1.6.1/consul_1.6.1_darwin_amd64.zip": "4bc205e06b2921f998cb6ddbe70de57f8e558e226e44aba3f337f2f245678b85",
- "https://releases.hashicorp.com/consul/1.5.0/consul_1.5.0_linux_amd64.zip": "1399064050019db05d3378f757e058ec4426a917dd2d240336b51532065880b6",
- "https://releases.hashicorp.com/consul/1.5.0/consul_1.5.0_darwin_amd64.zip": "b4033ea6871fe6136ee5d940c834be2248463c3ec248dc22370e6d5360931325",
+ "https://github.com/traefik/traefik/releases/download/v2.4.8/traefik_v2.4.8_linux_arm64.tar.gz": "0931fdd9c855fcafd38eba7568a1d287200fad5afd1aef7d112fb3a48d822fcc",
+ "https://github.com/traefik/traefik/releases/download/v2.4.8/traefik_v2.4.8_linux_amd64.tar.gz": "de8d56f6777c5098834d4f8d9ed419b7353a3afe913393a55b6fd14779564129",
+ "https://github.com/traefik/traefik/releases/download/v2.4.8/traefik_v2.4.8_darwin_amd64.tar.gz": "7d946baa422acfcf166e19779052c005722db03de3ab4d7aff586c4b4873a0f3",
+ "https://github.com/traefik/traefik/releases/download/v2.4.8/traefik_v2.4.8_windows_amd64.zip": "4203443cb1e91d76f81d1e2a41fb70e66452d951b1ffd8964218a7bc211c377d",
+ "https://github.com/traefik/traefik/releases/download/v2.3.7/traefik_v2.3.7_linux_amd64.tar.gz": "a357d40bc9b81ae76070a2bc0334dfd15e77143f41415a93f83bb53af1756909",
+ "https://github.com/traefik/traefik/releases/download/v2.3.7/traefik_v2.3.7_darwin_amd64.tar.gz": "c84fc21b8ee34bba8a66f0f9e71c6c2ea69684ac6330916551f1f111826b9bb3",
+ "https://github.com/traefik/traefik/releases/download/v2.3.7/traefik_v2.3.7_windows_amd64.zip": "eb54b1c9c752a6eaf48d28ff8409c17379a29b9d58390107411762ab6e4edfb4",
+ "https://github.com/traefik/traefik/releases/download/v2.2.11/traefik_v2.2.11_linux_amd64.tar.gz": "b677386423403c63fb9ac9667d39591be587a1a4928afc2e59449c78343bad9c",
+ "https://github.com/traefik/traefik/releases/download/v2.2.11/traefik_v2.2.11_darwin_amd64.tar.gz": "efb1c2bc23e16a9083e5a210594186d026cdec0b522a6b4754ceff43b07d8031",
+ "https://github.com/traefik/traefik/releases/download/v2.2.11/traefik_v2.2.11_windows_amd64.zip": "ee867133e00b2d8395c239d8fed04a26b362e650b371dc0b653f0ee9d52471e6",
}
@@ -54,38 +32,28 @@ def checksum_file(path):
def install_traefik(prefix, plat, traefik_version):
- traefik_bin = os.path.join(prefix, "traefik")
+ plat = plat.replace("-", "_")
+ if "windows" in plat:
+ traefik_archive_extension = "zip"
+ traefik_bin = os.path.join(prefix, "traefik.exe")
+ else:
+ traefik_archive_extension = "tar.gz"
+ traefik_bin = os.path.join(prefix, "traefik")
+
+ traefik_archive = "traefik_v" + traefik_version + "_" + plat + "." + traefik_archive_extension
+ traefik_archive_path = os.path.join(prefix, traefik_archive)
traefik_url = (
- "https://github.com/containous/traefik/releases"
- f"/download/v{traefik_version}/traefik_{plat}"
+ "https://github.com/traefik/traefik/releases"
+ f"/download/v{traefik_version}/{traefik_archive}"
)
- if os.path.exists(traefik_bin):
- print(f"Traefik already exists")
- if traefik_url not in checksums_traefik:
- warnings.warn(
- f"Couldn't verify checksum for traefik-v{traefik_version}-{plat}",
- stacklevel=2,
- )
- os.chmod(traefik_bin, 0o755)
- print("--- Done ---")
- return
- else:
- checksum = checksum_file(traefik_bin)
- if checksum == checksums_traefik[traefik_url]:
- os.chmod(traefik_bin, 0o755)
- print("--- Done ---")
- return
- else:
- print(f"checksum mismatch on {traefik_bin}")
- os.remove(traefik_bin)
-
- print(f"Downloading traefik {traefik_version}...")
- urlretrieve(traefik_url, traefik_bin)
+ if not os.path.exists(traefik_archive_path):
+ print(f"Downloading traefik {traefik_archive}...")
+ urlretrieve(traefik_url, traefik_archive_path)
if traefik_url in checksums_traefik:
- checksum = checksum_file(traefik_bin)
+ checksum = checksum_file(traefik_archive_path)
if checksum != checksums_traefik[traefik_url]:
raise IOError("Checksum failed")
else:
@@ -94,158 +62,15 @@ def install_traefik(prefix, plat, traefik_version):
stacklevel=2,
)
- os.chmod(traefik_bin, 0o755)
-
- print("--- Done ---")
-
-
-def install_etcd(prefix, plat, etcd_version):
- etcd_downloaded_dir_name = f"etcd-v{etcd_version}-{plat}"
- etcd_archive_extension = ".tar.gz"
- if "linux" in plat:
- etcd_archive_extension = "tar.gz"
- else:
- etcd_archive_extension = "zip"
- etcd_downloaded_archive = os.path.join(
- prefix, etcd_downloaded_dir_name + "." + etcd_archive_extension
- )
- etcd_binaries = os.path.join(prefix, "etcd_binaries")
-
- etcd_bin = os.path.join(prefix, "etcd")
- etcdctl_bin = os.path.join(prefix, "etcdctl")
-
- etcd_url = (
- "https://github.com/etcd-io/etcd/releases/"
- f"/download/v{etcd_version}/etcd-v{etcd_version}-{plat}.{etcd_archive_extension}"
- )
-
- if os.path.exists(etcd_bin) and os.path.exists(etcdctl_bin):
- print(f"Etcd and etcdctl already exist")
- if etcd_url not in checksums_etcd:
- warnings.warn(
- f"Couldn't verify checksum for etcd-v{etcd_version}-{plat}",
- stacklevel=2,
- )
- os.chmod(etcd_bin, 0o755)
- os.chmod(etcdctl_bin, 0o755)
- print("--- Done ---")
- return
- else:
- checksum_etcd_archive = checksum_file(etcd_downloaded_archive)
- if checksum_etcd_archive == checksums_etcd[etcd_url]:
- os.chmod(etcd_bin, 0o755)
- os.chmod(etcdctl_bin, 0o755)
- print("--- Done ---")
- return
- else:
- print(f"checksum mismatch on etcd")
- os.remove(etcd_bin)
- os.remove(etcdctl_bin)
- os.remove(etcd_downloaded_archive)
-
- if not os.path.exists(etcd_downloaded_archive):
- print(f"Downloading {etcd_downloaded_dir_name} archive...")
- urlretrieve(etcd_url, etcd_downloaded_archive)
- else:
- print(f"Archive {etcd_downloaded_dir_name} already exists")
-
- if etcd_archive_extension == "zip":
- with zipfile.ZipFile(etcd_downloaded_archive, "r") as zip_ref:
- zip_ref.extract(etcd_downloaded_dir_name + "/etcd", etcd_binaries)
- zip_ref.extract(etcd_downloaded_dir_name + "/etcdctl", etcd_binaries)
- else:
- with (tarfile.open(etcd_downloaded_archive, "r")) as tar_ref:
- print("Extracting the archive...")
- tar_ref.extract(etcd_downloaded_dir_name + "/etcd", etcd_binaries)
- tar_ref.extract(etcd_downloaded_dir_name + "/etcdctl", etcd_binaries)
-
- shutil.copy(os.path.join(etcd_binaries, etcd_downloaded_dir_name, "etcd"), etcd_bin)
- shutil.copy(
- os.path.join(etcd_binaries, etcd_downloaded_dir_name, "etcdctl"), etcdctl_bin
- )
-
- if etcd_url in checksums_etcd:
- checksum_etcd_archive = checksum_file(etcd_downloaded_archive)
- if checksum_etcd_archive != checksums_etcd[etcd_url]:
- raise IOError("Checksum failed")
- else:
- warnings.warn(
- f"Couldn't verify checksum for etcd-v{etcd_version}-{plat}", stacklevel=2
- )
-
- os.chmod(etcd_bin, 0o755)
- os.chmod(etcdctl_bin, 0o755)
-
- # Cleanup
- shutil.rmtree(etcd_binaries)
-
- print("--- Done ---")
-
-
-def install_consul(prefix, plat, consul_version):
- plat = plat.replace("-", "_")
- consul_downloaded_dir_name = f"consul_v{consul_version}_{plat}"
- consul_archive_extension = ".tar.gz"
- consul_archive_extension = "zip"
-
- consul_downloaded_archive = os.path.join(
- prefix, consul_downloaded_dir_name + "." + consul_archive_extension
- )
- consul_binaries = os.path.join(prefix, "consul_binaries")
-
- consul_bin = os.path.join(prefix, "consul")
-
- consul_url = (
- "https://releases.hashicorp.com/consul/"
- f"{consul_version}/consul_{consul_version}_{plat}.{consul_archive_extension}"
- )
-
- if os.path.exists(consul_bin):
- print(f"Consul already exists")
- if consul_url not in checksums_consul:
- warnings.warn(
- f"Couldn't verify checksum for consul_v{consul_version}_{plat}",
- stacklevel=2,
- )
- os.chmod(consul_bin, 0o755)
- print("--- Done ---")
- return
- else:
- checksum_consul_archive = checksum_file(consul_downloaded_archive)
- if checksum_consul_archive == checksums_consul[consul_url]:
- os.chmod(consul_bin, 0o755)
- print("--- Done ---")
- return
- else:
- print(f"checksum mismatch on consul")
- os.remove(consul_bin)
- os.remove(consul_downloaded_archive)
-
- if not os.path.exists(consul_downloaded_archive):
- print(f"Downloading {consul_downloaded_dir_name} archive...")
- urlretrieve(consul_url, consul_downloaded_archive)
+ print("Extracting the archive...")
+ if traefik_archive_extension == "tar.gz":
+ with tarfile.open(traefik_archive_path, "r") as tar_ref:
+ tar_ref.extract("traefik", prefix)
else:
- print(f"Archive {consul_downloaded_dir_name} already exists")
+ with zipfile.ZipFile(traefik_archive_path, "r") as zip_ref:
+ zip_ref.extract("traefik.exe", prefix)
- with zipfile.ZipFile(consul_downloaded_archive, "r") as zip_ref:
- zip_ref.extract("consul", consul_binaries)
-
- shutil.copy(os.path.join(consul_binaries, "consul"), consul_bin)
-
- if consul_url in checksums_consul:
- checksum_consul_archive = checksum_file(consul_downloaded_archive)
- if checksum_consul_archive != checksums_consul[consul_url]:
- raise IOError("Checksum failed")
- else:
- warnings.warn(
- f"Couldn't verify checksum for consul_v{consul_version}_{plat}",
- stacklevel=2,
- )
-
- os.chmod(consul_bin, 0o755)
-
- # Cleanup
- shutil.rmtree(consul_binaries)
+ os.chmod(traefik_bin, 0o755)
print("--- Done ---")
@@ -258,24 +83,16 @@ def main():
"""\
Checksums available for:
- traefik:
- - v1.7.28-linux-amd64
- - v1.7.28-darwin-amd64
- - v1.7.18-linux-amd64
- - v1.7.18-darwin-amd64
- - v1.7.5-linux-amd64
- - v1.7.5-darwin-amd64
- - v1.7.0-linux-amd64
- - v1.7.0-darwin-amd64
- - etcd:
- - v3.3.10-linux-amd64
- - v3.3.10-darwin-amd64
- - v3.2.25-linux-amd64
- - v3.2.25-darwin-amd64
- - consul:
- - v1.5.0_linux_amd64
- - v1.5.0_darwin_amd64
- - v1.6.1_linux_amd64
- - v1.6.1_darwin_amd64
+ - v2.4.8-linux-arm64
+ - v2.4.8-linux-amd64
+ - v2.4.8-darwin-amd64
+ - v2.4.8-windows-amd64
+ - v2.3.7-linux-amd64
+ - v2.3.7-darwin-amd64
+ - v2.3.7-windows-amd64
+ - v2.2.11-linux-amd64
+ - v2.2.11-darwin-amd64
+ - v2.2.11-windows-amd64
"""
),
formatter_class=argparse.RawTextHelpFormatter,
@@ -324,7 +141,7 @@ def main():
parser.add_argument(
"--traefik-version",
dest="traefik_version",
- default="1.7.28",
+ default="2.4.8",
help=textwrap.dedent(
"""\
The version of traefik to download.
@@ -334,62 +151,13 @@ def main():
),
)
- parser.add_argument(
- "--etcd",
- action="store_true",
- help=textwrap.dedent(
- """\
- Whether or not to install etcd.
- By default etcd is NOT going to be installed.
- """
- ),
- )
-
- parser.add_argument(
- "--etcd-version",
- dest="etcd_version",
- default="3.3.10",
- help=textwrap.dedent(
- """\
- The version of etcd to download.
- If no version is provided, it defaults to:
- --- %(default)s ---
- """
- ),
- )
-
- parser.add_argument(
- "--consul",
- action="store_true",
- help=textwrap.dedent(
- """\
- Whether or not to install consul.
- By default consul is NOT going to be installed:
- """
- ),
- )
-
- parser.add_argument(
- "--consul-version",
- dest="consul_version",
- default="1.6.1",
- help=textwrap.dedent(
- """\
- The version of consul to download.
- If no version is provided, it defaults to:
- --- %(default)s ---
- """
- ),
- )
args = parser.parse_args()
deps_dir = args.installation_dir
plat = args.plat
traefik_version = args.traefik_version
- etcd_version = args.etcd_version
- consul_version = args.consul_version
- if not args.traefik and not args.etcd and not args.consul:
+ if not args.traefik:
print(
"""Please specify what binary to install.
Tip: python3 -m jupyterhub_traefik_proxy.install --help
@@ -405,11 +173,6 @@ def main():
if args.traefik:
install_traefik(deps_dir, plat, traefik_version)
- if args.etcd:
- install_etcd(deps_dir, plat, etcd_version)
- if args.consul:
- install_consul(deps_dir, plat, consul_version)
-
if __name__ == "__main__":
main()
diff --git a/jupyterhub_traefik_proxy/kv_proxy.py b/jupyterhub_traefik_proxy/kv_proxy.py
deleted file mode 100644
index b1dc4fb3..00000000
--- a/jupyterhub_traefik_proxy/kv_proxy.py
+++ /dev/null
@@ -1,369 +0,0 @@
-"""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 traitlets import Any, Unicode
-
-from . import traefik_utils
-from jupyterhub_traefik_proxy import TraefikProxy
-
-
-class TKvProxy(TraefikProxy):
- """
- JupyterHub Proxy implementation using traefik and a key-value store.
- Custom proxy implementations based on trafik and a key-value store
- can sublass :class:`TKvProxy`.
- """
-
- kv_client = Any()
- # Key-value store client
-
- kv_name = Unicode(config=False, help="""The name of the key value store""")
-
- kv_username = Unicode(
- config=True, help="""The username for key value store login"""
- )
-
- kv_password = Unicode(
- config=True, help="""The password for key value store login"""
- )
-
- kv_url = Unicode(config=True, help="""The URL of the key value store server""")
-
- kv_traefik_prefix = traefik_utils.KVStorePrefix(
- config=True,
- help="""The key value store key prefix for traefik static configuration""",
- )
-
- kv_jupyterhub_prefix = Unicode(
- config=True,
- help="""The key value store key prefix for traefik dynamic configuration""",
- )
-
- def _define_kv_specific_static_config(self):
- """Define the traefik static configuration that configures
- traefik's communication with the key-value store.
-
- Will be called during startup if should_start is True.
-
- **Subclasses must define this method**
- if the proxy is to be started by the Hub.
-
- In order to be picked up by the proxy, the static configuration
- must be stored into `proxy.static_config` dict under the `kv_name` key.
- """
- raise NotImplementedError()
-
- async def _kv_atomic_add_route_parts(
- self, jupyterhub_routespec, target, data, route_keys, rule
- ):
- """Add the key-value pairs associated with a route within a
- key-value store transaction.
-
- **Subclasses must define this method**
-
- Will be called during add_route.
-
- When retrieving or deleting a route, the parts of a route
- are expected to have the following structure:
- [ key: jupyterhub_routespec , value: target ]
- [ key: target , value: data ]
- [ key: route_keys.backend_url_path , value: target ]
- [ key: route_keys.frontend_rule_path , value: rule ]
- [ key: route_keys.frontend_backend_path, value:
- route_keys.backend_alias]
- [ key: route_keys.backend_weight_path , value: w(int) ]
- (where `w` is the weight of the backend to be used during load balancing)
-
- Returns:
- result (tuple):
- 'status'(int): The transaction status
- (0: failure, positive: success)
- 'response'(str): The transaction response
- """
- raise NotImplementedError()
-
- async def _kv_atomic_delete_route_parts(self, jupyterhub_routespec, route_keys):
- """Delete the key-value pairs associated with a route within a
- key-value store transaction (if the route exists).
-
- **Subclasses must define this method**
-
- Will be called during delete_route.
-
- The keys associated with a route are:
- jupyterhub_routespec,
- target,
- route_keys.backend_url_path,
- route_keys.frontend_rule_path,
- route_keys.frontend_backend_path,
- route_keys.backend_weight_path,
-
- Returns:
- result (tuple):
- 'status'(int): The transaction status
- (0: failure, positive: success).
- 'response'(str): The transaction response.
- """
- raise NotImplementedError()
-
- async def _kv_get_target(self, jupyterhub_routespec):
- """Retrive the target from the key-value store.
- The target is the value associated with `jupyterhub_routespec` key.
-
- **Subclasses must define this method**
-
- Returns:
- target (str): The full URL associated with this route.
- """
- raise NotImplementedError()
-
- async def _kv_get_data(self, target):
- """Retrive the data associated with the `target` from the key-value store.
-
- **Subclasses must define this method**
-
- Returns:
- data (dict): A JSONable dict that holds extra info about the route
- """
- raise NotImplementedError()
-
- async def _kv_get_route_parts(self, kv_entry):
- """Retrive all the parts that make up a route (i.e. routespec, target, data)
- from the key-value store given a `kv_entry`.
-
- A `kv_entry` is a key-value store entry where the key starts with
- `proxy.jupyterhub_prefix`. It is expected that only the routespecs
- will be prefixed with `proxy.jupyterhub_prefix` when added to the kv store.
-
- **Subclasses must define this method**
-
- Returns:
- 'routespec': The normalized route specification passed in to add_route
- ([host]/path/)
- 'target': The target host for this route (proto://host)
- 'data': The arbitrary data dict that was passed in by JupyterHub when adding this
- route.
- """
- raise NotImplementedError()
-
- async def _kv_get_jupyterhub_prefixed_entries(self):
- """Retrive from the kv store all the key-value pairs where the key starts with
- `proxy.jupyterhub_prefix`.
- It is expected that only the routespecs will be prefixed with `proxy.jupyterhub_prefix`
- when added to the kv store.
-
- **Subclasses must define this method**
-
- Returns:
- 'routes': A list of key-value store entries where the keys start
- with `proxy.jupyterhub_prefix`.
- """
-
- raise NotImplementedError()
-
- def _clean_resources(self):
- try:
- if self.should_start:
- os.remove(self.toml_static_config_file)
- except:
- self.log.error("Failed to remove traefik's configuration files")
- raise
-
- def _start_traefik(self):
- self.log.info("Starting traefik...")
- try:
- self._launch_traefik(config_type=self.kv_name)
- except FileNotFoundError as e:
- self.log.error(
- "Failed to find traefik \n"
- "The proxy can be downloaded from https://github.com/containous/traefik/releases/download."
- )
- raise
-
- async def _setup_traefik_static_config(self):
- await super()._setup_traefik_static_config()
- self._define_kv_specific_static_config()
- try:
- traefik_utils.persist_static_conf(
- self.toml_static_config_file, self.static_config
- )
- except IOError:
- self.log.exception("Couldn't set up traefik's static config.")
- raise
- except:
- self.log.error("Couldn't set up traefik's static config. Unexpected error:")
- raise
-
- async def start(self):
- """Start the proxy.
- Will be called during startup if should_start is True.
- """
- await super().start()
- await self._wait_for_static_config(provider=self.kv_name)
-
- async def stop(self):
- """Stop the proxy.
- Will be called during teardown if should_start is True.
- """
- await super().stop()
- self._clean_resources()
-
- async def add_route(self, routespec, target, data):
- """Add a route to the proxy.
-
- Args:
- routespec (str): A URL prefix ([host]/path/) for which this route will be matched,
- e.g. host.name/path/
- target (str): A full URL that will be the target of this route.
- data (dict): A JSONable dict that will be associated with this route, and will
- be returned when retrieving information about this route.
-
- Will raise an appropriate Exception (FIXME: find what?) if the route could
- not be added.
-
- This proxy implementation prefixes the routespec with `proxy.jupyterhub_prefix` when
- adding it to the kv store in orde to associate the fact that the route came from JupyterHub.
- Everything traefik related is prefixed with `proxy.traefik_prefix`.
- """
- self.log.info("Adding route for %s to %s.", routespec, target)
-
- routespec = self._routespec_to_traefik_path(routespec)
- route_keys = traefik_utils.generate_route_keys(self, routespec)
-
- # Store the data dict passed in by JupyterHub
- data = json.dumps(data)
- # Generate the routing rule
- rule = traefik_utils.generate_rule(routespec)
-
- # To be able to delete the route when only routespec is provided
- jupyterhub_routespec = self.kv_jupyterhub_prefix + routespec
-
- status, response = await self._kv_atomic_add_route_parts(
- jupyterhub_routespec, target, data, route_keys, rule
- )
-
- if self.should_start:
- try:
- # Check if traefik was launched
- pid = self.traefik_process.pid
- except AttributeError:
- self.log.error(
- "You cannot add routes if the proxy isn't running! Please start the proxy: proxy.start()"
- )
- raise
- if status:
- self.log.info(
- "Added backend %s with the alias %s.", target, route_keys.backend_alias
- )
- self.log.info(
- "Added frontend %s for backend %s with the following routing rule %s.",
- route_keys.frontend_alias,
- route_keys.backend_alias,
- routespec,
- )
- else:
- self.log.error(
- "Couldn't add route for %s. Response: %s", routespec, response
- )
-
- await self._wait_for_route(routespec, provider=self.kv_name)
-
- async def delete_route(self, routespec):
- """Delete a route and all the traefik related info associated given a routespec,
- (if it exists).
- """
- routespec = self._routespec_to_traefik_path(routespec)
- jupyterhub_routespec = self.kv_jupyterhub_prefix + routespec
- route_keys = traefik_utils.generate_route_keys(self, routespec)
-
- status, response = await self._kv_atomic_delete_route_parts(
- jupyterhub_routespec, route_keys
- )
- if status:
- self.log.info("Routespec %s was deleted.", routespec)
- else:
- self.log.error(
- "Couldn't delete route %s. Response: %s", routespec, response
- )
-
- async def get_all_routes(self):
- """Fetch and return all the routes associated by JupyterHub from the
- proxy.
-
- Returns a dictionary of routes, where the keys are
- routespecs and each value is a dict of the form::
-
- {
- 'routespec': the route specification ([host]/path/)
- 'target': the target host URL (proto://host) for this route
- 'data': the attached data dict for this route (as specified in add_route)
- }
- """
- all_routes = {}
- routes = await self._kv_get_jupyterhub_prefixed_entries()
-
- for kv_entry in routes:
- traefik_routespec, target, data = await self._kv_get_route_parts(kv_entry)
- routespec = self._routespec_from_traefik_path(traefik_routespec)
- all_routes[routespec] = {
- "routespec": routespec,
- "target": target,
- "data": None if data is None else json.loads(data),
- }
-
- return all_routes
-
- async def get_route(self, routespec):
- """Return the route info for a given routespec.
-
- Args:
- routespec (str):
- A URI that was used to add this route,
- e.g. `host.tld/path/`
-
- Returns:
- result (dict):
- dict with the following keys::
-
- 'routespec': The normalized route specification passed in to add_route
- ([host]/path/)
- 'target': The target host for this route (proto://host)
- 'data': The arbitrary data dict that was passed in by JupyterHub when adding this
- route.
-
- None: if there are no routes matching the given routespec
- """
- routespec = self.validate_routespec(routespec)
- traefik_routespec = self._routespec_to_traefik_path(routespec)
- jupyterhub_routespec = self.kv_jupyterhub_prefix + traefik_routespec
-
- target = await self._kv_get_target(jupyterhub_routespec)
- if target is None:
- return None
- data = await self._kv_get_data(target)
-
- return {
- "routespec": routespec,
- "target": target,
- "data": None if data is None else json.loads(data),
- }
diff --git a/jupyterhub_traefik_proxy/proxy.py b/jupyterhub_traefik_proxy/proxy.py
index d2421c8f..893b53b0 100644
--- a/jupyterhub_traefik_proxy/proxy.py
+++ b/jupyterhub_traefik_proxy/proxy.py
@@ -20,7 +20,7 @@
import json
from os.path import abspath, dirname, join
-from subprocess import Popen
+from subprocess import Popen, TimeoutExpired
from urllib.parse import urlparse
from traitlets import Any, Bool, Dict, Integer, Unicode, default
@@ -36,12 +36,12 @@ class TraefikProxy(Proxy):
traefik_process = Any()
- toml_static_config_file = Unicode(
+ static_config_file = Unicode(
"traefik.toml", config=True, help="""traefik's static configuration file"""
)
traefik_api_url = Unicode(
- "http://127.0.0.1:8099",
+ "http://localhost:8099",
config=True,
help="""traefik authenticated api endpoint url""",
)
@@ -52,12 +52,36 @@ class TraefikProxy(Proxy):
help="""validate SSL certificate of traefik api endpoint""",
)
- traefik_log_level = Unicode("ERROR", config=True, help="""traefik's log level""")
+ debug = Bool(False, config=True, help="""Debug the proxy class?""")
+
+ traefik_log_level = Unicode("DEBUG", config=True, help="""traefik's log level""")
traefik_api_password = Unicode(
config=True, help="""The password for traefik api login"""
)
+ provider_name = Unicode(
+ config=True, help="""The provider name that Traefik expects, e.g. file, consul, etcd"""
+ )
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ if kwargs.get('debug', self.debug) == True:
+ import sys, logging
+ # Check we don't already have a StreamHandler
+ addHandler = True
+ for handler in self.log.handlers:
+ if isinstance(handler, logging.StreamHandler):
+ addHandler = False
+ if addHandler:
+ self.log.setLevel("DEBUG")
+ handler = logging.StreamHandler(sys.stdout)
+ handler.setLevel("DEBUG")
+ self.log.addHandler(handler)
+ self.log.debug(f"Initialising {type(self).__name__}")
+
+ #if kwargs.get('debug', self.debug) is True:
+
@default("traefik_api_password")
def _warn_empty_password(self):
self.log.warning("Traefik API password was not set.")
@@ -100,48 +124,47 @@ def _warn_empty_username(self):
)
static_config = Dict()
+ dynamic_config = Dict()
def _generate_htpassword(self):
- from passlib.apache import HtpasswdFile
+ from passlib.hash import apr_md5_crypt
+ self.traefik_api_hashed_password = apr_md5_crypt.hash(self.traefik_api_password)
- ht = HtpasswdFile()
- ht.set_password(self.traefik_api_username, self.traefik_api_password)
- self.traefik_api_hashed_password = str(ht.to_string()).split(":")[1][:-3]
-
- async def _check_for_traefik_endpoint(self, routespec, kind, provider):
- """Check for an expected frontend or backend
+ async def _check_for_traefik_service(self, routespec, kind):
+ """Check for an expected router or service in the Traefik API.
This is used to wait for traefik to load configuration
from a provider
"""
- expected = traefik_utils.generate_alias(routespec, kind)
- path = "/api/providers/{}/{}s".format(provider, kind)
+ # expected e.g. 'service' + '_' + routespec @ file
+ expected = traefik_utils.generate_alias(routespec, kind) + "@" + self.provider_name
+ path = f"/api/http/{kind}s"
try:
resp = await self._traefik_api_request(path)
- data = json.loads(resp.body)
+ json_data = json.loads(resp.body)
except Exception:
- self.log.exception("Error checking traefik api for %s %s", kind, routespec)
+ self.log.exception(f"Error checking traefik api for {kind} {routespec}")
return False
- if expected not in data:
- self.log.debug("traefik %s not yet in %ss", expected, kind)
- self.log.debug("Current traefik %ss: %s", kind, data)
+ service_names = [service['name'] for service in json_data]
+ if expected not in service_names:
+ self.log.debug(f"traefik {expected} not yet in {kind}")
return False
# found the expected endpoint
return True
- async def _wait_for_route(self, routespec, provider):
- self.log.info("Waiting for %s to register with traefik", routespec)
+ async def _wait_for_route(self, routespec):
+ self.log.info(f"Waiting for {routespec} to register with traefik")
async def _check_traefik_dynamic_conf_ready():
"""Check if traefik loaded its dynamic configuration yet"""
- if not await self._check_for_traefik_endpoint(
- routespec, "backend", provider
+ if not await self._check_for_traefik_service(
+ routespec, "service"
):
return False
- if not await self._check_for_traefik_endpoint(
- routespec, "frontend", provider
+ if not await self._check_for_traefik_service(
+ routespec, "router"
):
return False
@@ -149,7 +172,7 @@ async def _check_traefik_dynamic_conf_ready():
await exponential_backoff(
_check_traefik_dynamic_conf_ready,
- "Traefik route for %s configuration not available" % routespec,
+ f"Traefik route for {routespec} configuration not available",
timeout=self.check_route_timeout,
)
@@ -167,13 +190,16 @@ async def _traefik_api_request(self, path):
self.log.warning("%s GET %s", resp.code, url)
else:
self.log.debug("%s GET %s", resp.code, url)
+
+ self.log.debug(f"Succesfully received data from {path}: {resp.body}")
return resp
- async def _wait_for_static_config(self, provider):
+ async def _wait_for_static_config(self, provider=None):
+ # TODO: Remove provider argument after refactoring kv_proxy and subclasses
async def _check_traefik_static_conf_ready():
"""Check if traefik loaded its static configuration yet"""
try:
- resp = await self._traefik_api_request("/api/providers/" + provider)
+ resp = await self._traefik_api_request("/api/overview")
except Exception:
self.log.exception("Error checking for traefik static configuration")
return False
@@ -195,58 +221,122 @@ async def _check_traefik_static_conf_ready():
def _stop_traefik(self):
self.log.info("Cleaning up proxy[%i]...", self.traefik_process.pid)
- self.traefik_process.kill()
- self.traefik_process.wait()
-
- def _launch_traefik(self, config_type):
- if config_type == "toml" or config_type == "etcdv3" or config_type == "consul":
- config_file_path = abspath(join(dirname(__file__), "traefik.toml"))
- self.traefik_process = Popen(
- ["traefik", "-c", config_file_path], stdout=None
- )
- else:
+ self.traefik_process.terminate()
+ try:
+ self.traefik_process.communicate(timeout=10)
+ except TimeoutExpired:
+ self.traefik_process.kill()
+ self.traefik_process.communicate()
+ finally:
+ self.traefik_process.wait()
+
+ def _launch_traefik(self, config_type=None):
+ # Keep the _launch_traefik API backwards-compatible, while otherwise
+ # getting the provider from self.provider_name
+ # TODO: Make the breaking change!
+ if config_type is None:
+ config_type = self.provider_name
+ if config_type not in ("file", "etcdv3", "consul"):
raise ValueError(
"Configuration mode not supported \n.\
- The proxy can only be configured through toml, etcd and consul"
+ The proxy can only be configured through fileprovider, etcd and consul"
+ )
+ try:
+ self.traefik_process = Popen([
+ "traefik", "--configfile", abspath(self.static_config_file)
+ ])
+ except FileNotFoundError as e:
+ self.log.error(
+ "Failed to find traefik \n"
+ "The proxy can be downloaded from https://github.com/containous/traefik/releases/download."
)
+ raise
+ except Exception as e:
+ self.log.exception(e)
+ raise
async def _setup_traefik_static_config(self):
+ """When should_start=True, we are in control of traefik's static configuration
+ file. This sets up the entrypoints and api handler in self.static_config, and
+ then saves it to :attrib:`self.static_config_file`.
+
+ Subclasses should specify any traefik providers themselves, in
+ :attrib:`self.static_config["providers"]`
+ """
self.log.info("Setting up traefik's static config...")
- self._generate_htpassword()
- self.static_config = {}
- self.static_config["debug"] = True
- self.static_config["logLevel"] = self.traefik_log_level
+ self.static_config["log"] = { "level": self.traefik_log_level }
+
entryPoints = {}
if self.ssl_cert and self.ssl_key:
- self.static_config["defaultentrypoints"] = ["https"]
- entryPoints["https"] = {
+ entryPoints["websecure"] = {
"address": ":" + str(urlparse(self.public_url).port),
- "tls": {
- "certificates": [
- {"certFile": self.ssl_cert, "keyFile": self.ssl_key}
- ]
- },
+ "tls": {},
}
else:
- self.static_config["defaultentrypoints"] = ["http"]
- entryPoints["http"] = {"address": ":" + str(urlparse(self.public_url).port)}
-
- auth = {
- "basic": {
- "users": [
- self.traefik_api_username + ":" + self.traefik_api_hashed_password
- ]
- }
- }
- entryPoints["auth_api"] = {
+ entryPoints["web"] = {"address": ":" + str(urlparse(self.public_url).port)}
+
+ entryPoints["enter_api"] = {
"address": ":" + str(urlparse(self.traefik_api_url).port),
- "auth": auth,
}
self.static_config["entryPoints"] = entryPoints
- self.static_config["api"] = {"dashboard": True, "entrypoint": "auth_api"}
- self.static_config["wss"] = {"protocol": "http"}
+ self.static_config["api"] = {"dashboard": True}
+
+ handler = traefik_utils.TraefikConfigFileHandler(self.static_config_file)
+ try:
+ self.log.debug(f"Persisting the static config: {self.static_config}")
+ handler.atomic_dump(self.static_config)
+ except IOError:
+ self.log.exception("Couldn't set up traefik's static config.")
+ raise
+ except:
+ self.log.error("Couldn't set up traefik's static config. Unexpected error:")
+ raise
+
+ async def _setup_traefik_dynamic_config(self):
+ self.log.info("Setting up traefik's dynamic config...")
+ self._generate_htpassword()
+ api_url = urlparse(self.traefik_api_url)
+ api_path = api_url.path if api_url.path else "/api"
+ api_credentials = f"{self.traefik_api_username}:" \
+ f"{self.traefik_api_hashed_password}"
+ self.dynamic_config.update({
+ "http": {
+ "routers": {
+ "route_api": {
+ "rule": f"Host(`{api_url.hostname}`) && " \
+ f"(PathPrefix(`{api_path}`) || PathPrefix(`/dashboard`))",
+ "entryPoints": ["enter_api"],
+ "service": "api@internal",
+ "middlewares": ["auth_api"]
+ },
+ },
+ "middlewares": {
+ "auth_api": {
+ "basicAuth": {
+ "users": [
+ api_credentials
+ ]
+ }
+ }
+ }
+ }
+ })
+ if self.ssl_cert and self.ssl_key:
+ self.dynamic_config.update({
+ "tls": {
+ "stores": {
+ "default": {
+ "defaultCertificate": {
+ "certFile": self.ssl_cert,
+ "keyFile": self.ssl_key
+ }
+ }
+ }
+ }
+ })
+
def _routespec_to_traefik_path(self, routespec):
path = self.validate_routespec(routespec)
@@ -268,6 +358,7 @@ async def start(self):
if the proxy is to be started by the Hub
"""
await self._setup_traefik_static_config()
+ await self._setup_traefik_dynamic_config()
self._start_traefik()
async def stop(self):
@@ -345,3 +436,14 @@ async def get_route(self, routespec):
None: if there are no routes matching the given routespec
"""
raise NotImplementedError()
+
+ async def persist_dynamic_config(self):
+ """Update the Traefik dynamic configuration, depending on the backend
+ provider in use. This is used to e.g. set up the api endpoint's
+ authentication (username and password), as well as default tls
+ certificates to use.
+
+ :arg:`settings` is a Dict containing the traefik settings, which will
+ be updated on the Traefik provider depending on the subclass in use.
+ """
+ raise NotImplementedError()
diff --git a/jupyterhub_traefik_proxy/toml.py b/jupyterhub_traefik_proxy/toml.py
index e20e8a38..856b670b 100644
--- a/jupyterhub_traefik_proxy/toml.py
+++ b/jupyterhub_traefik_proxy/toml.py
@@ -18,21 +18,18 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
-import json
import os
import asyncio
-import string
import escapism
-from traitlets import Any, default, Unicode
+from traitlets import Any, default, Unicode, observe
from . import traefik_utils
-from jupyterhub.proxy import Proxy
from jupyterhub_traefik_proxy import TraefikProxy
-class TraefikTomlProxy(TraefikProxy):
- """JupyterHub Proxy implementation using traefik and toml config file"""
+class TraefikFileProxy(TraefikProxy):
+ """JupyterHub Proxy implementation using traefik and toml or yaml config file"""
mutex = Any()
@@ -40,49 +37,63 @@ class TraefikTomlProxy(TraefikProxy):
def _default_mutex(self):
return asyncio.Lock()
- toml_dynamic_config_file = Unicode(
+ @default("provider_name")
+ def _provider_name(self):
+ return "file"
+
+ dynamic_config_file = Unicode(
"rules.toml", config=True, help="""traefik's dynamic configuration file"""
)
+ dynamic_config_handler = Any()
+
+ @default("dynamic_config_handler")
+ def _default_handler(self):
+ return traefik_utils.TraefikConfigFileHandler(self.dynamic_config_file)
+
+ # If dynamic_config_file is changed, then update the dynamic config file handler
+ @observe("dynamic_config_file")
+ def _set_dynamic_config_file(self, change):
+ self.dynamic_config_handler = traefik_utils.TraefikConfigFileHandler(self.dynamic_config_file)
+
def __init__(self, **kwargs):
super().__init__(**kwargs)
try:
- # Load initial routing table from disk
- self.routes_cache = traefik_utils.load_routes(self.toml_dynamic_config_file)
+ # Load initial dynamic config from disk
+ self.dynamic_config = self.dynamic_config_handler.load()
except FileNotFoundError:
- self.routes_cache = {}
+ self.dynamic_config = {}
- if not self.routes_cache:
- self.routes_cache = {"backends": {}, "frontends": {}}
+ if not self.dynamic_config:
+ self.dynamic_config = {
+ "http" : {"services": {}, "routers": {}},
+ "jupyter": {"routers" : {} }
+ }
- async def _setup_traefik_static_config(self):
- await super()._setup_traefik_static_config()
+ def persist_dynamic_config(self):
+ """Save the dynamic config file with the current dynamic_config"""
+ self.dynamic_config_handler.atomic_dump(self.dynamic_config)
- self.static_config["file"] = {"filename": "rules.toml", "watch": True}
+ async def _setup_traefik_dynamic_config(self):
+ await super()._setup_traefik_dynamic_config()
+ self.log.info(
+ f"Creating the dynamic configuration file: {self.dynamic_config_file}"
+ )
+ self.persist_dynamic_config()
- try:
- traefik_utils.persist_static_conf(
- self.toml_static_config_file, self.static_config
- )
- try:
- os.stat(self.toml_dynamic_config_file)
- except FileNotFoundError:
- # Make sure that the dynamic configuration file exists
- self.log.info(
- f"Creating the toml dynamic configuration file: {self.toml_dynamic_config_file}"
- )
- open(self.toml_dynamic_config_file, "a").close()
- except IOError:
- self.log.exception("Couldn't set up traefik's static config.")
- raise
- except:
- self.log.error("Couldn't set up traefik's static config. Unexpected error:")
- raise
+ async def _setup_traefik_static_config(self):
+ self.static_config["providers"] = {
+ "file" : {
+ "filename": self.dynamic_config_file,
+ "watch": True
+ }
+ }
+ await super()._setup_traefik_static_config()
def _start_traefik(self):
self.log.info("Starting traefik...")
try:
- self._launch_traefik(config_type="toml")
+ self._launch_traefik()
except FileNotFoundError as e:
self.log.error(
"Failed to find traefik \n"
@@ -93,24 +104,24 @@ def _start_traefik(self):
def _clean_resources(self):
try:
if self.should_start:
- os.remove(self.toml_static_config_file)
- os.remove(self.toml_dynamic_config_file)
+ os.remove(self.static_config_file)
+ os.remove(self.dynamic_config_file)
except:
self.log.error("Failed to remove traefik's configuration files")
raise
def _get_route_unsafe(self, traefik_routespec):
- backend_alias = traefik_utils.generate_alias(traefik_routespec, "backend")
- frontend_alias = traefik_utils.generate_alias(traefik_routespec, "frontend")
+ service_alias = traefik_utils.generate_alias(traefik_routespec, "service")
+ router_alias = traefik_utils.generate_alias(traefik_routespec, "router")
routespec = self._routespec_from_traefik_path(traefik_routespec)
- result = {"data": "", "target": "", "routespec": routespec}
+ result = {"data": None, "target": None, "routespec": routespec}
def get_target_data(d, to_find):
if to_find == "url":
key = "target"
else:
key = to_find
- if result[key]:
+ if result[key] is not None:
return
for k, v in d.items():
if k == to_find:
@@ -118,17 +129,19 @@ def get_target_data(d, to_find):
if isinstance(v, dict):
get_target_data(v, to_find)
- if backend_alias in self.routes_cache["backends"]:
- get_target_data(self.routes_cache["backends"][backend_alias], "url")
+ service_node = self.dynamic_config["http"]["services"].get(service_alias, None)
+ if service_node is not None:
+ get_target_data(service_node, "url")
- if frontend_alias in self.routes_cache["frontends"]:
- get_target_data(self.routes_cache["frontends"][frontend_alias], "data")
+ jupyter_routers = self.dynamic_config["jupyter"]["routers"].get(router_alias, None)
+ if jupyter_routers is not None:
+ get_target_data(jupyter_routers, "data")
- if not result["data"] and not result["target"]:
- self.log.info("No route for {} found!".format(routespec))
+ if result["data"] is None and result["target"] is None:
+ self.log.info(f"No route for {routespec} found!")
result = None
- else:
- result["data"] = json.loads(result["data"])
+ self.log.debug(f"traefik routespec: {traefik_routespec}")
+ self.log.debug(f"result for routespec {routespec}:-\n{result}")
return result
async def start(self):
@@ -140,7 +153,7 @@ async def start(self):
if the proxy is to be started by the Hub
"""
await super().start()
- await self._wait_for_static_config(provider="file")
+ await self._wait_for_static_config()
async def stop(self):
"""Stop the proxy.
@@ -164,6 +177,8 @@ async def add_route(self, routespec, target, data):
target (str): A full URL that will be the target of this route.
data (dict): A JSONable dict that will be associated with this route, and will
be returned when retrieving information about this route.
+ FIXME: Why do we need to pass data back and forth to traefik?
+ Traefik v2 doesn't seem to allow a data key...
Will raise an appropriate Exception (FIXME: find what?) if the route could
not be added.
@@ -171,25 +186,50 @@ async def add_route(self, routespec, target, data):
The proxy implementation should also have a way to associate the fact that a
route came from JupyterHub.
"""
- routespec = self._routespec_to_traefik_path(routespec)
- backend_alias = traefik_utils.generate_alias(routespec, "backend")
- frontend_alias = traefik_utils.generate_alias(routespec, "frontend")
- data = json.dumps(data)
- rule = traefik_utils.generate_rule(routespec)
+ self.log.debug(f"\tTraefikFileProxy.add_route: Adding {routespec} for {target}")
+ traefik_routespec = self._routespec_to_traefik_path(routespec)
+ service_alias = traefik_utils.generate_alias(traefik_routespec, "service")
+ router_alias = traefik_utils.generate_alias(traefik_routespec, "router")
+ rule = traefik_utils.generate_rule(traefik_routespec)
async with self.mutex:
- self.routes_cache["frontends"][frontend_alias] = {
- "backend": backend_alias,
- "passHostHeader": True,
- "routes": {"test": {"rule": rule, "data": data}},
+ # If we've emptied the http and/or routers section, create it.
+ if "http" not in self.dynamic_config:
+ self.dynamic_config["http"] = {
+ "routers": {},
+ }
+ self.dynamic_config["jupyter"] = {
+ "routers": {}
+ }
+
+ elif "routers" not in self.dynamic_config["http"]:
+ self.dynamic_config["http"]["routers"] = {}
+ self.dynamic_config["jupyter"]["routers"] = {}
+
+ # Is it worth querying the api for all entrypoints?
+ # Otherwise we just bind to all of them ...
+ self.dynamic_config["http"]["routers"][router_alias] = {
+ "service": service_alias,
+ "rule": rule,
}
+ # Add the data node to a separate top-level node, so traefik doesn't complain.
+ self.dynamic_config["jupyter"]["routers"][router_alias] = {
+ "data": data
+ }
+
+ if "services" not in self.dynamic_config["http"]:
+ self.dynamic_config["http"]["services"] = {}
- self.routes_cache["backends"][backend_alias] = {
- "servers": {"server1": {"url": target, "weight": 1}}
+ self.dynamic_config["http"]["services"][service_alias] = {
+ "loadBalancer": {
+ "servers": {"server1": {"url": target} },
+ "passHostHeader": True
+ }
}
- traefik_utils.persist_routes(
- self.toml_dynamic_config_file, self.routes_cache
- )
+ self.persist_dynamic_config()
+
+ self.log.debug(f"traefik routespec: {traefik_routespec}")
+ self.log.debug(f"data for routespec {routespec}:-\n{data}")
if self.should_start:
try:
@@ -201,10 +241,10 @@ async def add_route(self, routespec, target, data):
)
raise
try:
- await self._wait_for_route(routespec, provider="file")
+ await self._wait_for_route(traefik_routespec)
except TimeoutError:
self.log.error(
- f"Is Traefik configured to watch {self.toml_dynamic_config_file}?"
+ f"Is Traefik configured to watch {self.dynamic_config_file}?"
)
raise
@@ -214,14 +254,26 @@ async def delete_route(self, routespec):
**Subclasses must define this method**
"""
routespec = self._routespec_to_traefik_path(routespec)
- backend_alias = traefik_utils.generate_alias(routespec, "backend")
- frontend_alias = traefik_utils.generate_alias(routespec, "frontend")
+ service_alias = traefik_utils.generate_alias(routespec, "service")
+ router_alias = traefik_utils.generate_alias(routespec, "router")
async with self.mutex:
- self.routes_cache["frontends"].pop(frontend_alias, None)
- self.routes_cache["backends"].pop(backend_alias, None)
-
- traefik_utils.persist_routes(self.toml_dynamic_config_file, self.routes_cache)
+
+ self.dynamic_config["http"]["routers"].pop(router_alias, None)
+ self.dynamic_config["http"]["services"].pop(service_alias, None)
+ self.dynamic_config["jupyter"]["routers"].pop(router_alias, None)
+
+ # If empty, delete the keys
+ if not self.dynamic_config["http"]["routers"]:
+ self.dynamic_config["http"].pop("routers")
+ if not self.dynamic_config["http"]["services"]:
+ self.dynamic_config["http"].pop("services")
+ if not self.dynamic_config["http"]:
+ self.dynamic_config.pop("http")
+ if not self.dynamic_config["jupyter"]["routers"]:
+ self.dynamic_config["jupyter"].pop("routers")
+
+ self.persist_dynamic_config()
async def get_all_routes(self):
"""Fetch and return all the routes associated by JupyterHub from the
@@ -241,11 +293,16 @@ async def get_all_routes(self):
all_routes = {}
async with self.mutex:
- for key, value in self.routes_cache["frontends"].items():
- escaped_routespec = "".join(key.split("_", 1)[1:])
+ for router, value in self.dynamic_config["http"]["routers"].items():
+ if router not in self.dynamic_config["jupyter"]["routers"]:
+ # Only check routers defined in jupyter node
+ continue
+ escaped_routespec = "".join(router.split("_", 1)[1:])
traefik_routespec = escapism.unescape(escaped_routespec)
routespec = self._routespec_from_traefik_path(traefik_routespec)
- all_routes[routespec] = self._get_route_unsafe(traefik_routespec)
+ all_routes.update({
+ routespec : self._get_route_unsafe(traefik_routespec)
+ })
return all_routes
@@ -272,3 +329,4 @@ async def get_route(self, routespec):
routespec = self._routespec_to_traefik_path(routespec)
async with self.mutex:
return self._get_route_unsafe(routespec)
+
diff --git a/jupyterhub_traefik_proxy/traefik_utils.py b/jupyterhub_traefik_proxy/traefik_utils.py
index 76484f3e..cdab12bc 100644
--- a/jupyterhub_traefik_proxy/traefik_utils.py
+++ b/jupyterhub_traefik_proxy/traefik_utils.py
@@ -1,39 +1,22 @@
import os
import string
from tempfile import NamedTemporaryFile
-from traitlets import Unicode
from urllib.parse import unquote
import escapism
-import toml
from contextlib import contextmanager
-from collections import namedtuple
-
-
-class KVStorePrefix(Unicode):
- def validate(self, obj, value):
- u = super().validate(obj, value)
- if not u.endswith("/"):
- u = u + "/"
-
- proxy_class = type(obj).__name__
- if "Consul" in proxy_class and u.startswith("/"):
- u = u[1:]
-
- return u
def generate_rule(routespec):
routespec = unquote(routespec)
if routespec.startswith("/"):
# Path-based route, e.g. /proxy/path/
- rule = "PathPrefix:" + routespec
+ rule = f"PathPrefix(`{routespec}`)"
else:
# Host-based routing, e.g. host.tld/proxy/path/
host, path_prefix = routespec.split("/", 1)
- path_prefix = "/" + path_prefix
- rule = "Host:" + host + ";PathPrefix:" + path_prefix
+ rule = f"Host(`{host}`) && PathPrefix(`/{path_prefix}`)"
return rule
@@ -42,77 +25,39 @@ def generate_alias(routespec, server_type=""):
return server_type + "_" + escapism.escape(routespec, safe=safe)
-def generate_backend_entry(
- proxy, backend_alias, separator="/", url=False, weight=False
-):
- backend_entry = ""
+def generate_service_entry( proxy, service_alias, separator="/", url=False):
+ service_entry = separator.join(
+ ["http", "services", service_alias, "loadBalancer", "servers", "server1"]
+ )
if separator == "/":
- backend_entry = proxy.kv_traefik_prefix
- backend_entry += separator.join(["backends", backend_alias, "servers", "server1"])
+ service_entry = proxy.kv_traefik_prefix + separator + service_entry
if url:
- backend_entry += separator + "url"
- elif weight:
- backend_entry += separator + "weight"
-
- return backend_entry
+ service_entry += separator + "url"
+ return service_entry
-
-def generate_frontend_backend_entry(proxy, frontend_alias):
- return proxy.kv_traefik_prefix + "frontends/" + frontend_alias + "/backend"
-
-
-def generate_frontend_rule_entry(proxy, frontend_alias, separator="/"):
- frontend_rule_entry = separator.join(
- ["frontends", frontend_alias, "routes", "test"]
+def generate_service_weight_entry( proxy, service_alias, separator="/"):
+ return separator.join(
+ [proxy.kv_traefik_prefix, "http", "services", service_alias,
+ "weighted", "services", "0", "weight"]
)
- if separator == "/":
- frontend_rule_entry = (
- proxy.kv_traefik_prefix + frontend_rule_entry + separator + "rule"
- )
- return frontend_rule_entry
+def generate_router_service_entry(proxy, router_alias):
+ return "/".join(
+ [proxy.kv_traefik_prefix, "http", "routers", router_alias, "service"]
+ )
-def generate_route_keys(proxy, routespec, separator="/"):
- backend_alias = generate_alias(routespec, "backend")
- frontend_alias = generate_alias(routespec, "frontend")
- RouteKeys = namedtuple(
- "RouteKeys",
- [
- "backend_alias",
- "backend_url_path",
- "backend_weight_path",
- "frontend_alias",
- "frontend_backend_path",
- "frontend_rule_path",
- ],
+def generate_router_rule_entry(proxy, router_alias, separator="/"):
+ router_rule_entry = separator.join(
+ ["http", "routers", router_alias]
)
-
- if separator != ".":
- backend_url_path = generate_backend_entry(proxy, backend_alias, url=True)
- frontend_rule_path = generate_frontend_rule_entry(proxy, frontend_alias)
- backend_weight_path = generate_backend_entry(proxy, backend_alias, weight=True)
- frontend_backend_path = generate_frontend_backend_entry(proxy, frontend_alias)
- else:
- backend_url_path = generate_backend_entry(
- proxy, backend_alias, separator=separator
- )
- frontend_rule_path = generate_frontend_rule_entry(
- proxy, frontend_alias, separator=separator
+ if separator == "/":
+ router_rule_entry = separator.join(
+ [proxy.kv_traefik_prefix, router_rule_entry, "rule"]
)
- backend_weight_path = ""
- frontend_backend_path = ""
-
- return RouteKeys(
- backend_alias,
- backend_url_path,
- backend_weight_path,
- frontend_alias,
- frontend_backend_path,
- frontend_rule_path,
- )
+ return router_rule_entry
# atomic writing adapted from jupyter/notebook 5.7
# unlike atomic writing there, which writes the canonical path
@@ -142,20 +87,38 @@ def atomic_writing(path):
# already deleted by os.replace above
pass
+class TraefikConfigFileHandler(object):
+ """Handles reading and writing Traefik config files. Can operate
+ on both toml and yaml files"""
+ def __init__(self, file_path):
+ file_ext = file_path.rsplit('.', 1)[-1]
+ if file_ext == 'yaml':
+ from ruamel.yaml import YAML
+ config_handler = YAML(typ="safe")
+ elif file_ext == 'toml':
+ import toml as config_handler
+ else:
+ raise TypeError("type should be either 'toml' or 'yaml'")
+
+ self.file_path = file_path
+ # Redefined to either yaml.dump or toml.dump
+ self._dump = config_handler.dump
+ #self._dumps = config_handler.dumps
+ # Redefined by __init__, to either yaml.load or toml.load
+ self._load = config_handler.load
+
+ def load(self):
+ """Depending on self.file_path, call either yaml.load or toml.load"""
+ with open(self.file_path, "r") as fd:
+ return self._load(fd)
+
+ def dump(self, data):
+ with open(self.file_path, "w") as f:
+ self._dump(data, f)
+
+ def atomic_dump(self, data):
+ """Save data to self.file_path after opening self.file_path with
+ :func:`atomic_writing`"""
+ with atomic_writing(self.file_path) as f:
+ self._dump(data, f)
-def persist_static_conf(file, static_conf_dict):
- with open(file, "w") as f:
- toml.dump(static_conf_dict, f)
-
-
-def persist_routes(file, routes_dict):
- with atomic_writing(file) as config_fd:
- toml.dump(routes_dict, config_fd)
-
-
-def load_routes(file):
- try:
- with open(file, "r") as config_fd:
- return toml.load(config_fd)
- except:
- raise
diff --git a/requirements.txt b/requirements.txt
index ccb6dc38..b9540e50 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,4 +2,6 @@ aiohttp
escapism
jupyterhub>=0.9
passlib
-toml
+# toml is now optional, as can use yaml configuration files instead now...
+#toml
+#ruamel.yaml
diff --git a/setup.py b/setup.py
index a4a48cc1..cc5bef5b 100644
--- a/setup.py
+++ b/setup.py
@@ -73,7 +73,7 @@ def run(self):
cmdclass=cmdclass,
entry_points={
"jupyterhub.proxies": [
- "traefik_toml = jupyterhub_traefik_proxy:TraefikTomlProxy",
+ "traefik_file = jupyterhub_traefik_proxy:TraefikFileProxy",
]
},
)
diff --git a/tests/config_files/dynamic_config/dynamic_conf.toml b/tests/config_files/dynamic_config/dynamic_conf.toml
new file mode 100644
index 00000000..e309690e
--- /dev/null
+++ b/tests/config_files/dynamic_config/dynamic_conf.toml
@@ -0,0 +1,11 @@
+# Example dynamic configuration file for an external file provider proxy.
+# Defines the API listener and its authentication
+
+[http.routers.router-api]
+ rule = "Host(`localhost`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
+ entryPoints = ["enter_api"]
+ service = "api@internal"
+ middlewares = ["auth_api"]
+
+[http.middlewares.auth_api.basicAuth]
+ users = [ "api_admin:$apr1$eS/j3kum$q/X2khsIEG/bBGsteP.x./",]
diff --git a/tests/config_files/rules.toml b/tests/config_files/rules.toml
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/config_files/traefik.toml b/tests/config_files/traefik.toml
index 93013389..5f57109c 100644
--- a/tests/config_files/traefik.toml
+++ b/tests/config_files/traefik.toml
@@ -1,23 +1,20 @@
-defaultentrypoints = [ "http",]
-debug = true
-logLevel = "ERROR"
+[log]
+ level = "debug"
[api]
-dashboard = true
-entrypoint = "auth_api"
+ dashboard = true
[wss]
-protocol = "http"
+ protocol = "http"
-[file]
-filename = "./tests/config_files/rules.toml"
-watch = true
+[providers.file]
+ directory = "./tests/config_files/dynamic_config"
+ watch = true
-[entryPoints.http]
-address = "127.0.0.1:8000"
+[entryPoints]
+ [entryPoints.my_web_api]
+ address = "127.0.0.1:8000"
-[entryPoints.auth_api]
-address = "127.0.0.1:8099"
+ [entryPoints.enter_api]
+ address = "127.0.0.1:8099"
-[entryPoints.auth_api.auth.basic]
-users = [ "api_admin:$apr1$eS/j3kum$q/X2khsIEG/bBGsteP.x./",]
diff --git a/tests/conftest.py b/tests/conftest.py
index e1ca34e5..a690a964 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -9,7 +9,7 @@
import pytest
from _pytest.mark import Mark
-from jupyterhub_traefik_proxy import TraefikTomlProxy
+from jupyterhub_traefik_proxy import TraefikFileProxy
# Define a "slow" test marker so that we can run the slow tests at the end
@@ -36,14 +36,40 @@ def pytest_configure(config):
@pytest.fixture
+# There must be a way to parameterise this to run on both yaml and toml files?
async def toml_proxy():
- """Fixture returning a configured TraefikTomlProxy"""
- proxy = TraefikTomlProxy(
+ """Fixture returning a configured TraefikFileProxy"""
+ dynamic_config_file = os.path.join(
+ os.getcwd(), "tests", "config_files", "dynamic_config", "rules.toml"
+ )
+ proxy = TraefikFileProxy(
+ public_url="http://127.0.0.1:8000",
+ traefik_api_password="admin",
+ traefik_api_username="api_admin",
+ check_route_timeout=180,
+ should_start=True,
+ dynamic_config_file=dynamic_config_file,
+ static_config_file="traefik.toml"
+ )
+
+ await proxy.start()
+ yield proxy
+ await proxy.stop()
+
+
+@pytest.fixture
+async def yaml_proxy():
+ dynamic_config_file = os.path.join(
+ os.getcwd(), "tests", "config_files", "dynamic_config", "rules.yaml"
+ )
+ proxy = TraefikFileProxy(
public_url="http://127.0.0.1:8000",
traefik_api_password="admin",
traefik_api_username="api_admin",
check_route_timeout=180,
should_start=True,
+ dynamic_config_file=dynamic_config_file,
+ static_config_file="traefik.yaml"
)
await proxy.start()
@@ -52,23 +78,49 @@ async def toml_proxy():
@pytest.fixture
-def external_toml_proxy():
- proxy = TraefikTomlProxy(
+async def external_toml_proxy(launch_traefik_file):
+ dynamic_config_file = os.path.join(
+ os.getcwd(), "tests", "config_files", "dynamic_config", "rules.toml"
+ )
+ proxy = TraefikFileProxy(
public_url="http://127.0.0.1:8000",
traefik_api_password="admin",
traefik_api_username="api_admin",
check_route_timeout=45,
+ should_start=False,
+ dynamic_config_file=dynamic_config_file,
)
- proxy.should_start = False
- proxy.toml_dynamic_config_file = "./tests/config_files/rules.toml"
- # Start traefik manually
- traefik_process = subprocess.Popen(
- ["traefik", "-c", "./tests/config_files/traefik.toml"], stdout=None
+
+ yield proxy
+ os.remove(dynamic_config_file)
+
+
+@pytest.fixture
+async def external_yaml_proxy(launch_traefik_file):
+ dynamic_config_file = os.path.join(
+ os.getcwd(), "tests", "config_files", "dynamic_config", "rules.yaml"
+ )
+ proxy = TraefikFileProxy(
+ public_url="http://127.0.0.1:8000",
+ traefik_api_password="admin",
+ traefik_api_username="api_admin",
+ check_route_timeout=180,
+ should_start=False,
+ dynamic_config_file=dynamic_config_file,
)
+
yield proxy
- open("./tests/config_files/rules.toml", "w").close()
- traefik_process.kill()
- traefik_process.wait()
+ os.remove(dynamic_config_file)
+
+
+@pytest.fixture
+def launch_traefik_file():
+ proc = subprocess.Popen(
+ ["traefik", "--configfile", "./tests/config_files/traefik.toml"]
+ )
+ yield proc
+ proc.kill()
+ proc.wait()
@pytest.fixture(scope="session", autouse=False)
diff --git a/tests/dummy_http_server.py b/tests/dummy_http_server.py
index 73941b9b..11f572a7 100644
--- a/tests/dummy_http_server.py
+++ b/tests/dummy_http_server.py
@@ -42,7 +42,7 @@ def run(port=80):
run(port=int(argv[1]))
else:
asyncio.get_event_loop().run_until_complete(
- websockets.serve(send_port, "localhost", int(argv[1]))
+ websockets.serve(send_port, "127.0.0.1", int(argv[1]))
)
asyncio.get_event_loop().run_forever()
else:
diff --git a/tests/proxytest.py b/tests/proxytest.py
index 90c8ab17..3e389704 100644
--- a/tests/proxytest.py
+++ b/tests/proxytest.py
@@ -89,6 +89,8 @@ def _launch_backend(port, proto="http"):
for proc in running_backends:
proc.kill()
+ for proc in running_backends:
+ proc.communicate()
for proc in running_backends:
proc.wait()
diff --git a/tests/test_installer.py b/tests/test_installer.py
index 149a4ad5..8e9ad1bd 100644
--- a/tests/test_installer.py
+++ b/tests/test_installer.py
@@ -21,29 +21,9 @@ def assert_deps_dir_empty(deps_dir):
assert not os.listdir(deps_dir)
-def assert_only_traefik_existence(deps_dir):
+def assert_traefik_existence(deps_dir):
traefik_bin = os.path.join(deps_dir, "traefik")
- etcd_bin = os.path.join(deps_dir, "etcd")
- etcdctl_bin = os.path.join(deps_dir, "etcdctl")
- consul_bin = os.path.join(deps_dir, "consul")
-
assert os.path.exists(traefik_bin)
- assert not os.path.exists(etcd_bin)
- assert not os.path.exists(etcdctl_bin)
- assert not os.path.exists(consul_bin)
-
-
-def assert_binaries_existence(deps_dir):
- traefik_bin = os.path.join(deps_dir, "traefik")
- etcd_bin = os.path.join(deps_dir, "etcd")
- etcdctl_bin = os.path.join(deps_dir, "etcdctl")
- consul_bin = os.path.join(deps_dir, "consul")
-
- assert os.path.exists(traefik_bin)
- assert os.path.exists(etcd_bin)
- assert os.path.exists(etcdctl_bin)
- assert os.path.exists(consul_bin)
-
def test_default_conf():
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -59,7 +39,7 @@ def test_install_only_traefik_default_version():
try:
subprocess.run([sys.executable, "-m", installer_module, "--traefik"])
- assert_only_traefik_existence(default_deps_dir)
+ assert_traefik_existence(default_deps_dir)
finally:
cleanup(default_deps_dir)
@@ -70,9 +50,9 @@ def test_install_all_binaries_default_version():
try:
subprocess.run(
- [sys.executable, "-m", installer_module, "--traefik", "--etcd", "--consul"]
+ [sys.executable, "-m", installer_module, "--traefik"]
)
- assert_binaries_existence(default_deps_dir)
+ assert_traefik_existence(default_deps_dir)
finally:
cleanup(default_deps_dir)
@@ -84,7 +64,7 @@ def test_output_arg_new_dir(tmpdir):
)
assert os.path.exists(deps_dir)
- assert_only_traefik_existence(deps_dir)
+ assert_traefik_existence(deps_dir)
def test_output_arg_existing_dir(tmpdir):
@@ -92,7 +72,7 @@ def test_output_arg_existing_dir(tmpdir):
subprocess.run(
[sys.executable, "-m", installer_module, "--traefik", f"--output={deps_dir}"]
)
- assert_only_traefik_existence(deps_dir)
+ assert_traefik_existence(deps_dir)
def test_version(tmpdir):
@@ -104,16 +84,12 @@ def test_version(tmpdir):
installer_module,
f"--output={deps_dir}",
"--traefik",
- "--traefik-version=1.7.0",
- "--etcd",
- "--etcd-version=3.2.25",
- "--consul",
- "--consul-version=1.5.0",
+ "--traefik-version=2.4.8",
]
)
assert os.path.exists(deps_dir)
- assert_binaries_existence(deps_dir)
+ assert_traefik_existence(deps_dir)
def test_linux_arm_platform(tmpdir):
@@ -130,7 +106,7 @@ def test_linux_arm_platform(tmpdir):
)
assert os.path.exists(deps_dir)
- assert_only_traefik_existence(deps_dir)
+ assert_traefik_existence(deps_dir)
def test_linux_amd64_platform(tmpdir):
@@ -147,7 +123,7 @@ def test_linux_amd64_platform(tmpdir):
)
assert os.path.exists(deps_dir)
- assert_only_traefik_existence(deps_dir)
+ assert_traefik_existence(deps_dir)
def test_mac_platform(tmpdir):
@@ -164,7 +140,7 @@ def test_mac_platform(tmpdir):
)
assert os.path.exists(deps_dir)
- assert_only_traefik_existence(deps_dir)
+ assert_traefik_existence(deps_dir)
def test_warning(tmpdir):
@@ -176,10 +152,10 @@ def test_warning(tmpdir):
installer_module,
f"--output={deps_dir}",
"--traefik",
- "--traefik-version=1.6.6",
+ "--traefik-version=2.4.1",
],
stderr=subprocess.STDOUT,
)
assert os.path.exists(deps_dir)
- assert_only_traefik_existence(deps_dir)
+ assert_traefik_existence(deps_dir)
assert output.decode().count("UserWarning") == 1
diff --git a/tests/test_proxy.py b/tests/test_proxy.py
index 6cf311e2..bf1428f2 100644
--- a/tests/test_proxy.py
+++ b/tests/test_proxy.py
@@ -11,7 +11,9 @@
@pytest.fixture(
params=[
"toml_proxy",
+ "yaml_proxy",
"external_toml_proxy",
+ "external_yaml_proxy",
]
)
def proxy(request):
diff --git a/tests/test_traefik_api_auth.py b/tests/test_traefik_api_auth.py
index edf8f50b..0199f5b0 100644
--- a/tests/test_traefik_api_auth.py
+++ b/tests/test_traefik_api_auth.py
@@ -13,6 +13,7 @@
@pytest.fixture(
params=[
"toml_proxy",
+ "yaml_proxy",
]
)
def proxy(request):
@@ -24,25 +25,43 @@ def proxy(request):
[("api_admin", "admin", 200), ("api_admin", "1234", 401), ("", "", 401)],
)
async def test_traefik_api_auth(proxy, username, password, expected_rc):
- traefik_port = urlparse(proxy.public_url).port
+ traefik_api_url = proxy.traefik_api_url + "/api"
- await exponential_backoff(
- utils.check_host_up, "Traefik not reacheable", ip="localhost", port=traefik_port
- )
+ # Must have a trailing slash!
+ dashboard_url = proxy.traefik_api_url + "/dashboard/"
+
+ # There is now a delay between traefik's public ports
+ # being reachable and the API being accessible. So, give traefik
+ # a chance to load its dynamic configuration and configure the
+ # API handler
+ async def api_login():
+ try:
+ if not username and not password:
+ resp = await AsyncHTTPClient().fetch(traefik_api_url)
+ else:
+ resp = await AsyncHTTPClient().fetch(
+ dashboard_url,
+ auth_username=username,
+ auth_password=password,
+ )
+ except ConnectionRefusedError:
+ rc = None
+ except Exception as e:
+ rc = e.response.code
+ else:
+ rc = resp.code
+ return rc
- try:
- if not username and not password:
- resp = await AsyncHTTPClient().fetch(proxy.traefik_api_url + "/dashboard")
+ async def cmp_api_login():
+ rc = await api_login()
+ if rc == expected_rc:
+ return True
else:
- resp = await AsyncHTTPClient().fetch(
- proxy.traefik_api_url + "/dashboard/",
- auth_username=username,
- auth_password=password,
- )
- rc = resp.code
- except ConnectionRefusedError:
- rc = None
- except Exception as e:
- rc = e.response.code
+ return False
+
+ await exponential_backoff(
+ cmp_api_login, "Traefik API not reachable"
+ )
+ rc = await api_login()
assert rc == expected_rc
diff --git a/tests/test_traefik_utils.py b/tests/test_traefik_utils.py
index edfd4013..05311cc8 100644
--- a/tests/test_traefik_utils.py
+++ b/tests/test_traefik_utils.py
@@ -28,8 +28,10 @@ def test_roundtrip_routes():
file = "test_roudtrip.toml"
open(file, "a").close()
- traefik_utils.persist_routes(file, routes)
- reloaded = traefik_utils.load_routes(file)
+ save_handler = traefik_utils.TraefikConfigFileHandler(file)
+ save_handler.atomic_dump(routes)
+ load_handler = traefik_utils.TraefikConfigFileHandler(file)
+ reloaded = load_handler.load()
os.remove(file)
assert reloaded == routes