From 644d5ea618fd4bdc57bf087622ecd1b6f6f08b39 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Sat, 16 May 2026 20:25:29 +0800 Subject: [PATCH] feat(home): add support for disabling cluster discovery in Redis configuration --- cmd/server/home_flag.go | 3 ++- cmd/server/home_flag_test.go | 11 ++++++++++ cmd/server/main.go | 5 +++++ config.example.yaml | 3 +++ internal/config/home.go | 11 +++++----- internal/config/home_test.go | 4 ++++ internal/home/client.go | 23 ++++++++++++++++++++ internal/home/client_test.go | 42 ++++++++++++++++++++++++++++++++++++ 8 files changed, 96 insertions(+), 6 deletions(-) diff --git a/cmd/server/home_flag.go b/cmd/server/home_flag.go index 2d79ef833..ade94fbf3 100644 --- a/cmd/server/home_flag.go +++ b/cmd/server/home_flag.go @@ -76,10 +76,11 @@ func parseHomeURLConfig(rawAddr string, password string) (config.HomeConfig, err Port: port, Password: password, } + query := parsed.Query() + homeCfg.DisableClusterDiscovery = parseHomeBoolQuery(query, "disable-cluster-discovery", "disable_cluster_discovery") if scheme == "rediss" { homeCfg.TLS.Enable = true - query := parsed.Query() homeCfg.TLS.ServerName = strings.TrimSpace(firstHomeQueryValue(query, "server-name", "server_name")) homeCfg.TLS.InsecureSkipVerify = parseHomeBoolQuery(query, "insecure-skip-verify", "insecure_skip_verify", "skip_verify") homeCfg.TLS.CACert = strings.TrimSpace(firstHomeQueryValue(query, "ca-cert", "ca_cert")) diff --git a/cmd/server/home_flag_test.go b/cmd/server/home_flag_test.go index 9947f9402..e98d85f17 100644 --- a/cmd/server/home_flag_test.go +++ b/cmd/server/home_flag_test.go @@ -64,3 +64,14 @@ func TestParseHomeFlagConfigPasswordFlagOverridesURLPassword(t *testing.T) { t.Fatalf("Password = %q, want flag-secret", cfg.Password) } } + +func TestParseHomeFlagConfigDisableClusterDiscovery(t *testing.T) { + cfg, err := parseHomeFlagConfig("redis://home.example.com:8327?disable-cluster-discovery=true", "") + if err != nil { + t.Fatalf("parseHomeFlagConfig() error = %v", err) + } + + if !cfg.DisableClusterDiscovery { + t.Fatal("DisableClusterDiscovery = false, want true") + } +} diff --git a/cmd/server/main.go b/cmd/server/main.go index 70f7c9531..7da5b087a 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -73,6 +73,7 @@ func main() { var password string var homeAddr string var homePassword string + var homeDisableClusterDiscovery bool var tuiMode bool var standalone bool var localModel bool @@ -93,6 +94,7 @@ func main() { flag.StringVar(&password, "password", "", "") flag.StringVar(&homeAddr, "home", "", "Home control plane address in host:port, redis://host:port, or rediss://host:port format (loads config from home and skips local config file)") flag.StringVar(&homePassword, "home-password", "", "Home control plane password (Redis AUTH)") + flag.BoolVar(&homeDisableClusterDiscovery, "home-disable-cluster-discovery", false, "Disable Home CLUSTER NODES discovery and keep using the configured -home address") flag.BoolVar(&tuiMode, "tui", false, "Start with terminal management UI") flag.BoolVar(&standalone, "standalone", false, "In TUI mode, start an embedded local server") flag.BoolVar(&localModel, "local-model", false, "Use embedded model catalog only, skip remote model fetching") @@ -250,6 +252,9 @@ func main() { log.Errorf("invalid -home address %q: %v", homeAddr, errHomeCfg) return } + if homeDisableClusterDiscovery { + homeCfg.DisableClusterDiscovery = true + } homeClient := home.New(homeCfg) defer homeClient.Close() diff --git a/config.example.yaml b/config.example.yaml index d9a4fc047..d49c378cb 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -17,6 +17,9 @@ home: host: "127.0.0.1" port: 6379 password: "" + # Keep CPA pinned to the configured home address instead of switching to CLUSTER NODES entries. + # Useful when Home is behind NAT, Docker networking, or a reverse proxy. + disable-cluster-discovery: false # Optional TLS for the outbound Redis connection to the home control plane. # Enable this when connecting through rediss:// or an SSL stream proxy. tls: diff --git a/internal/config/home.go b/internal/config/home.go index ffcdd4b7a..8e7945b40 100644 --- a/internal/config/home.go +++ b/internal/config/home.go @@ -2,11 +2,12 @@ package config // HomeConfig configures the optional "home" control plane integration over Redis protocol. type HomeConfig struct { - Enabled bool `yaml:"enabled" json:"enabled"` - Host string `yaml:"host" json:"-"` - Port int `yaml:"port" json:"-"` - Password string `yaml:"password" json:"-"` - TLS HomeTLSConfig `yaml:"tls" json:"-"` + Enabled bool `yaml:"enabled" json:"enabled"` + Host string `yaml:"host" json:"-"` + Port int `yaml:"port" json:"-"` + Password string `yaml:"password" json:"-"` + DisableClusterDiscovery bool `yaml:"disable-cluster-discovery" json:"-"` + TLS HomeTLSConfig `yaml:"tls" json:"-"` } // HomeTLSConfig configures client-side TLS for the home Redis connection. diff --git a/internal/config/home_test.go b/internal/config/home_test.go index 2a5d64fb3..ac26d2cbf 100644 --- a/internal/config/home_test.go +++ b/internal/config/home_test.go @@ -9,6 +9,7 @@ home: host: home.example.com port: 444 password: secret + disable-cluster-discovery: true tls: enable: true server-name: home.example.com @@ -31,6 +32,9 @@ home: if cfg.Home.Password != "secret" { t.Fatalf("Home.Password = %q, want secret", cfg.Home.Password) } + if !cfg.Home.DisableClusterDiscovery { + t.Fatal("Home.DisableClusterDiscovery = false, want true") + } if !cfg.Home.TLS.Enable { t.Fatal("Home.TLS.Enable = false, want true") } diff --git a/internal/home/client.go b/internal/home/client.go index 5d0c96cea..3edd3135a 100644 --- a/internal/home/client.go +++ b/internal/home/client.go @@ -265,7 +265,23 @@ func (c *Client) Ping(ctx context.Context) error { return cmd.Ping(ctx).Err() } +func (c *Client) clusterDiscoveryEnabled() bool { + if c == nil { + return false + } + c.mu.Lock() + defer c.mu.Unlock() + return c.clusterDiscoveryEnabledLocked() +} + +func (c *Client) clusterDiscoveryEnabledLocked() bool { + return !c.homeCfg.DisableClusterDiscovery +} + func (c *Client) refreshBestClusterNode(ctx context.Context) { + if !c.clusterDiscoveryEnabled() { + return + } switched, errRefresh := c.refreshClusterNodes(ctx) if errRefresh != nil { log.Debugf("home cluster nodes unavailable: %v", errRefresh) @@ -279,6 +295,9 @@ func (c *Client) refreshBestClusterNode(ctx context.Context) { } func (c *Client) refreshClusterNodes(ctx context.Context) (bool, error) { + if !c.clusterDiscoveryEnabled() { + return false, nil + } if ctx == nil { ctx = context.Background() } @@ -353,6 +372,10 @@ func (c *Client) failoverAfterReconnectFailure() (bool, string) { c.mu.Lock() defer c.mu.Unlock() + if !c.clusterDiscoveryEnabledLocked() { + c.reconnectFailures = 0 + return false, "" + } c.reconnectFailures++ if c.reconnectFailures < homeReconnectFailoverThreshold { return false, "" diff --git a/internal/home/client_test.go b/internal/home/client_test.go index 65148f676..b3a1ae583 100644 --- a/internal/home/client_test.go +++ b/internal/home/client_test.go @@ -1,6 +1,7 @@ package home import ( + "context" "crypto/tls" "encoding/json" "net/http" @@ -115,3 +116,44 @@ func TestRedisOptionsHomeTLSEnabledUsesExplicitServerName(t *testing.T) { t.Fatal("InsecureSkipVerify = false, want true") } } + +func TestRefreshClusterNodesDisabledSkipsRedisCommand(t *testing.T) { + client := New(config.HomeConfig{ + Enabled: true, + Host: "127.0.0.1", + Port: 1, + DisableClusterDiscovery: true, + }) + + switched, err := client.refreshClusterNodes(context.Background()) + if err != nil { + t.Fatalf("refreshClusterNodes() error = %v", err) + } + if switched { + t.Fatal("refreshClusterNodes() switched = true, want false") + } + if client.cmd != nil || client.sub != nil { + t.Fatalf("redis clients were initialized when cluster discovery was disabled") + } +} + +func TestFailoverAfterReconnectFailureDisabledDoesNotSwitchToClusterNode(t *testing.T) { + client := New(config.HomeConfig{ + Enabled: true, + Host: "seed.example.com", + Port: 8327, + DisableClusterDiscovery: true, + }) + client.mu.Lock() + client.clusterNodes = []clusterNode{{IP: "other.example.com", Port: 8327}} + client.reconnectFailures = homeReconnectFailoverThreshold - 1 + client.mu.Unlock() + + switched, addr := client.failoverAfterReconnectFailure() + if switched { + t.Fatalf("failoverAfterReconnectFailure() switched to %s, want no switch", addr) + } + if got, _ := client.addr(); got != "seed.example.com:8327" { + t.Fatalf("addr() = %q, want seed.example.com:8327", got) + } +}