paste an oci-compose in the terminal

- "Install an image that is not in the catalog" also takes an oci-compose pasted in the terminal, next to reading it from a file or downloading it from an address.
- oci/README.md and the documentation page of the option describe the three ways, and the README links to the thread for suggesting applications (#440).
- oci/VALIDATION.md points to discussion #440 instead of the closed #360.
- The oci-compose tests pass on a host set to another language and without python3-jsonschema.
This commit is contained in:
MacRimi
2026-10-12 23:03:49 +02:00
parent aba805d4d9
commit c8e5421d75
8 changed files with 44 additions and 18 deletions
+2 -2
View File
@@ -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",
"",
+3
View File
@@ -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",
+3 -3
View File
@@ -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.
+1 -1
View File
@@ -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
+8 -4
View File
@@ -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
+21 -4
View File
@@ -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"
@@ -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 <code>http://</code> or <code>https://</code> 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 <code>ghcr.io/user/application:latest</code>. 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, <code>/root/oci-compose.json</code> 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, <code>/root/oci-compose.json</code> by default, of at most 2 MiB."],
["Download an oci-compose from an address", "An <code>http://</code> or <code>https://</code> address that serves the JSON file. At most 2 MiB are downloaded."]
]
}
@@ -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 <code>http://</code> o <code>https://</code> 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 <code>ghcr.io/user/application:latest</code>. 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, <code>/root/oci-compose.json</code> 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, <code>/root/oci-compose.json</code> por defecto, de 2 MiB como máximo."],
["Descargar un oci-compose desde una dirección", "Una dirección <code>http://</code> o <code>https://</code> que sirva el archivo JSON. Se descargan como máximo 2 MiB."]
]
}