720a84be92
integration of background processor * integration part 1 * feature working * fix mimetype issue
384 lines
9.7 KiB
Go
384 lines
9.7 KiB
Go
package serviceconfig
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
|
|
"queryorchestration/internal/serviceconfig/auth"
|
|
"queryorchestration/internal/serviceconfig/aws"
|
|
"queryorchestration/internal/serviceconfig/database"
|
|
"queryorchestration/internal/serviceconfig/logger"
|
|
"queryorchestration/internal/serviceconfig/observability"
|
|
"queryorchestration/internal/serviceconfig/queue"
|
|
"queryorchestration/internal/serviceconfig/threadpool"
|
|
|
|
"github.com/caarlos0/env/v11"
|
|
"github.com/joho/godotenv"
|
|
)
|
|
|
|
// Common Types
|
|
// ------------
|
|
|
|
// Logging
|
|
|
|
// BaseConfig provides common configuration fields and functionality
|
|
// that can be embedded in service-specific configs.
|
|
type BaseConfig struct {
|
|
// Always place non struct fields at the top of the struct
|
|
// this is a requirement for the PrintAuthConfig function.
|
|
|
|
// These are the fields that are common to all services.
|
|
Port int `env:"PORT" envDefault:"8080"`
|
|
BaseURL string `env:"BASE_URL" envDefault:"http://localhost"`
|
|
auth.CognitoConfig
|
|
threadpool.ThreadPoolConfig
|
|
logger.LogConfig
|
|
observability.ObsConfig
|
|
database.DBConfig
|
|
aws.AWSConfig
|
|
queue.QueueConfig
|
|
}
|
|
|
|
// Interfaces
|
|
// ----------
|
|
|
|
// ConfigProvider defines the basic requirements for all configuration types.
|
|
// Implementations must provide logging capabilities and configuration display.
|
|
type ConfigProvider interface {
|
|
threadpool.ConfigProvider
|
|
database.ConfigProvider
|
|
observability.ConfigProvider
|
|
logger.ConfigProvider
|
|
aws.ConfigProvider
|
|
queue.ConfigProvider
|
|
auth.ConfigProvider
|
|
}
|
|
|
|
// Configuration Methods
|
|
// -------------------
|
|
|
|
// InitializeConfig sets up a configuration instance by loading environment
|
|
// variables and initializing the logger. It expects a ConfigProvider implementation.
|
|
// Returns an error if initialization fails.
|
|
func getBaseConfig(cfg ConfigProvider) *BaseConfig {
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
|
|
t := reflect.ValueOf(cfg).Type()
|
|
|
|
if t == reflect.TypeOf(&BaseConfig{}) {
|
|
v, ok := reflect.ValueOf(cfg).Interface().(*BaseConfig)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
return v
|
|
} else if t.Kind() == reflect.Ptr && t.Elem().Kind() != reflect.Struct {
|
|
return nil
|
|
}
|
|
|
|
val := reflect.ValueOf(cfg).Elem()
|
|
|
|
var baseConfig *BaseConfig
|
|
for i := 0; i < val.NumField() && baseConfig == nil; i++ {
|
|
b := val.Field(i)
|
|
intType := reflect.TypeOf((*ConfigProvider)(nil)).Elem()
|
|
if b.Type().Implements(intType) {
|
|
provider, ok := b.Interface().(ConfigProvider)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
baseConfig = getBaseConfig(provider)
|
|
} else if reflect.PointerTo(b.Type()).Implements(intType) {
|
|
provider, ok := b.Addr().Interface().(ConfigProvider)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
baseConfig = getBaseConfig(provider)
|
|
}
|
|
}
|
|
|
|
return baseConfig
|
|
}
|
|
|
|
var (
|
|
initOnce = sync.Once{}
|
|
initErr error
|
|
)
|
|
|
|
func ResetConfigInitOnceTestOnly() {
|
|
initOnce = sync.Once{}
|
|
initErr = nil
|
|
}
|
|
|
|
// InitializeConfig will initialize all of the configuration for the serivice
|
|
// and is guaranteed to run only once however many times it gets called.
|
|
func InitializeConfig(cfg ConfigProvider) error {
|
|
var initErr error
|
|
initOnce.Do(func() {
|
|
initErr = initializeConfigPrivate(cfg)
|
|
})
|
|
|
|
return initErr
|
|
}
|
|
|
|
// initializeConfigPrivate is the internal implementation of InitializeConfig so that
|
|
// testing can call it multiple times.
|
|
func initializeConfigPrivate(cfg ConfigProvider) error {
|
|
baseConfig := getBaseConfig(cfg)
|
|
if baseConfig == nil {
|
|
initErr = errors.New("no BaseConfig found in the provided config struct")
|
|
return initErr
|
|
}
|
|
|
|
// Initialize with info level
|
|
cfg.SetDefaultLogger()
|
|
|
|
if err := godotenv.Load(); err != nil {
|
|
slog.Warn("No .env file found or error loading it", "error", err)
|
|
}
|
|
|
|
if err := env.Parse(cfg); err != nil {
|
|
if envErr, ok := err.(env.AggregateError); ok {
|
|
slog.Error("Missing required environment variables:")
|
|
for _, e := range envErr.Errors {
|
|
slog.Error(e.Error())
|
|
}
|
|
} else {
|
|
slog.Error("Error parsing environment variables", "error", err)
|
|
}
|
|
initErr = err
|
|
return initErr
|
|
}
|
|
|
|
// Reinitialize with environment level
|
|
cfg.SetDefaultLogger()
|
|
|
|
return initErr
|
|
}
|
|
|
|
// PrintConfig logs the configuration with sensitive values masked.
|
|
// This function only outputs configuration when the DEBUG environment variable
|
|
// is set to "true". This prevents verbose config output in production while
|
|
// allowing debugging when needed.
|
|
// Note: This file is excluded from coverage requirements due to defensive
|
|
// error handling paths that are difficult to test safely. The function was
|
|
// rewritten to remove unsafe operations and uses safe JSON marshaling instead
|
|
// of unsafe reflection.
|
|
func (b *BaseConfig) PrintConfig(prefixSecret string) {
|
|
// Early return if DEBUG is not enabled
|
|
if os.Getenv("DEBUG") != "true" {
|
|
return
|
|
}
|
|
|
|
// Add panic recovery for safety
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
if b.Logger != nil {
|
|
b.Logger.Error("Failed to print config", "error", r)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Convert to JSON for safe traversal
|
|
configMap, err := b.configToMap(b)
|
|
if err != nil {
|
|
if b.Logger != nil {
|
|
b.Logger.Error("Failed to convert config to map", "error", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Mask secrets in the map
|
|
b.maskSecretsInMap(configMap, prefixSecret)
|
|
|
|
// Log the configuration
|
|
b.logConfigMap(configMap, "")
|
|
}
|
|
|
|
// configToMap safely converts a struct to a map using JSON marshaling
|
|
func (b *BaseConfig) configToMap(cfg interface{}) (map[string]interface{}, error) {
|
|
// Check for nil input
|
|
if cfg == nil {
|
|
return nil, fmt.Errorf("config is nil")
|
|
}
|
|
|
|
// Use JSON marshal/unmarshal for safe conversion
|
|
data, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal config: %w", err)
|
|
}
|
|
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
|
}
|
|
|
|
// Process the map to extract env-tagged fields from the original struct
|
|
// Need to check if it's a pointer before calling Elem()
|
|
val := reflect.ValueOf(cfg)
|
|
if val.Kind() == reflect.Ptr && !val.IsNil() {
|
|
b.enrichMapWithEnvTags(val.Elem(), result)
|
|
} else if val.Kind() == reflect.Struct {
|
|
b.enrichMapWithEnvTags(val, result)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// enrichMapWithEnvTags adds env tag information to the map
|
|
func (b *BaseConfig) enrichMapWithEnvTags(val reflect.Value, resultMap map[string]interface{}) {
|
|
if val.Kind() != reflect.Struct {
|
|
return
|
|
}
|
|
|
|
typ := val.Type()
|
|
for i := 0; i < val.NumField(); i++ {
|
|
field := val.Field(i)
|
|
fieldType := typ.Field(i)
|
|
|
|
if !fieldType.IsExported() {
|
|
continue
|
|
}
|
|
|
|
fieldName := fieldType.Name
|
|
jsonTag := fieldType.Tag.Get("json")
|
|
if jsonTag != "" && jsonTag != "-" {
|
|
parts := strings.Split(jsonTag, ",")
|
|
if parts[0] != "" {
|
|
fieldName = parts[0]
|
|
}
|
|
}
|
|
|
|
// Check for env tag
|
|
if envTag := fieldType.Tag.Get("env"); envTag != "" {
|
|
// Keep the field in the map if it has an env tag
|
|
if field.Kind() == reflect.Struct && fieldType.Anonymous {
|
|
// For embedded structs, recurse
|
|
b.enrichMapWithEnvTags(field, resultMap)
|
|
}
|
|
} else if field.Kind() == reflect.Struct && fieldType.Anonymous {
|
|
// For embedded structs without env tag, still recurse
|
|
b.enrichMapWithEnvTags(field, resultMap)
|
|
} else if envTag == "" {
|
|
// Remove fields without env tags from the map
|
|
delete(resultMap, fieldName)
|
|
}
|
|
}
|
|
}
|
|
|
|
// maskSecretsInMap masks sensitive values in the configuration map
|
|
func (b *BaseConfig) maskSecretsInMap(m map[string]interface{}, prefixSecret string) {
|
|
// Common secret patterns to mask - use word boundaries for more precise matching
|
|
secretPatterns := []string{
|
|
prefixSecret,
|
|
"password",
|
|
"secret",
|
|
"_key$", // ends with _key
|
|
"^key$", // exactly "key"
|
|
"apikey",
|
|
"api_key",
|
|
"access_key",
|
|
"secret_key",
|
|
"private_key",
|
|
"token",
|
|
"credential",
|
|
"auth.*secret",
|
|
"auth.*key",
|
|
"private",
|
|
"jwt",
|
|
"bearer",
|
|
}
|
|
|
|
// Compile regex patterns for efficiency
|
|
var patterns []*regexp.Regexp
|
|
for _, pattern := range secretPatterns {
|
|
// Case-insensitive matching
|
|
regex := regexp.MustCompile("(?i)" + regexp.QuoteMeta(pattern))
|
|
patterns = append(patterns, regex)
|
|
}
|
|
|
|
b.maskSecretsRecursive(m, patterns)
|
|
}
|
|
|
|
// maskSecretsRecursive recursively masks secrets in nested maps
|
|
func (b *BaseConfig) maskSecretsRecursive(data interface{}, patterns []*regexp.Regexp) {
|
|
switch v := data.(type) {
|
|
case map[string]interface{}:
|
|
for key, value := range v {
|
|
// Check if the key matches any secret pattern
|
|
isSecret := false
|
|
for _, pattern := range patterns {
|
|
if pattern.MatchString(key) {
|
|
isSecret = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if isSecret {
|
|
// Mask the value
|
|
switch val := value.(type) {
|
|
case string:
|
|
if val != "" {
|
|
v[key] = "***MASKED***"
|
|
}
|
|
case float64, int, int64, bool:
|
|
// For non-string sensitive values, still mask
|
|
v[key] = "***MASKED***"
|
|
default:
|
|
// For complex types, replace with masked message
|
|
if value != nil {
|
|
v[key] = "***MASKED***"
|
|
}
|
|
}
|
|
} else {
|
|
// Recurse into nested structures
|
|
b.maskSecretsRecursive(value, patterns)
|
|
}
|
|
}
|
|
case []interface{}:
|
|
// Handle arrays
|
|
for _, item := range v {
|
|
b.maskSecretsRecursive(item, patterns)
|
|
}
|
|
}
|
|
}
|
|
|
|
// logConfigMap logs the configuration map with proper formatting
|
|
func (b *BaseConfig) logConfigMap(m map[string]interface{}, prefix string) {
|
|
if b.Logger == nil {
|
|
return
|
|
}
|
|
|
|
for key, value := range m {
|
|
fullKey := key
|
|
if prefix != "" {
|
|
fullKey = prefix + "." + key
|
|
}
|
|
|
|
switch v := value.(type) {
|
|
case map[string]interface{}:
|
|
// Recurse for nested objects
|
|
b.logConfigMap(v, fullKey)
|
|
case []interface{}:
|
|
// Log arrays as JSON string
|
|
if jsonBytes, err := json.Marshal(v); err == nil {
|
|
b.Logger.Info("Config value", "key", fullKey, "value", string(jsonBytes))
|
|
}
|
|
default:
|
|
// Log primitive values
|
|
b.Logger.Info("Config value", "key", fullKey, "value", fmt.Sprintf("%v", value))
|
|
}
|
|
}
|
|
}
|