Test server listening on IPv4/IPv6 (#2255)

* Test server listening on IPv4/IPv6

* Set up Docker in create-dev-env

* Show docker version

* Add info about docker client

* Check requests

* Show docker client version

* Try to pass docker sock

* Fix

* Break fast

* Revert

* Cleanup

* Better naming

* Always use docker.from_env

* Revert "Always use docker.from_env"

This reverts commit d03069ac28.

* Use custom docker client for only one test

* More logs

* Use cont_data_dir in test, so workdir doesn't matter

* Use common variable names

* Move patch to a separate function

* Try to use set-host option

* Use the same docker client in get_health

* Use .api

* Rewrite check_listening.py to use one function for both ipv4 and ipv6

* Add links to explain why we need to set up docker manually
This commit is contained in:
Ayaz Salikhov
2025-03-20 17:12:22 +00:00
committed by GitHub
parent 951dec9330
commit b35f1554d6
6 changed files with 134 additions and 9 deletions

View File

@@ -14,3 +14,12 @@ runs:
pip install --upgrade pip pip install --upgrade pip
pip install --upgrade -r requirements-dev.txt pip install --upgrade -r requirements-dev.txt
shell: bash shell: bash
# We need to have a recent docker version
# More info: https://github.com/jupyter/docker-stacks/pull/2255
# Can be removed after Docker Engine is updated
# https://github.com/actions/runner-images/issues/11766
- name: Set Up Docker 🐳
uses: docker/setup-docker-action@b60f85385d03ac8acfca6d9996982511d8620a19 # v4.3.0
with:
set-host: true

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import socket
import time
import requests
def make_get_request() -> None:
# Give some time for server to start
finish_time = time.time() + 10
sleep_time = 1
while time.time() < finish_time:
time.sleep(sleep_time)
try:
resp = requests.get("http://localhost:8888/api")
resp.raise_for_status()
except requests.RequestException:
pass
resp.raise_for_status()
def check_addrs(family: socket.AddressFamily) -> None:
assert family in {socket.AF_INET, socket.AF_INET6}
# https://docs.python.org/3/library/socket.html#socket.getaddrinfo
addrs = {
s[4][0]
for s in socket.getaddrinfo(host=socket.gethostname(), port=None, family=family)
}
loopback_addr = "127.0.0.1" if family == socket.AF_INET else "::1"
addrs.discard(loopback_addr)
assert addrs, f"No external addresses found for family: {family}"
for addr in addrs:
url = (
f"http://{addr}:8888/api"
if family == socket.AF_INET
else f"http://[{addr}]:8888/api"
)
r = requests.get(url)
r.raise_for_status()
assert "version" in r.json()
print(f"Successfully connected to: {url}")
def test_connect() -> None:
make_get_request()
check_addrs(socket.AF_INET)
check_addrs(socket.AF_INET6)
if __name__ == "__main__":
test_connect()

View File

@@ -3,6 +3,7 @@
import logging import logging
import time import time
import docker
import pytest # type: ignore import pytest # type: ignore
from tests.utils.get_container_health import get_health from tests.utils.get_container_health import get_health
@@ -13,6 +14,7 @@ LOGGER = logging.getLogger(__name__)
def get_healthy_status( def get_healthy_status(
container: TrackedContainer, container: TrackedContainer,
docker_client: docker.DockerClient,
env: list[str] | None, env: list[str] | None,
cmd: list[str] | None, cmd: list[str] | None,
user: str | None, user: str | None,
@@ -30,11 +32,11 @@ def get_healthy_status(
while time.time() < finish_time: while time.time() < finish_time:
time.sleep(sleep_time) time.sleep(sleep_time)
status = get_health(running_container) status = get_health(running_container, docker_client)
if status == "healthy": if status == "healthy":
return status return status
return get_health(running_container) return get_health(running_container, docker_client)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -82,11 +84,12 @@ def get_healthy_status(
) )
def test_healthy( def test_healthy(
container: TrackedContainer, container: TrackedContainer,
docker_client: docker.DockerClient,
env: list[str] | None, env: list[str] | None,
cmd: list[str] | None, cmd: list[str] | None,
user: str | None, user: str | None,
) -> None: ) -> None:
assert get_healthy_status(container, env, cmd, user) == "healthy" assert get_healthy_status(container, docker_client, env, cmd, user) == "healthy"
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -115,11 +118,12 @@ def test_healthy(
) )
def test_healthy_with_proxy( def test_healthy_with_proxy(
container: TrackedContainer, container: TrackedContainer,
docker_client: docker.DockerClient,
env: list[str] | None, env: list[str] | None,
cmd: list[str] | None, cmd: list[str] | None,
user: str | None, user: str | None,
) -> None: ) -> None:
assert get_healthy_status(container, env, cmd, user) == "healthy" assert get_healthy_status(container, docker_client, env, cmd, user) == "healthy"
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -138,9 +142,10 @@ def test_healthy_with_proxy(
) )
def test_not_healthy( def test_not_healthy(
container: TrackedContainer, container: TrackedContainer,
docker_client: docker.DockerClient,
env: list[str] | None, env: list[str] | None,
cmd: list[str] | None, cmd: list[str] | None,
) -> None: ) -> None:
assert ( assert (
get_healthy_status(container, env, cmd, user=None) != "healthy" get_healthy_status(container, docker_client, env, cmd, user=None) != "healthy"
), "Container should not be healthy for this testcase" ), "Container should not be healthy for this testcase"

View File

@@ -0,0 +1,50 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import logging
from collections.abc import Generator
from pathlib import Path
from random import randint
import docker
import pytest # type: ignore
from tests.utils.tracked_container import TrackedContainer
LOGGER = logging.getLogger(__name__)
THIS_DIR = Path(__file__).parent.resolve()
@pytest.fixture(scope="session")
def ipv6_network(docker_client: docker.DockerClient) -> Generator[str, None, None]:
"""Create a dual-stack IPv6 docker network"""
# Doesn't have to be routable since we're testing inside the container
subnet64 = "fc00:" + ":".join(hex(randint(0, 2**16))[2:] for _ in range(3))
name = subnet64.replace(":", "-")
docker_client.networks.create(
name,
ipam=docker.types.IPAMPool(
subnet=subnet64 + "::/64",
gateway=subnet64 + "::1",
),
enable_ipv6=True,
internal=True,
)
yield name
docker_client.networks.get(name).remove()
def test_ipv46(container: TrackedContainer, ipv6_network: str) -> None:
"""Check server is listening on the expected IP families"""
host_data_dir = THIS_DIR / "data"
cont_data_dir = "/home/jovyan/data"
LOGGER.info("Testing that server is listening on IPv4 and IPv6 ...")
running_container = container.run_detached(
network=ipv6_network,
volumes={str(host_data_dir): {"bind": cont_data_dir, "mode": "ro,z"}},
tty=True,
)
command = ["python", f"{cont_data_dir}/check_listening.py"]
exec_result = running_container.exec_run(command)
LOGGER.info(exec_result.output.decode())
assert exec_result.exit_code == 0

View File

@@ -1,5 +1,6 @@
# Copyright (c) Jupyter Development Team. # Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
import logging
import os import os
from collections.abc import Generator from collections.abc import Generator
@@ -11,6 +12,8 @@ from urllib3.util.retry import Retry
from tests.utils.tracked_container import TrackedContainer from tests.utils.tracked_container import TrackedContainer
LOGGER = logging.getLogger(__name__)
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def http_client() -> requests.Session: def http_client() -> requests.Session:
@@ -25,7 +28,9 @@ def http_client() -> requests.Session:
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def docker_client() -> docker.DockerClient: def docker_client() -> docker.DockerClient:
"""Docker client configured based on the host environment""" """Docker client configured based on the host environment"""
return docker.from_env() client = docker.from_env()
LOGGER.info(f"Docker client created: {client.version()}")
return client
@pytest.fixture(scope="session") @pytest.fixture(scope="session")

View File

@@ -4,7 +4,6 @@ import docker
from docker.models.containers import Container from docker.models.containers import Container
def get_health(container: Container) -> str: def get_health(container: Container, client: docker.DockerClient) -> str:
api_client = docker.APIClient() inspect_results = client.api.inspect_container(container.name)
inspect_results = api_client.inspect_container(container.name)
return inspect_results["State"]["Health"]["Status"] # type: ignore return inspect_results["State"]["Health"]["Status"] # type: ignore