mirror of
https://github.com/MAGICGrants/truenas-apps.git
synced 2026-01-09 20:47:58 -05:00
apps: bump library (#1205)
* ci: make ci work with larger matrixes * no anchor support :( * whopps * hmm * more * compact * apps: bump library * bump
This commit is contained in:
@@ -10,8 +10,8 @@ icon: https://media.sys.truenas.net/apps/actual-budget/icons/icon.png
|
||||
keywords:
|
||||
- finance
|
||||
- budget
|
||||
lib_version: 2.1.2
|
||||
lib_version_hash: c302ec00f678308058addbb58715c569532162c1a80b35900b081444bf73addd
|
||||
lib_version: 2.1.5
|
||||
lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12
|
||||
maintainers:
|
||||
- email: dev@ixsystems.com
|
||||
name: truenas
|
||||
@@ -32,4 +32,4 @@ sources:
|
||||
- https://hub.docker.com/r/actualbudget/actual-server
|
||||
title: Actual Budget
|
||||
train: community
|
||||
version: 1.2.5
|
||||
version: 1.2.6
|
||||
|
||||
@@ -1,347 +0,0 @@
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from render import Render
|
||||
from storage import IxStorage
|
||||
|
||||
try:
|
||||
from .configs import ContainerConfigs
|
||||
from .depends import Depends
|
||||
from .deploy import Deploy
|
||||
from .devices import Devices
|
||||
from .dns import Dns
|
||||
from .environment import Environment
|
||||
from .error import RenderError
|
||||
from .formatter import escape_dollar, get_image_with_hashed_data
|
||||
from .healthcheck import Healthcheck
|
||||
from .labels import Labels
|
||||
from .ports import Ports
|
||||
from .restart import RestartPolicy
|
||||
from .validations import valid_network_mode_or_raise, valid_cap_or_raise, valid_pull_policy_or_raise
|
||||
from .storage import Storage
|
||||
from .sysctls import Sysctls
|
||||
except ImportError:
|
||||
from configs import ContainerConfigs
|
||||
from depends import Depends
|
||||
from deploy import Deploy
|
||||
from devices import Devices
|
||||
from dns import Dns
|
||||
from environment import Environment
|
||||
from error import RenderError
|
||||
from formatter import escape_dollar, get_image_with_hashed_data
|
||||
from healthcheck import Healthcheck
|
||||
from labels import Labels
|
||||
from ports import Ports
|
||||
from restart import RestartPolicy
|
||||
from validations import valid_network_mode_or_raise, valid_cap_or_raise, valid_pull_policy_or_raise
|
||||
from storage import Storage
|
||||
from sysctls import Sysctls
|
||||
|
||||
|
||||
class Container:
|
||||
def __init__(self, render_instance: "Render", name: str, image: str):
|
||||
self._render_instance = render_instance
|
||||
|
||||
self._name: str = name
|
||||
self._image: str = self._resolve_image(image)
|
||||
self._build_image: str = ""
|
||||
self._pull_policy: str = ""
|
||||
self._user: str = ""
|
||||
self._tty: bool = False
|
||||
self._stdin_open: bool = False
|
||||
self._init: bool | None = None
|
||||
self._read_only: bool | None = None
|
||||
self._hostname: str = ""
|
||||
self._cap_drop: set[str] = set(["ALL"]) # Drop all capabilities by default and add caps granularly
|
||||
self._cap_add: set[str] = set()
|
||||
self._security_opt: set[str] = set(["no-new-privileges"])
|
||||
self._privileged: bool = False
|
||||
self._group_add: set[int | str] = set()
|
||||
self._network_mode: str = ""
|
||||
self._entrypoint: list[str] = []
|
||||
self._command: list[str] = []
|
||||
self._grace_period: int | None = None
|
||||
self._shm_size: int | None = None
|
||||
self._storage: Storage = Storage(self._render_instance)
|
||||
self.sysctls: Sysctls = Sysctls(self._render_instance, self)
|
||||
self.configs: ContainerConfigs = ContainerConfigs(self._render_instance, self._render_instance.configs)
|
||||
self.deploy: Deploy = Deploy(self._render_instance)
|
||||
self.networks: set[str] = set()
|
||||
self.devices: Devices = Devices(self._render_instance)
|
||||
self.environment: Environment = Environment(self._render_instance, self.deploy.resources)
|
||||
self.dns: Dns = Dns(self._render_instance)
|
||||
self.depends: Depends = Depends(self._render_instance)
|
||||
self.healthcheck: Healthcheck = Healthcheck(self._render_instance)
|
||||
self.labels: Labels = Labels(self._render_instance)
|
||||
self.restart: RestartPolicy = RestartPolicy(self._render_instance)
|
||||
self.ports: Ports = Ports(self._render_instance)
|
||||
|
||||
self._auto_set_network_mode()
|
||||
self._auto_add_labels()
|
||||
self._auto_add_groups()
|
||||
|
||||
def _auto_add_groups(self):
|
||||
self.add_group(568)
|
||||
|
||||
def _auto_set_network_mode(self):
|
||||
if self._render_instance.values.get("network", {}).get("host_network", False):
|
||||
self.set_network_mode("host")
|
||||
|
||||
def _auto_add_labels(self):
|
||||
labels = self._render_instance.values.get("labels", [])
|
||||
if not labels:
|
||||
return
|
||||
|
||||
for label in labels:
|
||||
containers = label.get("containers", [])
|
||||
if not containers:
|
||||
raise RenderError(f'Label [{label.get("key", "")}] must have at least one container')
|
||||
|
||||
if self._name in containers:
|
||||
self.labels.add_label(label["key"], label["value"])
|
||||
|
||||
def _resolve_image(self, image: str):
|
||||
images = self._render_instance.values["images"]
|
||||
if image not in images:
|
||||
raise RenderError(
|
||||
f"Image [{image}] not found in values. " f"Available images: [{', '.join(images.keys())}]"
|
||||
)
|
||||
repo = images[image].get("repository", "")
|
||||
tag = images[image].get("tag", "")
|
||||
|
||||
if not repo:
|
||||
raise RenderError(f"Repository not found for image [{image}]")
|
||||
if not tag:
|
||||
raise RenderError(f"Tag not found for image [{image}]")
|
||||
|
||||
return f"{repo}:{tag}"
|
||||
|
||||
def build_image(self, content: list[str | None]):
|
||||
dockerfile = f"FROM {self._image}\n"
|
||||
for line in content:
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("FROM"):
|
||||
# TODO: This will also block multi-stage builds
|
||||
# We can revisit this later if we need it
|
||||
raise RenderError(
|
||||
"FROM cannot be used in build image. Define the base image when creating the container."
|
||||
)
|
||||
dockerfile += line + "\n"
|
||||
|
||||
self._build_image = dockerfile
|
||||
self._image = get_image_with_hashed_data(self._image, dockerfile)
|
||||
|
||||
def set_pull_policy(self, pull_policy: str):
|
||||
self._pull_policy = valid_pull_policy_or_raise(pull_policy)
|
||||
|
||||
def set_user(self, user: int, group: int):
|
||||
for i in (user, group):
|
||||
if not isinstance(i, int) or i < 0:
|
||||
raise RenderError(f"User/Group [{i}] is not valid")
|
||||
self._user = f"{user}:{group}"
|
||||
|
||||
def add_group(self, group: int | str):
|
||||
if isinstance(group, str):
|
||||
group = str(group).strip()
|
||||
if group.isdigit():
|
||||
raise RenderError(f"Group is a number [{group}] but passed as a string")
|
||||
|
||||
if group in self._group_add:
|
||||
raise RenderError(f"Group [{group}] already added")
|
||||
self._group_add.add(group)
|
||||
|
||||
def get_additional_groups(self) -> list[int | str]:
|
||||
result = []
|
||||
if self.deploy.resources.has_gpus() or self.devices.has_gpus():
|
||||
result.append(44) # video
|
||||
result.append(107) # render
|
||||
return result
|
||||
|
||||
def get_current_groups(self) -> list[str]:
|
||||
result = [str(g) for g in self._group_add]
|
||||
result.extend([str(g) for g in self.get_additional_groups()])
|
||||
return result
|
||||
|
||||
def set_tty(self, enabled: bool = False):
|
||||
self._tty = enabled
|
||||
|
||||
def set_stdin(self, enabled: bool = False):
|
||||
self._stdin_open = enabled
|
||||
|
||||
def set_init(self, enabled: bool = False):
|
||||
self._init = enabled
|
||||
|
||||
def set_read_only(self, enabled: bool = False):
|
||||
self._read_only = enabled
|
||||
|
||||
def set_hostname(self, hostname: str):
|
||||
self._hostname = hostname
|
||||
|
||||
def set_grace_period(self, grace_period: int):
|
||||
if grace_period < 0:
|
||||
raise RenderError(f"Grace period [{grace_period}] cannot be negative")
|
||||
self._grace_period = grace_period
|
||||
|
||||
def set_privileged(self, enabled: bool = False):
|
||||
self._privileged = enabled
|
||||
|
||||
def clear_caps(self):
|
||||
self._cap_add.clear()
|
||||
self._cap_drop.clear()
|
||||
|
||||
def add_caps(self, caps: list[str]):
|
||||
for c in caps:
|
||||
if c in self._cap_add:
|
||||
raise RenderError(f"Capability [{c}] already added")
|
||||
self._cap_add.add(valid_cap_or_raise(c))
|
||||
|
||||
def add_security_opt(self, opt: str):
|
||||
if opt in self._security_opt:
|
||||
raise RenderError(f"Security Option [{opt}] already added")
|
||||
self._security_opt.add(opt)
|
||||
|
||||
def remove_security_opt(self, opt: str):
|
||||
self._security_opt.remove(opt)
|
||||
|
||||
def set_network_mode(self, mode: str):
|
||||
self._network_mode = valid_network_mode_or_raise(mode, self._render_instance.container_names())
|
||||
|
||||
def set_entrypoint(self, entrypoint: list[str]):
|
||||
self._entrypoint = [escape_dollar(str(e)) for e in entrypoint]
|
||||
|
||||
def set_command(self, command: list[str]):
|
||||
self._command = [escape_dollar(str(e)) for e in command]
|
||||
|
||||
def add_storage(self, mount_path: str, config: "IxStorage"):
|
||||
self._storage.add(mount_path, config)
|
||||
|
||||
def add_docker_socket(self, read_only: bool = True, mount_path: str = "/var/run/docker.sock"):
|
||||
self.add_group(999)
|
||||
self._storage._add_docker_socket(read_only, mount_path)
|
||||
|
||||
def add_tun_device(self, read_only: bool = True, mount_path: str = "/dev/net/tun"):
|
||||
self._storage._add_tun_device(read_only, mount_path)
|
||||
|
||||
def add_snd_device(self):
|
||||
self.add_group(29)
|
||||
self.devices._add_snd_device()
|
||||
|
||||
def set_shm_size_mb(self, size: int):
|
||||
self._shm_size = size
|
||||
|
||||
# Easily remove devices from the container
|
||||
# Useful in dependencies like postgres and redis
|
||||
# where there is no need to pass devices to them
|
||||
def remove_devices(self):
|
||||
self.deploy.resources.remove_devices()
|
||||
self.devices.remove_devices()
|
||||
|
||||
@property
|
||||
def storage(self):
|
||||
return self._storage
|
||||
|
||||
def render(self) -> dict[str, Any]:
|
||||
if self._network_mode and self.networks:
|
||||
raise RenderError("Cannot set both [network_mode] and [networks]")
|
||||
|
||||
result = {
|
||||
"image": self._image,
|
||||
"platform": "linux/amd64",
|
||||
"tty": self._tty,
|
||||
"stdin_open": self._stdin_open,
|
||||
"restart": self.restart.render(),
|
||||
}
|
||||
|
||||
if self._pull_policy:
|
||||
result["pull_policy"] = self._pull_policy
|
||||
|
||||
if self.healthcheck.has_healthcheck():
|
||||
result["healthcheck"] = self.healthcheck.render()
|
||||
|
||||
if self._hostname:
|
||||
result["hostname"] = self._hostname
|
||||
|
||||
if self._build_image:
|
||||
result["build"] = {"tags": [self._image], "dockerfile_inline": self._build_image}
|
||||
|
||||
if self.configs.has_configs():
|
||||
result["configs"] = self.configs.render()
|
||||
|
||||
if self._init is not None:
|
||||
result["init"] = self._init
|
||||
|
||||
if self._read_only is not None:
|
||||
result["read_only"] = self._read_only
|
||||
|
||||
if self._grace_period is not None:
|
||||
result["stop_grace_period"] = f"{self._grace_period}s"
|
||||
|
||||
if self._user:
|
||||
result["user"] = self._user
|
||||
|
||||
for g in self.get_additional_groups():
|
||||
self.add_group(g)
|
||||
|
||||
if self._group_add:
|
||||
result["group_add"] = sorted(self._group_add, key=lambda g: (isinstance(g, str), g))
|
||||
|
||||
if self._shm_size is not None:
|
||||
result["shm_size"] = f"{self._shm_size}M"
|
||||
|
||||
if self._privileged is not None:
|
||||
result["privileged"] = self._privileged
|
||||
|
||||
if self._cap_drop:
|
||||
result["cap_drop"] = sorted(self._cap_drop)
|
||||
|
||||
if self._cap_add:
|
||||
result["cap_add"] = sorted(self._cap_add)
|
||||
|
||||
if self._security_opt:
|
||||
result["security_opt"] = sorted(self._security_opt)
|
||||
|
||||
if self._network_mode:
|
||||
result["network_mode"] = self._network_mode
|
||||
|
||||
if self.sysctls.has_sysctls():
|
||||
result["sysctls"] = self.sysctls.render()
|
||||
|
||||
if self._network_mode != "host":
|
||||
if self.ports.has_ports():
|
||||
result["ports"] = self.ports.render()
|
||||
|
||||
if self._entrypoint:
|
||||
result["entrypoint"] = self._entrypoint
|
||||
|
||||
if self._command:
|
||||
result["command"] = self._command
|
||||
|
||||
if self.devices.has_devices():
|
||||
result["devices"] = self.devices.render()
|
||||
|
||||
if self.deploy.has_deploy():
|
||||
result["deploy"] = self.deploy.render()
|
||||
|
||||
if self.environment.has_variables():
|
||||
result["environment"] = self.environment.render()
|
||||
|
||||
if self.labels.has_labels():
|
||||
result["labels"] = self.labels.render()
|
||||
|
||||
if self.dns.has_dns_nameservers():
|
||||
result["dns"] = self.dns.render_dns_nameservers()
|
||||
|
||||
if self.dns.has_dns_searches():
|
||||
result["dns_search"] = self.dns.render_dns_searches()
|
||||
|
||||
if self.dns.has_dns_opts():
|
||||
result["dns_opt"] = self.dns.render_dns_opts()
|
||||
|
||||
if self.depends.has_dependencies():
|
||||
result["depends_on"] = self.depends.render()
|
||||
|
||||
if self._storage.has_mounts():
|
||||
result["volumes"] = self._storage.render()
|
||||
|
||||
return result
|
||||
@@ -1,68 +0,0 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from render import Render
|
||||
|
||||
try:
|
||||
from .error import RenderError
|
||||
from .device import Device
|
||||
except ImportError:
|
||||
from error import RenderError
|
||||
from device import Device
|
||||
|
||||
|
||||
class Devices:
|
||||
def __init__(self, render_instance: "Render"):
|
||||
self._render_instance = render_instance
|
||||
self._devices: set[Device] = set()
|
||||
|
||||
# Tracks all container device paths to make sure they are not duplicated
|
||||
self._container_device_paths: set[str] = set()
|
||||
# Scan values for devices we should automatically add
|
||||
# for example /dev/dri for gpus
|
||||
self._auto_add_devices_from_values()
|
||||
|
||||
def _auto_add_devices_from_values(self):
|
||||
resources = self._render_instance.values.get("resources", {})
|
||||
|
||||
if resources.get("gpus", {}).get("use_all_gpus", False):
|
||||
self.add_device("/dev/dri", "/dev/dri", allow_disallowed=True)
|
||||
if resources["gpus"].get("kfd_device_exists", False):
|
||||
self.add_device("/dev/kfd", "/dev/kfd", allow_disallowed=True) # AMD ROCm
|
||||
|
||||
def add_device(self, host_device: str, container_device: str, cgroup_perm: str = "", allow_disallowed=False):
|
||||
# Host device can be mapped to multiple container devices,
|
||||
# so we only make sure container devices are not duplicated
|
||||
if container_device in self._container_device_paths:
|
||||
raise RenderError(f"Device with container path [{container_device}] already added")
|
||||
|
||||
self._devices.add(Device(host_device, container_device, cgroup_perm, allow_disallowed))
|
||||
self._container_device_paths.add(container_device)
|
||||
|
||||
def add_usb_bus(self):
|
||||
self.add_device("/dev/bus/usb", "/dev/bus/usb", allow_disallowed=True)
|
||||
|
||||
def _add_snd_device(self):
|
||||
self.add_device("/dev/snd", "/dev/snd", allow_disallowed=True)
|
||||
|
||||
def has_devices(self):
|
||||
return len(self._devices) > 0
|
||||
|
||||
# Mainly will be used from dependencies
|
||||
# There is no reason to pass devices to
|
||||
# redis or postgres for example
|
||||
def remove_devices(self):
|
||||
self._devices.clear()
|
||||
self._container_device_paths.clear()
|
||||
|
||||
# Check if there are any gpu devices
|
||||
# Used to determine if we should add groups
|
||||
# like 'video' to the container
|
||||
def has_gpus(self):
|
||||
for d in self._devices:
|
||||
if d.host_device == "/dev/dri":
|
||||
return True
|
||||
return False
|
||||
|
||||
def render(self) -> list[str]:
|
||||
return sorted([d.render() for d in self._devices])
|
||||
@@ -1,149 +0,0 @@
|
||||
import re
|
||||
import copy
|
||||
import bcrypt
|
||||
import secrets
|
||||
from base64 import b64encode
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from render import Render
|
||||
|
||||
try:
|
||||
from .error import RenderError
|
||||
from .volume_sources import HostPathSource, IxVolumeSource
|
||||
except ImportError:
|
||||
from error import RenderError
|
||||
from volume_sources import HostPathSource, IxVolumeSource
|
||||
|
||||
|
||||
class Functions:
|
||||
def __init__(self, render_instance: "Render"):
|
||||
self._render_instance = render_instance
|
||||
|
||||
def _bcrypt_hash(self, password):
|
||||
hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||
return hashed
|
||||
|
||||
def _htpasswd(self, username, password):
|
||||
hashed = self._bcrypt_hash(password)
|
||||
return username + ":" + hashed
|
||||
|
||||
def _secure_string(self, length):
|
||||
return secrets.token_urlsafe(length)
|
||||
|
||||
def _basic_auth(self, username, password):
|
||||
return b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8")
|
||||
|
||||
def _basic_auth_header(self, username, password):
|
||||
return f"Basic {self._basic_auth(username, password)}"
|
||||
|
||||
def _fail(self, message):
|
||||
raise RenderError(message)
|
||||
|
||||
def _camel_case(self, string):
|
||||
return string.title()
|
||||
|
||||
def _auto_cast(self, value):
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if value.lower() in ["true", "false"]:
|
||||
return value.lower() == "true"
|
||||
|
||||
return value
|
||||
|
||||
def _match_regex(self, value, regex):
|
||||
if not re.match(regex, value):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _must_match_regex(self, value, regex):
|
||||
if not self._match_regex(value, regex):
|
||||
raise RenderError(f"Expected [{value}] to match [{regex}]")
|
||||
return value
|
||||
|
||||
def _is_boolean(self, string):
|
||||
return string.lower() in ["true", "false"]
|
||||
|
||||
def _is_number(self, string):
|
||||
try:
|
||||
float(string)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def _copy_dict(self, dict):
|
||||
return copy.deepcopy(dict)
|
||||
|
||||
def _merge_dicts(self, *dicts):
|
||||
merged_dict = {}
|
||||
for dictionary in dicts:
|
||||
merged_dict.update(dictionary)
|
||||
return merged_dict
|
||||
|
||||
def _disallow_chars(self, string: str, chars: list[str], key: str):
|
||||
for char in chars:
|
||||
if char in string:
|
||||
raise RenderError(f"Disallowed character [{char}] in [{key}]")
|
||||
return string
|
||||
|
||||
def _or_default(self, value, default):
|
||||
if not value:
|
||||
return default
|
||||
return value
|
||||
|
||||
def _temp_config(self, name):
|
||||
if not name:
|
||||
raise RenderError("Expected [name] to be set when calling [temp_config].")
|
||||
return {"type": "temporary", "volume_config": {"volume_name": name}}
|
||||
|
||||
def _get_host_path(self, storage):
|
||||
source_type = storage.get("type", "")
|
||||
if not source_type:
|
||||
raise RenderError("Expected [type] to be set for volume mounts.")
|
||||
|
||||
match source_type:
|
||||
case "host_path":
|
||||
mount_config = storage.get("host_path_config")
|
||||
if mount_config is None:
|
||||
raise RenderError("Expected [host_path_config] to be set for [host_path] type.")
|
||||
host_source = HostPathSource(self._render_instance, mount_config).get()
|
||||
return host_source
|
||||
case "ix_volume":
|
||||
mount_config = storage.get("ix_volume_config")
|
||||
if mount_config is None:
|
||||
raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.")
|
||||
ix_source = IxVolumeSource(self._render_instance, mount_config).get()
|
||||
return ix_source
|
||||
case _:
|
||||
raise RenderError(f"Storage type [{source_type}] does not support host path.")
|
||||
|
||||
def func_map(self):
|
||||
# TODO: Check what is no longer used and remove
|
||||
return {
|
||||
"auto_cast": self._auto_cast,
|
||||
"basic_auth_header": self._basic_auth_header,
|
||||
"basic_auth": self._basic_auth,
|
||||
"bcrypt_hash": self._bcrypt_hash,
|
||||
"camel_case": self._camel_case,
|
||||
"copy_dict": self._copy_dict,
|
||||
"fail": self._fail,
|
||||
"htpasswd": self._htpasswd,
|
||||
"is_boolean": self._is_boolean,
|
||||
"is_number": self._is_number,
|
||||
"match_regex": self._match_regex,
|
||||
"merge_dicts": self._merge_dicts,
|
||||
"must_match_regex": self._must_match_regex,
|
||||
"secure_string": self._secure_string,
|
||||
"disallow_chars": self._disallow_chars,
|
||||
"get_host_path": self._get_host_path,
|
||||
"or_default": self._or_default,
|
||||
"temp_config": self._temp_config,
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
from typing import TYPE_CHECKING, TypedDict, Literal, NotRequired, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from render import Render
|
||||
|
||||
try:
|
||||
from .error import RenderError
|
||||
from .validations import valid_fs_path_or_raise
|
||||
from .volume_mount import VolumeMount
|
||||
except ImportError:
|
||||
from error import RenderError
|
||||
from validations import valid_fs_path_or_raise
|
||||
from volume_mount import VolumeMount
|
||||
|
||||
|
||||
class IxStorageTmpfsConfig(TypedDict):
|
||||
size: NotRequired[int]
|
||||
mode: NotRequired[str]
|
||||
|
||||
|
||||
class AclConfig(TypedDict, total=False):
|
||||
path: str
|
||||
|
||||
|
||||
class IxStorageHostPathConfig(TypedDict):
|
||||
path: NotRequired[str] # Either this or acl.path must be set
|
||||
acl_enable: NotRequired[bool]
|
||||
acl: NotRequired[AclConfig]
|
||||
create_host_path: NotRequired[bool]
|
||||
propagation: NotRequired[Literal["shared", "slave", "private", "rshared", "rslave", "rprivate"]]
|
||||
auto_permissions: NotRequired[bool] # Only when acl_enable is false
|
||||
|
||||
|
||||
class IxStorageIxVolumeConfig(TypedDict):
|
||||
dataset_name: str
|
||||
acl_enable: NotRequired[bool]
|
||||
acl_entries: NotRequired[AclConfig]
|
||||
create_host_path: NotRequired[bool]
|
||||
propagation: NotRequired[Literal["shared", "slave", "private", "rshared", "rslave", "rprivate"]]
|
||||
auto_permissions: NotRequired[bool] # Only when acl_enable is false
|
||||
|
||||
|
||||
class IxStorageVolumeConfig(TypedDict):
|
||||
volume_name: NotRequired[str]
|
||||
nocopy: NotRequired[bool]
|
||||
auto_permissions: NotRequired[bool]
|
||||
|
||||
|
||||
class IxStorageNfsConfig(TypedDict):
|
||||
server: str
|
||||
path: str
|
||||
options: NotRequired[list[str]]
|
||||
|
||||
|
||||
class IxStorageCifsConfig(TypedDict):
|
||||
server: str
|
||||
path: str
|
||||
username: str
|
||||
password: str
|
||||
domain: NotRequired[str]
|
||||
options: NotRequired[list[str]]
|
||||
|
||||
|
||||
IxStorageVolumeLikeConfigs = Union[IxStorageVolumeConfig, IxStorageNfsConfig, IxStorageCifsConfig, IxStorageTmpfsConfig]
|
||||
IxStorageBindLikeConfigs = Union[IxStorageHostPathConfig, IxStorageIxVolumeConfig]
|
||||
IxStorageLikeConfigs = Union[IxStorageBindLikeConfigs, IxStorageVolumeLikeConfigs]
|
||||
|
||||
|
||||
class IxStorage(TypedDict):
|
||||
type: Literal["ix_volume", "host_path", "tmpfs", "volume", "anonymous", "temporary"]
|
||||
read_only: NotRequired[bool]
|
||||
|
||||
ix_volume_config: NotRequired[IxStorageIxVolumeConfig]
|
||||
host_path_config: NotRequired[IxStorageHostPathConfig]
|
||||
tmpfs_config: NotRequired[IxStorageTmpfsConfig]
|
||||
volume_config: NotRequired[IxStorageVolumeConfig]
|
||||
nfs_config: NotRequired[IxStorageNfsConfig]
|
||||
cifs_config: NotRequired[IxStorageCifsConfig]
|
||||
|
||||
|
||||
class Storage:
|
||||
def __init__(self, render_instance: "Render"):
|
||||
self._render_instance = render_instance
|
||||
self._volume_mounts: set[VolumeMount] = set()
|
||||
|
||||
def add(self, mount_path: str, config: "IxStorage"):
|
||||
mount_path = valid_fs_path_or_raise(mount_path)
|
||||
if mount_path in [m.mount_path for m in self._volume_mounts]:
|
||||
raise RenderError(f"Mount path [{mount_path}] already used for another volume mount")
|
||||
|
||||
volume_mount = VolumeMount(self._render_instance, mount_path, config)
|
||||
self._volume_mounts.add(volume_mount)
|
||||
|
||||
def _add_docker_socket(self, read_only: bool = True, mount_path: str = ""):
|
||||
mount_path = valid_fs_path_or_raise(mount_path)
|
||||
cfg: "IxStorage" = {
|
||||
"type": "host_path",
|
||||
"read_only": read_only,
|
||||
"host_path_config": {"path": "/var/run/docker.sock", "create_host_path": False},
|
||||
}
|
||||
self.add(mount_path, cfg)
|
||||
|
||||
def _add_tun_device(self, read_only: bool = True, mount_path: str = ""):
|
||||
mount_path = valid_fs_path_or_raise(mount_path)
|
||||
cfg: "IxStorage" = {
|
||||
"type": "host_path",
|
||||
"read_only": read_only,
|
||||
"host_path_config": {"path": "/dev/net/tun", "create_host_path": False},
|
||||
}
|
||||
self.add(mount_path, cfg)
|
||||
|
||||
def has_mounts(self) -> bool:
|
||||
return bool(self._volume_mounts)
|
||||
|
||||
def render(self):
|
||||
return [vm.render() for vm in sorted(self._volume_mounts, key=lambda vm: vm.mount_path)]
|
||||
@@ -1,360 +0,0 @@
|
||||
import pytest
|
||||
|
||||
|
||||
from render import Render
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_values():
|
||||
return {
|
||||
"images": {
|
||||
"test_image": {
|
||||
"repository": "nginx",
|
||||
"tag": "latest",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_empty_container_name(mock_values):
|
||||
render = Render(mock_values)
|
||||
with pytest.raises(Exception):
|
||||
render.add_container(" ", "test_image")
|
||||
|
||||
|
||||
def test_resolve_image(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["image"] == "nginx:latest"
|
||||
|
||||
|
||||
def test_missing_repo(mock_values):
|
||||
mock_values["images"]["test_image"]["repository"] = ""
|
||||
render = Render(mock_values)
|
||||
with pytest.raises(Exception):
|
||||
render.add_container("test_container", "test_image")
|
||||
|
||||
|
||||
def test_missing_tag(mock_values):
|
||||
mock_values["images"]["test_image"]["tag"] = ""
|
||||
render = Render(mock_values)
|
||||
with pytest.raises(Exception):
|
||||
render.add_container("test_container", "test_image")
|
||||
|
||||
|
||||
def test_non_existing_image(mock_values):
|
||||
render = Render(mock_values)
|
||||
with pytest.raises(Exception):
|
||||
render.add_container("test_container", "non_existing_image")
|
||||
|
||||
|
||||
def test_pull_policy(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_pull_policy("always")
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["pull_policy"] == "always"
|
||||
|
||||
|
||||
def test_invalid_pull_policy(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
with pytest.raises(Exception):
|
||||
c1.set_pull_policy("invalid_policy")
|
||||
|
||||
|
||||
def test_clear_caps(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.add_caps(["NET_ADMIN"])
|
||||
c1.clear_caps()
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert "cap_drop" not in output["services"]["test_container"]
|
||||
assert "cap_add" not in output["services"]["test_container"]
|
||||
|
||||
|
||||
def test_privileged(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_privileged(True)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["privileged"] is True
|
||||
|
||||
|
||||
def test_tty(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_tty(True)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["tty"] is True
|
||||
|
||||
|
||||
def test_init(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_init(True)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["init"] is True
|
||||
|
||||
|
||||
def test_read_only(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_read_only(True)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["read_only"] is True
|
||||
|
||||
|
||||
def test_stdin(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_stdin(True)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["stdin_open"] is True
|
||||
|
||||
|
||||
def test_hostname(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_hostname("test_hostname")
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["hostname"] == "test_hostname"
|
||||
|
||||
|
||||
def test_grace_period(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_grace_period(10)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["stop_grace_period"] == "10s"
|
||||
|
||||
|
||||
def test_user(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_user(1000, 1000)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["user"] == "1000:1000"
|
||||
|
||||
|
||||
def test_invalid_user(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.set_user(-100, 1000)
|
||||
|
||||
|
||||
def test_add_group(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_group(1000)
|
||||
c1.add_group("video")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["group_add"] == [568, 1000, "video"]
|
||||
|
||||
|
||||
def test_add_duplicate_group(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_group(1000)
|
||||
with pytest.raises(Exception):
|
||||
c1.add_group(1000)
|
||||
|
||||
|
||||
def test_add_group_as_string(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_group("1000")
|
||||
|
||||
|
||||
def test_add_docker_socket(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_docker_socket()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["group_add"] == [568, 999]
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/var/run/docker.sock",
|
||||
"target": "/var/run/docker.sock",
|
||||
"read_only": True,
|
||||
"bind": {
|
||||
"propagation": "rprivate",
|
||||
"create_host_path": False,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_tun_device(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_tun_device()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/dev/net/tun",
|
||||
"target": "/dev/net/tun",
|
||||
"read_only": True,
|
||||
"bind": {
|
||||
"propagation": "rprivate",
|
||||
"create_host_path": False,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_snd_device(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_snd_device()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["devices"] == ["/dev/snd:/dev/snd"]
|
||||
assert output["services"]["test_container"]["group_add"] == [29, 568]
|
||||
|
||||
|
||||
def test_shm_size(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.set_shm_size_mb(10)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["shm_size"] == "10M"
|
||||
|
||||
|
||||
def test_valid_caps(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_caps(["ALL", "NET_ADMIN"])
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["cap_add"] == ["ALL", "NET_ADMIN"]
|
||||
assert output["services"]["test_container"]["cap_drop"] == ["ALL"]
|
||||
|
||||
|
||||
def test_add_duplicate_caps(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_caps(["ALL", "NET_ADMIN", "NET_ADMIN"])
|
||||
|
||||
|
||||
def test_invalid_caps(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_caps(["invalid_cap"])
|
||||
|
||||
|
||||
def test_remove_security_opt(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.remove_security_opt("no-new-privileges")
|
||||
output = render.render()
|
||||
assert "security_opt" not in output["services"]["test_container"]
|
||||
|
||||
|
||||
def test_add_security_opt(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_security_opt("seccomp=unconfined")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["security_opt"] == [
|
||||
"no-new-privileges",
|
||||
"seccomp=unconfined",
|
||||
]
|
||||
|
||||
|
||||
def test_add_duplicate_security_opt(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_security_opt("no-new-privileges")
|
||||
|
||||
|
||||
def test_network_mode(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.set_network_mode("host")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["network_mode"] == "host"
|
||||
|
||||
|
||||
def test_auto_network_mode_with_host_network(mock_values):
|
||||
mock_values["network"] = {"host_network": True}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["network_mode"] == "host"
|
||||
|
||||
|
||||
def test_network_mode_with_container(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.set_network_mode("service:test_container")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["network_mode"] == "service:test_container"
|
||||
|
||||
|
||||
def test_network_mode_with_container_missing(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.set_network_mode("service:missing_container")
|
||||
|
||||
|
||||
def test_invalid_network_mode(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.set_network_mode("invalid_mode")
|
||||
|
||||
|
||||
def test_entrypoint(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_entrypoint(["/bin/bash", "-c", "echo hello $MY_ENV"])
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["entrypoint"] == ["/bin/bash", "-c", "echo hello $$MY_ENV"]
|
||||
|
||||
|
||||
def test_command(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_command(["echo", "hello $MY_ENV"])
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["command"] == ["echo", "hello $$MY_ENV"]
|
||||
@@ -1,131 +0,0 @@
|
||||
import pytest
|
||||
|
||||
|
||||
from render import Render
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_values():
|
||||
return {
|
||||
"images": {
|
||||
"test_image": {
|
||||
"repository": "nginx",
|
||||
"tag": "latest",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_add_device(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
|
||||
c1.devices.add_device("/h/dev/sdb", "/c/dev/sdb", "rwm")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["devices"] == ["/h/dev/sda:/c/dev/sda", "/h/dev/sdb:/c/dev/sdb:rwm"]
|
||||
|
||||
|
||||
def test_devices_without_host(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.devices.add_device("", "/c/dev/sda")
|
||||
|
||||
|
||||
def test_devices_without_container(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.devices.add_device("/h/dev/sda", "")
|
||||
|
||||
|
||||
def test_add_duplicate_device(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
|
||||
with pytest.raises(Exception):
|
||||
c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
|
||||
|
||||
|
||||
def test_add_device_with_invalid_container_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.devices.add_device("/h/dev/sda", "c/dev/sda")
|
||||
|
||||
|
||||
def test_add_device_with_invalid_host_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.devices.add_device("h/dev/sda", "/c/dev/sda")
|
||||
|
||||
|
||||
def test_add_disallowed_device(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.devices.add_device("/dev/dri", "/c/dev/sda")
|
||||
|
||||
|
||||
def test_add_device_with_invalid_cgroup_perm(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.devices.add_device("/h/dev/sda", "/c/dev/sda", "invalid")
|
||||
|
||||
|
||||
def test_automatically_add_gpu_devices(mock_values):
|
||||
mock_values["resources"] = {"gpus": {"use_all_gpus": True}}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["devices"] == ["/dev/dri:/dev/dri"]
|
||||
assert output["services"]["test_container"]["group_add"] == [44, 107, 568]
|
||||
|
||||
|
||||
def test_automatically_add_gpu_devices_and_kfd(mock_values):
|
||||
mock_values["resources"] = {"gpus": {"use_all_gpus": True, "kfd_device_exists": True}}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["devices"] == ["/dev/dri:/dev/dri", "/dev/kfd:/dev/kfd"]
|
||||
assert output["services"]["test_container"]["group_add"] == [44, 107, 568]
|
||||
|
||||
|
||||
def test_remove_gpu_devices(mock_values):
|
||||
mock_values["resources"] = {"gpus": {"use_all_gpus": True}}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.devices.remove_devices()
|
||||
output = render.render()
|
||||
assert "devices" not in output["services"]["test_container"]
|
||||
assert output["services"]["test_container"]["group_add"] == [568]
|
||||
|
||||
|
||||
def test_add_usb_bus(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.devices.add_usb_bus()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["devices"] == ["/dev/bus/usb:/dev/bus/usb"]
|
||||
|
||||
|
||||
def test_add_usb_bus_disallowed(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.devices.add_device("/dev/bus/usb", "/dev/bus/usb")
|
||||
@@ -1,666 +0,0 @@
|
||||
import pytest
|
||||
|
||||
|
||||
from render import Render
|
||||
from formatter import get_hashed_name_for_volume
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_values():
|
||||
return {
|
||||
"images": {
|
||||
"test_image": {
|
||||
"repository": "nginx",
|
||||
"tag": "latest",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_add_volume_invalid_type(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", {"type": "invalid_type"})
|
||||
|
||||
|
||||
def test_add_volume_empty_mount_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("", {"type": "tmpfs"})
|
||||
|
||||
|
||||
def test_add_volume_duplicate_mount_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_storage("/some/path", {"type": "tmpfs"})
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", {"type": "tmpfs"})
|
||||
|
||||
|
||||
def test_add_volume_host_path_invalid_propagation(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {
|
||||
"type": "host_path",
|
||||
"host_path_config": {"path": "/mnt/test", "propagation": "invalid_propagation"},
|
||||
}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
|
||||
|
||||
def test_add_host_path_volume_no_host_path_config(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path"}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
|
||||
|
||||
def test_add_host_path_volume_no_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "host_path_config": {"path": ""}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
|
||||
|
||||
def test_add_host_path_with_acl_no_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "host_path_config": {"acl_enable": True, "acl": {"path": ""}}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
|
||||
|
||||
def test_add_host_path_volume_mount(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test"}}
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_host_path_volume_mount_with_acl(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {
|
||||
"type": "host_path",
|
||||
"host_path_config": {"path": "/mnt/test", "acl_enable": True, "acl": {"path": "/mnt/test/acl"}},
|
||||
}
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test/acl",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_host_path_volume_mount_with_propagation(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "propagation": "slave"}}
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": False, "propagation": "slave"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_host_path_volume_mount_with_create_host_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "create_host_path": True}}
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": True, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_host_path_volume_mount_with_read_only(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "read_only": True, "host_path_config": {"path": "/mnt/test"}}
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test",
|
||||
"target": "/some/path",
|
||||
"read_only": True,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_ix_volume_invalid_dataset_name(mock_values):
|
||||
mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
ix_volume_config = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "invalid_dataset"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", ix_volume_config)
|
||||
|
||||
|
||||
def test_add_ix_volume_no_ix_volume_config(mock_values):
|
||||
mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
ix_volume_config = {"type": "ix_volume"}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", ix_volume_config)
|
||||
|
||||
|
||||
def test_add_ix_volume_volume_mount(mock_values):
|
||||
mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
ix_volume_config = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset"}}
|
||||
c1.add_storage("/some/path", ix_volume_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_ix_volume_volume_mount_with_options(mock_values):
|
||||
mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
ix_volume_config = {
|
||||
"type": "ix_volume",
|
||||
"ix_volume_config": {"dataset_name": "test_dataset", "propagation": "rslave", "create_host_path": True},
|
||||
}
|
||||
c1.add_storage("/some/path", ix_volume_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": True, "propagation": "rslave"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_cifs_volume_missing_server(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {"type": "cifs", "cifs_config": {"path": "/path", "username": "user", "password": "password"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_missing_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "username": "user", "password": "password"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_missing_username(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "path": "/path", "password": "password"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_missing_password(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "path": "/path", "username": "user"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_without_cifs_config(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {"type": "cifs"}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_duplicate_option(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {
|
||||
"type": "cifs",
|
||||
"cifs_config": {
|
||||
"server": "server",
|
||||
"path": "/path",
|
||||
"username": "user",
|
||||
"password": "pas$word",
|
||||
"options": ["verbose=true", "verbose=true"],
|
||||
},
|
||||
}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_disallowed_option(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {
|
||||
"type": "cifs",
|
||||
"cifs_config": {
|
||||
"server": "server",
|
||||
"path": "/path",
|
||||
"username": "user",
|
||||
"password": "pas$word",
|
||||
"options": ["user=username"],
|
||||
},
|
||||
}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_invalid_options(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {
|
||||
"type": "cifs",
|
||||
"cifs_config": {
|
||||
"server": "server",
|
||||
"path": "/path",
|
||||
"username": "user",
|
||||
"password": "pas$word",
|
||||
"options": {"verbose": True},
|
||||
},
|
||||
}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_invalid_options2(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {
|
||||
"type": "cifs",
|
||||
"cifs_config": {
|
||||
"server": "server",
|
||||
"path": "/path",
|
||||
"username": "user",
|
||||
"password": "pas$word",
|
||||
"options": [{"verbose": True}],
|
||||
},
|
||||
}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_add_cifs_volume(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_inner_config = {"server": "server", "path": "/path", "username": "user", "password": "pas$word"}
|
||||
cifs_config = {"type": "cifs", "cifs_config": cifs_inner_config}
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
output = render.render()
|
||||
vol_name = get_hashed_name_for_volume("cifs", cifs_inner_config)
|
||||
assert output["volumes"] == {
|
||||
vol_name: {
|
||||
"driver_opts": {"type": "cifs", "device": "//server/path", "o": "noperm,password=pas$$word,user=user"}
|
||||
}
|
||||
}
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
|
||||
]
|
||||
|
||||
|
||||
def test_cifs_volume_with_options(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_inner_config = {
|
||||
"server": "server",
|
||||
"path": "/path",
|
||||
"username": "user",
|
||||
"password": "pas$word",
|
||||
"options": ["vers=3.0", "verbose=true"],
|
||||
}
|
||||
cifs_config = {"type": "cifs", "cifs_config": cifs_inner_config}
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
output = render.render()
|
||||
vol_name = get_hashed_name_for_volume("cifs", cifs_inner_config)
|
||||
assert output["volumes"] == {
|
||||
vol_name: {
|
||||
"driver_opts": {
|
||||
"type": "cifs",
|
||||
"device": "//server/path",
|
||||
"o": "noperm,password=pas$$word,user=user,verbose=true,vers=3.0",
|
||||
}
|
||||
}
|
||||
}
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
|
||||
]
|
||||
|
||||
|
||||
def test_nfs_volume_missing_server(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {"type": "nfs", "nfs_config": {"path": "/path"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_nfs_volume_missing_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {"type": "nfs", "nfs_config": {"server": "server"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_nfs_volume_without_nfs_config(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {"type": "nfs"}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_nfs_volume_duplicate_option(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {
|
||||
"type": "nfs",
|
||||
"nfs_config": {"server": "server", "path": "/path", "options": ["verbose=true", "verbose=true"]},
|
||||
}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_nfs_volume_disallowed_option(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": ["addr=server"]}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_nfs_volume_invalid_options(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": {"verbose": True}}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_nfs_volume_invalid_options2(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": [{"verbose": True}]}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_add_nfs_volume(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_inner_config = {"server": "server", "path": "/path"}
|
||||
nfs_config = {"type": "nfs", "nfs_config": nfs_inner_config}
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
output = render.render()
|
||||
vol_name = get_hashed_name_for_volume("nfs", nfs_inner_config)
|
||||
assert output["volumes"] == {vol_name: {"driver_opts": {"type": "nfs", "device": ":/path", "o": "addr=server"}}}
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
|
||||
]
|
||||
|
||||
|
||||
def test_nfs_volume_with_options(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_inner_config = {"server": "server", "path": "/path", "options": ["vers=3.0", "verbose=true"]}
|
||||
nfs_config = {"type": "nfs", "nfs_config": nfs_inner_config}
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
output = render.render()
|
||||
vol_name = get_hashed_name_for_volume("nfs", nfs_inner_config)
|
||||
assert output["volumes"] == {
|
||||
vol_name: {
|
||||
"driver_opts": {
|
||||
"type": "nfs",
|
||||
"device": ":/path",
|
||||
"o": "addr=server,verbose=true,vers=3.0",
|
||||
}
|
||||
}
|
||||
}
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
|
||||
]
|
||||
|
||||
|
||||
def test_tmpfs_invalid_size(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "tmpfs", "tmpfs_config": {"size": "2"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
|
||||
|
||||
def test_tmpfs_zero_size(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "tmpfs", "tmpfs_config": {"size": 0}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
|
||||
|
||||
def test_tmpfs_invalid_mode(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "tmpfs", "tmpfs_config": {"mode": "invalid"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
|
||||
|
||||
def test_tmpfs_volume(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "tmpfs"}
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "tmpfs",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_temporary_volume(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "temporary", "volume_config": {"volume_name": "test_temp_volume"}}
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"source": "test_temp_volume",
|
||||
"type": "volume",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"volume": {"nocopy": False},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_docker_volume_missing_config(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "volume", "volume_config": {}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
|
||||
|
||||
def test_docker_volume_missing_volume_name(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "volume", "volume_config": {"volume_name": ""}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
|
||||
|
||||
def test_docker_volume(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "volume", "volume_config": {"volume_name": "test_volume"}}
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "volume",
|
||||
"source": "test_volume",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"volume": {"nocopy": False},
|
||||
}
|
||||
]
|
||||
assert output["volumes"] == {"test_volume": {}}
|
||||
|
||||
|
||||
def test_anonymous_volume(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "anonymous", "volume_config": {"nocopy": True}}
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{"type": "volume", "target": "/some/path", "read_only": False, "volume": {"nocopy": True}}
|
||||
]
|
||||
assert "volumes" not in output
|
||||
|
||||
|
||||
def test_add_docker_socket(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.storage._add_docker_socket(mount_path="/var/run/docker.sock")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/var/run/docker.sock",
|
||||
"target": "/var/run/docker.sock",
|
||||
"read_only": True,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_docker_socket_not_read_only(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.storage._add_docker_socket(read_only=False, mount_path="/var/run/docker.sock")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/var/run/docker.sock",
|
||||
"target": "/var/run/docker.sock",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_docker_socket_mount_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.storage._add_docker_socket(mount_path="/some/path")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/var/run/docker.sock",
|
||||
"target": "/some/path",
|
||||
"read_only": True,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
@@ -1,234 +0,0 @@
|
||||
import re
|
||||
import ipaddress
|
||||
|
||||
try:
|
||||
from .error import RenderError
|
||||
except ImportError:
|
||||
from error import RenderError
|
||||
|
||||
OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$")
|
||||
|
||||
|
||||
def valid_pull_policy_or_raise(pull_policy: str):
|
||||
valid_policies = ("missing", "always", "never", "build")
|
||||
if pull_policy not in valid_policies:
|
||||
raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]")
|
||||
return pull_policy
|
||||
|
||||
|
||||
def valid_sysctl_or_raise(sysctl: str, host_network: bool):
|
||||
if not sysctl:
|
||||
raise RenderError("Sysctl cannot be empty")
|
||||
if host_network and sysctl.startswith("net."):
|
||||
raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled")
|
||||
|
||||
valid_sysctls = [
|
||||
"kernel.msgmax",
|
||||
"kernel.msgmnb",
|
||||
"kernel.msgmni",
|
||||
"kernel.sem",
|
||||
"kernel.shmall",
|
||||
"kernel.shmmax",
|
||||
"kernel.shmmni",
|
||||
"kernel.shm_rmid_forced",
|
||||
]
|
||||
# https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls
|
||||
if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls:
|
||||
raise RenderError(
|
||||
f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]"
|
||||
)
|
||||
return sysctl
|
||||
|
||||
|
||||
def valid_redis_password_or_raise(password: str):
|
||||
forbidden_chars = [" ", "'"]
|
||||
for char in forbidden_chars:
|
||||
if char in password:
|
||||
raise RenderError(f"Redis password cannot contain [{char}]")
|
||||
|
||||
|
||||
def valid_octal_mode_or_raise(mode: str):
|
||||
mode = str(mode)
|
||||
if not OCTAL_MODE_REGEX.match(mode):
|
||||
raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]")
|
||||
return mode
|
||||
|
||||
|
||||
def valid_host_path_propagation(propagation: str):
|
||||
valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate")
|
||||
if propagation not in valid_propagations:
|
||||
raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]")
|
||||
return propagation
|
||||
|
||||
|
||||
def valid_portal_scheme_or_raise(scheme: str):
|
||||
schemes = ("http", "https")
|
||||
if scheme not in schemes:
|
||||
raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]")
|
||||
return scheme
|
||||
|
||||
|
||||
def valid_port_or_raise(port: int):
|
||||
if port < 1 or port > 65535:
|
||||
raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535")
|
||||
return port
|
||||
|
||||
|
||||
def valid_ip_or_raise(ip: str):
|
||||
try:
|
||||
ipaddress.ip_address(ip)
|
||||
except ValueError:
|
||||
raise RenderError(f"Invalid IP address [{ip}]")
|
||||
return ip
|
||||
|
||||
|
||||
def valid_port_mode_or_raise(mode: str):
|
||||
modes = ("ingress", "host")
|
||||
if mode not in modes:
|
||||
raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]")
|
||||
return mode
|
||||
|
||||
|
||||
def valid_port_protocol_or_raise(protocol: str):
|
||||
protocols = ("tcp", "udp")
|
||||
if protocol not in protocols:
|
||||
raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]")
|
||||
return protocol
|
||||
|
||||
|
||||
def valid_depend_condition_or_raise(condition: str):
|
||||
valid_conditions = ("service_started", "service_healthy", "service_completed_successfully")
|
||||
if condition not in valid_conditions:
|
||||
raise RenderError(
|
||||
f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]"
|
||||
)
|
||||
return condition
|
||||
|
||||
|
||||
def valid_cgroup_perm_or_raise(cgroup_perm: str):
|
||||
valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "")
|
||||
if cgroup_perm not in valid_cgroup_perms:
|
||||
raise RenderError(
|
||||
f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]"
|
||||
)
|
||||
return cgroup_perm
|
||||
|
||||
|
||||
def allowed_dns_opt_or_raise(dns_opt: str):
|
||||
disallowed_dns_opts = []
|
||||
if dns_opt in disallowed_dns_opts:
|
||||
raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.")
|
||||
return dns_opt
|
||||
|
||||
|
||||
def valid_http_path_or_raise(path: str):
|
||||
path = _valid_path_or_raise(path)
|
||||
return path
|
||||
|
||||
|
||||
def valid_fs_path_or_raise(path: str):
|
||||
# There is no reason to allow / as a path,
|
||||
# either on host or in a container side.
|
||||
if path == "/":
|
||||
raise RenderError(f"Path [{path}] cannot be [/]")
|
||||
path = _valid_path_or_raise(path)
|
||||
return path
|
||||
|
||||
|
||||
def _valid_path_or_raise(path: str):
|
||||
if path == "":
|
||||
raise RenderError(f"Path [{path}] cannot be empty")
|
||||
if not path.startswith("/"):
|
||||
raise RenderError(f"Path [{path}] must start with /")
|
||||
if "//" in path:
|
||||
raise RenderError(f"Path [{path}] cannot contain [//]")
|
||||
return path
|
||||
|
||||
|
||||
def allowed_device_or_raise(path: str):
|
||||
disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd"]
|
||||
if path in disallowed_devices:
|
||||
raise RenderError(f"Device [{path}] is not allowed to be manually added.")
|
||||
return path
|
||||
|
||||
|
||||
def valid_network_mode_or_raise(mode: str, containers: list[str]):
|
||||
valid_modes = ("host", "none")
|
||||
if mode in valid_modes:
|
||||
return mode
|
||||
|
||||
if mode.startswith("service:"):
|
||||
if mode[8:] not in containers:
|
||||
raise RenderError(f"Service [{mode[8:]}] not found")
|
||||
return mode
|
||||
|
||||
raise RenderError(
|
||||
f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:<name>]"
|
||||
)
|
||||
|
||||
|
||||
def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0):
|
||||
valid_restart_policies = ("always", "on-failure", "unless-stopped", "no")
|
||||
if policy not in valid_restart_policies:
|
||||
raise RenderError(
|
||||
f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]"
|
||||
)
|
||||
if policy != "on-failure" and maximum_retry_count != 0:
|
||||
raise RenderError("Maximum retry count can only be set for [on-failure] restart policy")
|
||||
|
||||
if maximum_retry_count < 0:
|
||||
raise RenderError("Maximum retry count must be a positive integer")
|
||||
|
||||
return policy
|
||||
|
||||
|
||||
def valid_cap_or_raise(cap: str):
|
||||
valid_policies = (
|
||||
"ALL",
|
||||
"AUDIT_CONTROL",
|
||||
"AUDIT_READ",
|
||||
"AUDIT_WRITE",
|
||||
"BLOCK_SUSPEND",
|
||||
"BPF",
|
||||
"CHECKPOINT_RESTORE",
|
||||
"CHOWN",
|
||||
"DAC_OVERRIDE",
|
||||
"DAC_READ_SEARCH",
|
||||
"FOWNER",
|
||||
"FSETID",
|
||||
"IPC_LOCK",
|
||||
"IPC_OWNER",
|
||||
"KILL",
|
||||
"LEASE",
|
||||
"LINUX_IMMUTABLE",
|
||||
"MAC_ADMIN",
|
||||
"MAC_OVERRIDE",
|
||||
"MKNOD",
|
||||
"NET_ADMIN",
|
||||
"NET_BIND_SERVICE",
|
||||
"NET_BROADCAST",
|
||||
"NET_RAW",
|
||||
"PERFMON",
|
||||
"SETFCAP",
|
||||
"SETGID",
|
||||
"SETPCAP",
|
||||
"SETUID",
|
||||
"SYS_ADMIN",
|
||||
"SYS_BOOT",
|
||||
"SYS_CHROOT",
|
||||
"SYS_MODULE",
|
||||
"SYS_NICE",
|
||||
"SYS_PACCT",
|
||||
"SYS_PTRACE",
|
||||
"SYS_RAWIO",
|
||||
"SYS_RESOURCE",
|
||||
"SYS_TIME",
|
||||
"SYS_TTY_CONFIG",
|
||||
"SYSLOG",
|
||||
"WAKE_ALARM",
|
||||
)
|
||||
|
||||
if cap not in valid_policies:
|
||||
raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]")
|
||||
|
||||
return cap
|
||||
@@ -1,106 +0,0 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from render import Render
|
||||
from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig
|
||||
|
||||
try:
|
||||
from .error import RenderError
|
||||
from .formatter import get_hashed_name_for_volume
|
||||
from .validations import valid_fs_path_or_raise
|
||||
except ImportError:
|
||||
from error import RenderError
|
||||
from formatter import get_hashed_name_for_volume
|
||||
from validations import valid_fs_path_or_raise
|
||||
|
||||
|
||||
class HostPathSource:
|
||||
def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"):
|
||||
self._render_instance = render_instance
|
||||
self.source: str = ""
|
||||
|
||||
if not config:
|
||||
raise RenderError("Expected [host_path_config] to be set for [host_path] type.")
|
||||
|
||||
path = ""
|
||||
if config.get("acl_enable", False):
|
||||
acl_path = config.get("acl", {}).get("path")
|
||||
if not acl_path:
|
||||
raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.")
|
||||
path = valid_fs_path_or_raise(acl_path)
|
||||
else:
|
||||
path = valid_fs_path_or_raise(config.get("path", ""))
|
||||
|
||||
self.source = path.rstrip("/")
|
||||
|
||||
def get(self):
|
||||
return self.source
|
||||
|
||||
|
||||
class IxVolumeSource:
|
||||
def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"):
|
||||
self._render_instance = render_instance
|
||||
self.source: str = ""
|
||||
|
||||
if not config:
|
||||
raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.")
|
||||
dataset_name = config.get("dataset_name")
|
||||
if not dataset_name:
|
||||
raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.")
|
||||
|
||||
ix_volumes = self._render_instance.values.get("ix_volumes", {})
|
||||
if dataset_name not in ix_volumes:
|
||||
available = ", ".join(ix_volumes.keys())
|
||||
raise RenderError(
|
||||
f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. "
|
||||
f"Available keys: [{available}]."
|
||||
)
|
||||
|
||||
self.source = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/"))
|
||||
|
||||
def get(self):
|
||||
return self.source
|
||||
|
||||
|
||||
class CifsSource:
|
||||
def __init__(self, render_instance: "Render", config: dict):
|
||||
self._render_instance = render_instance
|
||||
self.source: str = ""
|
||||
|
||||
if not config:
|
||||
raise RenderError("Expected [cifs_config] to be set for [cifs] type.")
|
||||
self.source = get_hashed_name_for_volume("cifs", config)
|
||||
|
||||
def get(self):
|
||||
return self.source
|
||||
|
||||
|
||||
class NfsSource:
|
||||
def __init__(self, render_instance: "Render", config: dict):
|
||||
self._render_instance = render_instance
|
||||
self.source: str = ""
|
||||
|
||||
if not config:
|
||||
raise RenderError("Expected [nfs_config] to be set for [nfs] type.")
|
||||
self.source = get_hashed_name_for_volume("nfs", config)
|
||||
|
||||
def get(self):
|
||||
return self.source
|
||||
|
||||
|
||||
class VolumeSource:
|
||||
def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"):
|
||||
self._render_instance = render_instance
|
||||
self.source: str = ""
|
||||
|
||||
if not config:
|
||||
raise RenderError("Expected [volume_config] to be set for [volume] type.")
|
||||
|
||||
volume_name: str = config.get("volume_name", "")
|
||||
if not volume_name:
|
||||
raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.")
|
||||
|
||||
self.source = volume_name
|
||||
|
||||
def get(self):
|
||||
return self.source
|
||||
@@ -0,0 +1,126 @@
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
from pathlib import Path
|
||||
from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN
|
||||
|
||||
|
||||
def mock_resolve(self):
|
||||
# Don't modify paths that are from RESTRICTED list initialization
|
||||
if str(self) in [str(p) for p in RESTRICTED]:
|
||||
return self
|
||||
|
||||
# For symlinks that point to restricted paths, return the target path
|
||||
# without stripping /private/
|
||||
if str(self).endswith("symlink_restricted"):
|
||||
return Path("/home") # Return the actual restricted target
|
||||
|
||||
# For other paths, strip /private/ if present
|
||||
return Path(str(self).removeprefix("/private/"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_path, expected",
|
||||
[
|
||||
# Non-restricted path (should be valid)
|
||||
("/tmp/somefile", True),
|
||||
# Exactly /mnt (restricted_in)
|
||||
("/mnt", False),
|
||||
# Exactly / (restricted_in)
|
||||
("/", False),
|
||||
# Subdirectory inside /mnt/.ix-apps (restricted)
|
||||
("/mnt/.ix-apps/something", False),
|
||||
# A path that is a restricted directory exactly
|
||||
("/home", False),
|
||||
("/var/log", False),
|
||||
("/mnt/.ix-apps", False),
|
||||
("/data", False),
|
||||
# Subdirectory inside e.g. /data
|
||||
("/data/subdir", False),
|
||||
# Not an obviously restricted path
|
||||
("/usr/local/share", True),
|
||||
# Another system path likely not in restricted list
|
||||
("/opt/myapp", True),
|
||||
],
|
||||
)
|
||||
@patch.object(Path, "resolve", mock_resolve)
|
||||
def test_is_allowed_path_direct(test_path, expected):
|
||||
"""Test direct paths against the is_allowed_path function."""
|
||||
assert is_allowed_path(test_path) == expected
|
||||
|
||||
|
||||
@patch.object(Path, "resolve", mock_resolve)
|
||||
def test_is_allowed_path_symlink(tmp_path):
|
||||
"""
|
||||
Test that a symlink pointing to a restricted directory is detected as invalid,
|
||||
and a symlink pointing to an allowed directory is valid.
|
||||
"""
|
||||
# Create a real (allowed) directory and a restricted directory in a temp location
|
||||
allowed_dir = tmp_path / "allowed_dir"
|
||||
allowed_dir.mkdir()
|
||||
|
||||
restricted_dir = tmp_path / "restricted_dir"
|
||||
restricted_dir.mkdir()
|
||||
|
||||
# We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log"
|
||||
# or we create a subdir to match the restricted pattern.
|
||||
# For demonstration, let's just patch it to a path in the restricted list.
|
||||
real_restricted_path = Path("/home") # This is one of the restricted directories
|
||||
|
||||
# Create symlinks to test
|
||||
symlink_allowed = tmp_path / "symlink_allowed"
|
||||
symlink_restricted = tmp_path / "symlink_restricted"
|
||||
|
||||
# Point the symlinks
|
||||
symlink_allowed.symlink_to(allowed_dir)
|
||||
symlink_restricted.symlink_to(real_restricted_path)
|
||||
|
||||
assert is_allowed_path(str(symlink_allowed)) is True
|
||||
assert is_allowed_path(str(symlink_restricted)) is False
|
||||
|
||||
|
||||
def test_is_allowed_path_nested_symlink(tmp_path):
|
||||
"""
|
||||
Test that even a nested symlink that eventually resolves into restricted
|
||||
directories is seen as invalid.
|
||||
"""
|
||||
# e.g., Create 2 symlinks that chain to /root
|
||||
link1 = tmp_path / "link1"
|
||||
link2 = tmp_path / "link2"
|
||||
|
||||
# link2 -> /root
|
||||
link2.symlink_to(Path("/root"))
|
||||
# link1 -> link2
|
||||
link1.symlink_to(link2)
|
||||
|
||||
assert is_allowed_path(str(link1)) is False
|
||||
|
||||
|
||||
def test_is_allowed_path_nonexistent(tmp_path):
|
||||
"""
|
||||
Test a path that does not exist at all. The code calls .resolve() which will
|
||||
give the absolute path, but if it's not restricted, it should still be valid.
|
||||
"""
|
||||
nonexistent = tmp_path / "this_does_not_exist"
|
||||
assert is_allowed_path(str(nonexistent)) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_path",
|
||||
list(RESTRICTED),
|
||||
)
|
||||
@patch.object(Path, "resolve", mock_resolve)
|
||||
def test_is_allowed_path_restricted_list(test_path):
|
||||
"""Test that all items in the RESTRICTED list are invalid."""
|
||||
assert is_allowed_path(test_path) is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_path",
|
||||
list(RESTRICTED_IN),
|
||||
)
|
||||
def test_is_allowed_path_restricted_in_list(test_path):
|
||||
"""
|
||||
Test that items in RESTRICTED_IN are invalid.
|
||||
"""
|
||||
assert is_allowed_path(test_path) is False
|
||||
@@ -0,0 +1,693 @@
|
||||
import pytest
|
||||
|
||||
|
||||
from render import Render
|
||||
from formatter import get_hashed_name_for_volume
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_values():
|
||||
return {
|
||||
"images": {
|
||||
"test_image": {
|
||||
"repository": "nginx",
|
||||
"tag": "latest",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_add_volume_invalid_type(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", {"type": "invalid_type"})
|
||||
|
||||
|
||||
def test_add_volume_empty_mount_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("", {"type": "tmpfs"})
|
||||
|
||||
|
||||
def test_add_volume_duplicate_mount_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_storage("/some/path", {"type": "tmpfs"})
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", {"type": "tmpfs"})
|
||||
|
||||
|
||||
def test_add_volume_host_path_invalid_propagation(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {
|
||||
"type": "host_path",
|
||||
"host_path_config": {"path": "/mnt/test", "propagation": "invalid_propagation"},
|
||||
}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
|
||||
|
||||
def test_add_host_path_volume_no_host_path_config(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path"}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
|
||||
|
||||
def test_add_host_path_volume_no_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "host_path_config": {"path": ""}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
|
||||
|
||||
def test_add_host_path_with_acl_no_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "host_path_config": {"acl_enable": True, "acl": {"path": ""}}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
|
||||
|
||||
def test_add_host_path_volume_mount(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test"}}
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_host_path_volume_mount_with_acl(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {
|
||||
"type": "host_path",
|
||||
"host_path_config": {"path": "/mnt/test", "acl_enable": True, "acl": {"path": "/mnt/test/acl"}},
|
||||
}
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test/acl",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_host_path_volume_mount_with_propagation(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "propagation": "slave"}}
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": False, "propagation": "slave"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_host_path_volume_mount_with_create_host_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "create_host_path": True}}
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": True, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_host_path_volume_mount_with_read_only(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "read_only": True, "host_path_config": {"path": "/mnt/test"}}
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test",
|
||||
"target": "/some/path",
|
||||
"read_only": True,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_ix_volume_invalid_dataset_name(mock_values):
|
||||
mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
ix_volume_config = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "invalid_dataset"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", ix_volume_config)
|
||||
|
||||
|
||||
def test_add_ix_volume_no_ix_volume_config(mock_values):
|
||||
mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
ix_volume_config = {"type": "ix_volume"}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", ix_volume_config)
|
||||
|
||||
|
||||
def test_add_ix_volume_volume_mount(mock_values):
|
||||
mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
ix_volume_config = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset"}}
|
||||
c1.add_storage("/some/path", ix_volume_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_ix_volume_volume_mount_with_options(mock_values):
|
||||
mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
ix_volume_config = {
|
||||
"type": "ix_volume",
|
||||
"ix_volume_config": {"dataset_name": "test_dataset", "propagation": "rslave", "create_host_path": True},
|
||||
}
|
||||
c1.add_storage("/some/path", ix_volume_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": True, "propagation": "rslave"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_cifs_volume_missing_server(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {"type": "cifs", "cifs_config": {"path": "/path", "username": "user", "password": "password"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_missing_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "username": "user", "password": "password"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_missing_username(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "path": "/path", "password": "password"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_missing_password(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "path": "/path", "username": "user"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_without_cifs_config(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {"type": "cifs"}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_duplicate_option(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {
|
||||
"type": "cifs",
|
||||
"cifs_config": {
|
||||
"server": "server",
|
||||
"path": "/path",
|
||||
"username": "user",
|
||||
"password": "pas$word",
|
||||
"options": ["verbose=true", "verbose=true"],
|
||||
},
|
||||
}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_disallowed_option(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {
|
||||
"type": "cifs",
|
||||
"cifs_config": {
|
||||
"server": "server",
|
||||
"path": "/path",
|
||||
"username": "user",
|
||||
"password": "pas$word",
|
||||
"options": ["user=username"],
|
||||
},
|
||||
}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_invalid_options(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {
|
||||
"type": "cifs",
|
||||
"cifs_config": {
|
||||
"server": "server",
|
||||
"path": "/path",
|
||||
"username": "user",
|
||||
"password": "pas$word",
|
||||
"options": {"verbose": True},
|
||||
},
|
||||
}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_invalid_options2(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {
|
||||
"type": "cifs",
|
||||
"cifs_config": {
|
||||
"server": "server",
|
||||
"path": "/path",
|
||||
"username": "user",
|
||||
"password": "pas$word",
|
||||
"options": [{"verbose": True}],
|
||||
},
|
||||
}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_add_cifs_volume(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_inner_config = {"server": "server", "path": "/path", "username": "user", "password": "pas$word"}
|
||||
cifs_config = {"type": "cifs", "cifs_config": cifs_inner_config}
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
output = render.render()
|
||||
vol_name = get_hashed_name_for_volume("cifs", cifs_inner_config)
|
||||
assert output["volumes"] == {
|
||||
vol_name: {
|
||||
"driver_opts": {"type": "cifs", "device": "//server/path", "o": "noperm,password=pas$$word,user=user"}
|
||||
}
|
||||
}
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
|
||||
]
|
||||
|
||||
|
||||
def test_cifs_volume_with_options(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_inner_config = {
|
||||
"server": "server",
|
||||
"path": "/path",
|
||||
"username": "user",
|
||||
"password": "pas$word",
|
||||
"options": ["vers=3.0", "verbose=true"],
|
||||
}
|
||||
cifs_config = {"type": "cifs", "cifs_config": cifs_inner_config}
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
output = render.render()
|
||||
vol_name = get_hashed_name_for_volume("cifs", cifs_inner_config)
|
||||
assert output["volumes"] == {
|
||||
vol_name: {
|
||||
"driver_opts": {
|
||||
"type": "cifs",
|
||||
"device": "//server/path",
|
||||
"o": "noperm,password=pas$$word,user=user,verbose=true,vers=3.0",
|
||||
}
|
||||
}
|
||||
}
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
|
||||
]
|
||||
|
||||
|
||||
def test_nfs_volume_missing_server(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {"type": "nfs", "nfs_config": {"path": "/path"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_nfs_volume_missing_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {"type": "nfs", "nfs_config": {"server": "server"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_nfs_volume_without_nfs_config(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {"type": "nfs"}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_nfs_volume_duplicate_option(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {
|
||||
"type": "nfs",
|
||||
"nfs_config": {"server": "server", "path": "/path", "options": ["verbose=true", "verbose=true"]},
|
||||
}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_nfs_volume_disallowed_option(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": ["addr=server"]}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_nfs_volume_invalid_options(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": {"verbose": True}}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_nfs_volume_invalid_options2(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": [{"verbose": True}]}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_add_nfs_volume(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_inner_config = {"server": "server", "path": "/path"}
|
||||
nfs_config = {"type": "nfs", "nfs_config": nfs_inner_config}
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
output = render.render()
|
||||
vol_name = get_hashed_name_for_volume("nfs", nfs_inner_config)
|
||||
assert output["volumes"] == {vol_name: {"driver_opts": {"type": "nfs", "device": ":/path", "o": "addr=server"}}}
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
|
||||
]
|
||||
|
||||
|
||||
def test_nfs_volume_with_options(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_inner_config = {"server": "server", "path": "/path", "options": ["vers=3.0", "verbose=true"]}
|
||||
nfs_config = {"type": "nfs", "nfs_config": nfs_inner_config}
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
output = render.render()
|
||||
vol_name = get_hashed_name_for_volume("nfs", nfs_inner_config)
|
||||
assert output["volumes"] == {
|
||||
vol_name: {
|
||||
"driver_opts": {
|
||||
"type": "nfs",
|
||||
"device": ":/path",
|
||||
"o": "addr=server,verbose=true,vers=3.0",
|
||||
}
|
||||
}
|
||||
}
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
|
||||
]
|
||||
|
||||
|
||||
def test_tmpfs_invalid_size(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "tmpfs", "tmpfs_config": {"size": "2"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
|
||||
|
||||
def test_tmpfs_zero_size(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "tmpfs", "tmpfs_config": {"size": 0}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
|
||||
|
||||
def test_tmpfs_invalid_mode(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "tmpfs", "tmpfs_config": {"mode": "invalid"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
|
||||
|
||||
def test_tmpfs_volume(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "tmpfs"}
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "tmpfs",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_temporary_volume(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "temporary", "volume_config": {"volume_name": "test_temp_volume"}}
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"source": "test_temp_volume",
|
||||
"type": "volume",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"volume": {"nocopy": False},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_docker_volume_missing_config(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "volume", "volume_config": {}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
|
||||
|
||||
def test_docker_volume_missing_volume_name(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "volume", "volume_config": {"volume_name": ""}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
|
||||
|
||||
def test_docker_volume(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "volume", "volume_config": {"volume_name": "test_volume"}}
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "volume",
|
||||
"source": "test_volume",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"volume": {"nocopy": False},
|
||||
}
|
||||
]
|
||||
assert output["volumes"] == {"test_volume": {}}
|
||||
|
||||
|
||||
def test_anonymous_volume(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "anonymous", "volume_config": {"nocopy": True}}
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{"type": "volume", "target": "/some/path", "read_only": False, "volume": {"nocopy": True}}
|
||||
]
|
||||
assert "volumes" not in output
|
||||
|
||||
|
||||
def test_add_docker_socket(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.storage._add_docker_socket(mount_path="/var/run/docker.sock")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/var/run/docker.sock",
|
||||
"target": "/var/run/docker.sock",
|
||||
"read_only": True,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_docker_socket_not_read_only(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.storage._add_docker_socket(read_only=False, mount_path="/var/run/docker.sock")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/var/run/docker.sock",
|
||||
"target": "/var/run/docker.sock",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_docker_socket_mount_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.storage._add_docker_socket(mount_path="/some/path")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/var/run/docker.sock",
|
||||
"target": "/some/path",
|
||||
"read_only": True,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_host_path_with_disallowed_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
|
||||
|
||||
def test_host_path_without_disallowed_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test"}}
|
||||
c1.add_storage("/mnt", host_path_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test",
|
||||
"target": "/mnt",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,271 @@
|
||||
import re
|
||||
import ipaddress
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
try:
|
||||
from .error import RenderError
|
||||
except ImportError:
|
||||
from error import RenderError
|
||||
|
||||
OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$")
|
||||
RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/"))
|
||||
RESTRICTED: tuple[Path, ...] = (
|
||||
Path("/mnt/.ix-apps"),
|
||||
Path("/data"),
|
||||
Path("/var/db"),
|
||||
Path("/root"),
|
||||
Path("/conf"),
|
||||
Path("/audit"),
|
||||
Path("/var/run/middleware"),
|
||||
Path("/home"),
|
||||
Path("/boot"),
|
||||
Path("/var/log"),
|
||||
)
|
||||
|
||||
|
||||
def valid_pull_policy_or_raise(pull_policy: str):
|
||||
valid_policies = ("missing", "always", "never", "build")
|
||||
if pull_policy not in valid_policies:
|
||||
raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]")
|
||||
return pull_policy
|
||||
|
||||
|
||||
def valid_sysctl_or_raise(sysctl: str, host_network: bool):
|
||||
if not sysctl:
|
||||
raise RenderError("Sysctl cannot be empty")
|
||||
if host_network and sysctl.startswith("net."):
|
||||
raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled")
|
||||
|
||||
valid_sysctls = [
|
||||
"kernel.msgmax",
|
||||
"kernel.msgmnb",
|
||||
"kernel.msgmni",
|
||||
"kernel.sem",
|
||||
"kernel.shmall",
|
||||
"kernel.shmmax",
|
||||
"kernel.shmmni",
|
||||
"kernel.shm_rmid_forced",
|
||||
]
|
||||
# https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls
|
||||
if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls:
|
||||
raise RenderError(
|
||||
f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]"
|
||||
)
|
||||
return sysctl
|
||||
|
||||
|
||||
def valid_redis_password_or_raise(password: str):
|
||||
forbidden_chars = [" ", "'"]
|
||||
for char in forbidden_chars:
|
||||
if char in password:
|
||||
raise RenderError(f"Redis password cannot contain [{char}]")
|
||||
|
||||
|
||||
def valid_octal_mode_or_raise(mode: str):
|
||||
mode = str(mode)
|
||||
if not OCTAL_MODE_REGEX.match(mode):
|
||||
raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]")
|
||||
return mode
|
||||
|
||||
|
||||
def valid_host_path_propagation(propagation: str):
|
||||
valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate")
|
||||
if propagation not in valid_propagations:
|
||||
raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]")
|
||||
return propagation
|
||||
|
||||
|
||||
def valid_portal_scheme_or_raise(scheme: str):
|
||||
schemes = ("http", "https")
|
||||
if scheme not in schemes:
|
||||
raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]")
|
||||
return scheme
|
||||
|
||||
|
||||
def valid_port_or_raise(port: int):
|
||||
if port < 1 or port > 65535:
|
||||
raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535")
|
||||
return port
|
||||
|
||||
|
||||
def valid_ip_or_raise(ip: str):
|
||||
try:
|
||||
ipaddress.ip_address(ip)
|
||||
except ValueError:
|
||||
raise RenderError(f"Invalid IP address [{ip}]")
|
||||
return ip
|
||||
|
||||
|
||||
def valid_port_mode_or_raise(mode: str):
|
||||
modes = ("ingress", "host")
|
||||
if mode not in modes:
|
||||
raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]")
|
||||
return mode
|
||||
|
||||
|
||||
def valid_port_protocol_or_raise(protocol: str):
|
||||
protocols = ("tcp", "udp")
|
||||
if protocol not in protocols:
|
||||
raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]")
|
||||
return protocol
|
||||
|
||||
|
||||
def valid_depend_condition_or_raise(condition: str):
|
||||
valid_conditions = ("service_started", "service_healthy", "service_completed_successfully")
|
||||
if condition not in valid_conditions:
|
||||
raise RenderError(
|
||||
f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]"
|
||||
)
|
||||
return condition
|
||||
|
||||
|
||||
def valid_cgroup_perm_or_raise(cgroup_perm: str):
|
||||
valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "")
|
||||
if cgroup_perm not in valid_cgroup_perms:
|
||||
raise RenderError(
|
||||
f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]"
|
||||
)
|
||||
return cgroup_perm
|
||||
|
||||
|
||||
def allowed_dns_opt_or_raise(dns_opt: str):
|
||||
disallowed_dns_opts = []
|
||||
if dns_opt in disallowed_dns_opts:
|
||||
raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.")
|
||||
return dns_opt
|
||||
|
||||
|
||||
def valid_http_path_or_raise(path: str):
|
||||
path = _valid_path_or_raise(path)
|
||||
return path
|
||||
|
||||
|
||||
def valid_fs_path_or_raise(path: str):
|
||||
# There is no reason to allow / as a path,
|
||||
# either on host or in a container side.
|
||||
if path == "/":
|
||||
raise RenderError(f"Path [{path}] cannot be [/]")
|
||||
path = _valid_path_or_raise(path)
|
||||
return path
|
||||
|
||||
|
||||
def is_allowed_path(input_path: str) -> bool:
|
||||
"""
|
||||
Validates that the given path (after resolving symlinks) is not
|
||||
one of the restricted paths or within those restricted directories.
|
||||
|
||||
Returns True if the path is allowed, False otherwise.
|
||||
"""
|
||||
# Resolve the path to avoid symlink bypasses
|
||||
real_path = Path(input_path).resolve()
|
||||
for restricted in RESTRICTED:
|
||||
if real_path.is_relative_to(restricted):
|
||||
return False
|
||||
|
||||
return real_path not in RESTRICTED_IN
|
||||
|
||||
|
||||
def allowed_fs_host_path_or_raise(path: str):
|
||||
if not is_allowed_path(path):
|
||||
raise RenderError(f"Path [{path}] is not allowed to be mounted.")
|
||||
return path
|
||||
|
||||
|
||||
def _valid_path_or_raise(path: str):
|
||||
if path == "":
|
||||
raise RenderError(f"Path [{path}] cannot be empty")
|
||||
if not path.startswith("/"):
|
||||
raise RenderError(f"Path [{path}] must start with /")
|
||||
if "//" in path:
|
||||
raise RenderError(f"Path [{path}] cannot contain [//]")
|
||||
return path
|
||||
|
||||
|
||||
def allowed_device_or_raise(path: str):
|
||||
disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"]
|
||||
if path in disallowed_devices:
|
||||
raise RenderError(f"Device [{path}] is not allowed to be manually added.")
|
||||
return path
|
||||
|
||||
|
||||
def valid_network_mode_or_raise(mode: str, containers: list[str]):
|
||||
valid_modes = ("host", "none")
|
||||
if mode in valid_modes:
|
||||
return mode
|
||||
|
||||
if mode.startswith("service:"):
|
||||
if mode[8:] not in containers:
|
||||
raise RenderError(f"Service [{mode[8:]}] not found")
|
||||
return mode
|
||||
|
||||
raise RenderError(
|
||||
f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:<name>]"
|
||||
)
|
||||
|
||||
|
||||
def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0):
|
||||
valid_restart_policies = ("always", "on-failure", "unless-stopped", "no")
|
||||
if policy not in valid_restart_policies:
|
||||
raise RenderError(
|
||||
f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]"
|
||||
)
|
||||
if policy != "on-failure" and maximum_retry_count != 0:
|
||||
raise RenderError("Maximum retry count can only be set for [on-failure] restart policy")
|
||||
|
||||
if maximum_retry_count < 0:
|
||||
raise RenderError("Maximum retry count must be a positive integer")
|
||||
|
||||
return policy
|
||||
|
||||
|
||||
def valid_cap_or_raise(cap: str):
|
||||
valid_policies = (
|
||||
"ALL",
|
||||
"AUDIT_CONTROL",
|
||||
"AUDIT_READ",
|
||||
"AUDIT_WRITE",
|
||||
"BLOCK_SUSPEND",
|
||||
"BPF",
|
||||
"CHECKPOINT_RESTORE",
|
||||
"CHOWN",
|
||||
"DAC_OVERRIDE",
|
||||
"DAC_READ_SEARCH",
|
||||
"FOWNER",
|
||||
"FSETID",
|
||||
"IPC_LOCK",
|
||||
"IPC_OWNER",
|
||||
"KILL",
|
||||
"LEASE",
|
||||
"LINUX_IMMUTABLE",
|
||||
"MAC_ADMIN",
|
||||
"MAC_OVERRIDE",
|
||||
"MKNOD",
|
||||
"NET_ADMIN",
|
||||
"NET_BIND_SERVICE",
|
||||
"NET_BROADCAST",
|
||||
"NET_RAW",
|
||||
"PERFMON",
|
||||
"SETFCAP",
|
||||
"SETGID",
|
||||
"SETPCAP",
|
||||
"SETUID",
|
||||
"SYS_ADMIN",
|
||||
"SYS_BOOT",
|
||||
"SYS_CHROOT",
|
||||
"SYS_MODULE",
|
||||
"SYS_NICE",
|
||||
"SYS_PACCT",
|
||||
"SYS_PTRACE",
|
||||
"SYS_RAWIO",
|
||||
"SYS_RESOURCE",
|
||||
"SYS_TIME",
|
||||
"SYS_TTY_CONFIG",
|
||||
"SYSLOG",
|
||||
"WAKE_ALARM",
|
||||
)
|
||||
|
||||
if cap not in valid_policies:
|
||||
raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]")
|
||||
|
||||
return cap
|
||||
@@ -0,0 +1,108 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from render import Render
|
||||
from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig
|
||||
|
||||
try:
|
||||
from .error import RenderError
|
||||
from .formatter import get_hashed_name_for_volume
|
||||
from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise
|
||||
except ImportError:
|
||||
from error import RenderError
|
||||
from formatter import get_hashed_name_for_volume
|
||||
from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise
|
||||
|
||||
|
||||
class HostPathSource:
|
||||
def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"):
|
||||
self._render_instance = render_instance
|
||||
self.source: str = ""
|
||||
|
||||
if not config:
|
||||
raise RenderError("Expected [host_path_config] to be set for [host_path] type.")
|
||||
|
||||
path = ""
|
||||
if config.get("acl_enable", False):
|
||||
acl_path = config.get("acl", {}).get("path")
|
||||
if not acl_path:
|
||||
raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.")
|
||||
path = valid_fs_path_or_raise(acl_path)
|
||||
else:
|
||||
path = valid_fs_path_or_raise(config.get("path", ""))
|
||||
|
||||
path = path.rstrip("/")
|
||||
self.source = allowed_fs_host_path_or_raise(path)
|
||||
|
||||
def get(self):
|
||||
return self.source
|
||||
|
||||
|
||||
class IxVolumeSource:
|
||||
def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"):
|
||||
self._render_instance = render_instance
|
||||
self.source: str = ""
|
||||
|
||||
if not config:
|
||||
raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.")
|
||||
dataset_name = config.get("dataset_name")
|
||||
if not dataset_name:
|
||||
raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.")
|
||||
|
||||
ix_volumes = self._render_instance.values.get("ix_volumes", {})
|
||||
if dataset_name not in ix_volumes:
|
||||
available = ", ".join(ix_volumes.keys())
|
||||
raise RenderError(
|
||||
f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. "
|
||||
f"Available keys: [{available}]."
|
||||
)
|
||||
|
||||
path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/"))
|
||||
self.source = allowed_fs_host_path_or_raise(path)
|
||||
|
||||
def get(self):
|
||||
return self.source
|
||||
|
||||
|
||||
class CifsSource:
|
||||
def __init__(self, render_instance: "Render", config: dict):
|
||||
self._render_instance = render_instance
|
||||
self.source: str = ""
|
||||
|
||||
if not config:
|
||||
raise RenderError("Expected [cifs_config] to be set for [cifs] type.")
|
||||
self.source = get_hashed_name_for_volume("cifs", config)
|
||||
|
||||
def get(self):
|
||||
return self.source
|
||||
|
||||
|
||||
class NfsSource:
|
||||
def __init__(self, render_instance: "Render", config: dict):
|
||||
self._render_instance = render_instance
|
||||
self.source: str = ""
|
||||
|
||||
if not config:
|
||||
raise RenderError("Expected [nfs_config] to be set for [nfs] type.")
|
||||
self.source = get_hashed_name_for_volume("nfs", config)
|
||||
|
||||
def get(self):
|
||||
return self.source
|
||||
|
||||
|
||||
class VolumeSource:
|
||||
def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"):
|
||||
self._render_instance = render_instance
|
||||
self.source: str = ""
|
||||
|
||||
if not config:
|
||||
raise RenderError("Expected [volume_config] to be set for [volume] type.")
|
||||
|
||||
volume_name: str = config.get("volume_name", "")
|
||||
if not volume_name:
|
||||
raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.")
|
||||
|
||||
self.source = volume_name
|
||||
|
||||
def get(self):
|
||||
return self.source
|
||||
@@ -20,8 +20,8 @@ icon: https://media.sys.truenas.net/apps/adguard-home/icons/icon.svg
|
||||
keywords:
|
||||
- dns
|
||||
- adblock
|
||||
lib_version: 2.1.2
|
||||
lib_version_hash: c302ec00f678308058addbb58715c569532162c1a80b35900b081444bf73addd
|
||||
lib_version: 2.1.5
|
||||
lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12
|
||||
maintainers:
|
||||
- email: dev@ixsystems.com
|
||||
name: truenas
|
||||
@@ -42,4 +42,4 @@ sources:
|
||||
- https://hub.docker.com/r/adguard/adguardhome
|
||||
title: AdGuard Home
|
||||
train: community
|
||||
version: 1.1.7
|
||||
version: 1.1.8
|
||||
|
||||
@@ -1,347 +0,0 @@
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from render import Render
|
||||
from storage import IxStorage
|
||||
|
||||
try:
|
||||
from .configs import ContainerConfigs
|
||||
from .depends import Depends
|
||||
from .deploy import Deploy
|
||||
from .devices import Devices
|
||||
from .dns import Dns
|
||||
from .environment import Environment
|
||||
from .error import RenderError
|
||||
from .formatter import escape_dollar, get_image_with_hashed_data
|
||||
from .healthcheck import Healthcheck
|
||||
from .labels import Labels
|
||||
from .ports import Ports
|
||||
from .restart import RestartPolicy
|
||||
from .validations import valid_network_mode_or_raise, valid_cap_or_raise, valid_pull_policy_or_raise
|
||||
from .storage import Storage
|
||||
from .sysctls import Sysctls
|
||||
except ImportError:
|
||||
from configs import ContainerConfigs
|
||||
from depends import Depends
|
||||
from deploy import Deploy
|
||||
from devices import Devices
|
||||
from dns import Dns
|
||||
from environment import Environment
|
||||
from error import RenderError
|
||||
from formatter import escape_dollar, get_image_with_hashed_data
|
||||
from healthcheck import Healthcheck
|
||||
from labels import Labels
|
||||
from ports import Ports
|
||||
from restart import RestartPolicy
|
||||
from validations import valid_network_mode_or_raise, valid_cap_or_raise, valid_pull_policy_or_raise
|
||||
from storage import Storage
|
||||
from sysctls import Sysctls
|
||||
|
||||
|
||||
class Container:
|
||||
def __init__(self, render_instance: "Render", name: str, image: str):
|
||||
self._render_instance = render_instance
|
||||
|
||||
self._name: str = name
|
||||
self._image: str = self._resolve_image(image)
|
||||
self._build_image: str = ""
|
||||
self._pull_policy: str = ""
|
||||
self._user: str = ""
|
||||
self._tty: bool = False
|
||||
self._stdin_open: bool = False
|
||||
self._init: bool | None = None
|
||||
self._read_only: bool | None = None
|
||||
self._hostname: str = ""
|
||||
self._cap_drop: set[str] = set(["ALL"]) # Drop all capabilities by default and add caps granularly
|
||||
self._cap_add: set[str] = set()
|
||||
self._security_opt: set[str] = set(["no-new-privileges"])
|
||||
self._privileged: bool = False
|
||||
self._group_add: set[int | str] = set()
|
||||
self._network_mode: str = ""
|
||||
self._entrypoint: list[str] = []
|
||||
self._command: list[str] = []
|
||||
self._grace_period: int | None = None
|
||||
self._shm_size: int | None = None
|
||||
self._storage: Storage = Storage(self._render_instance)
|
||||
self.sysctls: Sysctls = Sysctls(self._render_instance, self)
|
||||
self.configs: ContainerConfigs = ContainerConfigs(self._render_instance, self._render_instance.configs)
|
||||
self.deploy: Deploy = Deploy(self._render_instance)
|
||||
self.networks: set[str] = set()
|
||||
self.devices: Devices = Devices(self._render_instance)
|
||||
self.environment: Environment = Environment(self._render_instance, self.deploy.resources)
|
||||
self.dns: Dns = Dns(self._render_instance)
|
||||
self.depends: Depends = Depends(self._render_instance)
|
||||
self.healthcheck: Healthcheck = Healthcheck(self._render_instance)
|
||||
self.labels: Labels = Labels(self._render_instance)
|
||||
self.restart: RestartPolicy = RestartPolicy(self._render_instance)
|
||||
self.ports: Ports = Ports(self._render_instance)
|
||||
|
||||
self._auto_set_network_mode()
|
||||
self._auto_add_labels()
|
||||
self._auto_add_groups()
|
||||
|
||||
def _auto_add_groups(self):
|
||||
self.add_group(568)
|
||||
|
||||
def _auto_set_network_mode(self):
|
||||
if self._render_instance.values.get("network", {}).get("host_network", False):
|
||||
self.set_network_mode("host")
|
||||
|
||||
def _auto_add_labels(self):
|
||||
labels = self._render_instance.values.get("labels", [])
|
||||
if not labels:
|
||||
return
|
||||
|
||||
for label in labels:
|
||||
containers = label.get("containers", [])
|
||||
if not containers:
|
||||
raise RenderError(f'Label [{label.get("key", "")}] must have at least one container')
|
||||
|
||||
if self._name in containers:
|
||||
self.labels.add_label(label["key"], label["value"])
|
||||
|
||||
def _resolve_image(self, image: str):
|
||||
images = self._render_instance.values["images"]
|
||||
if image not in images:
|
||||
raise RenderError(
|
||||
f"Image [{image}] not found in values. " f"Available images: [{', '.join(images.keys())}]"
|
||||
)
|
||||
repo = images[image].get("repository", "")
|
||||
tag = images[image].get("tag", "")
|
||||
|
||||
if not repo:
|
||||
raise RenderError(f"Repository not found for image [{image}]")
|
||||
if not tag:
|
||||
raise RenderError(f"Tag not found for image [{image}]")
|
||||
|
||||
return f"{repo}:{tag}"
|
||||
|
||||
def build_image(self, content: list[str | None]):
|
||||
dockerfile = f"FROM {self._image}\n"
|
||||
for line in content:
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("FROM"):
|
||||
# TODO: This will also block multi-stage builds
|
||||
# We can revisit this later if we need it
|
||||
raise RenderError(
|
||||
"FROM cannot be used in build image. Define the base image when creating the container."
|
||||
)
|
||||
dockerfile += line + "\n"
|
||||
|
||||
self._build_image = dockerfile
|
||||
self._image = get_image_with_hashed_data(self._image, dockerfile)
|
||||
|
||||
def set_pull_policy(self, pull_policy: str):
|
||||
self._pull_policy = valid_pull_policy_or_raise(pull_policy)
|
||||
|
||||
def set_user(self, user: int, group: int):
|
||||
for i in (user, group):
|
||||
if not isinstance(i, int) or i < 0:
|
||||
raise RenderError(f"User/Group [{i}] is not valid")
|
||||
self._user = f"{user}:{group}"
|
||||
|
||||
def add_group(self, group: int | str):
|
||||
if isinstance(group, str):
|
||||
group = str(group).strip()
|
||||
if group.isdigit():
|
||||
raise RenderError(f"Group is a number [{group}] but passed as a string")
|
||||
|
||||
if group in self._group_add:
|
||||
raise RenderError(f"Group [{group}] already added")
|
||||
self._group_add.add(group)
|
||||
|
||||
def get_additional_groups(self) -> list[int | str]:
|
||||
result = []
|
||||
if self.deploy.resources.has_gpus() or self.devices.has_gpus():
|
||||
result.append(44) # video
|
||||
result.append(107) # render
|
||||
return result
|
||||
|
||||
def get_current_groups(self) -> list[str]:
|
||||
result = [str(g) for g in self._group_add]
|
||||
result.extend([str(g) for g in self.get_additional_groups()])
|
||||
return result
|
||||
|
||||
def set_tty(self, enabled: bool = False):
|
||||
self._tty = enabled
|
||||
|
||||
def set_stdin(self, enabled: bool = False):
|
||||
self._stdin_open = enabled
|
||||
|
||||
def set_init(self, enabled: bool = False):
|
||||
self._init = enabled
|
||||
|
||||
def set_read_only(self, enabled: bool = False):
|
||||
self._read_only = enabled
|
||||
|
||||
def set_hostname(self, hostname: str):
|
||||
self._hostname = hostname
|
||||
|
||||
def set_grace_period(self, grace_period: int):
|
||||
if grace_period < 0:
|
||||
raise RenderError(f"Grace period [{grace_period}] cannot be negative")
|
||||
self._grace_period = grace_period
|
||||
|
||||
def set_privileged(self, enabled: bool = False):
|
||||
self._privileged = enabled
|
||||
|
||||
def clear_caps(self):
|
||||
self._cap_add.clear()
|
||||
self._cap_drop.clear()
|
||||
|
||||
def add_caps(self, caps: list[str]):
|
||||
for c in caps:
|
||||
if c in self._cap_add:
|
||||
raise RenderError(f"Capability [{c}] already added")
|
||||
self._cap_add.add(valid_cap_or_raise(c))
|
||||
|
||||
def add_security_opt(self, opt: str):
|
||||
if opt in self._security_opt:
|
||||
raise RenderError(f"Security Option [{opt}] already added")
|
||||
self._security_opt.add(opt)
|
||||
|
||||
def remove_security_opt(self, opt: str):
|
||||
self._security_opt.remove(opt)
|
||||
|
||||
def set_network_mode(self, mode: str):
|
||||
self._network_mode = valid_network_mode_or_raise(mode, self._render_instance.container_names())
|
||||
|
||||
def set_entrypoint(self, entrypoint: list[str]):
|
||||
self._entrypoint = [escape_dollar(str(e)) for e in entrypoint]
|
||||
|
||||
def set_command(self, command: list[str]):
|
||||
self._command = [escape_dollar(str(e)) for e in command]
|
||||
|
||||
def add_storage(self, mount_path: str, config: "IxStorage"):
|
||||
self._storage.add(mount_path, config)
|
||||
|
||||
def add_docker_socket(self, read_only: bool = True, mount_path: str = "/var/run/docker.sock"):
|
||||
self.add_group(999)
|
||||
self._storage._add_docker_socket(read_only, mount_path)
|
||||
|
||||
def add_tun_device(self, read_only: bool = True, mount_path: str = "/dev/net/tun"):
|
||||
self._storage._add_tun_device(read_only, mount_path)
|
||||
|
||||
def add_snd_device(self):
|
||||
self.add_group(29)
|
||||
self.devices._add_snd_device()
|
||||
|
||||
def set_shm_size_mb(self, size: int):
|
||||
self._shm_size = size
|
||||
|
||||
# Easily remove devices from the container
|
||||
# Useful in dependencies like postgres and redis
|
||||
# where there is no need to pass devices to them
|
||||
def remove_devices(self):
|
||||
self.deploy.resources.remove_devices()
|
||||
self.devices.remove_devices()
|
||||
|
||||
@property
|
||||
def storage(self):
|
||||
return self._storage
|
||||
|
||||
def render(self) -> dict[str, Any]:
|
||||
if self._network_mode and self.networks:
|
||||
raise RenderError("Cannot set both [network_mode] and [networks]")
|
||||
|
||||
result = {
|
||||
"image": self._image,
|
||||
"platform": "linux/amd64",
|
||||
"tty": self._tty,
|
||||
"stdin_open": self._stdin_open,
|
||||
"restart": self.restart.render(),
|
||||
}
|
||||
|
||||
if self._pull_policy:
|
||||
result["pull_policy"] = self._pull_policy
|
||||
|
||||
if self.healthcheck.has_healthcheck():
|
||||
result["healthcheck"] = self.healthcheck.render()
|
||||
|
||||
if self._hostname:
|
||||
result["hostname"] = self._hostname
|
||||
|
||||
if self._build_image:
|
||||
result["build"] = {"tags": [self._image], "dockerfile_inline": self._build_image}
|
||||
|
||||
if self.configs.has_configs():
|
||||
result["configs"] = self.configs.render()
|
||||
|
||||
if self._init is not None:
|
||||
result["init"] = self._init
|
||||
|
||||
if self._read_only is not None:
|
||||
result["read_only"] = self._read_only
|
||||
|
||||
if self._grace_period is not None:
|
||||
result["stop_grace_period"] = f"{self._grace_period}s"
|
||||
|
||||
if self._user:
|
||||
result["user"] = self._user
|
||||
|
||||
for g in self.get_additional_groups():
|
||||
self.add_group(g)
|
||||
|
||||
if self._group_add:
|
||||
result["group_add"] = sorted(self._group_add, key=lambda g: (isinstance(g, str), g))
|
||||
|
||||
if self._shm_size is not None:
|
||||
result["shm_size"] = f"{self._shm_size}M"
|
||||
|
||||
if self._privileged is not None:
|
||||
result["privileged"] = self._privileged
|
||||
|
||||
if self._cap_drop:
|
||||
result["cap_drop"] = sorted(self._cap_drop)
|
||||
|
||||
if self._cap_add:
|
||||
result["cap_add"] = sorted(self._cap_add)
|
||||
|
||||
if self._security_opt:
|
||||
result["security_opt"] = sorted(self._security_opt)
|
||||
|
||||
if self._network_mode:
|
||||
result["network_mode"] = self._network_mode
|
||||
|
||||
if self.sysctls.has_sysctls():
|
||||
result["sysctls"] = self.sysctls.render()
|
||||
|
||||
if self._network_mode != "host":
|
||||
if self.ports.has_ports():
|
||||
result["ports"] = self.ports.render()
|
||||
|
||||
if self._entrypoint:
|
||||
result["entrypoint"] = self._entrypoint
|
||||
|
||||
if self._command:
|
||||
result["command"] = self._command
|
||||
|
||||
if self.devices.has_devices():
|
||||
result["devices"] = self.devices.render()
|
||||
|
||||
if self.deploy.has_deploy():
|
||||
result["deploy"] = self.deploy.render()
|
||||
|
||||
if self.environment.has_variables():
|
||||
result["environment"] = self.environment.render()
|
||||
|
||||
if self.labels.has_labels():
|
||||
result["labels"] = self.labels.render()
|
||||
|
||||
if self.dns.has_dns_nameservers():
|
||||
result["dns"] = self.dns.render_dns_nameservers()
|
||||
|
||||
if self.dns.has_dns_searches():
|
||||
result["dns_search"] = self.dns.render_dns_searches()
|
||||
|
||||
if self.dns.has_dns_opts():
|
||||
result["dns_opt"] = self.dns.render_dns_opts()
|
||||
|
||||
if self.depends.has_dependencies():
|
||||
result["depends_on"] = self.depends.render()
|
||||
|
||||
if self._storage.has_mounts():
|
||||
result["volumes"] = self._storage.render()
|
||||
|
||||
return result
|
||||
@@ -1,68 +0,0 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from render import Render
|
||||
|
||||
try:
|
||||
from .error import RenderError
|
||||
from .device import Device
|
||||
except ImportError:
|
||||
from error import RenderError
|
||||
from device import Device
|
||||
|
||||
|
||||
class Devices:
|
||||
def __init__(self, render_instance: "Render"):
|
||||
self._render_instance = render_instance
|
||||
self._devices: set[Device] = set()
|
||||
|
||||
# Tracks all container device paths to make sure they are not duplicated
|
||||
self._container_device_paths: set[str] = set()
|
||||
# Scan values for devices we should automatically add
|
||||
# for example /dev/dri for gpus
|
||||
self._auto_add_devices_from_values()
|
||||
|
||||
def _auto_add_devices_from_values(self):
|
||||
resources = self._render_instance.values.get("resources", {})
|
||||
|
||||
if resources.get("gpus", {}).get("use_all_gpus", False):
|
||||
self.add_device("/dev/dri", "/dev/dri", allow_disallowed=True)
|
||||
if resources["gpus"].get("kfd_device_exists", False):
|
||||
self.add_device("/dev/kfd", "/dev/kfd", allow_disallowed=True) # AMD ROCm
|
||||
|
||||
def add_device(self, host_device: str, container_device: str, cgroup_perm: str = "", allow_disallowed=False):
|
||||
# Host device can be mapped to multiple container devices,
|
||||
# so we only make sure container devices are not duplicated
|
||||
if container_device in self._container_device_paths:
|
||||
raise RenderError(f"Device with container path [{container_device}] already added")
|
||||
|
||||
self._devices.add(Device(host_device, container_device, cgroup_perm, allow_disallowed))
|
||||
self._container_device_paths.add(container_device)
|
||||
|
||||
def add_usb_bus(self):
|
||||
self.add_device("/dev/bus/usb", "/dev/bus/usb", allow_disallowed=True)
|
||||
|
||||
def _add_snd_device(self):
|
||||
self.add_device("/dev/snd", "/dev/snd", allow_disallowed=True)
|
||||
|
||||
def has_devices(self):
|
||||
return len(self._devices) > 0
|
||||
|
||||
# Mainly will be used from dependencies
|
||||
# There is no reason to pass devices to
|
||||
# redis or postgres for example
|
||||
def remove_devices(self):
|
||||
self._devices.clear()
|
||||
self._container_device_paths.clear()
|
||||
|
||||
# Check if there are any gpu devices
|
||||
# Used to determine if we should add groups
|
||||
# like 'video' to the container
|
||||
def has_gpus(self):
|
||||
for d in self._devices:
|
||||
if d.host_device == "/dev/dri":
|
||||
return True
|
||||
return False
|
||||
|
||||
def render(self) -> list[str]:
|
||||
return sorted([d.render() for d in self._devices])
|
||||
@@ -1,149 +0,0 @@
|
||||
import re
|
||||
import copy
|
||||
import bcrypt
|
||||
import secrets
|
||||
from base64 import b64encode
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from render import Render
|
||||
|
||||
try:
|
||||
from .error import RenderError
|
||||
from .volume_sources import HostPathSource, IxVolumeSource
|
||||
except ImportError:
|
||||
from error import RenderError
|
||||
from volume_sources import HostPathSource, IxVolumeSource
|
||||
|
||||
|
||||
class Functions:
|
||||
def __init__(self, render_instance: "Render"):
|
||||
self._render_instance = render_instance
|
||||
|
||||
def _bcrypt_hash(self, password):
|
||||
hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||
return hashed
|
||||
|
||||
def _htpasswd(self, username, password):
|
||||
hashed = self._bcrypt_hash(password)
|
||||
return username + ":" + hashed
|
||||
|
||||
def _secure_string(self, length):
|
||||
return secrets.token_urlsafe(length)
|
||||
|
||||
def _basic_auth(self, username, password):
|
||||
return b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8")
|
||||
|
||||
def _basic_auth_header(self, username, password):
|
||||
return f"Basic {self._basic_auth(username, password)}"
|
||||
|
||||
def _fail(self, message):
|
||||
raise RenderError(message)
|
||||
|
||||
def _camel_case(self, string):
|
||||
return string.title()
|
||||
|
||||
def _auto_cast(self, value):
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if value.lower() in ["true", "false"]:
|
||||
return value.lower() == "true"
|
||||
|
||||
return value
|
||||
|
||||
def _match_regex(self, value, regex):
|
||||
if not re.match(regex, value):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _must_match_regex(self, value, regex):
|
||||
if not self._match_regex(value, regex):
|
||||
raise RenderError(f"Expected [{value}] to match [{regex}]")
|
||||
return value
|
||||
|
||||
def _is_boolean(self, string):
|
||||
return string.lower() in ["true", "false"]
|
||||
|
||||
def _is_number(self, string):
|
||||
try:
|
||||
float(string)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def _copy_dict(self, dict):
|
||||
return copy.deepcopy(dict)
|
||||
|
||||
def _merge_dicts(self, *dicts):
|
||||
merged_dict = {}
|
||||
for dictionary in dicts:
|
||||
merged_dict.update(dictionary)
|
||||
return merged_dict
|
||||
|
||||
def _disallow_chars(self, string: str, chars: list[str], key: str):
|
||||
for char in chars:
|
||||
if char in string:
|
||||
raise RenderError(f"Disallowed character [{char}] in [{key}]")
|
||||
return string
|
||||
|
||||
def _or_default(self, value, default):
|
||||
if not value:
|
||||
return default
|
||||
return value
|
||||
|
||||
def _temp_config(self, name):
|
||||
if not name:
|
||||
raise RenderError("Expected [name] to be set when calling [temp_config].")
|
||||
return {"type": "temporary", "volume_config": {"volume_name": name}}
|
||||
|
||||
def _get_host_path(self, storage):
|
||||
source_type = storage.get("type", "")
|
||||
if not source_type:
|
||||
raise RenderError("Expected [type] to be set for volume mounts.")
|
||||
|
||||
match source_type:
|
||||
case "host_path":
|
||||
mount_config = storage.get("host_path_config")
|
||||
if mount_config is None:
|
||||
raise RenderError("Expected [host_path_config] to be set for [host_path] type.")
|
||||
host_source = HostPathSource(self._render_instance, mount_config).get()
|
||||
return host_source
|
||||
case "ix_volume":
|
||||
mount_config = storage.get("ix_volume_config")
|
||||
if mount_config is None:
|
||||
raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.")
|
||||
ix_source = IxVolumeSource(self._render_instance, mount_config).get()
|
||||
return ix_source
|
||||
case _:
|
||||
raise RenderError(f"Storage type [{source_type}] does not support host path.")
|
||||
|
||||
def func_map(self):
|
||||
# TODO: Check what is no longer used and remove
|
||||
return {
|
||||
"auto_cast": self._auto_cast,
|
||||
"basic_auth_header": self._basic_auth_header,
|
||||
"basic_auth": self._basic_auth,
|
||||
"bcrypt_hash": self._bcrypt_hash,
|
||||
"camel_case": self._camel_case,
|
||||
"copy_dict": self._copy_dict,
|
||||
"fail": self._fail,
|
||||
"htpasswd": self._htpasswd,
|
||||
"is_boolean": self._is_boolean,
|
||||
"is_number": self._is_number,
|
||||
"match_regex": self._match_regex,
|
||||
"merge_dicts": self._merge_dicts,
|
||||
"must_match_regex": self._must_match_regex,
|
||||
"secure_string": self._secure_string,
|
||||
"disallow_chars": self._disallow_chars,
|
||||
"get_host_path": self._get_host_path,
|
||||
"or_default": self._or_default,
|
||||
"temp_config": self._temp_config,
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
from typing import TYPE_CHECKING, TypedDict, Literal, NotRequired, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from render import Render
|
||||
|
||||
try:
|
||||
from .error import RenderError
|
||||
from .validations import valid_fs_path_or_raise
|
||||
from .volume_mount import VolumeMount
|
||||
except ImportError:
|
||||
from error import RenderError
|
||||
from validations import valid_fs_path_or_raise
|
||||
from volume_mount import VolumeMount
|
||||
|
||||
|
||||
class IxStorageTmpfsConfig(TypedDict):
|
||||
size: NotRequired[int]
|
||||
mode: NotRequired[str]
|
||||
|
||||
|
||||
class AclConfig(TypedDict, total=False):
|
||||
path: str
|
||||
|
||||
|
||||
class IxStorageHostPathConfig(TypedDict):
|
||||
path: NotRequired[str] # Either this or acl.path must be set
|
||||
acl_enable: NotRequired[bool]
|
||||
acl: NotRequired[AclConfig]
|
||||
create_host_path: NotRequired[bool]
|
||||
propagation: NotRequired[Literal["shared", "slave", "private", "rshared", "rslave", "rprivate"]]
|
||||
auto_permissions: NotRequired[bool] # Only when acl_enable is false
|
||||
|
||||
|
||||
class IxStorageIxVolumeConfig(TypedDict):
|
||||
dataset_name: str
|
||||
acl_enable: NotRequired[bool]
|
||||
acl_entries: NotRequired[AclConfig]
|
||||
create_host_path: NotRequired[bool]
|
||||
propagation: NotRequired[Literal["shared", "slave", "private", "rshared", "rslave", "rprivate"]]
|
||||
auto_permissions: NotRequired[bool] # Only when acl_enable is false
|
||||
|
||||
|
||||
class IxStorageVolumeConfig(TypedDict):
|
||||
volume_name: NotRequired[str]
|
||||
nocopy: NotRequired[bool]
|
||||
auto_permissions: NotRequired[bool]
|
||||
|
||||
|
||||
class IxStorageNfsConfig(TypedDict):
|
||||
server: str
|
||||
path: str
|
||||
options: NotRequired[list[str]]
|
||||
|
||||
|
||||
class IxStorageCifsConfig(TypedDict):
|
||||
server: str
|
||||
path: str
|
||||
username: str
|
||||
password: str
|
||||
domain: NotRequired[str]
|
||||
options: NotRequired[list[str]]
|
||||
|
||||
|
||||
IxStorageVolumeLikeConfigs = Union[IxStorageVolumeConfig, IxStorageNfsConfig, IxStorageCifsConfig, IxStorageTmpfsConfig]
|
||||
IxStorageBindLikeConfigs = Union[IxStorageHostPathConfig, IxStorageIxVolumeConfig]
|
||||
IxStorageLikeConfigs = Union[IxStorageBindLikeConfigs, IxStorageVolumeLikeConfigs]
|
||||
|
||||
|
||||
class IxStorage(TypedDict):
|
||||
type: Literal["ix_volume", "host_path", "tmpfs", "volume", "anonymous", "temporary"]
|
||||
read_only: NotRequired[bool]
|
||||
|
||||
ix_volume_config: NotRequired[IxStorageIxVolumeConfig]
|
||||
host_path_config: NotRequired[IxStorageHostPathConfig]
|
||||
tmpfs_config: NotRequired[IxStorageTmpfsConfig]
|
||||
volume_config: NotRequired[IxStorageVolumeConfig]
|
||||
nfs_config: NotRequired[IxStorageNfsConfig]
|
||||
cifs_config: NotRequired[IxStorageCifsConfig]
|
||||
|
||||
|
||||
class Storage:
|
||||
def __init__(self, render_instance: "Render"):
|
||||
self._render_instance = render_instance
|
||||
self._volume_mounts: set[VolumeMount] = set()
|
||||
|
||||
def add(self, mount_path: str, config: "IxStorage"):
|
||||
mount_path = valid_fs_path_or_raise(mount_path)
|
||||
if mount_path in [m.mount_path for m in self._volume_mounts]:
|
||||
raise RenderError(f"Mount path [{mount_path}] already used for another volume mount")
|
||||
|
||||
volume_mount = VolumeMount(self._render_instance, mount_path, config)
|
||||
self._volume_mounts.add(volume_mount)
|
||||
|
||||
def _add_docker_socket(self, read_only: bool = True, mount_path: str = ""):
|
||||
mount_path = valid_fs_path_or_raise(mount_path)
|
||||
cfg: "IxStorage" = {
|
||||
"type": "host_path",
|
||||
"read_only": read_only,
|
||||
"host_path_config": {"path": "/var/run/docker.sock", "create_host_path": False},
|
||||
}
|
||||
self.add(mount_path, cfg)
|
||||
|
||||
def _add_tun_device(self, read_only: bool = True, mount_path: str = ""):
|
||||
mount_path = valid_fs_path_or_raise(mount_path)
|
||||
cfg: "IxStorage" = {
|
||||
"type": "host_path",
|
||||
"read_only": read_only,
|
||||
"host_path_config": {"path": "/dev/net/tun", "create_host_path": False},
|
||||
}
|
||||
self.add(mount_path, cfg)
|
||||
|
||||
def has_mounts(self) -> bool:
|
||||
return bool(self._volume_mounts)
|
||||
|
||||
def render(self):
|
||||
return [vm.render() for vm in sorted(self._volume_mounts, key=lambda vm: vm.mount_path)]
|
||||
@@ -1,360 +0,0 @@
|
||||
import pytest
|
||||
|
||||
|
||||
from render import Render
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_values():
|
||||
return {
|
||||
"images": {
|
||||
"test_image": {
|
||||
"repository": "nginx",
|
||||
"tag": "latest",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_empty_container_name(mock_values):
|
||||
render = Render(mock_values)
|
||||
with pytest.raises(Exception):
|
||||
render.add_container(" ", "test_image")
|
||||
|
||||
|
||||
def test_resolve_image(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["image"] == "nginx:latest"
|
||||
|
||||
|
||||
def test_missing_repo(mock_values):
|
||||
mock_values["images"]["test_image"]["repository"] = ""
|
||||
render = Render(mock_values)
|
||||
with pytest.raises(Exception):
|
||||
render.add_container("test_container", "test_image")
|
||||
|
||||
|
||||
def test_missing_tag(mock_values):
|
||||
mock_values["images"]["test_image"]["tag"] = ""
|
||||
render = Render(mock_values)
|
||||
with pytest.raises(Exception):
|
||||
render.add_container("test_container", "test_image")
|
||||
|
||||
|
||||
def test_non_existing_image(mock_values):
|
||||
render = Render(mock_values)
|
||||
with pytest.raises(Exception):
|
||||
render.add_container("test_container", "non_existing_image")
|
||||
|
||||
|
||||
def test_pull_policy(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_pull_policy("always")
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["pull_policy"] == "always"
|
||||
|
||||
|
||||
def test_invalid_pull_policy(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
with pytest.raises(Exception):
|
||||
c1.set_pull_policy("invalid_policy")
|
||||
|
||||
|
||||
def test_clear_caps(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.add_caps(["NET_ADMIN"])
|
||||
c1.clear_caps()
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert "cap_drop" not in output["services"]["test_container"]
|
||||
assert "cap_add" not in output["services"]["test_container"]
|
||||
|
||||
|
||||
def test_privileged(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_privileged(True)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["privileged"] is True
|
||||
|
||||
|
||||
def test_tty(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_tty(True)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["tty"] is True
|
||||
|
||||
|
||||
def test_init(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_init(True)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["init"] is True
|
||||
|
||||
|
||||
def test_read_only(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_read_only(True)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["read_only"] is True
|
||||
|
||||
|
||||
def test_stdin(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_stdin(True)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["stdin_open"] is True
|
||||
|
||||
|
||||
def test_hostname(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_hostname("test_hostname")
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["hostname"] == "test_hostname"
|
||||
|
||||
|
||||
def test_grace_period(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_grace_period(10)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["stop_grace_period"] == "10s"
|
||||
|
||||
|
||||
def test_user(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_user(1000, 1000)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["user"] == "1000:1000"
|
||||
|
||||
|
||||
def test_invalid_user(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.set_user(-100, 1000)
|
||||
|
||||
|
||||
def test_add_group(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_group(1000)
|
||||
c1.add_group("video")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["group_add"] == [568, 1000, "video"]
|
||||
|
||||
|
||||
def test_add_duplicate_group(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_group(1000)
|
||||
with pytest.raises(Exception):
|
||||
c1.add_group(1000)
|
||||
|
||||
|
||||
def test_add_group_as_string(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_group("1000")
|
||||
|
||||
|
||||
def test_add_docker_socket(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_docker_socket()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["group_add"] == [568, 999]
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/var/run/docker.sock",
|
||||
"target": "/var/run/docker.sock",
|
||||
"read_only": True,
|
||||
"bind": {
|
||||
"propagation": "rprivate",
|
||||
"create_host_path": False,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_tun_device(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_tun_device()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/dev/net/tun",
|
||||
"target": "/dev/net/tun",
|
||||
"read_only": True,
|
||||
"bind": {
|
||||
"propagation": "rprivate",
|
||||
"create_host_path": False,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_snd_device(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_snd_device()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["devices"] == ["/dev/snd:/dev/snd"]
|
||||
assert output["services"]["test_container"]["group_add"] == [29, 568]
|
||||
|
||||
|
||||
def test_shm_size(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.set_shm_size_mb(10)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["shm_size"] == "10M"
|
||||
|
||||
|
||||
def test_valid_caps(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_caps(["ALL", "NET_ADMIN"])
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["cap_add"] == ["ALL", "NET_ADMIN"]
|
||||
assert output["services"]["test_container"]["cap_drop"] == ["ALL"]
|
||||
|
||||
|
||||
def test_add_duplicate_caps(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_caps(["ALL", "NET_ADMIN", "NET_ADMIN"])
|
||||
|
||||
|
||||
def test_invalid_caps(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_caps(["invalid_cap"])
|
||||
|
||||
|
||||
def test_remove_security_opt(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.remove_security_opt("no-new-privileges")
|
||||
output = render.render()
|
||||
assert "security_opt" not in output["services"]["test_container"]
|
||||
|
||||
|
||||
def test_add_security_opt(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_security_opt("seccomp=unconfined")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["security_opt"] == [
|
||||
"no-new-privileges",
|
||||
"seccomp=unconfined",
|
||||
]
|
||||
|
||||
|
||||
def test_add_duplicate_security_opt(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_security_opt("no-new-privileges")
|
||||
|
||||
|
||||
def test_network_mode(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.set_network_mode("host")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["network_mode"] == "host"
|
||||
|
||||
|
||||
def test_auto_network_mode_with_host_network(mock_values):
|
||||
mock_values["network"] = {"host_network": True}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["network_mode"] == "host"
|
||||
|
||||
|
||||
def test_network_mode_with_container(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.set_network_mode("service:test_container")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["network_mode"] == "service:test_container"
|
||||
|
||||
|
||||
def test_network_mode_with_container_missing(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.set_network_mode("service:missing_container")
|
||||
|
||||
|
||||
def test_invalid_network_mode(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.set_network_mode("invalid_mode")
|
||||
|
||||
|
||||
def test_entrypoint(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_entrypoint(["/bin/bash", "-c", "echo hello $MY_ENV"])
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["entrypoint"] == ["/bin/bash", "-c", "echo hello $$MY_ENV"]
|
||||
|
||||
|
||||
def test_command(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_command(["echo", "hello $MY_ENV"])
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["command"] == ["echo", "hello $$MY_ENV"]
|
||||
@@ -1,131 +0,0 @@
|
||||
import pytest
|
||||
|
||||
|
||||
from render import Render
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_values():
|
||||
return {
|
||||
"images": {
|
||||
"test_image": {
|
||||
"repository": "nginx",
|
||||
"tag": "latest",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_add_device(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
|
||||
c1.devices.add_device("/h/dev/sdb", "/c/dev/sdb", "rwm")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["devices"] == ["/h/dev/sda:/c/dev/sda", "/h/dev/sdb:/c/dev/sdb:rwm"]
|
||||
|
||||
|
||||
def test_devices_without_host(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.devices.add_device("", "/c/dev/sda")
|
||||
|
||||
|
||||
def test_devices_without_container(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.devices.add_device("/h/dev/sda", "")
|
||||
|
||||
|
||||
def test_add_duplicate_device(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
|
||||
with pytest.raises(Exception):
|
||||
c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
|
||||
|
||||
|
||||
def test_add_device_with_invalid_container_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.devices.add_device("/h/dev/sda", "c/dev/sda")
|
||||
|
||||
|
||||
def test_add_device_with_invalid_host_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.devices.add_device("h/dev/sda", "/c/dev/sda")
|
||||
|
||||
|
||||
def test_add_disallowed_device(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.devices.add_device("/dev/dri", "/c/dev/sda")
|
||||
|
||||
|
||||
def test_add_device_with_invalid_cgroup_perm(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.devices.add_device("/h/dev/sda", "/c/dev/sda", "invalid")
|
||||
|
||||
|
||||
def test_automatically_add_gpu_devices(mock_values):
|
||||
mock_values["resources"] = {"gpus": {"use_all_gpus": True}}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["devices"] == ["/dev/dri:/dev/dri"]
|
||||
assert output["services"]["test_container"]["group_add"] == [44, 107, 568]
|
||||
|
||||
|
||||
def test_automatically_add_gpu_devices_and_kfd(mock_values):
|
||||
mock_values["resources"] = {"gpus": {"use_all_gpus": True, "kfd_device_exists": True}}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["devices"] == ["/dev/dri:/dev/dri", "/dev/kfd:/dev/kfd"]
|
||||
assert output["services"]["test_container"]["group_add"] == [44, 107, 568]
|
||||
|
||||
|
||||
def test_remove_gpu_devices(mock_values):
|
||||
mock_values["resources"] = {"gpus": {"use_all_gpus": True}}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.devices.remove_devices()
|
||||
output = render.render()
|
||||
assert "devices" not in output["services"]["test_container"]
|
||||
assert output["services"]["test_container"]["group_add"] == [568]
|
||||
|
||||
|
||||
def test_add_usb_bus(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.devices.add_usb_bus()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["devices"] == ["/dev/bus/usb:/dev/bus/usb"]
|
||||
|
||||
|
||||
def test_add_usb_bus_disallowed(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.devices.add_device("/dev/bus/usb", "/dev/bus/usb")
|
||||
@@ -1,666 +0,0 @@
|
||||
import pytest
|
||||
|
||||
|
||||
from render import Render
|
||||
from formatter import get_hashed_name_for_volume
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_values():
|
||||
return {
|
||||
"images": {
|
||||
"test_image": {
|
||||
"repository": "nginx",
|
||||
"tag": "latest",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_add_volume_invalid_type(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", {"type": "invalid_type"})
|
||||
|
||||
|
||||
def test_add_volume_empty_mount_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("", {"type": "tmpfs"})
|
||||
|
||||
|
||||
def test_add_volume_duplicate_mount_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_storage("/some/path", {"type": "tmpfs"})
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", {"type": "tmpfs"})
|
||||
|
||||
|
||||
def test_add_volume_host_path_invalid_propagation(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {
|
||||
"type": "host_path",
|
||||
"host_path_config": {"path": "/mnt/test", "propagation": "invalid_propagation"},
|
||||
}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
|
||||
|
||||
def test_add_host_path_volume_no_host_path_config(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path"}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
|
||||
|
||||
def test_add_host_path_volume_no_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "host_path_config": {"path": ""}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
|
||||
|
||||
def test_add_host_path_with_acl_no_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "host_path_config": {"acl_enable": True, "acl": {"path": ""}}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
|
||||
|
||||
def test_add_host_path_volume_mount(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test"}}
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_host_path_volume_mount_with_acl(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {
|
||||
"type": "host_path",
|
||||
"host_path_config": {"path": "/mnt/test", "acl_enable": True, "acl": {"path": "/mnt/test/acl"}},
|
||||
}
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test/acl",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_host_path_volume_mount_with_propagation(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "propagation": "slave"}}
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": False, "propagation": "slave"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_host_path_volume_mount_with_create_host_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "create_host_path": True}}
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": True, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_host_path_volume_mount_with_read_only(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
host_path_config = {"type": "host_path", "read_only": True, "host_path_config": {"path": "/mnt/test"}}
|
||||
c1.add_storage("/some/path", host_path_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test",
|
||||
"target": "/some/path",
|
||||
"read_only": True,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_ix_volume_invalid_dataset_name(mock_values):
|
||||
mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
ix_volume_config = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "invalid_dataset"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", ix_volume_config)
|
||||
|
||||
|
||||
def test_add_ix_volume_no_ix_volume_config(mock_values):
|
||||
mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
ix_volume_config = {"type": "ix_volume"}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", ix_volume_config)
|
||||
|
||||
|
||||
def test_add_ix_volume_volume_mount(mock_values):
|
||||
mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
ix_volume_config = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset"}}
|
||||
c1.add_storage("/some/path", ix_volume_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_ix_volume_volume_mount_with_options(mock_values):
|
||||
mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
ix_volume_config = {
|
||||
"type": "ix_volume",
|
||||
"ix_volume_config": {"dataset_name": "test_dataset", "propagation": "rslave", "create_host_path": True},
|
||||
}
|
||||
c1.add_storage("/some/path", ix_volume_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/mnt/test",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": True, "propagation": "rslave"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_cifs_volume_missing_server(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {"type": "cifs", "cifs_config": {"path": "/path", "username": "user", "password": "password"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_missing_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "username": "user", "password": "password"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_missing_username(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "path": "/path", "password": "password"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_missing_password(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "path": "/path", "username": "user"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_without_cifs_config(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {"type": "cifs"}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_duplicate_option(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {
|
||||
"type": "cifs",
|
||||
"cifs_config": {
|
||||
"server": "server",
|
||||
"path": "/path",
|
||||
"username": "user",
|
||||
"password": "pas$word",
|
||||
"options": ["verbose=true", "verbose=true"],
|
||||
},
|
||||
}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_disallowed_option(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {
|
||||
"type": "cifs",
|
||||
"cifs_config": {
|
||||
"server": "server",
|
||||
"path": "/path",
|
||||
"username": "user",
|
||||
"password": "pas$word",
|
||||
"options": ["user=username"],
|
||||
},
|
||||
}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_invalid_options(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {
|
||||
"type": "cifs",
|
||||
"cifs_config": {
|
||||
"server": "server",
|
||||
"path": "/path",
|
||||
"username": "user",
|
||||
"password": "pas$word",
|
||||
"options": {"verbose": True},
|
||||
},
|
||||
}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_cifs_volume_invalid_options2(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_config = {
|
||||
"type": "cifs",
|
||||
"cifs_config": {
|
||||
"server": "server",
|
||||
"path": "/path",
|
||||
"username": "user",
|
||||
"password": "pas$word",
|
||||
"options": [{"verbose": True}],
|
||||
},
|
||||
}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
|
||||
|
||||
def test_add_cifs_volume(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_inner_config = {"server": "server", "path": "/path", "username": "user", "password": "pas$word"}
|
||||
cifs_config = {"type": "cifs", "cifs_config": cifs_inner_config}
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
output = render.render()
|
||||
vol_name = get_hashed_name_for_volume("cifs", cifs_inner_config)
|
||||
assert output["volumes"] == {
|
||||
vol_name: {
|
||||
"driver_opts": {"type": "cifs", "device": "//server/path", "o": "noperm,password=pas$$word,user=user"}
|
||||
}
|
||||
}
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
|
||||
]
|
||||
|
||||
|
||||
def test_cifs_volume_with_options(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
cifs_inner_config = {
|
||||
"server": "server",
|
||||
"path": "/path",
|
||||
"username": "user",
|
||||
"password": "pas$word",
|
||||
"options": ["vers=3.0", "verbose=true"],
|
||||
}
|
||||
cifs_config = {"type": "cifs", "cifs_config": cifs_inner_config}
|
||||
c1.add_storage("/some/path", cifs_config)
|
||||
output = render.render()
|
||||
vol_name = get_hashed_name_for_volume("cifs", cifs_inner_config)
|
||||
assert output["volumes"] == {
|
||||
vol_name: {
|
||||
"driver_opts": {
|
||||
"type": "cifs",
|
||||
"device": "//server/path",
|
||||
"o": "noperm,password=pas$$word,user=user,verbose=true,vers=3.0",
|
||||
}
|
||||
}
|
||||
}
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
|
||||
]
|
||||
|
||||
|
||||
def test_nfs_volume_missing_server(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {"type": "nfs", "nfs_config": {"path": "/path"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_nfs_volume_missing_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {"type": "nfs", "nfs_config": {"server": "server"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_nfs_volume_without_nfs_config(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {"type": "nfs"}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_nfs_volume_duplicate_option(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {
|
||||
"type": "nfs",
|
||||
"nfs_config": {"server": "server", "path": "/path", "options": ["verbose=true", "verbose=true"]},
|
||||
}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_nfs_volume_disallowed_option(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": ["addr=server"]}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_nfs_volume_invalid_options(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": {"verbose": True}}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_nfs_volume_invalid_options2(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": [{"verbose": True}]}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
|
||||
|
||||
def test_add_nfs_volume(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_inner_config = {"server": "server", "path": "/path"}
|
||||
nfs_config = {"type": "nfs", "nfs_config": nfs_inner_config}
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
output = render.render()
|
||||
vol_name = get_hashed_name_for_volume("nfs", nfs_inner_config)
|
||||
assert output["volumes"] == {vol_name: {"driver_opts": {"type": "nfs", "device": ":/path", "o": "addr=server"}}}
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
|
||||
]
|
||||
|
||||
|
||||
def test_nfs_volume_with_options(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
nfs_inner_config = {"server": "server", "path": "/path", "options": ["vers=3.0", "verbose=true"]}
|
||||
nfs_config = {"type": "nfs", "nfs_config": nfs_inner_config}
|
||||
c1.add_storage("/some/path", nfs_config)
|
||||
output = render.render()
|
||||
vol_name = get_hashed_name_for_volume("nfs", nfs_inner_config)
|
||||
assert output["volumes"] == {
|
||||
vol_name: {
|
||||
"driver_opts": {
|
||||
"type": "nfs",
|
||||
"device": ":/path",
|
||||
"o": "addr=server,verbose=true,vers=3.0",
|
||||
}
|
||||
}
|
||||
}
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
|
||||
]
|
||||
|
||||
|
||||
def test_tmpfs_invalid_size(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "tmpfs", "tmpfs_config": {"size": "2"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
|
||||
|
||||
def test_tmpfs_zero_size(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "tmpfs", "tmpfs_config": {"size": 0}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
|
||||
|
||||
def test_tmpfs_invalid_mode(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "tmpfs", "tmpfs_config": {"mode": "invalid"}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
|
||||
|
||||
def test_tmpfs_volume(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "tmpfs"}
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "tmpfs",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_temporary_volume(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "temporary", "volume_config": {"volume_name": "test_temp_volume"}}
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"source": "test_temp_volume",
|
||||
"type": "volume",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"volume": {"nocopy": False},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_docker_volume_missing_config(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "volume", "volume_config": {}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
|
||||
|
||||
def test_docker_volume_missing_volume_name(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "volume", "volume_config": {"volume_name": ""}}
|
||||
with pytest.raises(Exception):
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
|
||||
|
||||
def test_docker_volume(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "volume", "volume_config": {"volume_name": "test_volume"}}
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "volume",
|
||||
"source": "test_volume",
|
||||
"target": "/some/path",
|
||||
"read_only": False,
|
||||
"volume": {"nocopy": False},
|
||||
}
|
||||
]
|
||||
assert output["volumes"] == {"test_volume": {}}
|
||||
|
||||
|
||||
def test_anonymous_volume(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
vol_config = {"type": "anonymous", "volume_config": {"nocopy": True}}
|
||||
c1.add_storage("/some/path", vol_config)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{"type": "volume", "target": "/some/path", "read_only": False, "volume": {"nocopy": True}}
|
||||
]
|
||||
assert "volumes" not in output
|
||||
|
||||
|
||||
def test_add_docker_socket(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.storage._add_docker_socket(mount_path="/var/run/docker.sock")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/var/run/docker.sock",
|
||||
"target": "/var/run/docker.sock",
|
||||
"read_only": True,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_docker_socket_not_read_only(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.storage._add_docker_socket(read_only=False, mount_path="/var/run/docker.sock")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/var/run/docker.sock",
|
||||
"target": "/var/run/docker.sock",
|
||||
"read_only": False,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_add_docker_socket_mount_path(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.storage._add_docker_socket(mount_path="/some/path")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/var/run/docker.sock",
|
||||
"target": "/some/path",
|
||||
"read_only": True,
|
||||
"bind": {"create_host_path": False, "propagation": "rprivate"},
|
||||
}
|
||||
]
|
||||
@@ -1,234 +0,0 @@
|
||||
import re
|
||||
import ipaddress
|
||||
|
||||
try:
|
||||
from .error import RenderError
|
||||
except ImportError:
|
||||
from error import RenderError
|
||||
|
||||
OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$")
|
||||
|
||||
|
||||
def valid_pull_policy_or_raise(pull_policy: str):
|
||||
valid_policies = ("missing", "always", "never", "build")
|
||||
if pull_policy not in valid_policies:
|
||||
raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]")
|
||||
return pull_policy
|
||||
|
||||
|
||||
def valid_sysctl_or_raise(sysctl: str, host_network: bool):
|
||||
if not sysctl:
|
||||
raise RenderError("Sysctl cannot be empty")
|
||||
if host_network and sysctl.startswith("net."):
|
||||
raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled")
|
||||
|
||||
valid_sysctls = [
|
||||
"kernel.msgmax",
|
||||
"kernel.msgmnb",
|
||||
"kernel.msgmni",
|
||||
"kernel.sem",
|
||||
"kernel.shmall",
|
||||
"kernel.shmmax",
|
||||
"kernel.shmmni",
|
||||
"kernel.shm_rmid_forced",
|
||||
]
|
||||
# https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls
|
||||
if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls:
|
||||
raise RenderError(
|
||||
f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]"
|
||||
)
|
||||
return sysctl
|
||||
|
||||
|
||||
def valid_redis_password_or_raise(password: str):
|
||||
forbidden_chars = [" ", "'"]
|
||||
for char in forbidden_chars:
|
||||
if char in password:
|
||||
raise RenderError(f"Redis password cannot contain [{char}]")
|
||||
|
||||
|
||||
def valid_octal_mode_or_raise(mode: str):
|
||||
mode = str(mode)
|
||||
if not OCTAL_MODE_REGEX.match(mode):
|
||||
raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]")
|
||||
return mode
|
||||
|
||||
|
||||
def valid_host_path_propagation(propagation: str):
|
||||
valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate")
|
||||
if propagation not in valid_propagations:
|
||||
raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]")
|
||||
return propagation
|
||||
|
||||
|
||||
def valid_portal_scheme_or_raise(scheme: str):
|
||||
schemes = ("http", "https")
|
||||
if scheme not in schemes:
|
||||
raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]")
|
||||
return scheme
|
||||
|
||||
|
||||
def valid_port_or_raise(port: int):
|
||||
if port < 1 or port > 65535:
|
||||
raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535")
|
||||
return port
|
||||
|
||||
|
||||
def valid_ip_or_raise(ip: str):
|
||||
try:
|
||||
ipaddress.ip_address(ip)
|
||||
except ValueError:
|
||||
raise RenderError(f"Invalid IP address [{ip}]")
|
||||
return ip
|
||||
|
||||
|
||||
def valid_port_mode_or_raise(mode: str):
|
||||
modes = ("ingress", "host")
|
||||
if mode not in modes:
|
||||
raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]")
|
||||
return mode
|
||||
|
||||
|
||||
def valid_port_protocol_or_raise(protocol: str):
|
||||
protocols = ("tcp", "udp")
|
||||
if protocol not in protocols:
|
||||
raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]")
|
||||
return protocol
|
||||
|
||||
|
||||
def valid_depend_condition_or_raise(condition: str):
|
||||
valid_conditions = ("service_started", "service_healthy", "service_completed_successfully")
|
||||
if condition not in valid_conditions:
|
||||
raise RenderError(
|
||||
f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]"
|
||||
)
|
||||
return condition
|
||||
|
||||
|
||||
def valid_cgroup_perm_or_raise(cgroup_perm: str):
|
||||
valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "")
|
||||
if cgroup_perm not in valid_cgroup_perms:
|
||||
raise RenderError(
|
||||
f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]"
|
||||
)
|
||||
return cgroup_perm
|
||||
|
||||
|
||||
def allowed_dns_opt_or_raise(dns_opt: str):
|
||||
disallowed_dns_opts = []
|
||||
if dns_opt in disallowed_dns_opts:
|
||||
raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.")
|
||||
return dns_opt
|
||||
|
||||
|
||||
def valid_http_path_or_raise(path: str):
|
||||
path = _valid_path_or_raise(path)
|
||||
return path
|
||||
|
||||
|
||||
def valid_fs_path_or_raise(path: str):
|
||||
# There is no reason to allow / as a path,
|
||||
# either on host or in a container side.
|
||||
if path == "/":
|
||||
raise RenderError(f"Path [{path}] cannot be [/]")
|
||||
path = _valid_path_or_raise(path)
|
||||
return path
|
||||
|
||||
|
||||
def _valid_path_or_raise(path: str):
|
||||
if path == "":
|
||||
raise RenderError(f"Path [{path}] cannot be empty")
|
||||
if not path.startswith("/"):
|
||||
raise RenderError(f"Path [{path}] must start with /")
|
||||
if "//" in path:
|
||||
raise RenderError(f"Path [{path}] cannot contain [//]")
|
||||
return path
|
||||
|
||||
|
||||
def allowed_device_or_raise(path: str):
|
||||
disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd"]
|
||||
if path in disallowed_devices:
|
||||
raise RenderError(f"Device [{path}] is not allowed to be manually added.")
|
||||
return path
|
||||
|
||||
|
||||
def valid_network_mode_or_raise(mode: str, containers: list[str]):
|
||||
valid_modes = ("host", "none")
|
||||
if mode in valid_modes:
|
||||
return mode
|
||||
|
||||
if mode.startswith("service:"):
|
||||
if mode[8:] not in containers:
|
||||
raise RenderError(f"Service [{mode[8:]}] not found")
|
||||
return mode
|
||||
|
||||
raise RenderError(
|
||||
f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:<name>]"
|
||||
)
|
||||
|
||||
|
||||
def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0):
|
||||
valid_restart_policies = ("always", "on-failure", "unless-stopped", "no")
|
||||
if policy not in valid_restart_policies:
|
||||
raise RenderError(
|
||||
f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]"
|
||||
)
|
||||
if policy != "on-failure" and maximum_retry_count != 0:
|
||||
raise RenderError("Maximum retry count can only be set for [on-failure] restart policy")
|
||||
|
||||
if maximum_retry_count < 0:
|
||||
raise RenderError("Maximum retry count must be a positive integer")
|
||||
|
||||
return policy
|
||||
|
||||
|
||||
def valid_cap_or_raise(cap: str):
|
||||
valid_policies = (
|
||||
"ALL",
|
||||
"AUDIT_CONTROL",
|
||||
"AUDIT_READ",
|
||||
"AUDIT_WRITE",
|
||||
"BLOCK_SUSPEND",
|
||||
"BPF",
|
||||
"CHECKPOINT_RESTORE",
|
||||
"CHOWN",
|
||||
"DAC_OVERRIDE",
|
||||
"DAC_READ_SEARCH",
|
||||
"FOWNER",
|
||||
"FSETID",
|
||||
"IPC_LOCK",
|
||||
"IPC_OWNER",
|
||||
"KILL",
|
||||
"LEASE",
|
||||
"LINUX_IMMUTABLE",
|
||||
"MAC_ADMIN",
|
||||
"MAC_OVERRIDE",
|
||||
"MKNOD",
|
||||
"NET_ADMIN",
|
||||
"NET_BIND_SERVICE",
|
||||
"NET_BROADCAST",
|
||||
"NET_RAW",
|
||||
"PERFMON",
|
||||
"SETFCAP",
|
||||
"SETGID",
|
||||
"SETPCAP",
|
||||
"SETUID",
|
||||
"SYS_ADMIN",
|
||||
"SYS_BOOT",
|
||||
"SYS_CHROOT",
|
||||
"SYS_MODULE",
|
||||
"SYS_NICE",
|
||||
"SYS_PACCT",
|
||||
"SYS_PTRACE",
|
||||
"SYS_RAWIO",
|
||||
"SYS_RESOURCE",
|
||||
"SYS_TIME",
|
||||
"SYS_TTY_CONFIG",
|
||||
"SYSLOG",
|
||||
"WAKE_ALARM",
|
||||
)
|
||||
|
||||
if cap not in valid_policies:
|
||||
raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]")
|
||||
|
||||
return cap
|
||||
@@ -1,106 +0,0 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from render import Render
|
||||
from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig
|
||||
|
||||
try:
|
||||
from .error import RenderError
|
||||
from .formatter import get_hashed_name_for_volume
|
||||
from .validations import valid_fs_path_or_raise
|
||||
except ImportError:
|
||||
from error import RenderError
|
||||
from formatter import get_hashed_name_for_volume
|
||||
from validations import valid_fs_path_or_raise
|
||||
|
||||
|
||||
class HostPathSource:
|
||||
def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"):
|
||||
self._render_instance = render_instance
|
||||
self.source: str = ""
|
||||
|
||||
if not config:
|
||||
raise RenderError("Expected [host_path_config] to be set for [host_path] type.")
|
||||
|
||||
path = ""
|
||||
if config.get("acl_enable", False):
|
||||
acl_path = config.get("acl", {}).get("path")
|
||||
if not acl_path:
|
||||
raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.")
|
||||
path = valid_fs_path_or_raise(acl_path)
|
||||
else:
|
||||
path = valid_fs_path_or_raise(config.get("path", ""))
|
||||
|
||||
self.source = path.rstrip("/")
|
||||
|
||||
def get(self):
|
||||
return self.source
|
||||
|
||||
|
||||
class IxVolumeSource:
|
||||
def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"):
|
||||
self._render_instance = render_instance
|
||||
self.source: str = ""
|
||||
|
||||
if not config:
|
||||
raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.")
|
||||
dataset_name = config.get("dataset_name")
|
||||
if not dataset_name:
|
||||
raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.")
|
||||
|
||||
ix_volumes = self._render_instance.values.get("ix_volumes", {})
|
||||
if dataset_name not in ix_volumes:
|
||||
available = ", ".join(ix_volumes.keys())
|
||||
raise RenderError(
|
||||
f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. "
|
||||
f"Available keys: [{available}]."
|
||||
)
|
||||
|
||||
self.source = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/"))
|
||||
|
||||
def get(self):
|
||||
return self.source
|
||||
|
||||
|
||||
class CifsSource:
|
||||
def __init__(self, render_instance: "Render", config: dict):
|
||||
self._render_instance = render_instance
|
||||
self.source: str = ""
|
||||
|
||||
if not config:
|
||||
raise RenderError("Expected [cifs_config] to be set for [cifs] type.")
|
||||
self.source = get_hashed_name_for_volume("cifs", config)
|
||||
|
||||
def get(self):
|
||||
return self.source
|
||||
|
||||
|
||||
class NfsSource:
|
||||
def __init__(self, render_instance: "Render", config: dict):
|
||||
self._render_instance = render_instance
|
||||
self.source: str = ""
|
||||
|
||||
if not config:
|
||||
raise RenderError("Expected [nfs_config] to be set for [nfs] type.")
|
||||
self.source = get_hashed_name_for_volume("nfs", config)
|
||||
|
||||
def get(self):
|
||||
return self.source
|
||||
|
||||
|
||||
class VolumeSource:
|
||||
def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"):
|
||||
self._render_instance = render_instance
|
||||
self.source: str = ""
|
||||
|
||||
if not config:
|
||||
raise RenderError("Expected [volume_config] to be set for [volume] type.")
|
||||
|
||||
volume_name: str = config.get("volume_name", "")
|
||||
if not volume_name:
|
||||
raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.")
|
||||
|
||||
self.source = volume_name
|
||||
|
||||
def get(self):
|
||||
return self.source
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user