Skip to content

Commit 9e76f6f

Browse files
jiridanekopenshift-merge-bot[bot]
authored andcommitted
RHOAIENG-18459: chore(tests/containers): improve podman machine handling: detect the right one to use if user has multiple machines around
1 parent 13fd504 commit 9e76f6f

File tree

4 files changed

+54
-31
lines changed

4 files changed

+54
-31
lines changed

tests/containers/conftest.py

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
import pytest
1212

13+
from tests.containers import docker_utils
14+
1315
if TYPE_CHECKING:
1416
from pytest import ExitCode, Session, Parser, Metafunc
1517

@@ -58,7 +60,7 @@ def pytest_sessionstart(session: Session) -> None:
5860

5961
# determine the local socket path
6062
# NOTE: this will not work for remote docker, but we will cross the bridge when we come to it
61-
socket_path = the_one(adapter.socket_path for adapter in client.client.api.adapters.values())
63+
socket_path = docker_utils.get_socket_path(client.client)
6264

6365
# set that socket path for ryuk's use, unless user overrode that
6466
if TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE not in os.environ:
@@ -84,21 +86,6 @@ def pytest_sessionfinish(session: Session, exitstatus: int | ExitCode) -> None:
8486
testcontainers.core.container.Reaper.delete_instance()
8587

8688

87-
# https://docs.python.org/3/library/functions.html#iter
88-
def the_one[T](iterable: Iterable[T]) -> T:
89-
"""Checks that there is exactly one element in the iterable, and returns it."""
90-
it = iter(iterable)
91-
try:
92-
v = next(it)
93-
except StopIteration:
94-
raise ValueError("No elements in iterable")
95-
try:
96-
next(it)
97-
except StopIteration:
98-
return v
99-
raise ValueError("More than one element in iterable")
100-
101-
10289
@pytest.fixture(scope="function")
10390
def test_frame():
10491
class TestFrame:

tests/containers/docker_utils.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
import sys
77
import tarfile
88
import time
9-
from typing import TYPE_CHECKING
9+
from typing import Iterable, TYPE_CHECKING
10+
11+
import docker.client
1012

1113
import testcontainers.core.container
1214

@@ -133,10 +135,10 @@ def __init__(self, client, id, output: list[int] | list[str]):
133135
def inspect(self):
134136
return self.client.api.exec_inspect(self.id)
135137

136-
def poll(self):
138+
def poll(self) -> int:
137139
return self.inspect()["ExitCode"]
138140

139-
def communicate(self, line_prefix=b""):
141+
def communicate(self, line_prefix=b"") -> int:
140142
for data in self.output:
141143
if not data:
142144
continue
@@ -155,3 +157,31 @@ def communicate(self, line_prefix=b""):
155157
while self.poll() is None:
156158
raise RuntimeError("Hm could that really happen?")
157159
return self.poll()
160+
161+
def get_socket_path(client: docker.client.DockerClient) -> str:
162+
"""Determine the local socket path.
163+
This works even when `podman machine` with its own host-mounts is involved
164+
NOTE: this will not work for remote docker, but we will cross the bridge when we come to it"""
165+
socket_path = _the_one(adapter.socket_path for adapter in client.api.adapters.values())
166+
return socket_path
167+
168+
def get_container_pid(container: Container) -> int | None:
169+
"""Get the network namespace of a Docker container."""
170+
container.reload()
171+
container_pid = container.attrs['State']['Pid']
172+
return container_pid
173+
174+
175+
# https://docs.python.org/3/library/functions.html#iter
176+
def _the_one[T](iterable: Iterable[T]) -> T:
177+
"""Checks that there is exactly one element in the iterable, and returns it."""
178+
it = iter(iterable)
179+
try:
180+
v = next(it)
181+
except StopIteration:
182+
raise ValueError("No elements in iterable")
183+
try:
184+
next(it)
185+
except StopIteration:
186+
return v
187+
raise ValueError("More than one element in iterable")

tests/containers/podman_machine_utils.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,22 @@
22
import logging
33
import socket
44
import subprocess
5-
from tests.containers.schemas import PodmanMachineInspect
5+
from typing import Callable
6+
7+
import tests.containers.schemas
68

79
logging.basicConfig(level=logging.DEBUG)
810

9-
def open_ssh_tunnel(machine_name: str, local_port: int, remote_port: int, remote_interface: str = "localhost") -> subprocess.Popen:
11+
def open_ssh_tunnel(machine_predicate: Callable[[tests.containers.schemas.PodmanMachine], bool],
12+
local_port: int, remote_port: int, remote_interface: str = "localhost") -> subprocess.Popen:
1013
# Load and parse the Podman machine data
11-
json_data = subprocess.check_output(["podman", "machine", "inspect", machine_name], text=True)
12-
inspect = PodmanMachineInspect(machines=json.loads(json_data))
14+
json_data = subprocess.check_output(["podman", "machine", "inspect"], text=True)
15+
inspect = tests.containers.schemas.PodmanMachineInspect(machines=json.loads(json_data))
1316
machines = inspect.machines
1417

15-
machine = next((m for m in machines if m.Name == machine_name), None)
18+
machine = next((m for m in machines if machine_predicate(m)), None)
1619
if not machine:
17-
raise ValueError(f"Machine '{machine_name}' not found")
20+
raise ValueError(f"Machine matching given predicate not found")
1821

1922
ssh_command = [
2023
"ssh",
@@ -30,7 +33,7 @@ def open_ssh_tunnel(machine_name: str, local_port: int, remote_port: int, remote
3033
# Open the SSH tunnel
3134
process = subprocess.Popen(ssh_command)
3235

33-
logging.info(f"SSH tunnel opened for {machine_name}: {remote_interface}:{local_port} -> localhost:{remote_port}")
36+
logging.info(f"SSH tunnel opened for {machine.Name}: {remote_interface}:{local_port} -> localhost:{remote_port}")
3437
return process
3538

3639
def find_free_port() -> int:
@@ -46,8 +49,8 @@ def find_free_port() -> int:
4649
# Usage example
4750
if __name__ == "__main__":
4851
tunnel_process = open_ssh_tunnel(
49-
"podman-machine-default",
50-
8080, 8080,
52+
machine_predicate=lambda m: m.Name == "podman-machine-default",
53+
local_port=8080, remote_port=8080,
5154
remote_interface="[fc00::2]"
5255
)
5356

tests/containers/workbenches/workbench_image_test.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import functools
44
import http.cookiejar
55
import logging
6+
import os
67
import platform
78
import urllib.error
89
import urllib.request
@@ -87,9 +88,11 @@ def test_ipv6_only(self, image: str, test_frame):
8788
# the container host is a podman machine, we need to expose port on podman machine first
8889
host = "localhost"
8990
port = podman_machine_utils.find_free_port()
90-
process = podman_machine_utils.open_ssh_tunnel("podman-machine-default",
91-
local_port=port, remote_port=container.port,
92-
remote_interface=f"[{ipv6_address}]")
91+
socket_path = os.path.realpath(docker_utils.get_socket_path(client.client))
92+
process = podman_machine_utils.open_ssh_tunnel(
93+
machine_predicate=lambda m: os.path.realpath(m.ConnectionInfo.PodmanSocket.Path) == socket_path,
94+
local_port=port, remote_port=container.port,
95+
remote_interface=f"[{ipv6_address}]")
9396
test_frame.append(process, lambda p: p.kill())
9497
else:
9598
host = ipv6_address

0 commit comments

Comments
 (0)