mirror of
https://github.com/jupyterhub/the-littlest-jupyterhub.git
synced 2025-12-18 21:54:05 +08:00
Merge pull request #586 from GeorgianaElena/extend-traefik-rules
This commit is contained in:
@@ -8,11 +8,19 @@ Custom configuration snippets
|
|||||||
The two main TLJH components are **JupyterHub** and **Traefik**.
|
The two main TLJH components are **JupyterHub** and **Traefik**.
|
||||||
|
|
||||||
* JupyterHub takes its configuration from the ``jupyterhub_config.py`` file.
|
* JupyterHub takes its configuration from the ``jupyterhub_config.py`` file.
|
||||||
* Traefik takes its configuration from the ``traefik.toml`` file.
|
* Traefik loads its:
|
||||||
|
* `static configuration <https://docs.traefik.io/v1.7/basics/#static-traefik-configuration>`_
|
||||||
|
from the ``traefik.toml`` file.
|
||||||
|
* `dynamic configuration <https://docs.traefik.io/v1.7/basics/#dynamic-traefik-configuration>`_
|
||||||
|
from the ``rules`` directory.
|
||||||
|
|
||||||
These files are created by TLJH during installation and can be edited by the
|
The ``jupyterhub_config.py`` and ``traefik.toml`` files are created by TLJH during installation
|
||||||
user only through ``tljh-config``. Any direct modification to these files
|
and can be edited by the user only through ``tljh-config``. The ``rules`` directory is also created
|
||||||
is unsupported, and will cause hard to debug issues.
|
during install along with a ``rules/rules.toml`` file, to be used by JupyterHub to store the routing
|
||||||
|
table from users to their notebooks.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Any direct modification to these files is unsupported, and will cause hard to debug issues.
|
||||||
|
|
||||||
But because sometimes TLJH needs to be customized in ways that are not officially
|
But because sometimes TLJH needs to be customized in ways that are not officially
|
||||||
supported, an escape hatch has been introduced to allow easily extending the
|
supported, an escape hatch has been introduced to allow easily extending the
|
||||||
@@ -65,3 +73,22 @@ proxy for the new configuration to take effect:
|
|||||||
sudo tljh-config reload proxy
|
sudo tljh-config reload proxy
|
||||||
|
|
||||||
.. warning:: This instructions might change when TLJH will switch to Traefik > 2.0
|
.. warning:: This instructions might change when TLJH will switch to Traefik > 2.0
|
||||||
|
|
||||||
|
Extending ``rules.toml``
|
||||||
|
========================
|
||||||
|
|
||||||
|
``Traefik`` is configured to load its routing table from the ``/opt/tljh/state/rules``
|
||||||
|
directory. The existing ``rules.toml`` file inside this directory is used by
|
||||||
|
``jupyterhub-traefik-proxy`` to add the JupyterHub routes from users to their notebook servers
|
||||||
|
and shouldn't be modified.
|
||||||
|
|
||||||
|
However, the routing table can be extended outside JupyterHub's scope using the ``rules``
|
||||||
|
directory, by adding other dynamic configuration files with the desired routing rules.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
* Any files in ``/opt/tljh/state/rules`` that end in ``.toml`` will be hot reload by Traefik.
|
||||||
|
This means that there is no need to reload the proxy service for the rules to take effect.
|
||||||
|
|
||||||
|
Checkout Traefik' docs about `dynamic configuration <https://docs.traefik.io/v1.7/basics/#dynamic-traefik-configuration>`_
|
||||||
|
and how to provide dynamic configuration through
|
||||||
|
`multiple separated files <https://docs.traefik.io/v1.7/configuration/backends/file/#multiple-separated-files>`_.
|
||||||
|
|||||||
@@ -9,7 +9,37 @@ import toml
|
|||||||
from tornado.httpclient import HTTPClient, HTTPRequest, HTTPClientError
|
from tornado.httpclient import HTTPClient, HTTPRequest, HTTPClientError
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tljh.config import reload_component, set_config_value, CONFIG_FILE, CONFIG_DIR
|
from tljh.config import (
|
||||||
|
reload_component,
|
||||||
|
set_config_value,
|
||||||
|
unset_config_value,
|
||||||
|
CONFIG_FILE,
|
||||||
|
CONFIG_DIR,
|
||||||
|
STATE_DIR,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def send_request(url, max_sleep, validate_cert=True, username=None, password=None):
|
||||||
|
resp = None
|
||||||
|
for i in range(max_sleep):
|
||||||
|
time.sleep(i)
|
||||||
|
try:
|
||||||
|
req = HTTPRequest(
|
||||||
|
url,
|
||||||
|
method="GET",
|
||||||
|
auth_username=username,
|
||||||
|
auth_password=password,
|
||||||
|
validate_cert=validate_cert,
|
||||||
|
follow_redirects=True,
|
||||||
|
max_redirects=15,
|
||||||
|
)
|
||||||
|
resp = HTTPClient().fetch(req)
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
pass
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
def test_manual_https(preserve_config):
|
def test_manual_https(preserve_config):
|
||||||
@@ -55,32 +85,49 @@ def test_manual_https(preserve_config):
|
|||||||
# verify that our certificate was loaded by traefik
|
# verify that our certificate was loaded by traefik
|
||||||
assert server_cert == file_cert
|
assert server_cert == file_cert
|
||||||
|
|
||||||
for i in range(10):
|
|
||||||
time.sleep(i)
|
|
||||||
# verify that we can still connect to the hub
|
# verify that we can still connect to the hub
|
||||||
try:
|
resp = send_request(
|
||||||
req = HTTPRequest(
|
url="https://127.0.0.1/hub/api", max_sleep=10, validate_cert=False
|
||||||
"https://127.0.0.1/hub/api", method="GET", validate_cert=False
|
|
||||||
)
|
)
|
||||||
resp = HTTPClient().fetch(req)
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
assert resp.code == 200
|
assert resp.code == 200
|
||||||
|
|
||||||
|
|
||||||
# cleanup
|
# cleanup
|
||||||
shutil.rmtree(ssl_dir)
|
shutil.rmtree(ssl_dir)
|
||||||
|
set_config_value(CONFIG_FILE, "https.enabled", False)
|
||||||
|
|
||||||
|
reload_component("proxy")
|
||||||
|
|
||||||
|
|
||||||
def test_extra_traefik_config():
|
def test_extra_traefik_config():
|
||||||
extra_config_dir = os.path.join(CONFIG_DIR, "traefik_config.d")
|
extra_static_config_dir = os.path.join(CONFIG_DIR, "traefik_config.d")
|
||||||
os.makedirs(extra_config_dir, exist_ok=True)
|
os.makedirs(extra_static_config_dir, exist_ok=True)
|
||||||
|
|
||||||
extra_config = {
|
dynamic_config_dir = os.path.join(STATE_DIR, "rules")
|
||||||
|
os.makedirs(dynamic_config_dir, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
extra_static_config = {
|
||||||
"entryPoints": {"no_auth_api": {"address": "127.0.0.1:9999"}},
|
"entryPoints": {"no_auth_api": {"address": "127.0.0.1:9999"}},
|
||||||
"api": {"dashboard": True, "entrypoint": "no_auth_api"},
|
"api": {"dashboard": True, "entrypoint": "no_auth_api"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extra_dynamic_config = {
|
||||||
|
"frontends": {
|
||||||
|
"test": {
|
||||||
|
"backend": "test",
|
||||||
|
"routes": {
|
||||||
|
"rule1": {"rule": "PathPrefixStrip: /the/hub/runs/here/too"}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"backends": {
|
||||||
|
# redirect to hub
|
||||||
|
"test": {"servers": {"server1": {"url": "http://127.0.0.1:15001"}}}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
success = False
|
success = False
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
time.sleep(i)
|
time.sleep(i)
|
||||||
@@ -96,23 +143,31 @@ def test_extra_traefik_config():
|
|||||||
|
|
||||||
assert success == True
|
assert success == True
|
||||||
|
|
||||||
# Load the extra config
|
# write the extra static config
|
||||||
with open(os.path.join(extra_config_dir, "extra.toml"), "w+") as extra_config_file:
|
with open(
|
||||||
toml.dump(extra_config, extra_config_file)
|
os.path.join(extra_static_config_dir, "extra.toml"), "w+"
|
||||||
|
) as extra_config_file:
|
||||||
|
toml.dump(extra_static_config, extra_config_file)
|
||||||
|
|
||||||
|
# write the extra dynamic config
|
||||||
|
with open(
|
||||||
|
os.path.join(dynamic_config_dir, "extra_rules.toml"), "w+"
|
||||||
|
) as extra_config_file:
|
||||||
|
toml.dump(extra_dynamic_config, extra_config_file)
|
||||||
|
|
||||||
|
# load the extra config
|
||||||
reload_component("proxy")
|
reload_component("proxy")
|
||||||
|
|
||||||
for i in range(5):
|
# the new dashboard entrypoint shouldn't require authentication anymore
|
||||||
time.sleep(i)
|
resp = send_request(url="http://127.0.0.1:9999/dashboard/", max_sleep=5)
|
||||||
try:
|
|
||||||
# The new dashboard entrypoint shouldn't require authentication anymore
|
|
||||||
req = HTTPRequest("http://127.0.0.1:9999/dashboard/", method="GET")
|
|
||||||
resp = HTTPClient().fetch(req)
|
|
||||||
break
|
|
||||||
except ConnectionRefusedError:
|
|
||||||
pass
|
|
||||||
# If the request didn't get through after 5 tries, this should fail
|
|
||||||
assert resp.code == 200
|
assert resp.code == 200
|
||||||
|
|
||||||
|
# test extra dynamic config
|
||||||
|
resp = send_request(url="http://127.0.0.1/the/hub/runs/here/too", max_sleep=5)
|
||||||
|
assert resp.code == 200
|
||||||
|
assert resp.effective_url == "http://127.0.0.1/hub/login"
|
||||||
|
|
||||||
# cleanup
|
# cleanup
|
||||||
os.remove(os.path.join(extra_config_dir, "extra.toml"))
|
os.remove(os.path.join(extra_static_config_dir, "extra.toml"))
|
||||||
reload_component("proxy")
|
os.remove(os.path.join(dynamic_config_dir, "extra_rules.toml"))
|
||||||
|
open(os.path.join(STATE_DIR, "traefik.toml"), "w").close()
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ c.JupyterHub.hub_port = 15001
|
|||||||
|
|
||||||
c.TraefikTomlProxy.should_start = False
|
c.TraefikTomlProxy.should_start = False
|
||||||
|
|
||||||
dynamic_conf_file_path = os.path.join(INSTALL_PREFIX, 'state', 'rules.toml')
|
dynamic_conf_file_path = os.path.join(INSTALL_PREFIX, 'state', 'rules', 'rules.toml')
|
||||||
c.TraefikTomlProxy.toml_dynamic_config_file = dynamic_conf_file_path
|
c.TraefikTomlProxy.toml_dynamic_config_file = dynamic_conf_file_path
|
||||||
c.JupyterHub.proxy_class = TraefikTomlProxy
|
c.JupyterHub.proxy_class = TraefikTomlProxy
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ PrivateTmp=yes
|
|||||||
PrivateDevices=yes
|
PrivateDevices=yes
|
||||||
ProtectKernelTunables=yes
|
ProtectKernelTunables=yes
|
||||||
ProtectKernelModules=yes
|
ProtectKernelModules=yes
|
||||||
ReadWritePaths={install_prefix}/state/rules.toml
|
ReadWritePaths={install_prefix}/state/rules
|
||||||
ReadWritePaths={install_prefix}/state/acme.json
|
ReadWritePaths={install_prefix}/state/acme.json
|
||||||
WorkingDirectory={install_prefix}/state
|
WorkingDirectory={install_prefix}/state
|
||||||
ExecStart={install_prefix}/hub/bin/traefik \
|
ExecStart={install_prefix}/hub/bin/traefik \
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ def ensure_traefik_config(state_dir):
|
|||||||
"""Render the traefik.toml config file"""
|
"""Render the traefik.toml config file"""
|
||||||
traefik_std_config_file = os.path.join(state_dir, "traefik.toml")
|
traefik_std_config_file = os.path.join(state_dir, "traefik.toml")
|
||||||
traefik_extra_config_dir = os.path.join(CONFIG_DIR, "traefik_config.d")
|
traefik_extra_config_dir = os.path.join(CONFIG_DIR, "traefik_config.d")
|
||||||
|
traefik_dynamic_config_dir = os.path.join(state_dir, "rules")
|
||||||
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
config['traefik_api']['basic_auth'] = compute_basic_auth(
|
config['traefik_api']['basic_auth'] = compute_basic_auth(
|
||||||
@@ -116,9 +117,12 @@ def ensure_traefik_config(state_dir):
|
|||||||
):
|
):
|
||||||
raise ValueError("Both email and domains must be set for letsencrypt")
|
raise ValueError("Both email and domains must be set for letsencrypt")
|
||||||
|
|
||||||
# Ensure extra config dir exists and is private
|
# Ensure traefik extra static config dir exists and is private
|
||||||
os.makedirs(traefik_extra_config_dir, mode=0o700, exist_ok=True)
|
os.makedirs(traefik_extra_config_dir, mode=0o700, exist_ok=True)
|
||||||
|
|
||||||
|
# Ensure traefik dynamic config dir exists and is private
|
||||||
|
os.makedirs(traefik_dynamic_config_dir, mode=0o700, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Load standard config file merge it with the extra config files into a dict
|
# Load standard config file merge it with the extra config files into a dict
|
||||||
extra_config = load_extra_config(traefik_extra_config_dir)
|
extra_config = load_extra_config(traefik_extra_config_dir)
|
||||||
@@ -131,7 +135,7 @@ def ensure_traefik_config(state_dir):
|
|||||||
os.fchmod(f.fileno(), 0o600)
|
os.fchmod(f.fileno(), 0o600)
|
||||||
toml.dump(new_toml, f)
|
toml.dump(new_toml, f)
|
||||||
|
|
||||||
with open(os.path.join(state_dir, "rules.toml"), "w") as f:
|
with open(os.path.join(traefik_dynamic_config_dir, "rules.toml"), "w") as f:
|
||||||
os.fchmod(f.fileno(), 0o600)
|
os.fchmod(f.fileno(), 0o600)
|
||||||
|
|
||||||
# ensure acme.json exists and is private
|
# ensure acme.json exists and is private
|
||||||
|
|||||||
@@ -70,5 +70,5 @@ entryPoint = "https"
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
[file]
|
[file]
|
||||||
filename = "rules.toml"
|
directory = "rules"
|
||||||
watch = true
|
watch = true
|
||||||
|
|||||||
Reference in New Issue
Block a user