| package config |
|
|
| import ( |
| "encoding/json" |
| "fmt" |
| "io/ioutil" |
| "log" |
| "os" |
| "path/filepath" |
| "time" |
| ) |
|
|
| |
| type ConfigDiscovery struct { |
| searchPaths []string |
| manager *Manager |
| } |
|
|
| |
| func NewConfigDiscovery(manager *Manager) *ConfigDiscovery { |
| return &ConfigDiscovery{ |
| manager: manager, |
| searchPaths: []string{ |
| |
| "config.json", |
| "jetbrains-ai-proxy.json", |
| ".jetbrains-ai-proxy.json", |
|
|
| |
| "config/config.json", |
| "config/jetbrains-ai-proxy.json", |
| "configs/config.json", |
| "configs/jetbrains-ai-proxy.json", |
|
|
| |
| ".config/config.json", |
| ".config/jetbrains-ai-proxy.json", |
|
|
| |
| os.ExpandEnv("$HOME/.config/jetbrains-ai-proxy/config.json"), |
| os.ExpandEnv("$HOME/.jetbrains-ai-proxy/config.json"), |
| os.ExpandEnv("$HOME/.jetbrains-ai-proxy.json"), |
|
|
| |
| "/etc/jetbrains-ai-proxy/config.json", |
| "/usr/local/etc/jetbrains-ai-proxy/config.json", |
| }, |
| } |
| } |
|
|
| |
| func (cd *ConfigDiscovery) DiscoverAndLoad() error { |
| log.Println("Starting configuration discovery...") |
|
|
| |
| if configPath := os.Getenv("CONFIG_FILE"); configPath != "" { |
| if err := cd.loadConfigFile(configPath); err != nil { |
| log.Printf("Failed to load config from CONFIG_FILE=%s: %v", configPath, err) |
| } else { |
| log.Printf("Successfully loaded config from CONFIG_FILE: %s", configPath) |
| return nil |
| } |
| } |
|
|
| |
| for _, path := range cd.searchPaths { |
| if cd.fileExists(path) { |
| if err := cd.loadConfigFile(path); err != nil { |
| log.Printf("Failed to load config from %s: %v", path, err) |
| continue |
| } |
| log.Printf("Successfully loaded config from: %s", path) |
| return nil |
| } |
| } |
|
|
| |
| if cd.fileExists(".env") { |
| log.Println("Found .env file, loading environment variables...") |
| return nil |
| } |
|
|
| |
| log.Println("No configuration file found, generating example config...") |
| return cd.generateDefaultConfig() |
| } |
|
|
| |
| func (cd *ConfigDiscovery) loadConfigFile(path string) error { |
| data, err := ioutil.ReadFile(path) |
| if err != nil { |
| return fmt.Errorf("failed to read config file: %v", err) |
| } |
|
|
| var config Config |
| if err := json.Unmarshal(data, &config); err != nil { |
| return fmt.Errorf("failed to parse config file: %v", err) |
| } |
|
|
| |
| if err := cd.validateLoadedConfig(&config); err != nil { |
| return fmt.Errorf("invalid config: %v", err) |
| } |
|
|
| |
| cd.manager.mutex.Lock() |
| cd.manager.mergeConfig(&config) |
| cd.manager.configPath = path |
| cd.manager.mutex.Unlock() |
|
|
| return nil |
| } |
|
|
| |
| func (cd *ConfigDiscovery) validateLoadedConfig(config *Config) error { |
| if len(config.JetbrainsTokens) == 0 { |
| return fmt.Errorf("no JWT tokens found in config") |
| } |
|
|
| |
| for i, tokenConfig := range config.JetbrainsTokens { |
| if tokenConfig.Token == "" { |
| return fmt.Errorf("JWT token %d is empty", i+1) |
| } |
| if len(tokenConfig.Token) < 10 { |
| return fmt.Errorf("JWT token %d appears to be invalid (too short)", i+1) |
| } |
| } |
|
|
| if config.BearerToken == "" { |
| log.Println("Warning: No bearer token found in config file") |
| } |
|
|
| return nil |
| } |
|
|
| |
| func (cd *ConfigDiscovery) generateDefaultConfig() error { |
| configDir := "config" |
| configPath := filepath.Join(configDir, "config.json") |
|
|
| |
| if err := os.MkdirAll(configDir, 0755); err != nil { |
| return fmt.Errorf("failed to create config directory: %v", err) |
| } |
|
|
| |
| if err := cd.manager.GenerateExampleConfig(configPath); err != nil { |
| return fmt.Errorf("failed to generate example config: %v", err) |
| } |
|
|
| |
| envPath := ".env.example" |
| if err := cd.generateEnvExample(envPath); err != nil { |
| log.Printf("Warning: Failed to generate .env example: %v", err) |
| } |
|
|
| log.Printf("Generated example configuration files:") |
| log.Printf(" - %s (JSON format)", configPath) |
| log.Printf(" - %s (Environment variables)", envPath) |
| log.Printf("Please edit these files with your actual JWT tokens and restart the application.") |
|
|
| return fmt.Errorf("no valid configuration found, example files generated") |
| } |
|
|
| |
| func (cd *ConfigDiscovery) generateEnvExample(path string) error { |
| envContent := `# JetBrains AI Proxy Configuration |
| # Copy this file to .env and fill in your actual values |
| |
| # Multiple JWT tokens (comma-separated) |
| JWT_TOKENS=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...,eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... |
| |
| # Or single JWT token (for backward compatibility) |
| # JWT_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... |
| |
| # Bearer token for API authentication |
| BEARER_TOKEN=your_bearer_token_here |
| |
| # Load balancing strategy: round_robin or random |
| LOAD_BALANCE_STRATEGY=round_robin |
| |
| # Server configuration |
| SERVER_HOST=0.0.0.0 |
| SERVER_PORT=8080 |
| |
| # Alternative: specify config file path |
| # CONFIG_FILE=config/config.json |
| ` |
|
|
| return ioutil.WriteFile(path, []byte(envContent), 0644) |
| } |
|
|
| |
| func (cd *ConfigDiscovery) fileExists(path string) bool { |
| _, err := os.Stat(path) |
| return err == nil |
| } |
|
|
| |
| func (cd *ConfigDiscovery) WatchConfig() { |
| if cd.manager.configPath == "" { |
| return |
| } |
|
|
| go func() { |
| var lastModTime time.Time |
|
|
| |
| if stat, err := os.Stat(cd.manager.configPath); err == nil { |
| lastModTime = stat.ModTime() |
| } |
|
|
| ticker := time.NewTicker(5 * time.Second) |
| defer ticker.Stop() |
|
|
| for range ticker.C { |
| stat, err := os.Stat(cd.manager.configPath) |
| if err != nil { |
| continue |
| } |
|
|
| if stat.ModTime().After(lastModTime) { |
| log.Printf("Config file changed, reloading: %s", cd.manager.configPath) |
| if err := cd.loadConfigFile(cd.manager.configPath); err != nil { |
| log.Printf("Failed to reload config: %v", err) |
| } else { |
| log.Println("Config reloaded successfully") |
| } |
| lastModTime = stat.ModTime() |
| } |
| } |
| }() |
| } |
|
|
| |
| func (cd *ConfigDiscovery) ListAvailableConfigs() []string { |
| var available []string |
|
|
| for _, path := range cd.searchPaths { |
| if cd.fileExists(path) { |
| available = append(available, path) |
| } |
| } |
|
|
| return available |
| } |
|
|
| |
| func (cd *ConfigDiscovery) ValidateConfigFile(path string) error { |
| if !cd.fileExists(path) { |
| return fmt.Errorf("config file does not exist: %s", path) |
| } |
|
|
| data, err := ioutil.ReadFile(path) |
| if err != nil { |
| return fmt.Errorf("failed to read config file: %v", err) |
| } |
|
|
| var config Config |
| if err := json.Unmarshal(data, &config); err != nil { |
| return fmt.Errorf("invalid JSON format: %v", err) |
| } |
|
|
| return cd.validateLoadedConfig(&config) |
| } |
|
|
| |
| func (cd *ConfigDiscovery) GetConfigSummary() map[string]interface{} { |
| config := cd.manager.GetConfig() |
|
|
| |
| tokenSummary := make([]map[string]interface{}, len(config.JetbrainsTokens)) |
| for i, token := range config.JetbrainsTokens { |
| tokenSummary[i] = map[string]interface{}{ |
| "name": token.Name, |
| "description": token.Description, |
| "priority": token.Priority, |
| "token_preview": token.Token[:min(len(token.Token), 20)] + "...", |
| } |
| } |
|
|
| return map[string]interface{}{ |
| "jwt_tokens_count": len(config.JetbrainsTokens), |
| "jwt_tokens": tokenSummary, |
| "bearer_token_set": config.BearerToken != "", |
| "load_balance_strategy": config.LoadBalanceStrategy, |
| "health_check_interval": config.HealthCheckInterval.String(), |
| "server_host": config.ServerHost, |
| "server_port": config.ServerPort, |
| "config_file": cd.manager.configPath, |
| } |
| } |
|
|