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
|
||||||
pytest-asyncio
|
pytest-asyncio
|
||||||
|
passlib
|
||||||
git+https://github.com/yuvipanda/hubtraf.git
|
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
|
# 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
|
||||||
|
|||||||
@@ -119,16 +119,19 @@ def test_remove_from_config_error():
|
|||||||
def test_reload_hub():
|
def test_reload_hub():
|
||||||
with mock.patch('tljh.systemd.restart_service') as restart_service, mock.patch(
|
with mock.patch('tljh.systemd.restart_service') as restart_service, mock.patch(
|
||||||
'tljh.systemd.check_service_active'
|
'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')
|
config.reload_component('hub')
|
||||||
assert restart_service.called_with('jupyterhub')
|
assert restart_service.called_with('jupyterhub')
|
||||||
assert check_active.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('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'))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -159,3 +159,27 @@ def test_auth_github():
|
|||||||
assert c.JupyterHub.authenticator_class == 'oauthenticator.github.GitHubOAuthenticator'
|
assert c.JupyterHub.authenticator_class == 'oauthenticator.github.GitHubOAuthenticator'
|
||||||
assert c.GitHubOAuthenticator.client_id == 'something'
|
assert c.GitHubOAuthenticator.client_id == 'something'
|
||||||
assert c.GitHubOAuthenticator.client_secret == 'something-else'
|
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:
|
with open(config_path, 'w') as f:
|
||||||
yaml.dump(config, 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):
|
def reload_component(component):
|
||||||
"""
|
"""
|
||||||
@@ -182,17 +190,21 @@ 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
|
||||||
|
import time
|
||||||
|
|
||||||
if component == 'hub':
|
if component == 'hub':
|
||||||
systemd.restart_service('jupyterhub')
|
systemd.restart_service('jupyterhub')
|
||||||
# Ensure hub is back up
|
# Ensure hub is back up
|
||||||
while not systemd.check_service_active('jupyterhub'):
|
while not systemd.check_service_active('jupyterhub'):
|
||||||
asyncio.sleep(1)
|
time.sleep(1)
|
||||||
while not systemd.check_hub_ready():
|
while not check_hub_ready():
|
||||||
asyncio.sleep(1)
|
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('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')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -46,12 +46,18 @@ default = {
|
|||||||
'domains': [],
|
'domains': [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'auth_api': {
|
||||||
|
'ip': "127.0.0.1",
|
||||||
|
'port': 8099,
|
||||||
|
'username': 'api_admin',
|
||||||
|
'password': 'admin',
|
||||||
|
'basic_auth': ''
|
||||||
|
},
|
||||||
'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,6 +68,8 @@ def load_config(config_file=CONFIG_FILE):
|
|||||||
config_overrides = yaml.load(f)
|
config_overrides = yaml.load(f)
|
||||||
else:
|
else:
|
||||||
config_overrides = {}
|
config_overrides = {}
|
||||||
|
|
||||||
|
generate_traefik_api_credentials()
|
||||||
return _merge_dictionaries(dict(default), config_overrides)
|
return _merge_dictionaries(dict(default), config_overrides)
|
||||||
|
|
||||||
|
|
||||||
@@ -76,6 +84,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_auth_api(c, tljh_config)
|
||||||
|
|
||||||
|
|
||||||
def set_if_not_none(parent, key, value):
|
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:
|
if value is not None:
|
||||||
setattr(parent, key, value)
|
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):
|
def update_auth(c, config):
|
||||||
"""
|
"""
|
||||||
@@ -149,6 +166,14 @@ def update_user_account_config(c, config):
|
|||||||
c.SystemdSpawner.username_template = 'jupyter-{USERNAME}'
|
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):
|
def _merge_dictionaries(a, b, path=None, update=True):
|
||||||
"""
|
"""
|
||||||
Merge two dictionaries recursively.
|
Merge two dictionaries recursively.
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ c.JupyterHub.cleanup_servers = False
|
|||||||
c.JupyterHub.hub_port = 15001
|
c.JupyterHub.hub_port = 15001
|
||||||
|
|
||||||
c.TraefikTomlProxy.should_start = False
|
c.TraefikTomlProxy.should_start = False
|
||||||
c.TraefikTomlProxy.traefik_api_password = "admin"
|
|
||||||
c.TraefikTomlProxy.traefik_api_username = "api_admin"
|
dynamic_conf_file_path = os.path.join(INSTALL_PREFIX, 'state', 'rules.toml')
|
||||||
c.TraefikTomlProxy.toml_dynamic_config_file = "/opt/tljh/state/rules.toml"
|
c.TraefikTomlProxy.toml_dynamic_config_file = dynamic_conf_file_path
|
||||||
c.JupyterHub.proxy_class = TraefikTomlProxy
|
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')]
|
||||||
|
|||||||
@@ -84,34 +84,4 @@ def check_service_active(name):
|
|||||||
], check=True)
|
], check=True)
|
||||||
return True
|
return True
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
return False
|
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
|
|
||||||
@@ -75,11 +75,11 @@ 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:
|
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
|
# 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:
|
||||||
|
|||||||
@@ -41,9 +41,11 @@ idleTimeout = "10m0s"
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
[entryPoints.auth_api]
|
[entryPoints.auth_api]
|
||||||
address = ":8099"
|
address = ":{{auth_api['port']}}"
|
||||||
|
[entryPoints.auth_api.whiteList]
|
||||||
|
sourceRange = ['{{auth_api['ip']}}']
|
||||||
[entryPoints.auth_api.auth.basic]
|
[entryPoints.auth_api.auth.basic]
|
||||||
users = ["api_admin:$apr1$eS/j3kum$q/X2khsIEG/bBGsteP.x./"]
|
users = ['{{auth_api['basic_auth']}}']
|
||||||
|
|
||||||
[wss]
|
[wss]
|
||||||
protocol = "http"
|
protocol = "http"
|
||||||
@@ -67,5 +69,5 @@ entryPoint = "https"
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
[file]
|
[file]
|
||||||
filename = "/opt/tljh/state/rules.toml"
|
filename = "rules.toml"
|
||||||
watch = true
|
watch = true
|
||||||
|
|||||||
Reference in New Issue
Block a user