mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 19:02:31 +00:00
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:
+3
-3
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user