mirror of
https://github.com/jupyterhub/the-littlest-jupyterhub.git
synced 2025-12-18 21:54:05 +08:00
Merge pull request #266 from GeorgianaElena/replace_chp_traefik
Replace chp with traefik-proxy
This commit is contained in:
@@ -108,7 +108,7 @@ def show_logs(container_name):
|
|||||||
)
|
)
|
||||||
run_container_command(
|
run_container_command(
|
||||||
container_name,
|
container_name,
|
||||||
'systemctl --no-pager status jupyterhub configurable-http-proxy'
|
'systemctl --no-pager status jupyterhub traefik'
|
||||||
)
|
)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -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, 'set', 'auth.type', 'dummyauthenticator.DummyAuthenticator')).wait()
|
||||||
assert 0 == await (await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')).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:
|
async with User(username, hub_url, partial(login_dummy, password='')) as u:
|
||||||
await u.login()
|
await u.login()
|
||||||
await u.ensure_server()
|
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, 'add-item', 'users.admin', username)).wait()
|
||||||
assert 0 == await (await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')).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:
|
async with User(username, hub_url, partial(login_dummy, password='')) as u:
|
||||||
await u.login()
|
await u.login()
|
||||||
await u.ensure_server()
|
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, 'add-item', 'users.admin', username)).wait()
|
||||||
assert 0 == await (await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')).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:
|
async with User(username, hub_url, partial(login_dummy, password='')) as u:
|
||||||
await u.login()
|
await u.login()
|
||||||
await u.ensure_server()
|
await u.ensure_server()
|
||||||
@@ -107,16 +97,14 @@ async def test_user_admin_remove():
|
|||||||
# Assert that the user has admin rights
|
# Assert that the user has admin rights
|
||||||
assert f'jupyter-{username}' in grp.getgrnam('jupyterhub-admins').gr_mem
|
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, 'remove-item', 'users.admin', username)).wait()
|
||||||
assert 0 == await (await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')).wait()
|
assert 0 == await (await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')).wait()
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
await u.stop_server()
|
await u.stop_server()
|
||||||
await u.ensure_server()
|
await u.ensure_server()
|
||||||
|
|
||||||
# Assert that the user does *not* have admin rights
|
# 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
|
@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, 'set', 'auth.type', 'dummyauthenticator.DummyAuthenticator')).wait()
|
||||||
assert 0 == await (await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')).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:
|
try:
|
||||||
async with User(username, hub_url, partial(login_dummy, password='')) as u:
|
async with User(username, hub_url, partial(login_dummy, password='')) as u:
|
||||||
await u.login()
|
await u.login()
|
||||||
|
|||||||
@@ -53,8 +53,13 @@ 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
|
||||||
|
|
||||||
# verify that we can still connect to the hub
|
for i in range(5):
|
||||||
r = requests.get("https://127.0.0.1/hub/api", verify=False)
|
time.sleep(i)
|
||||||
|
# verify that we can still connect to the hub
|
||||||
|
r = requests.get("https://127.0.0.1/hub/api", verify=False)
|
||||||
|
if r.status_code == 200:
|
||||||
|
break;
|
||||||
|
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
# cleanup
|
# cleanup
|
||||||
|
|||||||
4
setup.py
4
setup.py
@@ -13,7 +13,9 @@ setup(
|
|||||||
install_requires=[
|
install_requires=[
|
||||||
'ruamel.yaml==0.15.*',
|
'ruamel.yaml==0.15.*',
|
||||||
'jinja2',
|
'jinja2',
|
||||||
'pluggy>0.7<1.0'
|
'pluggy>0.7<1.0',
|
||||||
|
'passlib',
|
||||||
|
'jupyterhub-traefik-proxy==0.1.*'
|
||||||
],
|
],
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ def test_add_to_config_zero_level():
|
|||||||
'a': ['b']
|
'a': ['b']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_add_to_config_multiple():
|
def test_add_to_config_multiple():
|
||||||
conf = {}
|
conf = {}
|
||||||
|
|
||||||
@@ -116,16 +117,21 @@ def test_remove_from_config_error():
|
|||||||
|
|
||||||
|
|
||||||
def test_reload_hub():
|
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.config.check_hub_ready') as check_ready:
|
||||||
config.reload_component('hub')
|
config.reload_component('hub')
|
||||||
assert restart_service.called_with('jupyterhub')
|
assert restart_service.called_with('jupyterhub')
|
||||||
|
assert check_active.called_with('jupyterhub')
|
||||||
|
|
||||||
|
|
||||||
def test_reload_proxy(tljh_dir):
|
def test_reload_proxy(tljh_dir):
|
||||||
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:
|
||||||
config.reload_component('proxy')
|
config.reload_component('proxy')
|
||||||
assert restart_service.called_with('configurable-http-proxy')
|
|
||||||
assert restart_service.called_with('traefik')
|
assert restart_service.called_with('traefik')
|
||||||
|
assert check_active.called_with('traefik')
|
||||||
assert os.path.exists(os.path.join(config.STATE_DIR, 'traefik.toml'))
|
assert os.path.exists(os.path.join(config.STATE_DIR, 'traefik.toml'))
|
||||||
|
|
||||||
|
|
||||||
@@ -140,8 +146,8 @@ def test_cli_no_command(capsys):
|
|||||||
"arg, value",
|
"arg, value",
|
||||||
[
|
[
|
||||||
("true", True),
|
("true", True),
|
||||||
("FALSE", False),
|
("FALSE", False)
|
||||||
],
|
]
|
||||||
)
|
)
|
||||||
def test_cli_set_bool(tljh_dir, arg, value):
|
def test_cli_set_bool(tljh_dir, arg, value):
|
||||||
config.main(["set", "https.enabled", arg])
|
config.main(["set", "https.enabled", arg])
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
Test
|
Test configurer
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from tljh import configurer
|
from tljh import configurer
|
||||||
|
|
||||||
|
|
||||||
@@ -161,6 +163,43 @@ def test_auth_github():
|
|||||||
assert c.GitHubOAuthenticator.client_secret == 'something-else'
|
assert c.GitHubOAuthenticator.client_secret == 'something-else'
|
||||||
|
|
||||||
|
|
||||||
|
def test_traefik_api_default():
|
||||||
|
"""
|
||||||
|
Test default traefik api authentication settings with no overrides
|
||||||
|
"""
|
||||||
|
c = apply_mock_config({})
|
||||||
|
|
||||||
|
assert c.TraefikTomlProxy.traefik_api_username == 'api_admin'
|
||||||
|
assert len(c.TraefikTomlProxy.traefik_api_password) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_traefik_api():
|
||||||
|
"""
|
||||||
|
Test setting per traefik api credentials
|
||||||
|
"""
|
||||||
|
c = apply_mock_config({
|
||||||
|
'traefik_api': {
|
||||||
|
'username': 'some_user',
|
||||||
|
'password': '1234'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
assert c.TraefikTomlProxy.traefik_api_username == 'some_user'
|
||||||
|
assert c.TraefikTomlProxy.traefik_api_password == '1234'
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_secrets(tljh_dir):
|
||||||
|
"""
|
||||||
|
Test loading secret files
|
||||||
|
"""
|
||||||
|
with open(os.path.join(tljh_dir, 'state', 'traefik-api.secret'), 'w') as f:
|
||||||
|
f.write("traefik-password")
|
||||||
|
|
||||||
|
tljh_config = configurer.load_config()
|
||||||
|
assert tljh_config['traefik_api']['password'] == "traefik-password"
|
||||||
|
c = apply_mock_config(tljh_config)
|
||||||
|
assert c.TraefikTomlProxy.traefik_api_password == "traefik-password"
|
||||||
|
|
||||||
|
|
||||||
def test_auth_native():
|
def test_auth_native():
|
||||||
"""
|
"""
|
||||||
Test setting Native Authenticator
|
Test setting Native Authenticator
|
||||||
@@ -175,3 +214,4 @@ def test_auth_native():
|
|||||||
})
|
})
|
||||||
assert c.JupyterHub.authenticator_class == 'nativeauthenticator.NativeAuthenticator'
|
assert c.JupyterHub.authenticator_class == 'nativeauthenticator.NativeAuthenticator'
|
||||||
assert c.NativeAuthenticator.open_signup == True
|
assert c.NativeAuthenticator.open_signup == True
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Test traefik configuration"""
|
"""Test traefik configuration"""
|
||||||
import os
|
import os
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
import pytoml as toml
|
import pytoml as toml
|
||||||
|
|
||||||
@@ -27,12 +28,19 @@ def test_default_config(tmpdir, tljh_dir):
|
|||||||
print(toml_cfg)
|
print(toml_cfg)
|
||||||
cfg = toml.loads(toml_cfg)
|
cfg = toml.loads(toml_cfg)
|
||||||
assert cfg["defaultEntryPoints"] == ["http"]
|
assert cfg["defaultEntryPoints"] == ["http"]
|
||||||
assert cfg["entryPoints"] == {"http": {"address": ":80"}}
|
assert len(cfg["entryPoints"]["auth_api"]["auth"]["basic"]["users"]) == 1
|
||||||
assert cfg["frontends"] == {
|
# runtime generated entry, value not testable
|
||||||
"jupyterhub": {"backend": "jupyterhub", "passHostHeader": True}
|
cfg["entryPoints"]["auth_api"]["auth"]["basic"]["users"] = [""]
|
||||||
}
|
|
||||||
assert cfg["backends"] == {
|
assert cfg["entryPoints"] == {
|
||||||
"jupyterhub": {"servers": {"chp": {"url": "http://127.0.0.1:15003"}}}
|
"http": {"address": ":80"},
|
||||||
|
"auth_api": {
|
||||||
|
"address": "127.0.0.1:8099",
|
||||||
|
"auth": {
|
||||||
|
"basic": {"users": [""]}
|
||||||
|
},
|
||||||
|
"whiteList": {"sourceRange": ["127.0.0.1"]}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -55,9 +63,20 @@ def test_letsencrypt_config(tljh_dir):
|
|||||||
cfg = toml.loads(toml_cfg)
|
cfg = toml.loads(toml_cfg)
|
||||||
assert cfg["defaultEntryPoints"] == ["http", "https"]
|
assert cfg["defaultEntryPoints"] == ["http", "https"]
|
||||||
assert "acme" in cfg
|
assert "acme" in cfg
|
||||||
|
assert len(cfg["entryPoints"]["auth_api"]["auth"]["basic"]["users"]) == 1
|
||||||
|
# runtime generated entry, value not testable
|
||||||
|
cfg["entryPoints"]["auth_api"]["auth"]["basic"]["users"] = [""]
|
||||||
|
|
||||||
assert cfg["entryPoints"] == {
|
assert cfg["entryPoints"] == {
|
||||||
"http": {"address": ":80", "redirect": {"entryPoint": "https"}},
|
"http": {"address": ":80", "redirect": {"entryPoint": "https"}},
|
||||||
"https": {"address": ":443", "backend": "jupyterhub", "tls": {}},
|
"https": {"address": ":443", "tls": {}},
|
||||||
|
"auth_api": {
|
||||||
|
"address": "127.0.0.1:8099",
|
||||||
|
"auth": {
|
||||||
|
"basic": {"users": [""]}
|
||||||
|
},
|
||||||
|
"whiteList": {"sourceRange": ["127.0.0.1"]}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
assert cfg["acme"] == {
|
assert cfg["acme"] == {
|
||||||
"email": "fake@jupyter.org",
|
"email": "fake@jupyter.org",
|
||||||
@@ -83,15 +102,24 @@ def test_manual_ssl_config(tljh_dir):
|
|||||||
cfg = toml.loads(toml_cfg)
|
cfg = toml.loads(toml_cfg)
|
||||||
assert cfg["defaultEntryPoints"] == ["http", "https"]
|
assert cfg["defaultEntryPoints"] == ["http", "https"]
|
||||||
assert "acme" not in cfg
|
assert "acme" not in cfg
|
||||||
|
assert len(cfg["entryPoints"]["auth_api"]["auth"]["basic"]["users"]) == 1
|
||||||
|
# runtime generated entry, value not testable
|
||||||
|
cfg["entryPoints"]["auth_api"]["auth"]["basic"]["users"] = [""]
|
||||||
assert cfg["entryPoints"] == {
|
assert cfg["entryPoints"] == {
|
||||||
"http": {"address": ":80", "redirect": {"entryPoint": "https"}},
|
"http": {"address": ":80", "redirect": {"entryPoint": "https"}},
|
||||||
"https": {
|
"https": {
|
||||||
"address": ":443",
|
"address": ":443",
|
||||||
"backend": "jupyterhub",
|
|
||||||
"tls": {
|
"tls": {
|
||||||
"certificates": [
|
"certificates": [
|
||||||
{"certFile": "/path/to/ssl.cert", "keyFile": "/path/to/ssl.key"}
|
{"certFile": "/path/to/ssl.cert", "keyFile": "/path/to/ssl.key"}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"auth_api": {
|
||||||
|
"address": "127.0.0.1:8099",
|
||||||
|
"auth": {
|
||||||
|
"basic": {"users": [""]}
|
||||||
|
},
|
||||||
|
"whiteList": {"sourceRange": ["127.0.0.1"]}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,15 @@ tljh-config show firstlevel.second_level
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import asyncio
|
||||||
from collections import Sequence, Mapping
|
from collections import Sequence, Mapping
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
from .yaml import yaml
|
from .yaml import yaml
|
||||||
|
|
||||||
@@ -85,7 +89,7 @@ def add_item_to_config(config, property_path, value):
|
|||||||
|
|
||||||
def remove_item_from_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('.')
|
path_components = property_path.split('.')
|
||||||
|
|
||||||
@@ -172,6 +176,12 @@ def remove_config_value(config_path, key_path, value):
|
|||||||
with open(config_path, 'w') as f:
|
with open(config_path, 'w') as f:
|
||||||
yaml.dump(config, f)
|
yaml.dump(config, f)
|
||||||
|
|
||||||
|
def check_hub_ready():
|
||||||
|
try:
|
||||||
|
r = requests.get('http://127.0.0.1:80', verify=False)
|
||||||
|
return r.status_code == 200
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
def reload_component(component):
|
def reload_component(component):
|
||||||
"""
|
"""
|
||||||
@@ -181,14 +191,20 @@ def reload_component(component):
|
|||||||
"""
|
"""
|
||||||
# import here to avoid circular imports
|
# import here to avoid circular imports
|
||||||
from tljh import systemd, traefik
|
from tljh import systemd, traefik
|
||||||
|
|
||||||
if component == 'hub':
|
if component == 'hub':
|
||||||
systemd.restart_service('jupyterhub')
|
systemd.restart_service('jupyterhub')
|
||||||
# FIXME: Verify hub is back up?
|
# Ensure hub is back up
|
||||||
|
while not systemd.check_service_active('jupyterhub'):
|
||||||
|
time.sleep(1)
|
||||||
|
while not check_hub_ready():
|
||||||
|
time.sleep(1)
|
||||||
print('Hub reload with new configuration complete')
|
print('Hub reload with new configuration complete')
|
||||||
elif component == 'proxy':
|
elif component == 'proxy':
|
||||||
traefik.ensure_traefik_config(STATE_DIR)
|
traefik.ensure_traefik_config(STATE_DIR)
|
||||||
systemd.restart_service('configurable-http-proxy')
|
|
||||||
systemd.restart_service('traefik')
|
systemd.restart_service('traefik')
|
||||||
|
while not systemd.check_service_active('traefik'):
|
||||||
|
time.sleep(1)
|
||||||
print('Proxy reload with new configuration complete')
|
print('Proxy reload with new configuration complete')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ FIXME: A strong feeling that JSON Schema should be involved somehow.
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from .config import CONFIG_FILE
|
from .config import CONFIG_FILE, STATE_DIR
|
||||||
from .yaml import yaml
|
from .yaml import yaml
|
||||||
|
|
||||||
# Default configuration for tljh
|
# Default configuration for tljh
|
||||||
@@ -46,12 +46,17 @@ default = {
|
|||||||
'domains': [],
|
'domains': [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'traefik_api': {
|
||||||
|
'ip': "127.0.0.1",
|
||||||
|
'port': 8099,
|
||||||
|
'username': 'api_admin',
|
||||||
|
'password': '',
|
||||||
|
},
|
||||||
'user_environment': {
|
'user_environment': {
|
||||||
'default_app': 'classic',
|
'default_app': 'classic',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def load_config(config_file=CONFIG_FILE):
|
def load_config(config_file=CONFIG_FILE):
|
||||||
"""Load the current config as a dictionary
|
"""Load the current config as a dictionary
|
||||||
|
|
||||||
@@ -62,7 +67,11 @@ def load_config(config_file=CONFIG_FILE):
|
|||||||
config_overrides = yaml.load(f)
|
config_overrides = yaml.load(f)
|
||||||
else:
|
else:
|
||||||
config_overrides = {}
|
config_overrides = {}
|
||||||
return _merge_dictionaries(dict(default), config_overrides)
|
|
||||||
|
secrets = load_secrets()
|
||||||
|
config = _merge_dictionaries(dict(default), secrets)
|
||||||
|
config = _merge_dictionaries(config, config_overrides)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
def apply_config(config_overrides, c):
|
def apply_config(config_overrides, c):
|
||||||
@@ -76,6 +85,7 @@ def apply_config(config_overrides, c):
|
|||||||
update_limits(c, tljh_config)
|
update_limits(c, tljh_config)
|
||||||
update_user_environment(c, tljh_config)
|
update_user_environment(c, tljh_config)
|
||||||
update_user_account_config(c, tljh_config)
|
update_user_account_config(c, tljh_config)
|
||||||
|
update_traefik_api(c, tljh_config)
|
||||||
|
|
||||||
|
|
||||||
def set_if_not_none(parent, key, value):
|
def set_if_not_none(parent, key, value):
|
||||||
@@ -86,6 +96,30 @@ def set_if_not_none(parent, key, value):
|
|||||||
setattr(parent, key, value)
|
setattr(parent, key, value)
|
||||||
|
|
||||||
|
|
||||||
|
def load_traefik_api_credentials():
|
||||||
|
"""Load traefik api secret from a file"""
|
||||||
|
proxy_secret_path = os.path.join(STATE_DIR, 'traefik-api.secret')
|
||||||
|
if not os.path.exists(proxy_secret_path):
|
||||||
|
return {}
|
||||||
|
with open(proxy_secret_path,'r') as f:
|
||||||
|
password = f.read()
|
||||||
|
return {
|
||||||
|
'traefik_api': {
|
||||||
|
'password': password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_secrets():
|
||||||
|
"""Load any secret values stored on disk
|
||||||
|
|
||||||
|
Returns dict to be merged into config during load
|
||||||
|
"""
|
||||||
|
config = {}
|
||||||
|
config = _merge_dictionaries(config, load_traefik_api_credentials())
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
def update_auth(c, config):
|
def update_auth(c, config):
|
||||||
"""
|
"""
|
||||||
Set auth related configuration from YAML config file
|
Set auth related configuration from YAML config file
|
||||||
@@ -149,6 +183,14 @@ def update_user_account_config(c, config):
|
|||||||
c.SystemdSpawner.username_template = 'jupyter-{USERNAME}'
|
c.SystemdSpawner.username_template = 'jupyter-{USERNAME}'
|
||||||
|
|
||||||
|
|
||||||
|
def update_traefik_api(c, config):
|
||||||
|
"""
|
||||||
|
Set traefik api endpoint credentials
|
||||||
|
"""
|
||||||
|
c.TraefikTomlProxy.traefik_api_username = config['traefik_api']['username']
|
||||||
|
c.TraefikTomlProxy.traefik_api_password = config['traefik_api']['password']
|
||||||
|
|
||||||
|
|
||||||
def _merge_dictionaries(a, b, path=None, update=True):
|
def _merge_dictionaries(a, b, path=None, update=True):
|
||||||
"""
|
"""
|
||||||
Merge two dictionaries recursively.
|
Merge two dictionaries recursively.
|
||||||
|
|||||||
@@ -99,15 +99,25 @@ sckuXINIU3DFWzZGr0QrqkuE/jyr7FXeUJj9B7cLo+s/TXo+RaVfi3kOc9BoxIvy
|
|||||||
apt.add_source('nodesource', 'https://deb.nodesource.com/node_10.x', 'main')
|
apt.add_source('nodesource', 'https://deb.nodesource.com/node_10.x', 'main')
|
||||||
apt.install_packages(['nodejs'])
|
apt.install_packages(['nodejs'])
|
||||||
|
|
||||||
|
def remove_chp():
|
||||||
def ensure_chp_package(prefix):
|
|
||||||
"""
|
"""
|
||||||
Ensure CHP is installed
|
Ensure CHP is not running
|
||||||
"""
|
"""
|
||||||
if not os.path.exists(os.path.join(prefix, 'node_modules', '.bin', 'configurable-http-proxy')):
|
if os.path.exists("/etc/systemd/system/configurable-http-proxy.service"):
|
||||||
subprocess.check_output([
|
if systemd.check_service_active('configurable-http-proxy.service'):
|
||||||
'npm', 'install', 'configurable-http-proxy@3.1.0'
|
try:
|
||||||
], cwd=prefix, stderr=subprocess.STDOUT)
|
systemd.stop_service('configurable-http-proxy.service')
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
logger.info("Cannot stop configurable-http-proxy...")
|
||||||
|
if systemd.check_service_enabled('configurable-http-proxy.service'):
|
||||||
|
try:
|
||||||
|
systemd.disable_service('configurable-http-proxy.service')
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
logger.info("Cannot disable configurable-http-proxy...")
|
||||||
|
try:
|
||||||
|
systemd.uninstall_unit('configurable-http-proxy.service')
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
logger.info("Cannot uninstall configurable-http-proxy...")
|
||||||
|
|
||||||
|
|
||||||
def ensure_jupyterhub_service(prefix):
|
def ensure_jupyterhub_service(prefix):
|
||||||
@@ -117,15 +127,22 @@ def ensure_jupyterhub_service(prefix):
|
|||||||
|
|
||||||
os.makedirs(STATE_DIR, mode=0o700, exist_ok=True)
|
os.makedirs(STATE_DIR, mode=0o700, exist_ok=True)
|
||||||
|
|
||||||
|
remove_chp()
|
||||||
|
systemd.reload_daemon()
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
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:
|
with open(os.path.join(HERE, 'systemd-units', 'traefik.service')) as f:
|
||||||
traefik_unit_template = f.read()
|
traefik_unit_template = f.read()
|
||||||
|
|
||||||
|
#Set up proxy / hub secret token if it is not already setup
|
||||||
|
proxy_secret_path = os.path.join(STATE_DIR, 'traefik-api.secret')
|
||||||
|
if not os.path.exists(proxy_secret_path):
|
||||||
|
with open(proxy_secret_path, 'w') as f:
|
||||||
|
f.write(secrets.token_hex(32))
|
||||||
|
|
||||||
traefik.ensure_traefik_config(STATE_DIR)
|
traefik.ensure_traefik_config(STATE_DIR)
|
||||||
|
|
||||||
unit_params = dict(
|
unit_params = dict(
|
||||||
@@ -133,28 +150,16 @@ def ensure_jupyterhub_service(prefix):
|
|||||||
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('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.install_unit('traefik.service', traefik_unit_template.format(**unit_params))
|
||||||
systemd.reload_daemon()
|
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.
|
# If JupyterHub is running, we want to restart it.
|
||||||
systemd.restart_service('jupyterhub')
|
systemd.restart_service('jupyterhub')
|
||||||
systemd.restart_service('traefik')
|
systemd.restart_service('traefik')
|
||||||
|
|
||||||
# Mark JupyterHub & CHP to start at boot time
|
# Mark JupyterHub & traefik to start at boot time
|
||||||
systemd.enable_service('jupyterhub')
|
systemd.enable_service('jupyterhub')
|
||||||
systemd.enable_service('configurable-http-proxy')
|
|
||||||
systemd.enable_service('traefik')
|
systemd.enable_service('traefik')
|
||||||
|
|
||||||
|
|
||||||
@@ -276,7 +281,7 @@ def ensure_admins(admins):
|
|||||||
yaml.dump(config, f)
|
yaml.dump(config, f)
|
||||||
|
|
||||||
|
|
||||||
def ensure_jupyterhub_running(times=4):
|
def ensure_jupyterhub_running(times=20):
|
||||||
"""
|
"""
|
||||||
Ensure that JupyterHub is up and running
|
Ensure that JupyterHub is up and running
|
||||||
|
|
||||||
@@ -433,7 +438,6 @@ def main():
|
|||||||
logger.info("Setting up JupyterHub...")
|
logger.info("Setting up JupyterHub...")
|
||||||
ensure_node()
|
ensure_node()
|
||||||
ensure_jupyterhub_package(HUB_ENV_PREFIX)
|
ensure_jupyterhub_package(HUB_ENV_PREFIX)
|
||||||
ensure_chp_package(HUB_ENV_PREFIX)
|
|
||||||
ensure_jupyterlab_extensions()
|
ensure_jupyterlab_extensions()
|
||||||
ensure_jupyterhub_service(HUB_ENV_PREFIX)
|
ensure_jupyterhub_service(HUB_ENV_PREFIX)
|
||||||
ensure_jupyterhub_running()
|
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.config import INSTALL_PREFIX, USER_ENV_PREFIX, CONFIG_DIR
|
||||||
from tljh.normalize import generate_system_username
|
from tljh.normalize import generate_system_username
|
||||||
from tljh.yaml import yaml
|
from tljh.yaml import yaml
|
||||||
|
from jupyterhub_traefik_proxy import TraefikTomlProxy
|
||||||
|
|
||||||
class UserCreatingSpawner(SystemdSpawner):
|
class UserCreatingSpawner(SystemdSpawner):
|
||||||
"""
|
"""
|
||||||
@@ -43,21 +43,19 @@ c.JupyterHub.cleanup_servers = False
|
|||||||
# 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
|
||||||
|
|
||||||
c.ConfigurableHTTPProxy.should_start = False
|
c.TraefikTomlProxy.should_start = False
|
||||||
c.ConfigurableHTTPProxy.api_url = 'http://127.0.0.1:15002'
|
|
||||||
|
dynamic_conf_file_path = os.path.join(INSTALL_PREFIX, 'state', 'rules.toml')
|
||||||
|
c.TraefikTomlProxy.toml_dynamic_config_file = dynamic_conf_file_path
|
||||||
|
c.JupyterHub.proxy_class = TraefikTomlProxy
|
||||||
|
|
||||||
c.SystemdSpawner.extra_paths = [os.path.join(USER_ENV_PREFIX, 'bin')]
|
c.SystemdSpawner.extra_paths = [os.path.join(USER_ENV_PREFIX, 'bin')]
|
||||||
c.SystemdSpawner.default_shell = '/bin/bash'
|
c.SystemdSpawner.default_shell = '/bin/bash'
|
||||||
# Drop the '-singleuser' suffix present in the default template
|
# Drop the '-singleuser' suffix present in the default template
|
||||||
c.SystemdSpawner.unit_name_template = 'jupyter-{USERNAME}'
|
c.SystemdSpawner.unit_name_template = 'jupyter-{USERNAME}'
|
||||||
|
|
||||||
config_overrides_path = os.path.join(CONFIG_DIR, 'config.yaml')
|
tljh_config = configurer.load_config()
|
||||||
if os.path.exists(config_overrides_path):
|
configurer.apply_config(tljh_config, c)
|
||||||
with open(config_overrides_path) as f:
|
|
||||||
config_overrides = yaml.load(f)
|
|
||||||
else:
|
|
||||||
config_overrides = {}
|
|
||||||
configurer.apply_config(config_overrides, c)
|
|
||||||
|
|
||||||
# Load arbitrary .py config files if they exist.
|
# Load arbitrary .py config files if they exist.
|
||||||
# This is our escape hatch
|
# This is our escape hatch
|
||||||
|
|||||||
@@ -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
|
# Template file for JupyterHub systemd service
|
||||||
# Uses simple string.format() for 'templating'
|
# Uses simple string.format() for 'templating'
|
||||||
[Unit]
|
[Unit]
|
||||||
# CHP must have successfully started *before* we launch JupyterHub
|
# Traefik must have successfully started *before* we launch JupyterHub
|
||||||
Requires=configurable-http-proxy.service
|
Requires=traefik.service
|
||||||
After=configurable-http-proxy.service
|
After=traefik.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
User=root
|
User=root
|
||||||
@@ -16,8 +16,6 @@ PrivateTmp=yes
|
|||||||
PrivateDevices=yes
|
PrivateDevices=yes
|
||||||
ProtectKernelTunables=yes
|
ProtectKernelTunables=yes
|
||||||
ProtectKernelModules=yes
|
ProtectKernelModules=yes
|
||||||
# Source CONFIGPROXY_AUTH_TOKEN from here!
|
|
||||||
EnvironmentFile={install_prefix}/state/configurable-http-proxy.secret
|
|
||||||
Environment=TLJH_INSTALL_PREFIX={install_prefix}
|
Environment=TLJH_INSTALL_PREFIX={install_prefix}
|
||||||
ExecStart={python_interpreter_path} -m jupyterhub.app -f {jupyterhub_config_path}
|
ExecStart={python_interpreter_path} -m jupyterhub.app -f {jupyterhub_config_path}
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ After=network.target
|
|||||||
[Service]
|
[Service]
|
||||||
User=root
|
User=root
|
||||||
Restart=always
|
Restart=always
|
||||||
# process only needs to write state/acme.json file, no other files
|
|
||||||
ProtectHome=tmpfs
|
ProtectHome=tmpfs
|
||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
PrivateTmp=yes
|
PrivateTmp=yes
|
||||||
PrivateDevices=yes
|
PrivateDevices=yes
|
||||||
ProtectKernelTunables=yes
|
ProtectKernelTunables=yes
|
||||||
ProtectKernelModules=yes
|
ProtectKernelModules=yes
|
||||||
|
ReadWritePaths={install_prefix}/state/rules.toml
|
||||||
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 \
|
||||||
|
|||||||
@@ -48,6 +48,17 @@ def start_service(name):
|
|||||||
], check=True)
|
], check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def stop_service(name):
|
||||||
|
"""
|
||||||
|
Start service with given name.
|
||||||
|
"""
|
||||||
|
subprocess.run([
|
||||||
|
'systemctl',
|
||||||
|
'stop',
|
||||||
|
name
|
||||||
|
], check=True)
|
||||||
|
|
||||||
|
|
||||||
def restart_service(name):
|
def restart_service(name):
|
||||||
"""
|
"""
|
||||||
Restart service with given name.
|
Restart service with given name.
|
||||||
@@ -70,3 +81,45 @@ def enable_service(name):
|
|||||||
'enable',
|
'enable',
|
||||||
name
|
name
|
||||||
], check=True)
|
], check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def disable_service(name):
|
||||||
|
"""
|
||||||
|
Enable a service with given name.
|
||||||
|
|
||||||
|
This most likely makes the service start on bootup
|
||||||
|
"""
|
||||||
|
subprocess.run([
|
||||||
|
'systemctl',
|
||||||
|
'disable',
|
||||||
|
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_service_enabled(name):
|
||||||
|
"""
|
||||||
|
Check if a service is enabled
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
subprocess.run([
|
||||||
|
'systemctl',
|
||||||
|
'is-enabled',
|
||||||
|
name
|
||||||
|
], check=True)
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return False
|
||||||
|
|||||||
@@ -4,16 +4,17 @@ import os
|
|||||||
from urllib.request import urlretrieve
|
from urllib.request import urlretrieve
|
||||||
|
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
|
from passlib.apache import HtpasswdFile
|
||||||
|
|
||||||
from tljh.configurer import load_config
|
from tljh.configurer import load_config
|
||||||
|
|
||||||
# FIXME: support more than one platform here
|
# FIXME: support more than one platform here
|
||||||
plat = "linux-amd64"
|
plat = "linux-amd64"
|
||||||
traefik_version = "1.6.5"
|
traefik_version = "1.7.5"
|
||||||
|
|
||||||
# record sha256 hashes for supported platforms here
|
# record sha256 hashes for supported platforms here
|
||||||
checksums = {
|
checksums = {
|
||||||
"linux-amd64": "9e77c7664e316953e3f5463c323dffeeecbb35d0b1db7fb49f52e1d9464ca193"
|
"linux-amd64": "4417a9d83753e1ad6bdd64bbbeaeb4b279bcc71542e779b7bcb3b027c6e3356e"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -55,9 +56,23 @@ def ensure_traefik_binary(prefix):
|
|||||||
raise IOError(f"Checksum failed {traefik_bin}: {checksum} != {checksums[plat]}")
|
raise IOError(f"Checksum failed {traefik_bin}: {checksum} != {checksums[plat]}")
|
||||||
|
|
||||||
|
|
||||||
|
def compute_basic_auth(username, password):
|
||||||
|
"""Generate hashed HTTP basic auth from traefik_api username+password"""
|
||||||
|
ht = HtpasswdFile()
|
||||||
|
# generate htpassword
|
||||||
|
ht.set_password(username, password)
|
||||||
|
hashed_password = str(ht.to_string()).split(":")[1][:-3]
|
||||||
|
return username + ":" + hashed_password
|
||||||
|
|
||||||
|
|
||||||
def ensure_traefik_config(state_dir):
|
def ensure_traefik_config(state_dir):
|
||||||
"""Render the traefik.toml config file"""
|
"""Render the traefik.toml config file"""
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
config['traefik_api']['basic_auth'] = compute_basic_auth(
|
||||||
|
config['traefik_api']['username'],
|
||||||
|
config['traefik_api']['password'],
|
||||||
|
)
|
||||||
|
|
||||||
with open(os.path.join(os.path.dirname(__file__), "traefik.toml.tpl")) as f:
|
with open(os.path.join(os.path.dirname(__file__), "traefik.toml.tpl")) as f:
|
||||||
template = Template(f.read())
|
template = Template(f.read())
|
||||||
new_toml = template.render(config)
|
new_toml = template.render(config)
|
||||||
@@ -75,9 +90,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")
|
||||||
with open(os.path.join(state_dir, "traefik.toml"), "w") as f:
|
with open(os.path.join(state_dir, "traefik.toml"), "w") as f:
|
||||||
os.fchmod(f.fileno(), 0o744)
|
os.fchmod(f.fileno(), 0o600)
|
||||||
f.write(new_toml)
|
f.write(new_toml)
|
||||||
|
|
||||||
|
with open(os.path.join(state_dir, "rules.toml"), "w") as f:
|
||||||
|
os.fchmod(f.fileno(), 0o600)
|
||||||
|
|
||||||
# ensure acme.json exists and is private
|
# ensure acme.json exists and is private
|
||||||
with open(os.path.join(state_dir, "acme.json"), "a") as f:
|
with open(os.path.join(state_dir, "acme.json"), "a") as f:
|
||||||
os.fchmod(f.fileno(), 0o600)
|
os.fchmod(f.fileno(), 0o600)
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ idleTimeout = "10m0s"
|
|||||||
{% if https['enabled'] %}
|
{% if https['enabled'] %}
|
||||||
[entryPoints.https]
|
[entryPoints.https]
|
||||||
address = ":{{https['port']}}"
|
address = ":{{https['port']}}"
|
||||||
backend = "jupyterhub"
|
|
||||||
[entryPoints.https.tls]
|
[entryPoints.https.tls]
|
||||||
{% if https['tls']['cert'] %}
|
{% if https['tls']['cert'] %}
|
||||||
[[entryPoints.https.tls.certificates]]
|
[[entryPoints.https.tls.certificates]]
|
||||||
@@ -41,6 +40,19 @@ idleTimeout = "10m0s"
|
|||||||
keyFile = "{{https['tls']['key']}}"
|
keyFile = "{{https['tls']['key']}}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
[entryPoints.auth_api]
|
||||||
|
address = "127.0.0.1:{{traefik_api['port']}}"
|
||||||
|
[entryPoints.auth_api.whiteList]
|
||||||
|
sourceRange = ['{{traefik_api['ip']}}']
|
||||||
|
[entryPoints.auth_api.auth.basic]
|
||||||
|
users = ['{{ traefik_api['basic_auth'] }}']
|
||||||
|
|
||||||
|
[wss]
|
||||||
|
protocol = "http"
|
||||||
|
|
||||||
|
[api]
|
||||||
|
dashboard = true
|
||||||
|
entrypoint = "auth_api"
|
||||||
|
|
||||||
{% if https['enabled'] and https['letsencrypt']['email'] %}
|
{% if https['enabled'] and https['letsencrypt']['email'] %}
|
||||||
[acme]
|
[acme]
|
||||||
@@ -57,13 +69,5 @@ entryPoint = "https"
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
[file]
|
[file]
|
||||||
|
filename = "rules.toml"
|
||||||
[frontends]
|
watch = true
|
||||||
[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