Files
api_antrean/internal/config/config.go
2026-02-13 15:57:28 +07:00

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
}