1328 lines
50 KiB
Go
1328 lines
50 KiB
Go
package config
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-playground/validator/v10"
|
|
"github.com/spf13/viper"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type Config struct {
|
|
Server ServerConfig
|
|
GRPC GRPCConfig
|
|
Databases map[string]DatabaseConfig
|
|
ReadReplicas map[string][]DatabaseConfig
|
|
Auth AuthConfig
|
|
Keycloak KeycloakConfig
|
|
Bpjs BpjsConfig
|
|
SatuSehat SatuSehatConfig
|
|
Swagger SwaggerConfig
|
|
Security SecurityConfig
|
|
Validator *validator.Validate
|
|
}
|
|
|
|
type GRPCConfig struct {
|
|
Port int
|
|
Enabled bool
|
|
}
|
|
type SwaggerConfig struct {
|
|
Title string
|
|
Description string
|
|
Version string
|
|
TermsOfService string
|
|
ContactName string
|
|
ContactURL string
|
|
ContactEmail string
|
|
LicenseName string
|
|
LicenseURL string
|
|
Host string
|
|
BasePath string
|
|
Schemes []string
|
|
}
|
|
|
|
type ServerConfig struct {
|
|
Port int
|
|
Mode string
|
|
}
|
|
|
|
type DatabaseConfig struct {
|
|
Name string
|
|
Type string // postgres, mysql, sqlserver, sqlite, mongodb
|
|
Host string
|
|
Port int
|
|
Username string
|
|
Password string
|
|
Database string
|
|
Schema string
|
|
SSLMode string
|
|
Path string // For SQLite
|
|
Options string // Additional connection options
|
|
MaxOpenConns int // Max open connections
|
|
MaxIdleConns int // Max idle connections
|
|
ConnMaxLifetime time.Duration // Connection max lifetime
|
|
// Security settings
|
|
RequireSSL bool // Require SSL connection
|
|
SSLRootCert string // Path to SSL root certificate
|
|
SSLCert string // Path to SSL client certificate
|
|
SSLKey string // Path to SSL client key
|
|
Timeout time.Duration // Connection timeout
|
|
ConnectTimeout time.Duration // Connect timeout
|
|
ReadTimeout time.Duration // Read timeout
|
|
WriteTimeout time.Duration // Write timeout
|
|
StatementTimeout time.Duration // Statement timeout for PostgreSQL
|
|
// Connection pool settings
|
|
MaxLifetime time.Duration // Maximum amount of time a connection may be reused
|
|
MaxIdleTime time.Duration // Maximum amount of time a connection may be idle
|
|
HealthCheckPeriod time.Duration // Health check period
|
|
}
|
|
|
|
type AuthConfig struct {
|
|
Type string `yaml:"type" env:"AUTH_TYPE"` // "keycloak", "jwt", "static", "hybrid"
|
|
StaticTokens []string `yaml:"static_tokens" env:"AUTH_STATIC_TOKENS"` // Support multiple static tokens
|
|
FallbackTo string `yaml:"fallback_to" env:"AUTH_FALLBACK_TO"` // fallback auth type if primary fails
|
|
}
|
|
|
|
// AuthYAMLConfig represents the auth section in config.yaml
|
|
type AuthYAMLConfig struct {
|
|
Type string `yaml:"type"`
|
|
StaticTokens []string `yaml:"static_tokens"`
|
|
FallbackTo string `yaml:"fallback_to"`
|
|
}
|
|
|
|
type KeycloakYAMLConfig struct {
|
|
Issuer string `yaml:"issuer"`
|
|
Audience string `yaml:"audience"`
|
|
JwksURL string `yaml:"jwks_url"`
|
|
Enabled bool `yaml:"enabled"`
|
|
}
|
|
|
|
type KeycloakConfig struct {
|
|
Issuer string
|
|
Audience string
|
|
JwksURL string
|
|
Enabled bool
|
|
}
|
|
|
|
type BpjsConfig struct {
|
|
BaseURL string `json:"base_url"`
|
|
ConsID string `json:"cons_id"`
|
|
UserKey string `json:"user_key"`
|
|
SecretKey string `json:"secret_key"`
|
|
Timeout time.Duration `json:"timeout"`
|
|
}
|
|
|
|
type SatuSehatConfig struct {
|
|
OrgID string `json:"org_id"`
|
|
FasyakesID string `json:"fasyakes_id"`
|
|
ClientID string `json:"client_id"`
|
|
ClientSecret string `json:"client_secret"`
|
|
AuthURL string `json:"auth_url"`
|
|
BaseURL string `json:"base_url"`
|
|
ConsentURL string `json:"consent_url"`
|
|
KFAURL string `json:"kfa_url"`
|
|
Timeout time.Duration `json:"timeout"`
|
|
}
|
|
|
|
// SecurityConfig berisi semua pengaturan untuk middleware keamanan
|
|
type SecurityConfig struct {
|
|
// CORS
|
|
TrustedOrigins []string `mapstructure:"trusted_origins"`
|
|
// Rate Limiting
|
|
RateLimit RateLimitConfig `mapstructure:"rate_limit"`
|
|
// Input Validation
|
|
MaxInputLength int `mapstructure:"max_input_length"`
|
|
// SQL Injection Protection
|
|
SanitizeQueries bool `mapstructure:"sanitize_queries"`
|
|
// Connection Security
|
|
RequireSecureConnections bool `mapstructure:"require_secure_connections"`
|
|
}
|
|
|
|
// RateLimitConfig berisi pengaturan untuk rate limiter
|
|
type RateLimitConfig struct {
|
|
RequestsPerMinute int `mapstructure:"requests_per_minute"`
|
|
Redis RedisConfig `mapstructure:"redis"`
|
|
}
|
|
|
|
// RedisConfig berisi detail koneksi ke Redis
|
|
type RedisConfig struct {
|
|
Host string `mapstructure:"host"`
|
|
Port int `mapstructure:"port"`
|
|
Password string `mapstructure:"password"`
|
|
DB int `mapstructure:"db"`
|
|
}
|
|
|
|
func (cfg BpjsConfig) SetHeader() (string, string, string, string, string) {
|
|
timenow := time.Now().UTC()
|
|
t, err := time.Parse(time.RFC3339, "1970-01-01T00:00:00Z")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
tstamp := timenow.Unix() - t.Unix()
|
|
secret := []byte(cfg.SecretKey)
|
|
message := []byte(cfg.ConsID + "&" + fmt.Sprint(tstamp))
|
|
hash := hmac.New(sha256.New, secret)
|
|
hash.Write(message)
|
|
|
|
// to lowercase hexits
|
|
hex.EncodeToString(hash.Sum(nil))
|
|
// to base64
|
|
xSignature := base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
|
|
|
return cfg.ConsID, cfg.SecretKey, cfg.UserKey, fmt.Sprint(tstamp), xSignature
|
|
}
|
|
|
|
type ConfigBpjs struct {
|
|
Cons_id string
|
|
Secret_key string
|
|
User_key string
|
|
}
|
|
|
|
// SetHeader for backward compatibility
|
|
func (cfg ConfigBpjs) SetHeader() (string, string, string, string, string) {
|
|
bpjsConfig := BpjsConfig{
|
|
ConsID: cfg.Cons_id,
|
|
SecretKey: cfg.Secret_key,
|
|
UserKey: cfg.User_key,
|
|
}
|
|
return bpjsConfig.SetHeader()
|
|
}
|
|
|
|
func LoadConfig() *Config {
|
|
log.Printf("DEBUG: Raw ENV for SECURITY_MAX_INPUT_LENGTH is: '%s'", os.Getenv("SECURITY_MAX_INPUT_LENGTH"))
|
|
config := &Config{
|
|
Server: ServerConfig{
|
|
Port: getEnvAsInt("PORT", 8080),
|
|
Mode: getEnv("GIN_MODE", "debug"),
|
|
},
|
|
GRPC: GRPCConfig{
|
|
Port: getEnvAsInt("GRPC_PORT", 50051),
|
|
Enabled: getEnvAsBool("GRPC_ENABLED", true),
|
|
},
|
|
Databases: make(map[string]DatabaseConfig),
|
|
ReadReplicas: make(map[string][]DatabaseConfig),
|
|
Auth: loadAuthConfig(),
|
|
Keycloak: loadKeycloakConfig(),
|
|
Bpjs: BpjsConfig{
|
|
BaseURL: getEnv("BPJS_BASEURL", "https://apijkn.bpjs-kesehatan.go.id"),
|
|
ConsID: getEnv("BPJS_CONSID", ""),
|
|
UserKey: getEnv("BPJS_USERKEY", ""),
|
|
SecretKey: getEnv("BPJS_SECRETKEY", ""),
|
|
Timeout: parseDuration(getEnv("BPJS_TIMEOUT", "30s")),
|
|
},
|
|
SatuSehat: SatuSehatConfig{
|
|
OrgID: getEnv("BRIDGING_SATUSEHAT_ORG_ID", ""),
|
|
FasyakesID: getEnv("BRIDGING_SATUSEHAT_FASYAKES_ID", ""),
|
|
ClientID: getEnv("BRIDGING_SATUSEHAT_CLIENT_ID", ""),
|
|
ClientSecret: getEnv("BRIDGING_SATUSEHAT_CLIENT_SECRET", ""),
|
|
AuthURL: getEnv("BRIDGING_SATUSEHAT_AUTH_URL", "https://api-satusehat.kemkes.go.id/oauth2/v1"),
|
|
BaseURL: getEnv("BRIDGING_SATUSEHAT_BASE_URL", "https://api-satusehat.kemkes.go.id/fhir-r4/v1"),
|
|
ConsentURL: getEnv("BRIDGING_SATUSEHAT_CONSENT_URL", "https://api-satusehat.dto.kemkes.go.id/consent/v1"),
|
|
KFAURL: getEnv("BRIDGING_SATUSEHAT_KFA_URL", "https://api-satusehat.kemkes.go.id/kfa-v2"),
|
|
Timeout: parseDuration(getEnv("BRIDGING_SATUSEHAT_TIMEOUT", "30s")),
|
|
},
|
|
Swagger: SwaggerConfig{
|
|
Title: getEnv("SWAGGER_TITLE", "SERVICE API"),
|
|
Description: getEnv("SWAGGER_DESCRIPTION", "CUSTUM SERVICE API"),
|
|
Version: getEnv("SWAGGER_VERSION", "1.0.0"),
|
|
TermsOfService: getEnv("SWAGGER_TERMS_OF_SERVICE", "http://swagger.io/terms/"),
|
|
ContactName: getEnv("SWAGGER_CONTACT_NAME", "API Support"),
|
|
ContactURL: getEnv("SWAGGER_CONTACT_URL", "http://rssa.example.com/support"),
|
|
ContactEmail: getEnv("SWAGGER_CONTACT_EMAIL", "support@swagger.io"),
|
|
LicenseName: getEnv("SWAGGER_LICENSE_NAME", "Apache 2.0"),
|
|
LicenseURL: getEnv("SWAGGER_LICENSE_URL", "http://www.apache.org/licenses/LICENSE-2.0.html"),
|
|
Host: getEnv("SWAGGER_HOST", "localhost:8080"),
|
|
BasePath: getEnv("SWAGGER_BASE_PATH", "/api/v1"),
|
|
Schemes: parseSchemes(getEnv("SWAGGER_SCHEMES", "http,https")),
|
|
},
|
|
Security: SecurityConfig{
|
|
TrustedOrigins: parseOrigins(getEnv("SECURITY_TRUSTED_ORIGINS", "http://localhost:3000,http://localhost,http://localhost:8080,http://10.10.150.207:3001,http://10.10.150.114:3000,http://10.10.150.175:3000,http://192.168.18.7:3001,http://localhost:3001")),
|
|
MaxInputLength: getEnvAsInt("SECURITY_MAX_INPUT_LENGTH", 500),
|
|
RateLimit: RateLimitConfig{
|
|
RequestsPerMinute: getEnvAsInt("RATE_LIMIT_REQUESTS_PER_MINUTE", 230),
|
|
Redis: RedisConfig{
|
|
Host: getEnv("REDIS_HOST", "localhost"),
|
|
Port: getEnvAsInt("REDIS_PORT", 6379),
|
|
Password: getEnv("REDIS_PASSWORD", ""),
|
|
DB: getEnvAsInt("REDIS_DB", 0),
|
|
},
|
|
},
|
|
SanitizeQueries: getEnvAsBool("SECURITY_SANITIZE_QUERIES", true),
|
|
RequireSecureConnections: getEnvAsBool("SECURITY_REQUIRE_SECURE_CONNECTIONS", false),
|
|
},
|
|
}
|
|
log.Printf("DEBUG: Final Config Object. MaxInputLength is: %d", config.Security.MaxInputLength)
|
|
// Initialize validator
|
|
config.Validator = validator.New()
|
|
|
|
// Load database configurations
|
|
config.loadDatabaseConfigs()
|
|
|
|
// Load read replica configurations
|
|
config.loadReadReplicaConfigs()
|
|
|
|
log.Printf("DEBUG [LoadConfig]: Config object created at address: %p", config)
|
|
log.Printf("DEBUG [LoadConfig]: Security.MaxInputLength is: %d", config.Security.MaxInputLength)
|
|
return config
|
|
}
|
|
|
|
func loadAuthConfig() AuthConfig {
|
|
// --- AWAL TAMBAHAN DEBUG ---
|
|
// Cetak direktori kerja saat ini untuk debugging
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
log.Printf("Error getting working directory: %v", err)
|
|
} else {
|
|
log.Printf("DEBUG: Current working directory is: %s", wd)
|
|
}
|
|
// --- AKHIR TAMBAHAN DEBUG ---
|
|
|
|
authConfig := AuthConfig{
|
|
Type: "jwt", // default to jwt for backward compatibility
|
|
FallbackTo: "",
|
|
StaticTokens: []string{},
|
|
}
|
|
|
|
// Path file yang akan dibaca
|
|
configPath := "internal/config/config.yaml"
|
|
log.Printf("DEBUG: Attempting to read auth config from: %s", configPath)
|
|
|
|
// Load auth configuration from config.yaml first
|
|
if data, err := os.ReadFile(configPath); err == nil {
|
|
log.Printf("DEBUG: Successfully read config.yaml file. Parsing...") // Tambahkan log sukses
|
|
|
|
var yamlConfig struct {
|
|
Auth AuthYAMLConfig `yaml:"auth"`
|
|
}
|
|
if err := yaml.Unmarshal(data, &yamlConfig); err == nil {
|
|
// Log nilai yang berhasil dibaca
|
|
log.Printf("DEBUG: Parsed YAML. Type: '%s', Tokens: %d", yamlConfig.Auth.Type, len(yamlConfig.Auth.StaticTokens))
|
|
|
|
authConfig.Type = yamlConfig.Auth.Type
|
|
authConfig.FallbackTo = yamlConfig.Auth.FallbackTo
|
|
authConfig.StaticTokens = yamlConfig.Auth.StaticTokens
|
|
} else {
|
|
log.Printf("ERROR: Failed to unmarshal YAML: %v", err)
|
|
}
|
|
} else {
|
|
// --- AWAL TAMBAHAN DEBUG ---
|
|
// Cetak error spesifik jika file tidak ditemukan
|
|
log.Printf("ERROR: Could not read config file at '%s': %v", configPath, err)
|
|
// --- AKHIR TAMBAHAN DEBUG ---
|
|
}
|
|
|
|
// Then override with environment variables if set
|
|
if envType := getEnv("AUTH_TYPE", ""); envType != "" {
|
|
log.Printf("DEBUG: Overriding auth type with environment variable: %s", envType)
|
|
authConfig.Type = envType
|
|
}
|
|
if envFallback := getEnv("AUTH_FALLBACK_TO", ""); envFallback != "" {
|
|
authConfig.FallbackTo = envFallback
|
|
}
|
|
envTokens := parseStaticTokens(getEnv("AUTH_STATIC_TOKENS", ""))
|
|
if len(envTokens) > 0 {
|
|
authConfig.StaticTokens = envTokens
|
|
}
|
|
|
|
// Log hasil akhir sebelum dikembalikan
|
|
log.Printf("DEBUG: Final AuthConfig before returning: Type='%s', TokenCount=%d", authConfig.Type, len(authConfig.StaticTokens))
|
|
|
|
return authConfig
|
|
}
|
|
|
|
// Lakukan hal yang sama untuk loadKeycloakConfig
|
|
func loadKeycloakConfig() KeycloakConfig {
|
|
// --- AWAL TAMBAHAN DEBUG ---
|
|
// Cetak direktori kerja saat ini untuk debugging
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
log.Printf("Error getting working directory for keycloak config: %v", err)
|
|
} else {
|
|
log.Printf("DEBUG (Keycloak): Current working directory is: %s", wd)
|
|
}
|
|
// --- AKHIR TAMBAHAN DEBUG ---
|
|
|
|
v := viper.New()
|
|
v.SetConfigName("config")
|
|
v.SetConfigType("yaml")
|
|
v.AddConfigPath(".")
|
|
v.AddConfigPath("./config")
|
|
v.AddConfigPath("./internal/config")
|
|
|
|
// --- AWAL TAMBAHAN DEBUG ---
|
|
log.Printf("DEBUG (Keycloak): Viper is set to search for config in: '.', './config', './internal/config'")
|
|
// --- AKHIR TAMBAHAN DEBUG ---
|
|
|
|
if err := v.ReadInConfig(); err == nil {
|
|
// Log jika file berhasil ditemukan dan dibaca
|
|
log.Printf("DEBUG (Keycloak): Successfully read config file: %s", v.ConfigFileUsed())
|
|
|
|
keycloakConfig := KeycloakConfig{
|
|
Issuer: v.GetString("keycloak.issuer"),
|
|
Audience: v.GetString("keycloak.audience"),
|
|
JwksURL: v.GetString("keycloak.jwks_url"),
|
|
Enabled: v.GetBool("keycloak.enabled"),
|
|
}
|
|
|
|
// Log nilai yang berhasil dibaca dari file
|
|
log.Printf("DEBUG (Keycloak): Parsed values from file. Issuer: '%s', Enabled: %t", keycloakConfig.Issuer, keycloakConfig.Enabled)
|
|
|
|
log.Printf("Loaded keycloak config from file: enabled=%t", keycloakConfig.Enabled)
|
|
return keycloakConfig
|
|
} else {
|
|
// --- AWAL TAMBAHAN DEBUG ---
|
|
// Cetak error spesifik jika file tidak ditemukan
|
|
log.Printf("ERROR (Keycloak): Could not read config file: %v", err)
|
|
// --- AKHIR TAMBAHAN DEBUG ---
|
|
}
|
|
|
|
// Fallback ke environment variable
|
|
log.Printf("DEBUG (Keycloak): Falling back to environment variables.")
|
|
fallbackConfig := KeycloakConfig{
|
|
Issuer: getEnv("KEYCLOAK_ISSUER", ""),
|
|
Audience: getEnv("KEYCLOAK_AUDIENCE", ""),
|
|
JwksURL: getEnv("KEYCLOAK_JWKS_URL", ""),
|
|
Enabled: getEnvAsBool("KEYCLOAK_ENABLED", false),
|
|
}
|
|
|
|
// Log hasil akhir dari fallback
|
|
log.Printf("DEBUG (Keycloak): Final fallback config. Issuer: '%s', Enabled: %t", fallbackConfig.Issuer, fallbackConfig.Enabled)
|
|
|
|
return fallbackConfig
|
|
}
|
|
|
|
func (c *Config) loadDatabaseConfigs() {
|
|
// Load PostgreSQL configurations
|
|
// c.addPostgreSQLConfigs()
|
|
|
|
// // Load MySQL configurations
|
|
// c.addMySQLConfigs()
|
|
|
|
// // Load MongoDB configurations
|
|
// c.addMongoDBConfigs()
|
|
|
|
// // Load SQLite configurations
|
|
// c.addSQLiteConfigs()
|
|
|
|
// Load custom database configurations from environment variables
|
|
c.loadCustomDatabaseConfigs()
|
|
|
|
// Remove duplicate database configurations
|
|
// c.removeDuplicateDatabases()
|
|
}
|
|
|
|
func (c *Config) removeDuplicateDatabases() {
|
|
// Create a map to track unique database connections
|
|
uniqueDBs := make(map[string]DatabaseConfig)
|
|
duplicates := make(map[string][]string)
|
|
|
|
// First pass: identify duplicates
|
|
for name, config := range c.Databases {
|
|
// Create a unique key based on connection parameters
|
|
key := fmt.Sprintf("%s:%s:%d:%s", config.Type, config.Host, config.Port, config.Database)
|
|
|
|
if existing, exists := uniqueDBs[key]; exists {
|
|
// Found a duplicate
|
|
if duplicates[key] == nil {
|
|
duplicates[key] = []string{existing.Name}
|
|
}
|
|
duplicates[key] = append(duplicates[key], name)
|
|
log.Printf("⚠️ Database %s is a duplicate of %s (same connection parameters)", name, existing.Name)
|
|
} else {
|
|
uniqueDBs[key] = config
|
|
}
|
|
}
|
|
|
|
// Second pass: remove duplicates, keeping the first one
|
|
for _, dupNames := range duplicates {
|
|
// Keep the first database name, remove the rest
|
|
keepName := dupNames[0]
|
|
for i := 1; i < len(dupNames); i++ {
|
|
removeName := dupNames[i]
|
|
delete(c.Databases, removeName)
|
|
log.Printf("🗑️ Removed duplicate database configuration: %s (kept: %s)", removeName, keepName)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Config) loadCustomDatabaseConfigs() {
|
|
envVars := os.Environ()
|
|
dbConfigs := make(map[string]map[string]string)
|
|
|
|
// Parse database configurations from environment variables
|
|
for _, envVar := range envVars {
|
|
parts := strings.SplitN(envVar, "=", 2)
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
|
|
key := parts[0]
|
|
value := parts[1]
|
|
|
|
// Parse specific database configurations
|
|
if strings.HasSuffix(key, "_CONNECTION") || strings.HasSuffix(key, "_HOST") ||
|
|
strings.HasSuffix(key, "_DATABASE") || strings.HasSuffix(key, "_USERNAME") ||
|
|
strings.HasSuffix(key, "_PASSWORD") || strings.HasSuffix(key, "_PORT") ||
|
|
strings.HasSuffix(key, "_NAME") {
|
|
|
|
segments := strings.Split(key, "_")
|
|
if len(segments) >= 2 {
|
|
dbName := strings.ToLower(strings.Join(segments[:len(segments)-1], "_"))
|
|
property := strings.ToLower(segments[len(segments)-1])
|
|
|
|
if dbConfigs[dbName] == nil {
|
|
dbConfigs[dbName] = make(map[string]string)
|
|
}
|
|
dbConfigs[dbName][property] = value
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create DatabaseConfig from parsed configurations for additional databases
|
|
for name, config := range dbConfigs {
|
|
// Skip empty configurations or system configurations
|
|
if name == "" || strings.Contains(name, "chrome_crashpad_pipe") || name == "primary" {
|
|
continue
|
|
}
|
|
|
|
dbType := getEnvFromMap(config, "connection", getEnvFromMap(config, "type", "postgres"))
|
|
|
|
// Skip if username is empty and it's not a system config
|
|
username := getEnvFromMap(config, "username", "")
|
|
if username == "" && !strings.HasPrefix(name, "chrome") {
|
|
continue
|
|
}
|
|
|
|
dbConfig := DatabaseConfig{
|
|
Name: name,
|
|
Type: dbType,
|
|
Host: getEnvFromMap(config, "host", "localhost"),
|
|
Port: getEnvAsIntFromMap(config, "port", getDefaultPort(dbType)),
|
|
Username: username,
|
|
Password: getEnvFromMap(config, "password", ""),
|
|
Database: getEnvFromMap(config, "database", getEnvFromMap(config, "name", name)),
|
|
Schema: getEnvFromMap(config, "schema", getDefaultSchema(dbType)),
|
|
SSLMode: getEnvFromMap(config, "sslmode", getDefaultSSLMode(dbType)),
|
|
Path: getEnvFromMap(config, "path", ""),
|
|
Options: getEnvFromMap(config, "options", ""),
|
|
MaxOpenConns: getEnvAsIntFromMap(config, "max_open_conns", getDefaultMaxOpenConns(dbType)),
|
|
MaxIdleConns: getEnvAsIntFromMap(config, "max_idle_conns", getDefaultMaxIdleConns(dbType)),
|
|
ConnMaxLifetime: parseDuration(getEnvFromMap(config, "conn_max_lifetime", getDefaultConnMaxLifetime(dbType))),
|
|
// Security settings
|
|
RequireSSL: getEnvAsBoolFromMap(config, "require_ssl", false),
|
|
SSLRootCert: getEnvFromMap(config, "ssl_root_cert", ""),
|
|
SSLCert: getEnvFromMap(config, "ssl_cert", ""),
|
|
SSLKey: getEnvFromMap(config, "ssl_key", ""),
|
|
Timeout: parseDuration(getEnvFromMap(config, "timeout", "30s")),
|
|
ConnectTimeout: parseDuration(getEnvFromMap(config, "connect_timeout", "10s")),
|
|
ReadTimeout: parseDuration(getEnvFromMap(config, "read_timeout", "30s")),
|
|
WriteTimeout: parseDuration(getEnvFromMap(config, "write_timeout", "30s")),
|
|
StatementTimeout: parseDuration(getEnvFromMap(config, "statement_timeout", "120s")),
|
|
// Connection pool settings
|
|
MaxLifetime: parseDuration(getEnvFromMap(config, "max_lifetime", "1h")),
|
|
MaxIdleTime: parseDuration(getEnvFromMap(config, "max_idle_time", "5m")),
|
|
HealthCheckPeriod: parseDuration(getEnvFromMap(config, "health_check_period", "1m")),
|
|
}
|
|
|
|
c.Databases[name] = dbConfig
|
|
}
|
|
}
|
|
|
|
func (c *Config) loadReadReplicaConfigs() {
|
|
envVars := os.Environ()
|
|
|
|
for _, envVar := range envVars {
|
|
parts := strings.SplitN(envVar, "=", 2)
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
|
|
key := parts[0]
|
|
value := parts[1]
|
|
|
|
// Parse read replica configurations (format: [DBNAME]_REPLICA_[INDEX]_[PROPERTY])
|
|
if strings.Contains(key, "_REPLICA_") {
|
|
segments := strings.Split(key, "_")
|
|
if len(segments) >= 5 && strings.ToUpper(segments[2]) == "REPLICA" {
|
|
dbName := strings.ToLower(segments[1])
|
|
replicaIndex := segments[3]
|
|
property := strings.ToLower(strings.Join(segments[4:], "_"))
|
|
|
|
replicaKey := dbName + "_replica_" + replicaIndex
|
|
|
|
if c.ReadReplicas[dbName] == nil {
|
|
c.ReadReplicas[dbName] = []DatabaseConfig{}
|
|
}
|
|
|
|
// Find or create replica config
|
|
var replicaConfig *DatabaseConfig
|
|
for i := range c.ReadReplicas[dbName] {
|
|
if c.ReadReplicas[dbName][i].Name == replicaKey {
|
|
replicaConfig = &c.ReadReplicas[dbName][i]
|
|
break
|
|
}
|
|
}
|
|
|
|
if replicaConfig == nil {
|
|
// Get primary DB config as base
|
|
primaryDB, exists := c.Databases[dbName]
|
|
if !exists {
|
|
log.Printf("Warning: Primary database %s not found for replica configuration", dbName)
|
|
continue
|
|
}
|
|
|
|
// Create new replica config based on primary
|
|
newConfig := DatabaseConfig{
|
|
Name: replicaKey,
|
|
Type: primaryDB.Type,
|
|
Host: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_HOST", primaryDB.Host),
|
|
Port: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_PORT", primaryDB.Port),
|
|
Username: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_USERNAME", primaryDB.Username),
|
|
Password: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_PASSWORD", primaryDB.Password),
|
|
Database: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_DATABASE", primaryDB.Database),
|
|
Schema: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SCHEMA", primaryDB.Schema),
|
|
SSLMode: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SSLMODE", primaryDB.SSLMode),
|
|
MaxOpenConns: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_OPEN_CONNS", primaryDB.MaxOpenConns),
|
|
MaxIdleConns: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_IDLE_CONNS", primaryDB.MaxIdleConns),
|
|
ConnMaxLifetime: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_CONN_MAX_LIFETIME", primaryDB.ConnMaxLifetime.String())),
|
|
// Security settings
|
|
RequireSSL: getEnvAsBool("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_REQUIRE_SSL", primaryDB.RequireSSL),
|
|
SSLRootCert: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SSL_ROOT_CERT", primaryDB.SSLRootCert),
|
|
SSLCert: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SSL_CERT", primaryDB.SSLCert),
|
|
SSLKey: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SSL_KEY", primaryDB.SSLKey),
|
|
Timeout: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_TIMEOUT", primaryDB.Timeout.String())),
|
|
ConnectTimeout: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_CONNECT_TIMEOUT", primaryDB.ConnectTimeout.String())),
|
|
ReadTimeout: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_READ_TIMEOUT", primaryDB.ReadTimeout.String())),
|
|
WriteTimeout: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_WRITE_TIMEOUT", primaryDB.WriteTimeout.String())),
|
|
StatementTimeout: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_STATEMENT_TIMEOUT", primaryDB.StatementTimeout.String())),
|
|
// Connection pool settings
|
|
MaxLifetime: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_LIFETIME", primaryDB.MaxLifetime.String())),
|
|
MaxIdleTime: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_IDLE_TIME", primaryDB.MaxIdleTime.String())),
|
|
HealthCheckPeriod: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_HEALTH_CHECK_PERIOD", primaryDB.HealthCheckPeriod.String())),
|
|
}
|
|
c.ReadReplicas[dbName] = append(c.ReadReplicas[dbName], newConfig)
|
|
replicaConfig = &c.ReadReplicas[dbName][len(c.ReadReplicas[dbName])-1]
|
|
}
|
|
|
|
// Update the specific replica
|
|
switch property {
|
|
case "host":
|
|
replicaConfig.Host = value
|
|
case "port":
|
|
replicaConfig.Port = getEnvAsInt(key, 5432)
|
|
case "username":
|
|
replicaConfig.Username = value
|
|
case "password":
|
|
replicaConfig.Password = value
|
|
case "database":
|
|
replicaConfig.Database = value
|
|
case "schema":
|
|
replicaConfig.Schema = value
|
|
case "sslmode":
|
|
replicaConfig.SSLMode = value
|
|
case "max_open_conns":
|
|
replicaConfig.MaxOpenConns = getEnvAsInt(key, 25)
|
|
case "max_idle_conns":
|
|
replicaConfig.MaxIdleConns = getEnvAsInt(key, 25)
|
|
case "conn_max_lifetime":
|
|
replicaConfig.ConnMaxLifetime = parseDuration(value)
|
|
case "require_ssl":
|
|
replicaConfig.RequireSSL = getEnvAsBool(key, false)
|
|
case "ssl_root_cert":
|
|
replicaConfig.SSLRootCert = value
|
|
case "ssl_cert":
|
|
replicaConfig.SSLCert = value
|
|
case "ssl_key":
|
|
replicaConfig.SSLKey = value
|
|
case "timeout":
|
|
replicaConfig.Timeout = parseDuration(value)
|
|
case "connect_timeout":
|
|
replicaConfig.ConnectTimeout = parseDuration(value)
|
|
case "read_timeout":
|
|
replicaConfig.ReadTimeout = parseDuration(value)
|
|
case "write_timeout":
|
|
replicaConfig.WriteTimeout = parseDuration(value)
|
|
case "statement_timeout":
|
|
replicaConfig.StatementTimeout = parseDuration(value)
|
|
case "max_lifetime":
|
|
replicaConfig.MaxLifetime = parseDuration(value)
|
|
case "max_idle_time":
|
|
replicaConfig.MaxIdleTime = parseDuration(value)
|
|
case "health_check_period":
|
|
replicaConfig.HealthCheckPeriod = parseDuration(value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Config) addSpecificDatabase(prefix, defaultType string) {
|
|
connection := getEnv(strings.ToUpper(prefix)+"_CONNECTION", defaultType)
|
|
host := getEnv(strings.ToUpper(prefix)+"_HOST", "")
|
|
if host != "" {
|
|
dbConfig := DatabaseConfig{
|
|
Name: prefix,
|
|
Type: connection,
|
|
Host: host,
|
|
Port: getEnvAsInt(strings.ToUpper(prefix)+"_PORT", getDefaultPort(connection)),
|
|
Username: getEnv(strings.ToUpper(prefix)+"_USERNAME", ""),
|
|
Password: getEnv(strings.ToUpper(prefix)+"_PASSWORD", ""),
|
|
Database: getEnv(strings.ToUpper(prefix)+"_DATABASE", getEnv(strings.ToUpper(prefix)+"_NAME", prefix)),
|
|
Schema: getEnv(strings.ToUpper(prefix)+"_SCHEMA", getDefaultSchema(connection)),
|
|
SSLMode: getEnv(strings.ToUpper(prefix)+"_SSLMODE", getDefaultSSLMode(connection)),
|
|
MaxOpenConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_OPEN_CONNS", getDefaultMaxOpenConns(connection)),
|
|
MaxIdleConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_IDLE_CONNS", getDefaultMaxIdleConns(connection)),
|
|
ConnMaxLifetime: parseDuration(getEnv(strings.ToUpper(prefix)+"_CONN_MAX_LIFETIME", getDefaultConnMaxLifetime(connection))),
|
|
// Security settings
|
|
RequireSSL: getEnvAsBool(strings.ToUpper(prefix)+"_REQUIRE_SSL", false),
|
|
SSLRootCert: getEnv(strings.ToUpper(prefix)+"_SSL_ROOT_CERT", ""),
|
|
SSLCert: getEnv(strings.ToUpper(prefix)+"_SSL_CERT", ""),
|
|
SSLKey: getEnv(strings.ToUpper(prefix)+"_SSL_KEY", ""),
|
|
Timeout: parseDuration(getEnv(strings.ToUpper(prefix)+"_TIMEOUT", "30s")),
|
|
ConnectTimeout: parseDuration(getEnv(strings.ToUpper(prefix)+"_CONNECT_TIMEOUT", "10s")),
|
|
ReadTimeout: parseDuration(getEnv(strings.ToUpper(prefix)+"_READ_TIMEOUT", "30s")),
|
|
WriteTimeout: parseDuration(getEnv(strings.ToUpper(prefix)+"_WRITE_TIMEOUT", "30s")),
|
|
StatementTimeout: parseDuration(getEnv(strings.ToUpper(prefix)+"_STATEMENT_TIMEOUT", "120s")),
|
|
// Connection pool settings
|
|
MaxLifetime: parseDuration(getEnv(strings.ToUpper(prefix)+"_MAX_LIFETIME", "1h")),
|
|
MaxIdleTime: parseDuration(getEnv(strings.ToUpper(prefix)+"_MAX_IDLE_TIME", "5m")),
|
|
HealthCheckPeriod: parseDuration(getEnv(strings.ToUpper(prefix)+"_HEALTH_CHECK_PERIOD", "1m")),
|
|
}
|
|
c.Databases[prefix] = dbConfig
|
|
}
|
|
}
|
|
|
|
// PostgreSQL database
|
|
func (c *Config) addPostgreSQLConfigs() {
|
|
// Support for custom PostgreSQL configurations with POSTGRES_ prefix
|
|
envVars := os.Environ()
|
|
for _, envVar := range envVars {
|
|
parts := strings.SplitN(envVar, "=", 2)
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
|
|
key := parts[0]
|
|
// Parse PostgreSQL configurations (format: POSTGRES_[NAME]_[PROPERTY])
|
|
if strings.HasPrefix(key, "POSTGRES_") && strings.Contains(key, "_") {
|
|
segments := strings.Split(key, "_")
|
|
if len(segments) >= 3 {
|
|
dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_"))
|
|
|
|
// Skip if it's a standard PostgreSQL configuration
|
|
if dbName == "connection" || dbName == "dev" || dbName == "default" || dbName == "satudata" {
|
|
continue
|
|
}
|
|
|
|
// Create or update PostgreSQL configuration
|
|
if _, exists := c.Databases[dbName]; !exists {
|
|
c.Databases[dbName] = DatabaseConfig{
|
|
Name: dbName,
|
|
Type: "postgres",
|
|
Host: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_HOST", "localhost"),
|
|
Port: getEnvAsInt("POSTGRES_"+strings.ToUpper(dbName)+"_PORT", 5432),
|
|
Username: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_USERNAME", ""),
|
|
Password: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_PASSWORD", ""),
|
|
Database: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_DATABASE", dbName),
|
|
Schema: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SCHEMA", "public"),
|
|
SSLMode: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"),
|
|
MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25),
|
|
MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25),
|
|
ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")),
|
|
// Security settings
|
|
RequireSSL: getEnvAsBool("POSTGRES_"+strings.ToUpper(dbName)+"_REQUIRE_SSL", false),
|
|
SSLRootCert: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SSL_ROOT_CERT", ""),
|
|
SSLCert: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SSL_CERT", ""),
|
|
SSLKey: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SSL_KEY", ""),
|
|
Timeout: parseDuration(getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_TIMEOUT", "30s")),
|
|
ConnectTimeout: parseDuration(getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_CONNECT_TIMEOUT", "10s")),
|
|
ReadTimeout: parseDuration(getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_READ_TIMEOUT", "30s")),
|
|
WriteTimeout: parseDuration(getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_WRITE_TIMEOUT", "30s")),
|
|
StatementTimeout: parseDuration(getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_STATEMENT_TIMEOUT", "120s")),
|
|
// Connection pool settings
|
|
MaxLifetime: parseDuration(getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_MAX_LIFETIME", "1h")),
|
|
MaxIdleTime: parseDuration(getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_MAX_IDLE_TIME", "5m")),
|
|
HealthCheckPeriod: parseDuration(getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_HEALTH_CHECK_PERIOD", "1m")),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// addMYSQLConfigs adds MYSQL database
|
|
func (c *Config) addMySQLConfigs() {
|
|
// Primary MySQL configuration
|
|
defaultMySQLHost := getEnv("MYSQL_HOST", "")
|
|
if defaultMySQLHost != "" {
|
|
c.Databases["mysql"] = DatabaseConfig{
|
|
Name: "mysql",
|
|
Type: getEnv("MYSQL_CONNECTION", "mysql"),
|
|
Host: defaultMySQLHost,
|
|
Port: getEnvAsInt("MYSQL_PORT", 3306),
|
|
Username: getEnv("MYSQL_USERNAME", ""),
|
|
Password: getEnv("MYSQL_PASSWORD", ""),
|
|
Database: getEnv("MYSQL_DATABASE", "mysql"),
|
|
SSLMode: getEnv("MYSQL_SSLMODE", "disable"),
|
|
MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25),
|
|
MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25),
|
|
ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")),
|
|
// Security settings
|
|
RequireSSL: getEnvAsBool("MYSQL_REQUIRE_SSL", false),
|
|
SSLRootCert: getEnv("MYSQL_SSL_ROOT_CERT", ""),
|
|
SSLCert: getEnv("MYSQL_SSL_CERT", ""),
|
|
SSLKey: getEnv("MYSQL_SSL_KEY", ""),
|
|
Timeout: parseDuration(getEnv("MYSQL_TIMEOUT", "30s")),
|
|
ConnectTimeout: parseDuration(getEnv("MYSQL_CONNECT_TIMEOUT", "10s")),
|
|
ReadTimeout: parseDuration(getEnv("MYSQL_READ_TIMEOUT", "30s")),
|
|
WriteTimeout: parseDuration(getEnv("MYSQL_WRITE_TIMEOUT", "30s")),
|
|
// Connection pool settings
|
|
MaxLifetime: parseDuration(getEnv("MYSQL_MAX_LIFETIME", "1h")),
|
|
MaxIdleTime: parseDuration(getEnv("MYSQL_MAX_IDLE_TIME", "5m")),
|
|
HealthCheckPeriod: parseDuration(getEnv("MYSQL_HEALTH_CHECK_PERIOD", "1m")),
|
|
}
|
|
}
|
|
|
|
// Support for custom MySQL configurations with MYSQL_ prefix
|
|
envVars := os.Environ()
|
|
for _, envVar := range envVars {
|
|
parts := strings.SplitN(envVar, "=", 2)
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
|
|
key := parts[0]
|
|
// Parse MySQL configurations (format: MYSQL_[NAME]_[PROPERTY])
|
|
if strings.HasPrefix(key, "MYSQL_") && strings.Contains(key, "_") {
|
|
segments := strings.Split(key, "_")
|
|
if len(segments) >= 3 {
|
|
dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_"))
|
|
|
|
// Skip if it's a standard MySQL configuration
|
|
if dbName == "connection" || dbName == "dev" || dbName == "max" || dbName == "conn" {
|
|
continue
|
|
}
|
|
|
|
// Create or update MySQL configuration
|
|
if _, exists := c.Databases[dbName]; !exists {
|
|
mysqlHost := getEnv("MYSQL_"+strings.ToUpper(dbName)+"_HOST", "")
|
|
if mysqlHost != "" {
|
|
c.Databases[dbName] = DatabaseConfig{
|
|
Name: dbName,
|
|
Type: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_CONNECTION", "mysql"),
|
|
Host: mysqlHost,
|
|
Port: getEnvAsInt("MYSQL_"+strings.ToUpper(dbName)+"_PORT", 3306),
|
|
Username: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_USERNAME", ""),
|
|
Password: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_PASSWORD", ""),
|
|
Database: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_DATABASE", dbName),
|
|
SSLMode: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"),
|
|
MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25),
|
|
MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25),
|
|
ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")),
|
|
// Security settings
|
|
RequireSSL: getEnvAsBool("MYSQL_"+strings.ToUpper(dbName)+"_REQUIRE_SSL", false),
|
|
SSLRootCert: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_SSL_ROOT_CERT", ""),
|
|
SSLCert: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_SSL_CERT", ""),
|
|
SSLKey: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_SSL_KEY", ""),
|
|
Timeout: parseDuration(getEnv("MYSQL_"+strings.ToUpper(dbName)+"_TIMEOUT", "30s")),
|
|
ConnectTimeout: parseDuration(getEnv("MYSQL_"+strings.ToUpper(dbName)+"_CONNECT_TIMEOUT", "10s")),
|
|
ReadTimeout: parseDuration(getEnv("MYSQL_"+strings.ToUpper(dbName)+"_READ_TIMEOUT", "30s")),
|
|
WriteTimeout: parseDuration(getEnv("MYSQL_"+strings.ToUpper(dbName)+"_WRITE_TIMEOUT", "30s")),
|
|
// Connection pool settings
|
|
MaxLifetime: parseDuration(getEnv("MYSQL_"+strings.ToUpper(dbName)+"_MAX_LIFETIME", "1h")),
|
|
MaxIdleTime: parseDuration(getEnv("MYSQL_"+strings.ToUpper(dbName)+"_MAX_IDLE_TIME", "5m")),
|
|
HealthCheckPeriod: parseDuration(getEnv("MYSQL_"+strings.ToUpper(dbName)+"_HEALTH_CHECK_PERIOD", "1m")),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// addMongoDBConfigs adds MongoDB database configurations from environment variables
|
|
func (c *Config) addMongoDBConfigs() {
|
|
// Primary MongoDB configuration
|
|
mongoHost := getEnv("MONGODB_HOST", "")
|
|
if mongoHost != "" {
|
|
c.Databases["mongodb"] = DatabaseConfig{
|
|
Name: "mongodb",
|
|
Type: getEnv("MONGODB_CONNECTION", "mongodb"),
|
|
Host: mongoHost,
|
|
Port: getEnvAsInt("MONGODB_PORT", 27017),
|
|
Username: getEnv("MONGODB_USER", ""),
|
|
Password: getEnv("MONGODB_PASS", ""),
|
|
Database: getEnv("MONGODB_MASTER", "master"),
|
|
SSLMode: getEnv("MONGODB_SSLMODE", "disable"),
|
|
MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100),
|
|
MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10),
|
|
ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")),
|
|
// Security settings
|
|
RequireSSL: getEnvAsBool("MONGODB_REQUIRE_SSL", false),
|
|
SSLRootCert: getEnv("MONGODB_SSL_ROOT_CERT", ""),
|
|
SSLCert: getEnv("MONGODB_SSL_CERT", ""),
|
|
SSLKey: getEnv("MONGODB_SSL_KEY", ""),
|
|
Timeout: parseDuration(getEnv("MONGODB_TIMEOUT", "30s")),
|
|
ConnectTimeout: parseDuration(getEnv("MONGODB_CONNECT_TIMEOUT", "10s")),
|
|
ReadTimeout: parseDuration(getEnv("MONGODB_READ_TIMEOUT", "30s")),
|
|
WriteTimeout: parseDuration(getEnv("MONGODB_WRITE_TIMEOUT", "30s")),
|
|
// Connection pool settings
|
|
MaxLifetime: parseDuration(getEnv("MONGODB_MAX_LIFETIME", "1h")),
|
|
MaxIdleTime: parseDuration(getEnv("MONGODB_MAX_IDLE_TIME", "5m")),
|
|
HealthCheckPeriod: parseDuration(getEnv("MONGODB_HEALTH_CHECK_PERIOD", "1m")),
|
|
}
|
|
}
|
|
|
|
// Additional MongoDB configurations for local database
|
|
mongoLocalHost := getEnv("MONGODB_LOCAL_HOST", "")
|
|
if mongoLocalHost != "" {
|
|
c.Databases["mongodb_local"] = DatabaseConfig{
|
|
Name: "mongodb_local",
|
|
Type: getEnv("MONGODB_CONNECTION", "mongodb"),
|
|
Host: mongoLocalHost,
|
|
Port: getEnvAsInt("MONGODB_LOCAL_PORT", 27017),
|
|
Username: getEnv("MONGODB_LOCAL_USER", ""),
|
|
Password: getEnv("MONGODB_LOCAL_PASS", ""),
|
|
Database: getEnv("MONGODB_LOCAL_DB", "local"),
|
|
SSLMode: getEnv("MONGOD_SSLMODE", "disable"),
|
|
MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100),
|
|
MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10),
|
|
ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")),
|
|
// Security settings
|
|
RequireSSL: getEnvAsBool("MONGODB_LOCAL_REQUIRE_SSL", false),
|
|
SSLRootCert: getEnv("MONGODB_LOCAL_SSL_ROOT_CERT", ""),
|
|
SSLCert: getEnv("MONGODB_LOCAL_SSL_CERT", ""),
|
|
SSLKey: getEnv("MONGODB_LOCAL_SSL_KEY", ""),
|
|
Timeout: parseDuration(getEnv("MONGODB_LOCAL_TIMEOUT", "30s")),
|
|
ConnectTimeout: parseDuration(getEnv("MONGODB_LOCAL_CONNECT_TIMEOUT", "10s")),
|
|
ReadTimeout: parseDuration(getEnv("MONGODB_LOCAL_READ_TIMEOUT", "30s")),
|
|
WriteTimeout: parseDuration(getEnv("MONGODB_LOCAL_WRITE_TIMEOUT", "30s")),
|
|
// Connection pool settings
|
|
MaxLifetime: parseDuration(getEnv("MONGODB_LOCAL_MAX_LIFETIME", "1h")),
|
|
MaxIdleTime: parseDuration(getEnv("MONGODB_LOCAL_MAX_IDLE_TIME", "5m")),
|
|
HealthCheckPeriod: parseDuration(getEnv("MONGODB_LOCAL_HEALTH_CHECK_PERIOD", "1m")),
|
|
}
|
|
}
|
|
|
|
// Support for custom MongoDB configurations with MONGODB_ prefix
|
|
envVars := os.Environ()
|
|
for _, envVar := range envVars {
|
|
parts := strings.SplitN(envVar, "=", 2)
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
|
|
key := parts[0]
|
|
// Parse MongoDB configurations (format: MONGODB_[NAME]_[PROPERTY])
|
|
if strings.HasPrefix(key, "MONGODB_") && strings.Contains(key, "_") {
|
|
segments := strings.Split(key, "_")
|
|
if len(segments) >= 3 {
|
|
dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_"))
|
|
// Skip if it's a standard MongoDB configuration
|
|
if dbName == "connection" || dbName == "dev" || dbName == "local" {
|
|
continue
|
|
}
|
|
|
|
// Create or update MongoDB configuration
|
|
if _, exists := c.Databases[dbName]; !exists {
|
|
c.Databases[dbName] = DatabaseConfig{
|
|
Name: dbName,
|
|
Type: "mongodb",
|
|
Host: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_HOST", "localhost"),
|
|
Port: getEnvAsInt("MONGODB_"+strings.ToUpper(dbName)+"_PORT", 27017),
|
|
Username: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_USER", ""),
|
|
Password: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_PASS", ""),
|
|
Database: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_DB", dbName),
|
|
SSLMode: getEnv("MONGOD_SSLMODE", "disable"),
|
|
MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100),
|
|
MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10),
|
|
ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")),
|
|
// Security settings
|
|
RequireSSL: getEnvAsBool("MONGODB_"+strings.ToUpper(dbName)+"_REQUIRE_SSL", false),
|
|
SSLRootCert: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_SSL_ROOT_CERT", ""),
|
|
SSLCert: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_SSL_CERT", ""),
|
|
SSLKey: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_SSL_KEY", ""),
|
|
Timeout: parseDuration(getEnv("MONGODB_"+strings.ToUpper(dbName)+"_TIMEOUT", "30s")),
|
|
ConnectTimeout: parseDuration(getEnv("MONGODB_"+strings.ToUpper(dbName)+"_CONNECT_TIMEOUT", "10s")),
|
|
ReadTimeout: parseDuration(getEnv("MONGODB_"+strings.ToUpper(dbName)+"_READ_TIMEOUT", "30s")),
|
|
WriteTimeout: parseDuration(getEnv("MONGODB_"+strings.ToUpper(dbName)+"_WRITE_TIMEOUT", "30s")),
|
|
// Connection pool settings
|
|
MaxLifetime: parseDuration(getEnv("MONGODB_"+strings.ToUpper(dbName)+"_MAX_LIFETIME", "1h")),
|
|
MaxIdleTime: parseDuration(getEnv("MONGODB_"+strings.ToUpper(dbName)+"_MAX_IDLE_TIME", "5m")),
|
|
HealthCheckPeriod: parseDuration(getEnv("MONGODB_"+strings.ToUpper(dbName)+"_HEALTH_CHECK_PERIOD", "1m")),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// addSQLiteConfigs adds SQLite database configurations from environment variables
|
|
func (c *Config) addSQLiteConfigs() {
|
|
// Support for custom SQLite configurations with SQLITE_ prefix
|
|
envVars := os.Environ()
|
|
for _, envVar := range envVars {
|
|
parts := strings.SplitN(envVar, "=", 2)
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
|
|
key := parts[0]
|
|
// Parse SQLite configurations (format: SQLITE_[NAME]_[PROPERTY])
|
|
if strings.HasPrefix(key, "SQLITE_") && strings.Contains(key, "_") {
|
|
segments := strings.Split(key, "_")
|
|
if len(segments) >= 3 {
|
|
dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_"))
|
|
|
|
// Skip if it's a standard SQLite configuration
|
|
if dbName == "connection" || dbName == "dev" || dbName == "default" {
|
|
continue
|
|
}
|
|
|
|
// Create or update SQLite configuration
|
|
if _, exists := c.Databases[dbName]; !exists {
|
|
sqlitePath := getEnv("SQLITE_"+strings.ToUpper(dbName)+"_PATH", "")
|
|
if sqlitePath != "" {
|
|
c.Databases[dbName] = DatabaseConfig{
|
|
Name: dbName,
|
|
Type: "sqlite",
|
|
Path: sqlitePath,
|
|
Database: getEnv("SQLITE_"+strings.ToUpper(dbName)+"_DATABASE", dbName),
|
|
MaxOpenConns: getEnvAsInt("SQLITE_MAX_OPEN_CONNS", 25),
|
|
MaxIdleConns: getEnvAsInt("SQLITE_MAX_IDLE_CONNS", 25),
|
|
ConnMaxLifetime: parseDuration(getEnv("SQLITE_CONN_MAX_LIFETIME", "5m")),
|
|
// Connection pool settings
|
|
MaxLifetime: parseDuration(getEnv("SQLITE_"+strings.ToUpper(dbName)+"_MAX_LIFETIME", "1h")),
|
|
MaxIdleTime: parseDuration(getEnv("SQLITE_"+strings.ToUpper(dbName)+"_MAX_IDLE_TIME", "5m")),
|
|
HealthCheckPeriod: parseDuration(getEnv("SQLITE_"+strings.ToUpper(dbName)+"_HEALTH_CHECK_PERIOD", "1m")),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper functions for getting default values based on database type
|
|
func getDefaultPort(dbType string) int {
|
|
switch dbType {
|
|
case "postgres":
|
|
return 5432
|
|
case "mysql":
|
|
return 3306
|
|
case "sqlserver":
|
|
return 1433
|
|
case "mongodb":
|
|
return 27017
|
|
case "sqlite":
|
|
return 0 // SQLite doesn't use port
|
|
default:
|
|
return 5432
|
|
}
|
|
}
|
|
|
|
func getDefaultSchema(dbType string) string {
|
|
switch dbType {
|
|
case "postgres":
|
|
return "public"
|
|
case "mysql":
|
|
return ""
|
|
case "sqlserver":
|
|
return "dbo"
|
|
case "mongodb":
|
|
return ""
|
|
case "sqlite":
|
|
return ""
|
|
default:
|
|
return "public"
|
|
}
|
|
}
|
|
|
|
func getDefaultSSLMode(dbType string) string {
|
|
switch dbType {
|
|
case "postgres":
|
|
return "disable"
|
|
case "mysql":
|
|
return "false"
|
|
case "sqlserver":
|
|
return "false"
|
|
case "mongodb":
|
|
return "false"
|
|
case "sqlite":
|
|
return ""
|
|
default:
|
|
return "disable"
|
|
}
|
|
}
|
|
|
|
func getDefaultMaxOpenConns(dbType string) int {
|
|
switch dbType {
|
|
case "postgres":
|
|
return 25
|
|
case "mysql":
|
|
return 25
|
|
case "sqlserver":
|
|
return 25
|
|
case "mongodb":
|
|
return 100
|
|
case "sqlite":
|
|
return 1 // SQLite only supports one writer at a time
|
|
default:
|
|
return 25
|
|
}
|
|
}
|
|
|
|
func getDefaultMaxIdleConns(dbType string) int {
|
|
switch dbType {
|
|
case "postgres":
|
|
return 25
|
|
case "mysql":
|
|
return 25
|
|
case "sqlserver":
|
|
return 25
|
|
case "mongodb":
|
|
return 10
|
|
case "sqlite":
|
|
return 1 // SQLite only supports one writer at a time
|
|
default:
|
|
return 25
|
|
}
|
|
}
|
|
|
|
func getDefaultConnMaxLifetime(dbType string) string {
|
|
switch dbType {
|
|
case "postgres":
|
|
return "5m"
|
|
case "mysql":
|
|
return "5m"
|
|
case "sqlserver":
|
|
return "5m"
|
|
case "mongodb":
|
|
return "30m"
|
|
case "sqlite":
|
|
return "5m"
|
|
default:
|
|
return "5m"
|
|
}
|
|
}
|
|
|
|
func getEnvFromMap(config map[string]string, key, defaultValue string) string {
|
|
if value, exists := config[key]; exists {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func getEnvAsIntFromMap(config map[string]string, key string, defaultValue int) int {
|
|
if value, exists := config[key]; exists {
|
|
if intValue, err := strconv.Atoi(value); err == nil {
|
|
return intValue
|
|
}
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func getEnvAsBoolFromMap(config map[string]string, key string, defaultValue bool) bool {
|
|
if value, exists := config[key]; exists {
|
|
if boolValue, err := strconv.ParseBool(value); err == nil {
|
|
return boolValue
|
|
}
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func parseDuration(durationStr string) time.Duration {
|
|
if duration, err := time.ParseDuration(durationStr); err == nil {
|
|
return duration
|
|
}
|
|
return 5 * time.Minute
|
|
}
|
|
|
|
func getEnv(key, defaultValue string) string {
|
|
if value := os.Getenv(key); value != "" {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func getEnvAsInt(key string, defaultValue int) int {
|
|
valueStr := getEnv(key, "")
|
|
if value, err := strconv.Atoi(valueStr); err == nil {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func getEnvAsBool(key string, defaultValue bool) bool {
|
|
valueStr := getEnv(key, "")
|
|
if value, err := strconv.ParseBool(valueStr); err == nil {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// parseSchemes parses comma-separated schemes string into a slice
|
|
func parseSchemes(schemesStr string) []string {
|
|
if schemesStr == "" {
|
|
return []string{"http"}
|
|
}
|
|
|
|
schemes := strings.Split(schemesStr, ",")
|
|
for i, scheme := range schemes {
|
|
schemes[i] = strings.TrimSpace(scheme)
|
|
}
|
|
return schemes
|
|
}
|
|
|
|
// parseStaticTokens parses comma-separated static tokens string into a slice
|
|
func parseStaticTokens(tokensStr string) []string {
|
|
if tokensStr == "" {
|
|
return []string{}
|
|
}
|
|
|
|
tokens := strings.Split(tokensStr, ",")
|
|
for i, token := range tokens {
|
|
tokens[i] = strings.TrimSpace(token)
|
|
// Remove empty tokens
|
|
if tokens[i] == "" {
|
|
tokens = append(tokens[:i], tokens[i+1:]...)
|
|
i--
|
|
}
|
|
}
|
|
return tokens
|
|
}
|
|
|
|
func parseOrigins(originsStr string) []string {
|
|
if originsStr == "" {
|
|
return []string{"http://localhost:8080"} // Default untuk pengembangan
|
|
}
|
|
origins := strings.Split(originsStr, ",")
|
|
for i, origin := range origins {
|
|
origins[i] = strings.TrimSpace(origin)
|
|
}
|
|
return origins
|
|
}
|
|
|
|
func (c *Config) Validate() error {
|
|
var errs []string
|
|
|
|
if len(c.Databases) == 0 {
|
|
errs = append(errs, "at least one database configuration is required")
|
|
}
|
|
|
|
for name, db := range c.Databases {
|
|
if db.Type != "sqlite" && db.Host == "" {
|
|
errs = append(errs, fmt.Sprintf("database host is required for %s", name))
|
|
}
|
|
if db.Type != "sqlite" && db.Username == "" {
|
|
errs = append(errs, fmt.Sprintf("database username is required for %s", name))
|
|
}
|
|
if db.Type != "sqlite" && db.Password == "" {
|
|
errs = append(errs, fmt.Sprintf("database password is required for %s", name))
|
|
}
|
|
if db.Type == "sqlite" && db.Path == "" {
|
|
errs = append(errs, fmt.Sprintf("database path is required for SQLite database %s", name))
|
|
}
|
|
if db.Type != "sqlite" && db.Database == "" {
|
|
errs = append(errs, fmt.Sprintf("database name is required for %s", name))
|
|
}
|
|
}
|
|
|
|
if c.Bpjs.BaseURL == "" {
|
|
errs = append(errs, "BPJS Base URL is required")
|
|
}
|
|
if c.Bpjs.ConsID == "" {
|
|
errs = append(errs, "BPJS Consumer ID is required")
|
|
}
|
|
if c.Bpjs.UserKey == "" {
|
|
errs = append(errs, "BPJS User Key is required")
|
|
}
|
|
if c.Bpjs.SecretKey == "" {
|
|
errs = append(errs, "BPJS Secret Key is required")
|
|
}
|
|
|
|
// Validate authentication configuration
|
|
switch c.Auth.Type {
|
|
case "keycloak":
|
|
if !c.Keycloak.Enabled {
|
|
errs = append(errs, "keycloak.enabled must be true when auth.type is 'keycloak'")
|
|
}
|
|
if c.Keycloak.Issuer == "" {
|
|
errs = append(errs, "keycloak.issuer is required when auth.type is 'keycloak'")
|
|
}
|
|
if c.Keycloak.Audience == "" {
|
|
errs = append(errs, "keycloak.audience is required when auth.type is 'keycloak'")
|
|
}
|
|
if c.Keycloak.JwksURL == "" {
|
|
errs = append(errs, "keycloak.jwks_url is required when auth.type is 'keycloak'")
|
|
}
|
|
case "static":
|
|
if len(c.Auth.StaticTokens) == 0 {
|
|
errs = append(errs, "auth.static_tokens is required when auth.type is 'static'")
|
|
}
|
|
case "hybrid":
|
|
if c.Auth.FallbackTo == "" {
|
|
errs = append(errs, "auth.fallback_to is required when auth.type is 'hybrid'")
|
|
}
|
|
// Validate fallback configuration
|
|
switch c.Auth.FallbackTo {
|
|
case "keycloak":
|
|
if !c.Keycloak.Enabled {
|
|
errs = append(errs, "keycloak.enabled must be true when auth.fallback_to is 'keycloak'")
|
|
}
|
|
case "static":
|
|
if len(c.Auth.StaticTokens) == 0 {
|
|
errs = append(errs, "auth.static_tokens is required when auth.fallback_to is 'static'")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Legacy validation for backward compatibility
|
|
if c.Auth.Type != "keycloak" && c.Keycloak.Enabled {
|
|
if c.Keycloak.Issuer == "" {
|
|
errs = append(errs, "Keycloak issuer is required when Keycloak is enabled")
|
|
}
|
|
if c.Keycloak.Audience == "" {
|
|
errs = append(errs, "Keycloak audience is required when Keycloak is enabled")
|
|
}
|
|
if c.Keycloak.JwksURL == "" {
|
|
errs = append(errs, "Keycloak JWKS URL is required when Keycloak is enabled")
|
|
}
|
|
}
|
|
|
|
// Validate SatuSehat configuration
|
|
if c.SatuSehat.OrgID == "" {
|
|
errs = append(errs, "SatuSehat Organization ID is required")
|
|
}
|
|
if c.SatuSehat.FasyakesID == "" {
|
|
errs = append(errs, "SatuSehat Fasyankes ID is required")
|
|
}
|
|
if c.SatuSehat.ClientID == "" {
|
|
errs = append(errs, "SatuSehat Client ID is required")
|
|
}
|
|
if c.SatuSehat.ClientSecret == "" {
|
|
errs = append(errs, "SatuSehat Client Secret is required")
|
|
}
|
|
if c.SatuSehat.AuthURL == "" {
|
|
errs = append(errs, "SatuSehat Auth URL is required")
|
|
}
|
|
if c.SatuSehat.BaseURL == "" {
|
|
errs = append(errs, "SatuSehat Base URL is required")
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return fmt.Errorf("configuration validation failed: %s", strings.Join(errs, "; "))
|
|
}
|
|
return nil
|
|
}
|