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
|
|
@ -1,18 +1,36 @@
|
||||||
# ── Étape 1 : Build du binaire Go ──────────────────────────────────────────
|
# ── Étape 1 : Build du binaire Go ──────────────────────────────────────────
|
||||||
|
# Build context = core/backend/ (context: ./backend dans docker-compose.yml)
|
||||||
|
# ARG MODULES : IDs des modules à compiler, séparés par des virgules (ex: "viewLogs,viewServices")
|
||||||
FROM golang:1.26-alpine AS builder
|
FROM golang:1.26-alpine AS builder
|
||||||
|
|
||||||
# Dépendances de compilation (git pour les modules Go)
|
RUN apk add --no-cache git ca-certificates
|
||||||
RUN apk add --no-cache git
|
|
||||||
|
|
||||||
WORKDIR /build
|
ARG MODULES=""
|
||||||
|
ENV MODULES=${MODULES}
|
||||||
|
|
||||||
# Copier les fichiers de dépendances en premier (optimise le cache Docker)
|
WORKDIR /workspace/core/backend
|
||||||
|
|
||||||
|
# Copier les sources du CORE (build context = backend/)
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
# Copier tout le code source
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Cloner les modules demandés et les ajouter au go.mod.
|
||||||
|
# Les replace directives utilisent ../../{module} ce qui correspond à /workspace/{module} ✓
|
||||||
|
RUN if [ -n "$MODULES" ]; then \
|
||||||
|
for mod in $(echo "$MODULES" | tr ',' ' '); do \
|
||||||
|
echo "→ Clonage du module $mod..." && \
|
||||||
|
git clone "https://git.geronzi.fr/proxmoxPanel/$mod" "/workspace/$mod" && \
|
||||||
|
printf "\nrequire git.geronzi.fr/proxmoxPanel/$mod v0.0.0\n" >> go.mod && \
|
||||||
|
printf "\nreplace git.geronzi.fr/proxmoxPanel/$mod => ../../$mod\n" >> go.mod; \
|
||||||
|
done; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Générer registered_modules.go avec les imports et appels RegisterModule corrects
|
||||||
|
RUN go run ./cmd/gen-modules
|
||||||
|
|
||||||
|
# Résoudre et télécharger toutes les dépendances (modules inclus)
|
||||||
|
RUN go mod tidy && go mod download
|
||||||
|
|
||||||
# Compiler le binaire de façon statique
|
# Compiler le binaire de façon statique
|
||||||
# -ldflags="-s -w" : supprime les infos de debug pour réduire la taille
|
# -ldflags="-s -w" : supprime les infos de debug pour réduire la taille
|
||||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||||
|
|
@ -21,26 +39,21 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||||
# ── Étape 2 : Image finale minimale ────────────────────────────────────────
|
# ── Étape 2 : Image finale minimale ────────────────────────────────────────
|
||||||
FROM alpine:3.20
|
FROM alpine:3.20
|
||||||
|
|
||||||
# Certificats CA pour les requêtes HTTPS vers l'API Proxmox
|
|
||||||
RUN apk add --no-cache ca-certificates tzdata
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
|
||||||
# Créer un utilisateur non-root pour la sécurité
|
# Utilisateur non-root pour la sécurité
|
||||||
RUN addgroup -g 1001 pxp && adduser -u 1001 -G pxp -s /bin/sh -D pxp
|
RUN addgroup -g 1001 pxp && adduser -u 1001 -G pxp -s /bin/sh -D pxp
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copier le binaire compilé
|
|
||||||
COPY --from=builder /bin/proxmoxpanel /app/proxmoxpanel
|
COPY --from=builder /bin/proxmoxpanel /app/proxmoxpanel
|
||||||
|
|
||||||
# Créer les répertoires de données avec les bonnes permissions
|
|
||||||
RUN mkdir -p /app/data && chown -R pxp:pxp /app
|
RUN mkdir -p /app/data && chown -R pxp:pxp /app
|
||||||
|
|
||||||
USER pxp
|
USER pxp
|
||||||
|
|
||||||
# Port d'écoute du backend
|
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
|
|
||||||
# Variables d'environnement par défaut
|
|
||||||
ENV DATA_DIR=/app/data \
|
ENV DATA_DIR=/app/data \
|
||||||
LISTEN_ADDR=:3001 \
|
LISTEN_ADDR=:3001 \
|
||||||
APP_ENV=production
|
APP_ENV=production
|
||||||
|
|
|
||||||
104
backend/cmd/gen-modules/main.go
Normal file
104
backend/cmd/gen-modules/main.go
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
// cmd/gen-modules génère registered_modules.go à partir de la variable d'env MODULES.
|
||||||
|
// Appelé pendant le build Docker : go run ./cmd/gen-modules
|
||||||
|
// MODULES = liste d'IDs séparés par des virgules (ex: "viewLogs,viewServices")
|
||||||
|
// Le fichier généré déclare RegisterModules(*modules.Loader) qui enregistre chaque module.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
const outputFile = "registered_modules.go"
|
||||||
|
|
||||||
|
// moduleDef décrit un module à importer.
|
||||||
|
type moduleDef struct {
|
||||||
|
ID string // ex: "viewLogs"
|
||||||
|
Alias string // ex: "viewlogs" (alias Go valide)
|
||||||
|
Pkg string // ex: "git.geronzi.fr/proxmoxPanel/viewLogs"
|
||||||
|
}
|
||||||
|
|
||||||
|
// tplNoModules : fichier généré quand aucun module n'est installé.
|
||||||
|
const tplNoModules = `// Code généré automatiquement par cmd/gen-modules — ne pas modifier manuellement.
|
||||||
|
// Régénéré lors du build Docker avec la liste des modules compilés.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "git.geronzi.fr/proxmoxPanel/core/backend/modules"
|
||||||
|
|
||||||
|
// RegisterModules enregistre les modules compilés dans le binaire.
|
||||||
|
func RegisterModules(l *modules.Loader) {}
|
||||||
|
`
|
||||||
|
|
||||||
|
// tplWithModules : fichier généré avec des modules.
|
||||||
|
const tplWithModules = `// Code généré automatiquement par cmd/gen-modules — ne pas modifier manuellement.
|
||||||
|
// Régénéré lors du build Docker avec la liste des modules compilés.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.geronzi.fr/proxmoxPanel/core/backend/modules"
|
||||||
|
{{- range .}}
|
||||||
|
{{.Alias}} "{{.Pkg}}"
|
||||||
|
{{- end}}
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterModules enregistre les modules compilés dans le binaire.
|
||||||
|
func RegisterModules(l *modules.Loader) {
|
||||||
|
{{- range .}}
|
||||||
|
l.RegisterModule({{.Alias}}.New())
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
modulesEnv := strings.TrimSpace(os.Getenv("MODULES"))
|
||||||
|
|
||||||
|
// Pas de modules : générer la version vide
|
||||||
|
if modulesEnv == "" {
|
||||||
|
if err := os.WriteFile(outputFile, []byte(tplNoModules), 0644); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Erreur écriture %s : %v\n", outputFile, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("Généré %s (aucun module)\n", outputFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parser la liste des modules
|
||||||
|
ids := strings.Split(modulesEnv, ",")
|
||||||
|
defs := make([]moduleDef, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
id = strings.TrimSpace(id)
|
||||||
|
if id == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Alias Go = ID en minuscules sans tirets ni underscores
|
||||||
|
alias := strings.ToLower(strings.NewReplacer("-", "", "_", "").Replace(id))
|
||||||
|
defs = append(defs, moduleDef{
|
||||||
|
ID: id,
|
||||||
|
Alias: alias,
|
||||||
|
Pkg: "git.geronzi.fr/proxmoxPanel/" + id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Générer le fichier avec les imports et les appels RegisterModule
|
||||||
|
tmpl, err := template.New("modules").Parse(tplWithModules)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Erreur template : %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(outputFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Erreur création %s : %v\n", outputFile, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if err := tmpl.Execute(f, defs); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Erreur génération : %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Généré %s avec %d module(s) : %s\n", outputFile, len(defs), modulesEnv)
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"git.geronzi.fr/proxmoxPanel/core/backend/internal/audit"
|
"git.geronzi.fr/proxmoxPanel/core/backend/internal/audit"
|
||||||
"git.geronzi.fr/proxmoxPanel/core/backend/internal/crypto"
|
"git.geronzi.fr/proxmoxPanel/core/backend/internal/crypto"
|
||||||
"git.geronzi.fr/proxmoxPanel/core/backend/internal/db"
|
"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"
|
"git.geronzi.fr/proxmoxPanel/core/backend/internal/logbuffer"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
@ -20,11 +21,12 @@ type SettingsHandler struct {
|
||||||
db *db.DB
|
db *db.DB
|
||||||
auditLogger *audit.Logger
|
auditLogger *audit.Logger
|
||||||
encryptor *crypto.Encryptor
|
encryptor *crypto.Encryptor
|
||||||
|
docker *dockerclient.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSettingsHandler crée un SettingsHandler.
|
// NewSettingsHandler crée un SettingsHandler.
|
||||||
func NewSettingsHandler(database *db.DB, auditLog *audit.Logger, enc *crypto.Encryptor) *SettingsHandler {
|
func NewSettingsHandler(database *db.DB, auditLog *audit.Logger, enc *crypto.Encryptor, docker *dockerclient.Client) *SettingsHandler {
|
||||||
return &SettingsHandler{db: database, auditLogger: auditLog, encryptor: enc}
|
return &SettingsHandler{db: database, auditLogger: auditLog, encryptor: enc, docker: docker}
|
||||||
}
|
}
|
||||||
|
|
||||||
// paramètres publics (non-sensibles) accessibles par les admins.
|
// paramètres publics (non-sensibles) accessibles par les admins.
|
||||||
|
|
@ -134,6 +136,7 @@ type moduleResp struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
IsCore bool `json:"is_core"`
|
IsCore bool `json:"is_core"`
|
||||||
IsEnabled bool `json:"is_enabled"`
|
IsEnabled bool `json:"is_enabled"`
|
||||||
|
HasBackend bool `json:"has_backend"`
|
||||||
NavHref string `json:"nav_href"`
|
NavHref string `json:"nav_href"`
|
||||||
NavIcon string `json:"nav_icon"`
|
NavIcon string `json:"nav_icon"`
|
||||||
NavColor string `json:"nav_color"`
|
NavColor string `json:"nav_color"`
|
||||||
|
|
@ -144,7 +147,7 @@ type moduleResp struct {
|
||||||
// GET /api/modules
|
// GET /api/modules
|
||||||
func (h *SettingsHandler) GetModules(w http.ResponseWriter, r *http.Request) {
|
func (h *SettingsHandler) GetModules(w http.ResponseWriter, r *http.Request) {
|
||||||
rows, err := h.db.Query(`
|
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,'')
|
COALESCE(nav_href,''), COALESCE(nav_icon,''), COALESCE(nav_color,''), COALESCE(nav_label_key,'')
|
||||||
FROM modules ORDER BY is_core DESC, name ASC
|
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
|
var modules []moduleResp
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var m moduleResp
|
var m moduleResp
|
||||||
var isCore, isEnabled int
|
var isCore, isEnabled, hasBackend int
|
||||||
rows.Scan(&m.ID, &m.Name, &m.Description, &m.Version, &isCore, &isEnabled,
|
rows.Scan(&m.ID, &m.Name, &m.Description, &m.Version, &isCore, &isEnabled, &hasBackend,
|
||||||
&m.NavHref, &m.NavIcon, &m.NavColor, &m.NavLabelKey)
|
&m.NavHref, &m.NavIcon, &m.NavColor, &m.NavLabelKey)
|
||||||
m.IsCore = isCore == 1
|
m.IsCore = isCore == 1
|
||||||
m.IsEnabled = isEnabled == 1
|
m.IsEnabled = isEnabled == 1
|
||||||
|
m.HasBackend = hasBackend == 1
|
||||||
modules = append(modules, m)
|
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))
|
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).
|
// 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.db.Exec(`UPDATE modules SET is_enabled = 0 WHERE id = ?`, id)
|
||||||
h.auditLogger.Log(&claims.UserID, claims.Username, "module_disable", id, nil, clientIP(r))
|
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).
|
// GetLogs retourne les dernières lignes du log applicatif (tampon mémoire).
|
||||||
|
|
@ -350,6 +393,7 @@ type moduleJSON struct {
|
||||||
NavColor string `json:"nav_color"`
|
NavColor string `json:"nav_color"`
|
||||||
NavLabelKey string `json:"nav_label_key"`
|
NavLabelKey string `json:"nav_label_key"`
|
||||||
CoreMinVersion string `json:"core_min_version"`
|
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.
|
// 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
|
// URL du repo
|
||||||
repoURL := fmt.Sprintf("https://git.geronzi.fr/proxmoxPanel/%s", id)
|
repoURL := fmt.Sprintf("https://git.geronzi.fr/proxmoxPanel/%s", id)
|
||||||
|
|
||||||
|
hasBackend := 0
|
||||||
|
if mod.HasBackend {
|
||||||
|
hasBackend = 1
|
||||||
|
}
|
||||||
|
|
||||||
// Insérer ou remplacer en DB
|
// Insérer ou remplacer en DB
|
||||||
_, err = h.db.Exec(`
|
_, 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)
|
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
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
name=excluded.name, description=excluded.description, version=excluded.version,
|
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_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,
|
nav_label_key=excluded.nav_label_key, repo_url=excluded.repo_url,
|
||||||
installed_at=CURRENT_TIMESTAMP
|
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)
|
mod.NavHref, mod.NavIcon, mod.NavColor, mod.NavLabelKey, repoURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
JSONError(w, fmt.Sprintf("Erreur installation module : %v", err), http.StatusInternalServerError)
|
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,
|
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{
|
// Si le module a du code backend : déclencher un rebuild Docker.
|
||||||
"message": fmt.Sprintf("Module %s installé — rebuild requis pour activation", mod.ID),
|
// 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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3
backend/internal/db/migrations/006_has_backend.sql
Normal file
3
backend/internal/db/migrations/006_has_backend.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- Migration 006 : ajout colonne has_backend aux modules
|
||||||
|
-- Indique si le module contient du code Go backend (nécessite un rebuild Docker pour installation).
|
||||||
|
ALTER TABLE modules ADD COLUMN has_backend INTEGER NOT NULL DEFAULT 0;
|
||||||
416
backend/internal/docker/client.go
Normal file
416
backend/internal/docker/client.go
Normal file
|
|
@ -0,0 +1,416 @@
|
||||||
|
// Package docker fournit un client HTTP léger pour l'API Docker via socket Unix.
|
||||||
|
// Utilisé pour reconstruire et redémarrer le container backend lors de l'installation d'un module.
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsRebuilding est vrai lorsqu'un rebuild est en cours.
|
||||||
|
var IsRebuilding atomic.Bool
|
||||||
|
|
||||||
|
// Client communique avec le daemon Docker via le socket Unix monté en volume.
|
||||||
|
type Client struct {
|
||||||
|
http *http.Client
|
||||||
|
socketPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New crée un Client Docker. Utilise /var/run/docker.sock par défaut.
|
||||||
|
func New() *Client {
|
||||||
|
socketPath := envOr("DOCKER_SOCKET", "/var/run/docker.sock")
|
||||||
|
transport := &http.Transport{
|
||||||
|
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||||
|
return (&net.Dialer{}).DialContext(ctx, "unix", socketPath)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return &Client{
|
||||||
|
http: &http.Client{Transport: transport, Timeout: 15 * time.Minute},
|
||||||
|
socketPath: socketPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Available indique si le socket Docker est accessible.
|
||||||
|
func (c *Client) Available() bool {
|
||||||
|
_, err := os.Stat(c.socketPath)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Structures Docker API ----
|
||||||
|
|
||||||
|
// buildEvent est une ligne du stream JSON retourné par l'API build.
|
||||||
|
type buildEvent struct {
|
||||||
|
Stream string `json:"stream"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// inspectResult contient les champs utiles retournés par /containers/{id}/json.
|
||||||
|
type inspectResult struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
Config struct {
|
||||||
|
Image string `json:"Image"`
|
||||||
|
Env []string `json:"Env"`
|
||||||
|
Cmd []string `json:"Cmd"`
|
||||||
|
Entrypoint []string `json:"Entrypoint"`
|
||||||
|
WorkingDir string `json:"WorkingDir"`
|
||||||
|
ExposedPorts map[string]struct{} `json:"ExposedPorts"`
|
||||||
|
} `json:"Config"`
|
||||||
|
HostConfig struct {
|
||||||
|
Binds []string `json:"Binds"`
|
||||||
|
RestartPolicy struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
} `json:"RestartPolicy"`
|
||||||
|
GroupAdd []string `json:"GroupAdd"`
|
||||||
|
NetworkMode string `json:"NetworkMode"`
|
||||||
|
} `json:"HostConfig"`
|
||||||
|
NetworkSettings struct {
|
||||||
|
Networks map[string]struct {
|
||||||
|
NetworkID string `json:"NetworkID"`
|
||||||
|
} `json:"Networks"`
|
||||||
|
} `json:"NetworkSettings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// containerCreateBody correspond au corps de POST /containers/create.
|
||||||
|
type containerCreateBody struct {
|
||||||
|
Image string `json:"Image"`
|
||||||
|
Env []string `json:"Env"`
|
||||||
|
Cmd []string `json:"Cmd,omitempty"`
|
||||||
|
Entrypoint []string `json:"Entrypoint,omitempty"`
|
||||||
|
WorkingDir string `json:"WorkingDir,omitempty"`
|
||||||
|
ExposedPorts map[string]struct{} `json:"ExposedPorts,omitempty"`
|
||||||
|
HostConfig struct {
|
||||||
|
Binds []string `json:"Binds"`
|
||||||
|
RestartPolicy struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
} `json:"RestartPolicy"`
|
||||||
|
GroupAdd []string `json:"GroupAdd,omitempty"`
|
||||||
|
NetworkMode string `json:"NetworkMode,omitempty"`
|
||||||
|
} `json:"HostConfig"`
|
||||||
|
NetworkingConfig struct {
|
||||||
|
EndpointsConfig map[string]map[string]string `json:"EndpointsConfig"`
|
||||||
|
} `json:"NetworkingConfig"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Opérations Docker ----
|
||||||
|
|
||||||
|
// BuildImage construit une nouvelle image depuis le sous-répertoire d'un repo git.
|
||||||
|
// Le build arg MODULES contient les IDs de modules séparés par des virgules.
|
||||||
|
func (c *Client) BuildImage(ctx context.Context, imageTag, gitRepo, gitBranch, subdir string, moduleIDs []string) error {
|
||||||
|
modulesStr := strings.Join(moduleIDs, ",")
|
||||||
|
|
||||||
|
// Format Docker : repo.git#branch:sous-répertoire
|
||||||
|
remoteCtx := fmt.Sprintf("%s#%s:%s", gitRepo, gitBranch, subdir)
|
||||||
|
|
||||||
|
buildArgsJSON, _ := json.Marshal(map[string]string{"MODULES": modulesStr})
|
||||||
|
|
||||||
|
q := url.Values{}
|
||||||
|
q.Set("remote", remoteCtx)
|
||||||
|
q.Set("dockerfile", "Dockerfile")
|
||||||
|
q.Set("t", imageTag)
|
||||||
|
q.Set("buildargs", string(buildArgsJSON))
|
||||||
|
q.Set("rm", "1")
|
||||||
|
|
||||||
|
resp, err := c.do(ctx, "POST", "/v1.47/build?"+q.Encode(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("appel API build : %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Lire le stream de build ligne par ligne — retourner dès qu'une erreur est détectée.
|
||||||
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
|
for scanner.Scan() {
|
||||||
|
var ev buildEvent
|
||||||
|
if err := json.Unmarshal(scanner.Bytes(), &ev); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ev.Error != "" {
|
||||||
|
return fmt.Errorf("erreur build Docker : %s", ev.Error)
|
||||||
|
}
|
||||||
|
if ev.Stream != "" {
|
||||||
|
log.Printf("[docker-build] %s", strings.TrimRight(ev.Stream, "\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// InspectContainer retourne la configuration du container (pour le recréer).
|
||||||
|
func (c *Client) InspectContainer(ctx context.Context, name string) (*inspectResult, error) {
|
||||||
|
resp, err := c.do(ctx, "GET", "/v1.47/containers/"+name+"/json", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
var info inspectResult
|
||||||
|
if err := json.Unmarshal(body, &info); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse inspect : %w", err)
|
||||||
|
}
|
||||||
|
return &info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateContainer crée un nouveau container et retourne son ID.
|
||||||
|
func (c *Client) CreateContainer(ctx context.Context, name string, body *containerCreateBody) (string, error) {
|
||||||
|
bodyJSON, _ := json.Marshal(body)
|
||||||
|
resp, err := c.do(ctx, "POST", "/v1.47/containers/create?name="+url.QueryEscape(name), bytes.NewReader(bodyJSON))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
raw, _ := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
ID string `json:"Id"`
|
||||||
|
}
|
||||||
|
json.Unmarshal(raw, &result)
|
||||||
|
if result.ID == "" {
|
||||||
|
return "", fmt.Errorf("création container échouée : %s", string(raw))
|
||||||
|
}
|
||||||
|
return result.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartContainer démarre un container par son ID.
|
||||||
|
func (c *Client) StartContainer(ctx context.Context, id string) error {
|
||||||
|
_, err := c.do(ctx, "POST", "/v1.47/containers/"+id+"/start", nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopContainer arrête un container (timeout 15s).
|
||||||
|
func (c *Client) StopContainer(ctx context.Context, name string) error {
|
||||||
|
_, err := c.do(ctx, "POST", "/v1.47/containers/"+name+"/stop?t=15", nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveContainer supprime un container arrêté.
|
||||||
|
func (c *Client) RemoveContainer(ctx context.Context, name string) error {
|
||||||
|
_, err := c.do(ctx, "DELETE", "/v1.47/containers/"+name, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenameContainer renomme un container.
|
||||||
|
func (c *Client) RenameContainer(ctx context.Context, current, newName string) error {
|
||||||
|
_, err := c.do(ctx, "POST",
|
||||||
|
fmt.Sprintf("/v1.47/containers/%s/rename?name=%s", current, url.QueryEscape(newName)), nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Orchestration rebuild ----
|
||||||
|
|
||||||
|
// RebuildAndRestart lance un rebuild en arrière-plan :
|
||||||
|
// 1. Construit la nouvelle image avec les modules demandés.
|
||||||
|
// 2. Crée un container de remplacement qui, à son démarrage, arrêtera l'ancien.
|
||||||
|
//
|
||||||
|
// Cette méthode est non-bloquante : elle retourne immédiatement.
|
||||||
|
// La réponse HTTP doit être envoyée AVANT d'appeler cette méthode.
|
||||||
|
func (c *Client) RebuildAndRestart(moduleIDs []string) {
|
||||||
|
if !IsRebuilding.CompareAndSwap(false, true) {
|
||||||
|
log.Println("[docker] Rebuild déjà en cours, ignoré")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
containerName := envOr("CONTAINER_NAME", "proxmoxpanel-backend")
|
||||||
|
gitRepo := envOr("GIT_REPO", "https://git.geronzi.fr/proxmoxPanel/core.git")
|
||||||
|
gitBranch := envOr("GIT_BRANCH", "frontend/alpine")
|
||||||
|
imageTag := envOr("IMAGE_TAG", "proxmoxpanel-backend:latest")
|
||||||
|
nextName := containerName + "-next"
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer IsRebuilding.Store(false)
|
||||||
|
|
||||||
|
log.Printf("[docker] Rebuild démarré — modules : %v", moduleIDs)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Étape 1 — Build de la nouvelle image
|
||||||
|
if err := c.BuildImage(ctx, imageTag, gitRepo, gitBranch, "backend", moduleIDs); err != nil {
|
||||||
|
log.Printf("[docker] Erreur build image : %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("[docker] Nouvelle image construite")
|
||||||
|
|
||||||
|
// Étape 2 — Inspecter le container courant pour récupérer sa config
|
||||||
|
info, err := c.InspectContainer(ctx, containerName)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[docker] Erreur inspection container : %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Étape 3 — Préparer la config du container de remplacement
|
||||||
|
body := buildReplaceBody(info, imageTag, nextName, containerName)
|
||||||
|
bodyJSON, _ := json.Marshal(body)
|
||||||
|
log.Printf("[docker] Config container next : %s", string(bodyJSON))
|
||||||
|
|
||||||
|
// Supprimer le container "next" s'il existe déjà (rebuild précédent raté)
|
||||||
|
c.StopContainer(ctx, nextName)
|
||||||
|
c.RemoveContainer(ctx, nextName)
|
||||||
|
|
||||||
|
// Étape 4 — Créer le container de remplacement
|
||||||
|
nextID, err := c.CreateContainer(ctx, nextName, body)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[docker] Erreur création container next : %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Étape 5 — Démarrer le container de remplacement
|
||||||
|
// Il stoppera et remplacera l'actuel au démarrage (voir main.go : handleReplacement).
|
||||||
|
if err := c.StartContainer(ctx, nextID); err != nil {
|
||||||
|
log.Printf("[docker] Erreur démarrage container next : %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[docker] Container %s démarré — remplacement de %s en cours", nextName, containerName)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart redémarre le container courant en arrière-plan (même image).
|
||||||
|
// Utilisé lors de l'activation/désactivation d'un module has_backend.
|
||||||
|
func (c *Client) Restart() {
|
||||||
|
containerName := envOr("CONTAINER_NAME", "proxmoxpanel-backend")
|
||||||
|
go func() {
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
log.Printf("[docker] Redémarrage du container %s...", containerName)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := c.StopContainer(ctx, containerName); err != nil {
|
||||||
|
log.Printf("[docker] Erreur stop container : %v", err)
|
||||||
|
}
|
||||||
|
// La restart policy unless-stopped ne redémarre pas après un stop explicite.
|
||||||
|
// On recrée le container à partir de son inspect.
|
||||||
|
info, err := c.InspectContainer(ctx, containerName)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[docker] Erreur inspect pour restart : %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.RemoveContainer(ctx, containerName)
|
||||||
|
body := buildSameBody(info)
|
||||||
|
id, err := c.CreateContainer(ctx, containerName, body)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[docker] Erreur recréation container : %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := c.StartContainer(ctx, id); err != nil {
|
||||||
|
log.Printf("[docker] Erreur démarrage après restart : %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleReplacement gère le cas où ce container est un remplacement.
|
||||||
|
// À appeler au tout début de main() si REPLACING_CONTAINER est défini.
|
||||||
|
// Arrête l'ancien container, le supprime, se renomme.
|
||||||
|
func (c *Client) HandleReplacement() {
|
||||||
|
old := os.Getenv("REPLACING_CONTAINER")
|
||||||
|
myName := envOr("CONTAINER_NAME", "proxmoxpanel-backend-next")
|
||||||
|
if old == "" || !c.Available() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[docker] Mode remplacement : arrêt du container %s...", old)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Petit délai pour que l'ancien container finisse d'envoyer ses réponses HTTP en cours
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
if err := c.StopContainer(ctx, old); err != nil {
|
||||||
|
log.Printf("[docker] Avertissement : impossible d'arrêter %s : %v", old, err)
|
||||||
|
}
|
||||||
|
if err := c.RemoveContainer(ctx, old); err != nil {
|
||||||
|
log.Printf("[docker] Avertissement : impossible de supprimer %s : %v", old, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se renommer pour prendre l'identité du container remplacé
|
||||||
|
if err := c.RenameContainer(ctx, myName, old); err != nil {
|
||||||
|
log.Printf("[docker] Avertissement : impossible de renommer %s → %s : %v", myName, old, err)
|
||||||
|
} else {
|
||||||
|
log.Printf("[docker] Container renommé %s → %s", myName, old)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Helpers internes ----
|
||||||
|
|
||||||
|
// buildReplaceBody construit la config du container de remplacement.
|
||||||
|
// Ajoute REPLACING_CONTAINER et met à jour CONTAINER_NAME dans les env vars.
|
||||||
|
func buildReplaceBody(info *inspectResult, newImage, newContainerName, replacingName string) *containerCreateBody {
|
||||||
|
body := buildSameBody(info)
|
||||||
|
body.Image = newImage
|
||||||
|
|
||||||
|
// Mettre à jour les env vars de remplacement
|
||||||
|
newEnv := make([]string, 0, len(info.Config.Env)+2)
|
||||||
|
for _, e := range info.Config.Env {
|
||||||
|
if strings.HasPrefix(e, "CONTAINER_NAME=") || strings.HasPrefix(e, "REPLACING_CONTAINER=") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newEnv = append(newEnv, e)
|
||||||
|
}
|
||||||
|
newEnv = append(newEnv,
|
||||||
|
"CONTAINER_NAME="+newContainerName,
|
||||||
|
"REPLACING_CONTAINER="+replacingName,
|
||||||
|
)
|
||||||
|
body.Env = newEnv
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildSameBody construit la config d'un container identique (même image).
|
||||||
|
func buildSameBody(info *inspectResult) *containerCreateBody {
|
||||||
|
body := &containerCreateBody{}
|
||||||
|
body.Image = info.Config.Image
|
||||||
|
body.Env = info.Config.Env
|
||||||
|
body.Cmd = info.Config.Cmd
|
||||||
|
body.Entrypoint = info.Config.Entrypoint
|
||||||
|
body.WorkingDir = info.Config.WorkingDir
|
||||||
|
body.ExposedPorts = info.Config.ExposedPorts
|
||||||
|
body.HostConfig.Binds = info.HostConfig.Binds
|
||||||
|
body.HostConfig.RestartPolicy.Name = info.HostConfig.RestartPolicy.Name
|
||||||
|
body.HostConfig.GroupAdd = info.HostConfig.GroupAdd
|
||||||
|
body.HostConfig.NetworkMode = info.HostConfig.NetworkMode
|
||||||
|
|
||||||
|
// Réseaux
|
||||||
|
endpoints := make(map[string]map[string]string)
|
||||||
|
for netName, netInfo := range info.NetworkSettings.Networks {
|
||||||
|
endpoints[netName] = map[string]string{"NetworkID": netInfo.NetworkID}
|
||||||
|
}
|
||||||
|
body.NetworkingConfig.EndpointsConfig = endpoints
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// do effectue une requête HTTP vers l'API Docker (http://docker = socket Unix).
|
||||||
|
func (c *Client) do(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, "http://docker"+path, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if body != nil {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
resp, err := c.http.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Les codes 2xx et 304 sont considérés comme succès
|
||||||
|
if resp.StatusCode >= 300 && resp.StatusCode != 304 {
|
||||||
|
raw, _ := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
return nil, fmt.Errorf("Docker API %s %s → HTTP %d : %s", method, path, resp.StatusCode, string(raw))
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func envOr(key, def string) string {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"git.geronzi.fr/proxmoxPanel/core/backend/internal/auth"
|
"git.geronzi.fr/proxmoxPanel/core/backend/internal/auth"
|
||||||
"git.geronzi.fr/proxmoxPanel/core/backend/internal/crypto"
|
"git.geronzi.fr/proxmoxPanel/core/backend/internal/crypto"
|
||||||
"git.geronzi.fr/proxmoxPanel/core/backend/internal/db"
|
"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"
|
"git.geronzi.fr/proxmoxPanel/core/backend/internal/logbuffer"
|
||||||
sshpool "git.geronzi.fr/proxmoxPanel/core/backend/internal/ssh"
|
sshpool "git.geronzi.fr/proxmoxPanel/core/backend/internal/ssh"
|
||||||
"git.geronzi.fr/proxmoxPanel/core/backend/internal/websocket"
|
"git.geronzi.fr/proxmoxPanel/core/backend/internal/websocket"
|
||||||
|
|
@ -26,6 +27,14 @@ func main() {
|
||||||
// Brancher le buffer de logs (stderr + mémoire) avant tout autre log
|
// Brancher le buffer de logs (stderr + mémoire) avant tout autre log
|
||||||
log.SetOutput(io.MultiWriter(os.Stderr, logbuffer.Global))
|
log.SetOutput(io.MultiWriter(os.Stderr, logbuffer.Global))
|
||||||
|
|
||||||
|
// ── Gestion du remplacement de container (rebuild module) ──────────────
|
||||||
|
// Si REPLACING_CONTAINER est défini, ce container est un successeur issu d'un rebuild.
|
||||||
|
// Il arrête l'ancien container et se renomme avant de continuer le démarrage normal.
|
||||||
|
docker := dockerclient.New()
|
||||||
|
if docker.Available() {
|
||||||
|
docker.HandleReplacement()
|
||||||
|
}
|
||||||
|
|
||||||
// Répertoire de données persistantes (volume Docker)
|
// Répertoire de données persistantes (volume Docker)
|
||||||
dataDir := getEnv("DATA_DIR", "/app/data")
|
dataDir := getEnv("DATA_DIR", "/app/data")
|
||||||
|
|
||||||
|
|
@ -68,6 +77,7 @@ func main() {
|
||||||
|
|
||||||
// ── Chargement des modules actifs ──────────────────────────────────────
|
// ── Chargement des modules actifs ──────────────────────────────────────
|
||||||
loader := modules.NewLoader(database, sshPool, encryptor)
|
loader := modules.NewLoader(database, sshPool, encryptor)
|
||||||
|
RegisterModules(loader) // généré par cmd/gen-modules selon les modules compilés
|
||||||
if err := loader.LoadActive(); err != nil {
|
if err := loader.LoadActive(); err != nil {
|
||||||
log.Fatalf("Erreur chargement modules : %v", err)
|
log.Fatalf("Erreur chargement modules : %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +87,7 @@ func main() {
|
||||||
authHandler := api.NewAuthHandler(database, jwtManager, sshAuthenticator, auditLogger)
|
authHandler := api.NewAuthHandler(database, jwtManager, sshAuthenticator, auditLogger)
|
||||||
proxmoxHandler := api.NewProxmoxHandler(database, hub, auditLogger, encryptor)
|
proxmoxHandler := api.NewProxmoxHandler(database, hub, auditLogger, encryptor)
|
||||||
updatesHandler := api.NewUpdatesHandler(database, sshPool, hub, auditLogger, encryptor)
|
updatesHandler := api.NewUpdatesHandler(database, sshPool, hub, auditLogger, encryptor)
|
||||||
settingsHandler := api.NewSettingsHandler(database, auditLogger, encryptor)
|
settingsHandler := api.NewSettingsHandler(database, auditLogger, encryptor, docker)
|
||||||
terminalHandler := api.NewTerminalHandler(database, auditLogger, encryptor)
|
terminalHandler := api.NewTerminalHandler(database, auditLogger, encryptor)
|
||||||
|
|
||||||
// Démarrer le polling Proxmox en arrière-plan
|
// Démarrer le polling Proxmox en arrière-plan
|
||||||
|
|
|
||||||
8
backend/registered_modules.go
Normal file
8
backend/registered_modules.go
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
// Code généré automatiquement par cmd/gen-modules — ne pas modifier manuellement.
|
||||||
|
// Régénéré lors du build Docker avec la liste des modules compilés.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "git.geronzi.fr/proxmoxPanel/core/backend/modules"
|
||||||
|
|
||||||
|
// RegisterModules enregistre les modules compilés dans le binaire.
|
||||||
|
func RegisterModules(l *modules.Loader) {}
|
||||||
|
|
@ -17,10 +17,25 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
# Volume persistant pour SQLite, clés JWT, clé maître AES
|
# Volume persistant pour SQLite, clés JWT, clé maître AES
|
||||||
- panel-data:/app/data
|
- panel-data:/app/data
|
||||||
|
# Socket Docker — permet au backend de reconstruire son propre container lors d'un install de module
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
group_add:
|
||||||
|
# GID du groupe docker sur l'hôte (docker group = accès au socket).
|
||||||
|
# Trouver avec : getent group docker | cut -d: -f3
|
||||||
|
# Surcharger avec : DOCKER_GID=xxx docker compose up -d
|
||||||
|
- "${DOCKER_GID:-999}"
|
||||||
environment:
|
environment:
|
||||||
- DATA_DIR=/app/data
|
- DATA_DIR=/app/data
|
||||||
- LISTEN_ADDR=:3001
|
- LISTEN_ADDR=:3001
|
||||||
- APP_ENV=production
|
- APP_ENV=production
|
||||||
|
# Identité de ce container (utilisé pour l'auto-rebuild des modules)
|
||||||
|
- CONTAINER_NAME=proxmoxpanel-backend
|
||||||
|
# Repo git du CORE (pour docker build --remote lors d'un install module)
|
||||||
|
- GIT_REPO=https://git.geronzi.fr/proxmoxPanel/core.git
|
||||||
|
# Branche git à utiliser pour le rebuild
|
||||||
|
- GIT_BRANCH=frontend/alpine
|
||||||
|
# Tag de l'image Docker construite
|
||||||
|
- IMAGE_TAG=proxmoxpanel-backend:latest
|
||||||
# Pas de réseau host — le container reste isolé
|
# Pas de réseau host — le container reste isolé
|
||||||
# Les connexions SSH sortantes vers le host Proxmox sont autorisées via le réseau Docker
|
# Les connexions SSH sortantes vers le host Proxmox sont autorisées via le réseau Docker
|
||||||
networks:
|
networks:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue