From ee8343c69f94ab852b4846a5ec83b35324f93d8d Mon Sep 17 00:00:00 2001 From: claude Date: Fri, 23 Oct 2026 18:41:59 +0200 Subject: [PATCH] =?UTF-8?q?Clusters=20:=20port=208006=20par=20d=C3=A9faut,?= =?UTF-8?q?=20liste=20des=20cibles=20jamais=20null?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/config/config.go | 12 ++++++++++++ internal/config/config_test.go | 15 +++++++++++++++ internal/server/server_test.go | 13 +++++++++++++ internal/service/service.go | 2 +- internal/store/store.go | 11 +++++++++-- 5 files changed, 50 insertions(+), 3 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index c4eff65..6a14b5e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -216,3 +216,15 @@ func (c *Config) HasKind(k model.Kind) bool { } return false } + +// NormalizeURL trims the URL of a cluster and adds the PVE port (8006) when +// none is given: https://pve1 becomes https://pve1:8006. +func NormalizeURL(raw string) string { + s := strings.TrimRight(strings.TrimSpace(raw), "/") + u, err := url.Parse(s) + if err != nil || u.Host == "" || u.Port() != "" { + return s + } + u.Host = net.JoinHostPort(u.Hostname(), "8006") + return u.String() +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index d9cdc31..f3a53ce 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -36,3 +36,18 @@ func TestSSHOptions(t *testing.T) { t.Fatalf("%+v", o) } } + +func TestNormalizeURL(t *testing.T) { + for in, want := range map[string]string{ + "https://pve1": "https://pve1:8006", + " https://pve1/ ": "https://pve1:8006", + "https://pve1:443": "https://pve1:443", + "https://10.0.0.1": "https://10.0.0.1:8006", + "https://[fd00::1]": "https://[fd00::1]:8006", + "https://pve1:8006/": "https://pve1:8006", + } { + if got := NormalizeURL(in); got != want { + t.Errorf("NormalizeURL(%q) = %q, want %q", in, got, want) + } + } +} diff --git a/internal/server/server_test.go b/internal/server/server_test.go index b540e65..1c4e0ba 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -239,6 +239,19 @@ func TestFromScratch(t *testing.T) { if _, d := adm.do("GET", "/api/state", nil, false); d["setup"] != "clusters" { t.Fatalf("state on an empty database: %v", d) } + // a primary alone: no target yet, the state lists none (not null); no port: 8006 + if code, d := adm.do("PUT", "/api/clusters/-", map[string]any{"name": "solo", "role": "primary", "url": "https://pve-solo", "password": "x", "tls": "insecure"}, true); code != http.StatusOK { + t.Fatalf("save solo: %d %v", code, d) + } + if _, d := adm.do("GET", "/api/clusters", nil, false); d["clusters"].([]any)[0].(map[string]any)["url"] != "https://pve-solo:8006" { + t.Fatalf("default port expected: %v", d) + } + if _, d := adm.do("GET", "/api/state", nil, false); d["state"] == nil || d["state"].(map[string]any)["clients"] == nil { + t.Fatalf("clients must be an empty list: %v", d) + } + if code, d := adm.do("DELETE", "/api/clusters/solo", nil, true); code != http.StatusOK { + t.Fatalf("delete solo: %d %v", code, d) + } for _, c := range []struct { name string s *pvetest.Server diff --git a/internal/service/service.go b/internal/service/service.go index bcd9a7f..5021f50 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -97,7 +97,7 @@ type State struct { // clients (empty: all). A cluster that cannot be read is reported in its // Error field, it does not stop the others. func Analyze(ctx context.Context, cfg *config.Config, only []string) *State { - st := &State{At: time.Now().UTC(), Kinds: cfg.Kinds, Prune: cfg.Prune, cfg: cfg, actuals: map[string][]model.Object{}} + st := &State{At: time.Now().UTC(), Kinds: cfg.Kinds, Prune: cfg.Prune, Clients: []Client{}, cfg: cfg, actuals: map[string][]model.Object{}} var refSnap model.Snapshot st.Reference, refSnap = read(ctx, cfg.Reference, cfg.Kinds, adapter.ReadRunning) st.ref = refSnap.Objects diff --git a/internal/store/store.go b/internal/store/store.go index 4148e6a..14feef2 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -283,7 +283,7 @@ func (s *Store) connection(c Cluster, blob []byte) (config.Cluster, error) { if err != nil { return config.Cluster{}, fmt.Errorf("cluster %s: password: %w", c.Name, err) } - cc := config.Cluster{Name: c.Name, URL: c.URL, User: c.User, Password: string(pass), Timeout: c.Timeout, + cc := config.Cluster{Name: c.Name, URL: config.NormalizeURL(c.URL), User: c.User, Password: string(pass), Timeout: c.Timeout, SSHHost: c.SSHHost, SSHPort: c.SSHPort, SSHHostKey: c.SSHHostKey} switch c.TLS { case TLSPin: @@ -307,7 +307,7 @@ func ValidClusterRole(r string) bool { // are forgotten when the address they belong to changes. func (s *Store) SaveCluster(ctx context.Context, oldName string, c Cluster) error { c.Name = strings.TrimSpace(c.Name) - c.URL = strings.TrimRight(strings.TrimSpace(c.URL), "/") + c.URL = config.NormalizeURL(c.URL) if c.Name == "" || c.URL == "" { return errors.New("name and url are required") } @@ -509,6 +509,13 @@ func (s *Store) Settings(ctx context.Context) (Settings, error) { if len(st.Kinds) == 0 { st.Kinds = append(st.Kinds, model.Kinds...) } + // empty maps, not null, for the web UI + if st.Objects == nil { + st.Objects = map[model.Kind]config.Selection{} + } + if st.Policies == nil { + st.Policies = map[model.Kind]map[string]adapter.Policy{} + } return st, nil }