mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-10-06 13:36:42 +00:00
feat(oci): run official container images as native LXC containers
Adds the OCI manager: an engine that turns a Docker Compose file into an LXC definition, a catalog of 365 applications drawn from LinuxServer.io and other container image sources, and a per-instance registry recording what each container was built from. Reachable from the main menu. Catalog text is translated like every other string in the project: the taglines go through translate() and land in lang/*.json, so the entries read in all eight languages instead of only English. Translation cache builder: - a failed translation leaves the key absent rather than writing English, which previously made the string count as translated forever - a result identical to a 3+ word source is rejected, catching a provider that silently returns the text it was given - strings that are nothing but glossary terms keep their source spelling instead of being discarded as failures - no backoff between attempts when the provider is deterministic - application names are protected so "HAOS One" survives translation - argos joins the provider list, and the workflow reads the OCI sources Audit & Report: - findings that moved in the wrong direction between runs are reported alongside the ones that improved - an accepted risk can carry a review date and is flagged when it falls due - backup checks explain in plain language what they looked at and what to do next Monitor: - disks can be excluded from periodic reads, and an idle disk says so instead of showing a stale temperature - per-disk identity survives a controller or enclosure change - scheduled Borg backups resolve their SSH key from the repository entry - PVE upgrades log the package list and the resulting dpkg changes The web build no longer copies scripts/ into public/: the documentation links to GitHub, so nothing read that folder. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Resolve the effective OCI process after Compose runtime overrides."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shlex
|
||||
import sys
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from oci_ui import translate
|
||||
|
||||
|
||||
class RuntimeResolutionError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _member(archive: tarfile.TarFile, name: str) -> tarfile.TarInfo:
|
||||
for item in archive.getmembers():
|
||||
if item.name.lstrip("./") == name:
|
||||
return item
|
||||
raise RuntimeResolutionError(f"{translate('Not found in the OCI archive:')} {name}")
|
||||
|
||||
|
||||
def _json_member(archive: tarfile.TarFile, name: str) -> dict[str, Any]:
|
||||
source = archive.extractfile(_member(archive, name))
|
||||
if source is None:
|
||||
raise RuntimeResolutionError(f"{translate('Cannot read')} {name}")
|
||||
value = json.load(source)
|
||||
if not isinstance(value, dict):
|
||||
raise RuntimeResolutionError(f"{translate('Not a JSON object:')} {name}")
|
||||
return value
|
||||
|
||||
|
||||
def _blob_name(digest: str) -> str:
|
||||
algorithm, separator, value = digest.partition(":")
|
||||
if separator != ":" or algorithm != "sha256" or len(value) != 64:
|
||||
raise RuntimeResolutionError(f"{translate('Unsupported OCI digest:')} {digest}")
|
||||
return f"blobs/sha256/{value}"
|
||||
|
||||
|
||||
def image_entrypoint(archive_path: Path) -> list[str]:
|
||||
with tarfile.open(archive_path, mode="r:*") as archive:
|
||||
index = _json_member(archive, "index.json")
|
||||
manifests = index.get("manifests") or []
|
||||
if len(manifests) != 1:
|
||||
raise RuntimeResolutionError(translate("The OCI archive does not contain exactly one manifest"))
|
||||
manifest = _json_member(archive, _blob_name(str(manifests[0]["digest"])))
|
||||
config = _json_member(archive, _blob_name(str(manifest["config"]["digest"])))
|
||||
entrypoint = (config.get("config") or {}).get("Entrypoint") or []
|
||||
if isinstance(entrypoint, str):
|
||||
return [entrypoint]
|
||||
if not isinstance(entrypoint, list) or not all(isinstance(item, str) for item in entrypoint):
|
||||
raise RuntimeResolutionError(translate("Invalid OCI Entrypoint"))
|
||||
return entrypoint
|
||||
|
||||
|
||||
def _arguments(value: Any, label: str) -> list[str]:
|
||||
if isinstance(value, str):
|
||||
return shlex.split(value)
|
||||
if isinstance(value, list) and all(isinstance(item, str) for item in value):
|
||||
return value
|
||||
raise RuntimeResolutionError(f"{translate('The Compose value must be text or a list:')} {label}")
|
||||
|
||||
|
||||
def effective_entrypoint(
|
||||
archive_path: Path,
|
||||
command: Any,
|
||||
compose_entrypoint: Any = None,
|
||||
) -> str:
|
||||
entrypoint = (
|
||||
image_entrypoint(archive_path)
|
||||
if compose_entrypoint is None
|
||||
else _arguments(compose_entrypoint, "entrypoint")
|
||||
)
|
||||
if command is None:
|
||||
arguments: list[str] = []
|
||||
elif isinstance(command, str):
|
||||
arguments = shlex.split(command)
|
||||
elif isinstance(command, list) and all(isinstance(item, str) for item in command):
|
||||
arguments = command
|
||||
else:
|
||||
raise RuntimeResolutionError(f"{translate('The Compose value must be text or a list:')} command")
|
||||
process = entrypoint + arguments
|
||||
if not process:
|
||||
raise RuntimeResolutionError(translate("The Entrypoint/Cmd combination is empty"))
|
||||
return " ".join(shlex.quote(item) for item in process)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) not in (3, 4):
|
||||
print(
|
||||
f"{translate('Usage:')} {sys.argv[0]} OCI_ARCHIVE COMMAND_JSON [ENTRYPOINT_JSON]",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
try:
|
||||
command = json.loads(sys.argv[2])
|
||||
compose_entrypoint = json.loads(sys.argv[3]) if len(sys.argv) == 4 else None
|
||||
print(effective_entrypoint(Path(sys.argv[1]), command, compose_entrypoint))
|
||||
except (OSError, tarfile.TarError, json.JSONDecodeError, KeyError, RuntimeResolutionError) as exc:
|
||||
print(f"{translate('Cannot apply the Compose command:')} {exc}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user