mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-10-08 22:46:41 +00:00
feat(oci): explain optional image variables and show their example value as a hint
This commit is contained in:
@@ -104,6 +104,7 @@
|
||||
},
|
||||
{
|
||||
"name": "MOD_AUTO_RESTART_ENABLED",
|
||||
"prompt": "Restart aMule automatically on a schedule (true or false)",
|
||||
"example": "",
|
||||
"required": false,
|
||||
"sensitive": false,
|
||||
@@ -111,6 +112,7 @@
|
||||
},
|
||||
{
|
||||
"name": "MOD_AUTO_RESTART_CRON",
|
||||
"prompt": "Schedule of the automatic restart, in cron format (for example 0 6 * * * for every day at 6:00)",
|
||||
"example": "",
|
||||
"required": false,
|
||||
"sensitive": false,
|
||||
@@ -118,6 +120,7 @@
|
||||
},
|
||||
{
|
||||
"name": "MOD_AUTO_SHARE_ENABLED",
|
||||
"prompt": "Share folders automatically (true or false)",
|
||||
"example": "",
|
||||
"required": false,
|
||||
"sensitive": false,
|
||||
@@ -125,6 +128,7 @@
|
||||
},
|
||||
{
|
||||
"name": "MOD_AUTO_SHARE_DIRECTORIES",
|
||||
"prompt": "Folders to share, separated by ; (for example /downloads/incoming;/my_movies)",
|
||||
"example": "",
|
||||
"required": false,
|
||||
"sensitive": false,
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
},
|
||||
{
|
||||
"name": "MOD_AUTO_RESTART_ENABLED",
|
||||
"prompt": "Restart aMule automatically on a schedule (true or false)",
|
||||
"example": "",
|
||||
"required": false,
|
||||
"sensitive": false,
|
||||
@@ -111,6 +112,7 @@
|
||||
},
|
||||
{
|
||||
"name": "MOD_AUTO_RESTART_CRON",
|
||||
"prompt": "Schedule of the automatic restart, in cron format (for example 0 6 * * * for every day at 6:00)",
|
||||
"example": "",
|
||||
"required": false,
|
||||
"sensitive": false,
|
||||
@@ -118,6 +120,7 @@
|
||||
},
|
||||
{
|
||||
"name": "MOD_AUTO_SHARE_ENABLED",
|
||||
"prompt": "Share folders automatically (true or false)",
|
||||
"example": "",
|
||||
"required": false,
|
||||
"sensitive": false,
|
||||
@@ -125,6 +128,7 @@
|
||||
},
|
||||
{
|
||||
"name": "MOD_AUTO_SHARE_DIRECTORIES",
|
||||
"prompt": "Folders to share, separated by ; (for example /downloads/incoming;/my_movies)",
|
||||
"example": "",
|
||||
"required": false,
|
||||
"sensitive": false,
|
||||
|
||||
@@ -43,6 +43,31 @@ def _example_default(name: str, example: str | None, timezone: str | None = None
|
||||
return example
|
||||
|
||||
|
||||
def _compose_example(template: dict[str, Any], name: str) -> str | None:
|
||||
"""The value the image's own Compose file shows for a variable, as a hint."""
|
||||
try:
|
||||
import yaml
|
||||
compose = yaml.safe_load(template["container_contract"].get("original_compose") or "") or {}
|
||||
except Exception:
|
||||
return None
|
||||
for service in (compose.get("services") or {}).values():
|
||||
environment = (service or {}).get("environment") or {}
|
||||
if isinstance(environment, list):
|
||||
environment = dict(str(entry).split("=", 1) for entry in environment if "=" in str(entry))
|
||||
value = environment.get(name) if isinstance(environment, dict) else None
|
||||
if value in (None, "") or re.search(r"\$\{?[A-Za-z_]", str(value)):
|
||||
continue
|
||||
value = " ".join(str(value).split())
|
||||
return value if len(value) <= 60 else value[:57] + "..."
|
||||
return None
|
||||
|
||||
|
||||
def _image_documentation(template: dict[str, Any]) -> str | None:
|
||||
ui = template.get("catalog_ui", {})
|
||||
return next((ui[key] for key in ("documentation", "repository", "website")
|
||||
if str(ui.get(key) or "").startswith(("http://", "https://"))), None)
|
||||
|
||||
|
||||
def _hostname_default(value: str) -> str:
|
||||
hostname = re.sub(r"[^a-z0-9-]+", "-", value.casefold()).strip("-")[:63].rstrip("-")
|
||||
return hostname or "oci-app"
|
||||
@@ -381,6 +406,7 @@ def build_deployment(
|
||||
host_firewall = confirm_host_monitor_firewall(ui, template, bridge) if host_monitor else None
|
||||
|
||||
environment: list[dict[str, str]] = []
|
||||
optional_explained = False
|
||||
for item in template["container_contract"]["environment"]:
|
||||
name = item["name"]
|
||||
if item.get("prompt"):
|
||||
@@ -401,8 +427,24 @@ def build_deployment(
|
||||
# The image keeps its own default for every optional setting.
|
||||
if not advanced:
|
||||
continue
|
||||
if default in (None, "") and not ui.confirm(f"{translate('Configure')} {label}", False):
|
||||
continue
|
||||
if default in (None, ""):
|
||||
question = f"{translate('Configure the optional variable')} {name}?"
|
||||
if item.get("prompt"):
|
||||
question = f"{translate(item['prompt'])}\n\n{question}"
|
||||
example = _compose_example(template, name)
|
||||
if example:
|
||||
question += f"\n\n{translate('Example value:')} {example}"
|
||||
if not optional_explained:
|
||||
# Said once, with the first optional variable.
|
||||
notice = translate("If you do not know what a variable does, answer No: the image keeps "
|
||||
"its own value.")
|
||||
documentation = _image_documentation(template)
|
||||
if documentation:
|
||||
notice += f"\n{translate('The image documentation describes them:')} {documentation}"
|
||||
question = f"{notice}\n\n{question}"
|
||||
optional_explained = True
|
||||
if not ui.confirm(question, False):
|
||||
continue
|
||||
if item["sensitive"]:
|
||||
if generated and (not advanced or not generated.get("prompt", True)):
|
||||
value = ""
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Optional image variables are asked with what identifies them, and stay unset by default."""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
from proxmenux_oci.catalog import Catalog
|
||||
from proxmenux_oci.installer import ADVANCED_MODE, build_deployment
|
||||
from test_advanced_flow_order import RecordingUI, addresses, storages
|
||||
|
||||
|
||||
@patch("proxmenux_oci.i18n.language", return_value="en")
|
||||
@patch("proxmenux_oci.installer.host.storages", side_effect=storages)
|
||||
@patch("proxmenux_oci.installer.host.bridges", return_value=[{"iface": "vmbr0", "cidr": "192.0.2.10/24"}])
|
||||
@patch("proxmenux_oci.installer.host.timezone", return_value="Europe/Madrid")
|
||||
@patch("proxmenux_oci.installer.access.ask_ipv4", return_value=("dhcp", None))
|
||||
@patch("proxmenux_oci.installer.access.ask_addresses", side_effect=addresses)
|
||||
class OptionalVariableTests(unittest.TestCase):
|
||||
def build(self, app, ui):
|
||||
return build_deployment(Catalog(ROOT).compose(app), ui, ADVANCED_MODE)
|
||||
|
||||
def optional_questions(self, ui):
|
||||
return [text for text in ui.asked if "Configure the optional variable" in text]
|
||||
|
||||
def test_first_question_explains_them_and_links_the_documentation(self, *_):
|
||||
ui = RecordingUI()
|
||||
plan = self.build("amule", ui)
|
||||
questions = self.optional_questions(ui)
|
||||
self.assertIn("answer No: the image keeps its own value", questions[0])
|
||||
self.assertIn("https://github.com/ngosang/docker-amule", questions[0])
|
||||
self.assertTrue(all("answer No" not in text for text in questions[1:]))
|
||||
restart = next(text for text in questions if "MOD_AUTO_RESTART_ENABLED" in text)
|
||||
self.assertIn("Restart aMule automatically on a schedule (true or false)", restart)
|
||||
self.assertIn("Example value: true", restart)
|
||||
self.assertFalse([e for e in plan["environment"] if e["name"].startswith("MOD_")])
|
||||
|
||||
def test_the_example_is_a_hint_not_a_default(self, *_):
|
||||
ui = RecordingUI()
|
||||
asked_defaults = {}
|
||||
ui.confirm = lambda text, default=False: ui.asked.append(text) or "MOD_AUTO_RESTART_CRON" in text
|
||||
ui.ask = lambda text, default=None, required=True: asked_defaults.setdefault(text, default) or ""
|
||||
self.build("amule", ui)
|
||||
prompt = next(text for text in asked_defaults if text.startswith("Schedule of the automatic restart"))
|
||||
self.assertIn(asked_defaults[prompt], (None, ""))
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user