From fc7c3181bc81057e8353be7d06d734ee18d5eb9b Mon Sep 17 00:00:00 2001 From: MacRimi Date: Mon, 26 Oct 2026 17:40:33 +0100 Subject: [PATCH] OCI: retry registry lookups and say why one failed --- CHANGELOG.md | 1 + lang/de.json | 4 ++ lang/es.json | 4 ++ lang/fr.json | 4 ++ lang/it.json | 4 ++ lang/pt.json | 4 ++ lang/sk.json | 4 ++ lang/sv.json | 4 ++ oci/remote/oci_installation_state.py | 32 ++++++++++- oci/remote/oci_update_current.py | 6 ++- oci/tests/test_registry_errors.py | 81 ++++++++++++++++++++++++++++ web/data/changelog/es.md | 1 + 12 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 oci/tests/test_registry_errors.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 03d223bc..7ac51d17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -133,6 +133,7 @@ ProxMenux does not build its own containers for these applications: it reads the - **Services that are not web pages update and recover like the rest**: MariaDB, SSH or a speech server answer on their own protocol, and Update, Modify and Recover take that answer as the service being up. - **The backup of an operation can be made on another storage** when it does not fit on the system disk, and an operation that has no room says so before anything is stopped. - **A damaged image download or working backup is written a second time** before anything fails, and a failed update leaves no temporary container behind. +- **A registry that fails for a moment is asked again**, up to three times, before an update gives up, and a failure says why: the registry limited the requests, refused access, does not have the image or could not be reached. - **Modify is a menu of settings** ([PR #495](https://github.com/MacRimi/ProxMenux/pull/495), contributed by [@jcastro](https://github.com/jcastro)): each row shows the current value, only the chosen setting is edited, and the changes are kept until they are reviewed and applied together, or discarded. Refreshing the catalog options is one more entry of the menu. - **Manage opens at once** ([PR #493](https://github.com/MacRimi/ProxMenux/pull/493), contributed by [@jcastro](https://github.com/jcastro)): the configuration of each application is read again only when it changed, so a host with 26 applications goes from 15 seconds to a fraction of a second. - **Modify and Update open without waiting** ([PR #498](https://github.com/MacRimi/ProxMenux/pull/498), contributed by [@jcastro](https://github.com/jcastro)): the space for the backup is checked once the operation is accepted, and Escape goes back at once. diff --git a/lang/de.json b/lang/de.json index 5cbfade1..f4b1859b 100644 --- a/lang/de.json +++ b/lang/de.json @@ -7030,6 +7030,7 @@ "The image is in its registry.": "Das Bild befindet sich in seiner Registry.", "The image is in its registry:": "Das Bild befindet sich in seiner Registrierung:", "The image manifest failed integrity verification. Try again or contact the image publisher.": "Die Integritätsüberprüfung des Bildmanifests ist fehlgeschlagen. Versuchen Sie es erneut oder wenden Sie sich an den Bildherausgeber.", + "The image or its tag does not exist in the registry.": "Das Image oder sein Tag existiert in der Registry nicht.", "The image or tag was not found on {registry}, or this account cannot access it. Check the reference.": "Das Bild oder Tag wurde auf {registry} nicht gefunden oder dieses Konto kann nicht darauf zugreifen. Überprüfen Sie die Referenz.", "The image requests NVIDIA, but the host has no working NVIDIA driver": "Das Bild fordert NVIDIA an, aber der Host hat keinen funktionierenden NVIDIA-Treiber", "The image requests disabling part of the AppArmor or seccomp confinement. Continue only if you trust the image and accept this risk.": "Das Bild fordert die Deaktivierung eines Teils der AppArmor- oder seccomp-Beschränkung auf. Fahren Sie nur fort, wenn Sie dem Image vertrauen und dieses Risiko akzeptieren.", @@ -7176,8 +7177,11 @@ "The registry authentication file must have an absolute path.": "Die Registrierungsauthentifizierungsdatei muss einen absoluten Pfad haben.", "The registry authentication path cannot contain symbolic links.": "Der Registrierungsauthentifizierungspfad darf keine symbolischen Links enthalten.", "The registry authentication path has an unsafe parent directory.": "Der Registrierungsauthentifizierungspfad verfügt über ein unsicheres übergeordnetes Verzeichnis.", + "The registry could not be reached. Check the connection and try again.": "Die Registry ist nicht erreichbar. Prüfen Sie die Verbindung und versuchen Sie es erneut.", "The registry credential directory must be owned by root with mode 700.": "Das Registrierungs-Anmeldeinformationsverzeichnis muss Root mit Modus 700 gehören.", "The registry did not return the saved image digest": "Die Registrierung hat den gespeicherten Image-Digest nicht zurückgegeben", + "The registry limited the requests. Try again later, or add an account for this registry.": "Die Registry hat die Anfragen begrenzt. Versuchen Sie es später erneut oder fügen Sie ein Konto für diese Registry hinzu.", + "The registry refused access to the image.": "Die Registry hat den Zugriff auf das Image verweigert.", "The registry returned incomplete or unsupported image metadata.": "Die Registrierung hat unvollständige oder nicht unterstützte Bildmetadaten zurückgegeben.", "The registry returned invalid or unsupported image metadata.": "Die Registrierung hat ungültige oder nicht unterstützte Bildmetadaten zurückgegeben.", "The remote does not exist; create and authorize it first in the WebUI:": "Die Fernbedienung existiert nicht; erstellen und autorisieren Sie sie zuerst in der WebUI:", diff --git a/lang/es.json b/lang/es.json index 9a83ef0b..d3354b1d 100644 --- a/lang/es.json +++ b/lang/es.json @@ -7012,6 +7012,7 @@ "The image is in its registry.": "La imagen está en su registro.", "The image is in its registry:": "La imagen está en su registro:", "The image manifest failed integrity verification. Try again or contact the image publisher.": "El manifiesto de la imagen no ha superado la verificación de integridad. Inténtalo de nuevo o contacta con quien publica la imagen.", + "The image or its tag does not exist in the registry.": "La imagen o su etiqueta no existe en el registro.", "The image or tag was not found on {registry}, or this account cannot access it. Check the reference.": "La imagen o la etiqueta no está en {registry}, o esta cuenta no puede acceder a ella. Comprueba la referencia.", "The image requests NVIDIA, but the host has no working NVIDIA driver": "La imagen solicita NVIDIA, pero el host no tiene ningún controlador NVIDIA trabajando", "The image requests disabling part of the AppArmor or seccomp confinement. Continue only if you trust the image and accept this risk.": "La imagen pide desactivar parte del confinamiento de AppArmor o seccomp. Continúa solo si confías en la imagen y aceptas este riesgo.", @@ -7152,8 +7153,11 @@ "The registry authentication file must have an absolute path.": "El fichero de autenticación del registro debe tener una ruta absoluta.", "The registry authentication path cannot contain symbolic links.": "La ruta del fichero de autenticación no puede contener enlaces simbólicos.", "The registry authentication path has an unsafe parent directory.": "La ruta del fichero de autenticación tiene una carpeta superior insegura.", + "The registry could not be reached. Check the connection and try again.": "No se ha podido conectar con el registro. Comprueba la conexión y vuelve a intentarlo.", "The registry credential directory must be owned by root with mode 700.": "La carpeta de credenciales del registro debe pertenecer a root y tener el modo 700.", "The registry did not return the saved image digest": "El registro no devolvió el digest de imagen guardado", + "The registry limited the requests. Try again later, or add an account for this registry.": "El registro ha limitado las consultas. Vuelve a intentarlo más tarde o añade una cuenta para este registro.", + "The registry refused access to the image.": "El registro ha denegado el acceso a la imagen.", "The registry returned incomplete or unsupported image metadata.": "El registro ha devuelto metadatos de imagen incompletos o no admitidos.", "The registry returned invalid or unsupported image metadata.": "El registro ha devuelto metadatos de imagen no válidos o no admitidos.", "The remote does not exist; create and authorize it first in the WebUI:": "El remoto no existe; crear y autorizar primero en el WebUI:", diff --git a/lang/fr.json b/lang/fr.json index c32bd335..7f69ae6d 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -7029,6 +7029,7 @@ "The image is in its registry.": "L'image est dans son registre.", "The image is in its registry:": "L'image est dans son registre:", "The image manifest failed integrity verification. Try again or contact the image publisher.": "La vérification de l'intégrité du manifeste de l'image a échoué. Réessayez ou contactez l'éditeur d'images.", + "The image or its tag does not exist in the registry.": "L’image ou son tag n’existe pas dans le registre.", "The image or tag was not found on {registry}, or this account cannot access it. Check the reference.": "L'image ou la balise est introuvable sur {registry}, ou ce compte ne peut pas y accéder. Vérifiez la référence.", "The image requests NVIDIA, but the host has no working NVIDIA driver": "L'image demande NVIDIA, mais l'hôte n'a pas de pilote NVIDIA fonctionnant", "The image requests disabling part of the AppArmor or seccomp confinement. Continue only if you trust the image and accept this risk.": "L'image demande l'invalidation d'une partie de l'AppArmor ou du confinement de seccomp. Continuez seulement si vous faites confiance à l'image et acceptez ce risque.", @@ -7175,8 +7176,11 @@ "The registry authentication file must have an absolute path.": "Le fichier d’authentification du registre doit avoir un chemin absolu.", "The registry authentication path cannot contain symbolic links.": "Le chemin d’authentification du registre ne peut pas contenir de liens symboliques.", "The registry authentication path has an unsafe parent directory.": "Le chemin d'authentification du registre possède un répertoire parent non sécurisé.", + "The registry could not be reached. Check the connection and try again.": "Impossible de joindre le registre. Vérifiez la connexion et réessayez.", "The registry credential directory must be owned by root with mode 700.": "Le répertoire des informations d'identification du registre doit appartenir à root avec le mode 700.", "The registry did not return the saved image digest": "Le registre n'a pas renvoyé le digest d'image enregistré", + "The registry limited the requests. Try again later, or add an account for this registry.": "Le registre a limité les requêtes. Réessayez plus tard ou ajoutez un compte pour ce registre.", + "The registry refused access to the image.": "Le registre a refusé l’accès à l’image.", "The registry returned incomplete or unsupported image metadata.": "Le registre a renvoyé des métadonnées d'image incomplètes ou non prises en charge.", "The registry returned invalid or unsupported image metadata.": "Le registre a renvoyé des métadonnées d'image invalides ou non prises en charge.", "The remote does not exist; create and authorize it first in the WebUI:": "La télécommande n'existe pas; créez et autorisez-la d'abord dans le WebUI:", diff --git a/lang/it.json b/lang/it.json index 56c75864..818a5c9c 100644 --- a/lang/it.json +++ b/lang/it.json @@ -7030,6 +7030,7 @@ "The image is in its registry.": "L’immagine è presente nel relativo registry.", "The image is in its registry:": "L’immagine è presente nel relativo registry:", "The image manifest failed integrity verification. Try again or contact the image publisher.": "Il manifest dell'immagine non ha superato la verifica dell'integrità. Riprova o contatta l'editore dell'immagine.", + "The image or its tag does not exist in the registry.": "L'immagine o il suo tag non esiste nel registro.", "The image or tag was not found on {registry}, or this account cannot access it. Check the reference.": "L'immagine o il tag non è stato trovato su {registry} oppure questo account non può accedervi. Controlla il riferimento.", "The image requests NVIDIA, but the host has no working NVIDIA driver": "L'immagine richiede NVIDIA, ma l'host non ha un driver NVIDIA funzionante", "The image requests disabling part of the AppArmor or seccomp confinement. Continue only if you trust the image and accept this risk.": "L’immagine richiede di disabilitare parte dell’isolamento AppArmor o seccomp. Continuare solo se si ritiene affidabile l’immagine e si accetta il rischio.", @@ -7176,8 +7177,11 @@ "The registry authentication file must have an absolute path.": "Il file di autenticazione del registro deve avere un percorso assoluto.", "The registry authentication path cannot contain symbolic links.": "Il percorso di autenticazione del registro non può contenere collegamenti simbolici.", "The registry authentication path has an unsafe parent directory.": "Il percorso di autenticazione del registro ha una directory superiore non sicura.", + "The registry could not be reached. Check the connection and try again.": "Impossibile raggiungere il registro. Controlla la connessione e riprova.", "The registry credential directory must be owned by root with mode 700.": "La directory delle credenziali del registro deve essere di proprietà di root con modalità 700.", "The registry did not return the saved image digest": "Il registry non ha restituito il digest salvato dell’immagine", + "The registry limited the requests. Try again later, or add an account for this registry.": "Il registro ha limitato le richieste. Riprova più tardi oppure aggiungi un account per questo registro.", + "The registry refused access to the image.": "Il registro ha negato l'accesso all'immagine.", "The registry returned incomplete or unsupported image metadata.": "Il registro ha restituito metadati dell'immagine incompleti o non supportati.", "The registry returned invalid or unsupported image metadata.": "Il registro ha restituito metadati dell'immagine non validi o non supportati.", "The remote does not exist; create and authorize it first in the WebUI:": "Il remote non esiste; crearlo e autorizzarlo prima nella WebUI:", diff --git a/lang/pt.json b/lang/pt.json index f1894936..4d9ad67f 100644 --- a/lang/pt.json +++ b/lang/pt.json @@ -7030,6 +7030,7 @@ "The image is in its registry.": "A imagem está no seu registo.", "The image is in its registry:": "A imagem está em seu registro:", "The image manifest failed integrity verification. Try again or contact the image publisher.": "O manifesto da imagem falhou na verificação de integridade. Tente novamente ou entre em contato com o editor da imagem.", + "The image or its tag does not exist in the registry.": "A imagem ou a sua etiqueta não existe no registo.", "The image or tag was not found on {registry}, or this account cannot access it. Check the reference.": "A imagem ou tag não foi encontrada em {registry} ou esta conta não pode acessá-la. Verifique a referência.", "The image requests NVIDIA, but the host has no working NVIDIA driver": "A imagem solicita NVIDIA, mas a máquina não tem driver NVIDIA funcionando", "The image requests disabling part of the AppArmor or seccomp confinement. Continue only if you trust the image and accept this risk.": "A imagem solicita desativar parte do confinamento AppArmor ou seccomp. Continue apenas se você confiar na imagem e aceitar este risco.", @@ -7176,8 +7177,11 @@ "The registry authentication file must have an absolute path.": "O arquivo de autenticação do registro deve ter um caminho absoluto.", "The registry authentication path cannot contain symbolic links.": "O caminho de autenticação do registro não pode conter links simbólicos.", "The registry authentication path has an unsafe parent directory.": "O caminho de autenticação do registro possui um diretório pai inseguro.", + "The registry could not be reached. Check the connection and try again.": "Não foi possível contactar o registo. Verifique a ligação e tente novamente.", "The registry credential directory must be owned by root with mode 700.": "O diretório de credenciais do registro deve pertencer ao root com modo 700.", "The registry did not return the saved image digest": "O registro não retornou o digest de imagem salvo", + "The registry limited the requests. Try again later, or add an account for this registry.": "O registo limitou os pedidos. Tente novamente mais tarde ou adicione uma conta para este registo.", + "The registry refused access to the image.": "O registo recusou o acesso à imagem.", "The registry returned incomplete or unsupported image metadata.": "O registro retornou metadados de imagem incompletos ou não suportados.", "The registry returned invalid or unsupported image metadata.": "O registro retornou metadados de imagem inválidos ou não suportados.", "The remote does not exist; create and authorize it first in the WebUI:": "O remoto não existe; crie e autorize-o primeiro na WebUI:", diff --git a/lang/sk.json b/lang/sk.json index e131fa35..03bbeb89 100644 --- a/lang/sk.json +++ b/lang/sk.json @@ -7031,6 +7031,7 @@ "The image is in its registry.": "Obraz je dostupný vo svojom registri.", "The image is in its registry:": "Obraz je dostupný vo svojom registri:", "The image manifest failed integrity verification. Try again or contact the image publisher.": "Overenie integrity manifestu obrazu zlyhalo. Skúste to znova alebo kontaktujte vydavateľa obrazu.", + "The image or its tag does not exist in the registry.": "Obraz alebo jeho značka v registri neexistuje.", "The image or tag was not found on {registry}, or this account cannot access it. Check the reference.": "Obraz alebo tag sa v registri {registry} nenašiel alebo k nemu tento účet nemá prístup. Skontrolujte odkaz na obraz.", "The image requests NVIDIA, but the host has no working NVIDIA driver": "Obraz požaduje NVIDIA, ale hostiteľ nemá funkčný ovládač NVIDIA", "The image requests disabling part of the AppArmor or seccomp confinement. Continue only if you trust the image and accept this risk.": "Obraz požaduje vypnutie časti obmedzení AppArmor alebo seccomp. Pokračujte iba vtedy, ak obrazu dôverujete a riziko prijímate.", @@ -7177,8 +7178,11 @@ "The registry authentication file must have an absolute path.": "Cesta k súboru s prihlasovacími údajmi registrov musí byť absolútna.", "The registry authentication path cannot contain symbolic links.": "Cesta k súboru s prihlasovacími údajmi registrov nesmie obsahovať symbolické odkazy.", "The registry authentication path has an unsafe parent directory.": "Nadradený adresár súboru s prihlasovacími údajmi registrov nie je bezpečný.", + "The registry could not be reached. Check the connection and try again.": "Register nie je dostupný. Skontrolujte pripojenie a skúste to znova.", "The registry credential directory must be owned by root with mode 700.": "Adresár s prihlasovacími údajmi registrov musí vlastniť root a musí mať oprávnenia 700.", "The registry did not return the saved image digest": "Register nevrátil uložený identifikátor digest obrazu", + "The registry limited the requests. Try again later, or add an account for this registry.": "Register obmedzil počet požiadaviek. Skúste to neskôr alebo pridajte účet pre tento register.", + "The registry refused access to the image.": "Register odmietol prístup k obrazu.", "The registry returned incomplete or unsupported image metadata.": "Register vrátil neúplné alebo nepodporované metadáta obrazu.", "The registry returned invalid or unsupported image metadata.": "Register vrátil neplatné alebo nepodporované metadáta obrazu.", "The remote does not exist; create and authorize it first in the WebUI:": "Vzdialené úložisko neexistuje; najprv ho vytvorte a autorizujte vo webovom rozhraní:", diff --git a/lang/sv.json b/lang/sv.json index 30119b59..bf5be48c 100644 --- a/lang/sv.json +++ b/lang/sv.json @@ -7030,6 +7030,7 @@ "The image is in its registry.": "Bilden finns i dess register.", "The image is in its registry:": "Bilden finns i dess register:", "The image manifest failed integrity verification. Try again or contact the image publisher.": "Bildmanifestet misslyckades med integritetsverifieringen. Försök igen eller kontakta bildutgivaren.", + "The image or its tag does not exist in the registry.": "Avbildningen eller dess tagg finns inte i registret.", "The image or tag was not found on {registry}, or this account cannot access it. Check the reference.": "Bilden eller taggen hittades inte på {registry}, eller så kan det här kontot inte komma åt den. Kontrollera referensen.", "The image requests NVIDIA, but the host has no working NVIDIA driver": "Bilden begär NVIDIA, men värden har ingen fungerande NVIDIA-förare", "The image requests disabling part of the AppArmor or seccomp confinement. Continue only if you trust the image and accept this risk.": "Bilden begär inaktivering av en del av AppArmor eller seccomp confinement. Fortsätt bara om du litar på bilden och accepterar denna risk.", @@ -7176,8 +7177,11 @@ "The registry authentication file must have an absolute path.": "Registerautentiseringsfilen måste ha en absolut sökväg.", "The registry authentication path cannot contain symbolic links.": "Registerautentiseringssökvägen kan inte innehålla symboliska länkar.", "The registry authentication path has an unsafe parent directory.": "Registerautentiseringssökvägen har en osäker överordnad katalog.", + "The registry could not be reached. Check the connection and try again.": "Registret kunde inte nås. Kontrollera anslutningen och försök igen.", "The registry credential directory must be owned by root with mode 700.": "Registerreferenskatalogen måste ägas av root med läge 700.", "The registry did not return the saved image digest": "Registret returnerade inte den sparade avbildsdigesten", + "The registry limited the requests. Try again later, or add an account for this registry.": "Registret begränsade förfrågningarna. Försök igen senare eller lägg till ett konto för det här registret.", + "The registry refused access to the image.": "Registret nekade åtkomst till avbildningen.", "The registry returned incomplete or unsupported image metadata.": "Registret returnerade ofullständig eller ostödd bildmetadata.", "The registry returned invalid or unsupported image metadata.": "Registret returnerade ogiltig eller ostödd bildmetadata.", "The remote does not exist; create and authorize it first in the WebUI:": "Fjärrkontrollen finns inte; skapa och godkänna den först i WebUI:", diff --git a/oci/remote/oci_installation_state.py b/oci/remote/oci_installation_state.py index 344d88ac..0528f306 100644 --- a/oci/remote/oci_installation_state.py +++ b/oci/remote/oci_installation_state.py @@ -56,11 +56,39 @@ def version_from_environment(repository, environment): return found.get(expected) +# What a registry answer means, read from skopeo's error output. Only the +# cause is reported: the output itself can carry credentials. +REGISTRY_ERRORS = ( + (('toomanyrequests', 'too many requests', 'rate limit', 'registry 429'), + 'The registry limited the requests. Try again later, or add an account for this registry.'), + (('unauthorized', 'authentication required', 'access to the resource is denied', '403 forbidden', + 'registry 401', 'registry 403'), + 'The registry refused access to the image.'), + (('manifest unknown', 'name unknown', '404 not found'), + 'The image or its tag does not exist in the registry.'), + (('no such host', 'i/o timeout', 'connection refused', 'connection reset', 'network is unreachable', + 'tls handshake timeout', 'context deadline exceeded', ': eof'), + 'The registry could not be reached. Check the connection and try again.'), +) + + +def registry_error(errors): + text = errors.decode(errors='replace').casefold() + for patterns, message in REGISTRY_ERRORS: + if any(pattern in text for pattern in patterns): + return translate(message) + return None + + def command(*args): - result = subprocess.run(args, capture_output=True, timeout=120) + if args[:2] == ('skopeo', 'inspect'): + # A registry that fails for a moment is asked again before giving up. + args = args[:2] + ('--retry-times', '3', '--retry-delay', '5s') + args[2:] + result = subprocess.run(args, capture_output=True, timeout=300) if result.returncode: # Tool errors may contain credentials or environment values. - raise RuntimeError(f"{args[0]} {translate('failed with exit code')} {result.returncode}") + reason = registry_error(result.stderr) if args[0] == 'skopeo' else None + raise RuntimeError(reason or f"{args[0]} {translate('failed with exit code')} {result.returncode}") return result.stdout diff --git a/oci/remote/oci_update_current.py b/oci/remote/oci_update_current.py index f4f94038..3f0a10ab 100644 --- a/oci/remote/oci_update_current.py +++ b/oci/remote/oci_update_current.py @@ -67,8 +67,10 @@ def resolve_archive(desired, config, current=None, check=None, required_digest=N if required_digest is not None and not re.fullmatch(r'sha256:[a-f0-9]{64}', required_digest): raise ValueError(translate('Invalid saved image digest')) lookup = repository(reference) + '@' + required_digest if required_digest else reference - code = ('import json,sys; from oci_installation_state import resolve_candidate; ' - 'print(json.dumps(resolve_candidate(sys.argv[1],sys.argv[2])))') + # The reason of a failure reaches the log as one line, not as a traceback. + code = ('import json,sys; from oci_installation_state import resolve_candidate\n' + 'try: print(json.dumps(resolve_candidate(sys.argv[1],sys.argv[2])))\n' + 'except (RuntimeError, ValueError, OSError) as error: sys.exit(str(error))') candidate = json.loads(run_quiet([sys.executable, '-c', code, lookup, architecture], translate('Could not query the image registry'), capture=True)) digest = candidate['manifest_digest'] diff --git a/oci/tests/test_registry_errors.py b/oci/tests/test_registry_errors.py new file mode 100644 index 00000000..944b49e3 --- /dev/null +++ b/oci/tests/test_registry_errors.py @@ -0,0 +1,81 @@ +"""A registry that fails for a moment is asked again, and a failure says why.""" +import ast +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +REMOTE = Path(__file__).resolve().parents[1] / 'remote' +sys.path.insert(0, str(REMOTE)) +import oci_installation_state as state + +RATE = (b'time="2026-10-26T08:35:58Z" level=fatal msg="Error parsing image name \\"docker://gtstef/filebrowser:latest\\": ' + b'reading manifest latest in docker.io/gtstef/filebrowser: toomanyrequests: You have reached your unauthenticated pull rate limit."') +OFFLINE = (b'level=fatal msg="Error parsing image name: pinging container registry registry-1.docker.io: ' + b'Get \\"https://registry-1.docker.io/v2/\\": dial tcp: lookup registry-1.docker.io: no such host"') +DENIED = b'level=fatal msg="reading manifest latest in ghcr.io/example/private: unauthorized: authentication required"' +MISSING = b'level=fatal msg="reading manifest nope in docker.io/library/alpine: manifest unknown"' +FORBIDDEN = (b'level=fatal msg="Error parsing image name \\"docker://ghcr.io/example/private:latest\\": ' + b'Requesting bearer token: invalid status code from registry 403 (Forbidden)"') + + +def failed(stderr): + return subprocess.CompletedProcess([], 1, stdout=b'', stderr=stderr) + + +class RegistryErrorTests(unittest.TestCase): + def test_an_image_lookup_is_retried_by_skopeo(self): + with patch.object(state.subprocess, 'run', return_value=subprocess.CompletedProcess([], 0, b'{}', b'')) as run: + state.command('skopeo', 'inspect', '--raw', 'docker://example/app:latest') + self.assertEqual(run.call_args.args[0], ('skopeo', 'inspect', '--retry-times', '3', '--retry-delay', '5s', + '--raw', 'docker://example/app:latest')) + + def test_other_commands_are_run_as_given(self): + with patch.object(state.subprocess, 'run', return_value=subprocess.CompletedProcess([], 0, b'', b'')) as run: + state.command('pct', 'config', '101') + self.assertEqual(run.call_args.args[0], ('pct', 'config', '101')) + + def test_a_failure_names_its_cause_without_the_output_of_skopeo(self): + cases = {RATE: 'The registry limited the requests', + OFFLINE: 'The registry could not be reached', + DENIED: 'The registry refused access to the image', + FORBIDDEN: 'The registry refused access to the image', + MISSING: 'does not exist in the registry'} + for stderr, expected in cases.items(): + with self.subTest(expected=expected), patch.object(state.subprocess, 'run', return_value=failed(stderr)), \ + patch.object(state, 'translate', side_effect=lambda text: text): + with self.assertRaises(RuntimeError) as raised: + state.command('skopeo', 'inspect', '--raw', 'docker://example/app:latest') + self.assertIn(expected, str(raised.exception)) + self.assertNotIn('docker.io', str(raised.exception)) + + def test_an_unknown_failure_keeps_the_exit_code_message(self): + with patch.object(state.subprocess, 'run', return_value=failed(b'level=fatal msg="something else"')), \ + patch.object(state, 'translate', side_effect=lambda text: text): + with self.assertRaises(RuntimeError) as raised: + state.command('skopeo', 'inspect', '--raw', 'docker://example/app:latest') + self.assertEqual(str(raised.exception), 'skopeo failed with exit code 1') + + def test_a_digest_that_contains_error_codes_is_not_a_cause(self): + stderr = b'level=fatal msg="reading blob sha256:4290404401429ab403: unexpected status"' + self.assertIsNone(state.registry_error(stderr)) + + def test_the_update_logs_the_reason_as_one_line(self): + tree = ast.parse((REMOTE / 'oci_update_current.py').read_text()) + code = next(ast.literal_eval(node.value) for node in ast.walk(tree) + if isinstance(node, ast.Assign) and getattr(node.targets[0], 'id', None) == 'code') + with tempfile.TemporaryDirectory() as folder: + Path(folder, 'oci_installation_state.py').write_text( + 'def resolve_candidate(reference, architecture):\n' + ' raise RuntimeError("The registry could not be reached. Check the connection and try again.")\n') + result = subprocess.run([sys.executable, '-c', code, 'example/app:latest', 'amd64'], + capture_output=True, text=True, cwd=folder, env={'PYTHONPATH': folder}) + self.assertEqual(result.returncode, 1) + self.assertEqual(result.stderr.strip(), 'The registry could not be reached. Check the connection and try again.') + self.assertNotIn('Traceback', result.stderr) + + +if __name__ == '__main__': + unittest.main() diff --git a/web/data/changelog/es.md b/web/data/changelog/es.md index 0d54fa92..b8cba74f 100644 --- a/web/data/changelog/es.md +++ b/web/data/changelog/es.md @@ -132,6 +132,7 @@ ProxMenux no construye sus propios contenedores para estas aplicaciones: lee la - **Los servicios que no son páginas web se actualizan y se recuperan como los demás**: MariaDB, SSH o un servidor de voz responden por su propio protocolo, y Actualizar, Modificar y Recuperar toman esa respuesta como señal de que el servicio está en marcha. - **El backup de una operación se puede hacer en otro almacenamiento** cuando no cabe en el disco del sistema, y una operación que no tiene espacio lo indica antes de detener nada. - **Una descarga de imagen o un backup de trabajo dañados se escriben una segunda vez** antes de que nada falle, y una actualización fallida no deja ningún contenedor temporal. +- **Si un registro falla un momento, se le vuelve a preguntar** hasta tres veces antes de que una actualización se rinda, y un fallo dice por qué: el registro ha limitado las consultas, ha denegado el acceso, no tiene la imagen o no se ha podido conectar con él. - **Modificar es un menú de ajustes** ([PR #495](https://github.com/MacRimi/ProxMenux/pull/495), aportado por [@jcastro](https://github.com/jcastro)): cada fila muestra el valor actual, solo se edita el ajuste elegido, y los cambios se guardan hasta que se revisan y se aplican juntos, o se descartan. Actualizar las opciones del catálogo es una entrada más del menú. - **Gestionar se abre al instante** ([PR #493](https://github.com/MacRimi/ProxMenux/pull/493), aportado por [@jcastro](https://github.com/jcastro)): la configuración de cada aplicación solo se vuelve a leer cuando ha cambiado, así que un host con 26 aplicaciones pasa de 15 segundos a una fracción de segundo. - **Modificar y Actualizar se abren sin esperas** ([PR #498](https://github.com/MacRimi/ProxMenux/pull/498), aportado por [@jcastro](https://github.com/jcastro)): el espacio para el backup se comprueba una vez aceptada la operación, y la tecla Esc vuelve atrás al instante.