Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ version.txt
tox_docker.egg-info
build/
dist/
.idea
17 changes: 10 additions & 7 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,12 @@ The ``[docker:container-name]`` section may contain the following directives:
``volumes``
A multi-line list of `volumes
<https://docs.docker.com/storage/volumes/>`__ to make available to the
container, as ``<type>:<options>:<outside_path_or_name>:<inside_path>``.
The ``type`` must be ``bind``, and the only supported options are ``rw``
(read-write) or ``ro`` (read-only). The ``outside_path_or_name`` must
be a path that exists on the host system. Both the ``outside_path``
and ``inside_path`` must be absolute paths.
container, as ``<type>:<options>:[<outside_path_or_name>:]<inside_path>``.
The ``type`` must be ``bind`` or ``tmpfs``. For ``bind`` type the only supported options are ``rw``
(read-write) or ``ro`` (read-only). The ``tmpfs`` type additionally supports ``size`` and ``mode`` options with ``;`` as delimiter.
The ``outside_path`` is required for the ``bind`` type and must be a path that exists on the host system.
For the ``tmpfs`` type ``outside_path`` should NOT be specified.
Both the ``outside_path`` and ``inside_path`` must be absolute paths.

``healthcheck_cmd``, ``healthcheck_interval``, ``healthcheck_retries``, ``healthcheck_start_period``, ``healthcheck_timeout``
These set or customize parameters of the container `health check
Expand Down Expand Up @@ -183,12 +184,14 @@ Example
healthcheck_retries = 30
healthcheck_interval = 1
healthcheck_start_period = 1
# Configure a bind-mounted volume on the host to store Postgres' data
# Configure a bind-mounted volume on the host to store Postgres' data and tmpfs as /tmp/
# NOTE: this is included for demonstration purposes of tox-docker's
# volume capability; you probably _don't_ want to do this for real
# volume capability; you probably _don't_ want to use bind mounts for real
# testing use cases, as this could persist data between test runs
volumes =
bind:rw:/my/own/datadir:/var/lib/postgresql/data
tmpfs:rw;size=64m;mode=1777:/tmp/


[docker:appserv]
# You can use any value that `docker run` would accept as the image
Expand Down
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ healthcheck_timeout = 1
healthcheck_start_period = 1
volumes =
bind:rw:{toxworkdir}:/healthcheck/web
tmpfs:rw;size=1m;mode=1777:/tmp/

# do NOT add this env to the envlist; it is supposed to fail,
# and the CI scripts run it directly with this expectation
Expand Down
82 changes: 63 additions & 19 deletions tox_docker/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pathlib import Path
from typing import Collection, Dict, List, Mapping, Optional
from typing import Collection, Dict, List, Mapping, Optional, Union
import os
import os.path
import re
Expand Down Expand Up @@ -118,25 +118,69 @@ def __init__(self, config_line: str) -> None:
class Volume:
def __init__(self, config_line: str) -> None:
parts = config_line.split(":")
if len(parts) != 4:
if len(parts) < 3 or len(parts) > 4:
raise ValueError(f"Volume {config_line!r} is malformed")
if parts[0] != "bind":
raise ValueError(f"Volume {config_line!r} type must be 'bind:'")
if parts[1] not in ("ro", "rw"):
raise ValueError(f"Volume {config_line!r} options must be 'ro' or 'rw'")

volume_type, mode, outside, inside = parts
if not os.path.isabs(outside):
raise ValueError(f"Volume source {outside!r} must be an absolute path")
if not os.path.isabs(inside):
raise ValueError(f"Mount point {inside!r} must be an absolute path")

self.docker_mount = Mount(
source=outside,
target=inside,
type=volume_type,
read_only=bool(mode == "ro"),
)

volume_type, options_str, *_outside_path, inside_path = parts

if not os.path.isabs(inside_path):
raise ValueError(f"Mount point {inside_path!r} must be an absolute path")

mount_params = {
"target": inside_path,
"type": volume_type,
**self._parse_options(config_line, volume_type, options_str),
}

# bind-specific checks and setup
if volume_type == "bind":
if len(_outside_path) != 1:
raise ValueError(
f"Volume {config_line!r} of type 'bind' must have an outside path"
)
outside = _outside_path[0]

if not os.path.isabs(outside):
raise ValueError(f"Volume source {outside!r} must be an absolute path")
mount_params["source"] = outside
# tmpfs-specific setup
elif volume_type == "tmpfs":
# tmpfs does not have source, so emtpy string
mount_params["source"] = ""
else:
raise ValueError(f"Volume {config_line!r} type must be 'bind' or 'tmpfs'")

self.docker_mount = Mount(**mount_params)

def _parse_options(
self, config_line: str, volume_type: str, options_str: str
) -> dict:
"""Parse volume options into `Mount()` params."""
result: Dict[str, Union[str, int, bool]] = {}
(
access_mode,
*other_options,
) = options_str.split(";")

# parsing access mode
if access_mode not in ("ro", "rw"):
raise ValueError(f"Volume {config_line!r} access mode must be 'ro' or 'rw'")
result["read_only"] = bool(access_mode == "ro")

# parsing tmpfs-specific options
if volume_type == "tmpfs":
for other_option in other_options:
key, value = other_option.split("=")
if key == "size": # volume size, such as 64m
result["tmpfs_size"] = value
elif key == "mode": # permissions, such as 1777
result["tmpfs_mode"] = int(value)
else:
raise ValueError(
f"'{other_option!r}' is not a valid option for volume of type '{volume_type}'"
)

return {}


class ContainerConfig:
Expand Down
2 changes: 1 addition & 1 deletion tox_docker/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def docker_run(

for mount in container_config.mounts:
source = mount["Source"]
if not os.path.exists(source):
if mount["Type"] != "tmpfs" and not os.path.exists(source):
raise ValueError(f"Volume source {source!r} does not exist")

assert container_config.runnable_image
Expand Down
Loading