mirror of
https://github.com/jupyter/docker-stacks.git
synced 2025-10-07 10:04:03 +00:00
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:
9
.github/actions/create-dev-env/action.yml
vendored
9
.github/actions/create-dev-env/action.yml
vendored
@@ -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
|
||||||
|
57
tests/by_image/base-notebook/data/check_listening.py
Executable file
57
tests/by_image/base-notebook/data/check_listening.py
Executable 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()
|
@@ -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"
|
||||||
|
50
tests/by_image/base-notebook/test_ips.py
Normal file
50
tests/by_image/base-notebook/test_ips.py
Normal 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
|
@@ -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")
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user