diff --git a/script/forgejo/install_forgejo.sh b/script/forgejo/install_forgejo.sh index a86b9ef..a7c31bf 100755 --- a/script/forgejo/install_forgejo.sh +++ b/script/forgejo/install_forgejo.sh @@ -60,6 +60,30 @@ esac say() { echo -e " $*"; } die() { echo -e " ${Red}✗ $*${Color_Off}" >&2; exit 1; } +# Adresse par laquelle la machine est joignable, pour ROOT_URL et SSH_DOMAIN. +# +# « hostname -I » d'abord, mais PAS seulement : ce drapeau vient de net-tools et +# l'inetutils d'Arch ne le connaît pas. « ip route get » le remplace partout où +# iproute2 est là, c'est-à-dire partout. « localhost » ferme la marche : une +# forge joignable en local vaut mieux qu'un script qui s'arrête. +host_address() { + local h="" + # Chaque candidat est VALIDÉ comme adresse IPv4 avant d'être retenu : un + # « hostname » qui ne connaît pas -I peut rendre le nom de la machine, et + # une ROOT_URL bâtie sur un nom non résolvable est pire qu'un repli. + for h in \ + "$(hostname -I 2>/dev/null | awk '{print $1}')" \ + "$(ip -4 route get 1 2>/dev/null | awk '{print $7; exit}')" \ + "$(ip -4 -o addr show scope global 2>/dev/null \ + | awk '{split($4, a, "/"); print a[1]; exit}')" + do + case "$h" in + [0-9]*.[0-9]*.[0-9]*.[0-9]*) echo "$h"; return 0 ;; + esac + done + echo localhost +} + # --- 1. Architecture ------------------------------------------------------- # Forgejo publie amd64, arm64 et arm-6. PAS de s390x : sur cette architecture # il faudrait le bâtir depuis les sources en Go, ce que ce script ne fait pas — @@ -158,8 +182,7 @@ sudo chmod 770 "$CONF_DIR" if sudo test -f "$CONF"; then say "configuration conservée : $CONF" else - host=$(hostname -I 2>/dev/null | awk '{print $1}') - [ -n "$host" ] || host=localhost + host=$(host_address) # Les QUATRE secrets, et pas seulement les deux évidents. Vécu : sans # « oauth2.JWT_SECRET », Forgejo tente de l'écrire dans app.ini au # démarrage, n'y arrive pas — le fichier appartient à root — et s'arrête @@ -291,8 +314,7 @@ else fi # --- 9. Résumé ------------------------------------------------------------- -host=$(hostname -I 2>/dev/null | awk '{print $1}') -[ -n "$host" ] || host=localhost +host=$(host_address) version=$("$BIN" --version 2>/dev/null | head -1) say "${Green}Forgejo prêt${Color_Off} : http://$host:$HTTP_PORT/" say " $version" diff --git a/test/test_qemu_forgejo.py b/test/test_qemu_forgejo.py index d62edc0..be385af 100644 --- a/test/test_qemu_forgejo.py +++ b/test/test_qemu_forgejo.py @@ -204,6 +204,63 @@ class TestTheScript(unittest.TestCase): self.assertNotIn("Version de Forgejo introuvable", out) +class TestHostAddress(unittest.TestCase): + """L'adresse qui va dans ROOT_URL et SSH_DOMAIN, sur trois terrains. + + « hostname -I » vient de net-tools : l'inetutils d'Arch ne connaît pas ce + drapeau et peut rendre le NOM de la machine. Une ROOT_URL bâtie sur un nom + non résolvable est pire qu'un repli, d'où la validation de la forme. + """ + + def _host_address(self, stubs): + """Extrait la fonction du script et l'exécute avec un PATH bouchonné.""" + body = SCRIPT.read_text() + start = body.index("host_address() {") + end = body.index("\n}", start) + 2 + fn = body[start:end] + with tempfile.TemporaryDirectory() as tmp: + bin_dir = pathlib.Path(tmp) / "bin" + bin_dir.mkdir() + for name, script in stubs.items(): + (bin_dir / name).write_text(f"#!/bin/bash\n{script}\n") + (bin_dir / name).chmod(0o755) + res = subprocess.run( + ["bash", "-c", fn + "\nhost_address"], + capture_output=True, + text=True, + env=dict(os.environ, PATH=f"{bin_dir}:/usr/bin:/bin"), + timeout=30, + ) + return res.stdout.strip() + + def test_it_takes_the_address_hostname_gives(self): + got = self._host_address({"hostname": "echo 10.1.2.3"}) + self.assertEqual("10.1.2.3", got) + + def test_a_hostname_that_returns_a_name_is_rejected(self): + """Le cas Arch : on tombe alors sur « ip », et non sur un nom.""" + got = self._host_address( + { + "hostname": "echo erplibre-arch", + "ip": "echo '1.0.0.1 via 10.0.0.1 dev eth0 src 10.9.9.9 uid 0'", + } + ) + self.assertEqual("10.9.9.9", got) + + def test_without_hostname_nor_ip_it_falls_back_to_localhost(self): + """Une forge joignable en local vaut mieux qu'un script qui s'arrête.""" + got = self._host_address({"hostname": "exit 1", "ip": "exit 1"}) + self.assertEqual("localhost", got) + + def test_it_never_returns_an_empty_string(self): + """Une ROOT_URL « http://:3000/ » ne mène nulle part.""" + for stubs in ( + {"hostname": "echo", "ip": "echo"}, + {"hostname": "exit 2", "ip": "exit 2"}, + ): + self.assertTrue(self._host_address(stubs), stubs) + + class TestTheScriptGuards(unittest.TestCase): """Quatre pièges rencontrés en le mettant au point, tous mesurés."""