feat: système de rebuild Docker pour installation de modules has_backend
- internal/docker/client.go : client HTTP brut sur socket Unix - BuildImage() : build depuis repo git avec ARG MODULES - RebuildAndRestart() : rebuild async + remplacement de container - HandleReplacement() : le container successeur arrête et renomme l'ancien - Restart() : redémarrage simple (enable/disable sans rebuild) - cmd/gen-modules/main.go : générateur de registered_modules.go Lit MODULES env var, génère imports + appels RegisterModules() - registered_modules.go : version par défaut (aucun module) - main.go : appel RegisterModules(loader) + HandleReplacement() au démarrage - settings.go : inject DockerClient, has_backend dans moduleResp/moduleJSON, trigger rebuild à l'install, restart à l'enable/disable - migrations/006 : colonne has_backend sur table modules - Dockerfile : ARG MODULES, git clone modules, go run ./cmd/gen-modules - docker-compose.yml : socket Docker, group_add, env vars CONTAINER_NAME/GIT_REPO/GIT_BRANCH Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dcf3b937fa
commit
a61f805cd0
8 changed files with 658 additions and 27 deletions
|
|
@ -11,6 +11,7 @@ import (
|
|||
"git.geronzi.fr/proxmoxPanel/core/backend/internal/audit"
|
||||
"git.geronzi.fr/proxmoxPanel/core/backend/internal/crypto"
|
||||
"git.geronzi.fr/proxmoxPanel/core/backend/internal/db"
|
||||
dockerclient "git.geronzi.fr/proxmoxPanel/core/backend/internal/docker"
|
||||
"git.geronzi.fr/proxmoxPanel/core/backend/internal/logbuffer"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
|
@ -20,11 +21,12 @@ type SettingsHandler struct {
|
|||
db *db.DB
|
||||
auditLogger *audit.Logger
|
||||
encryptor *crypto.Encryptor
|
||||
docker *dockerclient.Client
|
||||
}
|
||||
|
||||
// NewSettingsHandler crée un SettingsHandler.
|
||||
func NewSettingsHandler(database *db.DB, auditLog *audit.Logger, enc *crypto.Encryptor) *SettingsHandler {
|
||||
return &SettingsHandler{db: database, auditLogger: auditLog, encryptor: enc}
|
||||
func NewSettingsHandler(database *db.DB, auditLog *audit.Logger, enc *crypto.Encryptor, docker *dockerclient.Client) *SettingsHandler {
|
||||
return &SettingsHandler{db: database, auditLogger: auditLog, encryptor: enc, docker: docker}
|
||||
}
|
||||
|
||||
// paramètres publics (non-sensibles) accessibles par les admins.
|
||||
|
|
@ -134,6 +136,7 @@ type moduleResp struct {
|
|||
Version string `json:"version"`
|
||||
IsCore bool `json:"is_core"`
|
||||
IsEnabled bool `json:"is_enabled"`
|
||||
HasBackend bool `json:"has_backend"`
|
||||
NavHref string `json:"nav_href"`
|
||||
NavIcon string `json:"nav_icon"`
|
||||
NavColor string `json:"nav_color"`
|
||||
|
|
@ -144,7 +147,7 @@ type moduleResp struct {
|
|||
// GET /api/modules
|
||||
func (h *SettingsHandler) GetModules(w http.ResponseWriter, r *http.Request) {
|
||||
rows, err := h.db.Query(`
|
||||
SELECT id, name, description, version, is_core, is_enabled,
|
||||
SELECT id, name, description, version, is_core, is_enabled, has_backend,
|
||||
COALESCE(nav_href,''), COALESCE(nav_icon,''), COALESCE(nav_color,''), COALESCE(nav_label_key,'')
|
||||
FROM modules ORDER BY is_core DESC, name ASC
|
||||
`)
|
||||
|
|
@ -157,11 +160,12 @@ func (h *SettingsHandler) GetModules(w http.ResponseWriter, r *http.Request) {
|
|||
var modules []moduleResp
|
||||
for rows.Next() {
|
||||
var m moduleResp
|
||||
var isCore, isEnabled int
|
||||
rows.Scan(&m.ID, &m.Name, &m.Description, &m.Version, &isCore, &isEnabled,
|
||||
var isCore, isEnabled, hasBackend int
|
||||
rows.Scan(&m.ID, &m.Name, &m.Description, &m.Version, &isCore, &isEnabled, &hasBackend,
|
||||
&m.NavHref, &m.NavIcon, &m.NavColor, &m.NavLabelKey)
|
||||
m.IsCore = isCore == 1
|
||||
m.IsEnabled = isEnabled == 1
|
||||
m.HasBackend = hasBackend == 1
|
||||
modules = append(modules, m)
|
||||
}
|
||||
|
||||
|
|
@ -186,7 +190,10 @@ func (h *SettingsHandler) EnableModule(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
h.auditLogger.Log(&claims.UserID, claims.Username, "module_enable", id, nil, clientIP(r))
|
||||
JSONResponse(w, http.StatusOK, map[string]string{"message": "Module activé (redémarrage requis pour prendre effet)"})
|
||||
|
||||
// Si le module a du code backend, redémarrer le container pour que LoadActive() le prenne en compte.
|
||||
restart := h.triggerRestartIfBackend(id)
|
||||
JSONResponse(w, http.StatusOK, map[string]interface{}{"message": "Module activé", "restarting": restart})
|
||||
}
|
||||
|
||||
// DisableModule désactive un module (ne peut pas désactiver les modules CORE).
|
||||
|
|
@ -208,7 +215,43 @@ func (h *SettingsHandler) DisableModule(w http.ResponseWriter, r *http.Request)
|
|||
|
||||
h.db.Exec(`UPDATE modules SET is_enabled = 0 WHERE id = ?`, id)
|
||||
h.auditLogger.Log(&claims.UserID, claims.Username, "module_disable", id, nil, clientIP(r))
|
||||
JSONResponse(w, http.StatusOK, map[string]string{"message": "Module désactivé"})
|
||||
|
||||
restart := h.triggerRestartIfBackend(id)
|
||||
JSONResponse(w, http.StatusOK, map[string]interface{}{"message": "Module désactivé", "restarting": restart})
|
||||
}
|
||||
|
||||
// triggerRestartIfBackend déclenche un redémarrage Docker si le module a du code backend.
|
||||
// Retourne true si un redémarrage a été déclenché.
|
||||
func (h *SettingsHandler) triggerRestartIfBackend(moduleID string) bool {
|
||||
if h.docker == nil || !h.docker.Available() {
|
||||
return false
|
||||
}
|
||||
var hasBackend int
|
||||
if err := h.db.QueryRow(`SELECT has_backend FROM modules WHERE id = ?`, moduleID).Scan(&hasBackend); err != nil {
|
||||
return false
|
||||
}
|
||||
if hasBackend == 1 {
|
||||
h.docker.Restart()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// enabledBackendModuleIDs retourne les IDs de tous les modules installés avec has_backend=1.
|
||||
// Ces modules doivent être compilés dans le binaire lors d'un rebuild.
|
||||
func (h *SettingsHandler) enabledBackendModuleIDs() ([]string, error) {
|
||||
rows, err := h.db.Query(`SELECT id FROM modules WHERE has_backend = 1`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var ids []string
|
||||
for rows.Next() {
|
||||
var id string
|
||||
rows.Scan(&id)
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// GetLogs retourne les dernières lignes du log applicatif (tampon mémoire).
|
||||
|
|
@ -350,6 +393,7 @@ type moduleJSON struct {
|
|||
NavColor string `json:"nav_color"`
|
||||
NavLabelKey string `json:"nav_label_key"`
|
||||
CoreMinVersion string `json:"core_min_version"`
|
||||
HasBackend bool `json:"has_backend"` // true = nécessite un rebuild Docker pour compilation
|
||||
}
|
||||
|
||||
// InstallRegistryModule installe un module depuis le store Forgejo.
|
||||
|
|
@ -399,17 +443,23 @@ func (h *SettingsHandler) InstallRegistryModule(w http.ResponseWriter, r *http.R
|
|||
// URL du repo
|
||||
repoURL := fmt.Sprintf("https://git.geronzi.fr/proxmoxPanel/%s", id)
|
||||
|
||||
hasBackend := 0
|
||||
if mod.HasBackend {
|
||||
hasBackend = 1
|
||||
}
|
||||
|
||||
// Insérer ou remplacer en DB
|
||||
_, err = h.db.Exec(`
|
||||
INSERT INTO modules (id, name, description, version, is_core, is_enabled,
|
||||
INSERT INTO modules (id, name, description, version, is_core, is_enabled, has_backend,
|
||||
nav_href, nav_icon, nav_color, nav_label_key, repo_url, installed_at)
|
||||
VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
name=excluded.name, description=excluded.description, version=excluded.version,
|
||||
has_backend=excluded.has_backend,
|
||||
nav_href=excluded.nav_href, nav_icon=excluded.nav_icon, nav_color=excluded.nav_color,
|
||||
nav_label_key=excluded.nav_label_key, repo_url=excluded.repo_url,
|
||||
installed_at=CURRENT_TIMESTAMP
|
||||
`, mod.ID, mod.Name, mod.Description, mod.Version,
|
||||
`, mod.ID, mod.Name, mod.Description, mod.Version, hasBackend,
|
||||
mod.NavHref, mod.NavIcon, mod.NavColor, mod.NavLabelKey, repoURL)
|
||||
if err != nil {
|
||||
JSONError(w, fmt.Sprintf("Erreur installation module : %v", err), http.StatusInternalServerError)
|
||||
|
|
@ -417,9 +467,21 @@ func (h *SettingsHandler) InstallRegistryModule(w http.ResponseWriter, r *http.R
|
|||
}
|
||||
|
||||
h.auditLogger.Log(&claims.UserID, claims.Username, "module_install", mod.ID,
|
||||
map[string]string{"version": mod.Version}, clientIP(r))
|
||||
map[string]string{"version": mod.Version, "has_backend": fmt.Sprintf("%v", mod.HasBackend)}, clientIP(r))
|
||||
|
||||
JSONResponse(w, http.StatusOK, map[string]string{
|
||||
"message": fmt.Sprintf("Module %s installé — rebuild requis pour activation", mod.ID),
|
||||
// Si le module a du code backend : déclencher un rebuild Docker.
|
||||
// Le rebuild compile le nouveau module dans le binaire et recrée le container.
|
||||
rebuilding := false
|
||||
if mod.HasBackend && h.docker != nil && h.docker.Available() {
|
||||
moduleIDs, err := h.enabledBackendModuleIDs()
|
||||
if err == nil {
|
||||
h.docker.RebuildAndRestart(moduleIDs)
|
||||
rebuilding = true
|
||||
}
|
||||
}
|
||||
|
||||
JSONResponse(w, http.StatusAccepted, map[string]interface{}{
|
||||
"message": fmt.Sprintf("Module %s installé", mod.ID),
|
||||
"rebuilding": rebuilding,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue