feat(server): add support for loading configuration from a remote home control plane

- Introduced `-home` and `-home-password` flags for specifying home control plane address and authentication.
- Implemented fetching and parsing configuration from the home control plane when `-home` is used.
- Adjusted server configuration handling to bypass local config files when loading from home.
- Ensured compatibility with cloud deploy mode and validation of home configurations.
This commit is contained in:
Luis Pater
2026-05-09 07:14:44 +08:00
parent 1721994111
commit c67096b687
+87 -15
View File
@@ -10,9 +10,11 @@ import (
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"net"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"time" "time"
@@ -21,6 +23,7 @@ import (
"github.com/router-for-me/CLIProxyAPI/v7/internal/buildinfo" "github.com/router-for-me/CLIProxyAPI/v7/internal/buildinfo"
"github.com/router-for-me/CLIProxyAPI/v7/internal/cmd" "github.com/router-for-me/CLIProxyAPI/v7/internal/cmd"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/home"
"github.com/router-for-me/CLIProxyAPI/v7/internal/logging" "github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v7/internal/managementasset" "github.com/router-for-me/CLIProxyAPI/v7/internal/managementasset"
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc" "github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
@@ -70,6 +73,8 @@ func main() {
var vertexImportPrefix string var vertexImportPrefix string
var configPath string var configPath string
var password string var password string
var homeAddr string
var homePassword string
var tuiMode bool var tuiMode bool
var standalone bool var standalone bool
var localModel bool var localModel bool
@@ -88,6 +93,8 @@ func main() {
flag.StringVar(&vertexImport, "vertex-import", "", "Import Vertex service account key JSON file") flag.StringVar(&vertexImport, "vertex-import", "", "Import Vertex service account key JSON file")
flag.StringVar(&vertexImportPrefix, "vertex-import-prefix", "", "Prefix for Vertex model namespacing (use with -vertex-import)") flag.StringVar(&vertexImportPrefix, "vertex-import-prefix", "", "Prefix for Vertex model namespacing (use with -vertex-import)")
flag.StringVar(&password, "password", "", "") flag.StringVar(&password, "password", "", "")
flag.StringVar(&homeAddr, "home", "", "Home control plane address in 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(&tuiMode, "tui", false, "Start with terminal management UI") 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(&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") flag.BoolVar(&localModel, "local-model", false, "Use embedded model catalog only, skip remote model fetching")
@@ -126,6 +133,7 @@ func main() {
var err error var err error
var cfg *config.Config var cfg *config.Config
var isCloudDeploy bool var isCloudDeploy bool
var configLoadedFromHome bool
var ( var (
usePostgresStore bool usePostgresStore bool
pgStoreDSN string pgStoreDSN string
@@ -236,7 +244,67 @@ func main() {
// Determine and load the configuration file. // Determine and load the configuration file.
// Prefer the Postgres store when configured, otherwise fallback to git or local files. // Prefer the Postgres store when configured, otherwise fallback to git or local files.
var configFilePath string var configFilePath string
if usePostgresStore { if strings.TrimSpace(homeAddr) != "" {
configLoadedFromHome = true
trimmedHomePassword := strings.TrimSpace(homePassword)
host, portStr, errSplit := net.SplitHostPort(strings.TrimSpace(homeAddr))
if errSplit != nil {
log.Errorf("invalid -home address %q (expected host:port): %v", homeAddr, errSplit)
return
}
host = strings.TrimSpace(host)
if host == "" {
log.Errorf("invalid -home address %q: host is empty", homeAddr)
return
}
port, errPort := strconv.Atoi(strings.TrimSpace(portStr))
if errPort != nil || port <= 0 {
log.Errorf("invalid -home address %q: invalid port %q", homeAddr, portStr)
return
}
homeCfg := config.HomeConfig{
Enabled: true,
Host: host,
Port: port,
Password: trimmedHomePassword,
}
homeClient := home.New(homeCfg)
defer homeClient.Close()
ctxHome, cancelHome := context.WithTimeout(context.Background(), 30*time.Second)
raw, errGetConfig := homeClient.GetConfig(ctxHome)
cancelHome()
if errGetConfig != nil {
log.Errorf("failed to fetch config from home: %v", errGetConfig)
return
}
parsed, errParseConfig := config.ParseConfigBytes(raw)
if errParseConfig != nil {
log.Errorf("failed to parse config payload from home: %v", errParseConfig)
return
}
if parsed == nil {
parsed = &config.Config{}
}
parsed.Home = homeCfg
parsed.Port = 8317 // Default to 8317 for home mode, can be overridden by home config
cfg = parsed
// Keep a non-empty config path for downstream components (log paths, management assets, etc),
// but do not require the file to exist when loading config from home.
if strings.TrimSpace(configPath) != "" {
configFilePath = configPath
} else {
configFilePath = filepath.Join(wd, "config.yaml")
}
// Local stores are intentionally disabled when config is loaded from home.
usePostgresStore = false
useObjectStore = false
useGitStore = false
} else if usePostgresStore {
if pgStoreLocalPath == "" { if pgStoreLocalPath == "" {
pgStoreLocalPath = wd pgStoreLocalPath = wd
} }
@@ -400,21 +468,25 @@ func main() {
// In cloud deploy mode, check if we have a valid configuration // In cloud deploy mode, check if we have a valid configuration
var configFileExists bool var configFileExists bool
if isCloudDeploy { if isCloudDeploy {
if info, errStat := os.Stat(configFilePath); errStat != nil { if configLoadedFromHome && cfg != nil {
// Don't mislead: API server will not start until configuration is provided. configFileExists = cfg.Port != 0
log.Info("Cloud deploy mode: No configuration file detected; standing by for configuration")
configFileExists = false
} else if info.IsDir() {
log.Info("Cloud deploy mode: Config path is a directory; standing by for configuration")
configFileExists = false
} else if cfg.Port == 0 {
// LoadConfigOptional returns empty config when file is empty or invalid.
// Config file exists but is empty or invalid; treat as missing config
log.Info("Cloud deploy mode: Configuration file is empty or invalid; standing by for valid configuration")
configFileExists = false
} else { } else {
log.Info("Cloud deploy mode: Configuration file detected; starting service") if info, errStat := os.Stat(configFilePath); errStat != nil {
configFileExists = true // Don't mislead: API server will not start until configuration is provided.
log.Info("Cloud deploy mode: No configuration file detected; standing by for configuration")
configFileExists = false
} else if info.IsDir() {
log.Info("Cloud deploy mode: Config path is a directory; standing by for configuration")
configFileExists = false
} else if cfg.Port == 0 {
// LoadConfigOptional returns empty config when file is empty or invalid.
// Config file exists but is empty or invalid; treat as missing config
log.Info("Cloud deploy mode: Configuration file is empty or invalid; standing by for valid configuration")
configFileExists = false
} else {
log.Info("Cloud deploy mode: Configuration file detected; starting service")
configFileExists = true
}
} }
} }
redisqueue.SetUsageStatisticsEnabled(cfg.UsageStatisticsEnabled) redisqueue.SetUsageStatisticsEnabled(cfg.UsageStatisticsEnabled)