Publish new changes in catalog [skip ci]

This commit is contained in:
sonicaj
2025-11-17 16:27:28 +00:00
parent 065096f5f6
commit 9b9935252c
78 changed files with 316 additions and 15 deletions

View File

@@ -13,8 +13,8 @@ icon: https://media.sys.truenas.net/apps/rust-desk/icons/icon.png
keywords:
- remote
- desktop
lib_version: 2.1.57
lib_version_hash: 6659ad369fff2f1df318cb6353bb32bffe7047dc21df4823750d39be7284e605
lib_version: 2.1.63
lib_version_hash: ae27738f46453ea3e6b964592b2e8e1fea7adb0faf5808ffc5eea4690e6368e7
maintainers:
- email: dev@truenas.com
name: truenas
@@ -32,4 +32,4 @@ sources:
- https://github.com/rustdesk/rustdesk-server
title: Rust Desk
train: community
version: 1.2.11
version: 1.3.0

View File

@@ -2,6 +2,9 @@ images:
image:
repository: rustdesk/rustdesk-server
tag: 1.1.14
pro_image:
repository: rustdesk/rustdesk-server-pro
tag: 1.7.1
consts:
rust_desk_container_name: rust-desk

View File

@@ -19,6 +19,29 @@ questions:
schema:
type: dict
attrs:
- variable: image_selector
label: Image Selector
description: The image selector to use for Scrypted.
schema:
type: string
default: image
enum:
- value: image
description: OSS Image
- value: pro_image
description: Pro Image - Requires a paid license
- variable: mac_address
label: MAC Address
description: |
For licensing purposes, Rust Desk needs the MAC Address to tie the license to your server.</br>
Format: XX:XX:XX:XX:XX:XX
schema:
type: string
default: ""
min_length: 17
max_length: 17
show_if: [["image_selector", "=", "pro_image"]]
required: true
- variable: allow_only_encrypted_connections
label: Allow Only Encrypted Connections
description: |
@@ -100,6 +123,59 @@ questions:
schema:
type: boolean
default: false
- variable: webui_port
label: Port for the webui
description: |
This port will be used to connect to the webui.</br>
The internal port will always be 21114.</br>
This is only available in the PRO version.
schema:
type: dict
show_if: [["host_network", "=", false]]
attrs:
- variable: bind_mode
label: Port Bind Mode
description: |
The port bind mode.</br>
- Publish: The port will be published on the host for external access.</br>
- Expose: The port will be exposed for inter-container communication.</br>
- None: The port will not be exposed or published.</br>
Note: If the Dockerfile defines an EXPOSE directive,
the port will still be exposed for inter-container communication regardless of this setting.
schema:
type: string
default: ""
enum:
- value: "published"
description: Publish port on the host for external access
- value: "exposed"
description: Expose port for inter-container communication
- value: ""
description: None
- variable: port_number
label: Port Number
schema:
type: int
show_if: [["bind_mode", "=", "published"]]
default: 21114
min: 1
max: 65535
required: true
- variable: host_ips
label: Host IPs
description: IPs on the host to bind this port
schema:
type: list
show_if: [["bind_mode", "=", "published"]]
default: []
items:
- variable: host_ip
label: Host IP
schema:
type: string
required: true
$ref:
- definitions/node_bind_ip
- variable: nat_type_test_port
label: NAT Type Test Port
description: |

View File

@@ -1,11 +1,18 @@
{% set tpl = ix_lib.base.render.Render(values) %}
{% set server = tpl.add_container(values.consts.rust_desk_container_name, "image") %}
{% set relay = tpl.add_container(values.consts.rust_desk_relay_container_name, "image") %}
{% set server = tpl.add_container(values.consts.rust_desk_container_name, values.rust_desk.image_selector) %}
{% set relay = tpl.add_container(values.consts.rust_desk_relay_container_name, values.rust_desk.image_selector) %}
{% set is_pro = values.rust_desk.image_selector == "pro_image" %}
{% set perm_container = tpl.deps.perms(values.consts.perms_container_name) %}
{% set perms_config = {"uid": values.run_as.user, "gid": values.run_as.group, "mode": "check"} %}
{% if is_pro %}
{% do server.set_mac(values.rust_desk.mac_address) %}
{% do relay.set_mac(values.rust_desk.mac_address) %}
{% endif %}
{% do server.set_user(values.run_as.user, values.run_as.group) %}
{% do relay.set_user(values.run_as.user, values.run_as.group) %}
@@ -29,6 +36,7 @@
{% do relay.environment.add_user_envs(values.rust_desk.additional_envs) %}
{% if not values.network.host_network %}
{% if is_pro %}{% do server.add_port(values.network.webui_port, {"container_port": 21114}) %}{% endif %}
{% do server.add_port(values.network.nat_type_test_port, {"container_port": 21115}) %}
{% do server.add_port(values.network.id_reg_hole_punch_port, {"container_port": 21116}) %}
{% do server.add_port(values.network.id_reg_hole_punch_port, {"container_port": 21116, "protocol": "udp"}) %}
@@ -59,4 +67,6 @@
{% do relay.depends.add_dependency(values.consts.perms_container_name, "service_completed_successfully") %}
{% endif %}
{% if is_pro %}{% do tpl.portals.add(values.network.web_port) %}{% endif %}
{{ tpl.render() | tojson }}

View File

@@ -25,6 +25,7 @@ try:
valid_cap_or_raise,
valid_cgroup_or_raise,
valid_ipc_mode_or_raise,
valid_mac_or_raise,
valid_network_mode_or_raise,
valid_pid_mode_or_raise,
valid_port_bind_mode_or_raise,
@@ -55,6 +56,7 @@ except ImportError:
valid_cap_or_raise,
valid_cgroup_or_raise,
valid_ipc_mode_or_raise,
valid_mac_or_raise,
valid_network_mode_or_raise,
valid_pid_mode_or_raise,
valid_port_bind_mode_or_raise,
@@ -95,6 +97,7 @@ class Container:
self._tmpfs: Tmpfs = Tmpfs(self._render_instance, self)
self._ipc_mode: str | None = None
self._pid_mode: str | None = None
self.mac_address: 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)
@@ -223,6 +226,10 @@ class Container:
def set_cgroup(self, cgroup: str):
self._cgroup = valid_cgroup_or_raise(cgroup)
def set_mac(self, mac_address: str):
valid_mac_or_raise(mac_address)
self.mac_address = mac_address
def set_init(self, enabled: bool = False):
self._init = enabled
@@ -377,6 +384,9 @@ class Container:
if self._cgroup is not None:
result["cgroup"] = self._cgroup
if self.mac_address is not None:
result["mac_address"] = self.mac_address
if self._extra_hosts.has_hosts():
result["extra_hosts"] = self._extra_hosts.render()

View File

@@ -12,6 +12,7 @@ try:
from .deps_postgres import PostgresContainer, PostgresConfig
from .deps_redis import RedisContainer, RedisConfig
from .deps_solr import SolrContainer, SolrConfig
from .deps_tika import TikaContainer, TikaConfig
except ImportError:
from deps_elastic import ElasticSearchContainer, ElasticConfig
from deps_mariadb import MariadbContainer, MariadbConfig
@@ -21,6 +22,7 @@ except ImportError:
from deps_postgres import PostgresContainer, PostgresConfig
from deps_redis import RedisContainer, RedisConfig
from deps_solr import SolrContainer, SolrConfig
from deps_tika import TikaContainer, TikaConfig
class Deps:
@@ -50,3 +52,6 @@ class Deps:
def solr(self, name: str, image: str, config: SolrConfig, perms_instance: PermsContainer):
return SolrContainer(self._render_instance, name, image, config, perms_instance)
def tika(self, name: str, image: str, config: TikaConfig):
return TikaContainer(self._render_instance, name, image, config)

View File

@@ -37,7 +37,13 @@ class MongoDBContainer:
c = self._render_instance.add_container(name, image)
c.set_user(999, 999)
user, group = 568, 568
run_as = self._render_instance.values.get("run_as")
if run_as:
user = run_as["user"] or user # Avoids running as root
group = run_as["group"] or group # Avoids running as root
c.set_user(user, group)
c.healthcheck.set_test("mongodb", {"db": config["database"]})
c.remove_devices()
c.add_storage(self._data_dir, config["volume"])
@@ -47,7 +53,7 @@ class MongoDBContainer:
c.environment.add_env("MONGO_INITDB_DATABASE", config["database"])
perms_instance.add_or_skip_action(
f"{self._name}_mongodb_data", config["volume"], {"uid": 999, "gid": 999, "mode": "check"}
f"{self._name}_mongodb_data", config["volume"], {"uid": user, "gid": group, "mode": "check"}
)
self._get_repo(image, ("mongodb"))

View File

@@ -0,0 +1,63 @@
from typing import TYPE_CHECKING, TypedDict, NotRequired
if TYPE_CHECKING:
from render import Render
try:
from .error import RenderError
except ImportError:
from error import RenderError
class TikaConfig(TypedDict):
port: NotRequired[int]
class TikaContainer:
def __init__(self, render_instance: "Render", name: str, image: str, config: TikaConfig):
self._render_instance = render_instance
self._name = name
self._config = config
c = self._render_instance.add_container(name, image)
user, group = 568, 568
run_as = self._render_instance.values.get("run_as")
if run_as:
user = run_as["user"] or user # Avoids running as root
group = run_as["group"] or group # Avoids running as root
c.set_user(user, group)
c.healthcheck.set_test("wget", {"port": self.get_port(), "path": "/tika", "spider": False})
c.remove_devices()
c.set_command(["--port", str(self.get_port())])
self._get_repo(image, ("apache/tika"))
# Store container for further configuration
# For example: c.depends.add_dependency("other_container", "service_started")
self._container = c
@property
def container(self):
return self._container
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 tika. Supported repos: {', '.join(supported_repos)}")
return repo
def get_port(self):
return self._config.get("port") or 9998
def get_url(self):
return f"http://{self._name}:{self.get_port()}"

View File

@@ -24,6 +24,7 @@ class Environment:
self._app_dev_variables: dict[str, Any] = {}
self._skip_generic_variables: bool = render_instance.values.get("skip_generic_variables", False)
self._skip_id_variables: bool = render_instance.values.get("skip_id_variables", False)
self._auto_add_variables_from_values()
@@ -65,6 +66,12 @@ class Environment:
value = value.lower()
return value
def remove_auto_env(self, name: str):
if name in self._auto_variables.keys():
del self._auto_variables[name]
return
raise RenderError(f"Environment variable [{name}] is not defined.")
def add_env(self, name: str, value: Any):
if not name:
raise RenderError(f"Environment variable name cannot be empty. [{name}]")

View File

@@ -1,5 +1,6 @@
import re
import copy
import yaml
import bcrypt
import secrets
import urllib.parse
@@ -21,6 +22,9 @@ class Functions:
def __init__(self, render_instance: "Render"):
self._render_instance = render_instance
def _to_yaml(self, data):
return yaml.dump(data)
def _bcrypt_hash(self, password):
hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
return hashed
@@ -120,6 +124,7 @@ class Functions:
"scheme": parsed.scheme,
"host": parsed.hostname,
"port": parsed.port,
"path": parsed.path,
}
if v6_brackets and parsed.hostname and ":" in parsed.hostname:
result["host"] = f"[{parsed.hostname}]"
@@ -209,4 +214,5 @@ class Functions:
"require_no_reserved": self._require_no_reserved,
"url_encode": self._url_encode,
"url_to_dict": self._url_to_dict,
"to_yaml": self._to_yaml,
}

View File

@@ -458,6 +458,23 @@ def test_set_cgroup_invalid(mock_values):
c1.set_cgroup("invalid")
def test_set_mac_invalid(mock_values):
render = Render(mock_values)
c1 = render.add_container("test_container", "test_image")
c1.healthcheck.disable()
with pytest.raises(Exception):
c1.set_mac("invalid_mac")
def test_set_mac_valid(mock_values):
render = Render(mock_values)
c1 = render.add_container("test_container", "test_image")
c1.healthcheck.disable()
c1.set_mac("00:00:00:00:00:00")
output = render.render()
assert output["services"]["test_container"]["mac_address"] == "00:00:00:00:00:00"
def test_setup_as_helper(mock_values):
mock_values["resources"] = {"gpus": {"use_all_gpus": True}}
render = Render(mock_values)

View File

@@ -620,7 +620,7 @@ def test_add_mongodb(mock_values):
assert "devices" not in output["services"]["mongodb_container"]
assert "reservations" not in output["services"]["mongodb_container"]["deploy"]["resources"]
assert output["services"]["mongodb_container"]["image"] == "mongodb:latest"
assert output["services"]["mongodb_container"]["user"] == "999:999"
assert output["services"]["mongodb_container"]["user"] == "568:568"
assert output["services"]["mongodb_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
assert output["services"]["mongodb_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
assert output["services"]["mongodb_container"]["healthcheck"] == {
@@ -952,3 +952,59 @@ def test_add_solr_unsupported_repo(mock_values):
},
perms_container,
)
def test_add_tika(mock_values):
mock_values["images"]["tika_image"] = {"repository": "apache/tika", "tag": "3.2.3.0-full"}
render = Render(mock_values)
c1 = render.add_container("test_container", "test_image")
c1.healthcheck.disable()
render.deps.tika(
"tika_container",
"tika_image",
{
"port": 10999,
},
)
output = render.render()
assert "devices" not in output["services"]["tika_container"]
assert "reservations" not in output["services"]["tika_container"]["deploy"]["resources"]
assert output["services"]["tika_container"]["image"] == "apache/tika:3.2.3.0-full"
assert output["services"]["tika_container"]["user"] == "568:568"
assert output["services"]["tika_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
assert output["services"]["tika_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
assert output["services"]["tika_container"]["healthcheck"] == {
"test": [
"CMD",
"wget",
"--quiet",
"-O",
"/dev/null",
"http://127.0.0.1:10999/tika",
],
"interval": "30s",
"timeout": "5s",
"retries": 5,
"start_period": "15s",
"start_interval": "2s",
}
assert output["services"]["tika_container"]["environment"] == {
"TZ": "Etc/UTC",
"UMASK": "002",
"UMASK_SET": "002",
"NVIDIA_VISIBLE_DEVICES": "void",
}
assert output["services"]["tika_container"]["command"] == ["--port", "10999"]
def test_add_tika_unsupported_repo(mock_values):
mock_values["images"]["tika_image"] = {"repository": "unsupported_repo", "tag": "7"}
render = Render(mock_values)
c1 = render.add_container("test_container", "test_image")
c1.healthcheck.disable()
with pytest.raises(Exception):
render.deps.tika(
"tika_container",
"tika_image",
{},
)

View File

@@ -49,6 +49,7 @@ def test_auto_add_vars(mock_values):
def test_skip_generic_variables(mock_values):
mock_values["skip_generic_variables"] = True
mock_values["run_as"] = {"user": "1000", "group": "1000"}
render = Render(mock_values)
c1 = render.add_container("test_container", "test_image")
c1.healthcheck.disable()
@@ -56,7 +57,29 @@ def test_skip_generic_variables(mock_values):
envs = output["services"]["test_container"]["environment"]
assert len(envs) == 1
assert envs["NVIDIA_VISIBLE_DEVICES"] == "void"
assert envs == {
"NVIDIA_VISIBLE_DEVICES": "void",
}
def test_remove_auto_env(mock_values):
mock_values["run_as"] = {"user": "1000", "group": "1000"}
render = Render(mock_values)
c1 = render.add_container("test_container", "test_image")
c1.healthcheck.disable()
c1.environment.remove_auto_env("UID")
output = render.render()
envs = output["services"]["test_container"]["environment"]
assert "UID" not in envs
def test_remove_env_not_defined(mock_values):
render = Render(mock_values)
c1 = render.add_container("test_container", "test_image")
c1.healthcheck.disable()
with pytest.raises(Exception):
c1.environment.remove_auto_env("NOT_DEFINED")
def test_add_from_all_sources(mock_values):

View File

@@ -105,24 +105,36 @@ def test_funcs(mock_values):
{
"func": "url_to_dict",
"values": ["192.168.1.1:8080"],
"expected": {"host": "192.168.1.1", "port": 8080, "scheme": "http", "netloc": "192.168.1.1:8080"},
"expected": {
"host": "192.168.1.1",
"port": 8080,
"scheme": "http",
"netloc": "192.168.1.1:8080",
"path": "",
},
},
{
"func": "url_to_dict",
"values": ["[::]:8080"],
"expected": {"host": "::", "port": 8080, "scheme": "http", "netloc": "[::]:8080"},
"expected": {"host": "::", "port": 8080, "scheme": "http", "netloc": "[::]:8080", "path": ""},
},
{
"func": "url_to_dict",
"values": ["[::]:8080", True],
"values": ["[::]:8080/abc/", True],
"expected": {
"host": "[::]",
"port": 8080,
"host_no_brackets": "::",
"scheme": "http",
"netloc": "[::]:8080",
"path": "/abc/",
},
},
{
"func": "to_yaml",
"values": [{"a": 1, "b": 2}],
"expected": "a: 1\nb: 2\n",
},
]
for test in tests:

View File

@@ -8,6 +8,8 @@ try:
except ImportError:
from error import RenderError
MAC_ADDR_REGEX = re.compile(r"^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$")
OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$")
RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/"))
RESTRICTED: tuple[Path, ...] = (
@@ -24,6 +26,12 @@ RESTRICTED: tuple[Path, ...] = (
)
def valid_mac_or_raise(mac: str):
if MAC_ADDR_REGEX.match(mac):
return mac
raise RenderError(f"Invalid MAC Address [{mac}], valid format is either XX:XX:XX:XX:XX:XX")
def valid_security_opt_or_raise(opt: str):
if ":" in opt or "=" in opt:
raise RenderError(f"Security Option [{opt}] cannot contain [:] or [=]. Pass value as an argument")

View File

@@ -101,9 +101,6 @@ class CifsVolume:
raise RenderError(f"Option [{key}] already added for [cifs] type.")
if key in disallowed_opts:
raise RenderError(f"Option [{key}] is not allowed for [cifs] type.")
for disallowed in disallowed_opts:
if key == disallowed:
raise RenderError(f"Option [{key}] is not allowed for [cifs] type.")
opts.append(opt)
tracked_keys.add(key)
opts.sort()

View File

@@ -4,6 +4,8 @@ resources:
memory: 4096
rust_desk:
image_selector: "image"
# mac_address: "02:42:ac:11:00:02" # Optional MAC address for PRO edition
allow_only_encrypted_connections: false
additional_relay_servers: []
additional_envs: []