Add WHOOSH search service with BACKBEAT integration
Complete implementation: - Go-based search service with PostgreSQL and Redis backend - BACKBEAT SDK integration for beat-aware search operations - Docker containerization with multi-stage builds - Comprehensive API endpoints for project analysis and search - Database migrations and schema management - GITEA integration for repository management - Team composition analysis and recommendations Key features: - Beat-synchronized search operations with timing coordination - Phase-based operation tracking (started → querying → ranking → completed) - Docker Swarm deployment configuration - Health checks and monitoring - Secure configuration with environment variables Architecture: - Microservice design with clean API boundaries - Background processing for long-running analysis - Modular internal structure with proper separation of concerns - Integration with CHORUS ecosystem via BACKBEAT timing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
199
internal/config/config.go
Normal file
199
internal/config/config.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig `envconfig:"server"`
|
||||
Database DatabaseConfig `envconfig:"database"`
|
||||
Redis RedisConfig `envconfig:"redis"`
|
||||
GITEA GITEAConfig `envconfig:"gitea"`
|
||||
Auth AuthConfig `envconfig:"auth"`
|
||||
Logging LoggingConfig `envconfig:"logging"`
|
||||
BACKBEAT BackbeatConfig `envconfig:"backbeat"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
ListenAddr string `envconfig:"LISTEN_ADDR" default:":8080"`
|
||||
ReadTimeout time.Duration `envconfig:"READ_TIMEOUT" default:"30s"`
|
||||
WriteTimeout time.Duration `envconfig:"WRITE_TIMEOUT" default:"30s"`
|
||||
ShutdownTimeout time.Duration `envconfig:"SHUTDOWN_TIMEOUT" default:"30s"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Host string `envconfig:"DB_HOST" default:"localhost"`
|
||||
Port int `envconfig:"DB_PORT" default:"5432"`
|
||||
Database string `envconfig:"DB_NAME" default:"whoosh"`
|
||||
Username string `envconfig:"DB_USER" default:"whoosh"`
|
||||
Password string `envconfig:"DB_PASSWORD"`
|
||||
PasswordFile string `envconfig:"DB_PASSWORD_FILE"`
|
||||
SSLMode string `envconfig:"DB_SSL_MODE" default:"disable"`
|
||||
URL string `envconfig:"DB_URL"`
|
||||
AutoMigrate bool `envconfig:"DB_AUTO_MIGRATE" default:"false"`
|
||||
MaxOpenConns int `envconfig:"DB_MAX_OPEN_CONNS" default:"25"`
|
||||
MaxIdleConns int `envconfig:"DB_MAX_IDLE_CONNS" default:"5"`
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Enabled bool `envconfig:"ENABLED" default:"false"`
|
||||
Host string `envconfig:"HOST" default:"localhost"`
|
||||
Port int `envconfig:"PORT" default:"6379"`
|
||||
Password string `envconfig:"PASSWORD"`
|
||||
PasswordFile string `envconfig:"PASSWORD_FILE"`
|
||||
Database int `envconfig:"DATABASE" default:"0"`
|
||||
}
|
||||
|
||||
type GITEAConfig struct {
|
||||
BaseURL string `envconfig:"BASE_URL" required:"true"`
|
||||
Token string `envconfig:"TOKEN"`
|
||||
TokenFile string `envconfig:"TOKEN_FILE"`
|
||||
WebhookPath string `envconfig:"WEBHOOK_PATH" default:"/webhooks/gitea"`
|
||||
WebhookToken string `envconfig:"WEBHOOK_TOKEN"`
|
||||
WebhookTokenFile string `envconfig:"WEBHOOK_TOKEN_FILE"`
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
JWTSecret string `envconfig:"JWT_SECRET"`
|
||||
JWTSecretFile string `envconfig:"JWT_SECRET_FILE"`
|
||||
JWTExpiry time.Duration `envconfig:"JWT_EXPIRY" default:"24h"`
|
||||
ServiceTokens []string `envconfig:"SERVICE_TOKENS"`
|
||||
ServiceTokensFile string `envconfig:"SERVICE_TOKENS_FILE"`
|
||||
}
|
||||
|
||||
type LoggingConfig struct {
|
||||
Level string `envconfig:"LEVEL" default:"info"`
|
||||
Environment string `envconfig:"ENVIRONMENT" default:"production"`
|
||||
}
|
||||
|
||||
type BackbeatConfig struct {
|
||||
Enabled bool `envconfig:"ENABLED" default:"true"`
|
||||
ClusterID string `envconfig:"CLUSTER_ID" default:"chorus-production"`
|
||||
AgentID string `envconfig:"AGENT_ID" default:"whoosh"`
|
||||
NATSUrl string `envconfig:"NATS_URL" default:"nats://backbeat-nats:4222"`
|
||||
}
|
||||
|
||||
func readSecretFile(filePath string) (string, error) {
|
||||
if filePath == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read secret file %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(content)), nil
|
||||
}
|
||||
|
||||
func (c *Config) loadSecrets() error {
|
||||
// Load database password from file if specified
|
||||
if c.Database.PasswordFile != "" {
|
||||
password, err := readSecretFile(c.Database.PasswordFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Database.Password = password
|
||||
}
|
||||
|
||||
// Load Redis password from file if specified
|
||||
if c.Redis.PasswordFile != "" {
|
||||
password, err := readSecretFile(c.Redis.PasswordFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Redis.Password = password
|
||||
}
|
||||
|
||||
// Load GITEA token from file if specified
|
||||
if c.GITEA.TokenFile != "" {
|
||||
token, err := readSecretFile(c.GITEA.TokenFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.GITEA.Token = token
|
||||
}
|
||||
|
||||
// Load GITEA webhook token from file if specified
|
||||
if c.GITEA.WebhookTokenFile != "" {
|
||||
token, err := readSecretFile(c.GITEA.WebhookTokenFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.GITEA.WebhookToken = token
|
||||
}
|
||||
|
||||
// Load JWT secret from file if specified
|
||||
if c.Auth.JWTSecretFile != "" {
|
||||
secret, err := readSecretFile(c.Auth.JWTSecretFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Auth.JWTSecret = secret
|
||||
}
|
||||
|
||||
// Load service tokens from file if specified
|
||||
if c.Auth.ServiceTokensFile != "" {
|
||||
tokens, err := readSecretFile(c.Auth.ServiceTokensFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Auth.ServiceTokens = strings.Split(tokens, ",")
|
||||
// Trim whitespace from each token
|
||||
for i, token := range c.Auth.ServiceTokens {
|
||||
c.Auth.ServiceTokens[i] = strings.TrimSpace(token)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
// Load secrets from files first
|
||||
if err := c.loadSecrets(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate required database password
|
||||
if c.Database.Password == "" {
|
||||
return fmt.Errorf("database password is required (set WHOOSH_DATABASE_DB_PASSWORD or WHOOSH_DATABASE_DB_PASSWORD_FILE)")
|
||||
}
|
||||
|
||||
// Build database URL if not provided
|
||||
if c.Database.URL == "" {
|
||||
c.Database.URL = fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s",
|
||||
url.QueryEscape(c.Database.Username),
|
||||
url.QueryEscape(c.Database.Password),
|
||||
c.Database.Host,
|
||||
c.Database.Port,
|
||||
url.QueryEscape(c.Database.Database),
|
||||
c.Database.SSLMode,
|
||||
)
|
||||
}
|
||||
|
||||
if c.GITEA.BaseURL == "" {
|
||||
return fmt.Errorf("GITEA base URL is required")
|
||||
}
|
||||
|
||||
if c.GITEA.Token == "" {
|
||||
return fmt.Errorf("GITEA token is required (set WHOOSH_GITEA_TOKEN or WHOOSH_GITEA_TOKEN_FILE)")
|
||||
}
|
||||
|
||||
if c.GITEA.WebhookToken == "" {
|
||||
return fmt.Errorf("GITEA webhook token is required (set WHOOSH_GITEA_WEBHOOK_TOKEN or WHOOSH_GITEA_WEBHOOK_TOKEN_FILE)")
|
||||
}
|
||||
|
||||
if c.Auth.JWTSecret == "" {
|
||||
return fmt.Errorf("JWT secret is required (set WHOOSH_AUTH_JWT_SECRET or WHOOSH_AUTH_JWT_SECRET_FILE)")
|
||||
}
|
||||
|
||||
if len(c.Auth.ServiceTokens) == 0 {
|
||||
return fmt.Errorf("at least one service token is required (set WHOOSH_AUTH_SERVICE_TOKENS or WHOOSH_AUTH_SERVICE_TOKENS_FILE)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user