diff --git a/.github/scripts/oci_validation.py b/.github/scripts/oci_validation.py index b63b75ba..8383c750 100644 --- a/.github/scripts/oci_validation.py +++ b/.github/scripts/oci_validation.py @@ -19,7 +19,7 @@ REVIEWERS = ROOT / ".github/oci-validation-reviewers" REPO = "https://github.com/MacRimi/ProxMenux" BOARD = "https://github.com/users/MacRimi/projects/1" FORM = f"{REPO}/issues/new?template=oci-validation.yml" -DISCUSSION = f"{REPO}/discussions/360" +DISCUSSION = f"{REPO}/discussions/440" MAINTAINERS = {"macrimi"} LAB = "laboratory-validated" @@ -102,7 +102,7 @@ def render(verification, index): "only Docker provides, a fix still pending in ProxMenux — moves to Blocked on the board, with a line saying why " "and what would make it worth trying again.", "", - f"Questions and ideas about OCI testing go in [discussion #360]({DISCUSSION}).", + f"Questions and ideas about OCI testing go in [discussion #440]({DISCUSSION}).", "", "## Tested by the community", "", diff --git a/lang/es.json b/lang/es.json index 5eb716f1..df9289a3 100644 --- a/lang/es.json +++ b/lang/es.json @@ -4186,6 +4186,7 @@ "No network configuration backups found.": "No se encontraron backups de la configuración de red.", "No network interfaces configured (besides loopback)": "No hay interfaces de red configuradas (aparte del loopback)", "No new Controller/NVMe entries were added.": "No se agregaron nuevas entradas de Controlador/NVMe.", + "No oci-compose was given": "No se dio ningún oci-compose", "No online node has a guest of this job.": "Ningún nodo en línea tiene una VM o CT de este trabajo.", "No optimizations detected to uninstall.": "No se detectaron optimizaciones para desinstalar.", "No other storage of this host that accepts backups has that much free space. Free space and try again; the application was not modified.": "Ningún otro almacenamiento de este host que acepte backups tiene tanto espacio libre. Libera espacio e inténtalo de nuevo; la aplicación no se ha modificado.", @@ -4541,6 +4542,7 @@ "Password reset completed.": "Restablecimiento de contraseña completado.", "Password to access the aMule web interface": "Contraseña para acceder a la interfaz web de aMule", "Passwords do not match. Please try again.": "Las contraseñas no coinciden. Por favor inténtalo de nuevo.", + "Paste an oci-compose in the terminal": "Pegar un oci-compose en la terminal", "Paste it here and press Ctrl+D on an empty line.": "Pégalo aquí y pulsa Ctrl+D en una línea vacía.", "Paste its Compose file in the terminal": "Pegar su archivo Compose en la terminal", "Paste its docker run command in the terminal": "Pegar su comando docker run en la terminal", @@ -7759,6 +7761,7 @@ "nvidia-smi not found after install. Check log:": "nvidia-smi no se encuentra tras la instalación. Revise el log:", "nvidia-smi not found in PATH. Please verify the driver installation.": "nvidia-smi no encontrado en RUTA. Verifique la instalación del controlador.", "nvidia-smi not found in container": "nvidia-smi no encontrado en el contenedor", + "oci-compose of the application": "oci-compose de la aplicación", "of": "de", "of free disk space.": "de espacio libre en disco.", "older firmware may increase passthrough instability": "un firmware antiguo puede aumentar la inestabilidad del passthrough", diff --git a/oci/README.md b/oci/README.md index 0aa347f4..f0016f8c 100644 --- a/oci/README.md +++ b/oci/README.md @@ -441,9 +441,9 @@ What the author writes always wins over what is added. ## Writing and testing an oci-compose 1. **Write the file.** Start from one of the [`examples/`](examples/) or from the smallest file above, and add the blocks the application needs. Any file of `catalog/apps/` or `catalog/curated/` is also a valid starting point. -2. **Copy it to the node**, for example to `/root/oci-compose.json`, or publish it where the node can download it. +2. **Take it to the node.** It can be pasted in the terminal, copied to the node, for example to `/root/oci-compose.json`, or published where the node can download it. 3. **Open OCI manager Apps** from the ProxMenux menu and choose *Install an image that is not in the catalog*. -4. **Choose how it is given**: *Read an oci-compose from a file of this host* or *Download an oci-compose from an address*. +4. **Choose how it is given**: *Paste an oci-compose in the terminal*, *Read an oci-compose from a file of this host* or *Download an oci-compose from an address*. 5. **Read the result.** A file with errors is not installed, and each error names the field: ``` @@ -475,7 +475,7 @@ python3 -m unittest discover -s tests -p 'test_*.py' An application reaches the catalog in three steps. -1. **Suggest it** in [Discussions](https://github.com/MacRimi/ProxMenux/discussions) with the image, the registry it is published in and the category. +1. **Suggest it** in the thread [Suggest an application for the OCI catalog](https://github.com/MacRimi/ProxMenux/discussions/440) with the image, the registry it is published in and the category. 2. **Try it.** Installing it from its Compose file or its `docker run` command shows whether the image runs as a native LXC. When it needs something Compose cannot say, an oci-compose written by hand lets that be tested as well. The file that worked, attached to the suggestion, is what the entry of the catalog is written from. 3. **Validate it.** An application tested for real is recorded in `catalog/verification.json` and shown as verified in the installer. [VALIDATION.md](VALIDATION.md) lists the verified applications and describes a test: install, check that it works, restart, update and remove. diff --git a/oci/VALIDATION.md b/oci/VALIDATION.md index 9e760381..ab136e60 100644 --- a/oci/VALIDATION.md +++ b/oci/VALIDATION.md @@ -18,7 +18,7 @@ Reviewers are listed in [.github/oci-validation-reviewers](../.github/oci-valida An application that cannot be validated for a reason outside the tester's hands — hardware nobody has, something only Docker provides, a fix still pending in ProxMenux — moves to Blocked on the board, with a line saying why and what would make it worth trying again. -Questions and ideas about OCI testing go in [discussion #360](https://github.com/MacRimi/ProxMenux/discussions/360). +Questions and ideas about OCI testing go in [discussion #440](https://github.com/MacRimi/ProxMenux/discussions/440). ## Tested by the community diff --git a/oci/src/proxmenux_oci/custom.py b/oci/src/proxmenux_oci/custom.py index 2e6cd6c0..4d8839ab 100644 --- a/oci/src/proxmenux_oci/custom.py +++ b/oci/src/proxmenux_oci/custom.py @@ -454,16 +454,16 @@ def describe_oci_compose(template: dict[str, Any]) -> str: return '\n' + '\n'.join(lines) -def _read_pasted(ui, title: str | None = None) -> str: +def _read_pasted(ui, title: str | None = None, limit: int = MAX_COMPOSE_BYTES, missing: str | None = None) -> str: """The definition is pasted in the terminal: dialog cannot take it.""" console.show_logo() console.msg_title(title or translate('Compose file of the application')) console.msg_info2(translate('Paste it here and press Ctrl+D on an empty line.')) print() - text = sys.stdin.read(MAX_COMPOSE_BYTES + 1) + text = sys.stdin.read(limit + 1) if not text.strip(): - raise UserCancelled(translate('No docker run command was given') if title is not None - else translate('No Compose file was given')) + raise UserCancelled(missing or (translate('No docker run command was given') if title is not None + else translate('No Compose file was given'))) return text @@ -477,11 +477,15 @@ def read_definition(ui) -> tuple[str, str]: ('url', translate('Download its Compose file from an address')), ('run', translate('Paste its docker run command in the terminal')), ('image', translate('Only the image reference, with no Compose file')), + ('oci-compose-paste', translate('Paste an oci-compose in the terminal')), ('oci-compose', translate('Read an oci-compose from a file of this host')), ('oci-compose-url', translate('Download an oci-compose from an address')), ], 'paste', title=translate('Image that is not in the catalog'), size=MENU_SIZE) if source is None: raise UserCancelled(translate('Image selection was cancelled')) + if source == 'oci-compose-paste': + return _read_pasted(ui, translate('oci-compose of the application'), MAX_OCI_COMPOSE_BYTES, + translate('No oci-compose was given')), OCI_COMPOSE if source == 'oci-compose': return _read_file(ui.ask(translate('Path of the oci-compose file'), '/root/oci-compose.json'), MAX_OCI_COMPOSE_BYTES), OCI_COMPOSE diff --git a/oci/tests/test_install_from_oci_compose.py b/oci/tests/test_install_from_oci_compose.py index 16e1a17e..60f354af 100644 --- a/oci/tests/test_install_from_oci_compose.py +++ b/oci/tests/test_install_from_oci_compose.py @@ -4,20 +4,24 @@ catalog, or the record of an installed application, of which only the template is read.""" import copy +import io import json from pathlib import Path import sys import tempfile +from types import SimpleNamespace import unittest from unittest.mock import patch ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "src")) -from proxmenux_oci import custom +from proxmenux_oci import custom, i18n from proxmenux_oci.catalog import Catalog from proxmenux_oci.converter import ConversionError +# The messages are compared in English, also on a host set to another language. +i18n._language = "en" CATALOG = Catalog(ROOT) EXAMPLES = ("jellyfin-official", "duplicati-official") # Fields that describe where a template came from and are not read to install it. @@ -129,7 +133,7 @@ class HandWrittenTests(unittest.TestCase): self.assertEqual(template["first_run"], catalog["first_run"]) self.assertEqual(template["catalog_ui"]["category_label"], catalog["catalog_ui"]["category_label"]) self.assertTrue(template["compatibility"]["automatic_install_candidate"]) - self.assertEqual(CATALOG.schema_errors(template), []) + self.assertEqual(custom._errors(template), []) def test_the_least_an_oci_compose_says_is_its_image(self): template = custom.template_from_oci_compose(json.dumps( @@ -137,7 +141,7 @@ class HandWrittenTests(unittest.TestCase): self.assertEqual(template["container_contract"]["image"]["repository"], "traefik/whoami") self.assertEqual(template["catalog_ui"]["title"], {"en_US": "whoami"}) self.assertEqual(template["container_contract"]["volumes"], []) - self.assertEqual(CATALOG.schema_errors(template), []) + self.assertEqual(custom._errors(template), []) def test_what_the_author_writes_is_kept(self): document = json.loads(example("duplicati-official")) @@ -229,10 +233,23 @@ class MenuTests(unittest.TestCase): path.write_text(example("jellyfin-official"), encoding="utf-8") ui = FakeUI(["oci-compose"], [str(path)]) text, origin = custom.read_definition(ui) - self.assertEqual(ui.offered[0][-2:], ["oci-compose", "oci-compose-url"]) + self.assertEqual(ui.offered[0][-3:], ["oci-compose-paste", "oci-compose", "oci-compose-url"]) self.assertEqual(origin, custom.OCI_COMPOSE) self.assertEqual(json.loads(text)["catalog_ui"]["title"], {"en_US": "Jellyfin Official"}) + def test_an_oci_compose_can_be_pasted_in_the_terminal(self): + quiet = SimpleNamespace(show_logo=lambda: None, msg_title=lambda *_: None, msg_info2=lambda *_: None) + with patch.object(custom, "console", quiet), patch("builtins.print"), \ + patch.object(custom.sys, "stdin", io.StringIO(example("duplicati-official"))): + text, origin = custom.read_definition(FakeUI(["oci-compose-paste"])) + self.assertEqual(origin, custom.OCI_COMPOSE) + self.assertEqual(custom.template_from_oci_compose(text)["catalog_ui"]["title"], {"en_US": "Duplicati Official"}) + with patch.object(custom, "console", quiet), patch("builtins.print"), \ + patch.object(custom.sys, "stdin", io.StringIO(" \n")): + with self.assertRaises(custom.UserCancelled) as raised: + custom.read_definition(FakeUI(["oci-compose-paste"])) + self.assertEqual(str(raised.exception), "No oci-compose was given") + def test_an_oci_compose_keeps_its_name_and_its_own_questions(self): with tempfile.TemporaryDirectory() as folder: path = Path(folder) / "oci-compose.json" diff --git a/web/messages/en/docs/oci-manager/custom-image.json b/web/messages/en/docs/oci-manager/custom-image.json index 426188e2..a1b6ef66 100644 --- a/web/messages/en/docs/oci-manager/custom-image.json +++ b/web/messages/en/docs/oci-manager/custom-image.json @@ -23,7 +23,7 @@ { "id": "sources", "title": "How the image is described", - "intro": "The first screen asks how the image is described. Four ways give a definition that is translated, a fifth uses only the image name, and two read an oci-compose that is already written.", + "intro": "The first screen asks how the image is described. Four ways give a definition that is translated, a fifth uses only the image name, and three take an oci-compose that is already written.", "blocks": [ { "table": { @@ -34,7 +34,8 @@ ["Download its Compose file from an address", "An http:// or https:// address that serves the raw YAML. At most 256 KiB are downloaded."], ["Paste its docker run command in the terminal", "The command published by the project. Ports, environment, volumes, devices, capabilities, user, shared memory and the other supported options are translated."], ["Only the image reference, with no Compose file", "A reference such as ghcr.io/user/application:latest. The registry is queried and the OCI metadata of the image is kept."], - ["Read an oci-compose from a file of this host", "A JSON file on the node, /root/oci-compose.json by default, of at most 2 MiB. It is not translated: it is checked and installed as it is written."], + ["Paste an oci-compose in the terminal", "The JSON is pasted in the terminal and ends with Ctrl+D on an empty line. It is not translated: it is checked and installed as it is written."], + ["Read an oci-compose from a file of this host", "A JSON file on the node, /root/oci-compose.json by default, of at most 2 MiB."], ["Download an oci-compose from an address", "An http:// or https:// address that serves the JSON file. At most 2 MiB are downloaded."] ] } diff --git a/web/messages/es/docs/oci-manager/custom-image.json b/web/messages/es/docs/oci-manager/custom-image.json index efbbb5a2..dd991851 100644 --- a/web/messages/es/docs/oci-manager/custom-image.json +++ b/web/messages/es/docs/oci-manager/custom-image.json @@ -23,7 +23,7 @@ { "id": "sources", "title": "Cómo se describe la imagen", - "intro": "La primera pantalla pregunta cómo se describe la imagen. Cuatro formas aportan una definición que se traduce, una quinta usa solo el nombre de la imagen y dos leen un oci-compose ya escrito.", + "intro": "La primera pantalla pregunta cómo se describe la imagen. Cuatro formas aportan una definición que se traduce, una quinta usa solo el nombre de la imagen y tres reciben un oci-compose ya escrito.", "blocks": [ { "table": { @@ -34,7 +34,8 @@ ["Descargar su archivo Compose desde una dirección", "Una dirección http:// o https:// que sirva el YAML sin procesar. Se descargan como máximo 256 KiB."], ["Pegar su comando docker run en la terminal", "El comando que publica el proyecto. Se traducen puertos, entorno, volúmenes, dispositivos, capacidades, usuario, memoria compartida y el resto de opciones admitidas."], ["Solo la referencia de la imagen, sin archivo Compose", "Una referencia como ghcr.io/user/application:latest. Se consulta el registro y se conservan los metadatos OCI de la imagen."], - ["Leer un oci-compose desde un archivo de este host", "Un archivo JSON del nodo, /root/oci-compose.json por defecto, de 2 MiB como máximo. No se traduce: se comprueba y se instala tal como está escrito."], + ["Pegar un oci-compose en la terminal", "El JSON se pega en la terminal y termina con Ctrl+D en una línea vacía. No se traduce: se comprueba y se instala tal como está escrito."], + ["Leer un oci-compose desde un archivo de este host", "Un archivo JSON del nodo, /root/oci-compose.json por defecto, de 2 MiB como máximo."], ["Descargar un oci-compose desde una dirección", "Una dirección http:// o https:// que sirva el archivo JSON. Se descargan como máximo 2 MiB."] ] }