diff --git a/.github/integration-test.py b/.github/integration-test.py index 6d05ecb..25af580 100755 --- a/.github/integration-test.py +++ b/.github/integration-test.py @@ -1,19 +1,68 @@ #!/usr/bin/env python3 import argparse +from shutil import which import subprocess +import time import os +def container_runtime(): + runtimes = ["docker", "podman"] + for runtime in runtimes: + if which(runtime): + return runtime + raise RuntimeError(f"No container runtime found, tried: {' '.join(runtimes)}") + + +def container_check_output(*args, **kwargs): + cmd = [container_runtime()] + list(*args) + print(f"Running {cmd} {kwargs}") + return subprocess.check_output(cmd, **kwargs) + + +def container_run(*args, **kwargs): + cmd = [container_runtime()] + list(*args) + print(f"Running {cmd} {kwargs}") + return subprocess.run(cmd, **kwargs) + + def build_systemd_image(image_name, source_path, build_args=None): """ Build docker image with systemd at source_path. Built image is tagged with image_name """ - cmd = ["docker", "build", f"-t={image_name}", source_path] + cmd = ["build", f"-t={image_name}", source_path] if build_args: cmd.extend([f"--build-arg={ba}" for ba in build_args]) - subprocess.check_call(cmd) + container_check_output(cmd) + + +def check_container_ready(container_name, timeout=60): + """ + Check if container is ready to run tests + """ + now = time.time() + while True: + try: + out = container_check_output(["exec", "-t", container_name, "id"]) + print(out.decode()) + return + except subprocess.CalledProcessError as e: + print(e) + try: + out = container_check_output(["inspect", container_name]) + print(out.decode()) + except subprocess.CalledProcessError as e: + print(e) + try: + out = container_check_output(["logs", container_name]) + print(out.decode()) + except subprocess.CalledProcessError as e: + print(e) + if time.time() - now > timeout: + raise RuntimeError(f"Container {container_name} hasn't started") + time.sleep(5) def run_systemd_image(image_name, container_name, bootstrap_pip_spec): @@ -25,10 +74,8 @@ def run_systemd_image(image_name, container_name, bootstrap_pip_spec): Container named container_name will be started. """ cmd = [ - "docker", "run", "--privileged", - "--mount=type=bind,source=/sys/fs/cgroup,target=/sys/fs/cgroup", "--detach", f"--name={container_name}", # A bit less than 1GB to ensure TLJH runs on 1GB VMs. @@ -42,7 +89,7 @@ def run_systemd_image(image_name, container_name, bootstrap_pip_spec): cmd.append(image_name) - subprocess.check_call(cmd) + container_check_output(cmd) def stop_container(container_name): @@ -50,21 +97,20 @@ def stop_container(container_name): Stop & remove docker container if it exists. """ try: - subprocess.check_output( - ["docker", "inspect", container_name], stderr=subprocess.STDOUT - ) + container_check_output(["inspect", container_name], stderr=subprocess.STDOUT) except subprocess.CalledProcessError: # No such container exists, nothing to do return - subprocess.check_call(["docker", "rm", "-f", container_name]) + container_check_output(["rm", "-f", container_name]) def run_container_command(container_name, cmd): """ Run cmd in a running container with a bash shell """ - proc = subprocess.run( - ["docker", "exec", "-t", container_name, "/bin/bash", "-c", cmd], check=True + proc = container_run( + ["exec", "-t", container_name, "/bin/bash", "-c", cmd], + check=True, ) @@ -72,7 +118,7 @@ def copy_to_container(container_name, src_path, dest_path): """ Copy files from src_path to dest_path inside container_name """ - subprocess.check_call(["docker", "cp", src_path, f"{container_name}:{dest_path}"]) + container_check_output(["cp", src_path, f"{container_name}:{dest_path}"]) def run_test( @@ -84,6 +130,8 @@ def run_test( stop_container(test_name) run_systemd_image(image_name, test_name, bootstrap_pip_spec) + check_container_ready(test_name) + source_path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) copy_to_container(test_name, os.path.join(source_path, "bootstrap/."), "/srv/src") @@ -93,7 +141,7 @@ def run_test( # These logs can be very relevant to debug a container startup failure print(f"--- Start of logs from the container: {test_name}") - print(subprocess.check_output(["docker", "logs", test_name]).decode()) + print(container_check_output(["logs", test_name]).decode()) print(f"--- End of logs from the container: {test_name}") # Install TLJH from the default branch first to test upgrades diff --git a/docs/requirements.txt b/docs/requirements.txt index 7d2b038..2de526f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,7 @@ pydata-sphinx-theme -sphinx>=4 +# Sphix 6.0.0 breaks pydata-sphinx-theme +# See pydata/pydata-sphinx-theme#1094 +sphinx<6 sphinx_copybutton sphinx-autobuild sphinxext-opengraph diff --git a/integration-tests/test_install.py b/integration-tests/test_install.py index 4411e10..1391ddd 100644 --- a/integration-tests/test_install.py +++ b/integration-tests/test_install.py @@ -35,6 +35,7 @@ def setgroup(group): gid = grp.getgrnam(group).gr_gid uid = pwd.getpwnam("nobody").pw_uid os.setgid(gid) + os.setgroups([]) os.setuid(uid) os.environ["HOME"] = "/tmp/test-home-%i-%i" % (uid, gid) @@ -45,6 +46,10 @@ def test_groups_exist(group): grp.getgrnam(group) +def debug_uid_gid(): + return subprocess.check_output("id").decode() + + def permissions_test(group, path, *, readable=None, writable=None, dirs_only=False): """Run a permissions test on all files in a path path""" # start a subprocess and become nobody:group in the process @@ -88,18 +93,22 @@ def permissions_test(group, path, *, readable=None, writable=None, dirs_only=Fal # check if the path should be writable if writable is not None: if access(path, os.W_OK) != writable: + stat = os.stat(path) + info = pool.submit(debug_uid_gid).result() failures.append( - "{} {} should {}be writable by {}".format( - stat_str, path, "" if writable else "not ", group + "{} {} should {}be writable by {} [{}]".format( + stat_str, path, "" if writable else "not ", group, info ) ) # check if the path should be readable if readable is not None: if access(path, os.R_OK) != readable: + stat = os.stat(path) + info = pool.submit(debug_uid_gid).result() failures.append( - "{} {} should {}be readable by {}".format( - stat_str, path, "" if readable else "not ", group + "{} {} should {}be readable by {} [{}]".format( + stat_str, path, "" if readable else "not ", group, info ) ) # verify that we actually tested some files diff --git a/tljh/installer.py b/tljh/installer.py index 42ba070..7e9948d 100644 --- a/tljh/installer.py +++ b/tljh/installer.py @@ -121,6 +121,7 @@ def ensure_jupyterhub_package(prefix): conda.ensure_pip_packages( prefix, [ + "SQLAlchemy<2.0.0", "jupyterhub==1.*", "jupyterhub-systemdspawner==0.16.*", "jupyterhub-firstuseauthenticator==1.*", diff --git a/tljh/requirements-base.txt b/tljh/requirements-base.txt index 02df904..1985db7 100644 --- a/tljh/requirements-base.txt +++ b/tljh/requirements-base.txt @@ -5,6 +5,8 @@ # the requirements-txt-fixer pre-commit hook that sorted them and made # our integration tests fail. # +# For JupyterHub 1.x SQLAlchemy below 2.0.0 +SQLAlchemy<2.0.0 # JupyterHub + notebook package are base requirements for user environment jupyterhub==1.* notebook==6.*