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:
+87
-15
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user