La plateforme

Cycle de vie & proxy

Au démarrage, un module s'enregistre auprès du core ; ensuite le core proxifie ses routes et surveille sa santé par heartbeat.

Enregistrement #

Le module appelle les routes /internal/* du core (sur 127.0.0.1:8080), protégées par l'en-tête X-Internal-Secret. Il transmet son identité, son base_url, ses routes, ses entrées de sidebar, ses événements souscrits et sa version. Le core conserve l'instance en mémoire et en base (table core.module_instances).

En pratique, l'enregistrement au démarrage se réduit à un POST — dans le langage de votre choix :

// Rust — reqwest
let core   = std::env::var("KC__CORE__URL")?;
let secret = std::env::var("KC__CORE__INTERNAL_SECRET")?;

reqwest::Client::new()
    .post(format!("{core}/internal/modules/register"))
    .header("X-Internal-Secret", secret)
    .json(&serde_json::json!({
        "module_id": "demo",
        "base_url":  "http://127.0.0.1:3120",
        "version":   "0.1.0"
    }))
    .send().await?;
<?php
// PHP — cURL
$core   = getenv('KC__CORE__URL');
$secret = getenv('KC__CORE__INTERNAL_SECRET');

$ch = curl_init("$core/internal/modules/register");
curl_setopt_array($ch, [
    CURLOPT_POST       => true,
    CURLOPT_HTTPHEADER => [
        "X-Internal-Secret: $secret",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "module_id" => "demo",
        "base_url"  => "http://127.0.0.1:3120",
        "version"   => "0.1.0",
    ]),
]);
curl_exec($ch);
# Python — requests
import os, requests

core   = os.environ["KC__CORE__URL"]
secret = os.environ["KC__CORE__INTERNAL_SECRET"]

requests.post(
    f"{core}/internal/modules/register",
    headers={"X-Internal-Secret": secret},
    json={
        "module_id": "demo",
        "base_url":  "http://127.0.0.1:3120",
        "version":   "0.1.0",
    },
)
// Go — net/http
core   := os.Getenv("KC__CORE__URL")
secret := os.Getenv("KC__CORE__INTERNAL_SECRET")

body, _ := json.Marshal(map[string]string{
    "module_id": "demo",
    "base_url":  "http://127.0.0.1:3120",
    "version":   "0.1.0",
})
req, _ := http.NewRequest("POST", core+"/internal/modules/register", bytes.NewReader(body))
req.Header.Set("X-Internal-Secret", secret)
req.Header.Set("Content-Type", "application/json")
http.DefaultClient.Do(req)
# Perl — LWP::UserAgent
use LWP::UserAgent;
use JSON::PP;

my $core   = $ENV{'KC__CORE__URL'};
my $secret = $ENV{'KC__CORE__INTERNAL_SECRET'};

my $ua = LWP::UserAgent->new;
$ua->post("$core/internal/modules/register",
    'X-Internal-Secret' => $secret,
    'Content-Type'      => 'application/json',
    Content => encode_json({
        module_id => "demo",
        base_url  => "http://127.0.0.1:3120",
        version   => "0.1.0",
    }),
);
Note

Le secret et l'URL du core sont fournis au processus par l'environnement (KC__CORE__INTERNAL_SECRET, KC__CORE__URL). Voir Configuration.

Cycle de vie #

starting ──► healthy ──(heartbeat manqué)──► degraded ──► stopped
  • heartbeat — le module ping périodiquement le core pour rester healthy.
  • unregister — à l'arrêt propre, le module se retire du registre.

Le reverse-proxy #

Le core intercepte /api/v1/<module_id>/* et le réécrit vers http://127.0.0.1:<port>/*, en injectant l'identité authentifiée :

Requête navigateur :  GET /api/v1/calendar/events
        ▼ (core : auth + réécriture)
Vers le module :      GET http://127.0.0.1:3102/events
  + X-Internal-Secret, X-Kubuno-User-Id, X-Kubuno-User-Role, X-Kubuno-User-Email
Astuce

Le module n'est jamais exposé directement : il n'écoute que sur 127.0.0.1, et tout passe par le core qui authentifie et réécrit.