256 lines
8.6 KiB
Go
256 lines
8.6 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type Config struct {
|
|
HTTP HTTPConfig
|
|
Postgres PostgresConfig
|
|
Webhook WebhookConfig
|
|
PlatformAdapters PlatformAdaptersConfig
|
|
Runtime RuntimeConfig
|
|
}
|
|
|
|
type RuntimeConfig struct {
|
|
Env string
|
|
}
|
|
|
|
type HTTPConfig struct {
|
|
Addr string
|
|
ReadHeaderTimeout int
|
|
ReadTimeout int
|
|
WriteTimeout int
|
|
IdleTimeout int
|
|
MaxHeaderBytes int
|
|
MaxBodyBytes int64
|
|
}
|
|
|
|
type PostgresConfig struct {
|
|
Enabled bool
|
|
DSN string
|
|
MigrationDir string
|
|
MaxOpenConns int
|
|
MaxIdleConns int
|
|
ConnMaxLifetime int
|
|
}
|
|
|
|
type WebhookConfig struct {
|
|
Secret string
|
|
TimestampHeader string
|
|
SignatureHeader string
|
|
MaxSkewSeconds int
|
|
}
|
|
|
|
type PlatformAdaptersConfig struct {
|
|
Enabled bool
|
|
Sub2API PlatformAdapterProfileConfig
|
|
NewAPI PlatformAdapterProfileConfig
|
|
}
|
|
|
|
type PlatformAdapterProfileConfig struct {
|
|
Enabled bool
|
|
IngressSecret string
|
|
CallbackBaseURL string
|
|
CallbackSecret string
|
|
CallbackTimeoutMS int
|
|
CallbackMaxRetries int
|
|
CallbackPollIntervalMS int
|
|
CallbackBatchSize int
|
|
CallbackRetrySchedule []int
|
|
}
|
|
|
|
func Load() (*Config, error) {
|
|
cfg := &Config{
|
|
HTTP: HTTPConfig{
|
|
Addr: getEnv("AI_CS_ADDR", ":8080"),
|
|
ReadHeaderTimeout: getEnvInt("AI_CS_READ_HEADER_TIMEOUT_SEC", 5),
|
|
ReadTimeout: getEnvInt("AI_CS_READ_TIMEOUT_SEC", 10),
|
|
WriteTimeout: getEnvInt("AI_CS_WRITE_TIMEOUT_SEC", 15),
|
|
IdleTimeout: getEnvInt("AI_CS_IDLE_TIMEOUT_SEC", 60),
|
|
MaxHeaderBytes: getEnvInt("AI_CS_MAX_HEADER_BYTES", 1<<20),
|
|
MaxBodyBytes: getEnvInt64("AI_CS_MAX_BODY_BYTES", 1<<20),
|
|
},
|
|
Postgres: PostgresConfig{
|
|
Enabled: getEnvBool("AI_CS_POSTGRES_ENABLED", false),
|
|
DSN: getEnv("AI_CS_POSTGRES_DSN", ""),
|
|
MigrationDir: getEnv("AI_CS_POSTGRES_MIGRATION_DIR", "db/migration"),
|
|
MaxOpenConns: getEnvInt("AI_CS_POSTGRES_MAX_OPEN_CONNS", 20),
|
|
MaxIdleConns: getEnvInt("AI_CS_POSTGRES_MAX_IDLE_CONNS", 5),
|
|
ConnMaxLifetime: getEnvInt("AI_CS_POSTGRES_CONN_MAX_LIFETIME_SEC", 300),
|
|
},
|
|
Webhook: WebhookConfig{
|
|
Secret: getEnv("AI_CS_WEBHOOK_SECRET", ""),
|
|
TimestampHeader: getEnv("AI_CS_WEBHOOK_TIMESTAMP_HEADER", "X-CS-Timestamp"),
|
|
SignatureHeader: getEnv("AI_CS_WEBHOOK_SIGNATURE_HEADER", "X-CS-Signature"),
|
|
MaxSkewSeconds: getEnvInt("AI_CS_WEBHOOK_MAX_SKEW_SECONDS", 300),
|
|
},
|
|
PlatformAdapters: PlatformAdaptersConfig{
|
|
Enabled: getEnvBool("AI_CS_PLATFORM_ADAPTERS_ENABLED", false),
|
|
Sub2API: PlatformAdapterProfileConfig{
|
|
Enabled: getEnvBool("AI_CS_PLATFORM_SUB2API_ENABLED", false),
|
|
IngressSecret: getEnv("AI_CS_PLATFORM_SUB2API_INGRESS_SECRET", ""),
|
|
CallbackBaseURL: getEnv("AI_CS_PLATFORM_SUB2API_CALLBACK_BASE_URL", ""),
|
|
CallbackSecret: getEnv("AI_CS_PLATFORM_SUB2API_CALLBACK_SECRET", ""),
|
|
CallbackTimeoutMS: getEnvInt("AI_CS_PLATFORM_SUB2API_CALLBACK_TIMEOUT_MS", 3000),
|
|
CallbackMaxRetries: getEnvInt("AI_CS_PLATFORM_SUB2API_CALLBACK_MAX_RETRIES", 5),
|
|
CallbackPollIntervalMS: getEnvInt("AI_CS_PLATFORM_SUB2API_CALLBACK_POLL_INTERVAL_MS", 5000),
|
|
CallbackBatchSize: getEnvInt("AI_CS_PLATFORM_SUB2API_CALLBACK_BATCH_SIZE", 20),
|
|
CallbackRetrySchedule: getEnvIntList("AI_CS_PLATFORM_SUB2API_CALLBACK_RETRY_SCHEDULE_SEC", []int{10, 30, 60, 300, 900}),
|
|
},
|
|
NewAPI: PlatformAdapterProfileConfig{
|
|
Enabled: getEnvBool("AI_CS_PLATFORM_NEWAPI_ENABLED", false),
|
|
IngressSecret: getEnv("AI_CS_PLATFORM_NEWAPI_INGRESS_SECRET", ""),
|
|
CallbackBaseURL: getEnv("AI_CS_PLATFORM_NEWAPI_CALLBACK_BASE_URL", ""),
|
|
CallbackSecret: getEnv("AI_CS_PLATFORM_NEWAPI_CALLBACK_SECRET", ""),
|
|
CallbackTimeoutMS: getEnvInt("AI_CS_PLATFORM_NEWAPI_CALLBACK_TIMEOUT_MS", 3000),
|
|
CallbackMaxRetries: getEnvInt("AI_CS_PLATFORM_NEWAPI_CALLBACK_MAX_RETRIES", 5),
|
|
CallbackPollIntervalMS: getEnvInt("AI_CS_PLATFORM_NEWAPI_CALLBACK_POLL_INTERVAL_MS", 5000),
|
|
CallbackBatchSize: getEnvInt("AI_CS_PLATFORM_NEWAPI_CALLBACK_BATCH_SIZE", 20),
|
|
CallbackRetrySchedule: getEnvIntList("AI_CS_PLATFORM_NEWAPI_CALLBACK_RETRY_SCHEDULE_SEC", []int{10, 30, 60, 300, 900}),
|
|
},
|
|
},
|
|
Runtime: RuntimeConfig{
|
|
Env: normalizeRuntimeEnv(getEnv("AI_CS_RUNTIME_ENV", getEnv("AI_CS_ENV", "development"))),
|
|
},
|
|
}
|
|
if strings.TrimSpace(cfg.HTTP.Addr) == "" {
|
|
return nil, fmt.Errorf("AI_CS_ADDR must not be empty")
|
|
}
|
|
if cfg.HTTP.MaxBodyBytes <= 0 {
|
|
return nil, fmt.Errorf("AI_CS_MAX_BODY_BYTES must be positive")
|
|
}
|
|
if cfg.Postgres.Enabled && strings.TrimSpace(cfg.Postgres.DSN) == "" {
|
|
return nil, fmt.Errorf("AI_CS_POSTGRES_DSN must not be empty when postgres is enabled")
|
|
}
|
|
if cfg.Webhook.MaxSkewSeconds <= 0 {
|
|
return nil, fmt.Errorf("AI_CS_WEBHOOK_MAX_SKEW_SECONDS must be positive")
|
|
}
|
|
if err := validatePlatformProfile("sub2api", cfg.PlatformAdapters.Enabled, cfg.PlatformAdapters.Sub2API); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validatePlatformProfile("newapi", cfg.PlatformAdapters.Enabled, cfg.PlatformAdapters.NewAPI); err != nil {
|
|
return nil, err
|
|
}
|
|
if cfg.Runtime.Env != "production" && cfg.Runtime.Env != "development" && cfg.Runtime.Env != "test" {
|
|
return nil, fmt.Errorf("AI_CS_RUNTIME_ENV must be one of production/development/test, got: %s", cfg.Runtime.Env)
|
|
}
|
|
if cfg.Runtime.Env == "production" && !cfg.Postgres.Enabled {
|
|
return nil, fmt.Errorf("AI_CS_RUNTIME_ENV=production requires AI_CS_POSTGRES_ENABLED=true, but it is false (memory fallback is not allowed in production)")
|
|
}
|
|
if cfg.Runtime.Env == "production" && strings.TrimSpace(cfg.Webhook.Secret) == "" {
|
|
return nil, fmt.Errorf("AI_CS_WEBHOOK_SECRET must not be empty in production")
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func validatePlatformProfile(platform string, adaptersEnabled bool, profile PlatformAdapterProfileConfig) error {
|
|
if !adaptersEnabled || !profile.Enabled {
|
|
return nil
|
|
}
|
|
upperPlatform := strings.ToUpper(platform)
|
|
if strings.TrimSpace(profile.IngressSecret) == "" {
|
|
return fmt.Errorf("AI_CS_PLATFORM_%s_INGRESS_SECRET must not be empty when platform ingress is enabled", upperPlatform)
|
|
}
|
|
if profile.CallbackTimeoutMS <= 0 {
|
|
return fmt.Errorf("AI_CS_PLATFORM_%s_CALLBACK_TIMEOUT_MS must be positive", upperPlatform)
|
|
}
|
|
if profile.CallbackMaxRetries < 0 {
|
|
return fmt.Errorf("AI_CS_PLATFORM_%s_CALLBACK_MAX_RETRIES must not be negative", upperPlatform)
|
|
}
|
|
if profile.CallbackPollIntervalMS <= 0 {
|
|
return fmt.Errorf("AI_CS_PLATFORM_%s_CALLBACK_POLL_INTERVAL_MS must be positive", upperPlatform)
|
|
}
|
|
if profile.CallbackBatchSize <= 0 {
|
|
return fmt.Errorf("AI_CS_PLATFORM_%s_CALLBACK_BATCH_SIZE must be positive", upperPlatform)
|
|
}
|
|
if len(profile.CallbackRetrySchedule) == 0 {
|
|
return fmt.Errorf("AI_CS_PLATFORM_%s_CALLBACK_RETRY_SCHEDULE_SEC must not be empty", upperPlatform)
|
|
}
|
|
for _, seconds := range profile.CallbackRetrySchedule {
|
|
if seconds <= 0 {
|
|
return fmt.Errorf("AI_CS_PLATFORM_%s_CALLBACK_RETRY_SCHEDULE_SEC must contain only positive integers", upperPlatform)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizeRuntimeEnv(value string) string {
|
|
switch strings.TrimSpace(strings.ToLower(value)) {
|
|
case "", "dev", "development":
|
|
return "development"
|
|
case "prod", "production":
|
|
return "production"
|
|
case "test":
|
|
return "test"
|
|
default:
|
|
return strings.TrimSpace(strings.ToLower(value))
|
|
}
|
|
}
|
|
|
|
func getEnv(key, fallback string) string {
|
|
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func getEnvInt(key string, fallback int) int {
|
|
value := strings.TrimSpace(os.Getenv(key))
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
parsed, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func getEnvInt64(key string, fallback int64) int64 {
|
|
value := strings.TrimSpace(os.Getenv(key))
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
parsed, err := strconv.ParseInt(value, 10, 64)
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func getEnvBool(key string, fallback bool) bool {
|
|
value := strings.TrimSpace(strings.ToLower(os.Getenv(key)))
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
switch value {
|
|
case "1", "true", "yes", "on":
|
|
return true
|
|
case "0", "false", "no", "off":
|
|
return false
|
|
default:
|
|
return fallback
|
|
}
|
|
}
|
|
|
|
func getEnvIntList(key string, fallback []int) []int {
|
|
value := strings.TrimSpace(os.Getenv(key))
|
|
if value == "" {
|
|
return append([]int(nil), fallback...)
|
|
}
|
|
parts := strings.Split(value, ",")
|
|
result := make([]int, 0, len(parts))
|
|
for _, part := range parts {
|
|
parsed, err := strconv.Atoi(strings.TrimSpace(part))
|
|
if err != nil {
|
|
return append([]int(nil), fallback...)
|
|
}
|
|
result = append(result, parsed)
|
|
}
|
|
return result
|
|
}
|