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,
|
||||
install_requires=[
|
||||
'pyyaml==3.*',
|
||||
'ruamel.yaml==0.15.*'
|
||||
'ruamel.yaml==0.15.*',
|
||||
'jinja2',
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'tljh-config = tljh.config:main'
|
||||
]
|
||||
}
|
||||
'tljh-config = tljh.config:main',
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -26,16 +26,30 @@ default = {
|
||||
'users': {
|
||||
'allowed': [],
|
||||
'banned': [],
|
||||
'admin': []
|
||||
'admin': [],
|
||||
},
|
||||
'limits': {
|
||||
'memory': '1G',
|
||||
'cpu': None
|
||||
'cpu': None,
|
||||
},
|
||||
'http': {
|
||||
'port': 80,
|
||||
},
|
||||
'https': {
|
||||
'enabled': False,
|
||||
'port': 443,
|
||||
'tls': {
|
||||
'cert': '',
|
||||
'key': '',
|
||||
},
|
||||
'letsencrypt': {
|
||||
'email': '',
|
||||
'domains': [],
|
||||
},
|
||||
},
|
||||
'user_environment': {
|
||||
'default_app': 'classic'
|
||||
}
|
||||
|
||||
'default_app': 'classic',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from urllib.request import urlopen, URLError
|
||||
|
||||
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')
|
||||
HUB_ENV_PREFIX = os.path.join(INSTALL_PREFIX, 'hub')
|
||||
@@ -110,7 +110,7 @@ def ensure_chp_package(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:
|
||||
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:
|
||||
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(
|
||||
python_interpreter_path=sys.executable,
|
||||
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('jupyterhub.service', hub_unit_template.format(**unit_params))
|
||||
systemd.install_unit('traefik.service', traefik_unit_template.format(**unit_params))
|
||||
systemd.reload_daemon()
|
||||
|
||||
os.makedirs(STATE_DIR, mode=0o700, exist_ok=True)
|
||||
@@ -141,10 +147,12 @@ def ensure_jupyterhub_service(prefix):
|
||||
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 ime
|
||||
# Mark JupyterHub & CHP to start at boot time
|
||||
systemd.enable_service('jupyterhub')
|
||||
systemd.enable_service('configurable-http-proxy')
|
||||
systemd.enable_service('traefik')
|
||||
|
||||
|
||||
def ensure_jupyterhub_package(prefix):
|
||||
@@ -165,6 +173,7 @@ def ensure_jupyterhub_package(prefix):
|
||||
'jupyterhub-ldapauthenticator==1.2.2',
|
||||
'oauthenticator==0.7.3',
|
||||
])
|
||||
traefik.ensure_traefik_binary(prefix)
|
||||
|
||||
|
||||
def ensure_usergroups():
|
||||
|
||||
@@ -29,8 +29,6 @@ class CustomSpawner(SystemdSpawner):
|
||||
|
||||
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
|
||||
c.JupyterHub.hub_port = 15001
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
User=nobody
|
||||
Restart=always
|
||||
# chp process should have no write access anywhere on disk
|
||||
ProtectHome=tmpfs
|
||||
@@ -16,8 +16,8 @@ ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
EnvironmentFile={install_prefix}/state/configurable-http-proxy.secret
|
||||
ExecStart={install_prefix}/hub/node_modules/.bin/configurable-http-proxy \
|
||||
--ip 0.0.0.0 \
|
||||
--port 80 \
|
||||
--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
|
||||
|
||||
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