Tutoriel backend — « Memo »

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');