Merge pull request #104 from router-for-me/cloud
Browse files- cmd/server/main.go +32 -2
- docker-compose.yml +2 -0
- internal/cmd/run.go +14 -0
- internal/config/config.go +14 -0
cmd/server/main.go
CHANGED
|
@@ -96,6 +96,14 @@ func main() {
|
|
| 96 |
var err error
|
| 97 |
var cfg *config.Config
|
| 98 |
var wd string
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
// Determine and load the configuration file.
|
| 101 |
// If a config path is provided via flags, it is used directly.
|
|
@@ -103,18 +111,34 @@ func main() {
|
|
| 103 |
var configFilePath string
|
| 104 |
if configPath != "" {
|
| 105 |
configFilePath = configPath
|
| 106 |
-
cfg, err = config.
|
| 107 |
} else {
|
| 108 |
wd, err = os.Getwd()
|
| 109 |
if err != nil {
|
| 110 |
log.Fatalf("failed to get working directory: %v", err)
|
| 111 |
}
|
| 112 |
configFilePath = filepath.Join(wd, "config.yaml")
|
| 113 |
-
cfg, err = config.
|
| 114 |
}
|
| 115 |
if err != nil {
|
| 116 |
log.Fatalf("failed to load config: %v", err)
|
| 117 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
usage.SetStatisticsEnabled(cfg.UsageStatisticsEnabled)
|
| 119 |
|
| 120 |
if err = logging.ConfigureLogOutput(cfg.LoggingToFile); err != nil {
|
|
@@ -161,6 +185,12 @@ func main() {
|
|
| 161 |
} else if geminiWebAuth {
|
| 162 |
cmd.DoGeminiWebAuth(cfg)
|
| 163 |
} else {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
// Start the main proxy service
|
| 165 |
cmd.StartService(cfg, configFilePath, password)
|
| 166 |
}
|
|
|
|
| 96 |
var err error
|
| 97 |
var cfg *config.Config
|
| 98 |
var wd string
|
| 99 |
+
var isCloudDeploy bool
|
| 100 |
+
|
| 101 |
+
// Check for cloud deploy mode only on first execution
|
| 102 |
+
// Read env var name in uppercase: DEPLOY
|
| 103 |
+
deployEnv := os.Getenv("DEPLOY")
|
| 104 |
+
if deployEnv == "cloud" {
|
| 105 |
+
isCloudDeploy = true
|
| 106 |
+
}
|
| 107 |
|
| 108 |
// Determine and load the configuration file.
|
| 109 |
// If a config path is provided via flags, it is used directly.
|
|
|
|
| 111 |
var configFilePath string
|
| 112 |
if configPath != "" {
|
| 113 |
configFilePath = configPath
|
| 114 |
+
cfg, err = config.LoadConfigOptional(configPath, isCloudDeploy)
|
| 115 |
} else {
|
| 116 |
wd, err = os.Getwd()
|
| 117 |
if err != nil {
|
| 118 |
log.Fatalf("failed to get working directory: %v", err)
|
| 119 |
}
|
| 120 |
configFilePath = filepath.Join(wd, "config.yaml")
|
| 121 |
+
cfg, err = config.LoadConfigOptional(configFilePath, isCloudDeploy)
|
| 122 |
}
|
| 123 |
if err != nil {
|
| 124 |
log.Fatalf("failed to load config: %v", err)
|
| 125 |
}
|
| 126 |
+
|
| 127 |
+
// Log if we're running without a config file in cloud deploy mode
|
| 128 |
+
var configFileExists bool
|
| 129 |
+
if isCloudDeploy {
|
| 130 |
+
if info, errStat := os.Stat(configFilePath); errStat != nil {
|
| 131 |
+
// Don't mislead: API server will not start until configuration is provided.
|
| 132 |
+
log.Info("Cloud deploy mode: No configuration file detected; standing by for configuration (API server not started)")
|
| 133 |
+
configFileExists = false
|
| 134 |
+
} else if info.IsDir() {
|
| 135 |
+
log.Info("Cloud deploy mode: Config path is a directory; standing by for configuration (API server not started)")
|
| 136 |
+
configFileExists = false
|
| 137 |
+
} else {
|
| 138 |
+
log.Info("Cloud deploy mode: Configuration file detected; starting service")
|
| 139 |
+
configFileExists = true
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
usage.SetStatisticsEnabled(cfg.UsageStatisticsEnabled)
|
| 143 |
|
| 144 |
if err = logging.ConfigureLogOutput(cfg.LoggingToFile); err != nil {
|
|
|
|
| 185 |
} else if geminiWebAuth {
|
| 186 |
cmd.DoGeminiWebAuth(cfg)
|
| 187 |
} else {
|
| 188 |
+
// In cloud deploy mode without config file, just wait for shutdown signals
|
| 189 |
+
if isCloudDeploy && !configFileExists {
|
| 190 |
+
// No config file available, just wait for shutdown
|
| 191 |
+
cmd.WaitForCloudDeploy()
|
| 192 |
+
return
|
| 193 |
+
}
|
| 194 |
// Start the main proxy service
|
| 195 |
cmd.StartService(cfg, configFilePath, password)
|
| 196 |
}
|
docker-compose.yml
CHANGED
|
@@ -10,6 +10,8 @@ services:
|
|
| 10 |
COMMIT: ${COMMIT:-none}
|
| 11 |
BUILD_DATE: ${BUILD_DATE:-unknown}
|
| 12 |
container_name: cli-proxy-api
|
|
|
|
|
|
|
| 13 |
ports:
|
| 14 |
- "8317:8317"
|
| 15 |
- "8085:8085"
|
|
|
|
| 10 |
COMMIT: ${COMMIT:-none}
|
| 11 |
BUILD_DATE: ${BUILD_DATE:-unknown}
|
| 12 |
container_name: cli-proxy-api
|
| 13 |
+
environment:
|
| 14 |
+
DEPLOY: ${DEPLOY:-}
|
| 15 |
ports:
|
| 16 |
- "8317:8317"
|
| 17 |
- "8085:8085"
|
internal/cmd/run.go
CHANGED
|
@@ -53,3 +53,17 @@ func StartService(cfg *config.Config, configPath string, localPassword string) {
|
|
| 53 |
log.Fatalf("proxy service exited with error: %v", err)
|
| 54 |
}
|
| 55 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
log.Fatalf("proxy service exited with error: %v", err)
|
| 54 |
}
|
| 55 |
}
|
| 56 |
+
|
| 57 |
+
// WaitForCloudDeploy waits indefinitely for shutdown signals in cloud deploy mode
|
| 58 |
+
// when no configuration file is available.
|
| 59 |
+
func WaitForCloudDeploy() {
|
| 60 |
+
// Clarify that we are intentionally idle for configuration and not running the API server.
|
| 61 |
+
log.Info("Cloud deploy mode: No config found; standing by for configuration. API server is not started. Press Ctrl+C to exit.")
|
| 62 |
+
|
| 63 |
+
ctxSignal, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
| 64 |
+
defer cancel()
|
| 65 |
+
|
| 66 |
+
// Block until shutdown signal is received
|
| 67 |
+
<-ctxSignal.Done()
|
| 68 |
+
log.Info("Cloud deploy mode: Shutdown signal received; exiting")
|
| 69 |
+
}
|
internal/config/config.go
CHANGED
|
@@ -5,8 +5,10 @@
|
|
| 5 |
package config
|
| 6 |
|
| 7 |
import (
|
|
|
|
| 8 |
"fmt"
|
| 9 |
"os"
|
|
|
|
| 10 |
|
| 11 |
"github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
|
| 12 |
"golang.org/x/crypto/bcrypt"
|
|
@@ -187,9 +189,21 @@ type OpenAICompatibilityModel struct {
|
|
| 187 |
// - *Config: The loaded configuration
|
| 188 |
// - error: An error if the configuration could not be loaded
|
| 189 |
func LoadConfig(configFile string) (*Config, error) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
// Read the entire configuration file into memory.
|
| 191 |
data, err := os.ReadFile(configFile)
|
| 192 |
if err != nil {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
return nil, fmt.Errorf("failed to read config file: %w", err)
|
| 194 |
}
|
| 195 |
|
|
|
|
| 5 |
package config
|
| 6 |
|
| 7 |
import (
|
| 8 |
+
"errors"
|
| 9 |
"fmt"
|
| 10 |
"os"
|
| 11 |
+
"syscall"
|
| 12 |
|
| 13 |
"github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
|
| 14 |
"golang.org/x/crypto/bcrypt"
|
|
|
|
| 189 |
// - *Config: The loaded configuration
|
| 190 |
// - error: An error if the configuration could not be loaded
|
| 191 |
func LoadConfig(configFile string) (*Config, error) {
|
| 192 |
+
return LoadConfigOptional(configFile, false)
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
// LoadConfigOptional reads YAML from configFile.
|
| 196 |
+
// If optional is true and the file is missing, it returns an empty Config.
|
| 197 |
+
func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
| 198 |
// Read the entire configuration file into memory.
|
| 199 |
data, err := os.ReadFile(configFile)
|
| 200 |
if err != nil {
|
| 201 |
+
if optional {
|
| 202 |
+
if os.IsNotExist(err) || errors.Is(err, syscall.EISDIR) {
|
| 203 |
+
// Missing and optional: return empty config (cloud deploy standby).
|
| 204 |
+
return &Config{}, nil
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
return nil, fmt.Errorf("failed to read config file: %w", err)
|
| 208 |
}
|
| 209 |
|