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
+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"