|
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, |
|
} |
|
} |
|
|