55c569baeb
config consolidation
258 lines
6.2 KiB
Go
258 lines
6.2 KiB
Go
package serviceconfig
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"queryorchestration/internal/serviceconfig/auth"
|
|
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
|
|
"queryorchestration/internal/serviceconfig/aws"
|
|
"queryorchestration/internal/serviceconfig/database"
|
|
"queryorchestration/internal/serviceconfig/logger"
|
|
"queryorchestration/internal/serviceconfig/observability"
|
|
"queryorchestration/internal/serviceconfig/queue"
|
|
|
|
"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.
|
|
//Pwd string `env:"PWD,required,notEmpty"`
|
|
Port int `env:"PORT" envDefault:"8080"`
|
|
BaseURL string `env:"BASE_URL" envDefault:"http://localhost"`
|
|
auth.CognitoConfig
|
|
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 {
|
|
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
|
|
}
|
|
|
|
func (b *BaseConfig) PrintConfig(prefixSecret string) {
|
|
visited := make(map[uintptr]bool)
|
|
b.printConfigRecursive(reflect.ValueOf(b).Elem(), "", prefixSecret, visited)
|
|
}
|
|
|
|
func (b *BaseConfig) printConfigRecursive(val reflect.Value, prefix string, prefixSecret string, visited map[uintptr]bool) {
|
|
// Skip non-structs and nil pointers
|
|
if val.Kind() == reflect.Ptr {
|
|
if val.IsNil() {
|
|
return
|
|
}
|
|
val = val.Elem()
|
|
}
|
|
|
|
if val.Kind() != reflect.Struct {
|
|
return
|
|
}
|
|
|
|
// Check for cycles, but only for addressable values
|
|
if val.CanAddr() {
|
|
addr := uintptr(val.Addr().UnsafePointer())
|
|
if visited[addr] {
|
|
return
|
|
}
|
|
visited[addr] = true
|
|
}
|
|
|
|
typ := val.Type()
|
|
|
|
// Process all fields including embedded ones
|
|
for i := 0; i < val.NumField(); i++ {
|
|
field := val.Field(i)
|
|
fieldType := typ.Field(i)
|
|
|
|
// Skip unexported fields
|
|
if !fieldType.IsExported() {
|
|
continue
|
|
}
|
|
|
|
fieldName := fieldType.Name
|
|
// Handle anonymous (embedded) fields specially
|
|
if fieldType.Anonymous {
|
|
// For embedded fields, recurse without adding to the path
|
|
// This is key - we want to process the embedded struct's fields directly
|
|
b.printConfigRecursive(field, prefix, prefixSecret, visited)
|
|
continue
|
|
}
|
|
|
|
fullPath := prefix + fieldName
|
|
envTag := fieldType.Tag.Get("env")
|
|
|
|
// Process fields with env tags
|
|
if envTag != "" {
|
|
if field.Kind() == reflect.Struct {
|
|
// For structs with env tags, log the struct and recurse
|
|
var valueStr string
|
|
if field.CanInterface() {
|
|
valueStr = fmt.Sprintf("%+v", field.Interface())
|
|
}
|
|
|
|
b.Logger.Info("Struct Config value",
|
|
"key", fullPath,
|
|
"value", valueStr)
|
|
} else {
|
|
// For non-struct fields with env tags
|
|
var valueStr string
|
|
if field.Kind() == reflect.String {
|
|
valueStr = field.String()
|
|
} else {
|
|
valueStr = fmt.Sprintf("%v", field.Interface())
|
|
}
|
|
|
|
// Mask sensitive values
|
|
if strings.Contains(strings.ToLower(fieldName), strings.ToLower(prefixSecret)) {
|
|
if len(valueStr) > 3 {
|
|
valueStr = valueStr[:3] + "..."
|
|
}
|
|
}
|
|
|
|
b.Logger.Info("Config value",
|
|
"key", fullPath,
|
|
"value", valueStr)
|
|
}
|
|
}
|
|
|
|
// Recurse into structs regardless of env tag
|
|
if field.Kind() == reflect.Struct {
|
|
b.printConfigRecursive(field, fullPath+".", prefixSecret, visited)
|
|
}
|
|
}
|
|
}
|