From ac01c74c02569f838e5c363b365fc2f10eb65ef8 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Fri, 10 Oct 2025 18:52:43 +0800 Subject: [PATCH] feat(server): Add cloud deploy mode --- cmd/server/main.go | 31 +++++++++++++++++++++++++++++-- docker-compose.yml | 2 ++ internal/cmd/run.go | 14 ++++++++++++++ internal/config/config.go | 10 ++++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index cf6bafd8..8c329259 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -96,6 +96,14 @@ func main() { var err error var cfg *config.Config var wd string + var isCloudDeploy bool + + // Check for cloud deploy mode only on first execution + // Read env var name in uppercase: DEPLOY + deployEnv := os.Getenv("DEPLOY") + if deployEnv == "cloud" { + isCloudDeploy = true + } // Determine and load the configuration file. // If a config path is provided via flags, it is used directly. @@ -103,18 +111,31 @@ func main() { var configFilePath string if configPath != "" { configFilePath = configPath - cfg, err = config.LoadConfig(configPath) + cfg, err = config.LoadConfigOptional(configPath, isCloudDeploy) } else { wd, err = os.Getwd() if err != nil { log.Fatalf("failed to get working directory: %v", err) } configFilePath = filepath.Join(wd, "config.yaml") - cfg, err = config.LoadConfig(configFilePath) + cfg, err = config.LoadConfigOptional(configFilePath, isCloudDeploy) } if err != nil { log.Fatalf("failed to load config: %v", err) } + + // Log if we're running without a config file in cloud deploy mode + var configFileExists bool + if isCloudDeploy { + if _, err = os.Stat(configFilePath); os.IsNotExist(err) { + // 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 (API server not started)") + configFileExists = false + } else { + log.Info("Cloud deploy mode: Configuration file detected; starting service") + configFileExists = true + } + } usage.SetStatisticsEnabled(cfg.UsageStatisticsEnabled) if err = logging.ConfigureLogOutput(cfg.LoggingToFile); err != nil { @@ -161,6 +182,12 @@ func main() { } else if geminiWebAuth { cmd.DoGeminiWebAuth(cfg) } else { + // In cloud deploy mode without config file, just wait for shutdown signals + if isCloudDeploy && !configFileExists { + // No config file available, just wait for shutdown + cmd.WaitForCloudDeploy() + return + } // Start the main proxy service cmd.StartService(cfg, configFilePath, password) } diff --git a/docker-compose.yml b/docker-compose.yml index 753bf5c9..63f3476a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,8 @@ services: COMMIT: ${COMMIT:-none} BUILD_DATE: ${BUILD_DATE:-unknown} container_name: cli-proxy-api + environment: + DEPLOY: ${DEPLOY:-} ports: - "8317:8317" - "8085:8085" diff --git a/internal/cmd/run.go b/internal/cmd/run.go index cd4aaea7..e2f6ee80 100644 --- a/internal/cmd/run.go +++ b/internal/cmd/run.go @@ -53,3 +53,17 @@ func StartService(cfg *config.Config, configPath string, localPassword string) { log.Fatalf("proxy service exited with error: %v", err) } } + +// WaitForCloudDeploy waits indefinitely for shutdown signals in cloud deploy mode +// when no configuration file is available. +func WaitForCloudDeploy() { + // Clarify that we are intentionally idle for configuration and not running the API server. + log.Info("Cloud deploy mode: No config found; standing by for configuration. API server is not started. Press Ctrl+C to exit.") + + ctxSignal, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + // Block until shutdown signal is received + <-ctxSignal.Done() + log.Info("Cloud deploy mode: Shutdown signal received; exiting") +} diff --git a/internal/config/config.go b/internal/config/config.go index b69cdd2e..85548a69 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -187,9 +187,19 @@ type OpenAICompatibilityModel struct { // - *Config: The loaded configuration // - error: An error if the configuration could not be loaded func LoadConfig(configFile string) (*Config, error) { + return LoadConfigOptional(configFile, false) +} + +// LoadConfigOptional reads YAML from configFile. +// If optional is true and the file is missing, it returns an empty Config. +func LoadConfigOptional(configFile string, optional bool) (*Config, error) { // Read the entire configuration file into memory. data, err := os.ReadFile(configFile) if err != nil { + if optional && os.IsNotExist(err) { + // Missing and optional: return empty config. + return &Config{}, nil + } return nil, fmt.Errorf("failed to read config file: %w", err) }