fix(monitor): keep Docker web links and schemes accurate

- Reuse the Find snapshot for web links only after a complete search
- Skip unassigned or ranged host ports instead of dropping the container
- Prefill the https scheme of published HTTPS ports in the editor
- Run the Docker discovery tests when they change
- Translate the Docker search texts
This commit is contained in:
MacRimi
2026-10-19 17:12:28 +02:00
parent e44db4897b
commit 7bf1e1e34d
9 changed files with 95 additions and 14 deletions
@@ -13,6 +13,8 @@ on:
- 'AppImage/scripts/*.py'
- 'AppImage/scripts/tests/test_health_persistence_recurrence.py'
- 'AppImage/scripts/tests/test_native_app_discovery.py'
- 'AppImage/scripts/tests/test_docker_app_discovery.py'
- 'AppImage/scripts/tests/fixtures/docker_discovery_inspect.json'
- 'AppImage/app/**'
- 'AppImage/components/**'
- 'AppImage/hooks/**'
@@ -34,6 +36,8 @@ on:
- 'AppImage/scripts/*.py'
- 'AppImage/scripts/tests/test_health_persistence_recurrence.py'
- 'AppImage/scripts/tests/test_native_app_discovery.py'
- 'AppImage/scripts/tests/test_docker_app_discovery.py'
- 'AppImage/scripts/tests/fixtures/docker_discovery_inspect.json'
- 'AppImage/app/**'
- 'AppImage/components/**'
- 'AppImage/hooks/**'
+8 -1
View File
@@ -1748,7 +1748,14 @@
"nativeScanPartial": "Die Anwendungssuche ist unvollständig. Prüfen Sie die gefundenen Hinweise und versuchen Sie es erneut.",
"nativeVersionUnknown": "Installierte Version unbekannt",
"nativeEvidenceStale": "Die Hinweise sind möglicherweise veraltet oder unvollständig.",
"nativeRegisterOnly": "Bestätigen Sie die Registrierung im Editor. Eine bekannte Version wird als manueller Wert gespeichert; es wird kein Updater eingerichtet."
"nativeRegisterOnly": "Bestätigen Sie die Registrierung im Editor. Eine bekannte Version wird als manueller Wert gespeichert; es wird kein Updater eingerichtet.",
"dockerScanFailed": "Die Docker-Suche konnte nicht abgeschlossen werden. Frühere Ergebnisse sind möglicherweise veraltet; versuchen Sie es erneut.",
"dockerScanPartial": "Die Docker-Suche ist unvollständig. Einige Container konnten nicht geprüft werden.",
"dockerEvidenceContainer": "Docker-Container",
"dockerEvidenceImage": "Image",
"dockerEvidencePort": "Veröffentlichter TCP-Port",
"dockerEvidenceProject": "Compose-Projekt",
"dockerEndpointReview": "Container mit veröffentlichten TCP-Ports. Bestätigen Sie Namen und Webadresse vor dem Speichern. Die Registrierung richtet keine Updates ein."
},
"statusFilter": {
"ariaLabel": "Virtuelle Maschinen und Container filtern",
+7
View File
@@ -1734,6 +1734,13 @@
"nativeVersionUnknown": "Versión instalada desconocida",
"nativeEvidenceStale": "Lo detectado puede estar desactualizado o incompleto.",
"nativeRegisterOnly": "Confirma el registro en el editor. Una versión conocida se guarda como valor manual; no se configura ningún actualizador.",
"dockerScanFailed": "La búsqueda en Docker no pudo terminar. Los resultados anteriores pueden estar desactualizados; vuelve a intentarlo.",
"dockerScanPartial": "La búsqueda en Docker está incompleta. No se pudieron revisar algunos contenedores.",
"dockerEvidenceContainer": "Contenedor Docker",
"dockerEvidenceImage": "Imagen",
"dockerEvidencePort": "Puerto TCP publicado",
"dockerEvidenceProject": "Proyecto Compose",
"dockerEndpointReview": "Contenedor con puertos TCP publicados. Confirma el nombre y la dirección web antes de guardar. El registro no configura actualizaciones.",
"noNewApplicationsDetected": "No se han detectado aplicaciones nuevas.",
"oneNewApplicationDetected": "Se ha detectado una aplicación nueva.",
"newApplicationsDetected": "Se han detectado {count} aplicaciones nuevas.",
+8 -1
View File
@@ -1748,7 +1748,14 @@
"nativeScanPartial": "La recherche d'applications est incomplète. Vérifiez les éléments trouvés et réessayez.",
"nativeVersionUnknown": "Version installée inconnue",
"nativeEvidenceStale": "Les éléments trouvés peuvent être obsolètes ou incomplets.",
"nativeRegisterOnly": "Confirmez l'enregistrement dans l'éditeur. Une version connue est enregistrée comme valeur manuelle ; aucun outil de mise à jour n'est configuré."
"nativeRegisterOnly": "Confirmez l'enregistrement dans l'éditeur. Une version connue est enregistrée comme valeur manuelle ; aucun outil de mise à jour n'est configuré.",
"dockerScanFailed": "La recherche Docker n'a pas pu se terminer. Les résultats précédents peuvent être obsolètes ; réessayez.",
"dockerScanPartial": "La recherche Docker est incomplète. Certains conteneurs n'ont pas pu être vérifiés.",
"dockerEvidenceContainer": "Conteneur Docker",
"dockerEvidenceImage": "Image",
"dockerEvidencePort": "Port TCP publié",
"dockerEvidenceProject": "Projet Compose",
"dockerEndpointReview": "Conteneur avec des ports TCP publiés. Confirmez le nom et l'adresse web avant d'enregistrer. L'enregistrement ne configure pas les mises à jour."
},
"statusFilter": {
"ariaLabel": "Filtrer les machines virtuelles et les conteneurs",
+8 -1
View File
@@ -1748,7 +1748,14 @@
"nativeScanPartial": "La ricerca delle applicazioni è incompleta. Controlla gli elementi trovati e riprova.",
"nativeVersionUnknown": "Versione installata sconosciuta",
"nativeEvidenceStale": "Gli elementi trovati potrebbero non essere aggiornati o essere incompleti.",
"nativeRegisterOnly": "Conferma la registrazione nell'editor. Una versione nota viene salvata come valore manuale; non viene configurato alcun aggiornamento automatico."
"nativeRegisterOnly": "Conferma la registrazione nell'editor. Una versione nota viene salvata come valore manuale; non viene configurato alcun aggiornamento automatico.",
"dockerScanFailed": "La ricerca Docker non è stata completata. I risultati precedenti potrebbero non essere aggiornati; riprova.",
"dockerScanPartial": "La ricerca Docker è incompleta. Non è stato possibile controllare alcuni container.",
"dockerEvidenceContainer": "Container Docker",
"dockerEvidenceImage": "Immagine",
"dockerEvidencePort": "Porta TCP pubblicata",
"dockerEvidenceProject": "Progetto Compose",
"dockerEndpointReview": "Container con porte TCP pubblicate. Conferma il nome e l'indirizzo web prima di salvare. La registrazione non configura gli aggiornamenti."
},
"statusFilter": {
"ariaLabel": "Filtra macchine virtuali e container",
+8 -1
View File
@@ -1748,7 +1748,14 @@
"nativeScanPartial": "A pesquisa de aplicativos está incompleta. Revise os itens encontrados e tente novamente.",
"nativeVersionUnknown": "Versão instalada desconhecida",
"nativeEvidenceStale": "Os itens encontrados podem estar desatualizados ou incompletos.",
"nativeRegisterOnly": "Confirme o registro no editor. Uma versão conhecida é salva como valor manual; nenhum atualizador é configurado."
"nativeRegisterOnly": "Confirme o registro no editor. Uma versão conhecida é salva como valor manual; nenhum atualizador é configurado.",
"dockerScanFailed": "A pesquisa no Docker não pôde ser concluída. Os resultados anteriores podem estar desatualizados; tente novamente.",
"dockerScanPartial": "A pesquisa no Docker está incompleta. Não foi possível verificar alguns contentores.",
"dockerEvidenceContainer": "Contentor Docker",
"dockerEvidenceImage": "Imagem",
"dockerEvidencePort": "Porta TCP publicada",
"dockerEvidenceProject": "Projeto Compose",
"dockerEndpointReview": "Contentor com portas TCP publicadas. Confirme o nome e o endereço web antes de guardar. O registo não configura atualizações."
},
"statusFilter": {
"ariaLabel": "Filtrar máquinas virtuais e contêineres",
+8 -1
View File
@@ -1748,7 +1748,14 @@
"nativeScanPartial": "Programsökningen är ofullständig. Granska det som hittades och försök igen.",
"nativeVersionUnknown": "Installerad version okänd",
"nativeEvidenceStale": "Det som hittades kan vara inaktuellt eller ofullständigt.",
"nativeRegisterOnly": "Bekräfta registreringen i redigeraren. En känd version sparas som ett manuellt värde; ingen uppdatering konfigureras."
"nativeRegisterOnly": "Bekräfta registreringen i redigeraren. En känd version sparas som ett manuellt värde; ingen uppdatering konfigureras.",
"dockerScanFailed": "Docker-sökningen kunde inte slutföras. Tidigare resultat kan vara inaktuella; försök igen.",
"dockerScanPartial": "Docker-sökningen är ofullständig. Vissa containrar kunde inte kontrolleras.",
"dockerEvidenceContainer": "Docker-container",
"dockerEvidenceImage": "Avbild",
"dockerEvidencePort": "Publicerad TCP-port",
"dockerEvidenceProject": "Compose-projekt",
"dockerEndpointReview": "Container med publicerade TCP-portar. Bekräfta namnet och webbadressen innan du sparar. Registreringen konfigurerar inga uppdateringar."
},
"statusFilter": {
"ariaLabel": "Filtrera virtuella maskiner och containrar",
+15 -9
View File
@@ -116,18 +116,19 @@ def public_ports(record):
if not isinstance(value, dict):
raise ValueError('invalid binding')
address = value.get('HostIp') or '0.0.0.0'
raw = value.get('HostPort')
# An unassigned ("") or ranged host port is not a single address.
if not isinstance(raw, str) or not raw.isdecimal():
continue
try:
ip = ipaddress.ip_address(address)
if ip.is_loopback or getattr(ip, 'ipv4_mapped', None) and ip.ipv4_mapped.is_loopback:
continue
raw = value.get('HostPort')
if not isinstance(raw, str) or not raw.isdecimal():
raise ValueError('invalid port')
outer = int(raw)
except (ValueError, TypeError):
raise ValueError('invalid binding') from None
continue
if ip.is_loopback or getattr(ip, 'ipv4_mapped', None) and ip.ipv4_mapped.is_loopback:
continue
outer = int(raw)
if not 1 <= outer <= 65535:
raise ValueError('invalid port')
continue
result[outer] = {'host_port': outer, 'container_port': inner,
'scheme': 'https' if inner in {443, 4443, 8443, 9443} or outer in {443, 4443, 8443, 9443} else 'http'}
return sorted(result.values(), key=lambda item: item['host_port'])
@@ -167,6 +168,9 @@ def _candidate(vmid, record):
'container_name': record['name'], 'name': name,
'installed_version': record['version'], 'evidence': evidence,
'default_ports': [port['host_port'] for port in record['endpoints'] if port['container_port'] in WEB_PORTS],
'port_details': [{'port': port['host_port'], 'scheme': port['scheme'], 'path': None,
'description': '', 'logo_url': ''}
for port in record['endpoints'] if port['container_port'] in WEB_PORTS] or None,
'tracking_suggestion': None}
@@ -272,7 +276,9 @@ def cached_web_links(vmid, lookup):
"""Reuse a Find snapshot in existing web-link suggestions without another scan."""
with _mutex:
value = copy.deepcopy(_cache.get(int(vmid)))
if not value or value.get('_retired') or time.time() - value['_attempt_at'] >= SNAPSHOT_SECONDS:
# Only a complete search lists every container; otherwise probe as before.
if (not value or value.get('_retired') or value.get('status') != 'complete'
or time.time() - value['_attempt_at'] >= SNAPSHOT_SECONDS):
return None
result = []
for record in value.get('_containers', []):
@@ -100,6 +100,21 @@ class Scan(unittest.TestCase):
self.assertEqual(item['default_ports'],[8080])
self.assertIn({'kind':'tcp_listener','value':'21115'},item['evidence'])
def test_unassigned_or_ranged_host_ports_are_skipped_not_invalid(self):
app = record(running=False, bindings={'8080/tcp': [{'HostIp': '', 'HostPort': ''}],
'9000/tcp': [{'HostIp': '', 'HostPort': '9000-9010'}],
'443/tcp': [{'HostIp': '', 'HostPort': '8444'}]})
result = self.scan(records=[app])
self.assertEqual(result['status'], 'complete')
endpoints = result['_containers'][0]['endpoints']
self.assertEqual([e['host_port'] for e in endpoints], [8444])
def test_https_container_port_prefills_its_scheme(self):
app = record(bindings={'443/tcp': [{'HostIp': '0.0.0.0', 'HostPort': '8444'}]})
item, = self.scan(records=[app])['candidates']
self.assertEqual(item['default_ports'], [8444])
self.assertEqual([(d['port'], d['scheme']) for d in item['port_details']], [(8444, 'https')])
def test_empty_engine_is_complete_and_missing_docker_never_calls_docker(self):
self.assertEqual(self.scan(ids=[])['status'], 'complete')
self.calls.clear()
@@ -202,6 +217,20 @@ class Cache(unittest.TestCase):
self.assertEqual(lxc_apps._probe_docker_web_links(101)[0]['host_port'], 8080)
execute.assert_not_called()
def test_incomplete_search_never_replaces_the_web_link_probe(self):
lookup = Mock(return_value={'slug': None, 'name': 'web', 'logo_url': None})
partial = {**copy.deepcopy(self.value), 'status': 'partial', 'issues': ['inspect_failed']}
with patch.object(docker, 'scan', return_value=partial), patch.object(docker.time, 'time', return_value=1000):
docker.discover(101)
with patch.object(docker.time, 'time', return_value=1001):
self.assertIsNone(docker.cached_web_links(101, lookup))
docker._cache.clear()
with patch.object(docker, 'scan', side_effect=native.ProbeError('timeout')), \
patch.object(docker.time, 'time', return_value=1000):
docker.discover(101)
with patch.object(docker.time, 'time', return_value=1001):
self.assertIsNone(docker.cached_web_links(101, lookup))
def test_shared_links_keep_existing_name_then_port_order(self):
value = copy.deepcopy(self.value)
value['_containers'][0]['name'] = 'z-last'