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:
claude BotandClaude Opus 5.5 committed 2026-10-13 15:56:10 +02:00
1 parent b597830360
commit e61f2e4740
4 files changed
+351 -37

No files matched your search

+16 -2
View File
@@ -30,6 +30,10 @@ type Cluster struct {
Fingerprint string `yaml:"fingerprint,omitempty"`
Insecure bool `yaml:"insecure,omitempty"`
Timeout string `yaml:"timeout,omitempty"`
// Set by the web UI from its database, never read from nor written to YAML.
Secret string `yaml:"-"` // token secret, decrypted
CAPEM string `yaml:"-"` // CA bundle, PEM
}
// Client is a target cluster with its mapping (kind -> id -> field -> value)
@@ -74,6 +78,9 @@ func Load(file string) (*Config, error) {
return &c, c.validate()
}
// Validate checks a configuration built elsewhere (web UI database, YAML import).
func (c *Config) Validate() error { return c.validate() }
func (c *Config) validate() error {
var errs []error
seen := map[string]bool{}
@@ -86,7 +93,7 @@ func (c *Config) validate() error {
errs = append(errs, fmt.Errorf("cluster %s: duplicate name", cl.Name))
}
seen[cl.Name] = true
if cl.TokenSecretEnv == "" && cl.TokenSecretFile == "" {
if cl.Secret == "" && cl.TokenSecretEnv == "" && cl.TokenSecretFile == "" {
errs = append(errs, fmt.Errorf("cluster %s: token_secret_env or token_secret_file is required", cl.Name))
}
}
@@ -158,11 +165,18 @@ func (cl Cluster) PVE() (*pve.Client, error) {
}
return pve.New(pve.Options{
Name: cl.Name, BaseURL: cl.URL, TokenID: cl.TokenID, TokenSecret: secret,
CAFile: cl.CAFile, Fingerprint: cl.Fingerprint, Insecure: cl.Insecure, Timeout: timeout,
CAFile: cl.CAFile, CAPEM: []byte(cl.CAPEM), Fingerprint: cl.Fingerprint, Insecure: cl.Insecure, Timeout: timeout,
})
}
// ResolveSecret reads the token secret from the environment variable or file of
// the YAML configuration (used when a YAML file is imported in the web UI).
func (cl Cluster) ResolveSecret() (string, error) { return cl.secret() }
func (cl Cluster) secret() (string, error) {
if cl.Secret != "" {
return cl.Secret, nil
}
if cl.TokenSecretEnv != "" {
if v := os.Getenv(cl.TokenSecretEnv); v != "" {
return v, nil
+129
View File
@@ -0,0 +1,129 @@
// Package demo starts fake PVE clusters (pvetest) and fills a database with
// them, to try the web UI without a Proxmox cluster: a reference, two clients
// with differences (missing objects, different fields, a missing mapping) and
// a PVE 8 cluster that is refused.
package demo
import (
"context"
"crypto/sha256"
"encoding/hex"
"pve-confsync/internal/adapter"
"pve-confsync/internal/model"
"pve-confsync/internal/pve/pvetest"
"pve-confsync/internal/store"
)
// Password of the demo accounts admin, operateur and lecteur.
const Password = "confsync-demo"
// Clusters are the fake clusters; Close stops them.
type Clusters struct{ servers []*pvetest.Server }
// Close stops the fake clusters.
func (c *Clusters) Close() {
for _, s := range c.servers {
s.Close()
}
}
// Start starts the fake clusters and records them (and the demo accounts) in st.
func Start(ctx context.Context, st *store.Store) (*Clusters, error) {
ref := pvetest.New("9.1.4")
ref.Options = map[string]string{
"registered-tags": "backup;dev;prod",
"tag-style": "color-map=prod:c0392b:ffffff;dev:2980b9:ffffff,ordering=config,shape=full",
"next-id": "lower=100,upper=999999",
"user-tag-access": "user-allow=list,user-allow-list=team-a;team-b",
"keyboard": "fr",
}
ref.Pending.Zones["vlanz"] = map[string]any{"type": "vlan", "bridge": "vmbr0", "mtu": 1500, "ipam": "pve"}
ref.Pending.Zones["stor"] = map[string]any{"type": "vlan", "bridge": "vmbr2", "mtu": 9000, "ipam": "pve"}
ref.Pending.Zones["vxz"] = map[string]any{"type": "vxlan", "peers": "10.0.0.1,10.0.0.2", "mtu": 1450, "ipam": "pve"}
ref.Pending.Zones["evpnz"] = map[string]any{"type": "evpn", "controller": "evpnctl", "vrf-vxlan": 4000}
ref.Pending.VNets["v100"] = map[string]any{"zone": "vlanz", "tag": 100, "alias": "prod"}
ref.Pending.VNets["v200"] = map[string]any{"zone": "vlanz", "tag": 200, "alias": "dev"}
ref.Pending.VNets["ceph"] = map[string]any{"zone": "stor", "tag": 50, "alias": "ceph public"}
ref.Pending.VNets["vx1"] = map[string]any{"zone": "vxz", "tag": 10001, "alias": "overlay"}
ref.Pending.Subnets["vlanz-10.1.0.0-24"] = map[string]any{"vnet": "v100", "gateway": "10.1.0.1",
"dhcp-range": []any{"start-address=10.1.0.100,end-address=10.1.0.200"}}
ref.Pending.Subnets["vlanz-10.2.0.0-24"] = map[string]any{"vnet": "v200", "gateway": "10.2.0.1"}
ref.Commit()
// site-a: tags and an MTU behind, the VXLAN zone and the dev network missing
a := pvetest.New("9.0.10")
a.Options = map[string]string{
"registered-tags": "dev;prod",
"tag-style": "color-map=prod:c0392b:ffffff;dev:2980b9:ffffff,ordering=config,shape=full",
"next-id": "lower=2000,upper=2999",
"keyboard": "en-us",
}
a.Pending.Zones["vlanz"] = map[string]any{"type": "vlan", "bridge": "vmbr1", "mtu": 9000, "ipam": "pve"}
a.Pending.Zones["stor"] = map[string]any{"type": "vlan", "bridge": "vmbr2", "mtu": 9000, "ipam": "pve", "nodes": "pve-a1,pve-a2"}
a.Pending.VNets["v100"] = map[string]any{"zone": "vlanz", "tag": 100, "alias": "production"}
a.Pending.VNets["ceph"] = map[string]any{"zone": "stor", "tag": 50, "alias": "ceph public"}
a.Pending.Subnets["vlanz-10.1.0.0-24"] = map[string]any{"vnet": "v100", "gateway": "10.1.0.254"}
a.Commit()
// site-b: nearly in line, but no mapping for the bridge of the "stor" zone
// (blocking error) and a local zone absent from the reference (kept, no prune)
b := pvetest.New("9.1.2")
b.Options = map[string]string{
"registered-tags": "backup;dev;prod",
"tag-style": "color-map=prod:c0392b:ffffff;dev:2980b9:ffffff,ordering=config,shape=full",
"next-id": "lower=100,upper=999999",
"user-tag-access": "user-allow=list,user-allow-list=team-a;team-b",
}
b.Pending.Zones["vlanz"] = map[string]any{"type": "vlan", "bridge": "vmbr0", "mtu": 1500, "ipam": "pve"}
b.Pending.Zones["vxz"] = map[string]any{"type": "vxlan", "peers": "172.16.0.1,172.16.0.2", "mtu": 1450, "ipam": "pve"}
b.Pending.Zones["labz"] = map[string]any{"type": "vlan", "bridge": "vmbr9", "ipam": "pve"}
b.Pending.VNets["v100"] = map[string]any{"zone": "vlanz", "tag": 100, "alias": "prod"}
b.Pending.VNets["v200"] = map[string]any{"zone": "vlanz", "tag": 200, "alias": "dev"}
b.Pending.Subnets["vlanz-10.1.0.0-24"] = map[string]any{"vnet": "v100", "gateway": "10.1.0.1",
"dhcp-range": []any{"start-address=10.1.0.100,end-address=10.1.0.200"}}
b.Pending.Subnets["vlanz-10.2.0.0-24"] = map[string]any{"vnet": "v200", "gateway": "10.2.0.1"}
b.Commit()
// legacy: PVE 8, refused by the version check
old := pvetest.New("8.4.1")
c := &Clusters{servers: []*pvetest.Server{ref, a, b, old}}
save := func(s *pvetest.Server, name, token string, refer bool, pos int,
mapping map[model.Kind]map[string]map[string]any, pol map[model.Kind]map[string]adapter.Policy) error {
sum := sha256.Sum256(s.Certificate().Raw)
return st.SaveCluster(ctx, "", store.Cluster{Name: name, Reference: refer, Position: pos, URL: s.URL,
TokenID: token, Secret: "demo-" + name, Fingerprint: hex.EncodeToString(sum[:]),
Mapping: mapping, Policies: pol})
}
steps := []error{
save(ref, "ref-rennes", "confsync@pve!ro", true, 0, nil, nil),
save(a, "site-a", "confsync@pve!rw", false, 1, map[model.Kind]map[string]map[string]any{
model.KindDatacenter: {"datacenter": {"next-id": "lower=2000,upper=2999"}},
model.KindZone: {
"vlanz": {"bridge": "vmbr1"},
"stor": {"bridge": "vmbr2", "nodes": "pve-a1,pve-a2"},
"vxz": {"peers": "192.168.10.1,192.168.10.2"},
},
}, nil),
save(b, "site-b", "confsync@pve!rw", false, 2, map[model.Kind]map[string]map[string]any{
model.KindZone: {
"vlanz": {"bridge": "vmbr0"},
"vxz": {"peers": "172.16.0.1,172.16.0.2"},
},
}, map[model.Kind]map[string]adapter.Policy{model.KindZone: {"mtu": adapter.Ignore}}),
save(old, "legacy-pve8", "confsync@pve!rw", false, 3, nil, nil),
st.SaveSettings(ctx, store.Settings{Kinds: model.Kinds,
Policies: map[model.Kind]map[string]adapter.Policy{model.KindDatacenter: {"keyboard": adapter.Sync}}}),
}
for _, u := range []struct{ name, role string }{{"admin", store.RoleAdmin}, {"operateur", store.RoleOperator}, {"lecteur", store.RoleViewer}} {
steps = append(steps, st.SaveUser(ctx, u.name, u.role, Password))
}
for _, err := range steps {
if err != nil {
c.Close()
return nil, err
}
}
return c, nil
}