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."]
]
}