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:
Stavros Kois
2025-05-06 11:58:53 +03:00
committed by GitHub
parent 77318537ee
commit 033277ccf7
69 changed files with 332 additions and 46 deletions

View File

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

View File

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

View File

@@ -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) %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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