Bootstrap (main.rs)
Le point d'entrée orchestre tout : bac à sable, config, pool, migrations, état, enregistrement, puis serveur HTTP.
Cargo.toml #
[dependencies]
tokio = { version = "1", features = ["full"] }
axum = { version = "0.7", features = ["macros"] }
sqlx = { version = "0.8", features = ["runtime-tokio","postgres","uuid","chrono","json","migrate"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.12", features = ["json"] }
config = "0.14"
validator = { version = "0.18", features = ["derive"] }
tracing = "0.1"
thiserror = "1"
anyhow = "1"
uuid = { version = "1", features = ["v4","serde"] }
chrono = { version = "0.4", features = ["serde"] }
# Crate partagée, tirée d'un git-tag du core
kubuno-seccomp = { git = "https://github.com/kubuno/core", tag = "seccomp-v0.1.0", package = "kubuno-seccomp" }main.rs #
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt().init();
// 1. Bac à sable : interdit l'exécution de processus dans ce module
kubuno_seccomp::lock_down_process_execution("memo");
// 2. Configuration (KC__… + overrides KUBUNO_* injectés par le core)
let settings = Settings::load()?;
// 3. Pool PostgreSQL
let pool = PgPoolOptions::new()
.max_connections(settings.database.max_connections)
.connect_with(settings.database.connect_options()?)
.await?;
// 4. Schéma dédié + migrations (search_path = "memo,public")
if settings.database.run_migrations {
sqlx::query("CREATE SCHEMA IF NOT EXISTS memo").execute(&pool).await?;
let opts = settings.database.connect_options()?
.options([("search_path", "memo,public")]);
let mpool = PgPoolOptions::new().max_connections(1).connect_with(opts).await?;
sqlx::migrate!("./migrations").run(&mpool).await?;
}
// 5. État partagé
let state = AppState { db: pool, settings: Arc::new(settings.clone()) };
// 6. S'enregistrer auprès du core + heartbeat (cf. article dédié)
let http = reqwest::Client::new();
register_with_core(&http, &settings).await;
spawn_heartbeat(http.clone(), settings.clone());
// 7. Démarrer le serveur
let addr = format!("{}:{}", settings.server.host, settings.server.port);
let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, router::build(state)).await?;
Ok(())
}Sécurité
lock_down_process_execution applique le filtre seccomp dès le démarrage : aucun execve possible ensuite. Ne le retirez jamais.
Le même squelette, dans d'autres langages #
Cargo, SQLx et le bac à sable seccomp sont propres à Rust, mais un module n'est qu'un service HTTP : tout runtime convient (déclarez runtime = "binary" au manifeste). Le serveur minimal :
// Rust — Axum
use axum::{routing::get, Json, Router};
use serde_json::json;
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/ping", get(|| async { Json(json!({ "ok": true })) }));
let l = tokio::net::TcpListener::bind("127.0.0.1:3120").await.unwrap();
axum::serve(l, app).await.unwrap();
}<?php
// PHP — lancé avec : php -S 127.0.0.1:3120 router.php
header('Content-Type: application/json');
if (parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) === '/ping') {
echo json_encode(['ok' => true]);
exit;
}
http_response_code(404);# Python — Flask
from flask import Flask, jsonify
app = Flask(__name__)
@app.get("/ping")
def ping():
return jsonify(ok=True)
app.run(host="127.0.0.1", port=3120)// Go — net/http
package main
import (
"encoding/json"
"net/http"
)
func main() {
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]bool{"ok": true})
})
http.ListenAndServe("127.0.0.1:3120", nil)
}# Perl — Mojolicious::Lite
use Mojolicious::Lite -signatures;
get '/ping' => sub ($c) {
$c->render(json => { ok => \1 });
};
app->start('daemon', '-l', 'http://127.0.0.1:3120');