Skip to content

Commit 1d45389

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

File tree

7 files changed

+199
-104
lines changed

7 files changed

+199
-104
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: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
from pytest_container.volume import ContainerVolume
5959
from pytest_container.volume import get_volume_creator
6060

61+
6162
if sys.version_info >= (3, 8):
6263
from importlib import metadata
6364
from typing import Literal
@@ -273,7 +274,6 @@ def get_launch_cmd(
273274
if self.extra_environment_variables
274275
else []
275276
)
276-
+ [vol.cli_arg for vol in self.volume_mounts]
277277
)
278278

279279
id_or_url = self.container_id or self.url
@@ -840,20 +840,26 @@ def release_lock() -> None:
840840
else:
841841
self._stack.callback(release_lock)
842842

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-
850843
extra_run_args = self.extra_run_args
851844

852845
if self.container_name:
853846
extra_run_args.extend(("--name", self.container_name))
854847

855848
extra_run_args.append(f"--cidfile={self._cidfile}")
856849

850+
new_container_volumes = []
851+
for cont_vol in self.container.volume_mounts:
852+
vol_creator = get_volume_creator(cont_vol, self.container_runtime)
853+
self._stack.enter_context(vol_creator)
854+
855+
new_volume = vol_creator.created_volume
856+
assert new_volume
857+
new_container_volumes.append(new_volume)
858+
859+
extra_run_args.append(new_volume.cli_arg)
860+
861+
forwarded_ports = self.container.forwarded_ports
862+
857863
# Create a copy of the container which was used to parametrize this test
858864
cls = type(self.container)
859865
constructor = inspect.signature(cls.__init__)
@@ -864,6 +870,8 @@ def release_lock() -> None:
864870
for k, v in self.container.__dict__.items()
865871
if k in constructor.parameters
866872
}
873+
kwargs["volume_mounts"] = new_container_volumes
874+
867875
# We must perform the launches in separate branches, as containers with
868876
# port forwards must be launched while the lock is being held. Otherwise
869877
# another container could pick the same ports before this one launches.

pytest_container/volume.py

Lines changed: 96 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,30 @@
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+
25+
try:
26+
from typing import TypedDict
27+
except ImportError:
28+
from typing_extensions import TypedDict
1929

2030

2131
@enum.unique
@@ -60,16 +70,15 @@ class ContainerVolumeBase:
6070
#: Path inside the container where this volume will be mounted
6171
container_path: str
6272

63-
_: KW_ONLY
64-
6573
#: Flags for mounting this volume.
6674
#:
6775
#: Note that some flags are mutually exclusive and potentially not supported
6876
#: by all container runtimes.
6977
#:
70-
#: The :py:attr:`VolumeFlag.SELINUX_PRIVATE` flag will be added by default
78+
#: The :py:attr:`VolumeFlag.SELINUX_PRIVATE` flag will be used by default
7179
#: if flags is ``None``, unless :py:attr:`ContainerVolumeBase.shared` is
72-
#: ``True``, then :py:attr:`VolumeFlag.SELINUX_SHARED` is added.
80+
#: ``True``, then :py:attr:`VolumeFlag.SELINUX_SHARED` is added to the list
81+
#: of flags to use.
7382
#:
7483
#: If flags is a list (even an empty one), then no flags are added.
7584
flags: Optional[List[VolumeFlag]] = None
@@ -81,10 +90,6 @@ class ContainerVolumeBase:
8190
#: :py:attr:`~ContainerVolumeBase.flags`.
8291
shared: bool = False
8392

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-
8893
def __post_init__(self) -> None:
8994

9095
for mutually_exclusive_flags in (
@@ -102,22 +107,31 @@ def __post_init__(self) -> None:
102107
)
103108

104109
@property
105-
def _flags(self) -> Iterable[VolumeFlag]:
106-
if self.flags:
110+
def _flags(self) -> Sequence[VolumeFlag]:
111+
"""Internal sequence of flags to be used to mount this volume. If the
112+
user supplied no flags, then this property gives you one of the SELinux
113+
flags.
114+
115+
"""
116+
if self.flags is not None:
107117
return self.flags
108118

109119
if self.shared:
110120
return (VolumeFlag.SELINUX_SHARED,)
111-
else:
112-
return (VolumeFlag.SELINUX_PRIVATE,)
113121

122+
return (VolumeFlag.SELINUX_PRIVATE,)
114123

115-
def container_volume_cli_arg(
124+
125+
def _container_volume_cli_arg(
116126
container_volume: ContainerVolumeBase, volume_name: str
117127
) -> str:
118-
"""Command line argument to mount the supplied ``container_volume`` volume."""
128+
"""Command line argument to mount the supplied ``container_volume`` volume
129+
via the "name" `volume_name` (can be a volume ID or a path on the host).
130+
131+
"""
119132
res = f"-v={volume_name}:{container_volume.container_path}"
120-
res += ":" + ",".join(str(f) for f in container_volume._flags)
133+
if container_volume._flags:
134+
res += ":" + ",".join(str(f) for f in container_volume._flags)
121135
return res
122136

123137

@@ -129,10 +143,29 @@ class ContainerVolume(ContainerVolumeBase):
129143
"""
130144

131145

146+
def _create_required_parameter_factory(
147+
parameter_name: str,
148+
) -> Callable[[], str]:
149+
def _required_parameter() -> str:
150+
raise ValueError(f"Parameter {parameter_name} is required")
151+
152+
return _required_parameter
153+
154+
132155
@dataclass(frozen=True)
133156
class CreatedContainerVolume(ContainerVolume):
157+
"""A container volume that exists and has a volume id assigned to it."""
158+
159+
#: The hash/ID of the volume in the container runtime.
160+
#: This parameter is required
161+
volume_id: str = field(
162+
default_factory=_create_required_parameter_factory("volume_id")
163+
)
134164

135-
volume_id: str
165+
@property
166+
def cli_arg(self) -> str:
167+
"""The command line argument to mount this volume."""
168+
return _container_volume_cli_arg(self, self.volume_id)
136169

137170

138171
@dataclass(frozen=True)
@@ -144,19 +177,34 @@ class BindMount(ContainerVolumeBase):
144177
container via :py:attr:`~ContainerVolumeBase.container_path`. The
145178
``container*`` fixtures will then create a temporary directory on the host
146179
for you that will be used as the mount point. Alternatively, you can also
147-
specify the path on the host yourself via :py:attr:`host_path`.
180+
specify the path on the host yourself via :py:attr:`BindMount.host_path`.
148181
149182
"""
150183

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

156189

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

161209

162210
class _ContainerVolumeKWARGS(TypedDict, total=False):
@@ -174,8 +222,13 @@ class VolumeCreator:
174222
"""Context Manager to create and remove a :py:class:`ContainerVolume`.
175223
176224
This context manager creates a volume using the supplied
177-
:py:attr:`container_runtime` When the ``with`` block is entered and removes
225+
:py:attr:`container_runtime` when the ``with`` block is entered and removes
178226
it once it is exited.
227+
228+
The :py:class:`ContainerVolume` in :py:attr:`volume` is used as the
229+
blueprint to create the volume, the actually created container volume is
230+
saved in :py:attr:`created_volume`.
231+
179232
"""
180233

181234
#: The volume to be created
@@ -184,7 +237,9 @@ class VolumeCreator:
184237
#: The container runtime, via which the volume is created & destroyed
185238
container_runtime: OciRuntimeBase
186239

187-
_created_volume: Optional[CreatedContainerVolume] = None
240+
#: The created container volume once it has been setup & created. It's
241+
#: ``None`` until then.
242+
created_volume: Optional[CreatedContainerVolume] = None
188243

189244
def __enter__(self) -> "VolumeCreator":
190245
"""Creates the container volume"""
@@ -195,7 +250,7 @@ def __enter__(self) -> "VolumeCreator":
195250
.decode()
196251
.strip()
197252
)
198-
self._created_volume = CreatedContainerVolume(
253+
self.created_volume = CreatedContainerVolume(
199254
container_path=self.volume.container_path,
200255
flags=self.volume.flags,
201256
shared=self.volume.shared,
@@ -211,11 +266,11 @@ def __exit__(
211266
__traceback: Optional[TracebackType],
212267
) -> None:
213268
"""Cleans up the container volume."""
214-
assert self._created_volume and self._created_volume.volume_id
269+
assert self.created_volume and self.created_volume.volume_id
215270

216271
_logger.debug(
217272
"cleaning up volume %s via %s",
218-
self._created_volume.volume_id,
273+
self.created_volume.volume_id,
219274
self.container_runtime.runner_binary,
220275
)
221276

@@ -226,7 +281,7 @@ def __exit__(
226281
"volume",
227282
"rm",
228283
"-f",
229-
self._created_volume.volume_id,
284+
self.created_volume.volume_id,
230285
],
231286
)
232287

@@ -244,7 +299,7 @@ class BindMountCreator:
244299
#: internal temporary directory
245300
_tmpdir: Optional[TEMPDIR_T] = None
246301

247-
_created_volume: Optional[CreatedBindMount] = None
302+
created_volume: Optional[CreatedBindMount] = None
248303

249304
def __post__init__(self) -> None:
250305
# the tempdir must not be set accidentally by the user
@@ -281,7 +336,7 @@ def __enter__(self) -> "BindMountCreator":
281336
"was requested but the directory does not exist"
282337
)
283338

284-
self._created_volume = CreatedBindMount(**kwargs)
339+
self.created_volume = CreatedBindMount(**kwargs)
285340

286341
return self
287342

@@ -292,13 +347,13 @@ def __exit__(
292347
__traceback: Optional[TracebackType],
293348
) -> None:
294349
"""Cleans up the temporary host directory or the container volume."""
295-
assert self._created_volume and self._created_volume.host_path
350+
assert self.created_volume
296351

297352
if self._tmpdir:
298353
_logger.debug(
299354
"cleaning up directory %s for the container volume %s",
300-
self.volume.host_path,
301-
self.volume.container_path,
355+
self.created_volume.host_path,
356+
self.created_volume.container_path,
302357
)
303358
self._tmpdir.cleanup()
304359

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)