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:
Stavros Kois
2024-12-23 11:08:34 +02:00
committed by GitHub
parent 57ed861de7
commit 17c713ad0c
8305 changed files with 295513 additions and 272189 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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])

View File

@@ -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,
}

View File

@@ -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)]

View File

@@ -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"]

View File

@@ -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")

View File

@@ -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"},
}
]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"},
}
]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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])

View File

@@ -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,
}

View File

@@ -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)]

View File

@@ -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"]

View File

@@ -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")

View File

@@ -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"},
}
]

View File

@@ -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

View File

@@ -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