Source code for ecs_composex.compose.compose_volumes.services_helpers
# SPDX-License-Identifier: MPL-2.0
# Copyright 2020-2022 John Mille <john@compose-x.io>
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ecs_composex.compose.compose_services import ComposeService
import re
from uuid import uuid4
from compose_x_common.compose_x_common import keyisset, set_else_none
from ecs_composex.common.logging import LOG
from . import ComposeVolume
[docs]def match_volumes_services_config(
service: ComposeService, vol_config: dict, volumes: list
):
"""
Function to map volume config in services and top-level volumes
:param service:
:param vol_config:
:param volumes:
:raises LookupError:
"""
if keyisset("source", vol_config) and vol_config["source"].startswith(r"/"):
vol_config["volume"] = None
service.volumes.append(vol_config)
LOG.info(f"volumes.{vol_config['source']} - Mapped to {service.name}")
return
else:
for volume in volumes:
v_volume = set_else_none("volume", volume)
v_source = set_else_none("source", vol_config)
if not v_source and not v_volume:
LOG.error(f"volumes - Failure to process {volume}")
continue
if volume.name == v_source:
volume.services.append(service)
vol_config["volume"] = volume
service.volumes.append(vol_config)
LOG.info(f"volumes.{volume.name} - Mapped to {service.name}")
return
raise LookupError(
f"Volume {vol_config['source']} was not found in {[vol.name for vol in volumes]}"
)
[docs]def handle_volume_str_config(service: ComposeService, config: str, volumes: list):
"""
Function to return the volume configuration (long)
:param ComposeService service:
:param str config:
:param list volumes:
"""
volume_config = {"read_only": False}
path_finder = re.compile(
r"(?:(?P<source>[\S][^:]+):)?(?P<target>/[^:\n]+)(?::(?P<mode>ro|rw|z))?"
)
path_match = path_finder.match(config)
if not path_match or (path_match and not path_match.group("target")):
raise ValueError(
f"Volume syntax {config} is invalid. Must follow the pattern",
path_finder.pattern,
)
else:
volume_config["target"] = path_match.group("target")
if path_match.group("source"):
volume_config["source"] = path_match.group("source")
else:
LOG.warning(f"No source defined with {config}. Creating docker volume")
new_volume = ComposeVolume(str(uuid4().hex)[:6], {})
new_volume.autogenerated = True
volumes.append(new_volume)
volume_config["source"] = new_volume.name
volume_config["volume"] = new_volume
if path_match.group("mode") and path_match.group("mode") == "ro":
volume_config["read_only"] = True
match_volumes_services_config(service, volume_config, volumes)
[docs]def is_tmpfs(config: dict) -> bool:
"""
Function to identify whether the volume defined is tmpfs
:param dict config:
:return: whether the volume defined is tmpfs
:rtype: bool
"""
if keyisset("tmpfs", config) or (
keyisset("type", config) and config["type"] == "tmpfs"
):
return True
return False
[docs]def handle_volume_dict_config(service: ComposeService, config: dict, volumes: list):
"""
:param ComposeService service:
:param dict config:
:param list volumes:
"""
volume_config = {"read_only": False}
required_keys = ["target", "source"]
if not is_tmpfs(config) and not all(key in config.keys() for key in required_keys):
raise KeyError(
"Volume configuration, when not tmpfs, requires at least",
required_keys,
"Got",
config.keys(),
)
volume_config.update(config)
if not is_tmpfs(volume_config):
match_volumes_services_config(service, volume_config, volumes)
[docs]def handle_tmpfs(service: ComposeService, volume: dict) -> None:
"""
Detect whether the volume is tmpfs and therefore validates further input
:param service:
:param volume:
"""
tmpfs_def = {}
if not keyisset("target", volume):
raise KeyError(
f"{service.name}.volumes - When defining tmpfs as volume, you must define a target"
)
tmpfs_def["ContainerPath"] = volume["target"]
if (
keyisset("tmpfs", volume)
and isinstance(volume["tmpfs"], dict)
and keyisset("size", volume["tmpfs"])
):
tmpfs_def["Size"] = int(volume["tmpfs"]["size"])
service.tmpfses.append(tmpfs_def)
[docs]def map_volumes(service: ComposeService, volumes: list = None) -> None:
"""
Method to apply mapping of volumes to the service and define the mapping configuration
:param service: The Service to map the volumes to.
:param list volumes:
"""
if not keyisset(ComposeVolume.main_key, service.definition):
return
for s_volume in service.definition[ComposeVolume.main_key]:
if (
isinstance(s_volume, dict)
and (keyisset("type", s_volume) and s_volume["type"] == "tmpfs")
or keyisset("tmpfs", s_volume)
):
handle_tmpfs(service, s_volume)
else:
if not volumes:
continue
if isinstance(s_volume, str):
handle_volume_str_config(service, s_volume, volumes)
elif isinstance(s_volume, dict):
handle_volume_dict_config(service, s_volume, volumes)