From 4b859c4eb66dcfe8effa7908f7d0d2e4288c563c Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Tue, 13 Oct 2026 07:33:18 +0200 Subject: [PATCH] =?UTF-8?q?Conteneur=20:=20fa=C3=A7ade=20Rust=20en=20front?= =?UTF-8?q?al,=20binaires=20mis=20=C3=A0=20jour=20au=20d=C3=A9marrage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'image embarque loki-rs (étape Alpine, binaire musl statique) et l'entrypoint en fait la porte d'entrée : `loki-rs serve` sur le port public, le Go derrière lui sur 127.0.0.1:8091. LOKI_FACADE=0 revient à l'ancien schéma si la façade pose problème. Au démarrage, le loki-rs de l'image installe la dernière release « rolling » dans /data/bin// ; l'entrypoint prend ensuite loki et loki-rs du même dossier (which-bin), et un lien dans /usr/local/sbin fait que `docker exec loki loki …` vise lui aussi la version installée. C'est toujours le loki-rs de l'image qui met à jour : un binaire publié défectueux ne peut pas bloquer le correctif suivant. CI : job Rust (fmt, clippy -D warnings, tests). Publication : la release « rolling » part à chaque push sur main avec le même numéro de run que l'image (LOKI_BUILD) ; IMAGE_ABI, source unique, empêche d'installer des binaires qui exigent une image plus récente. manifest.json est envoyé en dernier. Message corrigé quand l'image est plus récente que la release : rien à tirer dans ce cas. Co-Authored-By: Claude Opus 5.5 --- .dockerignore | 1 + .github/workflows/ci.yml | 18 ++++++++++ .github/workflows/docker-publish.yml | 54 ++++++++++++++++++++++++++++ Dockerfile | 21 +++++++++++ IMAGE_ABI | 1 + README.md | 39 ++++++++++++++++++-- docker-compose.unraid.yml | 6 ++-- docker-entrypoint.sh | 42 ++++++++++++++++++++-- rust/loki-rs/src/update/mod.rs | 7 ++++ 9 files changed, 182 insertions(+), 7 deletions(-) create mode 100644 IMAGE_ABI diff --git a/.dockerignore b/.dockerignore index 69adb94..59e6d61 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,3 +8,4 @@ dist/ !README.md .env .env.* +rust/target/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2320f31..1230a9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,24 @@ jobs: GOOS=${t%/*} GOARCH=${t#*/} CGO_ENABLED=0 go build -o /dev/null ./cmd/loki done + # Façade Rust (rust/loki-rs) : même exigence que le Go, zéro avertissement. + rust: + runs-on: ubuntu-latest + defaults: + run: + working-directory: rust + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + - uses: Swatinem/rust-cache@v2 + with: + workspaces: rust + - run: cargo fmt --all --check + - run: cargo clippy --all-targets --locked -- -D warnings + - run: cargo test --locked + macos: runs-on: macos-latest steps: diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index b7bd23e..e9bdcac 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -54,6 +54,7 @@ jobs: push: true build-args: | LOKI_VERSION=${{ github.sha }} + LOKI_BUILD=${{ github.run_number }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} # Le cache de couches réutilise l'étape asrfetch (téléchargement du @@ -61,3 +62,56 @@ jobs: # pas : seul le code Go et l'assemblage de l'image se refont. cache-from: type=gha cache-to: type=gha,mode=max + + # Release « rolling » : les binaires que le conteneur installe lui-même au + # démarrage (loki-rs self-update, voir docker-entrypoint.sh). Même numéro de + # run que l'image ci-dessus : c'est lui qui ordonne les versions. + # manifest.json part EN DERNIER — tant qu'il annonce l'ancien build, un + # conteneur qui démarre pendant l'envoi garde ce qu'il a ; s'il lit des + # binaires à moitié remplacés, l'empreinte ne correspond pas et il n'installe + # rien. Pré-release : `loki update` (hors conteneur, /releases/latest) ne la + # voit pas. + rolling: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: write + concurrency: + group: rolling-release + cancel-in-progress: false + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-unknown-linux-musl + - uses: Swatinem/rust-cache@v2 + with: + workspaces: rust + - name: Outils musl + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends musl-tools + - name: Binaires + run: | + mkdir -p dist + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -trimpath -ldflags "-s -w" -o dist/loki-linux ./cmd/loki + (cd rust && cargo build --release --locked --target x86_64-unknown-linux-musl) + cp rust/target/x86_64-unknown-linux-musl/release/loki-rs dist/loki-rs-linux + file dist/loki-rs-linux + (cd dist && sha256sum loki-linux loki-rs-linux > SHA256SUMS) + abi=$(tr -d '[:space:]' < IMAGE_ABI) + printf '{"build":%s,"abi":%s,"commit":"%s"}\n' \ + "${{ github.run_number }}" "$abi" "$GITHUB_SHA" > dist/manifest.json + cat dist/manifest.json dist/SHA256SUMS + - name: Publier + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release view rolling >/dev/null 2>&1 || \ + gh release create rolling --prerelease --target "$GITHUB_SHA" \ + --title "rolling (mise à jour au démarrage)" \ + --notes "Binaires installés automatiquement par le conteneur à son démarrage. Ne pas télécharger à la main." + gh release upload rolling dist/loki-linux dist/loki-rs-linux dist/SHA256SUMS --clobber + gh release upload rolling dist/manifest.json --clobber diff --git a/Dockerfile b/Dockerfile index 3fce9d7..7931eba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,6 +42,17 @@ RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /out/loki ./cmd/loki # limité à cette étape de build. RUN GOBIN=/out GOTOOLCHAIN=auto go install golang.org/x/tools/gopls@latest +# ── Étape 1 ter : façade Rust loki-rs ─────────────────────────────────── +# Premier lot de la migration Go → Rust (rust/, voir docs/superpowers/specs/). +# Alpine = musl : binaire STATIQUE, indépendant de la glibc de l'image runtime — +# le même que celui publié dans la release « rolling » et installé à chaud +# dans /data/bin par `loki-rs self-update`. +FROM rust:1-alpine AS rustbuild +RUN apk add --no-cache musl-dev +WORKDIR /src +COPY rust/ ./ +RUN cargo build --release --locked && mkdir -p /out && cp target/release/loki-rs /out/loki-rs + # ── Étape 1 bis : serveur de dictée (Parakeet via sherpa-onnx) ────────── # On TÉLÉCHARGE un binaire préconstruit, on ne compile plus rien. C'est le # changement de fond par rapport à whisper.cpp, qu'il remplace : deux étapes de @@ -131,6 +142,7 @@ RUN if [ "$PLAYWRIGHT" = "1" ]; then \ COPY --from=gobuild /out/loki /usr/local/bin/loki COPY --from=gobuild /out/gopls /usr/local/bin/gopls +COPY --from=rustbuild /out/loki-rs /usr/local/bin/loki-rs # Dictée vocale (POST /api/transcribe). Le modèle (~500 Mo) n'est PAS dans # l'image : téléchargé à la demande dans /data/asr/. COPY --from=asrfetch /out/sherpa-onnx-offline-websocket-server /usr/local/bin/sherpa-onnx-offline-websocket-server @@ -147,6 +159,15 @@ ENV LOKI_CONTAINER=1 \ NVIDIA_VISIBLE_DEVICES=all \ NVIDIA_DRIVER_CAPABILITIES=compute,utility +# Mise à jour au démarrage (loki-rs self-update) : IMAGE_ABI dit quelles +# dépendances d'image les binaires exigent — un binaire publié pour une autre +# ABI n'est jamais installé. LOKI_BUILD = numéro de run CI de cette image : un +# binaire de /data/bin n'est pris que s'il est PLUS RÉCENT. Posés ici, en fin +# d'image, pour ne pas invalider le cache des couches lourdes à chaque build. +COPY IMAGE_ABI /etc/loki/IMAGE_ABI +ARG LOKI_BUILD=0 +ENV LOKI_BUILD=${LOKI_BUILD} + # 8090 : interface web (seule à exposer). Le moteur (8080) reste interne au # conteneur — il n'est pas authentifié par défaut. EXPOSE 8090 diff --git a/IMAGE_ABI b/IMAGE_ABI new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/IMAGE_ABI @@ -0,0 +1 @@ +1 diff --git a/README.md b/README.md index 5fd41eb..eee231a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ seule image. ``` ┌────────────────── conteneur loki ──────────────────┐ -│ loki web (UI + API, port 8090, premier plan) │ +│ loki-rs serve (façade Rust, port 8090) │ +│ │ relaie HTTP/SSE/WebSocket │ +│ loki web (UI + API Go, 127.0.0.1:8091) │ │ │ pilote (fichier PID — pas de systemd) │ │ loki serve ──exec──► llama-server (CUDA, :8080) │ │ ▲ modèles .gguf │ @@ -35,6 +37,10 @@ seule image. redémarre le moteur normalement. - Le moteur (port 8080, non authentifié par défaut) **n'est pas exposé** ; seule l'UI (8090) l'est. +- **Migration vers Rust en cours**, par lots : la façade `loki-rs` + (`rust/loki-rs`) prend le port public et relaie au serveur Go ; chaque lot + lui fait reprendre des routes. Plan et feuille de route : + `docs/superpowers/specs/2026-10-13-rust-migration-lot1-design.md`. ## Démarrage rapide (Docker, GPU NVIDIA) @@ -172,6 +178,32 @@ Unraid. Compose prêt à l'emploi : [`docker-compose.unraid.yml`](docker-compose `docker-compose.unraid.yml` → **Compose Up**. 4. Interface : `http://:8090`. +## Mise à jour au démarrage + +Un **simple redémarrage du conteneur** (ou du serveur Unraid) suffit à passer +à la dernière version de Loki : pas besoin de `docker compose pull` pour les +changements de code. + +- À chaque push sur `main`, la CI publie la release GitHub + [`rolling`](https://github.com/R0m1k3/Loki/releases/tag/rolling) : binaires + `loki` (Go) et `loki-rs` (Rust), `SHA256SUMS`, `manifest.json`. +- Au démarrage, `loki-rs self-update` les télécharge, vérifie leur empreinte + SHA-256 et les installe dans `/data/bin//`. Le journal du conteneur + dit ce qui s'est passé (`[loki-rs update] …`, puis + `[entrypoint] binaires Loki : …`). +- **Jamais bloquant** : réseau coupé, empreinte fausse, GitHub injoignable → + les binaires déjà en place servent, le conteneur démarre normalement. +- **Quand une nouvelle image est nécessaire** (nouvelle dépendance système, + Python, Node, llama.cpp…), le fichier `IMAGE_ABI` est incrémenté : les + conteneurs plus anciens n'installent pas ces binaires et le journal demande + un `docker compose pull`. +- La mise à jour est faite par le `loki-rs` **de l'image**, jamais par celui + téléchargé : une version publiée défectueuse ne peut pas empêcher le + correctif suivant d'arriver au redémarrage d'après. +- `docker exec loki loki …` utilise aussi la version installée. +- Couper : `LOKI_AUTO_UPDATE=0`. Revenir aux binaires de l'image : supprimer + `/data/bin` et redémarrer avec `LOKI_AUTO_UPDATE=0`. + ## Configuration Tout se règle **dans l'UI** (modèle, contexte, presets…) et survit aux @@ -189,6 +221,9 @@ redémarrages (volume `/data`). Variables d'environnement du conteneur : | `LOKI_CHROME` | binaire du navigateur piloté (contrôle du navigateur) ; par défaut le Chromium de Playwright de l'image | — | | `LOKI_CU_HEADFUL` | `1` : ouvre une vraie fenêtre au lieu du mode headless (machine avec écran) | — | | `LOKI_TRUSTED_HOSTS` | noms de domaine autorisés à servir l'UI **sans** clé de pilotage (reverse proxy), séparés par des virgules | — | +| `LOKI_AUTO_UPDATE` | `0` : pas de mise à jour des binaires au démarrage | `1` | +| `LOKI_FACADE` | `0` : sans façade Rust, le Go sert directement le port public (porte de sortie) | `1` | +| `LOKI_GO_PORT` | port local (127.0.0.1) du serveur Go derrière la façade | `8091` | **Accès par nom de domaine.** Sans clé de pilotage, l'API n'accepte que les hôtes locaux (IP, `localhost`, nom sans point, `.local`/`.lan`…) et refuse @@ -1062,7 +1097,7 @@ Une étape à la fois, mesurée avant la suivante : | Agent de code | — | mode Code : critères d'acceptation, passe de vérification, LSP, outils git | | Accès distant | relais chiffré ajean.link | retiré de l'interface | | Endpoint OpenAI | `:8080/v1` du moteur, ouvert par `network on` | `/v1` servi par Loki sur le port de l'interface, protégé par la clé API | -| Mise à jour | `ajean update` (binaire GitHub) | `docker compose pull` | +| Mise à jour | `ajean update` (binaire GitHub) | au redémarrage du conteneur (release `rolling`) ; `docker compose pull` si l'image change | Le reste — mémoire, outils, protocole, moteur d'inférence — est celui d'AJEAN. Pour récupérer les évolutions de l'amont : diff --git a/docker-compose.unraid.yml b/docker-compose.unraid.yml index 9f12718..961718e 100644 --- a/docker-compose.unraid.yml +++ b/docker-compose.unraid.yml @@ -16,8 +16,10 @@ # Les modèles se téléchargent directement depuis l'UI (catalogue intégré), # ou dépose tes .gguf dans /mnt/user/appdata/loki/models. # -# Mise à jour : Compose Down puis Up avec « pull image », ou -# « docker compose pull » avant Up. +# Mise à jour : AUTOMATIQUE au redémarrage du conteneur (binaires Loki de +# la release « rolling », vérifiés SHA-256 ; LOKI_AUTO_UPDATE=0 pour couper). +# Si le journal demande une nouvelle image : Compose Down puis Up avec +# « pull image », ou « docker compose pull » avant Up. # ─────────────────────────────────────────────────────────────────────────── services: loki: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index b174a11..5fc35b1 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,6 +1,7 @@ #!/bin/sh -# Entrypoint du conteneur Loki : sème la configuration au premier démarrage, -# lance le moteur (supervision PID, sans systemd), puis l'UI au premier plan. +# Entrypoint du conteneur Loki : met à jour les binaires Loki, sème la +# configuration au premier démarrage, lance le moteur (supervision PID, sans +# systemd), puis l'UI au premier plan derrière la façade Rust (loki-rs). set -eu : "${LOKI_HOME:=/data}" @@ -15,6 +16,33 @@ if ! grep -qs " $LOKI_HOME " /proc/self/mountinfo; then echo "[entrypoint] Ajoute « -v /chemin/hote/data:/data » (docker run) ou le mapping volumes du docker-compose." fi +# ── Mise à jour au démarrage ─────────────────────────────────────────────── +# Le loki-rs DE L'IMAGE télécharge la dernière release « rolling » (binaires +# vérifiés par SHA-256) dans $LOKI_HOME/bin// : un simple redémarrage +# du conteneur suffit à passer à la dernière version. C'est toujours celui de +# l'image qui s'en charge — un binaire publié défectueux ne peut donc pas +# empêcher le correctif suivant d'arriver. Jamais bloquant (réseau coupé, +# empreinte fausse : l'état précédent reste). LOKI_AUTO_UPDATE=0 le coupe. +/usr/local/bin/loki-rs self-update || true + +# loki et loki-rs viennent TOUJOURS du même dossier : /data/bin/ s'il +# est plus récent que l'image et de même ABI, sinon l'image. +BIN_DIR=$(/usr/local/bin/loki-rs which-bin 2>/dev/null || true) +if [ -z "$BIN_DIR" ] || [ ! -x "$BIN_DIR/loki" ] || [ ! -x "$BIN_DIR/loki-rs" ]; then + BIN_DIR=/usr/local/bin +fi +echo "[entrypoint] binaires Loki : $BIN_DIR" +# Tous les appels « loki » ci-dessous visent le binaire choisi. +loki() { "$BIN_DIR/loki" "$@"; } +# Et `docker exec loki loki …` aussi : /usr/local/sbin passe AVANT +# /usr/local/bin dans le PATH, un lien y suffit sans toucher aux binaires de +# l'image (celui de loki-rs doit rester intact, c'est lui qui met à jour). +if [ "$BIN_DIR" != /usr/local/bin ]; then + ln -sf "$BIN_DIR/loki" /usr/local/sbin/loki 2>/dev/null || true +else + rm -f /usr/local/sbin/loki 2>/dev/null || true +fi + # Le chemin du moteur est imposé par l'image (llama-server précompilé de # l'image officielle llama.cpp, dans /app) : on le (re)pose à chaque boot, # une mise à jour de l'image ne doit pas laisser un BIN obsolète en base. @@ -59,4 +87,12 @@ else echo "[entrypoint] aucun MODEL configuré — télécharge un modèle depuis l'UI pour démarrer le moteur" fi -exec loki web "${LOKI_WEB_PORT:-8090}" +# Façade Rust (loki-rs) sur le port public ; elle lance `loki web` sur +# 127.0.0.1:${LOKI_GO_PORT:-8091} et lui relaie tout. LOKI_FACADE=0 revient à +# l'ancien schéma (Go seul, directement sur le port public) — porte de sortie +# si la façade pose problème. +export LOKI_WEB_PORT="${LOKI_WEB_PORT:-8090}" +if [ "${LOKI_FACADE:-1}" = "0" ]; then + exec "$BIN_DIR/loki" web "$LOKI_WEB_PORT" +fi +exec "$BIN_DIR/loki-rs" serve diff --git a/rust/loki-rs/src/update/mod.rs b/rust/loki-rs/src/update/mod.rs index 0e3bae1..fddc594 100644 --- a/rust/loki-rs/src/update/mod.rs +++ b/rust/loki-rs/src/update/mod.rs @@ -92,6 +92,13 @@ impl Outcome { match self { Outcome::Disabled => "mise à jour au démarrage désactivée (LOKI_AUTO_UPDATE=0)".into(), Outcome::UpToDate(b) => format!("binaires à jour (build {b})"), + Outcome::AbiMismatch { + remote_abi, + image_abi, + } if remote_abi < image_abi => format!( + "image plus récente que la release publiée (ABI {remote_abi}, image {image_abi}) \ + — binaires de l'image conservés" + ), Outcome::AbiMismatch { remote_abi, image_abi,