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 <noreply@anthropic.com>
10 KiB
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/<build>/, 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, assetsloki-linux,loki-rs-linux,SHA256SUMS,manifest.json(uploadé en dernier). manifest.json={"build": <github.run_number>, "abi": <IMAGE_ABI>, "commit": "<sha>"}.- Source unique de l'ABI : fichier
IMAGE_ABIà la racine (contenu1), copié dans l'image en/etc/loki/IMAGE_ABI. Build de l'image :ENV LOKI_BUILD=<run_number>(0 en build local). - Les binaires de
/data/bin/<build>/ne sont utilisés que siabi == IMAGE_ABIetbuild > 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-Viaentrant ; transmetHost,Authorization,Cookie,Origin,Sec-Fetch-*,X-Forwarded-Protointacts ; 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 -Anigit 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 defmt.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<String,String>(nom → hex minuscule ; accepte*nom, ignore lignes vides/invalides). -
state::Manifest { build:u64, abi:u32, commit:String }(serde). -
state::Installed= Manifest écrit dans<dir>/release.json. -
state::read_installed(dir:&Path) -> Option<Manifest>. -
state::select_dir(data_bin:&Path, image_dir:&Path, image_build:u64, image_abi:u32) -> PathBuf:<data_bin>/<build>si release.json valide, abi égal, build > image_build, etloki+loki-rsprésents dedans ; sinonimage_dir. -
state::needs_update(m:&Manifest, image_build:u64, image_abi:u32, installed:Option<&Manifest>) -> Decisionavecenum 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 testFAIL 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 <data_bin>/.tmp-<build>/ → vérifie chaque hash → chmod 755 (unix) → renomme en loki, loki-rs → écrit release.json → renomme le dossier en <data_bin>/<build> → écrit <data_bin>/release.json (tmp + rename) → supprime les autres <n>/ et .tmp-*. Toute erreur : supprime .tmp-<build>, 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-Viaabsent 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_binpar défaut ; test du backoff (fonction purenext_backoff(prev, lived) -> Duration). - Test manuel local :
loki-rs serveavec unloki.execonstruit →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 ; jobrolling(contents: write, concurrencyrolling) : build Go linux CGO=0, build Rust musl (rustup target add x86_64-unknown-linux-musl,musl-tools), SHA256SUMS, manifest,gh release create rolling --prereleasesi absent, upload--clobberbinaires+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/<build>/ (bascule atomique par release.json).