mirror of
https://github.com/jupyterhub/the-littlest-jupyterhub.git
synced 2025-12-18 21:54:05 +08:00
Replace chp with traefik-proxy
This commit is contained in:
@@ -108,7 +108,7 @@ def show_logs(container_name):
|
||||
)
|
||||
run_container_command(
|
||||
container_name,
|
||||
'systemctl --no-pager status jupyterhub configurable-http-proxy'
|
||||
'systemctl --no-pager status jupyterhub traefik'
|
||||
)
|
||||
|
||||
def main():
|
||||
|
||||
@@ -86,12 +86,20 @@ def main():
|
||||
'git+https://github.com/jupyterhub/the-littlest-jupyterhub.git'
|
||||
)
|
||||
|
||||
traefik_proxy_repo_path = 'git+https://github.com/jupyterhub/traefik-proxy.git'
|
||||
|
||||
subprocess.check_output([
|
||||
os.path.join(hub_prefix, 'bin', 'pip'),
|
||||
'install'
|
||||
] + pip_flags + [tljh_repo_path], stderr=subprocess.STDOUT)
|
||||
logger.info('Setup tljh package')
|
||||
|
||||
subprocess.check_output([
|
||||
os.path.join(hub_prefix, 'bin', 'pip'),
|
||||
'install'
|
||||
] + [traefik_proxy_repo_path], stderr=subprocess.STDOUT)
|
||||
logger.info('Setup traefik-proxy package')
|
||||
|
||||
logger.info('Starting TLJH installer...')
|
||||
os.execv(
|
||||
os.path.join(hub_prefix, 'bin', 'python3'),
|
||||
|
||||
@@ -34,10 +34,6 @@ async def test_user_code_execute():
|
||||
assert 0 == await (await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'set', 'auth.type', 'dummyauthenticator.DummyAuthenticator')).wait()
|
||||
assert 0 == await (await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')).wait()
|
||||
|
||||
# FIXME: wait for reload to finish & hub to come up
|
||||
# Should be part of tljh-config reload
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async with User(username, hub_url, partial(login_dummy, password='')) as u:
|
||||
await u.login()
|
||||
await u.ensure_server()
|
||||
@@ -62,9 +58,6 @@ async def test_user_admin_add():
|
||||
assert 0 == await (await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'add-item', 'users.admin', username)).wait()
|
||||
assert 0 == await (await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')).wait()
|
||||
|
||||
# FIXME: wait for reload to finish & hub to come up
|
||||
# Should be part of tljh-config reload
|
||||
await asyncio.sleep(1)
|
||||
async with User(username, hub_url, partial(login_dummy, password='')) as u:
|
||||
await u.login()
|
||||
await u.ensure_server()
|
||||
@@ -94,9 +87,6 @@ async def test_user_admin_remove():
|
||||
assert 0 == await (await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'add-item', 'users.admin', username)).wait()
|
||||
assert 0 == await (await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')).wait()
|
||||
|
||||
# FIXME: wait for reload to finish & hub to come up
|
||||
# Should be part of tljh-config reload
|
||||
await asyncio.sleep(1)
|
||||
async with User(username, hub_url, partial(login_dummy, password='')) as u:
|
||||
await u.login()
|
||||
await u.ensure_server()
|
||||
@@ -107,16 +97,14 @@ async def test_user_admin_remove():
|
||||
# Assert that the user has admin rights
|
||||
assert f'jupyter-{username}' in grp.getgrnam('jupyterhub-admins').gr_mem
|
||||
|
||||
|
||||
assert 0 == await (await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'remove-item', 'users.admin', username)).wait()
|
||||
assert 0 == await (await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')).wait()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
await u.stop_server()
|
||||
await u.ensure_server()
|
||||
|
||||
# Assert that the user does *not* have admin rights
|
||||
assert f'jupyter-{username}' in grp.getgrnam('jupyterhub-admins').gr_mem
|
||||
assert f'jupyter-{username}' not in grp.getgrnam('jupyterhub-admins').gr_mem
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -132,9 +120,6 @@ async def test_long_username():
|
||||
assert 0 == await (await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'set', 'auth.type', 'dummyauthenticator.DummyAuthenticator')).wait()
|
||||
assert 0 == await (await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')).wait()
|
||||
|
||||
# FIXME: wait for reload to finish & hub to come up
|
||||
# Should be part of tljh-config reload
|
||||
await asyncio.sleep(1)
|
||||
try:
|
||||
async with User(username, hub_url, partial(login_dummy, password='')) as u:
|
||||
await u.login()
|
||||
|
||||
@@ -75,6 +75,7 @@ def test_add_to_config_zero_level():
|
||||
'a': ['b']
|
||||
}
|
||||
|
||||
|
||||
def test_add_to_config_multiple():
|
||||
conf = {}
|
||||
|
||||
@@ -116,15 +117,17 @@ def test_remove_from_config_error():
|
||||
|
||||
|
||||
def test_reload_hub():
|
||||
with mock.patch('tljh.systemd.restart_service') as restart_service:
|
||||
with mock.patch('tljh.systemd.restart_service') as restart_service, mock.patch(
|
||||
'tljh.systemd.check_service_active'
|
||||
) as check_active, mock.patch('tljh.systemd.check_hub_ready') as check_ready:
|
||||
config.reload_component('hub')
|
||||
assert restart_service.called_with('jupyterhub')
|
||||
assert check_active.called_with('jupyterhub')
|
||||
|
||||
|
||||
def test_reload_proxy(tljh_dir):
|
||||
with mock.patch('tljh.systemd.restart_service') as restart_service:
|
||||
config.reload_component('proxy')
|
||||
assert restart_service.called_with('configurable-http-proxy')
|
||||
assert restart_service.called_with('traefik')
|
||||
assert os.path.exists(os.path.join(config.STATE_DIR, 'traefik.toml'))
|
||||
|
||||
@@ -140,8 +143,8 @@ def test_cli_no_command(capsys):
|
||||
"arg, value",
|
||||
[
|
||||
("true", True),
|
||||
("FALSE", False),
|
||||
],
|
||||
("FALSE", False)
|
||||
]
|
||||
)
|
||||
def test_cli_set_bool(tljh_dir, arg, value):
|
||||
config.main(["set", "https.enabled", arg])
|
||||
|
||||
@@ -27,12 +27,14 @@ def test_default_config(tmpdir, tljh_dir):
|
||||
print(toml_cfg)
|
||||
cfg = toml.loads(toml_cfg)
|
||||
assert cfg["defaultEntryPoints"] == ["http"]
|
||||
assert cfg["entryPoints"] == {"http": {"address": ":80"}}
|
||||
assert cfg["frontends"] == {
|
||||
"jupyterhub": {"backend": "jupyterhub", "passHostHeader": True}
|
||||
}
|
||||
assert cfg["backends"] == {
|
||||
"jupyterhub": {"servers": {"chp": {"url": "http://127.0.0.1:15003"}}}
|
||||
assert cfg["entryPoints"] == {
|
||||
"http": {"address": ":80"},
|
||||
"auth_api": {
|
||||
"address": ":8099",
|
||||
"auth": {
|
||||
"basic": {"users": ["api_admin:$apr1$eS/j3kum$q/X2khsIEG/bBGsteP.x./"]}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +59,13 @@ def test_letsencrypt_config(tljh_dir):
|
||||
assert "acme" in cfg
|
||||
assert cfg["entryPoints"] == {
|
||||
"http": {"address": ":80", "redirect": {"entryPoint": "https"}},
|
||||
"https": {"address": ":443", "backend": "jupyterhub", "tls": {}},
|
||||
"https": {"address": ":443", "tls": {}},
|
||||
"auth_api": {
|
||||
"address": ":8099",
|
||||
"auth": {
|
||||
"basic": {"users": ["api_admin:$apr1$eS/j3kum$q/X2khsIEG/bBGsteP.x./"]}
|
||||
},
|
||||
},
|
||||
}
|
||||
assert cfg["acme"] == {
|
||||
"email": "fake@jupyter.org",
|
||||
@@ -87,11 +95,16 @@ def test_manual_ssl_config(tljh_dir):
|
||||
"http": {"address": ":80", "redirect": {"entryPoint": "https"}},
|
||||
"https": {
|
||||
"address": ":443",
|
||||
"backend": "jupyterhub",
|
||||
"tls": {
|
||||
"certificates": [
|
||||
{"certFile": "/path/to/ssl.cert", "keyFile": "/path/to/ssl.key"}
|
||||
]
|
||||
},
|
||||
},
|
||||
"auth_api": {
|
||||
"address": ":8099",
|
||||
"auth": {
|
||||
"basic": {"users": ["api_admin:$apr1$eS/j3kum$q/X2khsIEG/bBGsteP.x./"]}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ from copy import deepcopy
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import asyncio
|
||||
|
||||
from .yaml import yaml
|
||||
|
||||
@@ -85,7 +86,7 @@ def add_item_to_config(config, property_path, value):
|
||||
|
||||
def remove_item_from_config(config, property_path, value):
|
||||
"""
|
||||
Add an item to a list in config.
|
||||
Remove an item from a list in config.
|
||||
"""
|
||||
path_components = property_path.split('.')
|
||||
|
||||
@@ -183,11 +184,14 @@ def reload_component(component):
|
||||
from tljh import systemd, traefik
|
||||
if component == 'hub':
|
||||
systemd.restart_service('jupyterhub')
|
||||
# FIXME: Verify hub is back up?
|
||||
# Ensure hub is back up
|
||||
while not systemd.check_service_active('jupyterhub'):
|
||||
asyncio.sleep(1)
|
||||
while not systemd.check_hub_ready():
|
||||
asyncio.sleep(1)
|
||||
print('Hub reload with new configuration complete')
|
||||
elif component == 'proxy':
|
||||
traefik.ensure_traefik_config(STATE_DIR)
|
||||
systemd.restart_service('configurable-http-proxy')
|
||||
systemd.restart_service('traefik')
|
||||
print('Proxy reload with new configuration complete')
|
||||
|
||||
|
||||
@@ -120,9 +120,6 @@ def ensure_jupyterhub_service(prefix):
|
||||
with open(os.path.join(HERE, 'systemd-units', 'jupyterhub.service')) as f:
|
||||
hub_unit_template = f.read()
|
||||
|
||||
with open(os.path.join(HERE, 'systemd-units', 'configurable-http-proxy.service')) as f:
|
||||
proxy_unit_template = f.read()
|
||||
|
||||
with open(os.path.join(HERE, 'systemd-units', 'traefik.service')) as f:
|
||||
traefik_unit_template = f.read()
|
||||
|
||||
@@ -133,28 +130,16 @@ def ensure_jupyterhub_service(prefix):
|
||||
jupyterhub_config_path=os.path.join(HERE, 'jupyterhub_config.py'),
|
||||
install_prefix=INSTALL_PREFIX,
|
||||
)
|
||||
systemd.install_unit('configurable-http-proxy.service', proxy_unit_template.format(**unit_params))
|
||||
systemd.install_unit('jupyterhub.service', hub_unit_template.format(**unit_params))
|
||||
systemd.install_unit('traefik.service', traefik_unit_template.format(**unit_params))
|
||||
systemd.reload_daemon()
|
||||
|
||||
# Set up proxy / hub secret oken if it is not already setup
|
||||
proxy_secret_path = os.path.join(STATE_DIR, 'configurable-http-proxy.secret')
|
||||
if not os.path.exists(proxy_secret_path):
|
||||
with open(proxy_secret_path, 'w') as f:
|
||||
f.write('CONFIGPROXY_AUTH_TOKEN=' + secrets.token_hex(32))
|
||||
# If we are changing CONFIGPROXY_AUTH_TOKEN, restart configurable-http-proxy!
|
||||
systemd.restart_service('configurable-http-proxy')
|
||||
|
||||
# Start CHP if it has already not been started
|
||||
systemd.start_service('configurable-http-proxy')
|
||||
# If JupyterHub is running, we want to restart it.
|
||||
systemd.restart_service('jupyterhub')
|
||||
systemd.restart_service('traefik')
|
||||
|
||||
# Mark JupyterHub & CHP to start at boot time
|
||||
systemd.enable_service('jupyterhub')
|
||||
systemd.enable_service('configurable-http-proxy')
|
||||
systemd.enable_service('traefik')
|
||||
|
||||
|
||||
@@ -275,7 +260,7 @@ def ensure_admins(admins):
|
||||
yaml.dump(config, f)
|
||||
|
||||
|
||||
def ensure_jupyterhub_running(times=4):
|
||||
def ensure_jupyterhub_running(times=20):
|
||||
"""
|
||||
Ensure that JupyterHub is up and running
|
||||
|
||||
@@ -432,7 +417,6 @@ def main():
|
||||
logger.info("Setting up JupyterHub...")
|
||||
ensure_node()
|
||||
ensure_jupyterhub_package(HUB_ENV_PREFIX)
|
||||
ensure_chp_package(HUB_ENV_PREFIX)
|
||||
ensure_jupyterlab_extensions()
|
||||
ensure_jupyterhub_service(HUB_ENV_PREFIX)
|
||||
ensure_jupyterhub_running()
|
||||
|
||||
@@ -10,7 +10,7 @@ from tljh import configurer, user
|
||||
from tljh.config import INSTALL_PREFIX, USER_ENV_PREFIX, CONFIG_DIR
|
||||
from tljh.normalize import generate_system_username
|
||||
from tljh.yaml import yaml
|
||||
|
||||
from jupyterhub_traefik_proxy import TraefikTomlProxy
|
||||
|
||||
class UserCreatingSpawner(SystemdSpawner):
|
||||
"""
|
||||
@@ -43,8 +43,11 @@ c.JupyterHub.cleanup_servers = False
|
||||
# Use a high port so users can try this on machines with a JupyterHub already present
|
||||
c.JupyterHub.hub_port = 15001
|
||||
|
||||
c.ConfigurableHTTPProxy.should_start = False
|
||||
c.ConfigurableHTTPProxy.api_url = 'http://127.0.0.1:15002'
|
||||
c.TraefikTomlProxy.should_start = False
|
||||
c.TraefikTomlProxy.traefik_api_password = "admin"
|
||||
c.TraefikTomlProxy.traefik_api_username = "api_admin"
|
||||
c.TraefikTomlProxy.toml_dynamic_config_file = "/opt/tljh/state/rules.toml"
|
||||
c.JupyterHub.proxy_class = TraefikTomlProxy
|
||||
|
||||
c.SystemdSpawner.extra_paths = [os.path.join(USER_ENV_PREFIX, 'bin')]
|
||||
c.SystemdSpawner.default_shell = '/bin/bash'
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# Template file for Configurable HTTP Proxy systemd service
|
||||
# Uses simple string.format() for 'templating'
|
||||
[Unit]
|
||||
# Wait for network stack to be fully up before starting CHP
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=nobody
|
||||
Restart=always
|
||||
# chp process should have no write access anywhere on disk
|
||||
ProtectHome=tmpfs
|
||||
ProtectSystem=strict
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
EnvironmentFile={install_prefix}/state/configurable-http-proxy.secret
|
||||
ExecStart={install_prefix}/hub/node_modules/.bin/configurable-http-proxy \
|
||||
--ip 127.0.0.1 \
|
||||
--port 15003 \
|
||||
--api-ip 127.0.0.1 \
|
||||
--api-port 15002 \
|
||||
--error-target http://127.0.0.1:15001/hub/error
|
||||
|
||||
[Install]
|
||||
# Start service when system boots
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,9 +1,9 @@
|
||||
# Template file for JupyterHub systemd service
|
||||
# Uses simple string.format() for 'templating'
|
||||
[Unit]
|
||||
# CHP must have successfully started *before* we launch JupyterHub
|
||||
Requires=configurable-http-proxy.service
|
||||
After=configurable-http-proxy.service
|
||||
# Traefik must have successfully started *before* we launch JupyterHub
|
||||
Requires=traefik.service
|
||||
After=traefik.service
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
@@ -17,7 +17,6 @@ PrivateDevices=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
# Source CONFIGPROXY_AUTH_TOKEN from here!
|
||||
EnvironmentFile={install_prefix}/state/configurable-http-proxy.secret
|
||||
Environment=TLJH_INSTALL_PREFIX={install_prefix}
|
||||
ExecStart={python_interpreter_path} -m jupyterhub.app -f {jupyterhub_config_path}
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ After=network.target
|
||||
[Service]
|
||||
User=root
|
||||
Restart=always
|
||||
# process only needs to write state/acme.json file, no other files
|
||||
ProtectHome=tmpfs
|
||||
ProtectSystem=strict
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
ReadWritePaths={install_prefix}/state/rules.toml
|
||||
ReadWritePaths={install_prefix}/state/acme.json
|
||||
WorkingDirectory={install_prefix}/state
|
||||
ExecStart={install_prefix}/hub/bin/traefik \
|
||||
|
||||
@@ -70,3 +70,48 @@ def enable_service(name):
|
||||
'enable',
|
||||
name
|
||||
], check=True)
|
||||
|
||||
|
||||
def check_service_active(name):
|
||||
"""
|
||||
Check if a service is currently active (running)
|
||||
"""
|
||||
try:
|
||||
subprocess.run([
|
||||
'systemctl',
|
||||
'is-active',
|
||||
name
|
||||
], check=True)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
|
||||
def check_hub_ready():
|
||||
"""
|
||||
Check if the hub is ready
|
||||
"""
|
||||
|
||||
try:
|
||||
last_restart = subprocess.check_output([
|
||||
'systemctl',
|
||||
'show',
|
||||
'jupyterhub',
|
||||
'-p',
|
||||
'ActiveEnterTimestamp'
|
||||
]).decode().strip()
|
||||
|
||||
last_restart = " ".join(last_restart.split(" ")[-3:-1])
|
||||
|
||||
out = subprocess.check_output([
|
||||
'journalctl',
|
||||
'-u',
|
||||
'jupyterhub',
|
||||
'--since',
|
||||
last_restart
|
||||
])
|
||||
|
||||
if "JupyterHub is now running at" in out.decode():
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
@@ -9,11 +9,11 @@ from tljh.configurer import load_config
|
||||
|
||||
# FIXME: support more than one platform here
|
||||
plat = "linux-amd64"
|
||||
traefik_version = "1.6.5"
|
||||
traefik_version = "1.7.5"
|
||||
|
||||
# record sha256 hashes for supported platforms here
|
||||
checksums = {
|
||||
"linux-amd64": "9e77c7664e316953e3f5463c323dffeeecbb35d0b1db7fb49f52e1d9464ca193"
|
||||
"linux-amd64": "4417a9d83753e1ad6bdd64bbbeaeb4b279bcc71542e779b7bcb3b027c6e3356e"
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,9 @@ def ensure_traefik_config(state_dir):
|
||||
os.fchmod(f.fileno(), 0o744)
|
||||
f.write(new_toml)
|
||||
|
||||
with open(os.path.join(state_dir, "rules.toml"), "w") as f:
|
||||
os.fchmod(f.fileno(), 0o744)
|
||||
|
||||
# ensure acme.json exists and is private
|
||||
with open(os.path.join(state_dir, "acme.json"), "a") as f:
|
||||
os.fchmod(f.fileno(), 0o600)
|
||||
|
||||
@@ -33,7 +33,6 @@ idleTimeout = "10m0s"
|
||||
{% if https['enabled'] %}
|
||||
[entryPoints.https]
|
||||
address = ":{{https['port']}}"
|
||||
backend = "jupyterhub"
|
||||
[entryPoints.https.tls]
|
||||
{% if https['tls']['cert'] %}
|
||||
[[entryPoints.https.tls.certificates]]
|
||||
@@ -41,6 +40,17 @@ idleTimeout = "10m0s"
|
||||
keyFile = "{{https['tls']['key']}}"
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
[entryPoints.auth_api]
|
||||
address = ":8099"
|
||||
[entryPoints.auth_api.auth.basic]
|
||||
users = ["api_admin:$apr1$eS/j3kum$q/X2khsIEG/bBGsteP.x./"]
|
||||
|
||||
[wss]
|
||||
protocol = "http"
|
||||
|
||||
[api]
|
||||
dashboard = true
|
||||
entrypoint = "auth_api"
|
||||
|
||||
{% if https['enabled'] and https['letsencrypt']['email'] %}
|
||||
[acme]
|
||||
@@ -57,13 +67,5 @@ entryPoint = "https"
|
||||
{% endif %}
|
||||
|
||||
[file]
|
||||
|
||||
[frontends]
|
||||
[frontends.jupyterhub]
|
||||
backend = "jupyterhub"
|
||||
passHostHeader = true
|
||||
[backends]
|
||||
[backends.jupyterhub]
|
||||
[backends.jupyterhub.servers.chp]
|
||||
url = "http://127.0.0.1:15003"
|
||||
|
||||
filename = "/opt/tljh/state/rules.toml"
|
||||
watch = true
|
||||
|
||||
Reference in New Issue
Block a user