mirror of
https://github.com/MAGICGrants/truenas-apps.git
synced 2026-01-09 20:47:58 -05:00
metube: allow setting ytdl options (#2205)
* lib: strip spaces * hashes * correct file * fix version * hash * vresion * fix autocast * hash * fix * metube: make sure options file is in a persistent place * allow setting options * update lib * rehash
This commit is contained in:
@@ -11,8 +11,8 @@ icon: https://media.sys.truenas.net/apps/metube/icons/icon.svg
|
||||
keywords:
|
||||
- youtube-dl
|
||||
- yt-dlp
|
||||
lib_version: 2.1.16
|
||||
lib_version_hash: dac15686f882b9ce65b8549a3d5c0ed7bafe2df7a9028880d1a99b0ff4af1eff
|
||||
lib_version: 2.1.22
|
||||
lib_version_hash: 60527959f07d0826d50f6ee354ee40bdbc43f690991ddde8e2f36fd4c13fe4b0
|
||||
maintainers:
|
||||
- email: dev@ixsystems.com
|
||||
name: truenas
|
||||
@@ -30,4 +30,4 @@ sources:
|
||||
- https://github.com/alexta69/metube
|
||||
title: MeTube
|
||||
train: community
|
||||
version: 1.2.35
|
||||
version: 1.2.36
|
||||
|
||||
@@ -33,6 +33,28 @@ questions:
|
||||
description: Dark
|
||||
- value: light
|
||||
description: Light
|
||||
- variable: ytdl_options
|
||||
label: YouTube-DL Options
|
||||
description: See available options at https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/YoutubeDL.py#L220
|
||||
schema:
|
||||
type: list
|
||||
default: []
|
||||
items:
|
||||
- variable: ytdl_option
|
||||
label: Option
|
||||
schema:
|
||||
type: dict
|
||||
attrs:
|
||||
- variable: key
|
||||
label: Key
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- variable: value
|
||||
label: Value
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- variable: additional_envs
|
||||
label: Additional Environment Variables
|
||||
description: Configure additional environment variables for MeTube.
|
||||
|
||||
@@ -10,6 +10,13 @@
|
||||
{% do c1.environment.add_env("PORT", values.network.web_port) %}
|
||||
{% do c1.environment.add_env("DOWNLOAD_DIR", values.consts.downloads_path) %}
|
||||
{% do c1.environment.add_env("STATE_DIR", "%s/.metube" | format(values.consts.downloads_path)) %}
|
||||
{% set opts = namespace(x={}) %}
|
||||
{% for opt in values.metube.ytdl_options %}
|
||||
{% do opts.x.update({opt.key: tpl.funcs.auto_cast(opt.value)}) %}
|
||||
{% endfor %}
|
||||
{% if opts.x %}
|
||||
{% do c1.environment.add_env("YTDL_OPTIONS", opts.x | tojson) %}
|
||||
{% endif %}
|
||||
{% do c1.environment.add_env("DEFAULT_THEME", values.metube.default_theme) %}
|
||||
{% do c1.environment.add_user_envs(values.metube.additional_envs) %}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ try:
|
||||
from .tmpfs import Tmpfs
|
||||
from .validations import (
|
||||
valid_cap_or_raise,
|
||||
valid_cgroup_or_raise,
|
||||
valid_ipc_mode_or_raise,
|
||||
valid_network_mode_or_raise,
|
||||
valid_port_bind_mode_or_raise,
|
||||
@@ -50,6 +51,7 @@ except ImportError:
|
||||
from tmpfs import Tmpfs
|
||||
from validations import (
|
||||
valid_cap_or_raise,
|
||||
valid_cgroup_or_raise,
|
||||
valid_ipc_mode_or_raise,
|
||||
valid_network_mode_or_raise,
|
||||
valid_port_bind_mode_or_raise,
|
||||
@@ -88,6 +90,7 @@ class Container:
|
||||
self._storage: Storage = Storage(self._render_instance, self)
|
||||
self._tmpfs: Tmpfs = Tmpfs(self._render_instance, self)
|
||||
self._ipc_mode: str | None = None
|
||||
self._cgroup: str | None = None
|
||||
self._device_cgroup_rules: DeviceCGroupRules = DeviceCGroupRules(self._render_instance)
|
||||
self.sysctls: Sysctls = Sysctls(self._render_instance, self)
|
||||
self.configs: ContainerConfigs = ContainerConfigs(self._render_instance, self._render_instance.configs)
|
||||
@@ -146,6 +149,7 @@ class Container:
|
||||
def build_image(self, content: list[str | None]):
|
||||
dockerfile = f"FROM {self._image}\n"
|
||||
for line in content:
|
||||
line = line.strip() if line else ""
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("FROM"):
|
||||
@@ -205,6 +209,9 @@ class Container:
|
||||
def add_device_cgroup_rule(self, dev_grp_rule: str):
|
||||
self._device_cgroup_rules.add_rule(dev_grp_rule)
|
||||
|
||||
def set_cgroup(self, cgroup: str):
|
||||
self._cgroup = valid_cgroup_or_raise(cgroup)
|
||||
|
||||
def set_init(self, enabled: bool = False):
|
||||
self._init = enabled
|
||||
|
||||
@@ -339,6 +346,9 @@ class Container:
|
||||
if self._device_cgroup_rules.has_rules():
|
||||
result["device_cgroup_rules"] = self._device_cgroup_rules.render()
|
||||
|
||||
if self._cgroup is not None:
|
||||
result["cgroup"] = self._cgroup
|
||||
|
||||
if self._extra_hosts.has_hosts():
|
||||
result["extra_hosts"] = self._extra_hosts.render()
|
||||
|
||||
@@ -30,15 +30,17 @@ class MariadbContainer:
|
||||
):
|
||||
self._render_instance = render_instance
|
||||
self._name = name
|
||||
self._config = config
|
||||
|
||||
for key in ("user", "password", "database", "volume"):
|
||||
if key not in config:
|
||||
raise RenderError(f"Expected [{key}] to be set for mariadb")
|
||||
|
||||
port = valid_port_or_raise(config.get("port") or 3306)
|
||||
port = valid_port_or_raise(self._get_port())
|
||||
root_password = config.get("root_password") or config["password"]
|
||||
auto_upgrade = config.get("auto_upgrade", True)
|
||||
|
||||
self._get_repo(image, ("mariadb"))
|
||||
c = self._render_instance.add_container(name, image)
|
||||
c.set_user(999, 999)
|
||||
c.healthcheck.set_test("mariadb")
|
||||
@@ -60,6 +62,20 @@ class MariadbContainer:
|
||||
# For example: c.depends.add_dependency("other_container", "service_started")
|
||||
self._container = c
|
||||
|
||||
def _get_port(self):
|
||||
return self._config.get("port") or 3306
|
||||
|
||||
def _get_repo(self, image, supported_repos):
|
||||
images = self._render_instance.values["images"]
|
||||
if image not in images:
|
||||
raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
|
||||
repo = images[image].get("repository")
|
||||
if not repo:
|
||||
raise RenderError("Could not determine repo")
|
||||
if repo not in supported_repos:
|
||||
raise RenderError(f"Unsupported repo [{repo}] for mariadb. Supported repos: {', '.join(supported_repos)}")
|
||||
return repo
|
||||
|
||||
@property
|
||||
def container(self):
|
||||
return self._container
|
||||
@@ -56,7 +56,7 @@ class PostgresContainer:
|
||||
"POSTGRES_USER": config["user"],
|
||||
"POSTGRES_PASSWORD": config["password"],
|
||||
"POSTGRES_DB": config["database"],
|
||||
"POSTGRES_PORT": port,
|
||||
"PGPORT": port,
|
||||
}
|
||||
|
||||
for k, v in common_variables.items():
|
||||
@@ -66,7 +66,7 @@ class PostgresContainer:
|
||||
f"{self._name}_postgres_data", config["volume"], {"uid": 999, "gid": 999, "mode": "check"}
|
||||
)
|
||||
|
||||
repo = self._get_repo(image)
|
||||
repo = self._get_repo(image, ("postgres", "tensorchord/pgvecto-rs"))
|
||||
# eg we don't want to handle upgrades of pg_vector at the moment
|
||||
if repo == "postgres":
|
||||
target_major_version = self._get_target_version(image)
|
||||
@@ -103,13 +103,15 @@ class PostgresContainer:
|
||||
def _get_port(self):
|
||||
return self._config.get("port") or 5432
|
||||
|
||||
def _get_repo(self, image):
|
||||
def _get_repo(self, image, supported_repos):
|
||||
images = self._render_instance.values["images"]
|
||||
if image not in images:
|
||||
raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
|
||||
repo = images[image].get("repository", "")
|
||||
repo = images[image].get("repository")
|
||||
if not repo:
|
||||
raise RenderError("Could not determine repo")
|
||||
if repo not in supported_repos:
|
||||
raise RenderError(f"Unsupported repo [{repo}] for postgres. Supported repos: {', '.join(supported_repos)}")
|
||||
return repo
|
||||
|
||||
def _get_target_version(self, image):
|
||||
@@ -36,6 +36,7 @@ class RedisContainer:
|
||||
valid_redis_password_or_raise(config["password"])
|
||||
|
||||
port = valid_port_or_raise(self._get_port())
|
||||
self._get_repo(image, ("bitnami/redis"))
|
||||
|
||||
c = self._render_instance.add_container(name, image)
|
||||
c.set_user(1001, 0)
|
||||
@@ -58,6 +59,17 @@ class RedisContainer:
|
||||
def _get_port(self):
|
||||
return self._config.get("port") or 6379
|
||||
|
||||
def _get_repo(self, image, supported_repos):
|
||||
images = self._render_instance.values["images"]
|
||||
if image not in images:
|
||||
raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
|
||||
repo = images[image].get("repository")
|
||||
if not repo:
|
||||
raise RenderError("Could not determine repo")
|
||||
if repo not in supported_repos:
|
||||
raise RenderError(f"Unsupported repo [{repo}] for redis. Supported repos: {', '.join(supported_repos)}")
|
||||
return repo
|
||||
|
||||
def get_url(self, variant: str):
|
||||
addr = f"{self._name}:{self._get_port()}"
|
||||
password = urllib.parse.quote_plus(self._config["password"])
|
||||
@@ -44,18 +44,19 @@ class Functions:
|
||||
return string.title()
|
||||
|
||||
def _auto_cast(self, value):
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
lower_str_value = str(value).lower()
|
||||
if lower_str_value in ["true", "false"]:
|
||||
return lower_str_value == "true"
|
||||
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if value.lower() in ["true", "false"]:
|
||||
return value.lower() == "true"
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return value
|
||||
|
||||
@@ -99,6 +100,23 @@ class Functions:
|
||||
return default
|
||||
return value
|
||||
|
||||
def _require_unique(self, values, key, split_char=""):
|
||||
new_values = []
|
||||
for value in values:
|
||||
new_values.append(value.split(split_char)[0] if split_char else value)
|
||||
|
||||
if len(new_values) != len(set(new_values)):
|
||||
raise RenderError(f"Expected values in [{key}] to be unique, but got [{', '.join(values)}]")
|
||||
|
||||
def _require_no_reserved(self, values, key, reserved, split_char=""):
|
||||
new_values = []
|
||||
for value in values:
|
||||
new_values.append(value.split(split_char)[0] if split_char else value)
|
||||
|
||||
for reserved_value in reserved:
|
||||
if reserved_value in new_values:
|
||||
raise RenderError(f"Value [{reserved_value}] is reserved and cannot be set in [{key}]")
|
||||
|
||||
def _temp_config(self, name):
|
||||
if not name:
|
||||
raise RenderError("Expected [name] to be set when calling [temp_config].")
|
||||
@@ -126,7 +144,6 @@ class Functions:
|
||||
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,
|
||||
@@ -146,4 +163,6 @@ class Functions:
|
||||
"get_host_path": self._get_host_path,
|
||||
"or_default": self._or_default,
|
||||
"temp_config": self._temp_config,
|
||||
"require_unique": self._require_unique,
|
||||
"require_no_reserved": self._require_no_reserved,
|
||||
}
|
||||
@@ -17,10 +17,11 @@ class Healthcheck:
|
||||
def __init__(self, render_instance: "Render"):
|
||||
self._render_instance = render_instance
|
||||
self._test: str | list[str] = ""
|
||||
self._interval_sec: int = 10
|
||||
self._interval_sec: int = 30
|
||||
self._timeout_sec: int = 5
|
||||
self._retries: int = 30
|
||||
self._start_period_sec: int = 10
|
||||
self._retries: int = 5
|
||||
self._start_period_sec: int = 15
|
||||
self._start_interval_sec: int = 2
|
||||
self._disabled: bool = False
|
||||
self._use_built_in: bool = False
|
||||
|
||||
@@ -57,6 +58,9 @@ class Healthcheck:
|
||||
def set_start_period(self, start_period: int):
|
||||
self._start_period_sec = start_period
|
||||
|
||||
def set_start_interval(self, start_interval: int):
|
||||
self._start_interval_sec = start_interval
|
||||
|
||||
def has_healthcheck(self):
|
||||
return not self._use_built_in
|
||||
|
||||
@@ -72,10 +76,11 @@ class Healthcheck:
|
||||
|
||||
return {
|
||||
"test": self._get_test(),
|
||||
"retries": self._retries,
|
||||
"interval": f"{self._interval_sec}s",
|
||||
"timeout": f"{self._timeout_sec}s",
|
||||
"retries": self._retries,
|
||||
"start_period": f"{self._start_period_sec}s",
|
||||
"start_interval": f"{self._start_interval_sec}s",
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +104,7 @@ def test_mapping(variant: str, config: dict | None = None) -> str:
|
||||
|
||||
|
||||
def get_key(config: dict, key: str, default: Any, required: bool):
|
||||
if not config.get(key):
|
||||
if key not in config:
|
||||
if not required:
|
||||
return default
|
||||
raise RenderError(f"Expected [{key}] to be set")
|
||||
@@ -137,6 +142,7 @@ def wget_test(config: dict) -> str:
|
||||
scheme = get_key(config, "scheme", "http", False)
|
||||
host = get_key(config, "host", "127.0.0.1", False)
|
||||
headers = get_key(config, "headers", [], False)
|
||||
spider = get_key(config, "spider", True, False)
|
||||
|
||||
opts = []
|
||||
if scheme == "https":
|
||||
@@ -147,7 +153,8 @@ def wget_test(config: dict) -> str:
|
||||
raise RenderError("Expected [header] to be a list of two items for wget test")
|
||||
opts.append(f'--header "{header[0]}: {header[1]}"')
|
||||
|
||||
cmd = "wget --spider --quiet"
|
||||
cmd = f"wget --quiet {'--spider' if spider else '-O /dev/null'}"
|
||||
|
||||
if opts:
|
||||
cmd += f" {' '.join(opts)}"
|
||||
cmd += f" {scheme}://{host}:{port}{path}"
|
||||
@@ -11,6 +11,7 @@ class Notes:
|
||||
self._app_train: str = ""
|
||||
self._warnings: list[str] = []
|
||||
self._deprecations: list[str] = []
|
||||
self._security: dict[str, list[str]] = {}
|
||||
self._header: str = ""
|
||||
self._body: str = ""
|
||||
self._footer: str = ""
|
||||
@@ -47,13 +48,44 @@ class Notes:
|
||||
def add_warning(self, warning: str):
|
||||
self._warnings.append(warning)
|
||||
|
||||
def _prepend_warning(self, warning: str):
|
||||
self._warnings.insert(0, warning)
|
||||
|
||||
def add_deprecation(self, deprecation: str):
|
||||
self._deprecations.append(deprecation)
|
||||
|
||||
def set_body(self, body: str):
|
||||
self._body = body
|
||||
|
||||
def scan_containers(self):
|
||||
for name, c in self._render_instance._containers.items():
|
||||
if self._security.get(name) is None:
|
||||
self._security[name] = []
|
||||
|
||||
if c._privileged:
|
||||
self._security[name].append("Is running with privileged mode enabled")
|
||||
if c._user.startswith("0:"):
|
||||
self._security[name].append("Is running as root user")
|
||||
if c._user.endswith(":0"):
|
||||
self._security[name].append("Is running as root group")
|
||||
if c._ipc_mode == "host":
|
||||
self._security[name].append("Is running with host IPC namespace")
|
||||
if c._cgroup == "host":
|
||||
self._security[name].append("Is running with host cgroup namespace")
|
||||
if "no-new-privileges=true" not in c._security_opt.render():
|
||||
self._security[name].append("Is running without [no-new-privileges] security option")
|
||||
if c._tty:
|
||||
self._prepend_warning(
|
||||
f"Container [{name}] is running with a TTY, "
|
||||
"Logs will not appear correctly in the UI due to an [upstream bug]"
|
||||
"(https://github.com/docker/docker-py/issues/1394)"
|
||||
)
|
||||
|
||||
self._security = {k: v for k, v in self._security.items() if v}
|
||||
|
||||
def render(self):
|
||||
self.scan_containers()
|
||||
|
||||
result = self._header
|
||||
|
||||
if self._warnings:
|
||||
@@ -68,6 +100,14 @@ class Notes:
|
||||
result += f"- {deprecation}\n"
|
||||
result += "\n"
|
||||
|
||||
if self._security:
|
||||
result += "## Security\n\n"
|
||||
for c_name, security in self._security.items():
|
||||
result += "### " + c_name + "\n\n"
|
||||
for s in security:
|
||||
result += f"- {s}\n"
|
||||
result += "\n"
|
||||
|
||||
if self._body:
|
||||
result += self._body.strip() + "\n\n"
|
||||
|
||||
@@ -23,6 +23,14 @@ def test_build_image_with_from(mock_values):
|
||||
c1.build_image(["FROM test_image"])
|
||||
|
||||
|
||||
def test_build_image_with_from_with_whitespace(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.build_image([" FROM test_image"])
|
||||
|
||||
|
||||
def test_build_image(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
@@ -394,3 +394,20 @@ def test_set_ipc_mode_with_container_ipc_mode_and_invalid_container(mock_values)
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.set_ipc_mode("container:invalid")
|
||||
|
||||
|
||||
def test_set_cgroup(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.set_cgroup("host")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["cgroup"] == "host"
|
||||
|
||||
|
||||
def test_set_cgroup_invalid(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.set_cgroup("invalid")
|
||||
@@ -22,12 +22,32 @@ def test_add_postgres_missing_config(mock_values):
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
render.deps.postgres(
|
||||
"test_container",
|
||||
"pg_container",
|
||||
"test_image",
|
||||
{"user": "test_user", "password": "test_password", "database": "test_database"}, # type: ignore
|
||||
)
|
||||
|
||||
|
||||
def test_add_postgres_unsupported_repo(mock_values):
|
||||
mock_values["images"]["pg_image"] = {"repository": "unsupported_repo", "tag": "16"}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
perms_container = render.deps.perms("perms_container")
|
||||
with pytest.raises(Exception):
|
||||
render.deps.postgres(
|
||||
"pg_container",
|
||||
"pg_image",
|
||||
{
|
||||
"user": "test_user",
|
||||
"password": "test_@password",
|
||||
"database": "test_database",
|
||||
"volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
|
||||
},
|
||||
perms_container,
|
||||
)
|
||||
|
||||
|
||||
def test_add_postgres(mock_values):
|
||||
mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": "16"}
|
||||
render = Render(mock_values)
|
||||
@@ -60,10 +80,11 @@ def test_add_postgres(mock_values):
|
||||
assert output["services"]["pg_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
|
||||
assert output["services"]["pg_container"]["healthcheck"] == {
|
||||
"test": "pg_isready -h 127.0.0.1 -p 5432 -U $$POSTGRES_USER -d $$POSTGRES_DB",
|
||||
"interval": "10s",
|
||||
"interval": "30s",
|
||||
"timeout": "5s",
|
||||
"retries": 30,
|
||||
"start_period": "10s",
|
||||
"retries": 5,
|
||||
"start_period": "15s",
|
||||
"start_interval": "2s",
|
||||
}
|
||||
assert output["services"]["pg_container"]["volumes"] == [
|
||||
{
|
||||
@@ -82,7 +103,7 @@ def test_add_postgres(mock_values):
|
||||
"POSTGRES_USER": "test_user",
|
||||
"POSTGRES_PASSWORD": "test_@password",
|
||||
"POSTGRES_DB": "test_database",
|
||||
"POSTGRES_PORT": "5432",
|
||||
"PGPORT": "5432",
|
||||
}
|
||||
assert output["services"]["pg_container"]["depends_on"] == {
|
||||
"perms_container": {"condition": "service_completed_successfully"},
|
||||
@@ -97,12 +118,30 @@ def test_add_redis_missing_config(mock_values):
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
render.deps.redis(
|
||||
"test_container",
|
||||
"redis_container",
|
||||
"test_image",
|
||||
{"password": "test_password", "volume": {}}, # type: ignore
|
||||
)
|
||||
|
||||
|
||||
def test_add_redis_unsupported_repo(mock_values):
|
||||
mock_values["images"]["redis_image"] = {"repository": "unsupported_repo", "tag": "latest"}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
perms_container = render.deps.perms("perms_container")
|
||||
with pytest.raises(Exception):
|
||||
render.deps.redis(
|
||||
"redis_container",
|
||||
"redis_image",
|
||||
{
|
||||
"password": "test&password@",
|
||||
"volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
|
||||
},
|
||||
perms_container,
|
||||
)
|
||||
|
||||
|
||||
def test_add_redis_with_password_with_spaces(mock_values):
|
||||
mock_values["images"]["redis_image"] = {"repository": "redis", "tag": "latest"}
|
||||
render = Render(mock_values)
|
||||
@@ -110,8 +149,8 @@ def test_add_redis_with_password_with_spaces(mock_values):
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
render.deps.redis(
|
||||
"test_container",
|
||||
"test_image",
|
||||
"redis_container",
|
||||
"redis_image",
|
||||
{"password": "test password", "volume": {}}, # type: ignore
|
||||
)
|
||||
|
||||
@@ -148,10 +187,11 @@ def test_add_redis(mock_values):
|
||||
assert output["services"]["redis_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
|
||||
assert output["services"]["redis_container"]["healthcheck"] == {
|
||||
"test": "redis-cli -h 127.0.0.1 -p 6379 -a $$REDIS_PASSWORD ping | grep -q PONG",
|
||||
"interval": "10s",
|
||||
"interval": "30s",
|
||||
"timeout": "5s",
|
||||
"retries": 30,
|
||||
"start_period": "10s",
|
||||
"retries": 5,
|
||||
"start_period": "15s",
|
||||
"start_interval": "2s",
|
||||
}
|
||||
assert output["services"]["redis_container"]["volumes"] == [
|
||||
{
|
||||
@@ -182,12 +222,32 @@ def test_add_mariadb_missing_config(mock_values):
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
render.deps.mariadb(
|
||||
"test_container",
|
||||
"mariadb_container",
|
||||
"test_image",
|
||||
{"user": "test_user", "password": "test_password", "database": "test_database"}, # type: ignore
|
||||
)
|
||||
|
||||
|
||||
def test_add_mariadb_unsupported_repo(mock_values):
|
||||
mock_values["images"]["mariadb_image"] = {"repository": "unsupported_repo", "tag": "latest"}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
perms_container = render.deps.perms("perms_container")
|
||||
with pytest.raises(Exception):
|
||||
render.deps.mariadb(
|
||||
"mariadb_container",
|
||||
"mariadb_image",
|
||||
{
|
||||
"user": "test_user",
|
||||
"password": "test_password",
|
||||
"database": "test_database",
|
||||
"volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
|
||||
},
|
||||
perms_container,
|
||||
)
|
||||
|
||||
|
||||
def test_add_mariadb(mock_values):
|
||||
mock_values["images"]["mariadb_image"] = {"repository": "mariadb", "tag": "latest"}
|
||||
render = Render(mock_values)
|
||||
@@ -217,10 +277,11 @@ def test_add_mariadb(mock_values):
|
||||
assert output["services"]["mariadb_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
|
||||
assert output["services"]["mariadb_container"]["healthcheck"] == {
|
||||
"test": "mariadb-admin --user=root --host=127.0.0.1 --port=3306 --password=$$MARIADB_ROOT_PASSWORD ping",
|
||||
"interval": "10s",
|
||||
"interval": "30s",
|
||||
"timeout": "5s",
|
||||
"retries": 30,
|
||||
"start_period": "10s",
|
||||
"retries": 5,
|
||||
"start_period": "15s",
|
||||
"start_interval": "2s",
|
||||
}
|
||||
assert output["services"]["mariadb_container"]["volumes"] == [
|
||||
{
|
||||
@@ -401,8 +462,8 @@ def test_add_postgres_with_invalid_tag(mock_values):
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
render.deps.postgres(
|
||||
"test_container",
|
||||
"test_image",
|
||||
"pg_container",
|
||||
"pg_image",
|
||||
{"user": "test_user", "password": "test_password", "database": "test_database"}, # type: ignore
|
||||
)
|
||||
|
||||
@@ -25,6 +25,10 @@ def test_funcs(mock_values):
|
||||
|
||||
tests = [
|
||||
{"func": "auto_cast", "values": ["1"], "expected": 1},
|
||||
{"func": "auto_cast", "values": ["TrUe"], "expected": True},
|
||||
{"func": "auto_cast", "values": ["FaLsE"], "expected": False},
|
||||
{"func": "auto_cast", "values": ["0.2"], "expected": 0.2},
|
||||
{"func": "auto_cast", "values": [True], "expected": True},
|
||||
{"func": "basic_auth_header", "values": ["my_user", "my_pass"], "expected": "Basic bXlfdXNlcjpteV9wYXNz"},
|
||||
{"func": "basic_auth", "values": ["my_user", "my_pass"], "expected": "bXlfdXNlcjpteV9wYXNz"},
|
||||
{
|
||||
@@ -72,6 +76,22 @@ def test_funcs(mock_values):
|
||||
"values": ["test"],
|
||||
"expected": {"type": "temporary", "volume_config": {"volume_name": "test"}},
|
||||
},
|
||||
{"func": "require_unique", "values": [["a=1", "b=2", "c"], "values.key", "="], "expected": None},
|
||||
{
|
||||
"func": "require_unique",
|
||||
"values": [["a=1", "b=2", "b=3"], "values.key", "="],
|
||||
"expect_raise": True,
|
||||
},
|
||||
{
|
||||
"func": "require_no_reserved",
|
||||
"values": [["a=1", "b=2", "c"], "values.key", ["d"], "="],
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"func": "require_no_reserved",
|
||||
"values": [["a=1", "b=2", "c"], "values.key", ["a"], "="],
|
||||
"expect_raise": True,
|
||||
},
|
||||
]
|
||||
|
||||
for test in tests:
|
||||
@@ -38,10 +38,11 @@ def test_set_custom_test(mock_values):
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["healthcheck"] == {
|
||||
"test": "echo $$1",
|
||||
"interval": "10s",
|
||||
"interval": "30s",
|
||||
"timeout": "5s",
|
||||
"retries": 30,
|
||||
"start_period": "10s",
|
||||
"retries": 5,
|
||||
"start_period": "15s",
|
||||
"start_interval": "2s",
|
||||
}
|
||||
|
||||
|
||||
@@ -52,10 +53,11 @@ def test_set_custom_test_array(mock_values):
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["healthcheck"] == {
|
||||
"test": ["CMD", "echo", "$$1"],
|
||||
"interval": "10s",
|
||||
"interval": "30s",
|
||||
"timeout": "5s",
|
||||
"retries": 30,
|
||||
"start_period": "10s",
|
||||
"retries": 5,
|
||||
"start_period": "15s",
|
||||
"start_interval": "2s",
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +69,7 @@ def test_set_options(mock_values):
|
||||
c1.healthcheck.set_timeout(8)
|
||||
c1.healthcheck.set_retries(7)
|
||||
c1.healthcheck.set_start_period(6)
|
||||
c1.healthcheck.set_start_interval(5)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["healthcheck"] == {
|
||||
"test": ["CMD", "echo", "$$1"],
|
||||
@@ -74,6 +77,7 @@ def test_set_options(mock_values):
|
||||
"timeout": "8s",
|
||||
"retries": 7,
|
||||
"start_period": "6s",
|
||||
"start_interval": "5s",
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +143,18 @@ def test_wget_healthcheck(mock_values):
|
||||
output = render.render()
|
||||
assert (
|
||||
output["services"]["test_container"]["healthcheck"]["test"]
|
||||
== "wget --spider --quiet http://127.0.0.1:8080/health"
|
||||
== "wget --quiet --spider http://127.0.0.1:8080/health"
|
||||
)
|
||||
|
||||
|
||||
def test_wget_healthcheck_no_spider(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.set_test("wget", {"port": 8080, "path": "/health", "spider": False})
|
||||
output = render.render()
|
||||
assert (
|
||||
output["services"]["test_container"]["healthcheck"]["test"]
|
||||
== "wget --quiet -O /dev/null http://127.0.0.1:8080/health"
|
||||
)
|
||||
|
||||
|
||||
@@ -155,6 +155,12 @@ some other info.
|
||||
)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.set_privileged(True)
|
||||
c1.set_user(0, 0)
|
||||
c1.set_ipc_mode("host")
|
||||
c1.set_cgroup("host")
|
||||
c1.set_tty(True)
|
||||
c1.remove_security_opt("no-new-privileges")
|
||||
output = render.render()
|
||||
assert (
|
||||
output["x-notes"]
|
||||
@@ -162,6 +168,7 @@ some other info.
|
||||
|
||||
## Warnings
|
||||
|
||||
- Container [test_container] is running with a TTY, Logs will not appear correctly in the UI due to an [upstream bug](https://github.com/docker/docker-py/issues/1394)
|
||||
- this is not properly configured. fix it now!
|
||||
- that is not properly configured. fix it later!
|
||||
|
||||
@@ -170,6 +177,17 @@ some other info.
|
||||
- this is will be removed later. fix it now!
|
||||
- that is will be removed later. fix it later!
|
||||
|
||||
## Security
|
||||
|
||||
### test_container
|
||||
|
||||
- Is running with privileged mode enabled
|
||||
- Is running as root user
|
||||
- Is running as root group
|
||||
- Is running with host IPC namespace
|
||||
- Is running with host cgroup namespace
|
||||
- Is running without [no-new-privileges] security option
|
||||
|
||||
## Additional info
|
||||
|
||||
Some info
|
||||
@@ -180,5 +198,5 @@ some other info.
|
||||
If you find a bug in this app or have an idea for a new feature, please file an issue at
|
||||
https://ixsystems.atlassian.net
|
||||
|
||||
"""
|
||||
""" # noqa
|
||||
)
|
||||
@@ -157,6 +157,13 @@ def valid_cgroup_perm_or_raise(cgroup_perm: str):
|
||||
return cgroup_perm
|
||||
|
||||
|
||||
def valid_cgroup_or_raise(cgroup: str):
|
||||
valid_cgroup = ("host", "private")
|
||||
if cgroup not in valid_cgroup:
|
||||
raise RenderError(f"Cgroup [{cgroup}] is not valid. Valid options are: [{', '.join(valid_cgroup)}]")
|
||||
return cgroup
|
||||
|
||||
|
||||
def valid_device_cgroup_rule_or_raise(dev_grp_rule: str):
|
||||
parts = dev_grp_rule.split(" ")
|
||||
if len(parts) != 3:
|
||||
@@ -5,6 +5,11 @@ resources:
|
||||
|
||||
metube:
|
||||
default_theme: auto
|
||||
ytdl_options:
|
||||
- key: forcejson
|
||||
value: true
|
||||
- key: cookiefile
|
||||
value: /tmp/cookies.txt
|
||||
additional_envs: []
|
||||
network:
|
||||
host_network: false
|
||||
|
||||
Reference in New Issue
Block a user