mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
feat(oci): recipes that say what they ask for
Manyfold, BookStack and NetBox declare the services they need. Lychee and Manyfold install with their own SQLite database. Folding@home, Healthchecks, NetBird, Speedtest Tracker, Twingate Connector, HA Bridge, ChatGPT Next Web, DDNS Updater and Minisatip explain each value, propose one or leave it to the advanced installation. Pinchflat gets 1024 MB.
This commit is contained in:
@@ -109,7 +109,27 @@
|
||||
"owner_strategy": "mapped-root",
|
||||
"only_when_mount_type": "managed-volume"
|
||||
}
|
||||
],
|
||||
"external_services": [
|
||||
"MariaDB"
|
||||
]
|
||||
}
|
||||
},
|
||||
"environment_overrides": {
|
||||
"APP_URL": {
|
||||
"prompt": "Address BookStack will be opened at (for example http://192.168.1.50:6875)"
|
||||
},
|
||||
"DB_HOST": {
|
||||
"prompt": "Address of the MariaDB server"
|
||||
},
|
||||
"DB_USERNAME": {
|
||||
"prompt": "User of the MariaDB server"
|
||||
},
|
||||
"DB_DATABASE": {
|
||||
"prompt": "Name of the database on the MariaDB server"
|
||||
},
|
||||
"DB_PASSWORD": {
|
||||
"prompt": "Password of the MariaDB user"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"environment_overrides": {
|
||||
"CODE": {
|
||||
"required": false,
|
||||
"prompt": "Access password of the web interface (optional)"
|
||||
},
|
||||
"PROXY_URL": {
|
||||
"required": false,
|
||||
"prompt": "Proxy address to reach the API (optional)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"environment_overrides": {
|
||||
"CONFIG": {
|
||||
"required": false,
|
||||
"prompt": "Configuration in JSON on one line (optional)"
|
||||
},
|
||||
"SHOUTRRR_ADDRESSES": {
|
||||
"required": false,
|
||||
"prompt": "Shoutrrr addresses for the notifications (optional)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,5 +10,13 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"environment_overrides": {
|
||||
"ACCOUNT_TOKEN": {
|
||||
"prompt": "Account token from the settings of https://app.foldingathome.org"
|
||||
},
|
||||
"MACHINE_NAME": {
|
||||
"prompt": "Name for this machine in Folding@home (no spaces)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,19 @@
|
||||
"owner_strategy": "mapped-root",
|
||||
"only_when_mount_type": "managed-volume"
|
||||
}
|
||||
]
|
||||
],
|
||||
"generated_sensitive_environment": {
|
||||
"SEC_KEY": {
|
||||
"strategy": "token-hex",
|
||||
"bytes": 16,
|
||||
"prompt": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"environment_overrides": {
|
||||
"SEC_KEY": {
|
||||
"sensitive": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,5 +10,22 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"environment_overrides": {
|
||||
"SECRET_KEY": {
|
||||
"required": false
|
||||
},
|
||||
"SITE_ROOT": {
|
||||
"prompt": "Address Healthchecks will be opened at (for example http://192.168.1.50:8000)"
|
||||
},
|
||||
"SITE_NAME": {
|
||||
"example": "Healthchecks"
|
||||
},
|
||||
"SUPERUSER_EMAIL": {
|
||||
"prompt": "Email address of the superuser account"
|
||||
},
|
||||
"SUPERUSER_PASSWORD": {
|
||||
"prompt": "Password of the superuser account"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,5 +16,30 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"environment_overrides": {
|
||||
"DB_CONNECTION": {
|
||||
"example": "sqlite",
|
||||
"prompt": "Database type (sqlite, mysql or pgsql)"
|
||||
},
|
||||
"DB_DATABASE": {
|
||||
"example": "/config/lychee.sqlite",
|
||||
"prompt": "Database file for sqlite, or database name for mysql and pgsql"
|
||||
},
|
||||
"DB_HOST": {
|
||||
"required": false,
|
||||
"prompt": "Address of the database server (mysql and pgsql only)"
|
||||
},
|
||||
"DB_PORT": {
|
||||
"required": false,
|
||||
"prompt": "Port of the database server (mysql and pgsql only)"
|
||||
},
|
||||
"DB_USERNAME": {
|
||||
"required": false,
|
||||
"prompt": "User of the database server (mysql and pgsql only)"
|
||||
},
|
||||
"DB_PASSWORD": {
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,22 @@
|
||||
"owner_strategy": "mapped-root",
|
||||
"only_when_mount_type": "managed-volume"
|
||||
}
|
||||
],
|
||||
"external_services": [
|
||||
"Redis/Valkey"
|
||||
]
|
||||
}
|
||||
},
|
||||
"environment_overrides": {
|
||||
"DATABASE_URL": {
|
||||
"example": "sqlite3:/config/manyfold.sqlite3",
|
||||
"prompt": "Database (a SQLite file, or postgresql://user:password@address:5432/name)"
|
||||
},
|
||||
"REDIS_URL": {
|
||||
"prompt": "Address of Redis or Valkey (for example redis://192.168.1.20:6379/1)"
|
||||
},
|
||||
"SECRET_KEY_BASE": {
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,5 +10,11 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"environment_overrides": {
|
||||
"RUN_OPTS": {
|
||||
"required": false,
|
||||
"prompt": "Extra parameters for minisatip (optional)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,5 +10,14 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"environment_overrides": {
|
||||
"NB_SETUP_KEY": {
|
||||
"prompt": "Setup key from the NetBird dashboard"
|
||||
},
|
||||
"NB_MANAGEMENT_URL": {
|
||||
"example": "https://api.netbird.io:443",
|
||||
"prompt": "Address of the NetBird management server"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +203,55 @@
|
||||
"owner_strategy": "mapped-root",
|
||||
"only_when_mount_type": "managed-volume"
|
||||
}
|
||||
],
|
||||
"external_services": [
|
||||
"PostgreSQL",
|
||||
"Redis/Valkey"
|
||||
]
|
||||
}
|
||||
},
|
||||
"environment_overrides": {
|
||||
"SUPERUSER_EMAIL": {
|
||||
"prompt": "Email address of the admin account"
|
||||
},
|
||||
"ALLOWED_HOST": {
|
||||
"prompt": "Name or address NetBox will be opened at (for example netbox.example.com)"
|
||||
},
|
||||
"DB_HOST": {
|
||||
"prompt": "Address of the PostgreSQL server"
|
||||
},
|
||||
"DB_PORT": {
|
||||
"example": "5432"
|
||||
},
|
||||
"DB_NAME": {
|
||||
"example": "netbox"
|
||||
},
|
||||
"DB_USER": {
|
||||
"prompt": "User of the PostgreSQL server"
|
||||
},
|
||||
"REDIS_HOST": {
|
||||
"prompt": "Address of the Redis or Valkey server"
|
||||
},
|
||||
"REDIS_PORT": {
|
||||
"example": "6379"
|
||||
},
|
||||
"REDIS_USERNAME": {
|
||||
"required": false
|
||||
},
|
||||
"REDIS_PASSWORD": {
|
||||
"required": false
|
||||
},
|
||||
"REDIS_DB_TASK": {
|
||||
"example": "0"
|
||||
},
|
||||
"REDIS_DB_CACHE": {
|
||||
"example": "1"
|
||||
},
|
||||
"DB_PASSWORD": {
|
||||
"prompt": "Password of the PostgreSQL user"
|
||||
},
|
||||
"SUPERUSER_PASSWORD": {
|
||||
"prompt": "Password of the admin account"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
"owner_strategy": "mapped-root",
|
||||
"only_when_mount_type": "managed-volume"
|
||||
}
|
||||
]
|
||||
],
|
||||
"resources": {
|
||||
"memory_default_mb": 1024
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,5 +139,18 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"environment_overrides": {
|
||||
"APP_URL": {
|
||||
"prompt": "Address Speedtest Tracker will be opened at (for example http://192.168.1.50)"
|
||||
},
|
||||
"SPEEDTEST_SCHEDULE": {
|
||||
"example": "0 */6 * * *",
|
||||
"prompt": "Schedule of the tests in cron format"
|
||||
},
|
||||
"SPEEDTEST_SERVERS": {
|
||||
"required": false,
|
||||
"prompt": "Server IDs to test against, separated by commas (optional)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"environment_overrides": {
|
||||
"TWINGATE_NETWORK": {
|
||||
"prompt": "Name of the Twingate network (the part before .twingate.com)"
|
||||
},
|
||||
"TWINGATE_ACCESS_TOKEN": {
|
||||
"prompt": "Access token of the connector, from the Twingate admin console"
|
||||
},
|
||||
"TWINGATE_REFRESH_TOKEN": {
|
||||
"prompt": "Refresh token of the connector, from the Twingate admin console"
|
||||
},
|
||||
"TWINGATE_LABEL_HOSTNAME": {
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
"""A path that holds content of the user is offered as a container volume or
|
||||
as a host directory. Either way it belongs to the user the application writes
|
||||
with: a volume left to root is one the application cannot write to."""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from proxmenux_oci.catalog import Catalog
|
||||
|
||||
|
||||
def owners(template):
|
||||
profile = template["proxmox"]["installer_profile"]
|
||||
return {item["container_path"]: item["owner_strategy"] for item in profile["volume_preparations"]}
|
||||
|
||||
|
||||
class ContentVolumeOwnerTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.catalog = Catalog(ROOT)
|
||||
|
||||
def test_the_content_of_the_user_belongs_to_the_application(self):
|
||||
found = owners(self.catalog.compose("duplicati"))
|
||||
self.assertEqual(found["/backups"], "mapped-application-user")
|
||||
self.assertEqual(found["/source"], "mapped-application-user")
|
||||
|
||||
def test_the_state_of_the_application_stays_as_the_recipe_says(self):
|
||||
self.assertEqual(owners(self.catalog.compose("duplicati"))["/config"], "mapped-root")
|
||||
|
||||
def test_it_follows_the_choice_the_installation_offers(self):
|
||||
contract = {"volumes": [
|
||||
{"container_path": "/downloads", "installation_choice": ["managed-volume", "host-bind", "skip"]},
|
||||
{"container_path": "/config", "installation_choice": ["managed-volume", "host-bind"],
|
||||
"managed_volume": {"default_size_gb": 4}}]}
|
||||
profile = {"volume_preparations": [
|
||||
{"container_path": "/downloads", "owner_strategy": "mapped-root"},
|
||||
{"container_path": "/config", "owner_strategy": "mapped-root"}]}
|
||||
self.catalog._apply_volume_policy("an-application", contract, profile)
|
||||
self.assertEqual({item["container_path"]: item["owner_strategy"] for item in profile["volume_preparations"]},
|
||||
{"/downloads": "mapped-application-user", "/config": "mapped-root"})
|
||||
self.assertEqual(contract["volumes"][1]["installation_choice"], ["managed-volume"])
|
||||
|
||||
def test_the_content_of_the_user_is_always_mounted(self):
|
||||
contract = {"volumes": [
|
||||
{"container_path": "/downloads", "installation_choice": ["managed-volume", "host-bind", "skip"],
|
||||
"default": "skip"},
|
||||
{"container_path": "/var/run/docker.sock", "installation_choice": ["host-bind", "skip"], "default": "skip"}]}
|
||||
self.catalog._apply_volume_policy("an-application", contract, {})
|
||||
self.assertEqual(contract["volumes"][0]["installation_choice"], ["managed-volume", "host-bind"])
|
||||
self.assertEqual(contract["volumes"][0]["default"], "managed-volume")
|
||||
# What is not content of the user, as a socket of the host, can still be left out.
|
||||
self.assertEqual(contract["volumes"][1]["installation_choice"], ["host-bind", "skip"])
|
||||
for app, path in (("sonarr", "/downloads"), ("manyfold", "/libraries"), ("qbittorrent", "/downloads")):
|
||||
volume = next(item for item in self.catalog.compose(app)["container_contract"]["volumes"]
|
||||
if item["container_path"] == path)
|
||||
self.assertNotIn("skip", volume["installation_choice"], app)
|
||||
self.assertEqual(volume["default"], "managed-volume", app)
|
||||
|
||||
def test_a_container_volume_is_never_read_only(self):
|
||||
source = (ROOT / "src" / "proxmenux_oci" / "installer.py").read_text()
|
||||
self.assertIn('"read_only": item["read_only"] and volume_mode != "managed-volume"', source)
|
||||
|
||||
def test_the_time_files_of_the_host_are_not_mounted(self):
|
||||
for app in ("openhab", "lyrionmusicserver"):
|
||||
template = self.catalog.compose(app)
|
||||
paths = [volume["container_path"] for volume in template["container_contract"]["volumes"]]
|
||||
self.assertNotIn("/etc/localtime", paths, app)
|
||||
self.assertNotIn("/etc/timezone", paths, app)
|
||||
self.assertNotIn("/etc/localtime", owners(template), app)
|
||||
|
||||
def test_a_recipe_without_preparations_is_left_alone(self):
|
||||
contract = {"volumes": [{"container_path": "/downloads", "installation_choice": ["managed-volume", "host-bind"]}]}
|
||||
self.catalog._apply_volume_policy("an-application", contract, None)
|
||||
self.catalog._apply_volume_policy("an-application", contract, {})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Some images are only the application and expect a database that is already
|
||||
running. The installation says so before its first question, with the address
|
||||
of the image documentation, and asks for the connection in plain words."""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from proxmenux_oci import i18n
|
||||
from proxmenux_oci.catalog import Catalog
|
||||
from proxmenux_oci.installer import external_services_notice
|
||||
|
||||
|
||||
def asked_without_a_proposal(template):
|
||||
return [item["name"] for item in template["container_contract"]["environment"]
|
||||
if item["required"] and item["name"] not in ("PUID", "PGID", "TZ")
|
||||
and not item["sensitive"] and item.get("example") in (None, "")]
|
||||
|
||||
|
||||
class ExternalServicesTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.language, i18n._language = getattr(i18n, "_language", None), "en"
|
||||
cls.catalog = Catalog(ROOT)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
i18n._language = cls.language
|
||||
|
||||
def test_the_notice_names_the_services_and_the_image_documentation(self):
|
||||
notice = external_services_notice(self.catalog.compose("manyfold"))
|
||||
self.assertIn("Manyfold needs these external services, already running: Redis/Valkey", notice)
|
||||
self.assertIn("ProxMenux does not install them", notice)
|
||||
self.assertIn("https://docs.linuxserver.io/images/docker-manyfold/", notice)
|
||||
|
||||
def test_an_application_that_needs_nothing_else_has_no_notice(self):
|
||||
self.assertIsNone(external_services_notice(self.catalog.compose("duplicati")))
|
||||
self.assertIsNone(external_services_notice(self.catalog.compose("lychee")))
|
||||
|
||||
def test_every_connection_question_says_what_it_asks_for(self):
|
||||
for app in ("manyfold", "bookstack", "netbox"):
|
||||
template = self.catalog.compose(app)
|
||||
self.assertTrue(external_services_notice(template), app)
|
||||
for item in template["container_contract"]["environment"]:
|
||||
if item["name"] in asked_without_a_proposal(template):
|
||||
self.assertTrue(item.get("prompt"), f"{app}: {item['name']}")
|
||||
|
||||
def test_no_default_installation_asks_for_a_value_without_saying_what_it_is(self):
|
||||
from proxmenux_oci.installer import _example_default
|
||||
unexplained = {}
|
||||
for item in self.catalog.load_index()["applications"]:
|
||||
if item.get("hidden") or not item.get("automatic_install_candidate"):
|
||||
continue
|
||||
template = self.catalog.compose(item["id"])
|
||||
for variable in template["container_contract"].get("environment", []):
|
||||
if (variable["required"] and not variable["sensitive"] and not variable.get("prompt")
|
||||
and variable.get("prompt_user", True) is not False
|
||||
and variable["name"] not in ("PUID", "PGID", "TZ")
|
||||
and _example_default(variable["name"], variable["example"], "Etc/UTC") in (None, "")):
|
||||
unexplained.setdefault(item["id"], []).append(variable["name"])
|
||||
self.assertEqual(unexplained, {})
|
||||
|
||||
def test_an_image_with_a_database_of_its_own_installs_without_asking_for_one(self):
|
||||
self.assertEqual(asked_without_a_proposal(self.catalog.compose("lychee")), [])
|
||||
database = {item["name"]: item for item in self.catalog.compose("lychee")["container_contract"]["environment"]}
|
||||
self.assertEqual(database["DB_CONNECTION"]["example"], "sqlite")
|
||||
self.assertFalse(database["DB_HOST"]["required"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user