mirror of
https://github.com/jupyterhub/the-littlest-jupyterhub.git
synced 2025-12-18 21:54:05 +08:00
Add traefik in front of CHP
introduces configuration for manual tls and letsencrypt
This commit is contained in:
9
setup.py
9
setup.py
@@ -12,11 +12,12 @@ setup(
|
|||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'pyyaml==3.*',
|
'pyyaml==3.*',
|
||||||
'ruamel.yaml==0.15.*'
|
'ruamel.yaml==0.15.*',
|
||||||
|
'jinja2',
|
||||||
],
|
],
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'tljh-config = tljh.config:main'
|
'tljh-config = tljh.config:main',
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,16 +26,30 @@ default = {
|
|||||||
'users': {
|
'users': {
|
||||||
'allowed': [],
|
'allowed': [],
|
||||||
'banned': [],
|
'banned': [],
|
||||||
'admin': []
|
'admin': [],
|
||||||
},
|
},
|
||||||
'limits': {
|
'limits': {
|
||||||
'memory': '1G',
|
'memory': '1G',
|
||||||
'cpu': None
|
'cpu': None,
|
||||||
|
},
|
||||||
|
'http': {
|
||||||
|
'port': 80,
|
||||||
|
},
|
||||||
|
'https': {
|
||||||
|
'enabled': False,
|
||||||
|
'port': 443,
|
||||||
|
'tls': {
|
||||||
|
'cert': '',
|
||||||
|
'key': '',
|
||||||
|
},
|
||||||
|
'letsencrypt': {
|
||||||
|
'email': '',
|
||||||
|
'domains': [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'user_environment': {
|
'user_environment': {
|
||||||
'default_app': 'classic'
|
'default_app': 'classic',
|
||||||
}
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from urllib.request import urlopen, URLError
|
|||||||
|
|
||||||
from ruamel.yaml import YAML
|
from ruamel.yaml import YAML
|
||||||
|
|
||||||
from tljh import conda, systemd, user, apt
|
from tljh import conda, systemd, traefik, user, apt
|
||||||
|
|
||||||
INSTALL_PREFIX = os.environ.get('TLJH_INSTALL_PREFIX', '/opt/tljh')
|
INSTALL_PREFIX = os.environ.get('TLJH_INSTALL_PREFIX', '/opt/tljh')
|
||||||
HUB_ENV_PREFIX = os.path.join(INSTALL_PREFIX, 'hub')
|
HUB_ENV_PREFIX = os.path.join(INSTALL_PREFIX, 'hub')
|
||||||
@@ -110,7 +110,7 @@ def ensure_chp_package(prefix):
|
|||||||
|
|
||||||
def ensure_jupyterhub_service(prefix):
|
def ensure_jupyterhub_service(prefix):
|
||||||
"""
|
"""
|
||||||
Ensure JupyterHub & CHP Services are set up properly
|
Ensure JupyterHub Services are set up properly
|
||||||
"""
|
"""
|
||||||
with open(os.path.join(HERE, 'systemd-units', 'jupyterhub.service')) as f:
|
with open(os.path.join(HERE, 'systemd-units', 'jupyterhub.service')) as f:
|
||||||
hub_unit_template = f.read()
|
hub_unit_template = f.read()
|
||||||
@@ -118,13 +118,19 @@ def ensure_jupyterhub_service(prefix):
|
|||||||
with open(os.path.join(HERE, 'systemd-units', 'configurable-http-proxy.service')) as f:
|
with open(os.path.join(HERE, 'systemd-units', 'configurable-http-proxy.service')) as f:
|
||||||
proxy_unit_template = f.read()
|
proxy_unit_template = f.read()
|
||||||
|
|
||||||
|
with open(os.path.join(HERE, 'systemd-units', 'traefik.service')) as f:
|
||||||
|
traefik_unit_template = f.read()
|
||||||
|
|
||||||
|
traefik.ensure_traefik_config(STATE_DIR)
|
||||||
|
|
||||||
unit_params = dict(
|
unit_params = dict(
|
||||||
python_interpreter_path=sys.executable,
|
python_interpreter_path=sys.executable,
|
||||||
jupyterhub_config_path=os.path.join(HERE, 'jupyterhub_config.py'),
|
jupyterhub_config_path=os.path.join(HERE, 'jupyterhub_config.py'),
|
||||||
install_prefix=INSTALL_PREFIX
|
install_prefix=INSTALL_PREFIX,
|
||||||
)
|
)
|
||||||
systemd.install_unit('configurable-http-proxy.service', proxy_unit_template.format(**unit_params))
|
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('jupyterhub.service', hub_unit_template.format(**unit_params))
|
||||||
|
systemd.install_unit('traefik.service', traefik_unit_template.format(**unit_params))
|
||||||
systemd.reload_daemon()
|
systemd.reload_daemon()
|
||||||
|
|
||||||
os.makedirs(STATE_DIR, mode=0o700, exist_ok=True)
|
os.makedirs(STATE_DIR, mode=0o700, exist_ok=True)
|
||||||
@@ -141,10 +147,12 @@ def ensure_jupyterhub_service(prefix):
|
|||||||
systemd.start_service('configurable-http-proxy')
|
systemd.start_service('configurable-http-proxy')
|
||||||
# If JupyterHub is running, we want to restart it.
|
# If JupyterHub is running, we want to restart it.
|
||||||
systemd.restart_service('jupyterhub')
|
systemd.restart_service('jupyterhub')
|
||||||
|
systemd.restart_service('traefik')
|
||||||
|
|
||||||
# Mark JupyterHub & CHP to start at boot ime
|
# Mark JupyterHub & CHP to start at boot time
|
||||||
systemd.enable_service('jupyterhub')
|
systemd.enable_service('jupyterhub')
|
||||||
systemd.enable_service('configurable-http-proxy')
|
systemd.enable_service('configurable-http-proxy')
|
||||||
|
systemd.enable_service('traefik')
|
||||||
|
|
||||||
|
|
||||||
def ensure_jupyterhub_package(prefix):
|
def ensure_jupyterhub_package(prefix):
|
||||||
@@ -165,6 +173,7 @@ def ensure_jupyterhub_package(prefix):
|
|||||||
'jupyterhub-ldapauthenticator==1.2.2',
|
'jupyterhub-ldapauthenticator==1.2.2',
|
||||||
'oauthenticator==0.7.3',
|
'oauthenticator==0.7.3',
|
||||||
])
|
])
|
||||||
|
traefik.ensure_traefik_binary(prefix)
|
||||||
|
|
||||||
|
|
||||||
def ensure_usergroups():
|
def ensure_usergroups():
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ class CustomSpawner(SystemdSpawner):
|
|||||||
|
|
||||||
c.JupyterHub.spawner_class = CustomSpawner
|
c.JupyterHub.spawner_class = CustomSpawner
|
||||||
|
|
||||||
c.JupyterHub.port = 80
|
|
||||||
|
|
||||||
# Use a high port so users can try this on machines with a JupyterHub already present
|
# Use a high port so users can try this on machines with a JupyterHub already present
|
||||||
c.JupyterHub.hub_port = 15001
|
c.JupyterHub.hub_port = 15001
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
User=root
|
User=nobody
|
||||||
Restart=always
|
Restart=always
|
||||||
# chp process should have no write access anywhere on disk
|
# chp process should have no write access anywhere on disk
|
||||||
ProtectHome=tmpfs
|
ProtectHome=tmpfs
|
||||||
@@ -16,8 +16,8 @@ ProtectKernelTunables=yes
|
|||||||
ProtectKernelModules=yes
|
ProtectKernelModules=yes
|
||||||
EnvironmentFile={install_prefix}/state/configurable-http-proxy.secret
|
EnvironmentFile={install_prefix}/state/configurable-http-proxy.secret
|
||||||
ExecStart={install_prefix}/hub/node_modules/.bin/configurable-http-proxy \
|
ExecStart={install_prefix}/hub/node_modules/.bin/configurable-http-proxy \
|
||||||
--ip 0.0.0.0 \
|
--ip 127.0.0.1 \
|
||||||
--port 80 \
|
--port 15003 \
|
||||||
--api-ip 127.0.0.1 \
|
--api-ip 127.0.0.1 \
|
||||||
--api-port 15002 \
|
--api-port 15002 \
|
||||||
--error-target http://127.0.0.1:15001/hub/error
|
--error-target http://127.0.0.1:15001/hub/error
|
||||||
|
|||||||
23
tljh/systemd-units/traefik.service
Normal file
23
tljh/systemd-units/traefik.service
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Template file for Traefik systemd service
|
||||||
|
# Uses simple string.format() for 'templating'
|
||||||
|
[Unit]
|
||||||
|
# Wait for network stack to be fully up before starting proxy
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=root
|
||||||
|
Restart=always
|
||||||
|
# process only needs to write acme.json file, no other files
|
||||||
|
ProtectHome=tmpfs
|
||||||
|
ProtectSystem=strict
|
||||||
|
PrivateTmp=yes
|
||||||
|
PrivateDevices=yes
|
||||||
|
ProtectKernelTunables=yes
|
||||||
|
ProtectKernelModules=yes
|
||||||
|
WorkingDirectory={install_prefix}/state
|
||||||
|
ExecStart={install_prefix}/hub/bin/traefik \
|
||||||
|
-c {install_prefix}/state/traefik.toml
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
# Start service when system boots
|
||||||
|
WantedBy=multi-user.target
|
||||||
79
tljh/traefik.py
Normal file
79
tljh/traefik.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
"""Traefik installation and setup"""
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
from urllib.request import urlretrieve
|
||||||
|
|
||||||
|
from jinja2 import Environment, Template
|
||||||
|
|
||||||
|
from tljh.configurer import load_config
|
||||||
|
|
||||||
|
# FIXME: support more than one platform here
|
||||||
|
plat = "linux-amd64"
|
||||||
|
traefik_version = "1.6.5"
|
||||||
|
|
||||||
|
# record sha256 hashes for supported platforms here
|
||||||
|
checksums = {
|
||||||
|
"linux-amd64": "9e77c7664e316953e3f5463c323dffeeecbb35d0b1db7fb49f52e1d9464ca193"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def checksum_file(path):
|
||||||
|
"""Compute the sha256 checksum of a path"""
|
||||||
|
hasher = hashlib.sha256()
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
for chunk in iter(lambda: f.read(4096), b""):
|
||||||
|
hasher.update(chunk)
|
||||||
|
return hasher.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_traefik_binary(prefix):
|
||||||
|
"""Download and install the traefik binary"""
|
||||||
|
traefik_bin = os.path.join(prefix, "bin", "traefik")
|
||||||
|
if os.path.exists(traefik_bin):
|
||||||
|
checksum = checksum_file(traefik_bin)
|
||||||
|
if checksum == checksums[plat]:
|
||||||
|
# already have the right binary
|
||||||
|
# ensure permissions and we're done
|
||||||
|
os.chmod(traefik_bin, 0o755)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print(f"checksum mismatch on {traefik_bin}")
|
||||||
|
os.remove(traefik_bin)
|
||||||
|
|
||||||
|
traefik_url = (
|
||||||
|
"https://github.com/containous/traefik/releases"
|
||||||
|
f"/download/v{traefik_version}/traefik_{plat}"
|
||||||
|
)
|
||||||
|
print(f"Downloading traefik {traefik_version}...")
|
||||||
|
# download the file
|
||||||
|
urlretrieve(traefik_url, traefik_bin)
|
||||||
|
os.chmod(traefik_bin, 0o755)
|
||||||
|
|
||||||
|
# verify that we got what we expected
|
||||||
|
checksum = checksum_file(traefik_bin)
|
||||||
|
if checksum != checksums[plat]:
|
||||||
|
raise IOError(f"Checksum failed {traefik_bin}: {checksum} != {checksums[plat]}")
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_traefik_config(state_dir):
|
||||||
|
"""Render the traefik.toml config file"""
|
||||||
|
config = load_config()
|
||||||
|
with open(os.path.join(os.path.dirname(__file__), "traefik.toml.tpl")) as f:
|
||||||
|
template = Template(f.read())
|
||||||
|
new_toml = template.render(config)
|
||||||
|
https = config["https"]
|
||||||
|
letsencrypt = https["letsencrypt"]
|
||||||
|
tls = https["tls"]
|
||||||
|
# validate https config
|
||||||
|
if https["enabled"]:
|
||||||
|
if not tls["cert"] and not letsencrypt["email"]:
|
||||||
|
raise ValueError(
|
||||||
|
"To enable https, you must set tls.cert+key or letsencrypt.email+domains"
|
||||||
|
)
|
||||||
|
if (letsencrypt["email"] and not letsencrypt["domains"]) or (
|
||||||
|
letsencrypt["domains"] and not letsencrypt["email"]
|
||||||
|
):
|
||||||
|
raise ValueError("Both email and domains must be set for letsencrypt")
|
||||||
|
with open(os.path.join(state_dir, "traefik.toml"), "w") as f:
|
||||||
|
os.fchmod(f.fileno(), 0o744)
|
||||||
|
f.write(new_toml)
|
||||||
62
tljh/traefik.toml.tpl
Normal file
62
tljh/traefik.toml.tpl
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# traefik.toml file template
|
||||||
|
{% if https['enabled'] %}
|
||||||
|
defaultEntryPoints = ["http", "https"]
|
||||||
|
{% else %}
|
||||||
|
defaultEntryPoints = ["http"]
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
logLevel = "INFO"
|
||||||
|
# log errors, which could be proxy errors
|
||||||
|
[accessLog]
|
||||||
|
format = "json"
|
||||||
|
[accessLog.filters]
|
||||||
|
status = ["500-999"]
|
||||||
|
|
||||||
|
[respondingTimeouts]
|
||||||
|
idleTimeout = "10m0s"
|
||||||
|
|
||||||
|
[entryPoints]
|
||||||
|
[entryPoints.http]
|
||||||
|
address = ":{{http['port']}}"
|
||||||
|
{% if https['enabled'] %}
|
||||||
|
[entryPoints.http.redirect]
|
||||||
|
entryPoint = "https"
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if https['enabled'] %}
|
||||||
|
[entryPoints.https]
|
||||||
|
address = ":{{https['port']}}"
|
||||||
|
backend = "jupyterhub"
|
||||||
|
[entryPoints.https.tls]
|
||||||
|
{% if https['tls']['cert'] %}
|
||||||
|
[[entryPoints.https.tls.certificates]]
|
||||||
|
certFile = "{{https['tls']['cert']}}"
|
||||||
|
keyFile = "{{https['tls']['key']}}"
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if https['enabled'] and https['letsencrypt']['email'] %}
|
||||||
|
[acme]
|
||||||
|
email = "{{https['letsencrypt']['email']}}"
|
||||||
|
storage = "acme.json"
|
||||||
|
entryPoint = "https"
|
||||||
|
[acme.httpChallenge]
|
||||||
|
entryPoint = "http"
|
||||||
|
|
||||||
|
{% for domain in https['letsencrypt']['domains'] %}
|
||||||
|
[[acme.domains]]
|
||||||
|
main = "{{domain}}"
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
[file]
|
||||||
|
|
||||||
|
[frontends]
|
||||||
|
[frontends.jupyterhub]
|
||||||
|
backend = "jupyterhub"
|
||||||
|
passHostHeader = true
|
||||||
|
[backends]
|
||||||
|
[backends.jupyterhub]
|
||||||
|
[backends.jupyterhub.servers.chp]
|
||||||
|
url = "http://127.0.0.1:15003"
|
||||||
|
|
||||||
Reference in New Issue
Block a user