2
2
and bind mounts.
3
3
4
4
"""
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
12
5
import enum
13
- import tempfile
14
6
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
16
21
17
- from pytest_container .runtime import OciRuntimeBase
18
22
from pytest_container .logging import _logger
23
+ from pytest_container .runtime import OciRuntimeBase
24
+ from typing_extensions import TypedDict
19
25
20
26
21
27
@enum .unique
@@ -60,16 +66,15 @@ class ContainerVolumeBase:
60
66
#: Path inside the container where this volume will be mounted
61
67
container_path : str
62
68
63
- _ : KW_ONLY
64
-
65
69
#: Flags for mounting this volume.
66
70
#:
67
71
#: Note that some flags are mutually exclusive and potentially not supported
68
72
#: by all container runtimes.
69
73
#:
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
71
75
#: 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.
73
78
#:
74
79
#: If flags is a list (even an empty one), then no flags are added.
75
80
flags : Optional [List [VolumeFlag ]] = None
@@ -81,10 +86,6 @@ class ContainerVolumeBase:
81
86
#: :py:attr:`~ContainerVolumeBase.flags`.
82
87
shared : bool = False
83
88
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
-
88
89
def __post_init__ (self ) -> None :
89
90
90
91
for mutually_exclusive_flags in (
@@ -102,20 +103,28 @@ def __post_init__(self) -> None:
102
103
)
103
104
104
105
@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 :
107
113
return self .flags
108
114
109
115
if self .shared :
110
116
return (VolumeFlag .SELINUX_SHARED ,)
111
- else :
112
- return (VolumeFlag .SELINUX_PRIVATE ,)
113
117
118
+ return (VolumeFlag .SELINUX_PRIVATE ,)
114
119
115
- def container_volume_cli_arg (
120
+
121
+ def _container_volume_cli_arg (
116
122
container_volume : ContainerVolumeBase , volume_name : str
117
123
) -> 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
+ """
119
128
res = f"-v={ volume_name } :{ container_volume .container_path } "
120
129
res += ":" + "," .join (str (f ) for f in container_volume ._flags )
121
130
return res
@@ -129,10 +138,29 @@ class ContainerVolume(ContainerVolumeBase):
129
138
"""
130
139
131
140
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
+
132
150
@dataclass (frozen = True )
133
151
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
+ )
134
159
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 )
136
164
137
165
138
166
@dataclass (frozen = True )
@@ -148,15 +176,29 @@ class BindMount(ContainerVolumeBase):
148
176
149
177
"""
150
178
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,
152
180
#: it refers to a volume to be auto-created. When omitted, a temporary
153
181
#: directory will be created and the path will be saved in this attribute.
154
182
host_path : Optional [str ] = None
155
183
156
184
157
185
@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 )
160
202
161
203
162
204
class _ContainerVolumeKWARGS (TypedDict , total = False ):
@@ -174,8 +216,13 @@ class VolumeCreator:
174
216
"""Context Manager to create and remove a :py:class:`ContainerVolume`.
175
217
176
218
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
178
220
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
+
179
226
"""
180
227
181
228
#: The volume to be created
@@ -184,7 +231,9 @@ class VolumeCreator:
184
231
#: The container runtime, via which the volume is created & destroyed
185
232
container_runtime : OciRuntimeBase
186
233
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
188
237
189
238
def __enter__ (self ) -> "VolumeCreator" :
190
239
"""Creates the container volume"""
@@ -195,7 +244,7 @@ def __enter__(self) -> "VolumeCreator":
195
244
.decode ()
196
245
.strip ()
197
246
)
198
- self ._created_volume = CreatedContainerVolume (
247
+ self .created_volume = CreatedContainerVolume (
199
248
container_path = self .volume .container_path ,
200
249
flags = self .volume .flags ,
201
250
shared = self .volume .shared ,
@@ -211,11 +260,11 @@ def __exit__(
211
260
__traceback : Optional [TracebackType ],
212
261
) -> None :
213
262
"""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
215
264
216
265
_logger .debug (
217
266
"cleaning up volume %s via %s" ,
218
- self ._created_volume .volume_id ,
267
+ self .created_volume .volume_id ,
219
268
self .container_runtime .runner_binary ,
220
269
)
221
270
@@ -226,7 +275,7 @@ def __exit__(
226
275
"volume" ,
227
276
"rm" ,
228
277
"-f" ,
229
- self ._created_volume .volume_id ,
278
+ self .created_volume .volume_id ,
230
279
],
231
280
)
232
281
@@ -244,7 +293,7 @@ class BindMountCreator:
244
293
#: internal temporary directory
245
294
_tmpdir : Optional [TEMPDIR_T ] = None
246
295
247
- _created_volume : Optional [CreatedBindMount ] = None
296
+ created_volume : Optional [CreatedBindMount ] = None
248
297
249
298
def __post__init__ (self ) -> None :
250
299
# the tempdir must not be set accidentally by the user
@@ -281,7 +330,7 @@ def __enter__(self) -> "BindMountCreator":
281
330
"was requested but the directory does not exist"
282
331
)
283
332
284
- self ._created_volume = CreatedBindMount (** kwargs )
333
+ self .created_volume = CreatedBindMount (** kwargs )
285
334
286
335
return self
287
336
@@ -292,13 +341,13 @@ def __exit__(
292
341
__traceback : Optional [TracebackType ],
293
342
) -> None :
294
343
"""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
296
345
297
346
if self ._tmpdir :
298
347
_logger .debug (
299
348
"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 ,
302
351
)
303
352
self ._tmpdir .cleanup ()
304
353
0 commit comments