移除旧 OIDC_* 与 VITE_OIDC_* 业务配置读取,统一使用数据库 Revision 和 SecretStore 引用构建运行时。同步更新 Compose、示例配置、接入文档及集成测试,并保留数据库、SecretStore 和安全事件健康窗口等部署级参数。\n\n验证:go test ./...;go test -race ./...;go vet ./...;pnpm test;pnpm lint;pnpm build;docker compose config -q
218 lines
8.5 KiB
Go
218 lines
8.5 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"log/slog"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
DefaultLocalGeneratedStorageDir = "data/static/generated"
|
|
DefaultLocalUploadedStorageDir = "data/static/uploaded"
|
|
)
|
|
|
|
type Config struct {
|
|
AppEnv string
|
|
HTTPAddr string
|
|
DatabaseURL string
|
|
IdentityMode string
|
|
JWTSecret string
|
|
ServerMainBaseURL string
|
|
ServerMainInternalToken string
|
|
ServerMainInternalKey string
|
|
ServerMainInternalSecret string
|
|
IdentitySecretStore string
|
|
IdentitySecretDir string
|
|
IdentityKubernetesNamespace string
|
|
IdentityKubernetesSecretName string
|
|
IdentityKubernetesAPIServer string
|
|
IdentityKubernetesTokenFile string
|
|
IdentityKubernetesCAFile string
|
|
IdentitySecurityEventHeartbeatIntervalSeconds int
|
|
IdentitySecurityEventStaleAfterSeconds int
|
|
IdentitySecurityEventClockSkewSeconds int
|
|
PublicBaseURL string
|
|
WebBaseURL string
|
|
LocalGeneratedStorageDir string
|
|
LocalUploadedStorageDir string
|
|
LocalTempAssetTTLHours int
|
|
TaskProgressCallbackEnabled bool
|
|
TaskProgressCallbackURL string
|
|
TaskProgressCallbackTimeoutMS string
|
|
TaskProgressCallbackMaxAttempts string
|
|
CORSAllowedOrigin string
|
|
GlobalHTTPProxy string
|
|
GlobalHTTPProxySource string
|
|
LogLevel slog.Level
|
|
}
|
|
|
|
func Load() Config {
|
|
globalProxy := LoadGlobalHTTPProxyStatus()
|
|
appEnv := env("APP_ENV", "development")
|
|
return Config{
|
|
AppEnv: appEnv,
|
|
HTTPAddr: env("HTTP_ADDR", ":8088"),
|
|
DatabaseURL: gatewayDatabaseURL(),
|
|
IdentityMode: env("IDENTITY_MODE", "hybrid"),
|
|
JWTSecret: env("CONFIG_JWT_SECRET", "this is a very secret secret"),
|
|
ServerMainBaseURL: strings.TrimRight(
|
|
env("SERVER_MAIN_BASE_URL", "http://localhost:3000"),
|
|
"/",
|
|
),
|
|
ServerMainInternalToken: env("SERVER_MAIN_INTERNAL_TOKEN", ""),
|
|
ServerMainInternalKey: env("SERVER_MAIN_INTERNAL_KEY", "gateway"),
|
|
ServerMainInternalSecret: env("SERVER_MAIN_INTERNAL_SECRET", env("SERVER_MAIN_INTERNAL_TOKEN", "")),
|
|
IdentitySecretStore: env("IDENTITY_SECRET_STORE", "file"),
|
|
IdentitySecretDir: env("IDENTITY_SECRET_DIR", ".local-secrets/identity"),
|
|
IdentityKubernetesNamespace: env("IDENTITY_KUBERNETES_NAMESPACE", env("POD_NAMESPACE", "")),
|
|
IdentityKubernetesSecretName: env("IDENTITY_KUBERNETES_SECRET_NAME", "easyai-gateway-identity"),
|
|
IdentityKubernetesAPIServer: env("IDENTITY_KUBERNETES_API_SERVER", "https://kubernetes.default.svc"),
|
|
IdentityKubernetesTokenFile: env("IDENTITY_KUBERNETES_TOKEN_FILE", "/var/run/secrets/kubernetes.io/serviceaccount/token"),
|
|
IdentityKubernetesCAFile: env("IDENTITY_KUBERNETES_CA_FILE", "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"),
|
|
IdentitySecurityEventHeartbeatIntervalSeconds: envInt("IDENTITY_SECURITY_EVENTS_HEARTBEAT_INTERVAL_SECONDS", 60),
|
|
IdentitySecurityEventStaleAfterSeconds: envInt("IDENTITY_SECURITY_EVENTS_STALE_AFTER_SECONDS", 180),
|
|
IdentitySecurityEventClockSkewSeconds: envInt("IDENTITY_SECURITY_EVENTS_CLOCK_SKEW_SECONDS", 60),
|
|
PublicBaseURL: strings.TrimRight(env("AI_GATEWAY_PUBLIC_BASE_URL", env("PUBLIC_BASE_URL", "")), "/"),
|
|
WebBaseURL: strings.TrimRight(env("AI_GATEWAY_WEB_BASE_URL", env("GATEWAY_WEB_BASE_URL", env("PUBLIC_WEB_BASE_URL", ""))), "/"),
|
|
LocalGeneratedStorageDir: env("AI_GATEWAY_GENERATED_STORAGE_DIR", env("LOCAL_GENERATED_STORAGE_DIR", env("AI_GATEWAY_STATIC_STORAGE_DIR", DefaultLocalGeneratedStorageDir))),
|
|
LocalUploadedStorageDir: env("AI_GATEWAY_UPLOADED_STORAGE_DIR", env("LOCAL_UPLOADED_STORAGE_DIR", DefaultLocalUploadedStorageDir)),
|
|
LocalTempAssetTTLHours: envInt("AI_GATEWAY_LOCAL_TEMP_ASSET_TTL_HOURS", 24),
|
|
TaskProgressCallbackEnabled: env("TASK_PROGRESS_CALLBACK_ENABLED", "true") == "true",
|
|
TaskProgressCallbackURL: env("TASK_PROGRESS_CALLBACK_URL",
|
|
strings.TrimRight(env("SERVER_MAIN_BASE_URL", "http://localhost:3000"), "/")+"/internal/platform/task-progress-callbacks",
|
|
),
|
|
TaskProgressCallbackTimeoutMS: env("TASK_PROGRESS_CALLBACK_TIMEOUT_MS", "5000"),
|
|
TaskProgressCallbackMaxAttempts: env("TASK_PROGRESS_CALLBACK_MAX_ATTEMPTS", "10"),
|
|
CORSAllowedOrigin: env("CORS_ALLOWED_ORIGIN", "http://localhost:5178,http://127.0.0.1:5178"),
|
|
GlobalHTTPProxy: globalProxy.HTTPProxy,
|
|
GlobalHTTPProxySource: globalProxy.Source,
|
|
LogLevel: logLevel(env("LOG_LEVEL", "info")),
|
|
}
|
|
}
|
|
|
|
func (c Config) Validate() error {
|
|
switch strings.ToLower(strings.TrimSpace(c.IdentitySecretStore)) {
|
|
case "":
|
|
case "file":
|
|
if strings.TrimSpace(c.IdentitySecretDir) == "" {
|
|
return errors.New("IDENTITY_SECRET_DIR is required for the file SecretStore")
|
|
}
|
|
case "kubernetes":
|
|
if strings.TrimSpace(c.IdentityKubernetesNamespace) == "" || strings.TrimSpace(c.IdentityKubernetesSecretName) == "" {
|
|
return errors.New("Kubernetes identity SecretStore requires namespace and Secret name")
|
|
}
|
|
default:
|
|
return errors.New("IDENTITY_SECRET_STORE must be file or kubernetes")
|
|
}
|
|
if c.IdentitySecurityEventHeartbeatIntervalSeconds != 0 || c.IdentitySecurityEventStaleAfterSeconds != 0 || c.IdentitySecurityEventClockSkewSeconds != 0 {
|
|
if c.IdentitySecurityEventHeartbeatIntervalSeconds <= 0 ||
|
|
c.IdentitySecurityEventStaleAfterSeconds < 2*c.IdentitySecurityEventHeartbeatIntervalSeconds ||
|
|
c.IdentitySecurityEventClockSkewSeconds < 0 || c.IdentitySecurityEventClockSkewSeconds > 300 {
|
|
return errors.New("identity security event heartbeat, stale threshold, or clock skew is invalid")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type GlobalHTTPProxyStatus struct {
|
|
HTTPProxy string
|
|
Source string
|
|
}
|
|
|
|
func LoadGlobalHTTPProxyStatus() GlobalHTTPProxyStatus {
|
|
for _, key := range []string{
|
|
"AI_GATEWAY_GLOBAL_HTTP_PROXY",
|
|
"GLOBAL_HTTP_PROXY",
|
|
"HTTPS_PROXY",
|
|
"https_proxy",
|
|
"HTTP_PROXY",
|
|
"http_proxy",
|
|
"ALL_PROXY",
|
|
"all_proxy",
|
|
} {
|
|
if value := envValue(key); value != "" {
|
|
return GlobalHTTPProxyStatus{HTTPProxy: value, Source: key}
|
|
}
|
|
}
|
|
return GlobalHTTPProxyStatus{}
|
|
}
|
|
|
|
func gatewayDatabaseURL() string {
|
|
if value := envValue("AI_GATEWAY_DATABASE_URL"); value != "" {
|
|
return normalizePostgresURL(value)
|
|
}
|
|
if value := envValue("DATABASE_URL"); value != "" {
|
|
return normalizePostgresURL(value)
|
|
}
|
|
if memoryURL := envValue("MEMORY_DATABASE_URL"); memoryURL != "" {
|
|
return normalizePostgresURL(withDatabase(memoryURL, env("AI_GATEWAY_DATABASE_NAME", "easyai_ai_gateway")))
|
|
}
|
|
return normalizePostgresURL("postgresql://easyai:easyai2025@localhost:5432/easyai_ai_gateway?sslmode=disable")
|
|
}
|
|
|
|
func normalizePostgresURL(raw string) string {
|
|
parsed, err := url.Parse(raw)
|
|
if err != nil {
|
|
return raw
|
|
}
|
|
values := parsed.Query()
|
|
schema := values.Get("schema")
|
|
if schema == "" {
|
|
return raw
|
|
}
|
|
values.Del("schema")
|
|
if values.Get("search_path") == "" {
|
|
values.Set("search_path", schema)
|
|
}
|
|
parsed.RawQuery = values.Encode()
|
|
return parsed.String()
|
|
}
|
|
|
|
func withDatabase(raw string, databaseName string) string {
|
|
parsed, err := url.Parse(raw)
|
|
if err != nil || databaseName == "" {
|
|
return raw
|
|
}
|
|
parsed.Path = "/" + databaseName
|
|
return parsed.String()
|
|
}
|
|
|
|
func envValue(key string) string {
|
|
return strings.TrimSpace(os.Getenv(key))
|
|
}
|
|
|
|
func env(key string, fallback string) string {
|
|
if value := envValue(key); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func envInt(key string, fallback int) int {
|
|
value := envValue(key)
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
parsed, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func logLevel(value string) slog.Level {
|
|
switch strings.ToLower(value) {
|
|
case "debug":
|
|
return slog.LevelDebug
|
|
case "warn", "warning":
|
|
return slog.LevelWarn
|
|
case "error":
|
|
return slog.LevelError
|
|
default:
|
|
return slog.LevelInfo
|
|
}
|
|
}
|