Add traefik in front of CHP

introduces configuration for manual tls and letsencrypt
This commit is contained in:
Min RK
2018-07-21 00:20:29 -07:00
parent 2cb7d10ac7
commit 000ac05e14
8 changed files with 204 additions and 18 deletions

View File

@@ -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',
],
},
)

View File

@@ -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',
},
}

View File

@@ -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():

View File

@@ -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

View File

@@ -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

View 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
View 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
View 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"