feat(phase-1.3): config validation, env-specific loading, SOPS secrets
CI / CI (pull_request) Successful in 4m38s
CI / CI (pull_request) Successful in 4m38s
config.go: - Environment enum (dev/staging/prod) with auto-detect - MemoryServiceConfig + LLMConfig structs - Validate(): TLS pair check, file existence, prod LLM key requirement - In-cluster default host detection via KUBERNETES_SERVICE_HOST - TaskQueue + WorkerCount from env config_test.go: 11 tests - defaults, env override, TLS mismatch, TLS file not found - prod validation (requires LLM key), env parsing - in-cluster detection, port defaults, int parsing k8s/secrets.enc.yaml: SOPS-encrypted Secret - anthropic-api-key, memory-service-jwt, temporal-postgres-password - Age-encrypted, KSOPS-compatible Build: clean, full test suite passes
This commit is contained in:
+128
-14
@@ -1,39 +1,145 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Environment represents the deployment environment.
|
||||
type Environment string
|
||||
|
||||
const (
|
||||
EnvDev Environment = "dev"
|
||||
EnvStaging Environment = "staging"
|
||||
EnvProd Environment = "prod"
|
||||
)
|
||||
|
||||
// TemporalConfig holds Temporal cluster configuration.
|
||||
type TemporalConfig struct {
|
||||
HostPort string // default: 127.0.0.1:7233
|
||||
Namespace string // default: production
|
||||
TLSCert string // env: TEMPORAL_TLS_CERT (file path)
|
||||
TLSKey string // env: TEMPORAL_TLS_KEY (file path)
|
||||
HostPort string // env: TEMPORAL_HOSTPORT
|
||||
Namespace string // env: TEMPORAL_NAMESPACE
|
||||
TLSCert string // env: TEMPORAL_TLS_CERT (file path)
|
||||
TLSKey string // env: TEMPORAL_TLS_KEY (file path)
|
||||
TaskQueue string // env: TEMPORAL_TASK_QUEUE
|
||||
WorkerCount int // env: TEMPORAL_WORKER_COUNT
|
||||
}
|
||||
|
||||
// AppConfig holds application configuration.
|
||||
// MemoryServiceConfig holds memory service connection settings.
|
||||
type MemoryServiceConfig struct {
|
||||
URL string // env: MEMORY_SERVICE_URL
|
||||
JWTToken string // env: MEMORY_SERVICE_JWT_TOKEN
|
||||
}
|
||||
|
||||
// LLMConfig holds LLM provider settings.
|
||||
type LLMConfig struct {
|
||||
BaseURL string // env: LOCAL_LLM_BASE_URL
|
||||
AnthropicKey string // env: ANTHROPIC_API_KEY
|
||||
AuthToken string // env: LLM_AUTH_TOKEN
|
||||
}
|
||||
|
||||
// AppConfig holds all application configuration.
|
||||
type AppConfig struct {
|
||||
Temporal TemporalConfig
|
||||
AnthropicAPIKey string
|
||||
Env Environment
|
||||
Temporal TemporalConfig
|
||||
MemoryService MemoryServiceConfig
|
||||
LLM LLMConfig
|
||||
LogLevel string // env: LOG_LEVEL
|
||||
}
|
||||
|
||||
// LoadConfig loads application configuration from environment variables.
|
||||
// LoadConfig loads configuration from environment variables with validation.
|
||||
func LoadConfig() (AppConfig, error) {
|
||||
cfg := AppConfig{
|
||||
Env: parseEnv(getEnvOrDefault("APP_ENV", "dev")),
|
||||
Temporal: TemporalConfig{
|
||||
HostPort: addDefaultPort(getEnvOrDefault("TEMPORAL_HOSTPORT", "127.0.0.1:7233")),
|
||||
Namespace: getEnvOrDefault("TEMPORAL_NAMESPACE", "poimen-harness"),
|
||||
TLSCert: os.Getenv("TEMPORAL_TLS_CERT"),
|
||||
TLSKey: os.Getenv("TEMPORAL_TLS_KEY"),
|
||||
HostPort: addDefaultPort(getEnvOrDefault("TEMPORAL_HOSTPORT", defaultTemporalHost())),
|
||||
Namespace: getEnvOrDefault("TEMPORAL_NAMESPACE", "poimen-harness"),
|
||||
TLSCert: os.Getenv("TEMPORAL_TLS_CERT"),
|
||||
TLSKey: os.Getenv("TEMPORAL_TLS_KEY"),
|
||||
TaskQueue: getEnvOrDefault("TEMPORAL_TASK_QUEUE", "poimen-taskqueue"),
|
||||
WorkerCount: getEnvIntOrDefault("TEMPORAL_WORKER_COUNT", 10),
|
||||
},
|
||||
AnthropicAPIKey: os.Getenv("ANTHROPIC_API_KEY"),
|
||||
MemoryService: MemoryServiceConfig{
|
||||
URL: os.Getenv("MEMORY_SERVICE_URL"),
|
||||
JWTToken: os.Getenv("MEMORY_SERVICE_JWT_TOKEN"),
|
||||
},
|
||||
LLM: LLMConfig{
|
||||
BaseURL: os.Getenv("LOCAL_LLM_BASE_URL"),
|
||||
AnthropicKey: os.Getenv("ANTHROPIC_API_KEY"),
|
||||
AuthToken: os.Getenv("LLM_AUTH_TOKEN"),
|
||||
},
|
||||
LogLevel: getEnvOrDefault("LOG_LEVEL", "info"),
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return AppConfig{}, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// Validate checks required fields and consistency.
|
||||
func (c *AppConfig) Validate() error {
|
||||
if c.Temporal.HostPort == "" {
|
||||
return fmt.Errorf("TEMPORAL_HOSTPORT is required")
|
||||
}
|
||||
if c.Temporal.Namespace == "" {
|
||||
return fmt.Errorf("TEMPORAL_NAMESPACE is required")
|
||||
}
|
||||
|
||||
// TLS: both or neither
|
||||
hasCert := c.Temporal.TLSCert != ""
|
||||
hasKey := c.Temporal.TLSKey != ""
|
||||
if hasCert != hasKey {
|
||||
return fmt.Errorf("TEMPORAL_TLS_CERT and TEMPORAL_TLS_KEY must both be set or both empty")
|
||||
}
|
||||
|
||||
// Validate TLS files exist if specified
|
||||
if hasCert {
|
||||
if _, err := os.Stat(c.Temporal.TLSCert); err != nil {
|
||||
return fmt.Errorf("TEMPORAL_TLS_CERT file not found: %s", c.Temporal.TLSCert)
|
||||
}
|
||||
if _, err := os.Stat(c.Temporal.TLSKey); err != nil {
|
||||
return fmt.Errorf("TEMPORAL_TLS_KEY file not found: %s", c.Temporal.TLSKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Prod requires LLM key
|
||||
if c.Env == EnvProd {
|
||||
if c.LLM.AnthropicKey == "" && c.LLM.AuthToken == "" {
|
||||
return fmt.Errorf("prod requires ANTHROPIC_API_KEY or LLM_AUTH_TOKEN")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsProd returns true if running in production.
|
||||
func (c *AppConfig) IsProd() bool { return c.Env == EnvProd }
|
||||
|
||||
// IsDevOrStaging returns true if running in dev or staging.
|
||||
func (c *AppConfig) IsDevOrStaging() bool { return c.Env == EnvDev || c.Env == EnvStaging }
|
||||
|
||||
func defaultTemporalHost() string {
|
||||
// In-cluster default vs local
|
||||
if os.Getenv("KUBERNETES_SERVICE_HOST") != "" {
|
||||
return "temporal-frontend.temporal.svc.cluster.local:7233"
|
||||
}
|
||||
return "127.0.0.1:7233"
|
||||
}
|
||||
|
||||
func parseEnv(s string) Environment {
|
||||
switch strings.ToLower(s) {
|
||||
case "prod", "production":
|
||||
return EnvProd
|
||||
case "staging", "stage":
|
||||
return EnvStaging
|
||||
default:
|
||||
return EnvDev
|
||||
}
|
||||
}
|
||||
|
||||
func getEnvOrDefault(key, defaultVal string) string {
|
||||
if val := os.Getenv(key); val != "" {
|
||||
return val
|
||||
@@ -41,8 +147,16 @@ func getEnvOrDefault(key, defaultVal string) string {
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
func getEnvIntOrDefault(key string, defaultVal int) int {
|
||||
if val := os.Getenv(key); val != "" {
|
||||
if i, err := strconv.Atoi(val); err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
func addDefaultPort(hostPort string) string {
|
||||
// If no port specified, add default port 7233
|
||||
if !strings.Contains(hostPort, ":") {
|
||||
return hostPort + ":7233"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user