diff --git a/cmd/server/main.go b/cmd/server/main.go index 44a314aee..481103809 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -10,9 +10,11 @@ import ( "fmt" "io" "io/fs" + "net" "net/url" "os" "path/filepath" + "strconv" "strings" "time" @@ -21,6 +23,7 @@ import ( "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/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/managementasset" "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" @@ -70,6 +73,8 @@ func main() { var vertexImportPrefix string var configPath string var password string + var homeAddr string + var homePassword string var tuiMode bool var standalone bool var localModel bool @@ -88,6 +93,8 @@ func main() { 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(&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(&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") @@ -126,6 +133,7 @@ func main() { var err error var cfg *config.Config var isCloudDeploy bool + var configLoadedFromHome bool var ( usePostgresStore bool pgStoreDSN string @@ -236,7 +244,67 @@ func main() { // Determine and load the configuration file. // Prefer the Postgres store when configured, otherwise fallback to git or local files. 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 == "" { pgStoreLocalPath = wd } @@ -400,21 +468,25 @@ func main() { // In cloud deploy mode, check if we have a valid configuration var configFileExists bool if isCloudDeploy { - if info, errStat := os.Stat(configFilePath); errStat != nil { - // 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 + if configLoadedFromHome && cfg != nil { + configFileExists = cfg.Port != 0 } else { - log.Info("Cloud deploy mode: Configuration file detected; starting service") - configFileExists = true + if info, errStat := os.Stat(configFilePath); errStat != nil { + // 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)