Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace chp with traefik-proxy #266

Merged
merged 14 commits into from
Feb 22, 2019
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/integration-test.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def show_logs(container_name):
)
run_container_command(
container_name,
'systemctl --no-pager status jupyterhub configurable-http-proxy'
'systemctl --no-pager status jupyterhub traefik'
)

def main():
Expand Down
2 changes: 1 addition & 1 deletion dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pytest
pytest-cov
codecov
pytoml
pytoml
17 changes: 1 addition & 16 deletions integration-tests/test_hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, '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:
await u.login()
await u.ensure_server()
Expand All @@ -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, '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:
await u.login()
await u.ensure_server()
Expand Down Expand Up @@ -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, '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:
await u.login()
await u.ensure_server()
Expand All @@ -107,16 +97,14 @@ async def test_user_admin_remove():
# Assert that the user has admin rights
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, 'reload')).wait()
await asyncio.sleep(1)

await u.stop_server()
await u.ensure_server()

# 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
Expand All @@ -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, 'reload')).wait()

# FIXME: wait for reload to finish & hub to come up
# Should be part of tljh-config reload
await asyncio.sleep(1)
try:
async with User(username, hub_url, partial(login_dummy, password='')) as u:
await u.login()
Expand Down
9 changes: 7 additions & 2 deletions integration-tests/test_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
install_requires=[
'ruamel.yaml==0.15.*',
'jinja2',
'pluggy>0.7<1.0'
'pluggy>0.7<1.0',
'passlib',
'jupyterhub-traefik-proxy==0.1.0a1'
GeorgianaElena marked this conversation as resolved.
Show resolved Hide resolved
],
entry_points={
'console_scripts': [
Expand Down
43 changes: 33 additions & 10 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def test_add_to_config_zero_level():
'a': ['b']
}


def test_add_to_config_multiple():
conf = {}

Expand Down Expand Up @@ -116,16 +117,23 @@ def test_remove_from_config_error():


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')
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, mock.patch(
"tljh.configurer.generate_traefik_api_credentials"
) as generate_credentials:
config.reload_component('proxy')
assert restart_service.called_with('configurable-http-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'))


Expand All @@ -140,34 +148,49 @@ def test_cli_no_command(capsys):
"arg, value",
[
("true", True),
("FALSE", False),
],
("FALSE", False)
]
)
def test_cli_set_bool(tljh_dir, arg, value):
config.main(["set", "https.enabled", arg])
cfg = configurer.load_config()
with mock.patch(
"tljh.configurer.generate_traefik_api_credentials"
) as generate_credentials:
cfg = configurer.load_config()
assert cfg['https']['enabled'] == value


def test_cli_set_int(tljh_dir):
config.main(["set", "https.port", "123"])
cfg = configurer.load_config()
with mock.patch(
"tljh.configurer.generate_traefik_api_credentials"
) as generate_credentials:
cfg = configurer.load_config()
assert cfg['https']['port'] == 123


def test_cli_add_float(tljh_dir):
config.main(["add-item", "foo.bar", "1.25"])
cfg = configurer.load_config()
with mock.patch(
"tljh.configurer.generate_traefik_api_credentials"
) as generate_credentials:
cfg = configurer.load_config()
assert cfg['foo']['bar'] == [1.25]


def test_cli_remove_int(tljh_dir):
config.main(["add-item", "foo.bar", "1"])
config.main(["add-item", "foo.bar", "2"])
cfg = configurer.load_config()
with mock.patch(
"tljh.configurer.generate_traefik_api_credentials"
) as generate_credentials:
cfg = configurer.load_config()
assert cfg['foo']['bar'] == [1, 2]
config.main(["remove-item", "foo.bar", "1"])
cfg = configurer.load_config()
with mock.patch(
"tljh.configurer.generate_traefik_api_credentials"
) as generate_credentials:
cfg = configurer.load_config()
assert cfg['foo']['bar'] == [2]


Expand Down
24 changes: 24 additions & 0 deletions tests/test_configurer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 len(c.TraefikTomlProxy.traefik_api_password) == 0


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'
59 changes: 48 additions & 11 deletions tests/test_traefik.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Test traefik configuration"""
import os
from unittest import mock

import pytoml as toml

Expand All @@ -17,7 +18,10 @@ def test_download_traefik(tmpdir):

def test_default_config(tmpdir, tljh_dir):
state_dir = tmpdir.mkdir("state")
traefik.ensure_traefik_config(str(state_dir))
with mock.patch(
"tljh.configurer.generate_traefik_api_credentials"
) as generate_credentials:
traefik.ensure_traefik_config(str(state_dir))
assert state_dir.join("traefik.toml").exists()
traefik_toml = os.path.join(state_dir, "traefik.toml")
with open(traefik_toml) as f:
Expand All @@ -27,12 +31,19 @@ def test_default_config(tmpdir, tljh_dir):
print(toml_cfg)
cfg = toml.loads(toml_cfg)
assert cfg["defaultEntryPoints"] == ["http"]
assert cfg["entryPoints"] == {"http": {"address": ":80"}}
assert cfg["frontends"] == {
"jupyterhub": {"backend": "jupyterhub", "passHostHeader": True}
}
assert cfg["backends"] == {
"jupyterhub": {"servers": {"chp": {"url": "http://127.0.0.1:15003"}}}
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"] == {
"http": {"address": ":80"},
"auth_api": {
"address": "127.0.0.1:8099",
"auth": {
"basic": {"users": [""]}
},
"whiteList": {"sourceRange": ["127.0.0.1"]}
},
}


Expand All @@ -45,7 +56,10 @@ def test_letsencrypt_config(tljh_dir):
config.set_config_value(
config.CONFIG_FILE, "https.letsencrypt.domains", ["testing.jovyan.org"]
)
traefik.ensure_traefik_config(str(state_dir))
with mock.patch(
"tljh.configurer.generate_traefik_api_credentials"
) as generate_credentials:
traefik.ensure_traefik_config(str(state_dir))
traefik_toml = os.path.join(state_dir, "traefik.toml")
with open(traefik_toml) as f:
toml_cfg = f.read()
Expand All @@ -55,9 +69,20 @@ def test_letsencrypt_config(tljh_dir):
cfg = toml.loads(toml_cfg)
assert cfg["defaultEntryPoints"] == ["http", "https"]
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"] == {
"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"] == {
"email": "fake@jupyter.org",
Expand All @@ -73,7 +98,10 @@ def test_manual_ssl_config(tljh_dir):
config.set_config_value(config.CONFIG_FILE, "https.enabled", True)
config.set_config_value(config.CONFIG_FILE, "https.tls.key", "/path/to/ssl.key")
config.set_config_value(config.CONFIG_FILE, "https.tls.cert", "/path/to/ssl.cert")
traefik.ensure_traefik_config(str(state_dir))
with mock.patch(
"tljh.configurer.generate_traefik_api_credentials"
) as generate_credentials:
traefik.ensure_traefik_config(str(state_dir))
traefik_toml = os.path.join(state_dir, "traefik.toml")
with open(traefik_toml) as f:
toml_cfg = f.read()
Expand All @@ -83,15 +111,24 @@ def test_manual_ssl_config(tljh_dir):
cfg = toml.loads(toml_cfg)
assert cfg["defaultEntryPoints"] == ["http", "https"]
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"] == {
"http": {"address": ":80", "redirect": {"entryPoint": "https"}},
"https": {
"address": ":443",
"backend": "jupyterhub",
"tls": {
"certificates": [
{"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"]}
},
}
22 changes: 19 additions & 3 deletions tljh/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@
"""

import argparse
import asyncio
from collections import Sequence, Mapping
from copy import deepcopy
import os
import re
import sys
import time

import requests

from .yaml import yaml

Expand Down Expand Up @@ -85,7 +89,7 @@ def add_item_to_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('.')

Expand Down Expand Up @@ -172,6 +176,12 @@ def remove_config_value(config_path, key_path, value):
with open(config_path, 'w') as 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):
"""
Expand All @@ -181,14 +191,20 @@ def reload_component(component):
"""
# import here to avoid circular imports
from tljh import systemd, traefik

if component == 'hub':
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')
elif component == 'proxy':
traefik.ensure_traefik_config(STATE_DIR)
systemd.restart_service('configurable-http-proxy')
systemd.restart_service('traefik')
while not systemd.check_service_active('traefik'):
time.sleep(1)
print('Proxy reload with new configuration complete')


Expand Down