cmd, config, demo : commande serve, secrets venant de la base, mode démo
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015a8P6WMRPeqdpd5VCh6jAy
This commit is contained in:
1 parent
b597830360
commit
e61f2e4740
4 files changed
+351
-37
No files matched your search
@@ -1,6 +1,7 @@
|
||||
// pve-confsync synchronizes part of the configuration of a reference Proxmox VE
|
||||
// cluster (datacenter options, SDN VLAN/VXLAN zones, vnets, subnets) to client
|
||||
// clusters through the PVE API. Phase 0: command line only.
|
||||
// clusters through the PVE API: command line (check, export, diff, apply) and
|
||||
// web UI (serve).
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
"pve-confsync/internal/engine"
|
||||
"pve-confsync/internal/model"
|
||||
"pve-confsync/internal/pve"
|
||||
"pve-confsync/internal/service"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
@@ -37,6 +39,7 @@ Commandes :
|
||||
export exporte l'état normalisé d'un cluster en YAML (défaut : la référence)
|
||||
diff affiche les écarts référence -> clients (code retour 2 s'il y en a)
|
||||
apply dry-run par défaut ; applique avec -confirm
|
||||
serve interface web (clusters, diff, dry-run, synchronisation, journal)
|
||||
version affiche la version
|
||||
|
||||
Lancer "pve-confsync <commande> -h" pour les options d'une commande.
|
||||
@@ -69,6 +72,8 @@ func main() {
|
||||
code, err = cmdDiff(ctx, *cfgFile, args[1:])
|
||||
case "apply":
|
||||
err = cmdApply(ctx, *cfgFile, args[1:])
|
||||
case "serve":
|
||||
err = cmdServe(ctx, args[1:])
|
||||
default:
|
||||
global.Usage()
|
||||
os.Exit(1)
|
||||
@@ -116,7 +121,7 @@ func cmdCheck(ctx context.Context, cfgFile string, args []string) error {
|
||||
fmt.Printf(" avertissement : droits du token non lisibles (%v)\n", err)
|
||||
return
|
||||
}
|
||||
for _, w := range privilegeWarnings(perms, wantWrite, cfg.HasKind(model.KindDatacenter)) {
|
||||
for _, w := range service.PrivilegeWarnings(perms, wantWrite, cfg.HasKind(model.KindDatacenter)) {
|
||||
fmt.Println(" avertissement :", w)
|
||||
}
|
||||
}
|
||||
@@ -127,39 +132,6 @@ func cmdCheck(ctx context.Context, cfgFile string, args []string) error {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// privilegeWarnings flags a reference token able to write, and a client token
|
||||
// missing the privileges needed by the synchronized kinds.
|
||||
func privilegeWarnings(perms map[string]map[string]any, write, dcOptions bool) []string {
|
||||
has := func(path, priv string) bool { _, ok := perms[path][priv]; return ok }
|
||||
any := func(priv string) bool {
|
||||
for p := range perms {
|
||||
if has(p, priv) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
var w []string
|
||||
if !write {
|
||||
for _, p := range []string{"Sys.Modify", "SDN.Allocate", "Permissions.Modify"} {
|
||||
if any(p) {
|
||||
w = append(w, "le token de référence possède "+p+" : un token en lecture seule est attendu")
|
||||
}
|
||||
}
|
||||
return w
|
||||
}
|
||||
if !has("/sdn", "SDN.Allocate") && !has("/sdn/zones", "SDN.Allocate") {
|
||||
w = append(w, "SDN.Allocate absent sur /sdn : la synchro SDN échouera")
|
||||
}
|
||||
if dcOptions && !has("/", "Sys.Modify") {
|
||||
w = append(w, "Sys.Modify absent sur / : les options datacenter ne pourront pas être écrites")
|
||||
}
|
||||
if !has("/", "Sys.Audit") {
|
||||
w = append(w, "Sys.Audit absent sur / : options datacenter et versions illisibles")
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// ---- export ----
|
||||
|
||||
func cmdExport(ctx context.Context, cfgFile string, args []string) error {
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"pve-confsync/internal/config"
|
||||
"pve-confsync/internal/demo"
|
||||
"pve-confsync/internal/server"
|
||||
"pve-confsync/internal/store"
|
||||
"pve-confsync/web"
|
||||
)
|
||||
|
||||
// cmdServe starts the web UI (phases 1 and 2). Settings come from flags or from
|
||||
// the environment, like Logstream (see README).
|
||||
func cmdServe(ctx context.Context, args []string) error {
|
||||
fl := flag.NewFlagSet("serve", flag.ExitOnError)
|
||||
listen := fl.String("listen", envOr("LISTEN", ":8080"), "adresse d'écoute HTTP")
|
||||
dataDir := fl.String("data", envOr("DATA_DIR", "/data"), "répertoire des données (base SQLite, clés)")
|
||||
demoMode := fl.Bool("demo", envBool("CONFSYNC_DEMO", false), "mode démo : faux clusters PVE en mémoire, base temporaire")
|
||||
webDir := fl.String("web", os.Getenv("WEB_DIR"), "servir les pages depuis ce répertoire au lieu de celles embarquées (développement)")
|
||||
_ = fl.Parse(args)
|
||||
|
||||
if *demoMode && os.Getenv("DATA_DIR") == "" && !flagSet(fl, "data") {
|
||||
dir, err := os.MkdirTemp("", "confsync-demo-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
*dataDir = dir
|
||||
}
|
||||
if err := os.MkdirAll(*dataDir, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
key, keySrc, err := store.LoadKey(*dataDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st, err := store.Open(filepath.Join(*dataDir, "confsync.db"), key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer st.Close()
|
||||
log.Printf("database %s, encryption key from %s", filepath.Join(*dataDir, "confsync.db"), keySrc)
|
||||
|
||||
if *demoMode {
|
||||
clusters, err := demo.Start(ctx, st)
|
||||
if err != nil {
|
||||
return fmt.Errorf("demo: %w", err)
|
||||
}
|
||||
defer clusters.Close()
|
||||
log.Printf("DEMO mode: fake PVE clusters started; accounts admin, operateur, lecteur, password %q", demo.Password)
|
||||
} else if err := bootstrap(ctx, st); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ttl, err := time.ParseDuration(envOr("SESSION_TTL", "12h"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("SESSION_TTL: %w", err)
|
||||
}
|
||||
var static fs.FS = web.FS
|
||||
if *webDir != "" {
|
||||
static = os.DirFS(*webDir)
|
||||
}
|
||||
srv, err := server.New(server.Options{
|
||||
Store: st,
|
||||
Static: static,
|
||||
Version: version,
|
||||
Demo: *demoMode,
|
||||
Auth: server.AuthConfig{
|
||||
Local: envBool("AUTH_LOCAL", true),
|
||||
Issuer: os.Getenv("OIDC_ISSUER"),
|
||||
ClientID: os.Getenv("OIDC_CLIENT_ID"),
|
||||
ClientSecret: secretEnv("OIDC_CLIENT_SECRET"),
|
||||
RedirectURL: os.Getenv("OIDC_REDIRECT_URL"),
|
||||
Scopes: os.Getenv("OIDC_SCOPES"),
|
||||
GroupsClaim: os.Getenv("OIDC_GROUPS_CLAIM"),
|
||||
AdminGroup: os.Getenv("OIDC_ADMIN_GROUP"),
|
||||
OperatorGroup: os.Getenv("OIDC_OPERATOR_GROUP"),
|
||||
DefaultRole: os.Getenv("OIDC_DEFAULT_ROLE"),
|
||||
SessionTTL: ttl,
|
||||
DataDir: *dataDir,
|
||||
Demo: *demoMode,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hs := &http.Server{Addr: *listen, Handler: srv.Handler(), ReadHeaderTimeout: 10 * time.Second}
|
||||
errc := make(chan error, 1)
|
||||
go func() { errc <- hs.ListenAndServe() }()
|
||||
log.Printf("pve-confsync %s: web UI on %s", version, *listen)
|
||||
select {
|
||||
case err := <-errc:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
}
|
||||
log.Printf("stopping: waiting for the synchronizations in progress")
|
||||
sctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
_ = hs.Shutdown(sctx)
|
||||
srv.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// bootstrap creates the first administrator and imports a YAML configuration
|
||||
// on an empty database.
|
||||
func bootstrap(ctx context.Context, st *store.Store) error {
|
||||
n, err := st.CountUsers(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
user := envOr("CONFSYNC_ADMIN_USER", "admin")
|
||||
pass := secretEnv("CONFSYNC_ADMIN_PASSWORD")
|
||||
generated := pass == ""
|
||||
if generated {
|
||||
pass = store.RandomPassword()
|
||||
}
|
||||
if err := st.SaveUser(ctx, user, store.RoleAdmin, pass); err != nil {
|
||||
return fmt.Errorf("first administrator: %w", err)
|
||||
}
|
||||
if generated {
|
||||
log.Printf("first administrator %q created, password: %s (change it in Settings > Account)", user, pass)
|
||||
} else {
|
||||
log.Printf("first administrator %q created", user)
|
||||
}
|
||||
}
|
||||
if file := os.Getenv("CONFSYNC_IMPORT"); file != "" {
|
||||
cls, err := st.Clusters(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(cls) > 0 {
|
||||
return nil // imported once: the database is the reference from now on
|
||||
}
|
||||
cfg, err := config.Load(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CONFSYNC_IMPORT: %w", err)
|
||||
}
|
||||
warns, err := st.ImportConfig(ctx, cfg, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CONFSYNC_IMPORT: %w", err)
|
||||
}
|
||||
for _, w := range warns {
|
||||
log.Printf("import: %s", w)
|
||||
}
|
||||
log.Printf("configuration imported from %s", file)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// secretEnv reads NAME, or the file named by NAME_FILE (docker secret).
|
||||
func secretEnv(name string) string {
|
||||
if v := os.Getenv(name); v != "" {
|
||||
return v
|
||||
}
|
||||
if f := os.Getenv(name + "_FILE"); f != "" {
|
||||
b, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
log.Printf("%s_FILE: %v", name, err)
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(b))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func envBool(name string, def bool) bool {
|
||||
v := os.Getenv(name)
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
b, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
log.Printf("%s=%q: true or false expected, using %t", name, v, def)
|
||||
return def
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func flagSet(fs *flag.FlagSet, name string) bool {
|
||||
found := false
|
||||
fs.Visit(func(f *flag.Flag) {
|
||||
if f.Name == name {
|
||||
found = true
|
||||
}
|
||||
})
|
||||
return found
|
||||
}
|
||||
Reference in new issue
Block a user