Skip to content

Commit 9526eec

Browse files
committed
Move volume related entities into volume module
1 parent b2d4c22 commit 9526eec

File tree

2 files changed

+336
-285
lines changed

2 files changed

+336
-285
lines changed

pytest_container/container.py

Lines changed: 3 additions & 285 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
from datetime import datetime
2222
from datetime import timedelta
2323
from hashlib import sha3_256
24-
from os.path import exists
25-
from os.path import isabs
2624
from os.path import join
2725
from pathlib import Path
2826
from subprocess import call
@@ -55,6 +53,9 @@
5553
from pytest_container.logging import _logger
5654
from pytest_container.runtime import get_selected_runtime
5755
from pytest_container.runtime import OciRuntimeBase
56+
from pytest_container.volume import BindMount
57+
from pytest_container.volume import ContainerVolume
58+
from pytest_container.volume import get_volume_creator
5859

5960
if sys.version_info >= (3, 8):
6061
from importlib import metadata
@@ -128,289 +129,6 @@ def create_host_port_port_forward(
128129
return finished_forwards
129130

130131

131-
@enum.unique
132-
class VolumeFlag(enum.Enum):
133-
"""Supported flags for mounting container volumes."""
134-
135-
#: The volume is mounted read-only
136-
READ_ONLY = "ro"
137-
#: The volume is mounted read-write (default)
138-
READ_WRITE = "rw"
139-
140-
#: The volume is relabeled so that it can be shared by two containers
141-
SELINUX_SHARED = "z"
142-
#: The volume is relabeled so that only a single container can access it
143-
SELINUX_PRIVATE = "Z"
144-
145-
#: chown the content of the volume for rootless runs
146-
CHOWN_USER = "U"
147-
148-
#: ensure the volume is mounted as noexec (data only)
149-
NOEXEC = "noexec"
150-
151-
#: The volume is mounted as a temporary storage using overlay-fs (only
152-
#: supported by :command:`podman`)
153-
OVERLAY = "O"
154-
155-
def __str__(self) -> str:
156-
assert isinstance(self.value, str)
157-
return self.value
158-
159-
160-
if sys.version_info >= (3, 9):
161-
TEMPDIR_T = tempfile.TemporaryDirectory[str]
162-
else:
163-
TEMPDIR_T = tempfile.TemporaryDirectory
164-
165-
166-
@dataclass
167-
class ContainerVolumeBase:
168-
"""Base class for container volumes."""
169-
170-
#: Path inside the container where this volume will be mounted
171-
container_path: str
172-
173-
#: Flags for mounting this volume.
174-
#:
175-
#: Note that some flags are mutually exclusive and potentially not supported
176-
#: by all container runtimes.
177-
#:
178-
#: The :py:attr:`VolumeFlag.SELINUX_PRIVATE` flag will be added by default
179-
#: if flags is ``None``, unless :py:attr:`ContainerVolumeBase.shared` is
180-
#: ``True``, then :py:attr:`VolumeFlag.SELINUX_SHARED` is added.
181-
#:
182-
#: If flags is a list (even an empty one), then no flags are added.
183-
flags: Optional[List[VolumeFlag]] = None
184-
185-
#: Define whether this volume should can be shared between
186-
#: containers. Defaults to ``False``.
187-
#:
188-
#: This affects only the addition of SELinux flags to
189-
#: :py:attr:`~ContainerVolumeBase.flags`.
190-
shared: bool = False
191-
192-
#: internal volume name via which it can be mounted, e.g. the volume's ID or
193-
#: the path on the host
194-
_vol_name: str = ""
195-
196-
def __post_init__(self) -> None:
197-
if self.flags is None:
198-
self.flags = [
199-
VolumeFlag.SELINUX_SHARED
200-
if self.shared
201-
else VolumeFlag.SELINUX_PRIVATE
202-
]
203-
204-
for mutually_exclusive_flags in (
205-
(VolumeFlag.READ_ONLY, VolumeFlag.READ_WRITE),
206-
(VolumeFlag.SELINUX_SHARED, VolumeFlag.SELINUX_PRIVATE),
207-
):
208-
if (
209-
mutually_exclusive_flags[0] in self.flags
210-
and mutually_exclusive_flags[1] in self.flags
211-
):
212-
raise ValueError(
213-
f"Invalid container volume flags: {', '.join(str(f) for f in self.flags)}; "
214-
f"flags {mutually_exclusive_flags[0]} and {mutually_exclusive_flags[1]} "
215-
"are mutually exclusive"
216-
)
217-
218-
@property
219-
def cli_arg(self) -> str:
220-
"""Command line argument to mount this volume."""
221-
assert self._vol_name
222-
res = f"-v={self._vol_name}:{self.container_path}"
223-
if self.flags:
224-
res += ":" + ",".join(str(f) for f in self.flags)
225-
return res
226-
227-
228-
@dataclass
229-
class ContainerVolume(ContainerVolumeBase):
230-
"""A container volume created by the container runtime for persisting files
231-
outside of (ephemeral) containers.
232-
233-
"""
234-
235-
@property
236-
def volume_id(self) -> str:
237-
"""Unique ID of the volume. It is automatically set when the volume is
238-
created by :py:class:`VolumeCreator`.
239-
240-
"""
241-
return self._vol_name
242-
243-
244-
@dataclass
245-
class BindMount(ContainerVolumeBase):
246-
"""A volume mounted into a container from the host using bind mounts.
247-
248-
This class describes a bind mount of a host directory into a container. In
249-
the most minimal configuration, all you need to specify is the path in the
250-
container via :py:attr:`~ContainerVolumeBase.container_path`. The
251-
``container*`` fixtures will then create a temporary directory on the host
252-
for you that will be used as the mount point. Alternatively, you can also
253-
specify the path on the host yourself via :py:attr:`host_path`.
254-
255-
"""
256-
257-
#: Path on the host that will be mounted if absolute. if relative,
258-
#: it refers to a volume to be auto-created. When omitted, a temporary
259-
#: directory will be created and the path will be saved in this attribute.
260-
host_path: Optional[str] = None
261-
262-
def __post_init__(self) -> None:
263-
super().__post_init__()
264-
if self.host_path:
265-
self._vol_name = self.host_path
266-
267-
268-
@dataclass
269-
class VolumeCreator:
270-
"""Context Manager to create and remove a :py:class:`ContainerVolume`.
271-
272-
This context manager creates a volume using the supplied
273-
:py:attr:`container_runtime` When the ``with`` block is entered and removes
274-
it once it is exited.
275-
"""
276-
277-
#: The volume to be created
278-
volume: ContainerVolume
279-
280-
#: The container runtime, via which the volume is created & destroyed
281-
container_runtime: OciRuntimeBase
282-
283-
def __enter__(self) -> "VolumeCreator":
284-
"""Creates the container volume"""
285-
vol_id = (
286-
check_output(
287-
[self.container_runtime.runner_binary, "volume", "create"]
288-
)
289-
.decode()
290-
.strip()
291-
)
292-
self.volume._vol_name = vol_id
293-
return self
294-
295-
def __exit__(
296-
self,
297-
__exc_type: Optional[Type[BaseException]],
298-
__exc_value: Optional[BaseException],
299-
__traceback: Optional[TracebackType],
300-
) -> None:
301-
"""Cleans up the container volume."""
302-
assert self.volume.volume_id
303-
304-
_logger.debug(
305-
"cleaning up volume %s via %s",
306-
self.volume.volume_id,
307-
self.container_runtime.runner_binary,
308-
)
309-
310-
# Clean up container volume
311-
check_output(
312-
[
313-
self.container_runtime.runner_binary,
314-
"volume",
315-
"rm",
316-
"-f",
317-
self.volume.volume_id,
318-
],
319-
)
320-
self.volume._vol_name = ""
321-
322-
323-
@dataclass
324-
class BindMountCreator:
325-
"""Context Manager that creates temporary directories for bind mounts (if
326-
necessary, i.e. when :py:attr:`BindMount.host_path` is ``None``).
327-
328-
"""
329-
330-
#: The bind mount which host path should be created
331-
volume: BindMount
332-
333-
#: internal temporary directory
334-
_tmpdir: Optional[TEMPDIR_T] = None
335-
336-
def __post__init__(self) -> None:
337-
# the tempdir must not be set accidentally by the user
338-
assert self._tmpdir is None, "_tmpdir must only be set in __enter__()"
339-
340-
def __enter__(self) -> "BindMountCreator":
341-
"""Creates the temporary host path if necessary."""
342-
if not self.volume.host_path:
343-
# we don't want to use a with statement, as the temporary directory
344-
# must survive this function
345-
# pylint: disable=consider-using-with
346-
self._tmpdir = tempfile.TemporaryDirectory()
347-
self.volume.host_path = self._tmpdir.name
348-
349-
_logger.debug(
350-
"created temporary directory %s for the container volume %s",
351-
self._tmpdir.name,
352-
self.volume.container_path,
353-
)
354-
355-
assert self.volume.host_path
356-
self.volume._vol_name = self.volume.host_path
357-
if isabs(self.volume.host_path) and not exists(self.volume.host_path):
358-
raise RuntimeError(
359-
f"Volume with the host path '{self.volume.host_path}' "
360-
"was requested but the directory does not exist"
361-
)
362-
return self
363-
364-
def __exit__(
365-
self,
366-
__exc_type: Optional[Type[BaseException]],
367-
__exc_value: Optional[BaseException],
368-
__traceback: Optional[TracebackType],
369-
) -> None:
370-
"""Cleans up the temporary host directory or the container volume."""
371-
assert self.volume.host_path
372-
373-
if self._tmpdir:
374-
_logger.debug(
375-
"cleaning up directory %s for the container volume %s",
376-
self.volume.host_path,
377-
self.volume.container_path,
378-
)
379-
self._tmpdir.cleanup()
380-
self.volume.host_path = None
381-
self.volume._vol_name = ""
382-
383-
384-
@overload
385-
def get_volume_creator(
386-
volume: ContainerVolume, runtime: OciRuntimeBase
387-
) -> VolumeCreator:
388-
... # pragma: no cover
389-
390-
391-
@overload
392-
def get_volume_creator(
393-
volume: BindMount, runtime: OciRuntimeBase
394-
) -> BindMountCreator:
395-
... # pragma: no cover
396-
397-
398-
def get_volume_creator(
399-
volume: Union[ContainerVolume, BindMount], runtime: OciRuntimeBase
400-
) -> Union[VolumeCreator, BindMountCreator]:
401-
"""Returns the appropriate volume creation context manager for the given
402-
volume.
403-
404-
"""
405-
if isinstance(volume, ContainerVolume):
406-
return VolumeCreator(volume, runtime)
407-
408-
if isinstance(volume, BindMount):
409-
return BindMountCreator(volume)
410-
411-
assert False, f"invalid volume type {type(volume)}" # pragma: no cover
412-
413-
414132
_CONTAINER_ENTRYPOINT = "/bin/bash"
415133
_CONTAINER_STOPSIGNAL = ("--stop-signal", "SIGTERM")
416134

0 commit comments

Comments
 (0)