mirror of
https://github.com/jupyterhub/the-littlest-jupyterhub.git
synced 2025-12-18 21:54:05 +08:00
Fixed some issues
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
pytest
|
||||
pytest-asyncio
|
||||
passlib
|
||||
git+https://github.com/yuvipanda/hubtraf.git
|
||||
@@ -53,8 +53,13 @@ def test_manual_https(preserve_config):
|
||||
# verify that our certificate was loaded by traefik
|
||||
assert server_cert == file_cert
|
||||
|
||||
# verify that we can still connect to the hub
|
||||
r = requests.get("https://127.0.0.1/hub/api", verify=False)
|
||||
for i in range(5):
|
||||
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()
|
||||
|
||||
# cleanup
|
||||
|
||||
@@ -119,16 +119,19 @@ def test_remove_from_config_error():
|
||||
def test_reload_hub():
|
||||
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:
|
||||
) as check_active, mock.patch('tljh.config.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:
|
||||
with mock.patch('tljh.systemd.restart_service') as restart_service, mock.patch(
|
||||
'tljh.systemd.check_service_active'
|
||||
) as check_active:
|
||||
config.reload_component('proxy')
|
||||
assert restart_service.called_with('traefik')
|
||||
assert check_active.called_with('traefik')
|
||||
assert os.path.exists(os.path.join(config.STATE_DIR, 'traefik.toml'))
|
||||
|
||||
|
||||
|
||||
@@ -159,3 +159,27 @@ def test_auth_github():
|
||||
assert c.JupyterHub.authenticator_class == 'oauthenticator.github.GitHubOAuthenticator'
|
||||
assert c.GitHubOAuthenticator.client_id == 'something'
|
||||
assert c.GitHubOAuthenticator.client_secret == 'something-else'
|
||||
|
||||
|
||||
def test_auth_api_default():
|
||||
"""
|
||||
Test default traefik api authentication settings with no overrides
|
||||
"""
|
||||
c = apply_mock_config({})
|
||||
|
||||
assert c.TraefikTomlProxy.traefik_api_username == 'api_admin'
|
||||
assert c.TraefikTomlProxy.traefik_api_password == 'admin'
|
||||
|
||||
|
||||
def test_set_auth_api():
|
||||
"""
|
||||
Test setting per traefik api credentials
|
||||
"""
|
||||
c = apply_mock_config({
|
||||
'auth_api': {
|
||||
'username': 'some_user',
|
||||
'password': '1234'
|
||||
}
|
||||
})
|
||||
assert c.TraefikTomlProxy.traefik_api_username == 'some_user'
|
||||
assert c.TraefikTomlProxy.traefik_api_password == '1234'
|
||||
@@ -173,6 +173,14 @@ def remove_config_value(config_path, key_path, value):
|
||||
with open(config_path, 'w') as f:
|
||||
yaml.dump(config, f)
|
||||
|
||||
def check_hub_ready():
|
||||
import requests
|
||||
|
||||
try:
|
||||
r = requests.get('http://127.0.0.1:80')
|
||||
return r.status_code == 200
|
||||
except:
|
||||
return False
|
||||
|
||||
def reload_component(component):
|
||||
"""
|
||||
@@ -182,17 +190,21 @@ def reload_component(component):
|
||||
"""
|
||||
# import here to avoid circular imports
|
||||
from tljh import systemd, traefik
|
||||
import time
|
||||
|
||||
if component == 'hub':
|
||||
systemd.restart_service('jupyterhub')
|
||||
# Ensure hub is back up
|
||||
while not systemd.check_service_active('jupyterhub'):
|
||||
asyncio.sleep(1)
|
||||
while not systemd.check_hub_ready():
|
||||
asyncio.sleep(1)
|
||||
time.sleep(1)
|
||||
while not check_hub_ready():
|
||||
time.sleep(1)
|
||||
print('Hub reload with new configuration complete')
|
||||
elif component == 'proxy':
|
||||
traefik.ensure_traefik_config(STATE_DIR)
|
||||
systemd.restart_service('traefik')
|
||||
while not systemd.check_service_active('traefik'):
|
||||
time.sleep(1)
|
||||
print('Proxy reload with new configuration complete')
|
||||
|
||||
|
||||
|
||||
@@ -46,12 +46,18 @@ default = {
|
||||
'domains': [],
|
||||
},
|
||||
},
|
||||
'auth_api': {
|
||||
'ip': "127.0.0.1",
|
||||
'port': 8099,
|
||||
'username': 'api_admin',
|
||||
'password': 'admin',
|
||||
'basic_auth': ''
|
||||
},
|
||||
'user_environment': {
|
||||
'default_app': 'classic',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def load_config(config_file=CONFIG_FILE):
|
||||
"""Load the current config as a dictionary
|
||||
|
||||
@@ -62,6 +68,8 @@ def load_config(config_file=CONFIG_FILE):
|
||||
config_overrides = yaml.load(f)
|
||||
else:
|
||||
config_overrides = {}
|
||||
|
||||
generate_traefik_api_credentials()
|
||||
return _merge_dictionaries(dict(default), config_overrides)
|
||||
|
||||
|
||||
@@ -76,6 +84,7 @@ def apply_config(config_overrides, c):
|
||||
update_limits(c, tljh_config)
|
||||
update_user_environment(c, tljh_config)
|
||||
update_user_account_config(c, tljh_config)
|
||||
update_auth_api(c, tljh_config)
|
||||
|
||||
|
||||
def set_if_not_none(parent, key, value):
|
||||
@@ -85,6 +94,14 @@ def set_if_not_none(parent, key, value):
|
||||
if value is not None:
|
||||
setattr(parent, key, value)
|
||||
|
||||
def generate_traefik_api_credentials():
|
||||
from passlib.apache import HtpasswdFile
|
||||
|
||||
ht = HtpasswdFile()
|
||||
ht.set_password(default['auth_api']['username'], default['auth_api']['password'])
|
||||
traefik_api_hashed_password = str(ht.to_string()).split(":")[1][:-3]
|
||||
default['auth_api']['basic_auth'] = default['auth_api']['username'] + ":" + traefik_api_hashed_password
|
||||
|
||||
|
||||
def update_auth(c, config):
|
||||
"""
|
||||
@@ -149,6 +166,14 @@ def update_user_account_config(c, config):
|
||||
c.SystemdSpawner.username_template = 'jupyter-{USERNAME}'
|
||||
|
||||
|
||||
def update_auth_api(c, config):
|
||||
"""
|
||||
Set traefik api endpoint credentials
|
||||
"""
|
||||
c.TraefikTomlProxy.traefik_api_username = config['auth_api']['username']
|
||||
c.TraefikTomlProxy.traefik_api_password = config['auth_api']['password']
|
||||
|
||||
|
||||
def _merge_dictionaries(a, b, path=None, update=True):
|
||||
"""
|
||||
Merge two dictionaries recursively.
|
||||
|
||||
@@ -44,9 +44,9 @@ c.JupyterHub.cleanup_servers = False
|
||||
c.JupyterHub.hub_port = 15001
|
||||
|
||||
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"
|
||||
|
||||
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')]
|
||||
|
||||
@@ -84,34 +84,4 @@ def check_service_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
|
||||
return False
|
||||
@@ -75,11 +75,11 @@ def ensure_traefik_config(state_dir):
|
||||
):
|
||||
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)
|
||||
os.fchmod(f.fileno(), 0o600)
|
||||
f.write(new_toml)
|
||||
|
||||
with open(os.path.join(state_dir, "rules.toml"), "w") as f:
|
||||
os.fchmod(f.fileno(), 0o744)
|
||||
os.fchmod(f.fileno(), 0o600)
|
||||
|
||||
# ensure acme.json exists and is private
|
||||
with open(os.path.join(state_dir, "acme.json"), "a") as f:
|
||||
|
||||
@@ -41,9 +41,11 @@ idleTimeout = "10m0s"
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
[entryPoints.auth_api]
|
||||
address = ":8099"
|
||||
address = ":{{auth_api['port']}}"
|
||||
[entryPoints.auth_api.whiteList]
|
||||
sourceRange = ['{{auth_api['ip']}}']
|
||||
[entryPoints.auth_api.auth.basic]
|
||||
users = ["api_admin:$apr1$eS/j3kum$q/X2khsIEG/bBGsteP.x./"]
|
||||
users = ['{{auth_api['basic_auth']}}']
|
||||
|
||||
[wss]
|
||||
protocol = "http"
|
||||
@@ -67,5 +69,5 @@ entryPoint = "https"
|
||||
{% endif %}
|
||||
|
||||
[file]
|
||||
filename = "/opt/tljh/state/rules.toml"
|
||||
filename = "rules.toml"
|
||||
watch = true
|
||||
|
||||
Reference in New Issue
Block a user