mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 10:52:22 +00:00
feat(oci): show update availability and update selected applications
This commit is contained in:
@@ -4338,8 +4338,87 @@ def _detect_with_alt_healing(vmid, app: dict) -> tuple:
|
||||
return installed, err, False
|
||||
|
||||
|
||||
_oci_check_locks: dict = {}
|
||||
_oci_check_locks_guard = threading.Lock()
|
||||
|
||||
|
||||
def check_app(
|
||||
vmid, app_id: str, force: bool = False, notify: bool = True,
|
||||
) -> Optional[dict]:
|
||||
# Keep legacy detectors' locking unchanged. OCI network requests use a
|
||||
# per-app lock and hold the shared cache lock only while reading/saving.
|
||||
with _cache_lock:
|
||||
sidecar = _read_sidecar(vmid)
|
||||
app = _find_app(sidecar, app_id) if sidecar else None
|
||||
if not app or app.get("installed_via") != "oci_image":
|
||||
return _check_non_oci_app(vmid, app_id, force=force, notify=notify)
|
||||
return _check_oci_app(vmid, app_id, force=force, notify=notify)
|
||||
|
||||
|
||||
def _check_oci_app(vmid, app_id, force=False, notify=True):
|
||||
key = (int(vmid), app_id)
|
||||
with _oci_check_locks_guard:
|
||||
lock = _oci_check_locks.setdefault(key, threading.Lock())
|
||||
with lock:
|
||||
with _cache_lock:
|
||||
sidecar = _read_sidecar(vmid)
|
||||
app = _find_app(sidecar, app_id) if sidecar else None
|
||||
if not app or app.get("installed_via") != "oci_image":
|
||||
return sidecar
|
||||
state = app.get("state") or _empty_state()
|
||||
if not force and state.get("checked_at"):
|
||||
try:
|
||||
checked = datetime.datetime.strptime(state["checked_at"].rstrip("Z"), "%Y-%m-%dT%H:%M:%S")
|
||||
age = (datetime.datetime.utcnow() - checked).total_seconds()
|
||||
if 0 <= age < _UPSTREAM_CACHE_TTL_SEC:
|
||||
return sidecar
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
before_app = copy.deepcopy(app)
|
||||
before_record = _read_oci_record(vmid)
|
||||
|
||||
# No global cache lock is held while waiting for registries. The same
|
||||
# native resolver, credentials and digest comparisons are retained.
|
||||
result = _oci_image_versions(
|
||||
vmid, known=state, with_latest=not _oci_secondary_member(before_record))
|
||||
if result.get("busy"):
|
||||
_recheck_after_oci_operation(vmid, app_id)
|
||||
return _read_sidecar(vmid)
|
||||
|
||||
with _cache_lock:
|
||||
# Re-read rather than writing the pre-query copy: other apps,
|
||||
# schedules and registrations may have changed while we waited.
|
||||
sidecar = _read_sidecar(vmid)
|
||||
app = _find_app(sidecar, app_id) if sidecar else None
|
||||
if app != before_app or _read_oci_record(vmid) != before_record:
|
||||
return sidecar # Never publish a result for a replaced/edited app.
|
||||
app["state"] = {
|
||||
"installed_version": result.get("installed_version"),
|
||||
"latest_version": result.get("latest_version"),
|
||||
"latest_published_at": None,
|
||||
"update_available": result.get("update_available"),
|
||||
"error": result.get("error"),
|
||||
"checked_at": _now_iso(),
|
||||
"installed_digest": result.get("installed_digest"),
|
||||
"installed_registry_digest": result.get("installed_registry_digest"),
|
||||
"registry_retry": bool(result.get("registry_retry")),
|
||||
"latest_digest": result.get("latest_digest"),
|
||||
"image_created": result.get("image_created"),
|
||||
"latest_image_created": result.get("latest_image_created"),
|
||||
"image_reference": result.get("image_reference"),
|
||||
"image_repository": result.get("image_repository"),
|
||||
}
|
||||
sidecar["updated_at"] = _now_iso()
|
||||
_write_sidecar(vmid, sidecar)
|
||||
if app["state"]["registry_retry"]:
|
||||
_retry_oci_registry(vmid, app_id)
|
||||
if notify and app["state"]["update_available"]:
|
||||
_fire_update_notification(vmid, app)
|
||||
return sidecar
|
||||
|
||||
|
||||
def _check_non_oci_app(
|
||||
vmid, app_id: str, force: bool = False, notify: bool = True,
|
||||
) -> Optional[dict]:
|
||||
with _cache_lock:
|
||||
sidecar = _read_sidecar(vmid)
|
||||
@@ -4369,40 +4448,6 @@ def check_app(
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if app.get("installed_via") == "oci_image":
|
||||
# A secondary container of a stack shows the version it runs; the
|
||||
# registry is not asked, because it is not updated on its own.
|
||||
result = _oci_image_versions(
|
||||
vmid, known=state, with_latest=not _oci_secondary_member(_read_oci_record(vmid)))
|
||||
if result.get("busy"):
|
||||
_recheck_after_oci_operation(vmid, app_id)
|
||||
return sidecar
|
||||
app["state"] = {
|
||||
"installed_version": result.get("installed_version"),
|
||||
"latest_version": result.get("latest_version"),
|
||||
"latest_published_at": None,
|
||||
"update_available": result.get("update_available"),
|
||||
"error": result.get("error"),
|
||||
"checked_at": _now_iso(),
|
||||
"installed_digest": result.get("installed_digest"),
|
||||
"installed_registry_digest": result.get("installed_registry_digest"),
|
||||
"registry_retry": bool(result.get("registry_retry")),
|
||||
"latest_digest": result.get("latest_digest"),
|
||||
"image_created": result.get("image_created"),
|
||||
"latest_image_created": result.get("latest_image_created"),
|
||||
"image_reference": result.get("image_reference"),
|
||||
"image_repository": result.get("image_repository"),
|
||||
}
|
||||
sidecar["updated_at"] = _now_iso()
|
||||
_write_sidecar(vmid, sidecar)
|
||||
if app["state"]["registry_retry"]:
|
||||
_retry_oci_registry(vmid, app_id)
|
||||
# The payload names an image by its build date and digest when it
|
||||
# states no version, so it decides whether there is anything to send.
|
||||
if notify and app["state"]["update_available"]:
|
||||
_fire_update_notification(vmid, app)
|
||||
return sidecar
|
||||
|
||||
installed, inst_err, _healed = _detect_with_alt_healing(vmid, app)
|
||||
# Trigger the upstream fetch when ANY upstream source is
|
||||
# configured. The dispatcher inside `fetch_latest_upstream`
|
||||
@@ -5975,14 +6020,22 @@ def _oci_repository(reference: str) -> str:
|
||||
return repository
|
||||
|
||||
|
||||
def _oci_resolve(module, reference: str, architecture: str) -> dict:
|
||||
def _oci_resolve(module, reference: str, architecture: str, known=None) -> dict:
|
||||
# One retry: an anonymous registry answers the occasional request with an
|
||||
# error that the next one does not repeat.
|
||||
try:
|
||||
def resolve():
|
||||
# Older installed engines remain compatible; both paths use their
|
||||
# native resolver. No separate registry client or verdict is added.
|
||||
metadata = getattr(module, "resolve_candidate_metadata", None)
|
||||
if known is not None and callable(metadata):
|
||||
return metadata(reference, architecture, known)
|
||||
return module.resolve_candidate(reference, architecture)
|
||||
|
||||
try:
|
||||
return resolve()
|
||||
except RuntimeError:
|
||||
time.sleep(2)
|
||||
return module.resolve_candidate(reference, architecture)
|
||||
return resolve()
|
||||
|
||||
|
||||
# What the panel shows, translated, when the registry of an image did not answer.
|
||||
@@ -6065,8 +6118,15 @@ def _oci_image_versions(vmid, known: Optional[dict] = None, with_latest: bool =
|
||||
result["installed_version"] = _oci_guest_version(vmid, meta.get("template_id"))
|
||||
if not with_latest:
|
||||
return result
|
||||
cached_metadata = None
|
||||
if (known.get("installed_digest") == installed_digest
|
||||
and known.get("image_reference") == reference and known.get("latest_digest")):
|
||||
cached_metadata = {"manifest_digest": known["latest_digest"],
|
||||
"version": known.get("latest_version"),
|
||||
"created": known.get("latest_image_created"),
|
||||
"architecture": architecture}
|
||||
try:
|
||||
latest = _oci_resolve(module, reference, architecture)
|
||||
latest = _oci_resolve(module, reference, architecture, known=cached_metadata)
|
||||
except Exception:
|
||||
# A registry that does not answer says nothing new about the image:
|
||||
# what the last check found for this same image still stands.
|
||||
|
||||
Reference in New Issue
Block a user