mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
fix(oci): services show their address instead of a web link, and applications are not asked for their own
This commit is contained in:
@@ -269,6 +269,9 @@ interface OciInstance {
|
||||
port: number | null
|
||||
path: string | null
|
||||
website: string | null
|
||||
// The ports of an application with no web address: a service that is
|
||||
// reached on them and has no page to open.
|
||||
service_ports?: { port: number; protocol: string }[] | null
|
||||
}
|
||||
|
||||
interface Suggestions {
|
||||
@@ -2904,6 +2907,19 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }:
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* A service has no page to open: its address is shown as
|
||||
text, on the ports of its container. */}
|
||||
{!app.ports?.length && app.installed_via === "oci_image" && !!ctIp && ctIp !== "DHCP"
|
||||
&& !!suggestions?.oci_instance?.service_ports?.length && (
|
||||
<div className="mb-3 flex flex-col gap-1">
|
||||
{suggestions.oci_instance.service_ports.map((p) => (
|
||||
<span key={`${p.port}/${p.protocol}`} className="font-mono text-sm text-foreground">
|
||||
{ctIp.split("/")[0]}:{p.port}{p.protocol === "udp" ? "/udp" : ""}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer with per-card actions — only rendered in the
|
||||
global edit mode (toggled from the "Edit" button next
|
||||
to Add another application). View mode keeps cards
|
||||
|
||||
@@ -5509,7 +5509,9 @@ def ensure_oci_registration(vmid) -> bool:
|
||||
# A database or a cache is reached by the application, not by the user.
|
||||
endpoints = []
|
||||
elif not endpoints:
|
||||
port = meta.get("endpoint_port") or next(iter(meta.get("ports") or []), None)
|
||||
# A recipe that names no web address has none: its ports are the
|
||||
# service itself, a database or an SSH server, and not a page to open.
|
||||
port = meta.get("endpoint_port")
|
||||
endpoints = [{"port": port, "scheme": meta.get("endpoint_scheme"), "path": meta.get("endpoint_path"),
|
||||
"description": "", "logo_url": ""}] if isinstance(port, int) else []
|
||||
ports = []
|
||||
@@ -5638,6 +5640,19 @@ def _oci_instance_meta(vmid) -> Optional[dict]:
|
||||
continue
|
||||
if 1 <= port <= 65535 and port not in ports:
|
||||
ports.append(port)
|
||||
# With no web address, the application is a service reached on the ports
|
||||
# of its container; they are shown as an address and never as a link. The
|
||||
# database or the cache of a stack is reached by its application only.
|
||||
service_ports = []
|
||||
if not endpoint.get("port") and not _oci_secondary_member(record):
|
||||
for entry in contract.get("ports") or []:
|
||||
try:
|
||||
port = int((entry or {}).get("container_port"))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if 1 <= port <= 65535:
|
||||
service_ports.append({"port": port,
|
||||
"protocol": "udp" if (entry or {}).get("protocol") == "udp" else "tcp"})
|
||||
template_id = str(template.get("id") or "").strip()
|
||||
# The application is reached on its first endpoint and carries its own
|
||||
# name and logo. Any other endpoint is a service of its own, such as
|
||||
@@ -5708,6 +5723,7 @@ def _oci_instance_meta(vmid) -> Optional[dict]:
|
||||
"endpoint_path": str(endpoint.get("path") or "").strip() or None,
|
||||
"ports": ports,
|
||||
"endpoints": endpoints,
|
||||
"service_ports": service_ports,
|
||||
# The exact image this container was created from. Its digest is what
|
||||
# an update is decided on; the version label is only for reading.
|
||||
"installed_digest": str(observed_image.get("manifest_digest") or "").strip() or None,
|
||||
@@ -6244,7 +6260,9 @@ def get_suggestions(vmid, force: bool = False) -> dict:
|
||||
ports = [oci_meta["endpoint_port"]] + [p for p in (oci_meta["ports"] or ports)
|
||||
if p != oci_meta["endpoint_port"]]
|
||||
elif oci_meta["ports"]:
|
||||
default_ports = list(oci_meta["ports"])
|
||||
# A service has no page to open: none of its ports is proposed
|
||||
# as a web link.
|
||||
default_ports = []
|
||||
web_hint = oci_meta["endpoint_path"] or web_hint
|
||||
if oci_meta["template_id"] == "image-adguard-home":
|
||||
# The first-run endpoint disappears once setup switches to :80.
|
||||
@@ -6289,6 +6307,7 @@ def get_suggestions(vmid, force: bool = False) -> dict:
|
||||
"port": oci_meta["endpoint_port"],
|
||||
"path": oci_meta["endpoint_path"],
|
||||
"website": oci_meta["website"],
|
||||
"service_ports": oci_meta["service_ports"],
|
||||
} if oci_meta else None,
|
||||
# Categoría preset for the primary detection — same lookup as
|
||||
# get_catalog_entry so the Register button pre-selects it.
|
||||
|
||||
@@ -105,6 +105,30 @@ class OciEndpointTests(CatalogFixture):
|
||||
self.assertNotIn('logo_url', ports[0])
|
||||
self.assertEqual(ports[1]['logo_url'], ICON)
|
||||
|
||||
def test_a_service_with_no_web_address_is_registered_without_a_link(self):
|
||||
# A database or an SSH server: its port is not a page to open.
|
||||
meta = self.install([])
|
||||
self.assertEqual(meta['service_ports'], [{'port': 8971, 'protocol': 'tcp'}])
|
||||
with patch.object(lxc_apps, '_read_sidecar', return_value=None), \
|
||||
patch.object(lxc_apps, '_oci_dismissed', return_value={}), \
|
||||
patch.object(lxc_apps, 'add_app', return_value=(True, {})) as add:
|
||||
self.assertTrue(lxc_apps.ensure_oci_registration(190))
|
||||
self.assertEqual(add.call_args.args[1]['ports'], [])
|
||||
|
||||
def test_a_service_is_shown_by_its_address_and_not_proposed_as_a_link(self):
|
||||
self.install([])
|
||||
with patch.object(lxc_apps, '_probe_listening_ports', return_value=[]), \
|
||||
patch.object(lxc_apps, '_helper_slug_meta', return_value=None), \
|
||||
patch.object(lxc_apps, '_fetch_tracking_hints', return_value={}), \
|
||||
patch.object(lxc_apps, '_oci_image_versions', return_value={'error': 'offline'}), \
|
||||
patch.object(lxc_apps, '_read_sidecar', return_value=None):
|
||||
suggestion = lxc_apps.get_suggestions(190)
|
||||
self.assertEqual(suggestion['default_ports'], [])
|
||||
self.assertEqual(suggestion['oci_instance']['service_ports'], [{'port': 8971, 'protocol': 'tcp'}])
|
||||
|
||||
def test_an_application_with_a_web_address_has_no_service_ports(self):
|
||||
self.assertEqual(self.install(ENDPOINTS)['service_ports'], [])
|
||||
|
||||
|
||||
class OciGuestVersionTests(CatalogFixture):
|
||||
def test_version_read_from_the_file_the_catalog_names(self):
|
||||
|
||||
+3
-1
@@ -284,7 +284,7 @@
|
||||
"Additional audio function(s) to be added": "Zusätzliche Audiofunktion(en) sollen hinzugefügt werden",
|
||||
"Additional media/GPU GIDs, comma-separated": "Zusätzliche Medien/GPU-GIDs, kommagetrennt",
|
||||
"Additional paths for": "Zusätzliche Pfade für",
|
||||
"Address BookStack will be opened at (for example http://192.168.1.50:6875)": "Adresse, unter der BookStack geöffnet wird (zum Beispiel http://192.168.1.50:6875)",
|
||||
"Address BookStack will be opened at (for example http://192.168.1.50)": "Adresse, unter der BookStack geöffnet wird (zum Beispiel http://192.168.1.50)",
|
||||
"Address Healthchecks will be opened at (for example http://192.168.1.50:8000)": "Adresse, unter der Healthchecks geöffnet wird (zum Beispiel http://192.168.1.50:8000)",
|
||||
"Address Speedtest Tracker will be opened at (for example http://192.168.1.50)": "Adresse, unter der Speedtest Tracker geöffnet wird (zum Beispiel http://192.168.1.50)",
|
||||
"Address of Redis or Valkey (for example redis://192.168.1.20:6379/1)": "Adresse von Redis oder Valkey (zum Beispiel redis://192.168.1.20:6379/1)",
|
||||
@@ -3593,6 +3593,7 @@
|
||||
"Manyfold is an open source, self-hosted web application for managing a collection of 3D models, particularly focused on 3D printing.": "Manyfold ist eine selbst gehostete Open-Source-Webanwendung zur Verwaltung einer Sammlung von 3D-Modellen, die sich insbesondere auf den 3D-Druck konzentriert.",
|
||||
"Mapped GID on host": "Zugeordnete GID auf dem Host",
|
||||
"Mapped UID on host": "Zugeordnete UID auf dem Host",
|
||||
"MariaDB has no web interface: applications and clients connect to port 3306 of this container.": "MariaDB hat keine Weboberfläche: Anwendungen und Clients verbinden sich mit Port 3306 dieses Containers.",
|
||||
"Mariadb is one of the most popular database servers. Made by the original developers of MySQL.": "Mariadb ist einer der beliebtesten Datenbankserver. Hergestellt von den ursprünglichen Entwicklern von MySQL.",
|
||||
"Mark the VMs/CTs to include in this job:": "Markieren Sie die VMs/CTs, die in diesen Job einbezogen werden sollen:",
|
||||
"Mastodon is a free, open-source social network server based on ActivityPub where users can follow friends and discover new ones..": "Mastodon ist ein kostenloser Open-Source-Sozialnetzwerkserver auf Basis von ActivityPub, auf dem Benutzer Freunden folgen und neue entdecken können.",
|
||||
@@ -6514,6 +6515,7 @@
|
||||
"The restored service did not pass its health check": "Der wiederhergestellte Service bestand seinen Gesundheitscheck nicht",
|
||||
"The restored service stopped; the recovery is not confirmed": "Der wiederhergestellte Dienst wurde gestoppt; die Wiederherstellung wird nicht bestätigt",
|
||||
"The reviewed Tandoor stack does not require a privileged LXC.": "Der überprüfte Tandoor-Stack requi ist kein privilegierter LXC.",
|
||||
"The root password is asked for when connecting from another machine. Inside the container, the mariadb command opens as root without a password.": "Das root-Passwort wird bei Verbindungen von einem anderen Rechner abgefragt. Im Container öffnet der Befehl mariadb als root ohne Passwort.",
|
||||
"The rootfs capture only belongs to the running installation": "Der Rootfs Capture gehört nur zur laufenden Installation",
|
||||
"The rootfs is not managed by Proxmox": "Die Rootfs werden nicht von Proxmox verwaltet",
|
||||
"The rootfs is not mounted": "Die Rootfs sind nicht montiert",
|
||||
|
||||
+3
-1
@@ -282,7 +282,7 @@
|
||||
"Additional audio function(s) to be added": "Funciones de audio adicionales que se agregarán",
|
||||
"Additional media/GPU GIDs, comma-separated": "GID adicionales de medios/GPU, separados por comas",
|
||||
"Additional paths for": "Rutas adicionales para",
|
||||
"Address BookStack will be opened at (for example http://192.168.1.50:6875)": "Dirección en la que se abrirá BookStack (por ejemplo http://192.168.1.50:6875)",
|
||||
"Address BookStack will be opened at (for example http://192.168.1.50)": "Dirección en la que se abrirá BookStack (por ejemplo http://192.168.1.50)",
|
||||
"Address Healthchecks will be opened at (for example http://192.168.1.50:8000)": "Dirección en la que se abrirá Healthchecks (por ejemplo http://192.168.1.50:8000)",
|
||||
"Address Speedtest Tracker will be opened at (for example http://192.168.1.50)": "Dirección en la que se abrirá Speedtest Tracker (por ejemplo http://192.168.1.50)",
|
||||
"Address of Redis or Valkey (for example redis://192.168.1.20:6379/1)": "Dirección de Redis o Valkey (por ejemplo redis://192.168.1.20:6379/1)",
|
||||
@@ -3589,6 +3589,7 @@
|
||||
"Manyfold is an open source, self-hosted web application for managing a collection of 3D models, particularly focused on 3D printing.": "Manyfold es una aplicación web autoalojada y de código abierto para gestionar una colección de modelos 3D, orientada sobre todo a la impresión 3D.",
|
||||
"Mapped GID on host": "GID asignado en el host",
|
||||
"Mapped UID on host": "UID asignado en el host",
|
||||
"MariaDB has no web interface: applications and clients connect to port 3306 of this container.": "MariaDB no tiene interfaz web: las aplicaciones y los clientes se conectan al puerto 3306 de este contenedor.",
|
||||
"Mariadb is one of the most popular database servers. Made by the original developers of MySQL.": "Mariadb es uno de los servidores de bases de datos más populares, creado por los desarrolladores originales de MySQL.",
|
||||
"Mark the VMs/CTs to include in this job:": "Marque las VM/CT que incluirá este trabajo:",
|
||||
"Mastodon is a free, open-source social network server based on ActivityPub where users can follow friends and discover new ones..": "Mastodon es un servidor de red social gratuito y de código abierto basado en ActivityPub, donde los usuarios pueden seguir a sus amigos y descubrir otros nuevos..",
|
||||
@@ -6490,6 +6491,7 @@
|
||||
"The restored service did not pass its health check": "El servicio restaurado no superó su comprobación de estado",
|
||||
"The restored service stopped; the recovery is not confirmed": "El servicio restaurado se detuvo; la recuperación no se confirma",
|
||||
"The reviewed Tandoor stack does not require a privileged LXC.": "La pila de Tandoor revisada no necesita un LXC privilegiado.",
|
||||
"The root password is asked for when connecting from another machine. Inside the container, the mariadb command opens as root without a password.": "La contraseña de root se pide al conectarse desde otra máquina. Dentro del contenedor, el comando mariadb abre como root sin contraseña.",
|
||||
"The rootfs capture only belongs to the running installation": "La captura del rootfs solo pertenece a la instalación en ejecución",
|
||||
"The rootfs is not managed by Proxmox": "Los rootfs no son gestionados por Proxmox",
|
||||
"The rootfs is not mounted": "Los rootfs no se montan",
|
||||
|
||||
+3
-1
@@ -284,7 +284,7 @@
|
||||
"Additional audio function(s) to be added": "Fonction(s) audio supplémentaire(s) à ajouter",
|
||||
"Additional media/GPU GIDs, comma-separated": "Autres médias/GPU GID, séparés par des virgules",
|
||||
"Additional paths for": "Voies supplémentaires pour",
|
||||
"Address BookStack will be opened at (for example http://192.168.1.50:6875)": "Adresse à laquelle BookStack sera ouvert (par exemple http://192.168.1.50:6875)",
|
||||
"Address BookStack will be opened at (for example http://192.168.1.50)": "Adresse à laquelle BookStack sera ouvert (par exemple http://192.168.1.50)",
|
||||
"Address Healthchecks will be opened at (for example http://192.168.1.50:8000)": "Adresse à laquelle Healthchecks sera ouvert (par exemple http://192.168.1.50:8000)",
|
||||
"Address Speedtest Tracker will be opened at (for example http://192.168.1.50)": "Adresse à laquelle Speedtest Tracker sera ouvert (par exemple http://192.168.1.50)",
|
||||
"Address of Redis or Valkey (for example redis://192.168.1.20:6379/1)": "Adresse de Redis ou Valkey (par exemple redis://192.168.1.20:6379/1)",
|
||||
@@ -3593,6 +3593,7 @@
|
||||
"Manyfold is an open source, self-hosted web application for managing a collection of 3D models, particularly focused on 3D printing.": "Manyfold est une application web ouverte, auto-organisée pour gérer une collection de modèles 3D, particulièrement axée sur l'impression 3D.",
|
||||
"Mapped GID on host": "GID mappé sur l'hôte",
|
||||
"Mapped UID on host": "UID mappé sur l'hôte",
|
||||
"MariaDB has no web interface: applications and clients connect to port 3306 of this container.": "MariaDB n'a pas d'interface web : les applications et les clients se connectent au port 3306 de ce conteneur.",
|
||||
"Mariadb is one of the most popular database servers. Made by the original developers of MySQL.": "Mariadb est l'un des serveurs de base de données les plus populaires. Réalisé par les développeurs originaux de MySQL.",
|
||||
"Mark the VMs/CTs to include in this job:": "Marquez les machines virtuelles/CT à inclure dans cette tâche :",
|
||||
"Mastodon is a free, open-source social network server based on ActivityPub where users can follow friends and discover new ones..": "Mastodon est un serveur de réseau social libre basé sur ActivitéPub où les utilisateurs peuvent suivre leurs amis et en découvrir de nouveaux.",
|
||||
@@ -6513,6 +6514,7 @@
|
||||
"The restored service did not pass its health check": "Le service restauré n'a pas réussi son bilan de santé",
|
||||
"The restored service stopped; the recovery is not confirmed": "Le service restauré s'est arrêté; la récupération n'est pas confirmée",
|
||||
"The reviewed Tandoor stack does not require a privileged LXC.": "La pile Tandoor revue ne require un LXC privilégié.",
|
||||
"The root password is asked for when connecting from another machine. Inside the container, the mariadb command opens as root without a password.": "Le mot de passe root est demandé lors d'une connexion depuis une autre machine. Dans le conteneur, la commande mariadb s'ouvre en tant que root sans mot de passe.",
|
||||
"The rootfs capture only belongs to the running installation": "La capture rootfs appartient uniquement à l'installation en cours d'exécution",
|
||||
"The rootfs is not managed by Proxmox": "Les roofs ne sont pas gérés par Proxmox",
|
||||
"The rootfs is not mounted": "Les roofs ne sont pas montés",
|
||||
|
||||
+3
-1
@@ -284,7 +284,7 @@
|
||||
"Additional audio function(s) to be added": "Ulteriori funzioni audio da aggiungere",
|
||||
"Additional media/GPU GIDs, comma-separated": "GID aggiuntivi per dispositivi multimediali/GPU, separati da virgole",
|
||||
"Additional paths for": "Percorsi aggiuntivi per",
|
||||
"Address BookStack will be opened at (for example http://192.168.1.50:6875)": "Indirizzo a cui verrà aperto BookStack (ad esempio http://192.168.1.50:6875)",
|
||||
"Address BookStack will be opened at (for example http://192.168.1.50)": "Indirizzo a cui verrà aperto BookStack (ad esempio http://192.168.1.50)",
|
||||
"Address Healthchecks will be opened at (for example http://192.168.1.50:8000)": "Indirizzo a cui verrà aperto Healthchecks (ad esempio http://192.168.1.50:8000)",
|
||||
"Address Speedtest Tracker will be opened at (for example http://192.168.1.50)": "Indirizzo a cui verrà aperto Speedtest Tracker (ad esempio http://192.168.1.50)",
|
||||
"Address of Redis or Valkey (for example redis://192.168.1.20:6379/1)": "Indirizzo di Redis o Valkey (ad esempio redis://192.168.1.20:6379/1)",
|
||||
@@ -3593,6 +3593,7 @@
|
||||
"Manyfold is an open source, self-hosted web application for managing a collection of 3D models, particularly focused on 3D printing.": "Manyfold è un'applicazione web open source e self-hosted per gestire collezioni di modelli 3D, in particolare per la stampa 3D.",
|
||||
"Mapped GID on host": "GID mappato sull'host",
|
||||
"Mapped UID on host": "UID mappato sull'host",
|
||||
"MariaDB has no web interface: applications and clients connect to port 3306 of this container.": "MariaDB non ha un'interfaccia web: le applicazioni e i client si connettono alla porta 3306 di questo container.",
|
||||
"Mariadb is one of the most popular database servers. Made by the original developers of MySQL.": "MariaDB è uno dei server di database più diffusi, creato dagli sviluppatori originali di MySQL.",
|
||||
"Mark the VMs/CTs to include in this job:": "Spunta le VM/CT da includere in questo job:",
|
||||
"Mastodon is a free, open-source social network server based on ActivityPub where users can follow friends and discover new ones..": "Mastodon è un server di social network gratuito e open source basato su ActivityPub, dove puoi seguire i tuoi amici e conoscere nuove persone.",
|
||||
@@ -6514,6 +6515,7 @@
|
||||
"The restored service did not pass its health check": "Il servizio ripristinato non ha superato il controllo dello stato",
|
||||
"The restored service stopped; the recovery is not confirmed": "Il servizio ripristinato si è arrestato; il recupero non è confermato",
|
||||
"The reviewed Tandoor stack does not require a privileged LXC.": "Lo stack Tandoor esaminato non richiede un container LXC privilegiato.",
|
||||
"The root password is asked for when connecting from another machine. Inside the container, the mariadb command opens as root without a password.": "La password di root viene richiesta quando ci si connette da un'altra macchina. All'interno del container, il comando mariadb si apre come root senza password.",
|
||||
"The rootfs capture only belongs to the running installation": "L'acquisizione della rootfs appartiene solo all'installazione in esecuzione",
|
||||
"The rootfs is not managed by Proxmox": "Il rootfs non è gestito da Proxmox",
|
||||
"The rootfs is not mounted": "Il rootfs non è montato",
|
||||
|
||||
+3
-1
@@ -284,7 +284,7 @@
|
||||
"Additional audio function(s) to be added": "Funções de áudio adicionais a serem adicionadas",
|
||||
"Additional media/GPU GIDs, comma-separated": "GIDs adicionais/GPU, separados por vírgulas",
|
||||
"Additional paths for": "Caminhos adicionais para",
|
||||
"Address BookStack will be opened at (for example http://192.168.1.50:6875)": "Endereço em que o BookStack será aberto (por exemplo http://192.168.1.50:6875)",
|
||||
"Address BookStack will be opened at (for example http://192.168.1.50)": "Endereço em que o BookStack será aberto (por exemplo http://192.168.1.50)",
|
||||
"Address Healthchecks will be opened at (for example http://192.168.1.50:8000)": "Endereço em que o Healthchecks será aberto (por exemplo http://192.168.1.50:8000)",
|
||||
"Address Speedtest Tracker will be opened at (for example http://192.168.1.50)": "Endereço em que o Speedtest Tracker será aberto (por exemplo http://192.168.1.50)",
|
||||
"Address of Redis or Valkey (for example redis://192.168.1.20:6379/1)": "Endereço do Redis ou Valkey (por exemplo redis://192.168.1.20:6379/1)",
|
||||
@@ -3593,6 +3593,7 @@
|
||||
"Manyfold is an open source, self-hosted web application for managing a collection of 3D models, particularly focused on 3D printing.": "Manyfold é um aplicativo web de código aberto e auto-hospedado para gerenciar uma coleção de modelos 3D, particularmente focado na impressão 3D.",
|
||||
"Mapped GID on host": "GID mapeado no host",
|
||||
"Mapped UID on host": "UID mapeado no host",
|
||||
"MariaDB has no web interface: applications and clients connect to port 3306 of this container.": "O MariaDB não tem interface web: os aplicativos e os clientes se conectam à porta 3306 deste contêiner.",
|
||||
"Mariadb is one of the most popular database servers. Made by the original developers of MySQL.": "Mariadb é um dos servidores de banco de dados mais populares. Feito pelos desenvolvedores originais do MySQL.",
|
||||
"Mark the VMs/CTs to include in this job:": "Marque as VMs/CTs a serem incluídas neste trabalho:",
|
||||
"Mastodon is a free, open-source social network server based on ActivityPub where users can follow friends and discover new ones..": "Mastodon é um servidor de rede social livre e de código aberto baseado no ActivityPub onde os usuários podem seguir amigos e descobrir novos..",
|
||||
@@ -6514,6 +6515,7 @@
|
||||
"The restored service did not pass its health check": "O serviço restaurado não passou no seu exame de saúde",
|
||||
"The restored service stopped; the recovery is not confirmed": "O serviço restaurado parou; a recuperação não é confirmada",
|
||||
"The reviewed Tandoor stack does not require a privileged LXC.": "A pilha Tandoor revisada não require um LXC privilegiado.",
|
||||
"The root password is asked for when connecting from another machine. Inside the container, the mariadb command opens as root without a password.": "A senha de root é solicitada ao conectar a partir de outra máquina. Dentro do contêiner, o comando mariadb abre como root sem senha.",
|
||||
"The rootfs capture only belongs to the running installation": "A captura do rootfs só pertence à instalação em execução",
|
||||
"The rootfs is not managed by Proxmox": "O rootfs não é gerido pelo Proxmox",
|
||||
"The rootfs is not mounted": "O rootfs não está montado",
|
||||
|
||||
+3
-1
@@ -284,7 +284,7 @@
|
||||
"Additional audio function(s) to be added": "Pridajú sa ďalšie audio funkcie",
|
||||
"Additional media/GPU GIDs, comma-separated": "Dodatočné médiá/GPU GID, čiarovo oddelené",
|
||||
"Additional paths for": "Ďalšie cesty pre",
|
||||
"Address BookStack will be opened at (for example http://192.168.1.50:6875)": "Adresa, na ktorej sa bude BookStack otvárať (napríklad http://192.168.1.50:6875)",
|
||||
"Address BookStack will be opened at (for example http://192.168.1.50)": "Adresa, na ktorej sa bude BookStack otvárať (napríklad http://192.168.1.50)",
|
||||
"Address Healthchecks will be opened at (for example http://192.168.1.50:8000)": "Adresa, na ktorej sa bude Healthchecks otvárať (napríklad http://192.168.1.50:8000)",
|
||||
"Address Speedtest Tracker will be opened at (for example http://192.168.1.50)": "Adresa, na ktorej sa bude Speedtest Tracker otvárať (napríklad http://192.168.1.50)",
|
||||
"Address of Redis or Valkey (for example redis://192.168.1.20:6379/1)": "Adresa Redis alebo Valkey (napríklad redis://192.168.1.20:6379/1)",
|
||||
@@ -3593,6 +3593,7 @@
|
||||
"Manyfold is an open source, self-hosted web application for managing a collection of 3D models, particularly focused on 3D printing.": "Manyfold je open source, self-hosted webová aplikácia pre správu kolekcie 3D modelov, najmä zamerané na 3D tlač.",
|
||||
"Mapped GID on host": "GID namapované na serveri",
|
||||
"Mapped UID on host": "UID namapované na serveri",
|
||||
"MariaDB has no web interface: applications and clients connect to port 3306 of this container.": "MariaDB nemá webové rozhranie: aplikácie a klienti sa pripájajú na port 3306 tohto kontajnera.",
|
||||
"Mariadb is one of the most popular database servers. Made by the original developers of MySQL.": "Mariadb je jedným z najpopulárnejších databázových serverov. Vyrobený pôvodnými vývojármi MySQL.",
|
||||
"Mark the VMs/CTs to include in this job:": "Označte VM/CT, ktoré chcete zahrnúť do tejto úlohy:",
|
||||
"Mastodon is a free, open-source social network server based on ActivityPub where users can follow friends and discover new ones..": "Mastodon je bezplatný, open-source server sociálnej siete založený na ActivityPub, kde používatelia môžu sledovať priateľov a objavovať nové..",
|
||||
@@ -6513,6 +6514,7 @@
|
||||
"The restored service did not pass its health check": "Obnovená služba neprešla kontrolou stavu",
|
||||
"The restored service stopped; the recovery is not confirmed": "Obnovená služba sa zastavila; obnova sa nepotvrdila",
|
||||
"The reviewed Tandoor stack does not require a privileged LXC.": "Preskúmaný Tandoor stack require privilegovaný LXC.",
|
||||
"The root password is asked for when connecting from another machine. Inside the container, the mariadb command opens as root without a password.": "Heslo používateľa root sa vyžaduje pri pripojení z iného počítača. Vnútri kontajnera sa príkaz mariadb otvorí ako root bez hesla.",
|
||||
"The rootfs capture only belongs to the running installation": "Rootfs capture patrí len do bežiacej inštalácie",
|
||||
"The rootfs is not managed by Proxmox": "Rootfs nie je riadený Proxmox",
|
||||
"The rootfs is not mounted": "Korene nie sú namontované",
|
||||
|
||||
+3
-1
@@ -284,7 +284,7 @@
|
||||
"Additional audio function(s) to be added": "Ytterligare ljudfunktion(er) ska läggas till",
|
||||
"Additional media/GPU GIDs, comma-separated": "Ytterligare media/GPU GID, comma-separerade",
|
||||
"Additional paths for": "Ytterligare vägar för",
|
||||
"Address BookStack will be opened at (for example http://192.168.1.50:6875)": "Adress som BookStack öppnas på (till exempel http://192.168.1.50:6875)",
|
||||
"Address BookStack will be opened at (for example http://192.168.1.50)": "Adress som BookStack öppnas på (till exempel http://192.168.1.50)",
|
||||
"Address Healthchecks will be opened at (for example http://192.168.1.50:8000)": "Adress som Healthchecks öppnas på (till exempel http://192.168.1.50:8000)",
|
||||
"Address Speedtest Tracker will be opened at (for example http://192.168.1.50)": "Adress som Speedtest Tracker öppnas på (till exempel http://192.168.1.50)",
|
||||
"Address of Redis or Valkey (for example redis://192.168.1.20:6379/1)": "Redis eller Valkeys adress (till exempel redis://192.168.1.20:6379/1)",
|
||||
@@ -3593,6 +3593,7 @@
|
||||
"Manyfold is an open source, self-hosted web application for managing a collection of 3D models, particularly focused on 3D printing.": "Manyfold är en öppen källkod, självvärd webbapplikation för att hantera en samling av 3D-modeller, särskilt fokuserad på 3D-utskrift.",
|
||||
"Mapped GID on host": "Mappad GID på värd",
|
||||
"Mapped UID on host": "Mappat UID på värd",
|
||||
"MariaDB has no web interface: applications and clients connect to port 3306 of this container.": "MariaDB har inget webbgränssnitt: applikationer och klienter ansluter till port 3306 på den här behållaren.",
|
||||
"Mariadb is one of the most popular database servers. Made by the original developers of MySQL.": "Mariadb är en av de mest populära databasservrarna. Tillverkad av de ursprungliga utvecklarna av MySQL.",
|
||||
"Mark the VMs/CTs to include in this job:": "Markera de virtuella datorerna/CT:erna som ska inkluderas i det här jobbet:",
|
||||
"Mastodon is a free, open-source social network server based on ActivityPub where users can follow friends and discover new ones..": "Mastodon är en gratis, öppen källkod sociala nätverksserver baserad på ActivityPub där användare kan följa vänner och upptäcka nya.",
|
||||
@@ -6514,6 +6515,7 @@
|
||||
"The restored service did not pass its health check": "Den återställda tjänsten passerade inte hälsokontrollen",
|
||||
"The restored service stopped; the recovery is not confirmed": "Den återställda tjänsten stannade; återhämtningen bekräftas inte",
|
||||
"The reviewed Tandoor stack does not require a privileged LXC.": "Den granskade Tandoor stack inte require en privilegierad LXC.",
|
||||
"The root password is asked for when connecting from another machine. Inside the container, the mariadb command opens as root without a password.": "Root-lösenordet efterfrågas vid anslutning från en annan dator. Inne i behållaren öppnas kommandot mariadb som root utan lösenord.",
|
||||
"The rootfs capture only belongs to the running installation": "Rotfarna fångar bara tillhör den löpande installationen",
|
||||
"The rootfs is not managed by Proxmox": "Rötterna hanteras inte av Proxmox",
|
||||
"The rootfs is not mounted": "Rötterna är inte monterade",
|
||||
|
||||
@@ -117,7 +117,8 @@
|
||||
},
|
||||
"environment_overrides": {
|
||||
"APP_URL": {
|
||||
"prompt": "Address BookStack will be opened at (for example http://192.168.1.50:6875)"
|
||||
"prompt": "Address BookStack will be opened at (for example http://192.168.1.50)",
|
||||
"container_address": "http://@CONTAINER_ADDRESS@"
|
||||
},
|
||||
"DB_HOST": {
|
||||
"prompt": "Address of the MariaDB server"
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"catalog_ui": {
|
||||
"launch": {
|
||||
"port": null
|
||||
}
|
||||
},
|
||||
"first_run": {
|
||||
"endpoints": []
|
||||
},
|
||||
"proxmox": {
|
||||
"installer_profile": {
|
||||
"volume_preparations": [
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"required": false
|
||||
},
|
||||
"SITE_ROOT": {
|
||||
"prompt": "Address Healthchecks will be opened at (for example http://192.168.1.50:8000)"
|
||||
"prompt": "Address Healthchecks will be opened at (for example http://192.168.1.50:8000)",
|
||||
"container_address": "http://@CONTAINER_ADDRESS@:8000"
|
||||
},
|
||||
"SITE_NAME": {
|
||||
"example": "Healthchecks"
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
{
|
||||
"catalog_ui": {
|
||||
"launch": {
|
||||
"port": null
|
||||
}
|
||||
},
|
||||
"first_run": {
|
||||
"endpoints": []
|
||||
},
|
||||
"proxmox": {
|
||||
"installer_profile": {
|
||||
"completion_notes": [
|
||||
"MariaDB has no web interface: applications and clients connect to port 3306 of this container.",
|
||||
"The root password is asked for when connecting from another machine. Inside the container, the mariadb command opens as root without a password."
|
||||
],
|
||||
"volume_preparations": [
|
||||
{
|
||||
"container_path": "/config",
|
||||
|
||||
@@ -215,7 +215,8 @@
|
||||
"prompt": "Email address of the admin account"
|
||||
},
|
||||
"ALLOWED_HOST": {
|
||||
"prompt": "Name or address NetBox will be opened at (for example netbox.example.com)"
|
||||
"prompt": "Name or address NetBox will be opened at (for example netbox.example.com)",
|
||||
"container_address": "@CONTAINER_ADDRESS@"
|
||||
},
|
||||
"DB_HOST": {
|
||||
"prompt": "Address of the PostgreSQL server"
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"catalog_ui": {
|
||||
"launch": {
|
||||
"port": null
|
||||
}
|
||||
},
|
||||
"first_run": {
|
||||
"endpoints": []
|
||||
},
|
||||
"proxmox": {
|
||||
"installer_profile": {
|
||||
"volume_preparations": [
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"catalog_ui": {
|
||||
"launch": {
|
||||
"port": null
|
||||
}
|
||||
},
|
||||
"first_run": {
|
||||
"endpoints": []
|
||||
},
|
||||
"proxmox": {
|
||||
"installer_profile": {
|
||||
"volume_preparations": [
|
||||
|
||||
@@ -142,7 +142,8 @@
|
||||
},
|
||||
"environment_overrides": {
|
||||
"APP_URL": {
|
||||
"prompt": "Address Speedtest Tracker will be opened at (for example http://192.168.1.50)"
|
||||
"prompt": "Address Speedtest Tracker will be opened at (for example http://192.168.1.50)",
|
||||
"container_address": "http://@CONTAINER_ADDRESS@"
|
||||
},
|
||||
"SPEEDTEST_SCHEDULE": {
|
||||
"example": "0 */6 * * *",
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"catalog_ui": {
|
||||
"launch": {
|
||||
"port": null
|
||||
}
|
||||
},
|
||||
"first_run": {
|
||||
"endpoints": []
|
||||
},
|
||||
"proxmox": {
|
||||
"installer_profile": {
|
||||
"volume_preparations": [
|
||||
|
||||
@@ -2060,6 +2060,60 @@ CONSOLE_STATE=$(python3 "${SCRIPT_DIR}/oci_console.py" configure "$VMID") \
|
||||
|| die "$(translate "The console of the container could not be configured")"
|
||||
RUNTIME_CONSOLE_LOG=$(jq -r '.log' <<<"$CONSOLE_STATE")
|
||||
|
||||
# A variable that holds the application's own address takes the one of the
|
||||
# container: the fixed one, or the one DHCP gives it. That one is asked for
|
||||
# with the application not started yet, so that its first start already has it.
|
||||
OWN_ADDRESS_MARK='@CONTAINER_ADDRESS@'
|
||||
OWN_ADDRESS=""
|
||||
apply_own_address() {
|
||||
local names name value original inert="" candidate attempt
|
||||
names=$(jq -r --arg mark "$OWN_ADDRESS_MARK" \
|
||||
'.environment[]? | select(.value | contains($mark)) | .name' "$DEPLOYMENT_FILE")
|
||||
[[ -n $names ]] || return 0
|
||||
msg_info "$(translate "Waiting for the network address...")"
|
||||
if [[ -n ${HOST_MONITOR:-} ]]; then
|
||||
OWN_ADDRESS=$HOST_MONITOR_IP
|
||||
elif [[ $IPV4 != dhcp ]]; then
|
||||
OWN_ADDRESS=${IPV4%%/*}
|
||||
else
|
||||
mount_ct_rootfs
|
||||
for candidate in /bin/sleep /usr/bin/sleep; do
|
||||
if [[ -e /var/lib/lxc/${VMID}/rootfs${candidate} || -L /var/lib/lxc/${VMID}/rootfs${candidate} ]]; then
|
||||
inert=$candidate
|
||||
break
|
||||
fi
|
||||
done
|
||||
oci_quiet pct unmount "$VMID" || true
|
||||
if [[ -z $inert ]]; then
|
||||
oci_log "The image has no sleep command to wait for the DHCP address with"
|
||||
die "$(translate "No LAN address was obtained")"
|
||||
fi
|
||||
original=$(pct config "$VMID" | sed -n 's/^entrypoint: //p')
|
||||
oci_quiet pct set "$VMID" --entrypoint "$inert 120" \
|
||||
|| die "$(translate "Could not set the container entrypoint")"
|
||||
oci_quiet pct start "$VMID" || die "$(translate "The container could not be started:") CT $VMID"
|
||||
for attempt in $(seq 1 60); do
|
||||
OWN_ADDRESS=$(own_ipv4 2>/dev/null) \
|
||||
|| OWN_ADDRESS=$(lxc-info -n "$VMID" -iH 2>/dev/null | grep -m1 -E '^[0-9]+\.' || true)
|
||||
[[ -n $OWN_ADDRESS ]] && break
|
||||
sleep 0.5
|
||||
done
|
||||
oci_quiet pct stop "$VMID" || die "$(translate "The container could not be started:") CT $VMID"
|
||||
if [[ -n $original ]]; then
|
||||
oci_quiet pct set "$VMID" --entrypoint "$original" || die "$(translate "Could not set the container entrypoint")"
|
||||
else
|
||||
oci_quiet pct set "$VMID" --delete entrypoint || die "$(translate "Could not set the container entrypoint")"
|
||||
fi
|
||||
fi
|
||||
[[ $OWN_ADDRESS =~ ^[0-9]+(\.[0-9]+){3}$ ]] || die "$(translate "No LAN address was obtained")"
|
||||
while IFS= read -r name; do
|
||||
value=$(jq -r --arg name "$name" '[.environment[] | select(.name == $name) | .value] | last' "$DEPLOYMENT_FILE")
|
||||
set_runtime_environment "$name" "${value//$OWN_ADDRESS_MARK/$OWN_ADDRESS}"
|
||||
done <<<"$names"
|
||||
msg_ok "$(translate "IP address:") $OWN_ADDRESS"
|
||||
}
|
||||
apply_own_address
|
||||
|
||||
oci_log "Configuration created for CT $VMID"
|
||||
PASSWORD_STATE=""
|
||||
if [[ $START_AFTER == 1 ]]; then
|
||||
@@ -2279,6 +2333,13 @@ URLS=$(jq -c --arg ip "$IP" '
|
||||
}]
|
||||
else [] end
|
||||
' "$TEMPLATE_FILE")
|
||||
# An application with no web address is reached on the ports of its container.
|
||||
SERVICE_ADDRESSES=$(jq -c --arg ip "$IP" '
|
||||
if $ip == "" or ((.first_run.endpoints? // []) | length) > 0 or .catalog_ui.launch.port != null then []
|
||||
else [.container_contract.ports[]?
|
||||
| $ip + ":" + (.container_port | tostring) + (if .protocol == "udp" then "/udp" else "" end)]
|
||||
end
|
||||
' "$TEMPLATE_FILE")
|
||||
if [[ -n $HAOS_URLS ]]; then
|
||||
URLS=$HAOS_URLS
|
||||
elif [[ $HAOS_HEALTHCHECK != 0 ]]; then
|
||||
@@ -2293,7 +2354,7 @@ elif [[ $START_AFTER == 1 ]]; then
|
||||
check_native_device_permissions
|
||||
fi
|
||||
if [[ -z ${PROXMENUX_OCI_TRANSACTION:-} ]] && ! oci_quiet python3 "${SCRIPT_DIR}/oci_instances.py" complete "$VMID" \
|
||||
--archive "$ARCHIVE_PATH" --digest "$DIGEST"; then
|
||||
--archive "$ARCHIVE_PATH" --digest "$DIGEST" ${OWN_ADDRESS:+--address "$OWN_ADDRESS"}; then
|
||||
msg_warn "$(translate "Container installed, but without a verifiable record for future updates.")"
|
||||
fi
|
||||
# The template's notes, then those of this installation's choices, such as the
|
||||
@@ -2309,9 +2370,10 @@ RESULT=$(jq -cn \
|
||||
--arg image_created "$CREATED" \
|
||||
--arg ip "$IP" \
|
||||
--argjson urls "$URLS" \
|
||||
--argjson service_addresses "$SERVICE_ADDRESSES" \
|
||||
--argjson credentials "$CREDENTIALS" \
|
||||
--argjson completion_notes "$COMPLETION_NOTES" \
|
||||
--argjson vmid "$VMID" \
|
||||
--arg log "${OCI_LOG:-}" \
|
||||
'{app_id:$app_id,vmid:$vmid,image:$image,digest:$digest,architecture:$architecture,image_version:$image_version,image_created:$image_created,ip:$ip,urls:$urls,credentials:$credentials,completion_notes:$completion_notes,log:$log}')
|
||||
'{app_id:$app_id,vmid:$vmid,image:$image,digest:$digest,architecture:$architecture,image_version:$image_version,image_created:$image_created,ip:$ip,urls:$urls,service_addresses:$service_addresses,credentials:$credentials,completion_notes:$completion_notes,log:$log}')
|
||||
printf 'PROXMENUX_RESULT=%s\n' "$(printf '%s' "$RESULT" | base64 | tr -d '\n')"
|
||||
|
||||
@@ -87,6 +87,13 @@ def render(template, digest, instance_id, ip=''):
|
||||
item = f'🌐 {link(str(endpoint.get("label") or "Web UI"), url)}: {link(url, url)}'
|
||||
if item:
|
||||
access.append(item)
|
||||
if not access:
|
||||
# With no web address, the application is reached on its ports.
|
||||
for item in (template.get('container_contract') or {}).get('ports') or []:
|
||||
port = item.get('container_port')
|
||||
if isinstance(port, int) and 1 <= port <= 65535:
|
||||
protocol = '/udp' if item.get('protocol') == 'udp' else ''
|
||||
access.append(f'🔌 <code>{address}:{port}{protocol}</code>')
|
||||
badges = (
|
||||
('Docs', DOCS, 'https://img.shields.io/badge/%F0%9F%93%9A_Docs-blue'),
|
||||
('Code', CODE, 'https://img.shields.io/badge/%F0%9F%92%BB_Code-green'),
|
||||
|
||||
@@ -24,6 +24,8 @@ from oci_ui import translate, msg_error, msg_ok
|
||||
|
||||
ROOT = Path('/usr/local/share/proxmenux/oci/instances')
|
||||
MARKER = 'proxmenux-instance='
|
||||
# Stands for the address of the container until the installation knows it.
|
||||
ADDRESS_MARK = '@CONTAINER_ADDRESS@'
|
||||
ACTIVE = {'installing', 'assembling', 'updating', 'recovering'}
|
||||
|
||||
|
||||
@@ -160,8 +162,13 @@ def observe(vmid, installation_id, archive, digest, image=None):
|
||||
return observed
|
||||
|
||||
|
||||
def finish(root, vmid, archive, digest):
|
||||
def finish(root, vmid, archive, digest, address=None):
|
||||
value = read(root, vmid)
|
||||
if address:
|
||||
# The address the installation gave the application is recorded as the
|
||||
# value it is, the way one typed by the user would be.
|
||||
for item in value['deployment'].get('environment', []):
|
||||
item['value'] = item['value'].replace(ADDRESS_MARK, address)
|
||||
observed = observe(vmid, value['installation_id'], archive, digest)
|
||||
value.update(status='assembling' if value['deployment'].get('stack_managed') else 'installed',
|
||||
completed_at=now(), observed=observed)
|
||||
@@ -356,6 +363,7 @@ def main():
|
||||
if action == 'complete':
|
||||
p.add_argument('--archive', required=True)
|
||||
p.add_argument('--digest', required=True)
|
||||
p.add_argument('--address')
|
||||
sub.add_parser('reconcile')
|
||||
resume = sub.add_parser('resume-stack-recording')
|
||||
resume.add_argument('vmid', type=int)
|
||||
@@ -372,7 +380,7 @@ def main():
|
||||
json.loads(Path(args.deployment).read_text()))
|
||||
print(value['installation_id'])
|
||||
elif args.action == 'complete':
|
||||
finish(args.root, args.vmid, args.archive, args.digest)
|
||||
finish(args.root, args.vmid, args.archive, args.digest, args.address)
|
||||
elif args.action == 'failed':
|
||||
value = read(args.root, args.vmid)
|
||||
value.update(status='failed', failed_at=now())
|
||||
|
||||
@@ -133,6 +133,7 @@
|
||||
"example": {"type": ["string", "null"]},
|
||||
"prompt": {"type": "string", "minLength": 1},
|
||||
"prompt_user": {"type": "boolean"},
|
||||
"container_address": {"type": "string", "pattern": "@CONTAINER_ADDRESS@"},
|
||||
"required": {"type": "boolean"},
|
||||
"sensitive": {"type": "boolean"},
|
||||
"source": {"type": "string"}
|
||||
|
||||
@@ -17,7 +17,9 @@ from .converter import ConversionError
|
||||
from .github_source import SourceError
|
||||
from .i18n import N_, source_text, translate
|
||||
from .installer import (
|
||||
ADDRESS_SHOWN,
|
||||
ADVANCED_MODE,
|
||||
CONTAINER_ADDRESS,
|
||||
DEFAULT_MODE,
|
||||
InstallError,
|
||||
build_deployment,
|
||||
@@ -345,7 +347,8 @@ def _deployment_summary_text(template: dict[str, Any], deployment: dict[str, Any
|
||||
translate("unprivileged LXC") if security.get("unprivileged") else translate("privileged LXC"))
|
||||
environment = plan.get("environment", [])
|
||||
if environment:
|
||||
row(translate("Variables"), ", ".join(f"{item['name']}={item['value']}" for item in environment))
|
||||
row(translate("Variables"), ", ".join(
|
||||
f"{item['name']}={item['value'].replace(CONTAINER_ADDRESS, ADDRESS_SHOWN)}" for item in environment))
|
||||
if plan.get("suite_arr"):
|
||||
lines += ["", *[translate(note) for note in plan.get("completion_notes", [])]]
|
||||
lines.append("")
|
||||
@@ -367,6 +370,8 @@ def _print_installation_summary(result: dict[str, Any], images_removed: str | No
|
||||
console.msg_ok(f"{translate(STACK_LABELS.get(name, name))}: CT {vmid}")
|
||||
for item in result.get("urls") or []:
|
||||
console.msg_ok(f"{translate(item['label'])}: {item['url']}")
|
||||
for address in result.get("service_addresses") or []:
|
||||
console.msg_ok(f"{translate('Service:')} {address}")
|
||||
for item in result.get("credentials") or []:
|
||||
console.msg_info2(translate(item["label"]))
|
||||
username = str(item["username"])
|
||||
|
||||
@@ -31,6 +31,20 @@ DEFAULT_MODE = "default"
|
||||
ADVANCED_MODE = "advanced"
|
||||
|
||||
|
||||
# Stands for the address of the container in a variable that holds the
|
||||
# application's own address, until the installation knows it.
|
||||
CONTAINER_ADDRESS = "@CONTAINER_ADDRESS@"
|
||||
ADDRESS_SHOWN = "<IP>"
|
||||
|
||||
|
||||
def own_address_value(pattern: str, ipv4: str) -> str:
|
||||
"""The application's own address for a container with this network: the
|
||||
fixed address when it has one, its mark when DHCP is still to give it."""
|
||||
if ipv4 in ("dhcp", "host"):
|
||||
return pattern
|
||||
return pattern.replace(CONTAINER_ADDRESS, ipv4.split("/", 1)[0])
|
||||
|
||||
|
||||
def _example_default(name: str, example: str | None, timezone: str | None = None) -> str | None:
|
||||
defaults = {"PUID": "1000", "PGID": "1000", "TZ": timezone or host.timezone()}
|
||||
if name in defaults:
|
||||
@@ -584,6 +598,15 @@ def build_deployment(
|
||||
label = f"{translate('Value for')} {name}"
|
||||
default = _example_default(name, item["example"], timezone)
|
||||
generated = installer_profile.get("generated_sensitive_environment", {}).get(name)
|
||||
if item.get("container_address"):
|
||||
# The address of the container is known or will be: nothing to ask.
|
||||
# The advanced installation shows it, for a name or a proxy in front.
|
||||
value = own_address_value(item["container_address"], ipv4)
|
||||
if advanced:
|
||||
shown = ui.ask(label, value.replace(CONTAINER_ADDRESS, ADDRESS_SHOWN), required=True)
|
||||
value = shown.replace(ADDRESS_SHOWN, CONTAINER_ADDRESS)
|
||||
environment.append({"name": name, "value": value, "sensitive": False})
|
||||
continue
|
||||
if item.get("prompt_user", True) is False:
|
||||
if default in (None, "") and item["required"]:
|
||||
raise InstallError(f"{name}: {translate('no declarative value')}")
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"""An application that has to be told its own address is not asked for it: the
|
||||
container has one, fixed or given by DHCP, and the installation uses that."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
sys.path.insert(0, str(ROOT / "remote"))
|
||||
|
||||
from proxmenux_oci.catalog import Catalog
|
||||
from proxmenux_oci.installer import CONTAINER_ADDRESS, own_address_value
|
||||
import oci_instances
|
||||
|
||||
# Application, its variable and the value it takes at 192.168.1.50.
|
||||
OWN_ADDRESS = {
|
||||
"bookstack": ("APP_URL", "http://192.168.1.50"),
|
||||
"healthchecks": ("SITE_ROOT", "http://192.168.1.50:8000"),
|
||||
"speedtest-tracker": ("APP_URL", "http://192.168.1.50"),
|
||||
"netbox": ("ALLOWED_HOST", "192.168.1.50"),
|
||||
}
|
||||
|
||||
|
||||
class OwnAddressTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
catalog = Catalog(ROOT)
|
||||
cls.variables = {}
|
||||
for app, (name, _) in OWN_ADDRESS.items():
|
||||
environment = catalog.compose(app)["container_contract"]["environment"]
|
||||
cls.variables[app] = next(item for item in environment if item["name"] == name)
|
||||
|
||||
def test_a_fixed_address_is_used_as_it_is(self):
|
||||
for app, (_, expected) in OWN_ADDRESS.items():
|
||||
with self.subTest(app=app):
|
||||
pattern = self.variables[app]["container_address"]
|
||||
self.assertEqual(own_address_value(pattern, "192.168.1.50/24"), expected)
|
||||
|
||||
def test_dhcp_leaves_the_mark_for_the_installation(self):
|
||||
for app in OWN_ADDRESS:
|
||||
with self.subTest(app=app):
|
||||
pattern = self.variables[app]["container_address"]
|
||||
self.assertIn(CONTAINER_ADDRESS, own_address_value(pattern, "dhcp"))
|
||||
|
||||
def test_the_address_is_that_of_the_port_the_application_listens_on(self):
|
||||
catalog = Catalog(ROOT)
|
||||
for app, (_, expected) in OWN_ADDRESS.items():
|
||||
with self.subTest(app=app):
|
||||
endpoint = catalog.compose(app)["first_run"]["endpoints"][0]
|
||||
if not expected.startswith("http"):
|
||||
continue # A bare address names the machine, not a port.
|
||||
port = "" if endpoint["port"] == 80 else f":{endpoint['port']}"
|
||||
self.assertEqual(expected, f"{endpoint['scheme']}://192.168.1.50{port}")
|
||||
|
||||
def test_the_installer_resolves_the_mark_before_the_first_start(self):
|
||||
script = (ROOT / "remote/install_oci.sh").read_text()
|
||||
self.assertIn(f"OWN_ADDRESS_MARK='{CONTAINER_ADDRESS}'", script)
|
||||
self.assertLess(script.index("\napply_own_address\n"), script.index('msg_info "$(translate "Starting the container...")"'))
|
||||
self.assertEqual(oci_instances.ADDRESS_MARK, CONTAINER_ADDRESS)
|
||||
|
||||
def test_the_record_keeps_the_address_and_not_the_mark(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
root = Path(folder)
|
||||
record = {"schema_version": 1, "vmid": 900, "installation_id": "00000000-0000-4000-8000-000000000000", "status": "installing",
|
||||
"deployment": {"environment": [{"name": "APP_URL", "value": f"http://{CONTAINER_ADDRESS}"},
|
||||
{"name": "TZ", "value": "Europe/Madrid"}]}}
|
||||
path = oci_instances.location(root, 900)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(record))
|
||||
observe, oci_instances.observe = oci_instances.observe, lambda *args, **kwargs: {"config": ""}
|
||||
try:
|
||||
oci_instances.finish(root, 900, "archive", "digest", "192.168.1.50")
|
||||
finally:
|
||||
oci_instances.observe = observe
|
||||
saved = json.loads(path.read_text())["deployment"]["environment"]
|
||||
self.assertEqual(saved[0]["value"], "http://192.168.1.50")
|
||||
self.assertEqual(saved[1]["value"], "Europe/Madrid")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,78 @@
|
||||
"""A port that does not speak HTTP is not announced as a web address: the link
|
||||
would open nothing, and an update would wait on it for an answer that never
|
||||
comes and undo itself."""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
sys.path.insert(0, str(ROOT / "remote"))
|
||||
|
||||
from proxmenux_oci.catalog import Catalog
|
||||
import oci_description
|
||||
import oci_instance_transaction
|
||||
|
||||
# Application and the port its recipe was generated with as web address.
|
||||
SERVICES = {
|
||||
"mariadb": 3306,
|
||||
"ngircd": 6667,
|
||||
"openssh-server": 2222,
|
||||
"syslog-ng": 6601,
|
||||
"faster-whisper": 10300,
|
||||
"piper": 10200,
|
||||
}
|
||||
|
||||
|
||||
class ServicePortTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
catalog = Catalog(ROOT)
|
||||
cls.templates = {app: catalog.compose(app) for app in SERVICES}
|
||||
|
||||
def test_no_web_address_is_announced(self):
|
||||
for app, template in self.templates.items():
|
||||
with self.subTest(app=app):
|
||||
self.assertEqual(template["first_run"]["endpoints"], [])
|
||||
self.assertIsNone(template["catalog_ui"]["launch"]["port"])
|
||||
|
||||
def test_the_port_is_still_the_one_of_the_container(self):
|
||||
for app, port in SERVICES.items():
|
||||
with self.subTest(app=app):
|
||||
ports = [item["container_port"] for item in self.templates[app]["container_contract"]["ports"]]
|
||||
self.assertIn(port, ports)
|
||||
|
||||
def test_an_update_checks_that_the_container_keeps_running(self):
|
||||
for app, template in self.templates.items():
|
||||
with self.subTest(app=app):
|
||||
self.assertEqual(oci_instance_transaction.effective_healthcheck(template)["type"], "running")
|
||||
|
||||
def test_the_notes_give_the_address_as_text(self):
|
||||
notes = oci_description.render(self.templates["mariadb"], "sha256:" + "0" * 64, "instance", "192.168.1.50")
|
||||
self.assertIn("<code>192.168.1.50:3306</code>", notes)
|
||||
self.assertNotIn("Web UI", notes)
|
||||
self.assertNotIn("http://192.168.1.50", notes)
|
||||
notes = oci_description.render(self.templates["syslog-ng"], "sha256:" + "0" * 64, "instance", "192.168.1.50")
|
||||
self.assertIn("<code>192.168.1.50:5514/udp</code>", notes)
|
||||
|
||||
def test_a_web_application_keeps_its_link_in_the_notes(self):
|
||||
template = Catalog(ROOT).compose("bookstack")
|
||||
notes = oci_description.render(template, "sha256:" + "0" * 64, "instance", "192.168.1.50")
|
||||
self.assertIn("http://192.168.1.50:80/", notes)
|
||||
self.assertNotIn("<code>192.168.1.50:", notes)
|
||||
|
||||
def test_the_summary_gives_the_address_of_a_service_only(self):
|
||||
script = (ROOT / "remote/install_oci.sh").read_text()
|
||||
self.assertIn("service_addresses:$service_addresses", script)
|
||||
cli = (ROOT / "src/proxmenux_oci/cli.py").read_text()
|
||||
self.assertIn("translate('Service:')", cli)
|
||||
|
||||
def test_mariadb_says_how_to_connect(self):
|
||||
notes = self.templates["mariadb"]["proxmox"]["installer_profile"]["completion_notes"]
|
||||
self.assertTrue(any("port 3306" in note for note in notes))
|
||||
self.assertTrue(any("without a password" in note for note in notes))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user