mirror of
https://github.com/jupyterhub/the-littlest-jupyterhub.git
synced 2025-12-18 21:54:05 +08:00
Merge remote-tracking branch 'upstream/main' into conda-channels
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""pytest fixtures"""
|
||||
from importlib import reload
|
||||
import os
|
||||
import types
|
||||
from importlib import reload
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
129
tests/test_bootstrap_functions.py
Normal file
129
tests/test_bootstrap_functions.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# Unit test some functions from bootstrap.py
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
# Since bootstrap.py isn't part of the package, it's not automatically importable
|
||||
GIT_REPO_PATH = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||
sys.path.insert(0, GIT_REPO_PATH)
|
||||
from bootstrap import bootstrap
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"requested,expected",
|
||||
[
|
||||
((1,), (1, 1, 0)),
|
||||
((1, 0), (1, 0, 1)),
|
||||
((1, 2), None),
|
||||
((2, 0, 0), (2, 0, 0)),
|
||||
("latest", (2, 0, 1)),
|
||||
],
|
||||
)
|
||||
def test_find_matching_version(requested, expected):
|
||||
all_versions = [
|
||||
(0, 0, 1),
|
||||
(1, 0, 0),
|
||||
(1, 0, 1),
|
||||
(1, 1, 0),
|
||||
(2, 0, 0),
|
||||
(2, 0, 1),
|
||||
]
|
||||
r = bootstrap._find_matching_version(all_versions, requested)
|
||||
assert r == expected
|
||||
|
||||
|
||||
# git ls-remote --tags --refs https://github.com/jupyterhub/jupyterhub.git
|
||||
mock_git_ls_remote = """\
|
||||
c345aa658eb482b8102b51f6ec3f0fc667b60520 refs/tags/0.1.0
|
||||
e2cbc2fb41c04cfe416b19e83f36ea67b1a4e693 refs/tags/0.2.0
|
||||
7fd56fe943bf0038cb14e7aa2af5a3e5ad929d47 refs/tags/0.3.0
|
||||
89cf4c60a905b4093ba7584c4561073a9faa0d3d refs/tags/0.4.0
|
||||
bd08efc4a5485a9ecd17882e9bfcab9486d9956a refs/tags/0.4.1
|
||||
19c77d0c7016c08a4a0e883446114a430a551882 refs/tags/0.5.0
|
||||
b4621d354b6bbc865373b7c033f29c6872237780 refs/tags/0.6.0
|
||||
ed38f08daf4d5cf84f04b2d96327681221c579dd refs/tags/0.6.1
|
||||
d5cf9657f2ca16df080e2be21da792288e9f4f99 refs/tags/0.7.0
|
||||
2cdb4be46a0eb291850fc706cfe044873889a9bc refs/tags/0.7.1
|
||||
e4dd65d2611e3c85fe486c561fc0efe9ca720042 refs/tags/0.7.2
|
||||
4179668e49ceedb805cb1a38dc5a70e6a21fa685 refs/tags/0.8.0
|
||||
9bab206eb96c6726ac936cf5da3f61eb9c9aa519 refs/tags/0.8.0b1
|
||||
5f2c6d25fefcbe91d86945f530e6533822449c46 refs/tags/0.8.0b2
|
||||
0d720103c5207d008947580b7b453e1eb0e7594a refs/tags/0.8.0b3
|
||||
a2458ffa402fa2d2905c558100c673e98789a8a8 refs/tags/0.8.0b4
|
||||
b9e2838a4d9b35a9ad7c3353e62ab006b4ec10a4 refs/tags/0.8.0b5
|
||||
a62fc1bc1c9b2408713511cb56e7751403ed5503 refs/tags/0.8.0rc1
|
||||
a77ca08e3e25c14552e246e8ad3ca65a354ba192 refs/tags/0.8.0rc2
|
||||
6eef64842a7d7939b3f1986558849d1977a0e121 refs/tags/0.8.1
|
||||
de46a16029b7ae217293e7e64e14a9c2e06e5e60 refs/tags/0.9.0
|
||||
9f612b52187db878f529458e304bd519fda82e42 refs/tags/0.9.0b1
|
||||
ec4b038b93495eb769007a0d3d56e6d6a5ff000c refs/tags/0.9.0b2
|
||||
ea8a30b1a5b189b2f2f0dbfdb22f83427d1c9163 refs/tags/0.9.0b3
|
||||
99c155a61a1d95a3a8ca41ebb684cdedc1fb170f refs/tags/0.9.0rc1
|
||||
1aeebd4e4937ea5185ce02f693f94272c30f4ebd refs/tags/0.9.1
|
||||
01b3601a12b52259b541b48eaa7a7afb3f7d988c refs/tags/0.9.2
|
||||
70ddc22e071bb7797528831d25c567f6f4920c67 refs/tags/0.9.3
|
||||
7ecb093163a453ae2edfa6bc8bf1f7cfc2320627 refs/tags/0.9.4
|
||||
3e83bc440b8d5abdc0a4336978bd542435402f77 refs/tags/0.9.5
|
||||
cc07c706931c78f46367db9c0c20e6ed9f0f6f85 refs/tags/0.9.6
|
||||
4e24276d40ad83fd113c7c2f1f8619a9ba3af0d8 refs/tags/1.0.0
|
||||
582162c760e81995f4f5405e9c8908d2a76f4abf refs/tags/1.0.0b1
|
||||
1193f6a74c38b36594f6f57c786fa923a2747150 refs/tags/1.0.0b2
|
||||
512dae6cd8a846dd490d77d21fd4e13f59c38961 refs/tags/1.1.0
|
||||
a420c55391273852332ef5f454a0a3b9e0e5b71f refs/tags/1.1.0b1
|
||||
317f0efaf25eb7cb2de4503817cf20937ce110bd refs/tags/1.2.0
|
||||
f66e5d35b5f89a28f6328c91801a8f99e0855a8e refs/tags/1.2.0b1
|
||||
27e1196471729cf6f25fd3786286797e32de973a refs/tags/1.2.1
|
||||
af0c1ed932d00fa26ac91f066a5a9eafb49b7cb1 refs/tags/1.2.2
|
||||
3794abfbdda0a92237f4c31985420691da70da36 refs/tags/1.3.0
|
||||
e22ab5dc93dd8e724b828a0880032f6b5dc00231 refs/tags/1.4.0
|
||||
0656586b75b30091583c0573b3d272cb3add24d2 refs/tags/1.4.1
|
||||
5744ce73bcf0014cc3de6c946f12027448b136da refs/tags/1.4.2
|
||||
c6fb64d8f30686c2c2667b69b53402d506a3bac5 refs/tags/1.5.0
|
||||
4ceb906435dbd4cf800b0480d413303f056e4900 refs/tags/2.0.0
|
||||
61233698dfb353c703ea2e085312b9066ea2e92e refs/tags/2.0.0b1
|
||||
fe61c932409550dc352abf68bd6aaaa8871ac81f refs/tags/2.0.0b2
|
||||
a79c5c5a6bfe553af277f2835419d65b98ae0cb9 refs/tags/2.0.0b3
|
||||
fa1098a998561321de29c6147235032fd6b0c3f5 refs/tags/2.0.0rc1
|
||||
75b115c356983c138c2d8d92cb45f068ad3d9c9d refs/tags/2.0.0rc2
|
||||
ed8e25ef3f471d60b671f2a1cf2db17581c778a2 refs/tags/2.0.0rc3
|
||||
4083307b3f37039075862034963ed42a459b1bdb refs/tags/2.0.0rc4
|
||||
baf1f36dbfe8d8264b3914650b4db6daed843389 refs/tags/2.0.0rc5
|
||||
12961be3b13a10617d8f95f333da2bb67390a2c7 refs/tags/2.0.1
|
||||
e40df3f1a5e284926f5c9ce66a1e57a814bb98f8 refs/tags/2.0.2
|
||||
11d40c13860bd02816ad724979ad2e08b8bd103a refs/tags/2.1.0
|
||||
bde6b66287e3d157f2577bcaf2e986af020139f4 refs/tags/2.1.1
|
||||
29f51794db562ecc4c7653525193d6e210151fdb refs/tags/2.2.0
|
||||
8cbafd25425b7eaf2fdc46e183cee437c09b53c1 refs/tags/2.2.1
|
||||
1eada986101f2385ee7498395a799f28bcd167e8 refs/tags/2.2.2
|
||||
1bb0ec38ae4c5e4e5c8b6cc3b89b7b20ea8bd400 refs/tags/2.3.0
|
||||
69f926706be03505f9b9e30a5ad2d4f8c9f9d48d refs/tags/2.3.1
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"requested,expected",
|
||||
[
|
||||
("1", "1.5.0"),
|
||||
("1.0", "1.0.0"),
|
||||
("1.2", "1.2.2"),
|
||||
("1.100", None),
|
||||
("2.0.0", "2.0.0"),
|
||||
("random-branch", "random-branch"),
|
||||
("1234567890abcdef", "1234567890abcdef"),
|
||||
("2.0.0rc4", "2.0.0rc4"),
|
||||
("latest", "2.3.1"),
|
||||
],
|
||||
)
|
||||
def test_resolve_git_version(monkeypatch, requested, expected):
|
||||
def mock_run_subprocess(*args, **kwargs):
|
||||
return mock_git_ls_remote
|
||||
|
||||
monkeypatch.setattr(bootstrap, "run_subprocess", mock_run_subprocess)
|
||||
|
||||
if expected is None:
|
||||
with pytest.raises(Exception) as exc:
|
||||
bootstrap._resolve_git_version(requested)
|
||||
assert exc.value.args[0].startswith("No version matching 1.100 found")
|
||||
else:
|
||||
assert bootstrap._resolve_git_version(requested) == expected
|
||||
@@ -1,37 +1,26 @@
|
||||
"""
|
||||
Test conda commandline wrappers
|
||||
"""
|
||||
from tljh import conda
|
||||
import os
|
||||
import pytest
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
from tljh import conda, installer
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def prefix():
|
||||
"""
|
||||
Provide a temporary directory with a mambaforge conda environment
|
||||
"""
|
||||
# see https://github.com/conda-forge/miniforge/releases
|
||||
mambaforge_version = "4.10.3-7"
|
||||
if os.uname().machine == "aarch64":
|
||||
installer_sha256 = (
|
||||
"ac95f137b287b3408e4f67f07a284357b1119ee157373b788b34e770ef2392b2"
|
||||
)
|
||||
elif os.uname().machine == "x86_64":
|
||||
installer_sha256 = (
|
||||
"fc872522ec427fcab10167a93e802efaf251024b58cc27b084b915a9a73c4474"
|
||||
)
|
||||
installer_url = "https://github.com/conda-forge/miniforge/releases/download/{v}/Mambaforge-{v}-Linux-{arch}.sh".format(
|
||||
v=mambaforge_version, arch=os.uname().machine
|
||||
)
|
||||
installer_url, checksum = installer._mambaforge_url()
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
with conda.download_miniconda_installer(
|
||||
installer_url, installer_sha256
|
||||
installer_url, checksum
|
||||
) as installer_path:
|
||||
conda.install_miniconda(installer_path, tmpdir)
|
||||
conda.ensure_conda_packages(tmpdir, ["conda==4.10.3"])
|
||||
yield tmpdir
|
||||
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ def test_set_overwrite():
|
||||
def test_unset_no_mutate():
|
||||
conf = {"a": "b"}
|
||||
|
||||
new_conf = config.unset_item_from_config(conf, "a")
|
||||
config.unset_item_from_config(conf, "a")
|
||||
assert conf == {"a": "b"}
|
||||
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
Test configurer
|
||||
"""
|
||||
|
||||
from traitlets.config import Config
|
||||
import os
|
||||
import sys
|
||||
|
||||
from traitlets.config import Config
|
||||
|
||||
from tljh import configurer
|
||||
|
||||
|
||||
@@ -57,24 +58,15 @@ def test_app_default():
|
||||
Test default application with no config overrides.
|
||||
"""
|
||||
c = apply_mock_config({})
|
||||
# default_url is not set, so JupyterHub will pick default.
|
||||
assert "default_url" not in c.Spawner
|
||||
|
||||
|
||||
def test_app_jupyterlab():
|
||||
"""
|
||||
Test setting JupyterLab as default application
|
||||
"""
|
||||
c = apply_mock_config({"user_environment": {"default_app": "jupyterlab"}})
|
||||
assert c.Spawner.default_url == "/lab"
|
||||
|
||||
|
||||
def test_app_nteract():
|
||||
def test_app_classic():
|
||||
"""
|
||||
Test setting nteract as default application
|
||||
Test setting classic as default application
|
||||
"""
|
||||
c = apply_mock_config({"user_environment": {"default_app": "nteract"}})
|
||||
assert c.Spawner.default_url == "/nteract"
|
||||
c = apply_mock_config({"user_environment": {"default_app": "classic"}})
|
||||
assert c.Spawner.default_url == "/tree"
|
||||
|
||||
|
||||
def test_auth_default():
|
||||
@@ -163,8 +155,8 @@ def test_traefik_api_default():
|
||||
"""
|
||||
c = apply_mock_config({})
|
||||
|
||||
assert c.TraefikTomlProxy.traefik_api_username == "api_admin"
|
||||
assert len(c.TraefikTomlProxy.traefik_api_password) == 0
|
||||
assert c.TraefikProxy.traefik_api_username == "api_admin"
|
||||
assert len(c.TraefikProxy.traefik_api_password) == 0
|
||||
|
||||
|
||||
def test_set_traefik_api():
|
||||
@@ -174,8 +166,8 @@ def test_set_traefik_api():
|
||||
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"
|
||||
assert c.TraefikProxy.traefik_api_username == "some_user"
|
||||
assert c.TraefikProxy.traefik_api_password == "1234"
|
||||
|
||||
|
||||
def test_cull_service_default():
|
||||
@@ -228,6 +220,43 @@ def test_set_cull_service():
|
||||
]
|
||||
|
||||
|
||||
def test_cull_service_named():
|
||||
"""
|
||||
Test default cull service settings with named server removal
|
||||
"""
|
||||
c = apply_mock_config(
|
||||
{
|
||||
"services": {
|
||||
"cull": {
|
||||
"every": 10,
|
||||
"cull_users": True,
|
||||
"remove_named_servers": True,
|
||||
"max_age": 60,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
cull_cmd = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"jupyterhub_idle_culler",
|
||||
"--timeout=600",
|
||||
"--cull-every=10",
|
||||
"--concurrency=5",
|
||||
"--max-age=60",
|
||||
"--cull-users",
|
||||
"--remove-named-servers",
|
||||
]
|
||||
assert c.JupyterHub.services == [
|
||||
{
|
||||
"name": "cull-idle",
|
||||
"admin": True,
|
||||
"command": cull_cmd,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_load_secrets(tljh_dir):
|
||||
"""
|
||||
Test loading secret files
|
||||
@@ -238,7 +267,7 @@ def test_load_secrets(tljh_dir):
|
||||
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"
|
||||
assert c.TraefikProxy.traefik_api_password == "traefik-password"
|
||||
|
||||
|
||||
def test_auth_native():
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
"""
|
||||
Unit test functions in installer.py
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import pytest
|
||||
from subprocess import PIPE, run
|
||||
from unittest import mock
|
||||
|
||||
from tljh import installer
|
||||
import pytest
|
||||
from packaging.specifiers import SpecifierSet
|
||||
from packaging.version import parse as V
|
||||
|
||||
from tljh import conda, installer
|
||||
from tljh.yaml import yaml
|
||||
|
||||
|
||||
@@ -36,3 +42,202 @@ def test_ensure_admins(tljh_dir, admins, expected_config):
|
||||
|
||||
# verify the list was flattened
|
||||
assert config["users"]["admin"] == expected_config
|
||||
|
||||
|
||||
def setup_conda(distro, version, prefix):
|
||||
"""Install mambaforge or miniconda in a prefix"""
|
||||
if distro == "mambaforge":
|
||||
installer_url, _ = installer._mambaforge_url(version)
|
||||
elif distro == "miniforge":
|
||||
installer_url, _ = installer._mambaforge_url(version)
|
||||
installer_url = installer_url.replace("Mambaforge", "Miniforge3")
|
||||
elif distro == "miniconda":
|
||||
arch = os.uname().machine
|
||||
installer_url = (
|
||||
f"https://repo.anaconda.com/miniconda/Miniconda3-{version}-Linux-{arch}.sh"
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"{distro=} must be 'miniconda' or 'mambaforge' or 'miniforge'"
|
||||
)
|
||||
with conda.download_miniconda_installer(installer_url, None) as installer_path:
|
||||
conda.install_miniconda(installer_path, str(prefix))
|
||||
# avoid auto-updating conda when we install other packages
|
||||
run(
|
||||
[
|
||||
str(prefix / "bin/conda"),
|
||||
"config",
|
||||
"--system",
|
||||
"--set",
|
||||
"auto_update_conda",
|
||||
"false",
|
||||
],
|
||||
input="",
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_env_prefix(tmp_path):
|
||||
user_env_prefix = tmp_path / "user_env"
|
||||
with mock.patch.object(installer, "USER_ENV_PREFIX", str(user_env_prefix)):
|
||||
yield user_env_prefix
|
||||
|
||||
|
||||
def _specifier(version):
|
||||
"""Convert version string to SpecifierSet
|
||||
|
||||
If just a version number, add == to make it a specifier
|
||||
|
||||
Any missing fields are replaced with .*
|
||||
|
||||
If it's already a specifier string, pass it directly to SpecifierSet
|
||||
|
||||
e.g.
|
||||
|
||||
- 3.7 -> ==3.7.*
|
||||
- 1.2.3 -> ==1.2.3
|
||||
"""
|
||||
if version[0].isdigit():
|
||||
# it's a version number, not a specifier
|
||||
if version.count(".") < 2:
|
||||
# pad missing fields
|
||||
version += ".*"
|
||||
version = f"=={version}"
|
||||
return SpecifierSet(version)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
# - distro: None, mambaforge, or miniforge
|
||||
# - distro_version: https://github.com/conda-forge/miniforge/releases
|
||||
# - expected_versions: versions of python, conda, and mamba in user env
|
||||
#
|
||||
# TLJH of a specific version comes with a specific distro_version as
|
||||
# declared in installer.py's MAMBAFORGE_VERSION variable, and it comes with
|
||||
# python, conda, and mamba of certain versions.
|
||||
#
|
||||
"distro, distro_version, expected_versions",
|
||||
[
|
||||
# No previous install, start fresh
|
||||
(
|
||||
None,
|
||||
None,
|
||||
{
|
||||
"python": "3.10.*",
|
||||
"conda": "23.1.0",
|
||||
"mamba": "1.4.1",
|
||||
},
|
||||
),
|
||||
# previous install, 1.0
|
||||
(
|
||||
"mambaforge",
|
||||
"23.1.0-1",
|
||||
{
|
||||
"python": "3.10.*",
|
||||
"conda": "23.1.0",
|
||||
"mamba": "1.4.1",
|
||||
},
|
||||
),
|
||||
# 0.2 install, no upgrade needed
|
||||
(
|
||||
"mambaforge",
|
||||
"4.10.3-7",
|
||||
{
|
||||
"python": "3.9.*",
|
||||
"conda": "4.10.3",
|
||||
"mamba": "0.16.0",
|
||||
},
|
||||
),
|
||||
# simulate missing mamba
|
||||
# will be installed but not pinned
|
||||
# to avoid conflicts
|
||||
(
|
||||
"miniforge",
|
||||
"4.10.3-7",
|
||||
{
|
||||
"python": "3.9.*",
|
||||
"conda": "4.10.3",
|
||||
"mamba": ">=1.1.0",
|
||||
},
|
||||
),
|
||||
# too-old Python (3.7), abort
|
||||
(
|
||||
"miniconda",
|
||||
"4.7.10",
|
||||
ValueError,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_ensure_user_environment(
|
||||
user_env_prefix,
|
||||
distro,
|
||||
distro_version,
|
||||
expected_versions,
|
||||
):
|
||||
if (
|
||||
distro_version
|
||||
and V(distro_version) < V("4.10.1")
|
||||
and os.uname().machine == "aarch64"
|
||||
):
|
||||
pytest.skip(f"{distro} {distro_version} not available for aarch64")
|
||||
canary_file = user_env_prefix / "test-file.txt"
|
||||
canary_package = "types-backports_abc"
|
||||
if distro:
|
||||
setup_conda(distro, distro_version, user_env_prefix)
|
||||
# install a noarch: python package that won't be used otherwise
|
||||
# should depend on Python, so it will interact with possible upgrades
|
||||
pkgs = [canary_package]
|
||||
run(
|
||||
[
|
||||
str(user_env_prefix / "bin/conda"),
|
||||
"install",
|
||||
"-S",
|
||||
"-y",
|
||||
"-c",
|
||||
"conda-forge",
|
||||
]
|
||||
+ pkgs,
|
||||
input="",
|
||||
check=True,
|
||||
)
|
||||
|
||||
# make a file not managed by conda, to check for wipeouts
|
||||
with canary_file.open("w") as f:
|
||||
f.write("I'm here\n")
|
||||
|
||||
if isinstance(expected_versions, type) and issubclass(expected_versions, Exception):
|
||||
exc_class = expected_versions
|
||||
with pytest.raises(exc_class):
|
||||
installer.ensure_user_environment("")
|
||||
return
|
||||
else:
|
||||
installer.ensure_user_environment("")
|
||||
|
||||
p = run(
|
||||
[str(user_env_prefix / "bin/conda"), "list", "--json"],
|
||||
stdout=PIPE,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
package_list = json.loads(p.stdout)
|
||||
packages = {package["name"]: package for package in package_list}
|
||||
|
||||
if distro:
|
||||
# make sure we didn't wipe out files
|
||||
assert canary_file.exists()
|
||||
# make sure we didn't delete the installed package
|
||||
assert canary_package in packages
|
||||
|
||||
for pkg, version in expected_versions.items():
|
||||
assert pkg in packages
|
||||
assert V(packages[pkg]["version"]) in _specifier(version)
|
||||
|
||||
|
||||
def test_ensure_user_environment_no_clobber(user_env_prefix):
|
||||
# don't clobber existing user-env dir if it's non-empty and not a conda install
|
||||
user_env_prefix.mkdir()
|
||||
canary_file = user_env_prefix / "test-file.txt"
|
||||
with canary_file.open("w") as f:
|
||||
pass
|
||||
with pytest.raises(OSError):
|
||||
installer.ensure_user_environment("")
|
||||
|
||||
@@ -4,7 +4,7 @@ Unit test functions in installer.py
|
||||
import os
|
||||
from datetime import date
|
||||
|
||||
from tljh import migrator, config
|
||||
from tljh import config, migrator
|
||||
|
||||
|
||||
def test_migrate_config(tljh_dir):
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""Test traefik configuration"""
|
||||
import os
|
||||
|
||||
import toml
|
||||
import pytest
|
||||
import toml
|
||||
|
||||
from tljh import config
|
||||
from tljh import traefik
|
||||
from tljh import config, traefik
|
||||
|
||||
|
||||
def test_download_traefik(tmpdir):
|
||||
@@ -16,30 +15,51 @@ def test_download_traefik(tmpdir):
|
||||
assert (traefik_bin.stat().mode & 0o777) == 0o755
|
||||
|
||||
|
||||
def _read_toml(path):
|
||||
"""Read a toml file
|
||||
|
||||
print config for debugging on failure
|
||||
"""
|
||||
print(path)
|
||||
with open(path) as f:
|
||||
toml_cfg = f.read()
|
||||
print(toml_cfg)
|
||||
return toml.loads(toml_cfg)
|
||||
|
||||
|
||||
def _read_static_config(state_dir):
|
||||
return _read_toml(os.path.join(state_dir, "traefik.toml"))
|
||||
|
||||
|
||||
def _read_dynamic_config(state_dir):
|
||||
return _read_toml(os.path.join(state_dir, "rules", "dynamic.toml"))
|
||||
|
||||
|
||||
def test_default_config(tmpdir, tljh_dir):
|
||||
state_dir = tmpdir.mkdir("state")
|
||||
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:
|
||||
toml_cfg = f.read()
|
||||
# print config for debugging on failure
|
||||
print(config.CONFIG_FILE)
|
||||
print(toml_cfg)
|
||||
cfg = toml.loads(toml_cfg)
|
||||
assert cfg["defaultEntryPoints"] == ["http"]
|
||||
assert len(cfg["entryPoints"]["auth_api"]["auth"]["basic"]["users"]) == 1
|
||||
# runtime generated entry, value not testable
|
||||
cfg["entryPoints"]["auth_api"]["auth"]["basic"]["users"] = [""]
|
||||
os.path.join(state_dir, "traefik.toml")
|
||||
rules_dir = os.path.join(state_dir, "rules")
|
||||
|
||||
cfg = _read_static_config(state_dir)
|
||||
assert cfg["api"] == {}
|
||||
assert cfg["entryPoints"] == {
|
||||
"http": {"address": ":80"},
|
||||
"http": {
|
||||
"address": ":80",
|
||||
"transport": {"respondingTimeouts": {"idleTimeout": "10m"}},
|
||||
},
|
||||
"auth_api": {
|
||||
"address": "127.0.0.1:8099",
|
||||
"auth": {"basic": {"users": [""]}},
|
||||
"whiteList": {"sourceRange": ["127.0.0.1"]},
|
||||
"address": "localhost:8099",
|
||||
},
|
||||
}
|
||||
assert cfg["providers"] == {
|
||||
"providersThrottleDuration": "0s",
|
||||
"file": {"directory": rules_dir, "watch": True},
|
||||
}
|
||||
|
||||
dynamic_config = _read_dynamic_config(state_dir)
|
||||
assert dynamic_config == {}
|
||||
|
||||
|
||||
def test_letsencrypt_config(tljh_dir):
|
||||
@@ -52,34 +72,67 @@ def test_letsencrypt_config(tljh_dir):
|
||||
config.CONFIG_FILE, "https.letsencrypt.domains", ["testing.jovyan.org"]
|
||||
)
|
||||
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()
|
||||
# print config for debugging on failure
|
||||
print(config.CONFIG_FILE)
|
||||
print(toml_cfg)
|
||||
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"] = [""]
|
||||
|
||||
cfg = _read_static_config(state_dir)
|
||||
assert cfg["entryPoints"] == {
|
||||
"http": {"address": ":80", "redirect": {"entryPoint": "https"}},
|
||||
"https": {"address": ":443", "tls": {"minVersion": "VersionTLS12"}},
|
||||
"http": {
|
||||
"address": ":80",
|
||||
"http": {
|
||||
"redirections": {
|
||||
"entryPoint": {
|
||||
"scheme": "https",
|
||||
"to": "https",
|
||||
},
|
||||
},
|
||||
},
|
||||
"transport": {"respondingTimeouts": {"idleTimeout": "10m"}},
|
||||
},
|
||||
"https": {
|
||||
"address": ":443",
|
||||
"http": {"tls": {"options": "default"}},
|
||||
"transport": {"respondingTimeouts": {"idleTimeout": "10m"}},
|
||||
},
|
||||
"auth_api": {
|
||||
"address": "127.0.0.1:8099",
|
||||
"auth": {"basic": {"users": [""]}},
|
||||
"whiteList": {"sourceRange": ["127.0.0.1"]},
|
||||
"address": "localhost:8099",
|
||||
},
|
||||
}
|
||||
assert cfg["acme"] == {
|
||||
assert "tls" not in cfg
|
||||
|
||||
dynamic_config = _read_dynamic_config(state_dir)
|
||||
|
||||
assert dynamic_config["tls"] == {
|
||||
"options": {
|
||||
"default": {
|
||||
"minVersion": "VersionTLS12",
|
||||
"cipherSuites": [
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
|
||||
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
|
||||
],
|
||||
}
|
||||
},
|
||||
"stores": {
|
||||
"default": {
|
||||
"defaultGeneratedCert": {
|
||||
"resolver": "letsencrypt",
|
||||
"domain": {
|
||||
"main": "testing.jovyan.org",
|
||||
"sans": [],
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
assert "certificatesResolvers" in cfg
|
||||
assert "letsencrypt" in cfg["certificatesResolvers"]
|
||||
|
||||
assert cfg["certificatesResolvers"]["letsencrypt"]["acme"] == {
|
||||
"email": "fake@jupyter.org",
|
||||
"storage": "acme.json",
|
||||
"entryPoint": "https",
|
||||
"httpChallenge": {"entryPoint": "http"},
|
||||
"domains": [{"main": "testing.jovyan.org"}],
|
||||
"tlsChallenge": {},
|
||||
}
|
||||
|
||||
|
||||
@@ -89,33 +142,62 @@ def test_manual_ssl_config(tljh_dir):
|
||||
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))
|
||||
traefik_toml = os.path.join(state_dir, "traefik.toml")
|
||||
with open(traefik_toml) as f:
|
||||
toml_cfg = f.read()
|
||||
# print config for debugging on failure
|
||||
print(config.CONFIG_FILE)
|
||||
print(toml_cfg)
|
||||
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"] = [""]
|
||||
|
||||
cfg = _read_static_config(state_dir)
|
||||
|
||||
assert cfg["entryPoints"] == {
|
||||
"http": {"address": ":80", "redirect": {"entryPoint": "https"}},
|
||||
"http": {
|
||||
"address": ":80",
|
||||
"http": {
|
||||
"redirections": {
|
||||
"entryPoint": {
|
||||
"scheme": "https",
|
||||
"to": "https",
|
||||
},
|
||||
},
|
||||
},
|
||||
"transport": {
|
||||
"respondingTimeouts": {
|
||||
"idleTimeout": "10m",
|
||||
}
|
||||
},
|
||||
},
|
||||
"https": {
|
||||
"address": ":443",
|
||||
"tls": {
|
||||
"http": {"tls": {"options": "default"}},
|
||||
"transport": {"respondingTimeouts": {"idleTimeout": "10m"}},
|
||||
},
|
||||
"auth_api": {
|
||||
"address": "localhost:8099",
|
||||
},
|
||||
}
|
||||
assert "tls" not in cfg
|
||||
|
||||
dynamic_config = _read_dynamic_config(state_dir)
|
||||
|
||||
assert "tls" in dynamic_config
|
||||
|
||||
assert dynamic_config["tls"] == {
|
||||
"options": {
|
||||
"default": {
|
||||
"minVersion": "VersionTLS12",
|
||||
"certificates": [
|
||||
{"certFile": "/path/to/ssl.cert", "keyFile": "/path/to/ssl.key"}
|
||||
"cipherSuites": [
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
|
||||
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
|
||||
],
|
||||
},
|
||||
},
|
||||
"auth_api": {
|
||||
"address": "127.0.0.1:8099",
|
||||
"auth": {"basic": {"users": [""]}},
|
||||
"whiteList": {"sourceRange": ["127.0.0.1"]},
|
||||
"stores": {
|
||||
"default": {
|
||||
"defaultCertificate": {
|
||||
"certFile": "/path/to/ssl.cert",
|
||||
"keyFile": "/path/to/ssl.key",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -132,18 +214,18 @@ def test_extra_config(tmpdir, tljh_dir):
|
||||
toml_cfg = toml.load(traefik_toml)
|
||||
|
||||
# Make sure the defaults are what we expect
|
||||
assert toml_cfg["logLevel"] == "INFO"
|
||||
assert toml_cfg["log"]["level"] == "INFO"
|
||||
with pytest.raises(KeyError):
|
||||
toml_cfg["checkNewVersion"]
|
||||
assert toml_cfg["entryPoints"]["auth_api"]["address"] == "127.0.0.1:8099"
|
||||
toml_cfg["api"]["dashboard"]
|
||||
assert toml_cfg["entryPoints"]["auth_api"]["address"] == "localhost:8099"
|
||||
|
||||
extra_config = {
|
||||
# modify existing value
|
||||
"logLevel": "ERROR",
|
||||
# modify existing value with multiple levels
|
||||
"entryPoints": {"auth_api": {"address": "127.0.0.1:9999"}},
|
||||
"log": {
|
||||
"level": "ERROR",
|
||||
},
|
||||
# add new setting
|
||||
"checkNewVersion": False,
|
||||
"api": {"dashboard": True},
|
||||
}
|
||||
|
||||
with open(os.path.join(extra_config_dir, "extra.toml"), "w+") as extra_config_file:
|
||||
@@ -156,6 +238,21 @@ def test_extra_config(tmpdir, tljh_dir):
|
||||
toml_cfg = toml.load(traefik_toml)
|
||||
|
||||
# Check that the defaults were updated by the extra config
|
||||
assert toml_cfg["logLevel"] == "ERROR"
|
||||
assert toml_cfg["checkNewVersion"] == False
|
||||
assert toml_cfg["entryPoints"]["auth_api"]["address"] == "127.0.0.1:9999"
|
||||
assert toml_cfg["log"]["level"] == "ERROR"
|
||||
assert toml_cfg["api"]["dashboard"] == True
|
||||
|
||||
|
||||
def test_listen_address(tmpdir, tljh_dir):
|
||||
state_dir = config.STATE_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")
|
||||
|
||||
config.set_config_value(config.CONFIG_FILE, "http.address", "127.0.0.1")
|
||||
config.set_config_value(config.CONFIG_FILE, "https.address", "127.0.0.1")
|
||||
|
||||
traefik.ensure_traefik_config(str(state_dir))
|
||||
|
||||
cfg = _read_static_config(state_dir)
|
||||
assert cfg["entryPoints"]["http"]["address"] == "127.0.0.1:80"
|
||||
assert cfg["entryPoints"]["https"]["address"] == "127.0.0.1:443"
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
"""
|
||||
Test wrappers in tljw.user module
|
||||
"""
|
||||
from tljh import user
|
||||
import grp
|
||||
import os
|
||||
import os.path
|
||||
import pwd
|
||||
import stat
|
||||
import uuid
|
||||
import pwd
|
||||
import grp
|
||||
|
||||
import pytest
|
||||
|
||||
from tljh import user
|
||||
|
||||
|
||||
def test_ensure_user():
|
||||
"""
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import pytest
|
||||
from tljh import utils
|
||||
import subprocess
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from tljh import utils
|
||||
|
||||
|
||||
def test_run_subprocess_exception(mocker):
|
||||
|
||||
Reference in New Issue
Block a user