mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 11:49:43 +02:00
Lot 21 : mise à jour de Loki tenue par la façade (LOKI_UPDATE=rust)
En conteneur, l'image se met à jour par Docker : /api/update rend la version du Go sans mise à jour disponible et la note Docker, /api/update/apply la refuse (409). La façade sert ces deux routes quand LOKI_UPDATE=rust et LOKI_CONTAINER=1 (sysupdate.rs) ; hors conteneur, loki-rs ne passe pas la variable et le Go garde le remplacement du binaire. Parité : parity_update_test.go tire du Go les quatre réponses en conteneur, rejouées par tests/sysupdate.rs ; TestFacadeMiseAJour côté Go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
16 files changed
+318
-1
No files matched your search
@@ -236,6 +236,7 @@ redémarrages (volume `/data`). Variables d'environnement du conteneur :
|
||||
| `LOKI_MEM` | `rust` : la mémoire (éditeur des pages, index, déplacement entre projets, trackers, mode mémoire, interrupteurs du mode agent) est tenue par la façade Rust ; le chiffrement reste au Go (expérimental, migration en cours) | Go |
|
||||
| `LOKI_SERVE` | `rust` : la ligne de commande de llama-server est composée par la façade Rust (`loki-rs engine serve`) au lieu de `loki serve` ; Strata et l'optimiseur restent au Go (expérimental, migration en cours) | Go |
|
||||
| `LOKI_PRESETS` | `rust` : les presets (liste, éditeur et aperçus, suppression, placement auto, presets externes, bascule) sont tenus par la façade Rust ; l'application au moteur reste au Go (expérimental, migration en cours) | Go |
|
||||
| `LOKI_UPDATE` | `rust` : la mise à jour de Loki (`/api/update`, `/api/update/apply`) est servie par la façade Rust ; en conteneur, l'image se met à jour par Docker (expérimental, migration en cours) | Go |
|
||||
| `LOKI_MCP` | `rust` : les serveurs MCP — configuration, catalogue, état, pool de sessions stdio et HTTP — sont tenus par la façade Rust ; le Go lui demande les outils MCP de l'agent et leurs appels (expérimental, migration en cours) | Go |
|
||||
| `LOKI_PROJECTS` | `rust` : le hub des projets (liste et comptes, création, renommage, description, suppression, bascule) est tenu par la façade Rust ; les discussions restent à qui tient la conversation (expérimental, migration en cours) | Go |
|
||||
| `LOKI_SYSTEM` | `rust` : les interrupteurs système — accès internet et Crawl4AI, pilotage du navigateur, clé de complétion, écoute réseau du moteur — sont tenus par la façade Rust ; le navigateur piloté reste au Go (expérimental, migration en cours) | Go |
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Lot 21 — mise à jour de Loki tenue par la façade
|
||||
|
||||
## Contexte
|
||||
|
||||
`/api/update` (version disponible) et `/api/update/apply` (téléchargement et
|
||||
remplacement du binaire) vivaient dans `sys_update.go`. En conteneur
|
||||
(`LOKI_CONTAINER=1`), le Go n'y fait rien. L'image se met à jour par Docker,
|
||||
et les deux routes le disent (`containerUpdateNote`) :
|
||||
- `/api/update` : `{current, available: false, note}` ;
|
||||
- `/api/update/apply` : 409 `{ok: false, error: note}`.
|
||||
|
||||
## Le lot
|
||||
|
||||
La façade ne tourne qu'en conteneur. Elle tient ces deux routes quand
|
||||
`LOKI_UPDATE=rust` (`sysupdate.rs`, expérimental) et `LOKI_CONTAINER=1`.
|
||||
Sinon `loki-rs` ne passe pas la variable au Go, qui garde le téléchargement
|
||||
et le remplacement du binaire hors conteneur.
|
||||
|
||||
La version annoncée est celle du Go (`/internal/v1/auth`). Les binaires de
|
||||
la façade suivent toujours `loki-rs self-update`.
|
||||
|
||||
## Parité
|
||||
|
||||
`internal/loki/parity_update_test.go` tire du Go les quatre réponses en
|
||||
conteneur (GET et POST sur chaque route). `tests/sysupdate.rs` les rejoue par
|
||||
la façade, version du Go comprise.
|
||||
@@ -150,7 +150,8 @@ var facadeChatRoutes = map[string]bool{
|
||||
// de la discussion (facade_files.go), les notifications (facade_push.go), la
|
||||
// mémoire (facade_mem.go), les presets (facade_presets.go), le pilotage du
|
||||
// moteur (facade_engine.go), les interrupteurs système (facade_system.go), les
|
||||
// projets (facade_projects.go), les serveurs MCP (facade_mcp.go).
|
||||
// projets (facade_projects.go), les serveurs MCP (facade_mcp.go), la mise à
|
||||
// jour (facade_update.go).
|
||||
// Posé APRÈS requireWebAuth : seule une requête authentifiée ici part, avec le
|
||||
// jeton interne qui en tient lieu là-bas.
|
||||
func withFacadeChat(path string, h http.HandlerFunc) http.HandlerFunc {
|
||||
@@ -179,6 +180,8 @@ func withFacadeChat(path string, h http.HandlerFunc) http.HandlerFunc {
|
||||
held, unavailable = facadeProjects, "projets indisponibles : "
|
||||
case facadeMCPRoutes[path]:
|
||||
held, unavailable = facadeMCP, "serveurs MCP indisponibles : "
|
||||
case facadeUpdateRoutes[path]:
|
||||
held, unavailable = facadeUpdate, "mise à jour indisponible : "
|
||||
default:
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -905,3 +905,25 @@ func TestFacadeMCP(t *testing.T) {
|
||||
t.Fatalf("outils sans façade : %v", tools)
|
||||
}
|
||||
}
|
||||
|
||||
// La mise à jour (LOKI_UPDATE=rust, lot 21) part à la façade.
|
||||
func TestFacadeMiseAJour(t *testing.T) {
|
||||
testHome(t)
|
||||
f := demarrerFacade(t)
|
||||
t.Setenv(facadeChatEnv, "")
|
||||
local := func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "go") }
|
||||
for _, on := range []string{"", "rust"} {
|
||||
t.Setenv(facadeUpdateEnv, on)
|
||||
for p := range facadeUpdateRoutes {
|
||||
w := httptest.NewRecorder()
|
||||
withFacadeChat(p, local)(w, httptest.NewRequest("GET", p, nil))
|
||||
want := "go"
|
||||
if on == "rust" {
|
||||
want = f.reply
|
||||
}
|
||||
if w.Body.String() != want {
|
||||
t.Fatalf("%s (LOKI_UPDATE=%q) : %q", p, on, w.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// facade_update.go — mise à jour de Loki tenue par la façade Rust
|
||||
// (LOKI_UPDATE=rust, lot 21 de la migration ; expérimental, le Go reste le
|
||||
// défaut) : /api/update et /api/update/apply. La façade ne tourne qu'en
|
||||
// conteneur, où l'image se met à jour par Docker : les deux routes le disent
|
||||
// (containerUpdateNote). Hors conteneur, loki-rs ne passe pas la variable.
|
||||
|
||||
const facadeUpdateEnv = "LOKI_UPDATE"
|
||||
|
||||
// facadeUpdate : la façade tient-elle la mise à jour ?
|
||||
func facadeUpdate() bool {
|
||||
return strings.EqualFold(strings.TrimSpace(os.Getenv(facadeUpdateEnv)), "rust") &&
|
||||
facadeAddr() != "" && internalToken() != ""
|
||||
}
|
||||
|
||||
// facadeUpdateRoutes : les routes que la façade sert alors (sysupdate.rs).
|
||||
var facadeUpdateRoutes = map[string]bool{
|
||||
"/api/update": true,
|
||||
"/api/update/apply": true,
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// parity_update_test.go — journal de référence de la mise à jour de Loki en
|
||||
// conteneur (lot 21 de la migration Rust) : l'image se met à jour par Docker,
|
||||
// les deux routes le disent. Rangé dans
|
||||
// rust/loki-rs/tests/parity_update/update.json ; tests/sysupdate.rs le rejoue
|
||||
// par la façade.
|
||||
//
|
||||
// LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteMiseAJour
|
||||
|
||||
const parityUpdateFile = "../../rust/loki-rs/tests/parity_update/update.json"
|
||||
|
||||
func TestPariteMiseAJour(t *testing.T) {
|
||||
testHome(t)
|
||||
t.Setenv("LOKI_CONTAINER", "1")
|
||||
var steps []map[string]any
|
||||
for _, c := range []struct{ method, path, body string }{
|
||||
{"GET", "/api/update", ""},
|
||||
{"POST", "/api/update", "{}"},
|
||||
{"POST", "/api/update/apply", "{}"},
|
||||
{"GET", "/api/update/apply", ""},
|
||||
} {
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(c.method, c.path, strings.NewReader(c.body))
|
||||
if c.path == "/api/update" {
|
||||
handleUpdateCheck(rec, req)
|
||||
} else {
|
||||
handleUpdateApply(rec, req)
|
||||
}
|
||||
var out any
|
||||
_ = json.Unmarshal([]byte(strings.ReplaceAll(rec.Body.String(), Version, "{{VERSION}}")), &out)
|
||||
steps = append(steps, map[string]any{"method": c.method, "path": c.path, "status": rec.Code, "out": out})
|
||||
}
|
||||
got, err := parityJSON(map[string]any{"steps": steps})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if os.Getenv("LOKI_PARITY_UPDATE") == "1" {
|
||||
if err := os.MkdirAll(filepath.Dir(parityUpdateFile), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(parityUpdateFile, got, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
want, err := os.ReadFile(parityUpdateFile)
|
||||
if err != nil {
|
||||
t.Fatalf("journal de référence absent (%v) : LOKI_PARITY_UPDATE=1 pour le créer", err)
|
||||
}
|
||||
if string(got) != string(want) {
|
||||
t.Errorf("la mise à jour en conteneur ne produit plus le journal rangé dans %s.\n"+
|
||||
"Si c'est voulu : LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteMiseAJour.\n--- obtenu ---\n%s",
|
||||
parityUpdateFile, got)
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,8 @@ pub struct Ctx {
|
||||
pub projects: bool,
|
||||
/// Serveurs MCP tenus par la façade (`LOKI_MCP=rust`).
|
||||
pub mcp: Option<std::sync::Arc<crate::mcp::Mcp>>,
|
||||
/// Mise à jour de Loki tenue par la façade (`LOKI_UPDATE=rust`).
|
||||
pub update: bool,
|
||||
/// Jeton de l'API interne : une requête qui le porte vient du Go.
|
||||
pub internal_token: String,
|
||||
}
|
||||
@@ -70,6 +72,7 @@ impl Ctx {
|
||||
|| self.system.is_some()
|
||||
|| self.projects
|
||||
|| self.mcp.is_some()
|
||||
|| self.update
|
||||
}
|
||||
|
||||
/// La requête vient du Go (jeton interne valide) : déjà authentifiée par
|
||||
|
||||
@@ -40,6 +40,7 @@ pub mod settings;
|
||||
pub mod store;
|
||||
pub mod sysinfo;
|
||||
pub mod system;
|
||||
pub mod sysupdate;
|
||||
pub mod tools;
|
||||
pub mod ui;
|
||||
pub mod update;
|
||||
@@ -159,6 +159,10 @@ fn serve() -> ExitCode {
|
||||
// LOKI_MCP=rust : les serveurs MCP (le Go demande alors à la façade
|
||||
// les outils MCP et leurs appels).
|
||||
let mcp_rust = held_by_facade("LOKI_MCP");
|
||||
// LOKI_UPDATE=rust : la mise à jour de Loki, en conteneur seulement
|
||||
// (l'image se met à jour par Docker).
|
||||
let update_rust = held_by_facade("LOKI_UPDATE")
|
||||
&& loki_rs::sysupdate::usable(std::env::var("LOKI_CONTAINER").ok().as_deref());
|
||||
let engine_rust = held_by_facade("LOKI_ENGINE")
|
||||
&& match loki_rs::enginectl::usable(
|
||||
std::env::var("LOKI_CONTAINER").ok().as_deref(),
|
||||
@@ -189,6 +193,7 @@ fn serve() -> ExitCode {
|
||||
("LOKI_SYSTEM", system_rust),
|
||||
("LOKI_PROJECTS", projects_rust),
|
||||
("LOKI_MCP", mcp_rust),
|
||||
("LOKI_UPDATE", update_rust),
|
||||
];
|
||||
for (var, on) in held {
|
||||
if on {
|
||||
@@ -294,6 +299,7 @@ fn serve() -> ExitCode {
|
||||
engine_ctl,
|
||||
system: system_rust.then(|| std::sync::Arc::new(loki_rs::system::System::new())),
|
||||
projects: projects_rust,
|
||||
update: update_rust,
|
||||
mcp: mcp_rust.then(|| std::sync::Arc::new(loki_rs::mcp::Mcp::new(mem_home))),
|
||||
};
|
||||
let res = proxy::serve(listener, cfg, shutdown_signal()).await;
|
||||
|
||||
@@ -90,6 +90,9 @@ pub struct Config {
|
||||
/// Serveurs MCP (pool de sessions, configuration, catalogue) tenus par la
|
||||
/// façade (`LOKI_MCP=rust`) ; `None` : au Go.
|
||||
pub mcp: Option<Arc<crate::mcp::Mcp>>,
|
||||
/// Mise à jour de Loki tenue par la façade (`LOKI_UPDATE=rust`, en
|
||||
/// conteneur).
|
||||
pub update: bool,
|
||||
}
|
||||
|
||||
struct Proxy {
|
||||
@@ -128,6 +131,7 @@ pub async fn serve(
|
||||
system: cfg.system,
|
||||
projects: cfg.projects,
|
||||
mcp: cfg.mcp,
|
||||
update: cfg.update,
|
||||
internal_token: cfg.internal_token.clone(),
|
||||
},
|
||||
});
|
||||
@@ -226,6 +230,9 @@ impl Proxy {
|
||||
{
|
||||
return chatapi::serve(&self.ctx, chat, req, from_go).await;
|
||||
}
|
||||
if self.ctx.update && crate::sysupdate::handles(path) {
|
||||
return crate::sysupdate::serve(&self.ctx, req, from_go).await;
|
||||
}
|
||||
if let Some(m) = &self.ctx.mcp
|
||||
&& crate::mcp::handles(path)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
//! Mise à jour de Loki tenue par la façade (lot 21 de la migration,
|
||||
//! `LOKI_UPDATE=rust` ; expérimental, le Go reste le défaut) — port de
|
||||
//! `handleUpdateCheck` et `handleUpdateApply` (`sys_update.go`) en conteneur.
|
||||
//!
|
||||
//! La façade ne tourne qu'en conteneur (`LOKI_CONTAINER=1`), où l'image se met
|
||||
//! à jour par Docker : les deux routes le disent, sans rien télécharger. Les
|
||||
//! binaires de la façade, eux, suivent `loki-rs self-update`. Hors conteneur,
|
||||
//! la façade ne tient pas ce lot (décidé au démarrage).
|
||||
|
||||
use hyper::body::Incoming;
|
||||
use hyper::{Request, Response, StatusCode};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::api::Ctx;
|
||||
use crate::auth::json as send_json;
|
||||
use crate::proxy::Body;
|
||||
|
||||
/// `containerUpdateNote`.
|
||||
pub const CONTAINER_UPDATE_NOTE: &str =
|
||||
"Mise à jour par l'image Docker : docker compose pull && docker compose up -d";
|
||||
|
||||
/// La façade peut-elle tenir ce lot (`containerManaged`) ?
|
||||
pub fn usable(container_env: Option<&str>) -> bool {
|
||||
container_env == Some("1")
|
||||
}
|
||||
|
||||
/// Les routes servies par ce module.
|
||||
pub fn handles(path: &str) -> bool {
|
||||
matches!(path, "/api/update" | "/api/update/apply")
|
||||
}
|
||||
|
||||
/// `from_go` : remise par le Go (tunnel), déjà authentifiée par lui.
|
||||
pub async fn serve(ctx: &Ctx, req: Request<Incoming>, from_go: bool) -> Response<Body> {
|
||||
let version = if from_go {
|
||||
ctx.internal
|
||||
.auth()
|
||||
.await
|
||||
.map(|i| i.version)
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
match ctx.guard(req.headers()).await {
|
||||
Ok(v) => v,
|
||||
Err(refus) => return *refus,
|
||||
}
|
||||
};
|
||||
if req.uri().path() == "/api/update" {
|
||||
return send_json(
|
||||
StatusCode::OK,
|
||||
&json!({ "current": version, "available": false, "note": CONTAINER_UPDATE_NOTE }),
|
||||
);
|
||||
}
|
||||
send_json(
|
||||
StatusCode::CONFLICT,
|
||||
&json!({ "ok": false, "error": CONTAINER_UPDATE_NOTE }),
|
||||
)
|
||||
}
|
||||
@@ -634,6 +634,7 @@ pub struct Opts {
|
||||
pub system: Option<Arc<loki_rs::system::System>>,
|
||||
pub projects: bool,
|
||||
pub mcp: Option<Arc<loki_rs::mcp::Mcp>>,
|
||||
pub update: bool,
|
||||
}
|
||||
|
||||
/// Comme [`start`], avec la conversation tenue par la façade
|
||||
@@ -734,6 +735,7 @@ pub async fn start_with(
|
||||
system: opts.system,
|
||||
projects: opts.projects,
|
||||
mcp: opts.mcp,
|
||||
update: opts.update,
|
||||
};
|
||||
tokio::spawn(loki_rs::proxy::serve(fl, cfg, async move {
|
||||
let _ = rx.await;
|
||||
|
||||
@@ -105,6 +105,7 @@ async fn banc(cle: Option<&str>) -> Banc {
|
||||
system: None,
|
||||
projects: false,
|
||||
mcp: None,
|
||||
update: false,
|
||||
};
|
||||
tokio::spawn(loki_rs::proxy::serve(l, cfg, async move {
|
||||
let _ = rx.await;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"steps": [
|
||||
{
|
||||
"method": "GET",
|
||||
"out": {
|
||||
"available": false,
|
||||
"current": "{{VERSION}}",
|
||||
"note": "Mise à jour par l'image Docker : docker compose pull && docker compose up -d"
|
||||
},
|
||||
"path": "/api/update",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"out": {
|
||||
"available": false,
|
||||
"current": "{{VERSION}}",
|
||||
"note": "Mise à jour par l'image Docker : docker compose pull && docker compose up -d"
|
||||
},
|
||||
"path": "/api/update",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"out": {
|
||||
"error": "Mise à jour par l'image Docker : docker compose pull && docker compose up -d",
|
||||
"ok": false
|
||||
},
|
||||
"path": "/api/update/apply",
|
||||
"status": 409
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"out": {
|
||||
"error": "Mise à jour par l'image Docker : docker compose pull && docker compose up -d",
|
||||
"ok": false
|
||||
},
|
||||
"path": "/api/update/apply",
|
||||
"status": 409
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -111,6 +111,7 @@ async fn facade(backend: SocketAddr) -> (SocketAddr, oneshot::Sender<()>) {
|
||||
system: None,
|
||||
projects: false,
|
||||
mcp: None,
|
||||
update: false,
|
||||
};
|
||||
tokio::spawn(loki_rs::proxy::serve(l, cfg, async move {
|
||||
let _ = rx.await;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
//! Lot 21 : mise à jour de Loki tenue par la façade (`LOKI_UPDATE=rust`, en
|
||||
//! conteneur). Le journal tiré du Go (`internal/loki/parity_update_test.go` →
|
||||
//! `tests/parity_update/update.json`) est rejoué par la façade.
|
||||
|
||||
mod common;
|
||||
|
||||
use common::fakego;
|
||||
use serde_json::Value;
|
||||
|
||||
#[tokio::test]
|
||||
async fn parite_en_conteneur() {
|
||||
let fx: Value = serde_json::from_str(include_str!("parity_update/update.json")).unwrap();
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let (facade, _go, _stop, _) = fakego::start_with(
|
||||
tmp.path().to_path_buf(),
|
||||
fakego::Opts {
|
||||
update: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
for st in fx["steps"].as_array().unwrap() {
|
||||
let (code, b) = fakego::send(
|
||||
facade,
|
||||
st["method"].as_str().unwrap(),
|
||||
st["path"].as_str().unwrap(),
|
||||
&[],
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(code.as_u16() as i64, st["status"].as_i64().unwrap(), "{st}");
|
||||
// La version est celle du Go (0.15.0 pour le faux Go).
|
||||
let out: Value =
|
||||
serde_json::from_str(&String::from_utf8_lossy(&b).replace("0.15.0", "{{VERSION}}"))
|
||||
.unwrap();
|
||||
assert_eq!(out, st["out"], "{st}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seulement_en_conteneur() {
|
||||
assert!(loki_rs::sysupdate::usable(Some("1")));
|
||||
assert!(!loki_rs::sysupdate::usable(Some("0")));
|
||||
assert!(!loki_rs::sysupdate::usable(None));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sans_la_variable_la_mise_a_jour_reste_au_go() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let (facade, _go, _stop) = fakego::start(tmp.path().to_path_buf()).await;
|
||||
for p in ["/api/update", "/api/update/apply"] {
|
||||
let (_, _, b) = fakego::get(facade, p, &[]).await;
|
||||
assert_eq!(&b[..], format!("go:{p}").as_bytes());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user