From 0ecee0adfde7d8c76f1fbaa4b9784240aa1c6274 Mon Sep 17 00:00:00 2001 From: Souren Araya Date: Wed, 5 Mar 2025 01:46:01 +0700 Subject: [PATCH] Add support for tmpfs mounts. --- .gitignore | 1 + README.rst | 17 +++++---- tox.ini | 1 + tox_docker/config.py | 82 ++++++++++++++++++++++++++++++++++---------- tox_docker/plugin.py | 2 +- 5 files changed, 76 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index 94035e0..bdd28de 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ version.txt tox_docker.egg-info build/ dist/ +.idea diff --git a/README.rst b/README.rst index 5bd8bb2..c9d7cdd 100644 --- a/README.rst +++ b/README.rst @@ -109,11 +109,12 @@ The ``[docker:container-name]`` section may contain the following directives: ``volumes`` A multi-line list of `volumes `__ to make available to the - container, as ``:::``. - 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 ``::[:]``. + 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 @@ -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 diff --git a/tox.ini b/tox.ini index cf96c01..299f6ee 100644 --- a/tox.ini +++ b/tox.ini @@ -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 diff --git a/tox_docker/config.py b/tox_docker/config.py index 6ec0118..9e5193a 100644 --- a/tox_docker/config.py +++ b/tox_docker/config.py @@ -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 @@ -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: diff --git a/tox_docker/plugin.py b/tox_docker/plugin.py index b6e38c4..46632a6 100644 --- a/tox_docker/plugin.py +++ b/tox_docker/plugin.py @@ -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