From 342f787eff7d983ace56add2fa0fffb773123722 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Tue, 13 Oct 2026 00:31:28 +0200 Subject: [PATCH] =?UTF-8?q?Web=20:=20adresse=20d'=C3=A9coute=20r=C3=A9glab?= =?UTF-8?q?le=20par=20LOKI=5FWEB=5FBIND?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La façade Rust du conteneur prend le port public et lance `loki web` derrière elle : le Go doit alors n'écouter qu'en local, sinon il resterait joignable en direct sans passer par la façade. Défaut inchangé (0.0.0.0). Plan du lot 1 de la migration Rust joint. Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-10-13-rust-migration-lot1.md | 122 ++++++++++++++++++ internal/loki/web_bind_test.go | 16 +++ internal/loki/web_server.go | 13 +- 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 docs/superpowers/plans/2026-10-13-rust-migration-lot1.md create mode 100644 internal/loki/web_bind_test.go diff --git a/docs/superpowers/plans/2026-10-13-rust-migration-lot1.md b/docs/superpowers/plans/2026-10-13-rust-migration-lot1.md new file mode 100644 index 0000000..8957798 --- /dev/null +++ b/docs/superpowers/plans/2026-10-13-rust-migration-lot1.md @@ -0,0 +1,122 @@ +# Migration Rust lot 1 — façade + mise à jour au démarrage — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Mettre un binaire Rust `loki-rs` en frontal du conteneur (proxy vers le Go) et faire que le conteneur prenne les derniers binaires publiés à chaque démarrage. + +**Architecture:** `loki-rs serve` écoute sur `LOKI_WEB_PORT` (8090), lance `loki web 8091` (Go, `LOKI_WEB_BIND=127.0.0.1`) en enfant supervisé et lui proxifie tout (HTTP, SSE, WebSocket). `loki-rs self-update` télécharge la release GitHub `rolling` dans `/data/bin//`, `loki-rs which-bin` dit à l'entrypoint quel dossier de binaires utiliser. + +**Tech Stack:** Rust 2021 (tokio, hyper 1, hyper-util, http-body-util, reqwest rustls, sha2, serde_json), cible `x86_64-unknown-linux-musl` ; Go 1.25 existant ; GitHub Actions. + +Spec : `docs/superpowers/specs/2026-10-13-rust-migration-lot1-design.md`. + +## Global Constraints + +- Port public inchangé : 8090 (`LOKI_WEB_PORT`). Go interne : `127.0.0.1:8091` (`LOKI_GO_PORT`). +- Release : tag `rolling`, pré-release, assets `loki-linux`, `loki-rs-linux`, `SHA256SUMS`, `manifest.json` (uploadé en dernier). +- `manifest.json` = `{"build": , "abi": , "commit": ""}`. +- Source unique de l'ABI : fichier `IMAGE_ABI` à la racine (contenu `1`), copié dans l'image en `/etc/loki/IMAGE_ABI`. Build de l'image : `ENV LOKI_BUILD=` (0 en build local). +- Les binaires de `/data/bin//` ne sont utilisés que si `abi == IMAGE_ABI` **et** `build > LOKI_BUILD`. +- `LOKI_AUTO_UPDATE=0` : aucune requête réseau. Le boot n'est jamais bloqué (code 0, timeouts bornés). +- La façade efface `X-Loki-Via` entrant ; transmet `Host`, `Authorization`, `Cookie`, `Origin`, `Sec-Fetch-*`, `X-Forwarded-Proto` intacts ; retire les hop-by-hop ; ne bufferise rien. +- Go : `gofmt -l .` vide + `staticcheck ./...` propre. Rust : `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, `cargo test`. +- Git : jamais `git add -A` ni `git stash` ; commits en français, style narratif. + +## File Structure + +``` +IMAGE_ABI ABI d'image (entier) +rust/Cargo.toml workspace +rust/loki-rs/Cargo.toml +rust/loki-rs/src/main.rs CLI : serve | self-update | which-bin | version +rust/loki-rs/src/proxy.rs proxy HTTP/SSE/WS vers le Go +rust/loki-rs/src/child.rs supervision de `loki web` +rust/loki-rs/src/update/mod.rs orchestration self-update + which-bin +rust/loki-rs/src/update/sums.rs parse SHA256SUMS +rust/loki-rs/src/update/state.rs release.json, décision, sélection du dossier +rust/loki-rs/tests/proxy.rs intégration façade +rust/loki-rs/tests/update.rs intégration self-update (faux serveur) +internal/loki/web_server.go LOKI_WEB_BIND +Dockerfile étape rustbuild, IMAGE_ABI, LOKI_BUILD +docker-entrypoint.sh self-update, which-bin, exec loki-rs serve +.github/workflows/ci.yml job rust +.github/workflows/docker-publish.yml LOKI_BUILD + job rolling +README.md section mise à jour au démarrage +``` + +--- + +### Task 1: Go — adresse d'écoute configurable + +**Files:** Modify `internal/loki/web_server.go` (cmdWeb), Test `internal/loki/web_bind_test.go` + +**Interfaces:** Produces `webBindAddr(port int) string` — `LOKI_WEB_BIND` (défaut `0.0.0.0`) + `:port`. + +- [ ] Test : `t.Setenv("LOKI_WEB_BIND","")` → `"0.0.0.0:8090"` ; `"127.0.0.1"` → `"127.0.0.1:8091"`. +- [ ] `go test ./internal/loki -run TestWebBindAddr` → FAIL (indéfini). +- [ ] Implémenter, l'utiliser dans `cmdWeb` à la place de `fmt.Sprintf("0.0.0.0:%d", port)`. +- [ ] Test PASS, `gofmt -l .` vide, staticcheck propre. Commit. + +### Task 2: Workspace Rust + parse SHA256SUMS + état d'installation + +**Files:** Create `rust/Cargo.toml`, `rust/loki-rs/Cargo.toml`, `src/main.rs` (squelette CLI), `src/update/{mod,sums,state}.rs`, `.gitignore` (`rust/target/`). + +**Interfaces (produits) :** +- `sums::parse(text:&str) -> HashMap` (nom → hex minuscule ; accepte `*nom`, ignore lignes vides/invalides). +- `state::Manifest { build:u64, abi:u32, commit:String }` (serde). +- `state::Installed` = Manifest écrit dans `/release.json`. +- `state::read_installed(dir:&Path) -> Option`. +- `state::select_dir(data_bin:&Path, image_dir:&Path, image_build:u64, image_abi:u32) -> PathBuf` : `/` si release.json valide, abi égal, build > image_build, et `loki` + `loki-rs` présents dedans ; sinon `image_dir`. +- `state::needs_update(m:&Manifest, image_build:u64, image_abi:u32, installed:Option<&Manifest>) -> Decision` avec `enum Decision { Install, UpToDate, AbiMismatch }`. + +- [ ] Tests unitaires : parse (2 espaces, `*`, majuscules → minuscules, ligne cassée ignorée) ; `needs_update` (abi ≠ → AbiMismatch ; build ≤ image → UpToDate ; build ≤ installé → UpToDate ; sinon Install) ; `select_dir` (aucun fichier → image ; abi ≠ → image ; build ≤ image → image ; binaire manquant → image ; ok → data dir). +- [ ] `cargo test` FAIL puis implémentation, PASS, fmt + clippy. Commit. + +### Task 3: self-update (téléchargement vérifié, installation atomique) + +**Files:** `src/update/mod.rs`, `tests/update.rs` + +**Interfaces :** `update::run(cfg:&UpdateConfig) -> Outcome` (jamais d'erreur remontée au CLI : journalise et rend `Outcome::{Installed(u64), UpToDate, AbiMismatch, Failed(String), Disabled}`) ; `UpdateConfig { base_url, data_bin, image_build, image_abi, enabled, timeout }` construit par `UpdateConfig::from_env()` (`LOKI_UPDATE_URL` défaut `https://github.com/R0m1k3/Loki/releases/download/rolling`, `LOKI_HOME/bin`, `LOKI_BUILD`, `/etc/loki/IMAGE_ABI` puis `LOKI_IMAGE_ABI`, `LOKI_AUTO_UPDATE`). + +Algorithme : manifeste → décision → `SHA256SUMS` → télécharge `loki-linux`, `loki-rs-linux` dans `/.tmp-/` → vérifie chaque hash → chmod 755 (unix) → renomme en `loki`, `loki-rs` → écrit `release.json` → renomme le dossier en `/` → écrit `/release.json` (tmp + rename) → supprime les autres `/` et `.tmp-*`. Toute erreur : supprime `.tmp-`, `Failed`. + +- [ ] Tests d'intégration (faux serveur hyper sur 127.0.0.1:0 servant des fichiers en mémoire) : installation OK (fichiers présents, release.json, select_dir pointe dessus) ; hash faux → rien installé, ancien état intact ; abi ≠ → rien téléchargé hormis le manifeste ; serveur absent → Failed, pas de panic ; `enabled=false` → Disabled sans requête ; seconde exécution → UpToDate et nettoyage des anciens builds. +- [ ] FAIL → implémentation → PASS, fmt + clippy. Commit. + +### Task 4: Façade proxy + +**Files:** `src/proxy.rs`, `tests/proxy.rs` + +**Interfaces :** `proxy::serve(listener: tokio::net::TcpListener, backend: SocketAddr, shutdown: impl Future) -> io::Result<()>`. + +Comportement : hyper http1 + `with_upgrades` ; chaque requête → client hyper-util vers `backend` (URI `http://backend{path_and_query}`), Host d'origine conservé ; retire hop-by-hop (`connection`, `keep-alive`, `proxy-connection`, `te`, `trailer`, `transfer-encoding`, `upgrade`, + noms listés dans `Connection`) et `x-loki-via` ; si requête d'upgrade (`Connection: upgrade` + `Upgrade`), garde `Connection: upgrade` + `Upgrade`, attend 101, puis `copy_bidirectional` entre les deux côtés upgradés ; corps en streaming dans les deux sens ; erreur de connexion → 502 `text/plain` « Loki (Go) redémarre — réessaie dans un instant ». + +- [ ] Tests (faux backend hyper) : GET relayé avec Host/Authorization/Origin/X-Forwarded-Proto intacts et `X-Loki-Via` absent côté backend ; POST corps relayé ; SSE : backend envoie 1er événement puis attend 500 ms — le client reçoit le 1er avant 300 ms ; WebSocket écho via tokio-tungstenite ; backend arrêté → 502. +- [ ] FAIL → implémentation → PASS, fmt + clippy. Commit. + +### Task 5: Supervision du Go + commande `serve` + +**Files:** `src/child.rs`, `src/main.rs` + +**Interfaces :** `child::Supervisor::spawn(go_bin:PathBuf, port:u16) -> Supervisor` (relance avec backoff 1 s → 30 s, remis à 1 s après 60 s de vie ; stdin null, stdout/stderr hérités ; env `LOKI_WEB_BIND=127.0.0.1`) ; `Supervisor::shutdown(self, grace:Duration)` (SIGTERM unix / kill windows, puis kill après `grace`). +`main serve` : go_bin = `LOKI_GO_BIN` sinon `loki` à côté de l'exe courant ; attend jusqu'à 30 s que `127.0.0.1:LOKI_GO_PORT` accepte (sinon ouvre quand même, 502 en attendant) ; `proxy::serve` sur `0.0.0.0:LOKI_WEB_PORT` ; SIGTERM/SIGINT → arrêt du proxy + `shutdown(10 s)`. +`main which-bin` imprime `select_dir(...)` ; `main version` imprime version crate + `LOKI_BUILD`. + +- [ ] Test unitaire `go_bin` par défaut ; test du backoff (fonction pure `next_backoff(prev, lived) -> Duration`). +- [ ] Test manuel local : `loki-rs serve` avec un `loki.exe` construit → `curl localhost:8090/` renvoie l'UI. +- [ ] Commit. + +### Task 6: Image, entrypoint, CI, release rolling, doc + +**Files:** `IMAGE_ABI`, `Dockerfile`, `docker-entrypoint.sh`, `.github/workflows/ci.yml`, `.github/workflows/docker-publish.yml`, `README.md`. + +- Dockerfile : étape `FROM rust:1-alpine AS rustbuild` (`apk add musl-dev`, `cargo build --release --locked`), copie `/usr/local/bin/loki-rs` ; `ARG LOKI_BUILD=0` → `ENV LOKI_BUILD` ; `COPY IMAGE_ABI /etc/loki/IMAGE_ABI`. +- Entrypoint : `loki-rs self-update` (best-effort) ; `BIN_DIR=$(loki-rs which-bin)` ; `LOKI="$BIN_DIR/loki"` pour tous les appels config/start ; `exec "$BIN_DIR/loki-rs" serve`. +- CI : job `rust` (fmt, clippy, test sur ubuntu). +- docker-publish : `LOKI_BUILD=${{ github.run_number }}` en build-arg ; job `rolling` (contents: write, concurrency `rolling`) : build Go linux CGO=0, build Rust musl (`rustup target add x86_64-unknown-linux-musl`, `musl-tools`), SHA256SUMS, manifest, `gh release create rolling --prerelease` si absent, upload `--clobber` binaires+sums puis manifest. +- README : section « Mise à jour au démarrage » (comportement, `LOKI_AUTO_UPDATE=0`, ABI → pull). +- [ ] `sh -n docker-entrypoint.sh`, YAML relu, commit. + +## Self-review + +Spec couvert : façade (T4/T5), X-Loki-Via (T4), bind Go (T1), rolling + manifeste + ABI (T2/T3/T6), repli et boot non bloquant (T3/T6), désactivation (T3), tests (T2–T4), CI (T6). Écart assumé vs spec : ordre des versions par `build` (run_number) et non par commit, et installation dans `/data/bin//` (bascule atomique par `release.json`). diff --git a/internal/loki/web_bind_test.go b/internal/loki/web_bind_test.go new file mode 100644 index 0000000..869f466 --- /dev/null +++ b/internal/loki/web_bind_test.go @@ -0,0 +1,16 @@ +package loki + +import "testing" + +// La façade Rust lance `loki web` derrière elle : le Go ne doit alors écouter +// qu'en local, sinon il resterait joignable en direct, sans la façade. +func TestWebBindAddr(t *testing.T) { + t.Setenv("LOKI_WEB_BIND", "") + if got := webBindAddr(8090); got != "0.0.0.0:8090" { + t.Fatalf("défaut : %q", got) + } + t.Setenv("LOKI_WEB_BIND", "127.0.0.1") + if got := webBindAddr(8091); got != "127.0.0.1:8091" { + t.Fatalf("local : %q", got) + } +} diff --git a/internal/loki/web_server.go b/internal/loki/web_server.go index 75f94d6..7bdbb30 100644 --- a/internal/loki/web_server.go +++ b/internal/loki/web_server.go @@ -33,7 +33,7 @@ func cmdWeb(args []string) error { port = n } mux := newWebMux() - addr := fmt.Sprintf("0.0.0.0:%d", port) + addr := webBindAddr(port) ln, err := net.Listen("tcp", addr) if err != nil { @@ -100,6 +100,17 @@ func cmdWeb(args []string) error { return srv.Serve(ln) } +// webBindAddr renvoie l'adresse d'écoute de l'UI. LOKI_WEB_BIND (défaut +// 0.0.0.0) existe pour la façade Rust du conteneur (rust/loki-rs) : elle prend +// le port public et lance `loki web` en local seulement, sur 127.0.0.1. +func webBindAddr(port int) string { + host := strings.TrimSpace(os.Getenv("LOKI_WEB_BIND")) + if host == "" { + host = "0.0.0.0" + } + return net.JoinHostPort(host, strconv.Itoa(port)) +} + // newWebMux construit le routeur HTTP de l'UI web. Extrait de cmdWeb pour être // réutilisé par `loki link`, qui sert ce même mux à travers le tunnel sans // repasser par un écouteur TCP local.