Skip to content

Commit fb41b59

Browse files
committed
Make volume classes immutable and create copies of them in launch_container()
1 parent 383f019 commit fb41b59

File tree

7 files changed

+185
-102
lines changed

7 files changed

+185
-102
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Next Release
44
Breaking changes:
55

66
- Change addition of SELinux flags to volumes: SELinux flags are only added if
7-
:py:attr:`~pytest_container.container.ContainerVolumeBase.flags` is ``None``.
7+
:py:attr:`~pytest_container.volume.ContainerVolumeBase.flags` is ``None``.
88

99
Improvements and new features:
1010

@@ -233,8 +233,8 @@ Improvements and new features:
233233
and comparing versions.
234234

235235
- Container volumes and bind mounts can now be automatically created via the
236-
:py:class:`~pytest_container.container.ContainerVolume` and
237-
:py:class:`~pytest_container.container.BindMount` classes and adding them to
236+
:py:class:`~pytest_container.volume.ContainerVolume` and
237+
:py:class:`~pytest_container.volume.BindMount` classes and adding them to
238238
the :py:attr:`~pytest_container.container.ContainerBase.volume_mounts`
239239
attribute.
240240

pytest_container/container.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,11 @@
5656
from pytest_container.runtime import OciRuntimeBase
5757
from pytest_container.volume import BindMount
5858
from pytest_container.volume import ContainerVolume
59+
from pytest_container.volume import CreatedBindMount
60+
from pytest_container.volume import CreatedContainerVolume
5961
from pytest_container.volume import get_volume_creator
6062

63+
6164
if sys.version_info >= (3, 8):
6265
from importlib import metadata
6366
from typing import Literal
@@ -273,7 +276,6 @@ def get_launch_cmd(
273276
if self.extra_environment_variables
274277
else []
275278
)
276-
+ [vol.cli_arg for vol in self.volume_mounts]
277279
)
278280

279281
id_or_url = self.container_id or self.url
@@ -840,20 +842,26 @@ def release_lock() -> None:
840842
else:
841843
self._stack.callback(release_lock)
842844

843-
for cont_vol in self.container.volume_mounts:
844-
self._stack.enter_context(
845-
get_volume_creator(cont_vol, self.container_runtime)
846-
)
847-
848-
forwarded_ports = self.container.forwarded_ports
849-
850845
extra_run_args = self.extra_run_args
851846

852847
if self.container_name:
853848
extra_run_args.extend(("--name", self.container_name))
854849

855850
extra_run_args.append(f"--cidfile={self._cidfile}")
856851

852+
new_container_volumes = []
853+
for cont_vol in self.container.volume_mounts:
854+
vol_creator = get_volume_creator(cont_vol, self.container_runtime)
855+
self._stack.enter_context(vol_creator)
856+
857+
new_volume = vol_creator.created_volume
858+
assert new_volume
859+
new_container_volumes.append(new_volume)
860+
861+
extra_run_args.append(new_volume.cli_arg)
862+
863+
forwarded_ports = self.container.forwarded_ports
864+
857865
# Create a copy of the container which was used to parametrize this test
858866
cls = type(self.container)
859867
constructor = inspect.signature(cls.__init__)
@@ -864,6 +872,8 @@ def release_lock() -> None:
864872
for k, v in self.container.__dict__.items()
865873
if k in constructor.parameters
866874
}
875+
kwargs["volume_mounts"] = new_container_volumes
876+
867877
# We must perform the launches in separate branches, as containers with
868878
# port forwards must be launched while the lock is being held. Otherwise
869879
# another container could pick the same ports before this one launches.

pytest_container/volume.py

Lines changed: 88 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,26 @@
22
and bind mounts.
33
44
"""
5-
6-
from subprocess import check_output
7-
from types import TracebackType
8-
from typing import Iterable, List, Optional, Type, Union, overload
9-
from os.path import exists
10-
from os.path import isabs
11-
from typing_extensions import TypedDict
125
import enum
13-
import tempfile
146
import sys
15-
from dataclasses import KW_ONLY, dataclass
7+
import tempfile
8+
from dataclasses import dataclass
9+
from dataclasses import field
10+
from os.path import exists
11+
from os.path import isabs
12+
from subprocess import check_output
13+
from types import TracebackType
14+
from typing import Callable
15+
from typing import List
16+
from typing import Optional
17+
from typing import overload
18+
from typing import Sequence
19+
from typing import Type
20+
from typing import Union
1621

17-
from pytest_container.runtime import OciRuntimeBase
1822
from pytest_container.logging import _logger
23+
from pytest_container.runtime import OciRuntimeBase
24+
from typing_extensions import TypedDict
1925

2026

2127
@enum.unique
@@ -60,16 +66,15 @@ class ContainerVolumeBase:
6066
#: Path inside the container where this volume will be mounted
6167
container_path: str
6268

63-
_: KW_ONLY
64-
6569
#: Flags for mounting this volume.
6670
#:
6771
#: Note that some flags are mutually exclusive and potentially not supported
6872
#: by all container runtimes.
6973
#:
70-
#: The :py:attr:`VolumeFlag.SELINUX_PRIVATE` flag will be added by default
74+
#: The :py:attr:`VolumeFlag.SELINUX_PRIVATE` flag will be used by default
7175
#: if flags is ``None``, unless :py:attr:`ContainerVolumeBase.shared` is
72-
#: ``True``, then :py:attr:`VolumeFlag.SELINUX_SHARED` is added.
76+
#: ``True``, then :py:attr:`VolumeFlag.SELINUX_SHARED` is added to the list
77+
#: of flags to use.
7378
#:
7479
#: If flags is a list (even an empty one), then no flags are added.
7580
flags: Optional[List[VolumeFlag]] = None
@@ -81,10 +86,6 @@ class ContainerVolumeBase:
8186
#: :py:attr:`~ContainerVolumeBase.flags`.
8287
shared: bool = False
8388

84-
#: internal volume name via which the volume can be mounted, e.g. the
85-
#: volume's ID or the path on the host
86-
# _vol_name: str = ""
87-
8889
def __post_init__(self) -> None:
8990

9091
for mutually_exclusive_flags in (
@@ -102,20 +103,28 @@ def __post_init__(self) -> None:
102103
)
103104

104105
@property
105-
def _flags(self) -> Iterable[VolumeFlag]:
106-
if self.flags:
106+
def _flags(self) -> Sequence[VolumeFlag]:
107+
"""Internal sequence of flags to be used to mount this volume. If the
108+
user supplied no flags, then this property gives you one of the SELinux
109+
flags.
110+
111+
"""
112+
if self.flags is not None:
107113
return self.flags
108114

109115
if self.shared:
110116
return (VolumeFlag.SELINUX_SHARED,)
111-
else:
112-
return (VolumeFlag.SELINUX_PRIVATE,)
113117

118+
return (VolumeFlag.SELINUX_PRIVATE,)
114119

115-
def container_volume_cli_arg(
120+
121+
def _container_volume_cli_arg(
116122
container_volume: ContainerVolumeBase, volume_name: str
117123
) -> str:
118-
"""Command line argument to mount the supplied ``container_volume`` volume."""
124+
"""Command line argument to mount the supplied ``container_volume`` volume
125+
via the "name" `volume_name` (can be a volume ID or a path on the host).
126+
127+
"""
119128
res = f"-v={volume_name}:{container_volume.container_path}"
120129
res += ":" + ",".join(str(f) for f in container_volume._flags)
121130
return res
@@ -129,10 +138,29 @@ class ContainerVolume(ContainerVolumeBase):
129138
"""
130139

131140

141+
def _create_required_parameter_factory(
142+
parameter_name: str,
143+
) -> Callable[[], str]:
144+
def _required_parameter() -> str:
145+
raise ValueError(f"Parameter {parameter_name} is required")
146+
147+
return _required_parameter
148+
149+
132150
@dataclass(frozen=True)
133151
class CreatedContainerVolume(ContainerVolume):
152+
"""A container volume that exists and has a volume id assigned to it."""
153+
154+
#: The hash/ID of the volume in the container runtime.
155+
#: This parameter is required
156+
volume_id: str = field(
157+
default_factory=_create_required_parameter_factory("volume_id")
158+
)
134159

135-
volume_id: str
160+
@property
161+
def cli_arg(self) -> str:
162+
"""The command line argument to mount this volume."""
163+
return _container_volume_cli_arg(self, self.volume_id)
136164

137165

138166
@dataclass(frozen=True)
@@ -148,15 +176,29 @@ class BindMount(ContainerVolumeBase):
148176
149177
"""
150178

151-
#: Path on the host that will be mounted if absolute. if relative,
179+
#: Path on the host that will be mounted if absolute. If relative,
152180
#: it refers to a volume to be auto-created. When omitted, a temporary
153181
#: directory will be created and the path will be saved in this attribute.
154182
host_path: Optional[str] = None
155183

156184

157185
@dataclass(frozen=True)
158-
class CreatedBindMount(ContainerVolumeBase):
159-
host_path: str
186+
class CreatedBindMount(BindMount):
187+
"""An established bind mount of the directory :py:attr:`host_path` on the
188+
host to :py:attr:`~ContainerVolumeBase.container_path` in the container.
189+
190+
"""
191+
192+
#: Path on the host that is bind mounted into the container.
193+
#: This parameter must be provided.
194+
host_path: str = field(
195+
default_factory=_create_required_parameter_factory("host_path")
196+
)
197+
198+
@property
199+
def cli_arg(self) -> str:
200+
"""The command line argument to mount this volume."""
201+
return _container_volume_cli_arg(self, self.host_path)
160202

161203

162204
class _ContainerVolumeKWARGS(TypedDict, total=False):
@@ -174,8 +216,13 @@ class VolumeCreator:
174216
"""Context Manager to create and remove a :py:class:`ContainerVolume`.
175217
176218
This context manager creates a volume using the supplied
177-
:py:attr:`container_runtime` When the ``with`` block is entered and removes
219+
:py:attr:`container_runtime` when the ``with`` block is entered and removes
178220
it once it is exited.
221+
222+
The :py:class:`ContainerVolume` in :py:attr:`volume` is used as the
223+
blueprint to create the volume, the actually created container volume is
224+
saved in :py:attr:`created_volume`.
225+
179226
"""
180227

181228
#: The volume to be created
@@ -184,7 +231,9 @@ class VolumeCreator:
184231
#: The container runtime, via which the volume is created & destroyed
185232
container_runtime: OciRuntimeBase
186233

187-
_created_volume: Optional[CreatedContainerVolume] = None
234+
#: The created container volume once it has been setup & created. It's
235+
#: ``None`` until then.
236+
created_volume: Optional[CreatedContainerVolume] = None
188237

189238
def __enter__(self) -> "VolumeCreator":
190239
"""Creates the container volume"""
@@ -195,7 +244,7 @@ def __enter__(self) -> "VolumeCreator":
195244
.decode()
196245
.strip()
197246
)
198-
self._created_volume = CreatedContainerVolume(
247+
self.created_volume = CreatedContainerVolume(
199248
container_path=self.volume.container_path,
200249
flags=self.volume.flags,
201250
shared=self.volume.shared,
@@ -211,11 +260,11 @@ def __exit__(
211260
__traceback: Optional[TracebackType],
212261
) -> None:
213262
"""Cleans up the container volume."""
214-
assert self._created_volume and self._created_volume.volume_id
263+
assert self.created_volume and self.created_volume.volume_id
215264

216265
_logger.debug(
217266
"cleaning up volume %s via %s",
218-
self._created_volume.volume_id,
267+
self.created_volume.volume_id,
219268
self.container_runtime.runner_binary,
220269
)
221270

@@ -226,7 +275,7 @@ def __exit__(
226275
"volume",
227276
"rm",
228277
"-f",
229-
self._created_volume.volume_id,
278+
self.created_volume.volume_id,
230279
],
231280
)
232281

@@ -244,7 +293,7 @@ class BindMountCreator:
244293
#: internal temporary directory
245294
_tmpdir: Optional[TEMPDIR_T] = None
246295

247-
_created_volume: Optional[CreatedBindMount] = None
296+
created_volume: Optional[CreatedBindMount] = None
248297

249298
def __post__init__(self) -> None:
250299
# the tempdir must not be set accidentally by the user
@@ -281,7 +330,7 @@ def __enter__(self) -> "BindMountCreator":
281330
"was requested but the directory does not exist"
282331
)
283332

284-
self._created_volume = CreatedBindMount(**kwargs)
333+
self.created_volume = CreatedBindMount(**kwargs)
285334

286335
return self
287336

@@ -292,13 +341,13 @@ def __exit__(
292341
__traceback: Optional[TracebackType],
293342
) -> None:
294343
"""Cleans up the temporary host directory or the container volume."""
295-
assert self._created_volume and self._created_volume.host_path
344+
assert self.created_volume
296345

297346
if self._tmpdir:
298347
_logger.debug(
299348
"cleaning up directory %s for the container volume %s",
300-
self.volume.host_path,
301-
self.volume.container_path,
349+
self.created_volume.host_path,
350+
self.created_volume.container_path,
302351
)
303352
self._tmpdir.cleanup()
304353

source/api.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ The pod module
1818
:undoc-members:
1919

2020

21+
The volume module
22+
-----------------
23+
24+
.. automodule:: pytest_container.volume
25+
:members:
26+
:undoc-members:
27+
28+
2129
The build module
2230
----------------
2331

0 commit comments

Comments
 (0)