package configuration_manager import ( "bytes" "log/slog" "os" "strings" "sync" "github.com/fsnotify/fsnotify" "github.com/spf13/pflag" "github.com/spf13/viper" ) // ConfigurationManager[T any] manages Viper for the specified configuration struct type ConfigurationManager[T any] struct { name string config T defaults []byte lock *sync.RWMutex vc *viper.Viper } // New[T any] creates a new configuration manager for the specified configuration struct func New[T any](name string, defaults []byte) *ConfigurationManager[T] { // Initialize p := &ConfigurationManager[T]{ name: name, defaults: defaults, lock: &sync.RWMutex{}, vc: viper.New(), } // Load defaults vd := viper.New() buf := bytes.NewBuffer(p.defaults) if err := vd.ReadConfig(buf); err != nil { slog.Error("Error loading default configuration", "error", err) os.Exit(1) } // Create Viper instance p.vc = viper.New() p.vc.AddConfigPath("/secrets") p.vc.AddConfigPath("/etc/joco") p.vc.AddConfigPath("/etc") p.vc.AddConfigPath(".") p.vc.SetConfigName(p.name) p.vc.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) p.vc.SetEnvPrefix(p.name) p.vc.AutomaticEnv() p.vc.OnConfigChange(p.onConfigChange) // Set defaults from embedded configuration file for k, v := range vd.AllSettings() { p.vc.SetDefault(k, v) } // Load the configuration (ignore file not found) if err := p.load(); err != nil { slog.Error("Error loading configuration", "error", err) os.Exit(1) } p.vc.BindPFlags(pflag.CommandLine) // Monitor changes to the configuration p.vc.WatchConfig() return p } func (p *ConfigurationManager[T]) Get() T { p.lock.RLock() defer p.lock.RUnlock() return p.config } func (p *ConfigurationManager[T]) load() error { if err := p.vc.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { return err } } var cfg T if err := viper.Unmarshal(&cfg); err != nil { return err } p.lock.Lock() p.config = cfg p.lock.Unlock() return nil } func (p *ConfigurationManager[T]) onConfigChange(in fsnotify.Event) { if err := p.load(); err != nil { slog.Error("Error re-loading configuration", "error", err) } } // SaveToFile saves the embedded default configuration to the specified file func (p *ConfigurationManager[T]) SaveToFile(path string) error { return os.WriteFile(path, p.defaults, os.ModeExclusive) }