Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c691f23
Test server listening on IPv4/IPv6
mathbunnyru Mar 20, 2025
2185322
Set up Docker in create-dev-env
mathbunnyru Mar 20, 2025
07d5526
Show docker version
mathbunnyru Mar 20, 2025
45c143b
Add info about docker client
mathbunnyru Mar 20, 2025
329bf69
Check requests
mathbunnyru Mar 20, 2025
06a4a0a
Show docker client version
mathbunnyru Mar 20, 2025
68d3d65
Try to pass docker sock
mathbunnyru Mar 20, 2025
d864483
Fix
mathbunnyru Mar 20, 2025
80793bf
Break fast
mathbunnyru Mar 20, 2025
e45b9c6
Revert
mathbunnyru Mar 20, 2025
3a07140
Cleanup
mathbunnyru Mar 20, 2025
dc6c26b
Better naming
mathbunnyru Mar 20, 2025
d03069a
Always use docker.from_env
mathbunnyru Mar 20, 2025
fba8fec
Revert "Always use docker.from_env"
mathbunnyru Mar 20, 2025
e618807
Use custom docker client for only one test
mathbunnyru Mar 20, 2025
ae3e529
More logs
mathbunnyru Mar 20, 2025
0213080
Use cont_data_dir in test, so workdir doesn't matter
mathbunnyru Mar 20, 2025
f7d32e1
Use common variable names
mathbunnyru Mar 20, 2025
db9a25e
Move patch to a separate function
mathbunnyru Mar 20, 2025
99f99e3
Try to use set-host option
mathbunnyru Mar 20, 2025
e165880
Merge branch 'main' into test_ipv6
mathbunnyru Mar 20, 2025
e0b5a54
Use the same docker client in get_health
mathbunnyru Mar 20, 2025
8df7eab
Use .api
mathbunnyru Mar 20, 2025
ac3dbd5
Rewrite check_listening.py to use one function for both ipv4 and ipv6
mathbunnyru Mar 20, 2025
1a62fa8
Add links to explain why we need to set up docker manually
mathbunnyru Mar 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/actions/create-dev-env/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,12 @@ runs:
pip install --upgrade pip
pip install --upgrade -r requirements-dev.txt
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
Copy link
Member Author

@mathbunnyru mathbunnyru Mar 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This magic option changes anything - using it there is need to change other code around docker client creation, it will automatically use the latest docker client.

It is also more general, as it will also work for other places where we're creating docker client - in tagging, for example.

57 changes: 57 additions & 0 deletions tests/by_image/base-notebook/data/check_listening.py
Original file line number Diff line number Diff line change
@@ -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()
15 changes: 10 additions & 5 deletions tests/by_image/base-notebook/test_healthcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
import time

import docker
import pytest # type: ignore

from tests.utils.get_container_health import get_health
Expand All @@ -13,6 +14,7 @@

def get_healthy_status(
container: TrackedContainer,
docker_client: docker.DockerClient,
env: list[str] | None,
cmd: list[str] | None,
user: str | None,
Expand All @@ -30,11 +32,11 @@ def get_healthy_status(
while time.time() < finish_time:
time.sleep(sleep_time)

status = get_health(running_container)
status = get_health(running_container, docker_client)
if status == "healthy":
return status

return get_health(running_container)
return get_health(running_container, docker_client)


@pytest.mark.parametrize(
Expand Down Expand Up @@ -82,11 +84,12 @@ def get_healthy_status(
)
def test_healthy(
container: TrackedContainer,
docker_client: docker.DockerClient,
env: list[str] | None,
cmd: list[str] | None,
user: str | 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(
Expand Down Expand Up @@ -115,11 +118,12 @@ def test_healthy(
)
def test_healthy_with_proxy(
container: TrackedContainer,
docker_client: docker.DockerClient,
env: list[str] | None,
cmd: list[str] | None,
user: str | 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(
Expand All @@ -138,9 +142,10 @@ def test_healthy_with_proxy(
)
def test_not_healthy(
container: TrackedContainer,
docker_client: docker.DockerClient,
env: list[str] | None,
cmd: list[str] | None,
) -> None:
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"
50 changes: 50 additions & 0 deletions tests/by_image/base-notebook/test_ips.py
Original file line number Diff line number Diff line change
@@ -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
7 changes: 6 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import logging
import os
from collections.abc import Generator

Expand All @@ -11,6 +12,8 @@

from tests.utils.tracked_container import TrackedContainer

LOGGER = logging.getLogger(__name__)


@pytest.fixture(scope="session")
def http_client() -> requests.Session:
Expand All @@ -25,7 +28,9 @@ def http_client() -> requests.Session:
@pytest.fixture(scope="session")
def docker_client() -> docker.DockerClient:
"""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")
Expand Down
5 changes: 2 additions & 3 deletions tests/utils/get_container_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from docker.models.containers import Container


def get_health(container: Container) -> str:
api_client = docker.APIClient()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is funny coincidence.
I refactored code around health check earlier today 😃

Didn't notice this bug.
We're using separate API client here, not the one from the docker_client fixture.

That's why it was impossible to make both healthcheck and ipv6 tests work.
Passing client as param and using it should fix the problem

inspect_results = api_client.inspect_container(container.name)
def get_health(container: Container, client: docker.DockerClient) -> str:
inspect_results = client.api.inspect_container(container.name)
return inspect_results["State"]["Health"]["Status"] # type: ignore