mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-29 18:16:43 +00:00
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>
111 lines
3.9 KiB
Python
111 lines
3.9 KiB
Python
#!/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())
|