Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ volume

go.work
go.work.sum
main
65 changes: 65 additions & 0 deletions cmd/relayproxy/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,32 @@
return proxyConf, nil
}

// ReloadFromFile reloads the configuration from the config file.
// It preserves command line flags and environment variables.
func ReloadFromFile(flagSet *pflag.FlagSet, log *zap.Logger, version string) (*Config, error) {

Check warning on line 223 in cmd/relayproxy/config/config.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unused parameter 'flagSet' should be removed.

See more on https://sonarcloud.io/project/issues?id=thomaspoignant_go-feature-flag&issues=AZsKQJmYYhhLTmDGVjcX&open=AZsKQJmYYhhLTmDGVjcX&pullRequest=4462
// Reload config file (this will overwrite the file-based config)
loadConfigFile(log)

// Map environment variables (they take precedence over file config)
_ = k.Load(mapEnvVariablesProvider(k.String("envVariablePrefix"), log), nil)
_ = k.Set("version", version)

proxyConf := &Config{}
errUnmarshal := k.Unmarshal("", &proxyConf)
if errUnmarshal != nil {
return nil, errUnmarshal
}

processExporters(proxyConf)

return proxyConf, nil
}

// GetConfigFilePath returns the path to the configuration file being used.
func GetConfigFilePath() (string, error) {
return locateConfigFile(k.String("config"))
}

// loadConfigFile handles the loading of configuration files
func loadConfigFile(log *zap.Logger) {
configFileLocation, errFileLocation := locateConfigFile(k.String("config"))
Expand All @@ -232,6 +258,45 @@
}
}

// WatchConfigFile sets up a file watcher using koanf's built-in file watching
// and calls the reloadCallback when the configuration file changes.
// This function spawns a goroutine to watch for changes (Watch() is blocking).
func WatchConfigFile(
configFilePath string,
reloadCallback func() error,
log *zap.Logger,
) error {
parser := getParserForFile(configFilePath)
fileProvider := file.Provider(configFilePath)

// Watch for changes using koanf's built-in Watch() method
// Watch() is blocking and spawns a goroutine internally, so we call it in a goroutine
go func() {
if err := fileProvider.Watch(func(event interface{}, err error) {
if err != nil {
log.Error("error watching config file", zap.Error(err))
return
}

// Reload the configuration file
if err := k.Load(fileProvider, parser); err != nil {
log.Error("error reloading config file", zap.Error(err))
return
}

// Call the reload callback
if err := reloadCallback(); err != nil {
log.Error("error in reload callback", zap.Error(err))
}
}); err != nil {
log.Error("failed to start file watcher", zap.Error(err))
}
}()

log.Info("watching configuration file for changes", zap.String("file", configFilePath))
return nil
}

// getParserForFile returns the appropriate parser based on file extension
func getParserForFile(configFileLocation string) koanf.Parser {
ext := filepath.Ext(configFileLocation)
Expand Down
12 changes: 10 additions & 2 deletions cmd/relayproxy/helper/echo_helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
ffclient "github.com/thomaspoignant/go-feature-flag"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/helper"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/service"
"github.com/thomaspoignant/go-feature-flag/notifier"
"go.uber.org/zap"
)

func TestGetAPIKey(t *testing.T) {
Expand Down Expand Up @@ -256,11 +259,11 @@
err error
}

func (m *MockFlagsetManager) GetFlagSet(apiKey string) (*ffclient.GoFeatureFlag, error) {
func (m *MockFlagsetManager) GetFlagSet(_ string) (*ffclient.GoFeatureFlag, error) {
return m.flagset, m.err
}

func (m *MockFlagsetManager) GetFlagSetName(apiKey string) (string, error) {
func (m *MockFlagsetManager) GetFlagSetName(_ string) (string, error) {
return "", nil
}

Expand All @@ -276,6 +279,11 @@
return false
}

func (m *MockFlagsetManager) ReloadFlagsets(newConfig *config.Config, logger *zap.Logger, notifiers []notifier.Notifier) error {

Check warning on line 282 in cmd/relayproxy/helper/echo_helper_test.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unused parameter 'notifiers' should be removed.

See more on https://sonarcloud.io/project/issues?id=thomaspoignant_go-feature-flag&issues=AZsKTUMuex_LbzHYH4Yo&open=AZsKTUMuex_LbzHYH4Yo&pullRequest=4462

Check warning on line 282 in cmd/relayproxy/helper/echo_helper_test.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unused parameter 'newConfig' should be removed.

See more on https://sonarcloud.io/project/issues?id=thomaspoignant_go-feature-flag&issues=AZsKTUMuex_LbzHYH4Ym&open=AZsKTUMuex_LbzHYH4Ym&pullRequest=4462

Check warning on line 282 in cmd/relayproxy/helper/echo_helper_test.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unused parameter 'logger' should be removed.

See more on https://sonarcloud.io/project/issues?id=thomaspoignant_go-feature-flag&issues=AZsKTUMuex_LbzHYH4Yn&open=AZsKTUMuex_LbzHYH4Yn&pullRequest=4462
// nothing to do for mock
return nil
}

func (m *MockFlagsetManager) Close() {
// nothing to do
}
49 changes: 49 additions & 0 deletions cmd/relayproxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,54 @@ func main() {
defer cancel()
apiServer.Stop(ctx)
}()

// Start config file watcher if flagsets are configured
if len(proxyConf.FlagSets) > 0 {
configFilePath, err := config.GetConfigFilePath()
if err == nil {
err = config.WatchConfigFile(configFilePath, func() error {
return reloadFlagsets(f, flagsetManager, logger.ZapLogger, version, []notifier.Notifier{
prometheusNotifier,
proxyNotifier,
})
}, logger.ZapLogger)
if err != nil {
logger.ZapLogger.Warn("could not start config file watcher", zap.Error(err))
}
} else {
logger.ZapLogger.Warn("could not start config file watcher", zap.Error(err))
}
}

apiServer.StartWithContext(context.Background())
}

// reloadFlagsets reloads the configuration file and updates flagsets
func reloadFlagsets(
flagSet *pflag.FlagSet,
flagsetManager service.FlagsetManager,
logger *zap.Logger,
version string,
notifiers []notifier.Notifier,
) error {
logger.Info("configuration file changed, reloading flagsets")

// Reload configuration from file
newConfig, err := config.ReloadFromFile(flagSet, logger, version)
if err != nil {
return fmt.Errorf("failed to reload configuration file: %w", err)
}

// Validate configuration
if err := newConfig.IsValid(); err != nil {
return fmt.Errorf("reloaded configuration is invalid: %w", err)
}

// Reload flagsets
if err := flagsetManager.ReloadFlagsets(newConfig, logger, notifiers); err != nil {
return fmt.Errorf("failed to reload flagsets: %w", err)
}

logger.Info("flagsets reloaded successfully")
return nil
}
Loading
Loading