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:
Claude committed 2026-10-15 20:01:37 +00:00
1 parent 090f15615e
commit 25e31c1c26
16 files changed
+318 -1

No files matched your search

+3
View File
@@ -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
+1
View File
@@ -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;
+6
View File
@@ -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;
+7
View File
@@ -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)
{
+56
View File
@@ -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 }),
)
}
+2
View File
@@ -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;
+1
View File
@@ -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
}
]
}
+1
View File
@@ -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;
+55
View File
@@ -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());
}
}