mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-10-01 02:56:41 +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:
@@ -2622,6 +2622,7 @@ def annotate_delegated_apps(apps: list, docker_inventory: dict) -> None:
|
||||
# showing the version of an image it no longer runs.
|
||||
app['docker_available_version'] = None
|
||||
app['docker_update_available'] = None
|
||||
app['docker_pinned'] = None
|
||||
link = resolve_docker_image_for_app(app, docker_inventory)
|
||||
app['docker_image_reference'] = link.get('image_reference')
|
||||
app['docker_binding_error'] = link.get('error')
|
||||
@@ -2633,6 +2634,7 @@ def annotate_delegated_apps(apps: list, docker_inventory: dict) -> None:
|
||||
continue
|
||||
app['docker_available_version'] = image.get('available_version')
|
||||
app['docker_update_available'] = image.get('update_available')
|
||||
app['docker_pinned'] = image.get('pinned')
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
@@ -2903,6 +2905,7 @@ def _docker_inventory_from_ct(vmid) -> dict:
|
||||
"architecture": str(inspected_image.get("Architecture") or ""),
|
||||
"variant": str(inspected_image.get("Variant") or ""),
|
||||
},
|
||||
"pinned": False,
|
||||
"available_version": None,
|
||||
"available_version_source": None,
|
||||
"update_available": None,
|
||||
@@ -2911,13 +2914,78 @@ def _docker_inventory_from_ct(vmid) -> dict:
|
||||
if len(images) >= _DOCKER_MAX_IMAGES:
|
||||
break
|
||||
|
||||
# A container pinned by digest runs exactly the image it names; a newer
|
||||
# tag upstream does not move it, only an edit to its reference does. It is
|
||||
# listed under that reference with its installed version, and is never
|
||||
# compared with the registry nor offered an update.
|
||||
pinned_groups: dict[str, list[dict]] = {}
|
||||
for item in containers:
|
||||
reference = str(item.get("image_reference") or item.get("image") or "").strip()
|
||||
if "@" in reference:
|
||||
pinned_groups.setdefault(reference, []).append(item)
|
||||
for reference in sorted(pinned_groups):
|
||||
if len(images) >= _DOCKER_MAX_IMAGES:
|
||||
break
|
||||
name, _, pinned_digest = reference.partition("@")
|
||||
if not re.fullmatch(r"sha256:[0-9a-f]{64}", pinned_digest):
|
||||
continue
|
||||
final_component = name.rsplit("/", 1)[-1]
|
||||
repository, tag = name.rsplit(":", 1) if ":" in final_component else (name, "")
|
||||
parsed = _parse_docker_reference(repository, tag or pinned_digest)
|
||||
if not parsed or reference in seen:
|
||||
continue
|
||||
seen.add(reference)
|
||||
parsed = {**parsed, "tag": tag, "reference": reference}
|
||||
group = pinned_groups[reference]
|
||||
image_id = next((str(item.get("image_id")) for item in group if item.get("image_id")), "")
|
||||
inspected_image = (
|
||||
inspected_images.get(image_id)
|
||||
or inspected_images.get(image_id.removeprefix("sha256:"))
|
||||
or {}
|
||||
)
|
||||
installed_version, installed_version_source = _docker_version_from_image_inspect(
|
||||
parsed, inspected_image,
|
||||
)
|
||||
primary_compose = group[0].get("compose") or {}
|
||||
display_meta = _docker_service_catalog_meta(
|
||||
str(primary_compose.get("service") or ""),
|
||||
str(group[0].get("name") or ""),
|
||||
reference,
|
||||
)
|
||||
images.append({
|
||||
**parsed,
|
||||
"local_digest": pinned_digest,
|
||||
"remote_digest": None,
|
||||
"image_id": image_id,
|
||||
"used_by": sorted({item["name"] for item in group}),
|
||||
"update_targets": [],
|
||||
"standalone_containers": [],
|
||||
"display_name": display_meta.get("name"),
|
||||
"logo_url": display_meta.get("logo_url"),
|
||||
"installed_version": installed_version,
|
||||
"installed_version_source": installed_version_source,
|
||||
"platform": {
|
||||
"os": str(inspected_image.get("Os") or ""),
|
||||
"architecture": str(inspected_image.get("Architecture") or ""),
|
||||
"variant": str(inspected_image.get("Variant") or ""),
|
||||
},
|
||||
"pinned": True,
|
||||
"available_version": None,
|
||||
"available_version_source": None,
|
||||
"update_available": None,
|
||||
"error": None,
|
||||
})
|
||||
|
||||
def _check(item: dict) -> tuple[str, Optional[str], Optional[str]]:
|
||||
remote, error = _fetch_registry_manifest_digest(item)
|
||||
return item["reference"], remote, error
|
||||
|
||||
if images:
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=min(4, len(images))) as pool:
|
||||
results = list(pool.map(_check, images))
|
||||
checkable = [item for item in images if not item.get("pinned")]
|
||||
results = []
|
||||
if checkable:
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=min(4, len(checkable))) as pool:
|
||||
results = list(pool.map(_check, checkable))
|
||||
by_ref = {ref: (digest, error) for ref, digest, error in results}
|
||||
for item in images:
|
||||
remote, remote_error = by_ref.get(item["reference"], (None, None))
|
||||
|
||||
Reference in New Issue
Block a user