diff --git a/viperconfig/configuration_manager.go b/viperconfig/configuration_manager.go new file mode 100644 index 0000000..c1efb57 --- /dev/null +++ b/viperconfig/configuration_manager.go @@ -0,0 +1,106 @@ +package viperconfig + +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 { + panic(err) + } + + // 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 { + panic(err) + } + + 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(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) +} diff --git a/viperconfig/context.go b/viperconfig/context.go new file mode 100644 index 0000000..1b13e57 --- /dev/null +++ b/viperconfig/context.go @@ -0,0 +1,26 @@ +package viperconfig + +import "context" + +type contextKey struct{} + +var ( + ConfigurationKey = contextKey{} +) + +// GetConfigFromContext[T any] returns the current configuration extracted from the passed context +func GetConfigFromContext[T any](ctx context.Context) T { + cm := GetConfigMgrFromContext[T](ctx) + + return cm.Get() +} + +// GetConfigMgrFromContext[T any] returns the configuration manager stored in the passed context +func GetConfigMgrFromContext[T any](ctx context.Context) ConfigurationManager[T] { + return ctx.Value(ConfigurationKey).(ConfigurationManager[T]) +} + +// SetConfigMgrOnContext[T any] stores the passed configuration manager on the passed context +func SetConfigMgrOnContext[T any](ctx context.Context, value T) context.Context { + return context.WithValue(ctx, ConfigurationKey, value) +}