From 6bde30f9e07f1c1d50c3ca2f1119b7fbeb3d5e4d Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Wed, 5 Nov 2025 17:55:18 +0000 Subject: [PATCH 01/22] Change format to configure server listening port --- cmd/relayproxy/api/server.go | 8 +- cmd/relayproxy/config/config.go | 99 +++-------------------- cmd/relayproxy/config/config_otel.go | 50 ++++++++++++ cmd/relayproxy/config/config_server.go | 38 +++++++++ cmd/relayproxy/config/config_validator.go | 21 +++++ cmd/relayproxy/config/exporter.go | 16 ++++ 6 files changed, 138 insertions(+), 94 deletions(-) create mode 100644 cmd/relayproxy/config/config_otel.go create mode 100644 cmd/relayproxy/config/config_server.go diff --git a/cmd/relayproxy/api/server.go b/cmd/relayproxy/api/server.go index 83de822c3f6..7f5c66d66a1 100644 --- a/cmd/relayproxy/api/server.go +++ b/cmd/relayproxy/api/server.go @@ -116,7 +116,7 @@ func (s *Server) Start() { // starting the monitoring server on a different port if configured if s.monitoringEcho != nil { go func() { - addressMonitoring := fmt.Sprintf("0.0.0.0:%d", s.config.MonitoringPort) + addressMonitoring := fmt.Sprintf("%s:%d", s.config.GetServerHost(), s.config.GetMonitoringPort()) s.zapLog.Info( "Starting monitoring", zap.String("address", addressMonitoring)) @@ -138,11 +138,7 @@ func (s *Server) Start() { // we can continue because otel is not mandatory to start the server } - // starting the main application - if s.config.ListenPort == 0 { - s.config.ListenPort = 1031 - } - address := fmt.Sprintf("0.0.0.0:%d", s.config.ListenPort) + address := fmt.Sprintf("%s:%d", s.config.GetServerHost(), s.config.GetServerPort()) s.zapLog.Info( "Starting go-feature-flag relay proxy ...", zap.String("address", address), diff --git a/cmd/relayproxy/config/config.go b/cmd/relayproxy/config/config.go index 5910a53dafa..ab2b2b5f144 100644 --- a/cmd/relayproxy/config/config.go +++ b/cmd/relayproxy/config/config.go @@ -17,7 +17,6 @@ import ( "github.com/knadh/koanf/v2" "github.com/spf13/pflag" ffclient "github.com/thomaspoignant/go-feature-flag" - "github.com/thomaspoignant/go-feature-flag/utils" "github.com/xitongsys/parquet-go/parquet" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -53,8 +52,19 @@ var DefaultExporter = struct { type Config struct { CommonFlagSet `mapstructure:",inline" koanf:",squash"` // ListenPort (optional) is the port we are using to start the proxy + // + // Deprecated: use Server.Port instead ListenPort int `mapstructure:"listen" koanf:"listen"` + // MonitoringPort (optional) is the port we are using to expose the metrics and healthchecks + // If not set we will use the same port as the proxy + // + // Deprecated: use Server.MonitoringPort instead + MonitoringPort int `mapstructure:"monitoringPort" koanf:"monitoringport"` + + // Server is the server configuration, including host, port, and unix socket + Server Server `mapstructure:"server" koanf:"server"` + // HideBanner (optional) if true, we don't display the go-feature-flag relay proxy banner HideBanner bool `mapstructure:"hideBanner" koanf:"hidebanner"` @@ -127,10 +137,6 @@ type Config struct { // Default: "" OpenTelemetryOtlpEndpoint string `mapstructure:"openTelemetryOtlpEndpoint" koanf:"opentelemetryotlpendpoint"` - // MonitoringPort (optional) is the port we are using to expose the metrics and healthchecks - // If not set we will use the same port as the proxy - MonitoringPort int `mapstructure:"monitoringPort" koanf:"monitoringport"` - // PersistentFlagConfigurationFile (optional) if set GO Feature Flag will store flags configuration in this file // to be able to serve the flags even if none of the retrievers is available during starting time. // @@ -229,89 +235,6 @@ func getParserForFile(configFileLocation string) koanf.Parser { } } -// processExporters handles the post-processing of exporters configuration -func processExporters(proxyConf *Config) { - if proxyConf.Exporters == nil { - return - } - - for i := range *proxyConf.Exporters { - addresses := (*proxyConf.Exporters)[i].Kafka.Addresses - if len(addresses) == 0 || (len(addresses) == 1 && strings.Contains(addresses[0], ",")) { - (*proxyConf.Exporters)[i].Kafka.Addresses = utils.StringToArray(addresses) - } - } -} - -// OpenTelemetryConfiguration is the configuration for the OpenTelemetry part of the relay proxy -// It is used to configure the OpenTelemetry SDK and the OpenTelemetry Exporter -// Most of the time this configuration is set using environment variables. -type OpenTelemetryConfiguration struct { - SDK struct { - Disabled bool `mapstructure:"disabled" koanf:"disabled"` - } `mapstructure:"sdk" koanf:"sdk"` - Exporter OtelExporter `mapstructure:"exporter" koanf:"exporter"` - Service struct { - Name string `mapstructure:"name" koanf:"name"` - } `mapstructure:"service" koanf:"service"` - Traces struct { - Sampler string `mapstructure:"sampler" koanf:"sampler"` - } `mapstructure:"traces" koanf:"traces"` - Resource OtelResource `mapstructure:"resource" koanf:"resource"` -} - -type OtelExporter struct { - Otlp OtelExporterOtlp `mapstructure:"otlp" koanf:"otlp"` -} - -type OtelExporterOtlp struct { - Endpoint string `mapstructure:"endpoint" koanf:"endpoint"` - Protocol string `mapstructure:"protocol" koanf:"protocol"` -} - -type OtelResource struct { - Attributes map[string]string `mapstructure:"attributes" koanf:"attributes"` -} - -// JaegerSamplerConfiguration is the configuration object to configure the sampling. -// Most of the time this configuration is set using environment variables. -type JaegerSamplerConfiguration struct { - Sampler struct { - Manager struct { - Host struct { - Port string `mapstructure:"port" koanf:"port"` - } `mapstructure:"host" koanf:"host"` - } `mapstructure:"manager" koanf:"manager"` - Refresh struct { - Interval string `mapstructure:"interval" koanf:"interval"` - } `mapstructure:"refresh" koanf:"refresh"` - Max struct { - Operations int `mapstructure:"operations" koanf:"operations"` - } `mapstructure:"max" koanf:"max"` - } `mapstructure:"sampler" koanf:"sampler"` -} - -// IsValid contains all the validation of the configuration. -func (c *Config) IsValid() error { - if c == nil { - return fmt.Errorf("empty config") - } - if c.ListenPort == 0 { - return fmt.Errorf("invalid port %d", c.ListenPort) - } - if err := validateLogLevel(c.LogLevel); err != nil { - return err - } - if err := validateLogFormat(c.LogFormat); err != nil { - return err - } - - if len(c.FlagSets) > 0 { - return c.validateFlagSets() - } - return c.validateDefaultMode() -} - // locateConfigFile is selecting the configuration file we will use. func locateConfigFile(inputFilePath string) (string, error) { filename := "goff-proxy" diff --git a/cmd/relayproxy/config/config_otel.go b/cmd/relayproxy/config/config_otel.go new file mode 100644 index 00000000000..c76cc91dbd6 --- /dev/null +++ b/cmd/relayproxy/config/config_otel.go @@ -0,0 +1,50 @@ +package config +package config + +// OpenTelemetryConfiguration is the configuration for the OpenTelemetry part of the relay proxy +// It is used to configure the OpenTelemetry SDK and the OpenTelemetry Exporter +// Most of the time this configuration is set using environment variables. +type OpenTelemetryConfiguration struct { + SDK struct { + Disabled bool `mapstructure:"disabled" koanf:"disabled"` + } `mapstructure:"sdk" koanf:"sdk"` + Exporter OtelExporter `mapstructure:"exporter" koanf:"exporter"` + Service struct { + Name string `mapstructure:"name" koanf:"name"` + } `mapstructure:"service" koanf:"service"` + Traces struct { + Sampler string `mapstructure:"sampler" koanf:"sampler"` + } `mapstructure:"traces" koanf:"traces"` + Resource OtelResource `mapstructure:"resource" koanf:"resource"` +} + +type OtelExporter struct { + Otlp OtelExporterOtlp `mapstructure:"otlp" koanf:"otlp"` +} + +type OtelExporterOtlp struct { + Endpoint string `mapstructure:"endpoint" koanf:"endpoint"` + Protocol string `mapstructure:"protocol" koanf:"protocol"` +} + +type OtelResource struct { + Attributes map[string]string `mapstructure:"attributes" koanf:"attributes"` +} + +// JaegerSamplerConfiguration is the configuration object to configure the sampling. +// Most of the time this configuration is set using environment variables. +type JaegerSamplerConfiguration struct { + Sampler struct { + Manager struct { + Host struct { + Port string `mapstructure:"port" koanf:"port"` + } `mapstructure:"host" koanf:"host"` + } `mapstructure:"manager" koanf:"manager"` + Refresh struct { + Interval string `mapstructure:"interval" koanf:"interval"` + } `mapstructure:"refresh" koanf:"refresh"` + Max struct { + Operations int `mapstructure:"operations" koanf:"operations"` + } `mapstructure:"max" koanf:"max"` + } `mapstructure:"sampler" koanf:"sampler"` +} \ No newline at end of file diff --git a/cmd/relayproxy/config/config_server.go b/cmd/relayproxy/config/config_server.go new file mode 100644 index 00000000000..39d73c7992b --- /dev/null +++ b/cmd/relayproxy/config/config_server.go @@ -0,0 +1,38 @@ +package config + +type Server struct { + Host string `mapstructure:"host" koanf:"host"` + Port int `mapstructure:"port" koanf:"port"` + UnixSocket string `mapstructure:"unixSocket" koanf:"unixsocket"` + MonitoringPort int `mapstructure:"monitoringPort" koanf:"monitoringport"` +} + +// GetMonitoringPort returns the monitoring port, checking first the top-level config +// and then the server config. +func (c *Config) GetMonitoringPort() int { + if c.MonitoringPort != 0 { + return c.MonitoringPort + } + return c.Server.MonitoringPort +} + +// GetServerHost returns the server host, defaulting to "0.0.0.0" if not set. +func (c *Config) GetServerHost() string { + if c.Server.Host != "" { + return c.Server.Host + } + return "0.0.0.0" +} + +// GetServerPort returns the server port, checking first the server config +// and then the top-level config, defaulting to 1031 if not set. +func (c *Config) GetServerPort() int { + if c.Server.Port != 0 { + return c.Server.Port + } + + if c.ListenPort != 0 { + return c.ListenPort + } + return 1031 +} diff --git a/cmd/relayproxy/config/config_validator.go b/cmd/relayproxy/config/config_validator.go index 63015b611fd..e523973feb4 100644 --- a/cmd/relayproxy/config/config_validator.go +++ b/cmd/relayproxy/config/config_validator.go @@ -8,6 +8,27 @@ import ( "go.uber.org/zap/zapcore" ) +// IsValid contains all the validation of the configuration. +func (c *Config) IsValid() error { + if c == nil { + return fmt.Errorf("empty config") + } + if c.GetServerPort() == 0 { + return fmt.Errorf("invalid port %d", c.GetServerPort()) + } + if err := validateLogLevel(c.LogLevel); err != nil { + return err + } + if err := validateLogFormat(c.LogFormat); err != nil { + return err + } + + if len(c.FlagSets) > 0 { + return c.validateFlagSets() + } + return c.validateDefaultMode() +} + // validateLogFormat validates the log format func validateLogFormat(logFormat string) error { switch strings.ToLower(logFormat) { diff --git a/cmd/relayproxy/config/exporter.go b/cmd/relayproxy/config/exporter.go index 73efb539c84..492ff485323 100644 --- a/cmd/relayproxy/config/exporter.go +++ b/cmd/relayproxy/config/exporter.go @@ -2,8 +2,10 @@ package config import ( "fmt" + "strings" "github.com/thomaspoignant/go-feature-flag/exporter/kafkaexporter" + "github.com/thomaspoignant/go-feature-flag/utils" "github.com/xitongsys/parquet-go/parquet" ) @@ -36,6 +38,20 @@ type ExporterConf struct { ExporterEventType string `mapstructure:"eventType" koanf:"eventtype"` } +// processExporters handles the post-processing of exporters configuration +func processExporters(proxyConf *Config) { + if proxyConf.Exporters == nil { + return + } + + for i := range *proxyConf.Exporters { + addresses := (*proxyConf.Exporters)[i].Kafka.Addresses + if len(addresses) == 0 || (len(addresses) == 1 && strings.Contains(addresses[0], ",")) { + (*proxyConf.Exporters)[i].Kafka.Addresses = utils.StringToArray(addresses) + } + } +} + func (c *ExporterConf) IsValid() error { if err := c.Kind.IsValid(); err != nil { return err From 1f7fd51084c4a71e737de0a6cc3a8099796bb7d0 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Wed, 5 Nov 2025 17:56:43 +0000 Subject: [PATCH 02/22] Adding comments --- cmd/relayproxy/config/config_server.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/cmd/relayproxy/config/config_server.go b/cmd/relayproxy/config/config_server.go index 39d73c7992b..35dcb89ce5c 100644 --- a/cmd/relayproxy/config/config_server.go +++ b/cmd/relayproxy/config/config_server.go @@ -1,10 +1,20 @@ package config type Server struct { - Host string `mapstructure:"host" koanf:"host"` - Port int `mapstructure:"port" koanf:"port"` - UnixSocket string `mapstructure:"unixSocket" koanf:"unixsocket"` - MonitoringPort int `mapstructure:"monitoringPort" koanf:"monitoringport"` + // Host is the server host. + // default: 0.0.0.0 + Host string `mapstructure:"host" koanf:"host"` + + // Port is the server port. + // default: 1031 + Port int `mapstructure:"port" koanf:"port"` + + // MonitoringPort is the monitoring port. + // default: none, it will use the same as server port. + MonitoringPort int `mapstructure:"monitoringPort" koanf:"monitoringport"` + + // UnixSocket is the server unix socket path. + UnixSocket string `mapstructure:"unixSocket" koanf:"unixsocket"` } // GetMonitoringPort returns the monitoring port, checking first the top-level config From cfb5980c4dbb8e95338bbbd44c0b1d39cffd392b Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Wed, 5 Nov 2025 18:06:53 +0000 Subject: [PATCH 03/22] adding a log to mention the deprecation --- cmd/relayproxy/api/server.go | 2 +- cmd/relayproxy/config/config_server.go | 12 ++++++++++-- cmd/relayproxy/config/config_validator.go | 4 ++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/cmd/relayproxy/api/server.go b/cmd/relayproxy/api/server.go index 7f5c66d66a1..8f1487cc201 100644 --- a/cmd/relayproxy/api/server.go +++ b/cmd/relayproxy/api/server.go @@ -138,7 +138,7 @@ func (s *Server) Start() { // we can continue because otel is not mandatory to start the server } - address := fmt.Sprintf("%s:%d", s.config.GetServerHost(), s.config.GetServerPort()) + address := fmt.Sprintf("%s:%d", s.config.GetServerHost(), s.config.GetServerPort(s.zapLog)) s.zapLog.Info( "Starting go-feature-flag relay proxy ...", zap.String("address", address), diff --git a/cmd/relayproxy/config/config_server.go b/cmd/relayproxy/config/config_server.go index 35dcb89ce5c..667cbcec2fd 100644 --- a/cmd/relayproxy/config/config_server.go +++ b/cmd/relayproxy/config/config_server.go @@ -1,5 +1,7 @@ package config +import "go.uber.org/zap" + type Server struct { // Host is the server host. // default: 0.0.0.0 @@ -19,8 +21,11 @@ type Server struct { // GetMonitoringPort returns the monitoring port, checking first the top-level config // and then the server config. -func (c *Config) GetMonitoringPort() int { +func (c *Config) GetMonitoringPort(logger *zap.Logger) int { if c.MonitoringPort != 0 { + if logger != nil { + logger.Warn("The monitoring port is set using `monitoringPort`, this option is deprecated, please migrate to `server.monitoringPort`") + } return c.MonitoringPort } return c.Server.MonitoringPort @@ -36,8 +41,11 @@ func (c *Config) GetServerHost() string { // GetServerPort returns the server port, checking first the server config // and then the top-level config, defaulting to 1031 if not set. -func (c *Config) GetServerPort() int { +func (c *Config) GetServerPort(logger *zap.Logger) int { if c.Server.Port != 0 { + if logger != nil { + logger.Warn("The server port is set using `port`, this option is deprecated, please migrate to `server.port`") + } return c.Server.Port } diff --git a/cmd/relayproxy/config/config_validator.go b/cmd/relayproxy/config/config_validator.go index e523973feb4..c7797931f66 100644 --- a/cmd/relayproxy/config/config_validator.go +++ b/cmd/relayproxy/config/config_validator.go @@ -13,8 +13,8 @@ func (c *Config) IsValid() error { if c == nil { return fmt.Errorf("empty config") } - if c.GetServerPort() == 0 { - return fmt.Errorf("invalid port %d", c.GetServerPort()) + if c.GetServerPort(nil) == 0 { + return fmt.Errorf("invalid port %d", c.GetServerPort(nil)) } if err := validateLogLevel(c.LogLevel); err != nil { return err From e4862966fc8a5fb6bf048ef9774daf6aeffee6a1 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Wed, 5 Nov 2025 18:07:46 +0000 Subject: [PATCH 04/22] remove duplicated package --- cmd/relayproxy/config/config_otel.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/relayproxy/config/config_otel.go b/cmd/relayproxy/config/config_otel.go index c76cc91dbd6..a1a377bf547 100644 --- a/cmd/relayproxy/config/config_otel.go +++ b/cmd/relayproxy/config/config_otel.go @@ -1,5 +1,4 @@ package config -package config // OpenTelemetryConfiguration is the configuration for the OpenTelemetry part of the relay proxy // It is used to configure the OpenTelemetry SDK and the OpenTelemetry Exporter @@ -47,4 +46,4 @@ type JaegerSamplerConfiguration struct { Operations int `mapstructure:"operations" koanf:"operations"` } `mapstructure:"max" koanf:"max"` } `mapstructure:"sampler" koanf:"sampler"` -} \ No newline at end of file +} From f7e568ef0559e7d90afebf72963cac69f60c72cc Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 6 Nov 2025 16:07:21 +0000 Subject: [PATCH 05/22] Support multiple starting modes --- cmd/relayproxy/api/server.go | 76 +++++++++++---- cmd/relayproxy/config/config.go | 6 ++ cmd/relayproxy/config/config_server.go | 108 +++++++++++++++++++++- cmd/relayproxy/config/config_validator.go | 4 +- 4 files changed, 171 insertions(+), 23 deletions(-) diff --git a/cmd/relayproxy/api/server.go b/cmd/relayproxy/api/server.go index 8f1487cc201..7603a58c3d7 100644 --- a/cmd/relayproxy/api/server.go +++ b/cmd/relayproxy/api/server.go @@ -4,7 +4,10 @@ import ( "context" "errors" "fmt" + "log" + "net" "net/http" + "os" "strings" "github.com/aws/aws-lambda-go/lambda" @@ -111,12 +114,59 @@ func (s *Server) initRoutes() { s.addAdminRoutes(cRetrieverRefresh) } -// Start launch the API server func (s *Server) Start() { + // start the OpenTelemetry tracing service + err := s.otelService.Init(context.Background(), s.zapLog, s.config) + if err != nil { + s.zapLog.Error( + "error while initializing OTel, continuing without tracing enabled", + zap.Error(err), + ) + // we can continue because otel is not mandatory to start the server + } + + switch s.config.GetServerMode(s.zapLog) { + case config.ServerModeLambda: + s.startAwsLambda() + case config.ServerModeUnixSocket: + s.startUnixSocketServer() + default: + s.startAsHTTPServer() + } +} + +func (s *Server) startUnixSocketServer() { + socketPath := s.config.GetUnixSocketPath() + // Clean up the old socket file if it exists (important for graceful restarts) + if _, err := os.Stat(socketPath); err == nil { + os.Remove(socketPath) + } + + listener, err := net.Listen("unix", socketPath) + if err != nil { + log.Fatalf("Error creating Unix listener: %v", err) + } + + defer listener.Close() + s.apiEcho.Listener = listener + + s.zapLog.Info( + "Starting go-feature-flag relay proxy as unix socket...", + zap.String("socket", socketPath), + zap.String("version", s.config.Version)) + + err = s.apiEcho.StartServer(new(http.Server)) + if err != nil && !errors.Is(err, http.ErrServerClosed) { + s.zapLog.Fatal("Error starting relay proxy as unix socket", zap.Error(err)) + } +} + +// startAsHTTPServer launch the API server +func (s *Server) startAsHTTPServer() { // starting the monitoring server on a different port if configured if s.monitoringEcho != nil { go func() { - addressMonitoring := fmt.Sprintf("%s:%d", s.config.GetServerHost(), s.config.GetMonitoringPort()) + addressMonitoring := fmt.Sprintf("%s:%d", s.config.GetServerHost(), s.config.GetMonitoringPort(s.zapLog)) s.zapLog.Info( "Starting monitoring", zap.String("address", addressMonitoring)) @@ -128,16 +178,6 @@ func (s *Server) Start() { defer func() { _ = s.monitoringEcho.Close() }() } - // start the OpenTelemetry tracing service - err := s.otelService.Init(context.Background(), s.zapLog, s.config) - if err != nil { - s.zapLog.Error( - "error while initializing OTel, continuing without tracing enabled", - zap.Error(err), - ) - // we can continue because otel is not mandatory to start the server - } - address := fmt.Sprintf("%s:%d", s.config.GetServerHost(), s.config.GetServerPort(s.zapLog)) s.zapLog.Info( "Starting go-feature-flag relay proxy ...", @@ -150,14 +190,10 @@ func (s *Server) Start() { } } -// StartAwsLambda is starting the relay proxy as an AWS Lambda -func (s *Server) StartAwsLambda() { - lambda.Start(s.getLambdaHandler()) -} - -func (s *Server) getLambdaHandler() interface{} { - handlerMngr := newAwsLambdaHandlerManager(s.apiEcho, s.config.AwsApiGatewayBasePath) - return handlerMngr.GetAdapter(s.config.AwsLambdaAdapter) +// startAwsLambda is starting the relay proxy as an AWS Lambda +func (s *Server) startAwsLambda() { + handlerMngr := newAwsLambdaHandlerManager(s.apiEcho, s.config.GetAwsApiGatewayBasePath()) + lambda.Start(handlerMngr.GetAdapter(s.config.GetLambdaAdapter())) } // Stop shutdown the API server diff --git a/cmd/relayproxy/config/config.go b/cmd/relayproxy/config/config.go index ab2b2b5f144..ec52e47ada8 100644 --- a/cmd/relayproxy/config/config.go +++ b/cmd/relayproxy/config/config.go @@ -110,11 +110,15 @@ type Config struct { AuthorizedKeys APIKeys `mapstructure:"authorizedKeys" koanf:"authorizedkeys"` // StartAsAwsLambda (optional) if true, the relay proxy will start ready to be launched as AWS Lambda + // + // Deprecated: use `Server.Mode = lambda` instead StartAsAwsLambda bool `mapstructure:"startAsAwsLambda" koanf:"startasawslambda"` // AwsLambdaAdapter (optional) is the adapter to use when the relay proxy is started as an AWS Lambda. // Possible values are "APIGatewayV1", "APIGatewayV2" and "ALB" // Default: "APIGatewayV2" + // + // Deprecated: use `Server.LambdaAdapter` instead AwsLambdaAdapter string `mapstructure:"awsLambdaAdapter" koanf:"awslambdaadapter"` // AwsApiGatewayBasePath (optional) is the base path prefix for AWS API Gateway deployments. @@ -122,6 +126,8 @@ type Config struct { // The relay proxy will strip this base path from incoming requests before processing. // Example: if set to "/api/feature-flags", requests to "/api/feature-flags/health" will be processed as "/health" // Default: "" + // + // Deprecated: use `Server.AwsApiGatewayBasePath` instead AwsApiGatewayBasePath string `mapstructure:"awsApiGatewayBasePath" koanf:"awsapigatewaybasepath"` // EvaluationContextEnrichment (optional) will be merged with the evaluation context sent during the evaluation. diff --git a/cmd/relayproxy/config/config_server.go b/cmd/relayproxy/config/config_server.go index 667cbcec2fd..4c3a845c05d 100644 --- a/cmd/relayproxy/config/config_server.go +++ b/cmd/relayproxy/config/config_server.go @@ -1,8 +1,35 @@ package config -import "go.uber.org/zap" +import ( + "errors" + + "go.uber.org/zap" +) + +type ServerMode = string + +const ( + // ServerModeHTTP is the HTTP server mode. + ServerModeHTTP ServerMode = "http" + // ServerModeLambda is the AWS Lambda server mode. + ServerModeLambda ServerMode = "lambda" + // ServerModeUnixSocket is the Unix Socket server mode. + ServerModeUnixSocket ServerMode = "unixsocket" +) + +type LambdaAdapter = string + +const ( + LambdaAdapterAPIGatewayV1 LambdaAdapter = "APIGatewayV1" + LambdaAdapterAPIGatewayV2 LambdaAdapter = "APIGatewayV2" + LambdaAdapterALB LambdaAdapter = "ALB" +) type Server struct { + // Mode is the server mode. + // default: http + Mode ServerMode `mapstructure:"mode" koanf:"mode"` + // Host is the server host. // default: 0.0.0.0 Host string `mapstructure:"host" koanf:"host"` @@ -11,12 +38,38 @@ type Server struct { // default: 1031 Port int `mapstructure:"port" koanf:"port"` - // MonitoringPort is the monitoring port. + // MonitoringPort is the monitoring port. It can be specified only if the server mode is HTTP. // default: none, it will use the same as server port. MonitoringPort int `mapstructure:"monitoringPort" koanf:"monitoringport"` // UnixSocket is the server unix socket path. UnixSocket string `mapstructure:"unixSocket" koanf:"unixsocket"` + + // AWS Lambda configuration + // LambdaAdapter is the adapter to use when the relay proxy is started as an AWS Lambda. + // default: APIGatewayV2 + LambdaAdapter LambdaAdapter `mapstructure:"awsLambdaAdapter" koanf:"awsLambdaAdapter"` + + // AwsApiGatewayBasePath (optional) is the base path prefix for AWS API Gateway deployments. + // This is useful when deploying behind a non-root path like "/api" or "/dev/feature-flags". + // The relay proxy will strip this base path from incoming requests before processing. + // Example: if set to "/api/feature-flags", requests to "/api/feature-flags/health" will be processed as "/health" + // Default: "" + AwsApiGatewayBasePath string `mapstructure:"awsApiGatewayBasePath" koanf:"awsapigatewaybasepath"` +} + +func (s *Server) Validate() error { + switch s.Mode { + case ServerModeUnixSocket: + if s.UnixSocket == "" { + return errors.New("unixSocket must be set when server mode is unixsocket") + } + return nil + case ServerModeLambda, ServerModeHTTP: + return nil + default: + return errors.New("invalid server mode: " + s.Mode) + } } // GetMonitoringPort returns the monitoring port, checking first the top-level config @@ -54,3 +107,54 @@ func (c *Config) GetServerPort(logger *zap.Logger) int { } return 1031 } + +// GetServerMode returns the server mode, checking first the server config +// and then the top-level config, defaulting to HTTP if not set. +func (c *Config) GetServerMode(logger *zap.Logger) ServerMode { + if c.Server.Mode != "" { + return c.Server.Mode + } + + if c.StartAsAwsLambda { + if logger != nil { + zap.L().Warn("The server mode is set using `startAsAwsLambda`, this option is deprecated, please migrate to `server.mode`") + } + return ServerModeLambda + } + + return ServerModeHTTP +} + +// GetLambdaAdapter returns the lambda adapter, checking first the server config +// and then the top-level config, defaulting to APIGatewayV2 if not set. +func (c *Config) GetLambdaAdapter(logger *zap.Logger) LambdaAdapter { + if c.Server.LambdaAdapter != "" { + return c.Server.LambdaAdapter + } + + if c.AwsLambdaAdapter != "" { + if logger != nil { + zap.L().Warn("The lambda adapter is set using `awsLambdaAdapter`, this option is deprecated, please migrate to `server.awsLambdaAdapter`") + } + return LambdaAdapter(c.AwsLambdaAdapter) + } + + return LambdaAdapterAPIGatewayV2 +} + +// GetAwsApiGatewayBasePath returns the AWS API Gateway base path, checking first the server config +// and then the top-level config, defaulting to empty string if not set. +func (c *Config) GetAwsApiGatewayBasePath(logger *zap.Logger) string { + if c.Server.AwsApiGatewayBasePath != "" { + return c.Server.AwsApiGatewayBasePath + } + + if c.AwsApiGatewayBasePath != "" { + if logger != nil { + zap.L().Warn("The AWS API Gateway base path is set using `awsApiGatewayBasePath`, this option is deprecated, please migrate to `server.awsApiGatewayBasePath`") + } + return c.AwsApiGatewayBasePath + } + + return "" +} diff --git a/cmd/relayproxy/config/config_validator.go b/cmd/relayproxy/config/config_validator.go index c7797931f66..9a76e44430a 100644 --- a/cmd/relayproxy/config/config_validator.go +++ b/cmd/relayproxy/config/config_validator.go @@ -22,7 +22,9 @@ func (c *Config) IsValid() error { if err := validateLogFormat(c.LogFormat); err != nil { return err } - + if err := c.Server.Validate(); err != nil { + return err + } if len(c.FlagSets) > 0 { return c.validateFlagSets() } From 645939782326cd041a9b32c069f22db8d2695216 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 6 Nov 2025 16:38:49 +0000 Subject: [PATCH 06/22] Adding test for server accessors --- cmd/relayproxy/config/config_server.go | 24 +- cmd/relayproxy/config/config_server_test.go | 634 ++++++++++++++++++++ 2 files changed, 649 insertions(+), 9 deletions(-) create mode 100644 cmd/relayproxy/config/config_server_test.go diff --git a/cmd/relayproxy/config/config_server.go b/cmd/relayproxy/config/config_server.go index 4c3a845c05d..f5fd0766646 100644 --- a/cmd/relayproxy/config/config_server.go +++ b/cmd/relayproxy/config/config_server.go @@ -43,7 +43,7 @@ type Server struct { MonitoringPort int `mapstructure:"monitoringPort" koanf:"monitoringport"` // UnixSocket is the server unix socket path. - UnixSocket string `mapstructure:"unixSocket" koanf:"unixsocket"` + UnixSocketPath string `mapstructure:"unixSocketPath" koanf:"unixsocketpath"` // AWS Lambda configuration // LambdaAdapter is the adapter to use when the relay proxy is started as an AWS Lambda. @@ -61,27 +61,28 @@ type Server struct { func (s *Server) Validate() error { switch s.Mode { case ServerModeUnixSocket: - if s.UnixSocket == "" { + if s.UnixSocketPath == "" { return errors.New("unixSocket must be set when server mode is unixsocket") } return nil - case ServerModeLambda, ServerModeHTTP: - return nil default: - return errors.New("invalid server mode: " + s.Mode) + return nil } } // GetMonitoringPort returns the monitoring port, checking first the top-level config // and then the server config. func (c *Config) GetMonitoringPort(logger *zap.Logger) int { + if c.Server.MonitoringPort != 0 { + return c.Server.MonitoringPort + } if c.MonitoringPort != 0 { if logger != nil { logger.Warn("The monitoring port is set using `monitoringPort`, this option is deprecated, please migrate to `server.monitoringPort`") } return c.MonitoringPort } - return c.Server.MonitoringPort + return 0 } // GetServerHost returns the server host, defaulting to "0.0.0.0" if not set. @@ -96,13 +97,13 @@ func (c *Config) GetServerHost() string { // and then the top-level config, defaulting to 1031 if not set. func (c *Config) GetServerPort(logger *zap.Logger) int { if c.Server.Port != 0 { - if logger != nil { - logger.Warn("The server port is set using `port`, this option is deprecated, please migrate to `server.port`") - } return c.Server.Port } if c.ListenPort != 0 { + if logger != nil { + logger.Warn("The server port is set using `port`, this option is deprecated, please migrate to `server.port`") + } return c.ListenPort } return 1031 @@ -158,3 +159,8 @@ func (c *Config) GetAwsApiGatewayBasePath(logger *zap.Logger) string { return "" } + +// GetUnixSocketPath returns the unix socket path. +func (c *Config) GetUnixSocketPath() string { + return c.Server.UnixSocketPath +} diff --git a/cmd/relayproxy/config/config_server_test.go b/cmd/relayproxy/config/config_server_test.go new file mode 100644 index 00000000000..6a83b91d23c --- /dev/null +++ b/cmd/relayproxy/config/config_server_test.go @@ -0,0 +1,634 @@ +package config_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest/observer" +) + +func TestConfig_GetMonitoringPort(t *testing.T) { + tests := []struct { + name string + config *config.Config + wantPort int + wantDeprecationWarned bool + setLoggerNil bool + }{ + { + name: "monitoring port from top-level config (deprecated)", + config: &config.Config{ + MonitoringPort: 8080, + Server: config.Server{ + MonitoringPort: 0, + }, + }, + wantPort: 8080, + wantDeprecationWarned: true, + }, + { + name: "monitoring port from server config", + config: &config.Config{ + MonitoringPort: 0, + Server: config.Server{ + MonitoringPort: 9090, + }, + }, + wantPort: 9090, + wantDeprecationWarned: false, + }, + { + name: "monitoring port from server takes precedence", + config: &config.Config{ + MonitoringPort: 8080, + Server: config.Server{ + MonitoringPort: 9090, + }, + }, + wantPort: 9090, + wantDeprecationWarned: false, + }, + { + name: "no monitoring port set", + config: &config.Config{ + MonitoringPort: 0, + Server: config.Server{ + MonitoringPort: 0, + }, + }, + wantPort: 0, + wantDeprecationWarned: false, + }, + { + name: "nil logger does not panic", + config: &config.Config{ + MonitoringPort: 8080, + Server: config.Server{ + MonitoringPort: 0, + }, + }, + wantPort: 8080, + wantDeprecationWarned: false, + setLoggerNil: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var logger *zap.Logger + var observedLogs *observer.ObservedLogs + + core, logs := observer.New(zapcore.WarnLevel) + logger = zap.New(core) + observedLogs = logs + + if tt.setLoggerNil { + logger = nil + } + got := tt.config.GetMonitoringPort(logger) + assert.Equal(t, tt.wantPort, got) + + if observedLogs != nil && tt.wantDeprecationWarned { + assert.Equal(t, 1, observedLogs.Len(), "expected deprecation warning") + assert.Contains(t, observedLogs.All()[0].Message, "deprecated") + } else if observedLogs != nil { + assert.Equal(t, 0, observedLogs.Len(), "expected no deprecation warning") + } + }) + } +} + +func TestConfig_GetServerHost(t *testing.T) { + tests := []struct { + name string + config *config.Config + wantHost string + }{ + { + name: "host from server config", + config: &config.Config{ + Server: config.Server{ + Host: "192.168.1.1", + }, + }, + wantHost: "192.168.1.1", + }, + { + name: "empty host returns default", + config: &config.Config{ + Server: config.Server{ + Host: "", + }, + }, + wantHost: "0.0.0.0", + }, + { + name: "localhost", + config: &config.Config{ + Server: config.Server{ + Host: "localhost", + }, + }, + wantHost: "localhost", + }, + { + name: "custom domain", + config: &config.Config{ + Server: config.Server{ + Host: "example.com", + }, + }, + wantHost: "example.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.config.GetServerHost() + assert.Equal(t, tt.wantHost, got) + }) + } +} + +func TestConfig_GetServerPort(t *testing.T) { + tests := []struct { + name string + config *config.Config + wantPort int + wantDeprecationWarned bool + setLoggerNil bool + }{ + { + name: "port from server config", + config: &config.Config{ + Server: config.Server{ + Port: 8080, + }, + ListenPort: 0, + }, + wantPort: 8080, + wantDeprecationWarned: false, + }, + { + name: "port from top-level ListenPort (deprecated)", + config: &config.Config{ + Server: config.Server{ + Port: 0, + }, + ListenPort: 9090, + }, + wantPort: 9090, + wantDeprecationWarned: true, + }, + { + name: "server port takes precedence over ListenPort", + config: &config.Config{ + Server: config.Server{ + Port: 8080, + }, + ListenPort: 9090, + }, + wantPort: 8080, + wantDeprecationWarned: false, + }, + { + name: "no port set returns default 1031", + config: &config.Config{ + Server: config.Server{ + Port: 0, + }, + ListenPort: 0, + }, + wantPort: 1031, + wantDeprecationWarned: false, + }, + { + name: "nil logger does not panic", + config: &config.Config{ + Server: config.Server{ + Port: 8080, + }, + ListenPort: 0, + }, + wantPort: 8080, + wantDeprecationWarned: false, + setLoggerNil: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var logger *zap.Logger + var observedLogs *observer.ObservedLogs + + core, logs := observer.New(zapcore.WarnLevel) + logger = zap.New(core) + observedLogs = logs + + if tt.setLoggerNil { + logger = nil + } + + got := tt.config.GetServerPort(logger) + assert.Equal(t, tt.wantPort, got) + + if observedLogs != nil && tt.wantDeprecationWarned { + assert.Equal(t, 1, observedLogs.Len(), "expected deprecation warning") + assert.Contains(t, observedLogs.All()[0].Message, "deprecated") + } else if observedLogs != nil { + assert.Equal(t, 0, observedLogs.Len(), "expected no deprecation warning") + } + }) + } +} + +func TestConfig_GetServerMode(t *testing.T) { + tests := []struct { + name string + config *config.Config + wantMode config.ServerMode + wantDeprecationWarned bool + }{ + { + name: "mode from server config - HTTP", + config: &config.Config{ + Server: config.Server{ + Mode: config.ServerModeHTTP, + }, + }, + wantMode: config.ServerModeHTTP, + wantDeprecationWarned: false, + }, + { + name: "mode from server config - Lambda", + config: &config.Config{ + Server: config.Server{ + Mode: config.ServerModeLambda, + }, + StartAsAwsLambda: false, + }, + wantMode: config.ServerModeLambda, + wantDeprecationWarned: false, + }, + { + name: "mode from server config - UnixSocket", + config: &config.Config{ + Server: config.Server{ + Mode: config.ServerModeUnixSocket, + }, + StartAsAwsLambda: false, + }, + wantMode: config.ServerModeUnixSocket, + wantDeprecationWarned: false, + }, + { + name: "mode from deprecated StartAsAwsLambda flag", + config: &config.Config{ + Server: config.Server{ + Mode: "", + }, + StartAsAwsLambda: true, + }, + wantMode: config.ServerModeLambda, + wantDeprecationWarned: true, + }, + { + name: "server mode takes precedence over deprecated flag", + config: &config.Config{ + Server: config.Server{ + Mode: config.ServerModeHTTP, + }, + StartAsAwsLambda: true, + }, + wantMode: config.ServerModeHTTP, + wantDeprecationWarned: false, + }, + { + name: "no mode set returns default HTTP", + config: &config.Config{ + Server: config.Server{ + Mode: "", + }, + StartAsAwsLambda: false, + }, + wantMode: config.ServerModeHTTP, + wantDeprecationWarned: false, + }, + { + name: "nil logger does not panic", + config: &config.Config{ + Server: config.Server{ + Mode: "", + }, + StartAsAwsLambda: true, + }, + wantMode: config.ServerModeLambda, + wantDeprecationWarned: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Note: GetServerMode uses zap.L() instead of the passed logger, + // so we need to replace the global logger to capture deprecation warnings + var logger *zap.Logger + var observedLogs *observer.ObservedLogs + var previousLogger *zap.Logger + + if tt.wantDeprecationWarned { + core, logs := observer.New(zapcore.WarnLevel) + logger = zap.New(core) + observedLogs = logs + previousLogger = zap.L() + zap.ReplaceGlobals(logger) + defer zap.ReplaceGlobals(previousLogger) + } else { + logger = zap.NewNop() + } + + got := tt.config.GetServerMode(logger) + assert.Equal(t, tt.wantMode, got) + + if observedLogs != nil && tt.wantDeprecationWarned { + assert.GreaterOrEqual(t, observedLogs.Len(), 1, "expected deprecation warning") + assert.Contains(t, observedLogs.All()[0].Message, "deprecated") + } + }) + } +} + +func TestConfig_GetLambdaAdapter(t *testing.T) { + tests := []struct { + name string + config *config.Config + wantAdapter config.LambdaAdapter + wantDeprecationWarned bool + }{ + { + name: "adapter from server config - APIGatewayV1", + config: &config.Config{ + Server: config.Server{ + LambdaAdapter: config.LambdaAdapterAPIGatewayV1, + }, + AwsLambdaAdapter: "", + }, + wantAdapter: config.LambdaAdapterAPIGatewayV1, + wantDeprecationWarned: false, + }, + { + name: "adapter from server config - APIGatewayV2", + config: &config.Config{ + Server: config.Server{ + LambdaAdapter: config.LambdaAdapterAPIGatewayV2, + }, + AwsLambdaAdapter: "", + }, + wantAdapter: config.LambdaAdapterAPIGatewayV2, + wantDeprecationWarned: false, + }, + { + name: "adapter from server config - ALB", + config: &config.Config{ + Server: config.Server{ + LambdaAdapter: config.LambdaAdapterALB, + }, + AwsLambdaAdapter: "", + }, + wantAdapter: config.LambdaAdapterALB, + wantDeprecationWarned: false, + }, + { + name: "adapter from deprecated top-level config", + config: &config.Config{ + Server: config.Server{ + LambdaAdapter: "", + }, + AwsLambdaAdapter: "APIGatewayV1", + }, + wantAdapter: config.LambdaAdapterAPIGatewayV1, + wantDeprecationWarned: true, + }, + { + name: "server adapter takes precedence over deprecated config", + config: &config.Config{ + Server: config.Server{ + LambdaAdapter: config.LambdaAdapterALB, + }, + AwsLambdaAdapter: "APIGatewayV1", + }, + wantAdapter: config.LambdaAdapterALB, + wantDeprecationWarned: false, + }, + { + name: "no adapter set returns default APIGatewayV2", + config: &config.Config{ + Server: config.Server{ + LambdaAdapter: "", + }, + AwsLambdaAdapter: "", + }, + wantAdapter: config.LambdaAdapterAPIGatewayV2, + wantDeprecationWarned: false, + }, + { + name: "nil logger does not panic", + config: &config.Config{ + Server: config.Server{ + LambdaAdapter: "", + }, + AwsLambdaAdapter: "APIGatewayV1", + }, + wantAdapter: config.LambdaAdapterAPIGatewayV1, + wantDeprecationWarned: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Note: GetLambdaAdapter uses zap.L() instead of the passed logger, + // so we need to replace the global logger to capture deprecation warnings + var logger *zap.Logger + var observedLogs *observer.ObservedLogs + var previousLogger *zap.Logger + + if tt.wantDeprecationWarned { + core, logs := observer.New(zapcore.WarnLevel) + logger = zap.New(core) + observedLogs = logs + previousLogger = zap.L() + zap.ReplaceGlobals(logger) + defer zap.ReplaceGlobals(previousLogger) + } else { + logger = zap.NewNop() + } + + got := tt.config.GetLambdaAdapter(logger) + assert.Equal(t, tt.wantAdapter, got) + + if observedLogs != nil && tt.wantDeprecationWarned { + assert.GreaterOrEqual(t, observedLogs.Len(), 1, "expected deprecation warning") + assert.Contains(t, observedLogs.All()[0].Message, "deprecated") + } + }) + } +} + +func TestConfig_GetAwsApiGatewayBasePath(t *testing.T) { + tests := []struct { + name string + config *config.Config + wantBasePath string + wantDeprecationWarned bool + }{ + { + name: "base path from server config", + config: &config.Config{ + Server: config.Server{ + AwsApiGatewayBasePath: "/api/v1", + }, + AwsApiGatewayBasePath: "", + }, + wantBasePath: "/api/v1", + wantDeprecationWarned: false, + }, + { + name: "base path from deprecated top-level config", + config: &config.Config{ + Server: config.Server{ + AwsApiGatewayBasePath: "", + }, + AwsApiGatewayBasePath: "/legacy/path", + }, + wantBasePath: "/legacy/path", + wantDeprecationWarned: true, + }, + { + name: "server base path takes precedence over deprecated config", + config: &config.Config{ + Server: config.Server{ + AwsApiGatewayBasePath: "/api/v2", + }, + AwsApiGatewayBasePath: "/api/v1", + }, + wantBasePath: "/api/v2", + wantDeprecationWarned: false, + }, + { + name: "no base path set returns empty string", + config: &config.Config{ + Server: config.Server{ + AwsApiGatewayBasePath: "", + }, + AwsApiGatewayBasePath: "", + }, + wantBasePath: "", + wantDeprecationWarned: false, + }, + { + name: "complex base path", + config: &config.Config{ + Server: config.Server{ + AwsApiGatewayBasePath: "/dev/feature-flags/v1", + }, + AwsApiGatewayBasePath: "", + }, + wantBasePath: "/dev/feature-flags/v1", + wantDeprecationWarned: false, + }, + { + name: "nil logger does not panic", + config: &config.Config{ + Server: config.Server{ + AwsApiGatewayBasePath: "", + }, + AwsApiGatewayBasePath: "/api", + }, + wantBasePath: "/api", + wantDeprecationWarned: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Note: GetAwsApiGatewayBasePath uses zap.L() instead of the passed logger, + // so we need to replace the global logger to capture deprecation warnings + var logger *zap.Logger + var observedLogs *observer.ObservedLogs + var previousLogger *zap.Logger + + if tt.wantDeprecationWarned { + core, logs := observer.New(zapcore.WarnLevel) + logger = zap.New(core) + observedLogs = logs + previousLogger = zap.L() + zap.ReplaceGlobals(logger) + defer zap.ReplaceGlobals(previousLogger) + } else { + logger = zap.NewNop() + } + + got := tt.config.GetAwsApiGatewayBasePath(logger) + assert.Equal(t, tt.wantBasePath, got) + + if observedLogs != nil && tt.wantDeprecationWarned { + assert.GreaterOrEqual(t, observedLogs.Len(), 1, "expected deprecation warning") + assert.Contains(t, observedLogs.All()[0].Message, "deprecated") + } + }) + } +} + +func TestConfig_GetUnixSocketPath(t *testing.T) { + tests := []struct { + name string + config *config.Config + wantSocketPath string + }{ + { + name: "unix socket path set", + config: &config.Config{ + Server: config.Server{ + UnixSocketPath: "/var/run/go-feature-flag.sock", + }, + }, + wantSocketPath: "/var/run/go-feature-flag.sock", + }, + { + name: "empty unix socket path", + config: &config.Config{ + Server: config.Server{ + UnixSocketPath: "", + }, + }, + wantSocketPath: "", + }, + { + name: "unix socket path with relative path", + config: &config.Config{ + Server: config.Server{ + UnixSocketPath: "./tmp/app.sock", + }, + }, + wantSocketPath: "./tmp/app.sock", + }, + { + name: "unix socket path with tmp directory", + config: &config.Config{ + Server: config.Server{ + UnixSocketPath: "/tmp/feature-flag.sock", + }, + }, + wantSocketPath: "/tmp/feature-flag.sock", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.config.GetUnixSocketPath() + assert.Equal(t, tt.wantSocketPath, got) + }) + } +} From 0f0da52312e7686c8c652b2a604fbeb2bafa1e38 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 6 Nov 2025 16:52:15 +0000 Subject: [PATCH 07/22] Change the way to start the API --- cmd/relayproxy/api/server.go | 9 ++++++--- cmd/relayproxy/main.go | 17 ++++++----------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/cmd/relayproxy/api/server.go b/cmd/relayproxy/api/server.go index 7603a58c3d7..385715094f1 100644 --- a/cmd/relayproxy/api/server.go +++ b/cmd/relayproxy/api/server.go @@ -135,6 +135,7 @@ func (s *Server) Start() { } } +// startUnixSocketServer launch the API server as a unix socket. func (s *Server) startUnixSocketServer() { socketPath := s.config.GetUnixSocketPath() // Clean up the old socket file if it exists (important for graceful restarts) @@ -210,8 +211,10 @@ func (s *Server) Stop(ctx context.Context) { } } - err = s.apiEcho.Close() - if err != nil { - s.zapLog.Fatal("impossible to stop go-feature-flag relay proxy", zap.Error(err)) + if s.apiEcho != nil { + err = s.apiEcho.Close() + if err != nil { + s.zapLog.Fatal("impossible to stop go-feature-flag relay proxy", zap.Error(err)) + } } } diff --git a/cmd/relayproxy/main.go b/cmd/relayproxy/main.go index 84f5374ebd3..4f4d70caa7a 100644 --- a/cmd/relayproxy/main.go +++ b/cmd/relayproxy/main.go @@ -103,15 +103,10 @@ func main() { } // Init API server apiServer := api.New(proxyConf, services, logger.ZapLogger) - - if proxyConf.StartAsAwsLambda { - apiServer.StartAwsLambda() - } else { - defer func() { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - apiServer.Stop(ctx) - }() - apiServer.Start() - } + defer func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + apiServer.Stop(ctx) + }() + apiServer.Start() } From e82c05d73342a80660391ba39ef236e97541583d Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 6 Nov 2025 17:00:13 +0000 Subject: [PATCH 08/22] Use logger pass in function --- cmd/relayproxy/api/server_test.go | 5 +++++ cmd/relayproxy/config/config_server.go | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/cmd/relayproxy/api/server_test.go b/cmd/relayproxy/api/server_test.go index cb2fc93521b..460b3471c18 100644 --- a/cmd/relayproxy/api/server_test.go +++ b/cmd/relayproxy/api/server_test.go @@ -2,12 +2,17 @@ package api_test import ( "context" + "fmt" + "net" "net/http" + "os" + "path/filepath" "strings" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/api" "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config" "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/log" diff --git a/cmd/relayproxy/config/config_server.go b/cmd/relayproxy/config/config_server.go index f5fd0766646..8d0de596184 100644 --- a/cmd/relayproxy/config/config_server.go +++ b/cmd/relayproxy/config/config_server.go @@ -118,7 +118,7 @@ func (c *Config) GetServerMode(logger *zap.Logger) ServerMode { if c.StartAsAwsLambda { if logger != nil { - zap.L().Warn("The server mode is set using `startAsAwsLambda`, this option is deprecated, please migrate to `server.mode`") + logger.Warn("The server mode is set using `startAsAwsLambda`, this option is deprecated, please migrate to `server.mode`") } return ServerModeLambda } @@ -135,7 +135,7 @@ func (c *Config) GetLambdaAdapter(logger *zap.Logger) LambdaAdapter { if c.AwsLambdaAdapter != "" { if logger != nil { - zap.L().Warn("The lambda adapter is set using `awsLambdaAdapter`, this option is deprecated, please migrate to `server.awsLambdaAdapter`") + logger.Warn("The lambda adapter is set using `awsLambdaAdapter`, this option is deprecated, please migrate to `server.awsLambdaAdapter`") } return LambdaAdapter(c.AwsLambdaAdapter) } From 41fc636da0bbbaf44d59da6efb58e5dc0c0e3610 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 6 Nov 2025 17:30:48 +0000 Subject: [PATCH 09/22] Add test unix socket --- cmd/relayproxy/api/routes_monitoring.go | 2 +- cmd/relayproxy/api/routes_monitoring_test.go | 2 +- cmd/relayproxy/api/server.go | 28 +- cmd/relayproxy/api/server_test.go | 409 ++++++++++++++++++- cmd/relayproxy/config/config_server.go | 14 +- cmd/relayproxy/main.go | 4 +- 6 files changed, 433 insertions(+), 26 deletions(-) diff --git a/cmd/relayproxy/api/routes_monitoring.go b/cmd/relayproxy/api/routes_monitoring.go index 880e554cff2..a07d1cfa67f 100644 --- a/cmd/relayproxy/api/routes_monitoring.go +++ b/cmd/relayproxy/api/routes_monitoring.go @@ -11,7 +11,7 @@ import ( ) func (s *Server) addMonitoringRoutes() { - if s.config.MonitoringPort != 0 { + if s.config.GetMonitoringPort(nil) != 0 { s.monitoringEcho = echo.New() s.monitoringEcho.HideBanner = true s.monitoringEcho.HidePort = true diff --git a/cmd/relayproxy/api/routes_monitoring_test.go b/cmd/relayproxy/api/routes_monitoring_test.go index 8233e3bc0b9..88d2bc10db6 100644 --- a/cmd/relayproxy/api/routes_monitoring_test.go +++ b/cmd/relayproxy/api/routes_monitoring_test.go @@ -70,7 +70,7 @@ func TestPprofEndpointsStarts(t *testing.T) { portToCheck = tt.MonitoringPort } - go apiServer.Start() + go apiServer.StartWithContext(context.TODO()) defer apiServer.Stop(context.Background()) time.Sleep(1 * time.Second) // waiting for the apiServer to start resp, err := http.Get(fmt.Sprintf("http://localhost:%d/debug/pprof/heap", portToCheck)) diff --git a/cmd/relayproxy/api/server.go b/cmd/relayproxy/api/server.go index 385715094f1..b18e359bfcd 100644 --- a/cmd/relayproxy/api/server.go +++ b/cmd/relayproxy/api/server.go @@ -114,9 +114,9 @@ func (s *Server) initRoutes() { s.addAdminRoutes(cRetrieverRefresh) } -func (s *Server) Start() { +func (s *Server) StartWithContext(ctx context.Context) { // start the OpenTelemetry tracing service - err := s.otelService.Init(context.Background(), s.zapLog, s.config) + err := s.otelService.Init(ctx, s.zapLog, s.config) if err != nil { s.zapLog.Error( "error while initializing OTel, continuing without tracing enabled", @@ -129,26 +129,27 @@ func (s *Server) Start() { case config.ServerModeLambda: s.startAwsLambda() case config.ServerModeUnixSocket: - s.startUnixSocketServer() + s.startUnixSocketServer(ctx) default: s.startAsHTTPServer() } } // startUnixSocketServer launch the API server as a unix socket. -func (s *Server) startUnixSocketServer() { +func (s *Server) startUnixSocketServer(ctx context.Context) { socketPath := s.config.GetUnixSocketPath() // Clean up the old socket file if it exists (important for graceful restarts) if _, err := os.Stat(socketPath); err == nil { - os.Remove(socketPath) + _ = os.Remove(socketPath) } - listener, err := net.Listen("unix", socketPath) + lc := net.ListenConfig{} + listener, err := lc.Listen(ctx, "unix", socketPath) if err != nil { log.Fatalf("Error creating Unix listener: %v", err) } - defer listener.Close() + defer func() { _ = listener.Close() }() s.apiEcho.Listener = listener s.zapLog.Info( @@ -185,7 +186,7 @@ func (s *Server) startAsHTTPServer() { zap.String("address", address), zap.String("version", s.config.Version)) - err = s.apiEcho.Start(address) + err := s.apiEcho.Start(address) if err != nil && !errors.Is(err, http.ErrServerClosed) { s.zapLog.Fatal("Error starting relay proxy", zap.Error(err)) } @@ -193,8 +194,15 @@ func (s *Server) startAsHTTPServer() { // startAwsLambda is starting the relay proxy as an AWS Lambda func (s *Server) startAwsLambda() { - handlerMngr := newAwsLambdaHandlerManager(s.apiEcho, s.config.GetAwsApiGatewayBasePath()) - lambda.Start(handlerMngr.GetAdapter(s.config.GetLambdaAdapter())) + lambda.Start(s.getLambdaHandler()) +} + +// getLambdaHandler returns the appropriate lambda handler based on the configuration. +// We need a dedicated function because it is called from tests as well, this is the +// reason why we can't merged it in startAwsLambda. +func (s *Server) getLambdaHandler() interface{} { + handlerMngr := newAwsLambdaHandlerManager(s.apiEcho, s.config.GetAwsApiGatewayBasePath(s.zapLog)) + return handlerMngr.GetAdapter(s.config.GetLambdaAdapter(s.zapLog)) } // Stop shutdown the API server diff --git a/cmd/relayproxy/api/server_test.go b/cmd/relayproxy/api/server_test.go index 460b3471c18..cf9f55b01b6 100644 --- a/cmd/relayproxy/api/server_test.go +++ b/cmd/relayproxy/api/server_test.go @@ -62,7 +62,7 @@ func Test_Starting_RelayProxy_with_monitoring_on_same_port(t *testing.T) { } s := api.New(proxyConf, services, log.ZapLogger) - go func() { s.Start() }() + go func() { s.StartWithContext(context.TODO()) }() defer s.Stop(context.Background()) time.Sleep(10 * time.Millisecond) @@ -120,7 +120,7 @@ func Test_Starting_RelayProxy_with_monitoring_on_different_port(t *testing.T) { } s := api.New(proxyConf, services, log.ZapLogger) - go func() { s.Start() }() + go func() { s.StartWithContext(context.TODO()) }() defer s.Stop(context.Background()) time.Sleep(10 * time.Millisecond) @@ -193,7 +193,7 @@ func Test_CheckOFREPAPIExists(t *testing.T) { } s := api.New(proxyConf, services, log.ZapLogger) - go func() { s.Start() }() + go func() { s.StartWithContext(context.TODO()) }() defer s.Stop(context.Background()) time.Sleep(10 * time.Millisecond) @@ -257,7 +257,7 @@ func Test_Middleware_VersionHeader_Enabled_Default(t *testing.T) { } s := api.New(proxyConf, services, log.ZapLogger) - go func() { s.Start() }() + go func() { s.StartWithContext(context.TODO()) }() defer s.Stop(context.Background()) time.Sleep(10 * time.Millisecond) @@ -297,7 +297,7 @@ func Test_VersionHeader_Disabled(t *testing.T) { } s := api.New(proxyConf, services, log.ZapLogger) - go func() { s.Start() }() + go func() { s.StartWithContext(context.TODO()) }() defer s.Stop(context.Background()) time.Sleep(10 * time.Millisecond) @@ -371,7 +371,7 @@ func Test_AuthenticationMiddleware(t *testing.T) { } s := api.New(proxyConf, services, log.ZapLogger) - go func() { s.Start() }() + go func() { s.StartWithContext(context.TODO()) }() defer s.Stop(context.Background()) time.Sleep(10 * time.Millisecond) @@ -446,7 +446,7 @@ func Test_AuthenticationMiddleware(t *testing.T) { } s := api.New(proxyConf, services, log.ZapLogger) - go func() { s.Start() }() + go func() { s.StartWithContext(context.TODO()) }() defer s.Stop(context.Background()) time.Sleep(10 * time.Millisecond) @@ -463,3 +463,398 @@ func Test_AuthenticationMiddleware(t *testing.T) { } }) } + +// Helper function to create an HTTP client that can connect via Unix socket +func newUnixSocketHTTPClient(socketPath string) *http.Client { + return &http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", socketPath) + }, + }, + } +} + +func Test_Starting_RelayProxy_UnixSocket(t *testing.T) { + // Create a temporary directory for the socket + tempDir, err := os.MkdirTemp("", "goff-test-socket-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + socketPath := filepath.Join(tempDir, "goff-test.sock") + + proxyConf := &config.Config{ + CommonFlagSet: config.CommonFlagSet{ + Retrievers: &[]retrieverconf.RetrieverConf{ + { + Kind: "file", + Path: "../../../testdata/flag-config.yaml", + }, + }, + }, + Server: config.Server{ + Mode: config.ServerModeUnixSocket, + UnixSocketPath: socketPath, + }, + } + log := log.InitLogger() + defer func() { _ = log.ZapLogger.Sync() }() + + metricsV2, err := metric.NewMetrics() + if err != nil { + log.ZapLogger.Error("impossible to initialize prometheus metrics", zap.Error(err)) + } + wsService := service.NewWebsocketService() + defer wsService.Close() + prometheusNotifier := metric.NewPrometheusNotifier(metricsV2) + proxyNotifier := service.NewNotifierWebsocket(wsService) + flagsetManager, err := service.NewFlagsetManager(proxyConf, log.ZapLogger, []notifier.Notifier{ + prometheusNotifier, + proxyNotifier, + }) + require.NoError(t, err) + + services := service.Services{ + MonitoringService: service.NewMonitoring(flagsetManager), + WebsocketService: wsService, + FlagsetManager: flagsetManager, + Metrics: metricsV2, + } + + s := api.New(proxyConf, services, log.ZapLogger) + go func() { s.StartWithContext(context.TODO()) }() + defer s.Stop(context.Background()) + + // Wait for the socket to be created + time.Sleep(50 * time.Millisecond) + + // Verify socket file exists + _, err = os.Stat(socketPath) + assert.NoError(t, err, "Unix socket file should exist") + + // Create a Unix socket HTTP client + client := newUnixSocketHTTPClient(socketPath) + + // Test health endpoint + response, err := client.Get("http://unix/health") + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, response.StatusCode) + + // Test metrics endpoint + responseM, err := client.Get("http://unix/metrics") + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, responseM.StatusCode) + + // Test info endpoint + responseI, err := client.Get("http://unix/info") + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, responseI.StatusCode) +} + +func Test_Starting_RelayProxy_UnixSocket_OFREP_API(t *testing.T) { + // Create a temporary directory for the socket + tempDir, err := os.MkdirTemp("", "goff-test-socket-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + socketPath := filepath.Join(tempDir, "goff-test-ofrep.sock") + + proxyConf := &config.Config{ + CommonFlagSet: config.CommonFlagSet{ + Retrievers: &[]retrieverconf.RetrieverConf{ + { + Kind: "file", + Path: "../../../testdata/flag-config.yaml", + }, + }, + }, + Server: config.Server{ + Mode: config.ServerModeUnixSocket, + UnixSocketPath: socketPath, + }, + AuthorizedKeys: config.APIKeys{ + Admin: nil, + Evaluation: []string{"test"}, + }, + } + log := log.InitLogger() + defer func() { _ = log.ZapLogger.Sync() }() + + metricsV2, err := metric.NewMetrics() + if err != nil { + log.ZapLogger.Error("impossible to initialize prometheus metrics", zap.Error(err)) + } + wsService := service.NewWebsocketService() + defer wsService.Close() + prometheusNotifier := metric.NewPrometheusNotifier(metricsV2) + proxyNotifier := service.NewNotifierWebsocket(wsService) + flagsetManager, err := service.NewFlagsetManager(proxyConf, log.ZapLogger, []notifier.Notifier{ + prometheusNotifier, + proxyNotifier, + }) + require.NoError(t, err) + + services := service.Services{ + MonitoringService: service.NewMonitoring(flagsetManager), + WebsocketService: wsService, + FlagsetManager: flagsetManager, + Metrics: metricsV2, + } + + s := api.New(proxyConf, services, log.ZapLogger) + go func() { s.StartWithContext(context.TODO()) }() + defer s.Stop(context.Background()) + + // Wait for the socket to be created + time.Sleep(50 * time.Millisecond) + + // Verify socket file exists + _, err = os.Stat(socketPath) + assert.NoError(t, err, "Unix socket file should exist") + + // Create a Unix socket HTTP client + client := newUnixSocketHTTPClient(socketPath) + + // Test OFREP evaluate all flags endpoint + req, err := http.NewRequest("POST", + "http://unix/ofrep/v1/evaluate/flags", + strings.NewReader(`{ "context":{"targetingKey":"some-key"}}`)) + require.NoError(t, err) + req.Header.Add("Authorization", "Bearer test") + req.Header.Add("Content-Type", "application/json") + response, err := client.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, response.StatusCode) + + // Test OFREP evaluate specific flag endpoint (non-existent flag) + req, err = http.NewRequest("POST", + "http://unix/ofrep/v1/evaluate/flags/some-key", + strings.NewReader(`{ "context":{"targetingKey":"some-key"}}`)) + require.NoError(t, err) + req.Header.Add("Authorization", "Bearer test") + req.Header.Add("Content-Type", "application/json") + response, err = client.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusNotFound, response.StatusCode) + + // Test OFREP evaluate specific flag endpoint (existing flag) + req, err = http.NewRequest("POST", + "http://unix/ofrep/v1/evaluate/flags/test-flag", + strings.NewReader(`{ "context":{"targetingKey":"some-key"}}`)) + require.NoError(t, err) + req.Header.Add("Authorization", "Bearer test") + req.Header.Add("Content-Type", "application/json") + response, err = client.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, response.StatusCode) +} + +func Test_Starting_RelayProxy_UnixSocket_Authentication(t *testing.T) { + tests := []struct { + name string + configAPIKeys *config.APIKeys + endpoint string + method string + body string + authHeader string + want int // http status code + }{ + { + name: "Authentication disabled - health endpoint", + configAPIKeys: nil, + endpoint: "http://unix/health", + method: "GET", + want: http.StatusOK, + }, + { + name: "Evaluation endpoint - with valid key", + configAPIKeys: &config.APIKeys{Evaluation: []string{"test-key"}}, + endpoint: "http://unix/ofrep/v1/evaluate/flags/test-flag", + method: "POST", + body: `{"context":{"targetingKey":"some-key"}}`, + authHeader: "Bearer test-key", + want: http.StatusOK, + }, + { + name: "Evaluation endpoint - without key (should fail)", + configAPIKeys: &config.APIKeys{Evaluation: []string{"test-key"}}, + endpoint: "http://unix/ofrep/v1/evaluate/flags/test-flag", + method: "POST", + body: `{"context":{"targetingKey":"some-key"}}`, + authHeader: "", + want: http.StatusUnauthorized, + }, + { + name: "Admin endpoint - with valid admin key", + configAPIKeys: &config.APIKeys{Admin: []string{"admin-key"}}, + endpoint: "http://unix/admin/v1/retriever/refresh", + method: "POST", + authHeader: "Bearer admin-key", + want: http.StatusOK, + }, + { + name: "Admin endpoint - without admin key (should fail)", + configAPIKeys: &config.APIKeys{Admin: []string{"admin-key"}}, + endpoint: "http://unix/admin/v1/retriever/refresh", + method: "POST", + authHeader: "", + want: http.StatusBadRequest, // Returns 400 when auth is required but not provided + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a temporary directory for the socket + tempDir, err := os.MkdirTemp("", "goff-test-socket-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + socketPath := filepath.Join(tempDir, fmt.Sprintf("goff-test-%s.sock", strings.ReplaceAll(tt.name, " ", "-"))) + + proxyConf := &config.Config{ + CommonFlagSet: config.CommonFlagSet{ + Retrievers: &[]retrieverconf.RetrieverConf{ + { + Kind: "file", + Path: "../../../testdata/flag-config.yaml", + }, + }, + }, + Server: config.Server{ + Mode: config.ServerModeUnixSocket, + UnixSocketPath: socketPath, + }, + } + if tt.configAPIKeys != nil { + proxyConf.AuthorizedKeys = *tt.configAPIKeys + } + + log := log.InitLogger() + defer func() { _ = log.ZapLogger.Sync() }() + + metricsV2, _ := metric.NewMetrics() + wsService := service.NewWebsocketService() + defer wsService.Close() + flagsetManager, _ := service.NewFlagsetManager(proxyConf, log.ZapLogger, nil) + + services := service.Services{ + MonitoringService: service.NewMonitoring(flagsetManager), + WebsocketService: wsService, + FlagsetManager: flagsetManager, + Metrics: metricsV2, + } + + s := api.New(proxyConf, services, log.ZapLogger) + go func() { s.StartWithContext(context.TODO()) }() + defer s.Stop(context.Background()) + + // Wait for the socket to be created + time.Sleep(50 * time.Millisecond) + + // Create a Unix socket HTTP client + client := newUnixSocketHTTPClient(socketPath) + + // Create and execute request + var req *http.Request + if tt.body != "" { + req, err = http.NewRequest(tt.method, tt.endpoint, strings.NewReader(tt.body)) + } else { + req, err = http.NewRequest(tt.method, tt.endpoint, nil) + } + require.NoError(t, err) + + if tt.authHeader != "" { + req.Header.Add("Authorization", tt.authHeader) + } + if tt.body != "" { + req.Header.Add("Content-Type", "application/json") + } + + response, err := client.Do(req) + assert.NoError(t, err) + assert.Equal(t, tt.want, response.StatusCode) + }) + } +} + +func Test_Starting_RelayProxy_UnixSocket_VersionHeader(t *testing.T) { + tests := []struct { + name string + disableVersionHeader bool + wantVersionHeader bool + }{ + { + name: "Version header enabled by default", + disableVersionHeader: false, + wantVersionHeader: true, + }, + { + name: "Version header disabled", + disableVersionHeader: true, + wantVersionHeader: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a temporary directory for the socket + tempDir, err := os.MkdirTemp("", "goff-test-socket-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + socketPath := filepath.Join(tempDir, fmt.Sprintf("goff-test-version-%s.sock", strings.ReplaceAll(tt.name, " ", "-"))) + + proxyConf := &config.Config{ + CommonFlagSet: config.CommonFlagSet{ + Retrievers: &[]retrieverconf.RetrieverConf{ + { + Kind: "file", + Path: "../../../testdata/flag-config.yaml", + }, + }, + }, + Server: config.Server{ + Mode: config.ServerModeUnixSocket, + UnixSocketPath: socketPath, + }, + DisableVersionHeader: tt.disableVersionHeader, + Version: "test-version-1.0.0", + } + + log := log.InitLogger() + defer func() { _ = log.ZapLogger.Sync() }() + + metricsV2, _ := metric.NewMetrics() + wsService := service.NewWebsocketService() + defer wsService.Close() + flagsetManager, _ := service.NewFlagsetManager(proxyConf, log.ZapLogger, nil) + + services := service.Services{ + MonitoringService: service.NewMonitoring(flagsetManager), + WebsocketService: wsService, + FlagsetManager: flagsetManager, + Metrics: metricsV2, + } + + s := api.New(proxyConf, services, log.ZapLogger) + go func() { s.StartWithContext(context.TODO()) }() + defer s.Stop(context.Background()) + + // Wait for the socket to be created + time.Sleep(50 * time.Millisecond) + + // Create a Unix socket HTTP client + client := newUnixSocketHTTPClient(socketPath) + + response, err := client.Get("http://unix/health") + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, response.StatusCode) + + if tt.wantVersionHeader { + assert.Equal(t, "test-version-1.0.0", response.Header.Get("X-GOFEATUREFLAG-VERSION")) + } else { + assert.Empty(t, response.Header.Get("X-GOFEATUREFLAG-VERSION")) + } + }) + } +} diff --git a/cmd/relayproxy/config/config_server.go b/cmd/relayproxy/config/config_server.go index 8d0de596184..407cd9f57e7 100644 --- a/cmd/relayproxy/config/config_server.go +++ b/cmd/relayproxy/config/config_server.go @@ -78,7 +78,8 @@ func (c *Config) GetMonitoringPort(logger *zap.Logger) int { } if c.MonitoringPort != 0 { if logger != nil { - logger.Warn("The monitoring port is set using `monitoringPort`, this option is deprecated, please migrate to `server.monitoringPort`") + logger.Warn("The monitoring port is set using `monitoringPort`, " + + "this option is deprecated, please migrate to `server.monitoringPort`") } return c.MonitoringPort } @@ -118,7 +119,8 @@ func (c *Config) GetServerMode(logger *zap.Logger) ServerMode { if c.StartAsAwsLambda { if logger != nil { - logger.Warn("The server mode is set using `startAsAwsLambda`, this option is deprecated, please migrate to `server.mode`") + logger.Warn("The server mode is set using `startAsAwsLambda`," + + " this option is deprecated, please migrate to `server.mode`") } return ServerModeLambda } @@ -135,9 +137,10 @@ func (c *Config) GetLambdaAdapter(logger *zap.Logger) LambdaAdapter { if c.AwsLambdaAdapter != "" { if logger != nil { - logger.Warn("The lambda adapter is set using `awsLambdaAdapter`, this option is deprecated, please migrate to `server.awsLambdaAdapter`") + logger.Warn("The lambda adapter is set using `awsLambdaAdapter`," + + " this option is deprecated, please migrate to `server.awsLambdaAdapter`") } - return LambdaAdapter(c.AwsLambdaAdapter) + return c.AwsLambdaAdapter } return LambdaAdapterAPIGatewayV2 @@ -152,7 +155,8 @@ func (c *Config) GetAwsApiGatewayBasePath(logger *zap.Logger) string { if c.AwsApiGatewayBasePath != "" { if logger != nil { - zap.L().Warn("The AWS API Gateway base path is set using `awsApiGatewayBasePath`, this option is deprecated, please migrate to `server.awsApiGatewayBasePath`") + zap.L().Warn("The AWS API Gateway base path is set using `awsApiGatewayBasePath`, " + + "this option is deprecated, please migrate to `server.awsApiGatewayBasePath`") } return c.AwsApiGatewayBasePath } diff --git a/cmd/relayproxy/main.go b/cmd/relayproxy/main.go index 4f4d70caa7a..8dcef7fd944 100644 --- a/cmd/relayproxy/main.go +++ b/cmd/relayproxy/main.go @@ -71,7 +71,7 @@ func main() { // Init swagger docs.SwaggerInfo.Version = proxyConf.Version - docs.SwaggerInfo.Host = fmt.Sprintf("%s:%d", proxyConf.Host, proxyConf.ListenPort) + docs.SwaggerInfo.Host = fmt.Sprintf("%s:%d", proxyConf.Host, proxyConf.GetServerPort(logger.ZapLogger)) // Init services metricsV2, err := metric.NewMetrics() @@ -108,5 +108,5 @@ func main() { defer cancel() apiServer.Stop(ctx) }() - apiServer.Start() + apiServer.StartWithContext(context.Background()) } From 8a12f6fa6c01d3d17622b05daff0fe66b620e9da Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 6 Nov 2025 18:17:45 +0000 Subject: [PATCH 10/22] Replace LISTEN --- README.md | 4 +- cmd/relayproxy/api/routes_monitoring_test.go | 7 +- cmd/relayproxy/api/server_test.go | 37 +++- cmd/relayproxy/config/config.go | 1 - cmd/relayproxy/config/config_test.go | 170 +++++++++++------ .../helm-charts/relay-proxy/README.md | 10 +- .../helm-charts/relay-proxy/README.md.gotmpl | 11 +- .../helm-charts/relay-proxy/values.yaml | 8 +- .../testdata/config/invalid-yaml.yaml | 5 +- .../config/valid-env-exporters-kafka.yaml | 6 +- .../testdata/config/valid-file-notifier.yaml | 6 +- .../testdata/config/valid-file-notifiers.yaml | 6 +- .../testdata/config/valid-file.json | 5 +- .../testdata/config/valid-file.toml | 5 +- .../testdata/config/valid-file.yaml | 4 +- .../testdata/config/valid-otel.yaml | 4 +- .../valid-yaml-exporter-and-exporters.yaml | 4 +- .../config/valid-yaml-multiple-exporters.yaml | 4 +- .../testdata/config/valid-yaml-notifier.yaml | 4 +- .../validate-array-env-file-envprefix.yaml | 4 +- .../validate-array-env-file-flagset.yaml | 4 +- .../validate-array-env-file-legacy.yaml | 4 +- .../config/validate-array-env-file.yaml | 5 +- .../dockerhub-example/goff-proxy.yaml | 4 +- .../openfeature_kotlin_server/goff-proxy.yaml | 8 +- examples/openfeature_nodejs/goff-proxy.yaml | 8 +- examples/openfeature_react/goff-proxy.yaml | 8 +- examples/openfeature_web/goff-proxy.yaml | 8 +- .../goff-proxy-authenticated.yaml | 6 +- openfeature/provider_tests/goff-proxy.yaml | 6 +- .../python-provider/tests/docker-compose.yml | 3 +- .../2025-09-18-introducing-flagsets/index.md | 4 +- .../relay-proxy/configure-relay-proxy.mdx | 174 +++++++++++------- .../getting_started/using-openfeature.md | 4 +- 34 files changed, 375 insertions(+), 176 deletions(-) diff --git a/README.md b/README.md index d02da0e1620..2a81e505646 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,9 @@ This flag split the usage of this flag, 20% will use the variation `my-new-featu Create a new `YAML` file containing the configuration of your relay proxy. ```yaml title="goff-proxy.yaml" -listen: 1031 +server: + mode: http + port: 1031 pollingInterval: 1000 startWithRetrieverError: false retriever: diff --git a/cmd/relayproxy/api/routes_monitoring_test.go b/cmd/relayproxy/api/routes_monitoring_test.go index 88d2bc10db6..97dea52481c 100644 --- a/cmd/relayproxy/api/routes_monitoring_test.go +++ b/cmd/relayproxy/api/routes_monitoring_test.go @@ -51,8 +51,11 @@ func TestPprofEndpointsStarts(t *testing.T) { }, }, }, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, MonitoringPort: tt.MonitoringPort, - ListenPort: 1031, EnablePprof: tt.EnablePprof, } @@ -65,7 +68,7 @@ func TestPprofEndpointsStarts(t *testing.T) { Metrics: metric.Metrics{}, }, z) - portToCheck := c.ListenPort + portToCheck := c.GetServerPort(nil) if tt.MonitoringPort != 0 { portToCheck = tt.MonitoringPort } diff --git a/cmd/relayproxy/api/server_test.go b/cmd/relayproxy/api/server_test.go index cf9f55b01b6..01e675ef8d8 100644 --- a/cmd/relayproxy/api/server_test.go +++ b/cmd/relayproxy/api/server_test.go @@ -33,7 +33,10 @@ func Test_Starting_RelayProxy_with_monitoring_on_same_port(t *testing.T) { }, }, }, - ListenPort: 11024, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 11024, + }, } log := log.InitLogger() defer func() { _ = log.ZapLogger.Sync() }() @@ -90,8 +93,11 @@ func Test_Starting_RelayProxy_with_monitoring_on_different_port(t *testing.T) { }, }, }, - ListenPort: 11024, - MonitoringPort: 11025, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 11024, + MonitoringPort: 11025, + }, } log := log.InitLogger() defer func() { _ = log.ZapLogger.Sync() }() @@ -160,7 +166,10 @@ func Test_CheckOFREPAPIExists(t *testing.T) { }, }, }, - ListenPort: 11024, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 11024, + }, AuthorizedKeys: config.APIKeys{ Admin: nil, Evaluation: []string{"test"}, @@ -239,7 +248,10 @@ func Test_Middleware_VersionHeader_Enabled_Default(t *testing.T) { }, }, }, - ListenPort: 11024, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 11024, + }, } log := log.InitLogger() defer func() { _ = log.ZapLogger.Sync() }() @@ -278,7 +290,10 @@ func Test_VersionHeader_Disabled(t *testing.T) { }, }, }, - ListenPort: 11024, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 11024, + }, DisableVersionHeader: true, } log := log.InitLogger() @@ -348,7 +363,10 @@ func Test_AuthenticationMiddleware(t *testing.T) { }, }, }, - ListenPort: 11024, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 11024, + }, DisableVersionHeader: true, } if tt.configAPIKeys != nil { @@ -423,7 +441,10 @@ func Test_AuthenticationMiddleware(t *testing.T) { }, }, }, - ListenPort: 11024, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 11024, + }, DisableVersionHeader: true, } if tt.configAPIKeys != nil { diff --git a/cmd/relayproxy/config/config.go b/cmd/relayproxy/config/config.go index ec52e47ada8..8fac4657fd2 100644 --- a/cmd/relayproxy/config/config.go +++ b/cmd/relayproxy/config/config.go @@ -184,7 +184,6 @@ func New(flagSet *pflag.FlagSet, log *zap.Logger, version string) (*Config, erro // Default values _ = k.Load(confmap.Provider(map[string]interface{}{ - "listen": "1031", "host": "localhost", "fileFormat": "yaml", "pollingInterval": 60000, diff --git a/cmd/relayproxy/config/config_test.go b/cmd/relayproxy/config/config_test.go index b6a63efc95b..33af750c76d 100644 --- a/cmd/relayproxy/config/config_test.go +++ b/cmd/relayproxy/config/config_test.go @@ -39,7 +39,10 @@ func TestParseConfig_fileFromPflag(t *testing.T) { }, StartWithRetrieverError: false, }, - ListenPort: 1031, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, Host: "localhost", Version: "1.X.X", EnableSwagger: true, @@ -75,7 +78,10 @@ func TestParseConfig_fileFromPflag(t *testing.T) { config.NotifierConf{Kind: config.DiscordNotifier, WebhookURL: "https://discord.com/api/webhooks/yyyy/xxxxxxx"}, }, }, - ListenPort: 1031, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, Host: "localhost", Version: "1.X.X", EnableSwagger: true, @@ -112,7 +118,10 @@ func TestParseConfig_fileFromPflag(t *testing.T) { config.NotifierConf{Kind: config.DiscordNotifier, WebhookURL: "https://discord.com/api/webhooks/yyyy/xxxxxxx"}, }, }, - ListenPort: 1031, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, Host: "localhost", Version: "1.X.X", EnableSwagger: true, @@ -152,7 +161,10 @@ func TestParseConfig_fileFromPflag(t *testing.T) { }, StartWithRetrieverError: false, }, - ListenPort: 1031, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, Host: "localhost", Version: "1.X.X", EnableSwagger: true, @@ -195,7 +207,10 @@ func TestParseConfig_fileFromPflag(t *testing.T) { }, StartWithRetrieverError: false, }, - ListenPort: 1031, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, Host: "localhost", @@ -230,8 +245,11 @@ func TestParseConfig_fileFromPflag(t *testing.T) { }, StartWithRetrieverError: false, }, - ListenPort: 1031, - Host: "localhost", + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, + Host: "localhost", Version: "1.X.X", EnableSwagger: true, @@ -259,7 +277,10 @@ func TestParseConfig_fileFromPflag(t *testing.T) { }, StartWithRetrieverError: false, }, - ListenPort: 1031, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, Host: "localhost", @@ -282,10 +303,13 @@ func TestParseConfig_fileFromPflag(t *testing.T) { FileFormat: "yaml", StartWithRetrieverError: false, }, - ListenPort: 1031, - Host: "localhost", - Version: "1.X.X", - LogLevel: config.DefaultLogLevel, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, + Host: "localhost", + Version: "1.X.X", + LogLevel: config.DefaultLogLevel, }, wantErr: assert.NoError, }, @@ -308,10 +332,13 @@ func TestParseConfig_fileFromPflag(t *testing.T) { }, }, }, - ListenPort: 1031, - Host: "localhost", - LogLevel: config.DefaultLogLevel, - Version: "1.X.X", + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, + Host: "localhost", + LogLevel: config.DefaultLogLevel, + Version: "1.X.X", OtelConfig: config.OpenTelemetryConfiguration{ Exporter: config.OtelExporter{ Otlp: config.OtelExporterOtlp{ @@ -369,7 +396,10 @@ func TestParseConfig_fileFromFolder(t *testing.T) { }, StartWithRetrieverError: false, }, - ListenPort: 1031, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, Host: "localhost", Version: "1.X.X", EnableSwagger: true, @@ -395,10 +425,13 @@ func TestParseConfig_fileFromFolder(t *testing.T) { FileFormat: "yaml", StartWithRetrieverError: false, }, - ListenPort: 1031, - Host: "localhost", - Version: "1.X.X", - LogLevel: config.DefaultLogLevel, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, + Host: "localhost", + Version: "1.X.X", + LogLevel: config.DefaultLogLevel, }, wantErr: assert.NoError, }, @@ -417,10 +450,13 @@ func TestParseConfig_fileFromFolder(t *testing.T) { FileFormat: "yaml", StartWithRetrieverError: false, }, - ListenPort: 1031, - Host: "localhost", - Version: "1.X.X", - LogLevel: config.DefaultLogLevel, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, + Host: "localhost", + Version: "1.X.X", + LogLevel: config.DefaultLogLevel, }, }, { @@ -433,10 +469,13 @@ func TestParseConfig_fileFromFolder(t *testing.T) { FileFormat: "yaml", StartWithRetrieverError: false, }, - ListenPort: 1031, - Host: "localhost", - Version: "1.X.X", - LogLevel: config.DefaultLogLevel, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, + Host: "localhost", + Version: "1.X.X", + LogLevel: config.DefaultLogLevel, }, }, { @@ -449,10 +488,13 @@ func TestParseConfig_fileFromFolder(t *testing.T) { FileFormat: "yaml", StartWithRetrieverError: false, }, - ListenPort: 1031, - Host: "localhost", - Version: "1.X.X", - LogLevel: config.DefaultLogLevel, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, + Host: "localhost", + Version: "1.X.X", + LogLevel: config.DefaultLogLevel, }, disableDefaultFileCreation: true, }, @@ -872,7 +914,10 @@ func TestConfig_IsValid(t *testing.T) { Notifiers: tt.fields.Notifiers, Retrievers: tt.fields.Retrievers, }, - ListenPort: tt.fields.ListenPort, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: tt.fields.ListenPort, + }, HideBanner: tt.fields.HideBanner, EnableSwagger: tt.fields.EnableSwagger, Host: tt.fields.Host, @@ -1231,7 +1276,10 @@ func TestMergeConfig_FromOSEnv(t *testing.T) { }, StartWithRetrieverError: false, }, - ListenPort: 1031, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, Host: "localhost", Version: "1.X.X", EnableSwagger: true, @@ -1309,7 +1357,10 @@ func TestMergeConfig_FromOSEnv(t *testing.T) { }, StartWithRetrieverError: false, }, - ListenPort: 1031, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, Host: "localhost", Version: "1.X.X", EnableSwagger: true, @@ -1382,10 +1433,13 @@ func TestMergeConfig_FromOSEnv(t *testing.T) { StartWithRetrieverError: false, }, EnvVariablePrefix: "GOFF_", - ListenPort: 1031, - Host: "localhost", - Version: "1.X.X", - EnableSwagger: true, + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, + Host: "localhost", + Version: "1.X.X", + EnableSwagger: true, AuthorizedKeys: config.APIKeys{ Admin: []string{ "apikey3", @@ -1443,8 +1497,11 @@ func TestMergeConfig_FromOSEnv(t *testing.T) { }, StartWithRetrieverError: false, }, - ListenPort: 1031, - Host: "localhost", + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, + Host: "localhost", AuthorizedKeys: config.APIKeys{ Admin: []string{ @@ -1498,8 +1555,11 @@ func TestMergeConfig_FromOSEnv(t *testing.T) { }, StartWithRetrieverError: false, }, - ListenPort: 1031, - Host: "localhost", + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, + Host: "localhost", AuthorizedKeys: config.APIKeys{ Admin: []string{ @@ -1534,10 +1594,13 @@ func TestMergeConfig_FromOSEnv(t *testing.T) { }, }, }, - ListenPort: 1031, - Host: "localhost", - LogLevel: config.DefaultLogLevel, - Version: "1.X.X", + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, + Host: "localhost", + LogLevel: config.DefaultLogLevel, + Version: "1.X.X", OtelConfig: config.OpenTelemetryConfiguration{ Exporter: config.OtelExporter{ Otlp: config.OtelExporterOtlp{ @@ -1568,10 +1631,13 @@ func TestMergeConfig_FromOSEnv(t *testing.T) { FileFormat: "yaml", PollingInterval: 60000, }, - ListenPort: 1031, - Host: "localhost", - LogLevel: config.DefaultLogLevel, - Version: "1.X.X", + Server: config.Server{ + Mode: config.ServerModeHTTP, + Port: 1031, + }, + Host: "localhost", + LogLevel: config.DefaultLogLevel, + Version: "1.X.X", FlagSets: []config.FlagSet{ { Name: "xxx", diff --git a/cmd/relayproxy/helm-charts/relay-proxy/README.md b/cmd/relayproxy/helm-charts/relay-proxy/README.md index 277a98012cd..27a02104d19 100644 --- a/cmd/relayproxy/helm-charts/relay-proxy/README.md +++ b/cmd/relayproxy/helm-charts/relay-proxy/README.md @@ -32,8 +32,10 @@ The Helm chart supports an optional `monitoringPort` configuration that allows y ```yaml relayproxy: config: | - listen: 1031 - monitoringPort: 1032 + server: + mode: http + port: 1031 + monitoringPort: 1032 pollingInterval: 1000 startWithRetrieverError: false logLevel: debug @@ -54,7 +56,9 @@ When `monitoringPort` is configured: ```yaml relayproxy: config: | - listen: 1031 + server: + mode: http + port: 1031 pollingInterval: 1000 startWithRetrieverError: false logLevel: debug diff --git a/cmd/relayproxy/helm-charts/relay-proxy/README.md.gotmpl b/cmd/relayproxy/helm-charts/relay-proxy/README.md.gotmpl index 9444dddbef6..d3604937e9e 100644 --- a/cmd/relayproxy/helm-charts/relay-proxy/README.md.gotmpl +++ b/cmd/relayproxy/helm-charts/relay-proxy/README.md.gotmpl @@ -31,8 +31,10 @@ The Helm chart supports an optional `monitoringPort` configuration that allows y ```yaml relayproxy: config: | - listen: 1031 - monitoringPort: 1032 + server: + mode: http + port: 1031 + monitoringPort: 1032 pollingInterval: 1000 startWithRetrieverError: false logLevel: debug @@ -53,7 +55,10 @@ When `monitoringPort` is configured: ```yaml relayproxy: config: | - listen: 1031 + server: + mode: http + port: 1031 + monitoringPort: 1032 pollingInterval: 1000 startWithRetrieverError: false logLevel: debug diff --git a/cmd/relayproxy/helm-charts/relay-proxy/values.yaml b/cmd/relayproxy/helm-charts/relay-proxy/values.yaml index 6f5068bae54..4f891c83ec6 100644 --- a/cmd/relayproxy/helm-charts/relay-proxy/values.yaml +++ b/cmd/relayproxy/helm-charts/relay-proxy/values.yaml @@ -1,7 +1,10 @@ relayproxy: # -- GO Feature Flag relay proxy configuration as string (accept template). If monitoringPort is specified in the config, it will be exposed as a separate port and used for liveness and readiness probes instead of the main HTTP port. Example: add "monitoringPort: 1032" to your config to enable separate monitoring port. config: | # This is a configuration example for the relay-proxy - listen: 1031 + server: + mode: http + port: 1031 + monitoringPort: 1032 pollingInterval: 1000 startWithRetrieverError: false logLevel: info @@ -11,11 +14,10 @@ relayproxy: exporter: kind: log # envVariablePrefix: "GOFFPROXY_" - # -- Environment variables to pass to the relay proxy env: {} # Examples: -# LISTEN: 1032 +# SERVER_PORT: 1032 # # ENV_VARIABLE_FROM_SECRET: # valueFrom: diff --git a/cmd/relayproxy/testdata/config/invalid-yaml.yaml b/cmd/relayproxy/testdata/config/invalid-yaml.yaml index 91178395061..92948375624 100644 --- a/cmd/relayproxy/testdata/config/invalid-yaml.yaml +++ b/cmd/relayproxy/testdata/config/invalid-yaml.yaml @@ -1,4 +1,7 @@ -listen: 1031 +server: + mode: http + port: 1031 + monitoringPort: 1032 - test pollingInterval: 1000 startWithRetrieverError: false diff --git a/cmd/relayproxy/testdata/config/valid-env-exporters-kafka.yaml b/cmd/relayproxy/testdata/config/valid-env-exporters-kafka.yaml index 211f24ccf16..5773286f14f 100644 --- a/cmd/relayproxy/testdata/config/valid-env-exporters-kafka.yaml +++ b/cmd/relayproxy/testdata/config/valid-env-exporters-kafka.yaml @@ -1,4 +1,6 @@ -listen: 1031 +server: + mode: http + port: 1031 pollingInterval: 1000 startWithRetrieverError: false retrievers: @@ -12,7 +14,7 @@ exporters: - kind: kafka kafka: topic: svc-goff.evaluation - addresses: [ kafka:9092 ] + addresses: [kafka:9092] enableSwagger: true authorizedKeys: evaluation: diff --git a/cmd/relayproxy/testdata/config/valid-file-notifier.yaml b/cmd/relayproxy/testdata/config/valid-file-notifier.yaml index 6088efe9ac4..c27fc628519 100644 --- a/cmd/relayproxy/testdata/config/valid-file-notifier.yaml +++ b/cmd/relayproxy/testdata/config/valid-file-notifier.yaml @@ -1,4 +1,6 @@ -listen: 1031 +server: + mode: http + port: 1031 pollingInterval: 1000 startWithRetrieverError: false retriever: @@ -16,4 +18,4 @@ authorizedKeys: loglevel: info notifier: - kind: discord - webhookUrl: "https://discord.com/api/webhooks/yyyy/xxxxxxx" \ No newline at end of file + webhookUrl: "https://discord.com/api/webhooks/yyyy/xxxxxxx" diff --git a/cmd/relayproxy/testdata/config/valid-file-notifiers.yaml b/cmd/relayproxy/testdata/config/valid-file-notifiers.yaml index 8dc2f72a2fc..30b347a9970 100644 --- a/cmd/relayproxy/testdata/config/valid-file-notifiers.yaml +++ b/cmd/relayproxy/testdata/config/valid-file-notifiers.yaml @@ -1,4 +1,6 @@ -listen: 1031 +server: + mode: http + port: 1031 pollingInterval: 1000 startWithRetrieverError: false retriever: @@ -16,4 +18,4 @@ authorizedKeys: loglevel: info notifiers: - kind: discord - webhookUrl: "https://discord.com/api/webhooks/yyyy/xxxxxxx" \ No newline at end of file + webhookUrl: "https://discord.com/api/webhooks/yyyy/xxxxxxx" diff --git a/cmd/relayproxy/testdata/config/valid-file.json b/cmd/relayproxy/testdata/config/valid-file.json index 57ba92fb1f5..ba2c8d4bdc3 100644 --- a/cmd/relayproxy/testdata/config/valid-file.json +++ b/cmd/relayproxy/testdata/config/valid-file.json @@ -1,5 +1,8 @@ { - "listen": 1031, + "server": { + "mode": "http", + "port": 1031 + }, "pollingInterval": 1000, "startWithRetrieverError": false, "retriever": { diff --git a/cmd/relayproxy/testdata/config/valid-file.toml b/cmd/relayproxy/testdata/config/valid-file.toml index f8dce8cee7f..91d7cc13ec1 100644 --- a/cmd/relayproxy/testdata/config/valid-file.toml +++ b/cmd/relayproxy/testdata/config/valid-file.toml @@ -1,9 +1,12 @@ -listen = 1031.00 pollingInterval = 1000.00 startWithRetrieverError = false enableSwagger = true apiKeys = [ "apikey1", "apikey2" ] +[server] +port = 1031.00 +mode = http + [retriever] kind = "http" url = "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/examples/retriever_file/flags.goff.yaml" diff --git a/cmd/relayproxy/testdata/config/valid-file.yaml b/cmd/relayproxy/testdata/config/valid-file.yaml index 462bab68026..87b3425ab8c 100644 --- a/cmd/relayproxy/testdata/config/valid-file.yaml +++ b/cmd/relayproxy/testdata/config/valid-file.yaml @@ -1,4 +1,6 @@ -listen: 1031 +server: + mode: http + port: 1031 pollingInterval: 1000 startWithRetrieverError: false retriever: diff --git a/cmd/relayproxy/testdata/config/valid-otel.yaml b/cmd/relayproxy/testdata/config/valid-otel.yaml index 82b24f4fabf..f79f5d7df9f 100644 --- a/cmd/relayproxy/testdata/config/valid-otel.yaml +++ b/cmd/relayproxy/testdata/config/valid-otel.yaml @@ -1,4 +1,6 @@ -listen: 1031 +server: + mode: http + port: 1031 retrievers: - kind: file path: examples/retriever_file/flags.goff.yaml diff --git a/cmd/relayproxy/testdata/config/valid-yaml-exporter-and-exporters.yaml b/cmd/relayproxy/testdata/config/valid-yaml-exporter-and-exporters.yaml index f5d8efd458f..191e3db1247 100644 --- a/cmd/relayproxy/testdata/config/valid-yaml-exporter-and-exporters.yaml +++ b/cmd/relayproxy/testdata/config/valid-yaml-exporter-and-exporters.yaml @@ -1,4 +1,6 @@ -listen: 1031 +server: + mode: http + port: 1031 pollingInterval: 1000 startWithRetrieverError: false retriever: diff --git a/cmd/relayproxy/testdata/config/valid-yaml-multiple-exporters.yaml b/cmd/relayproxy/testdata/config/valid-yaml-multiple-exporters.yaml index 4aa85676745..f9d82c6e787 100644 --- a/cmd/relayproxy/testdata/config/valid-yaml-multiple-exporters.yaml +++ b/cmd/relayproxy/testdata/config/valid-yaml-multiple-exporters.yaml @@ -1,4 +1,6 @@ -listen: 1031 +server: + mode: http + port: 1031 pollingInterval: 1000 startWithRetrieverError: false retriever: diff --git a/cmd/relayproxy/testdata/config/valid-yaml-notifier.yaml b/cmd/relayproxy/testdata/config/valid-yaml-notifier.yaml index 8fa72cb2929..82fa1f3dd01 100644 --- a/cmd/relayproxy/testdata/config/valid-yaml-notifier.yaml +++ b/cmd/relayproxy/testdata/config/valid-yaml-notifier.yaml @@ -1,4 +1,6 @@ -listen: 1031 +server: + mode: http + port: 1031 pollingInterval: 1000 startWithRetrieverError: false retriever: diff --git a/cmd/relayproxy/testdata/config/validate-array-env-file-envprefix.yaml b/cmd/relayproxy/testdata/config/validate-array-env-file-envprefix.yaml index 59be8498574..e09f3ce8202 100644 --- a/cmd/relayproxy/testdata/config/validate-array-env-file-envprefix.yaml +++ b/cmd/relayproxy/testdata/config/validate-array-env-file-envprefix.yaml @@ -1,4 +1,6 @@ -listen: 1031 +server: + mode: http + port: 1031 envVariablePrefix: GOFF_ pollingInterval: 1000 startWithRetrieverError: false diff --git a/cmd/relayproxy/testdata/config/validate-array-env-file-flagset.yaml b/cmd/relayproxy/testdata/config/validate-array-env-file-flagset.yaml index 857849ffd6e..40dfa5be784 100644 --- a/cmd/relayproxy/testdata/config/validate-array-env-file-flagset.yaml +++ b/cmd/relayproxy/testdata/config/validate-array-env-file-flagset.yaml @@ -1,4 +1,6 @@ -listen: 1031 +server: + mode: http + port: 1031 logLevel: info flagsets: - name: flagset1 diff --git a/cmd/relayproxy/testdata/config/validate-array-env-file-legacy.yaml b/cmd/relayproxy/testdata/config/validate-array-env-file-legacy.yaml index f4888c978eb..cb01635f6c2 100644 --- a/cmd/relayproxy/testdata/config/validate-array-env-file-legacy.yaml +++ b/cmd/relayproxy/testdata/config/validate-array-env-file-legacy.yaml @@ -1,4 +1,6 @@ -listen: 1031 +server: + mode: http + port: 1031 pollingInterval: 1000 startWithRetrieverError: false retrievers: diff --git a/cmd/relayproxy/testdata/config/validate-array-env-file.yaml b/cmd/relayproxy/testdata/config/validate-array-env-file.yaml index 42512faa0cf..82336703f5c 100644 --- a/cmd/relayproxy/testdata/config/validate-array-env-file.yaml +++ b/cmd/relayproxy/testdata/config/validate-array-env-file.yaml @@ -1,4 +1,6 @@ -listen: 1031 +server: + mode: http + port: 1031 pollingInterval: 1000 startWithRetrieverError: false retrievers: @@ -21,4 +23,3 @@ logLevel: info notifiers: - kind: "webhook" endpointUrl: "http://localhost:8080/webhook" - diff --git a/cmd/relayproxy/testdata/dockerhub-example/goff-proxy.yaml b/cmd/relayproxy/testdata/dockerhub-example/goff-proxy.yaml index 0bf2bef556b..28e0cd1c0d3 100644 --- a/cmd/relayproxy/testdata/dockerhub-example/goff-proxy.yaml +++ b/cmd/relayproxy/testdata/dockerhub-example/goff-proxy.yaml @@ -1,7 +1,9 @@ # This is an example of a configuration file for the relay proxy. # The port on which the relay proxy will listen to. -listen: 1031 +server: + mode: http + port: 1031 # The interval in milliseconds to check if your feature flag configuration has changed. pollingInterval: 1000 diff --git a/examples/openfeature_kotlin_server/goff-proxy.yaml b/examples/openfeature_kotlin_server/goff-proxy.yaml index 94aee32bf82..b68d7e4d288 100644 --- a/examples/openfeature_kotlin_server/goff-proxy.yaml +++ b/examples/openfeature_kotlin_server/goff-proxy.yaml @@ -1,5 +1,7 @@ -listen: 1031 +server: + mode: http + port: 1031 pollingInterval: 1000 retriever: - kind: file - path: /goff/config.goff.yaml + kind: file + path: /goff/config.goff.yaml diff --git a/examples/openfeature_nodejs/goff-proxy.yaml b/examples/openfeature_nodejs/goff-proxy.yaml index 94aee32bf82..b68d7e4d288 100644 --- a/examples/openfeature_nodejs/goff-proxy.yaml +++ b/examples/openfeature_nodejs/goff-proxy.yaml @@ -1,5 +1,7 @@ -listen: 1031 +server: + mode: http + port: 1031 pollingInterval: 1000 retriever: - kind: file - path: /goff/config.goff.yaml + kind: file + path: /goff/config.goff.yaml diff --git a/examples/openfeature_react/goff-proxy.yaml b/examples/openfeature_react/goff-proxy.yaml index 94aee32bf82..b68d7e4d288 100644 --- a/examples/openfeature_react/goff-proxy.yaml +++ b/examples/openfeature_react/goff-proxy.yaml @@ -1,5 +1,7 @@ -listen: 1031 +server: + mode: http + port: 1031 pollingInterval: 1000 retriever: - kind: file - path: /goff/config.goff.yaml + kind: file + path: /goff/config.goff.yaml diff --git a/examples/openfeature_web/goff-proxy.yaml b/examples/openfeature_web/goff-proxy.yaml index 94aee32bf82..b68d7e4d288 100644 --- a/examples/openfeature_web/goff-proxy.yaml +++ b/examples/openfeature_web/goff-proxy.yaml @@ -1,5 +1,7 @@ -listen: 1031 +server: + mode: http + port: 1031 pollingInterval: 1000 retriever: - kind: file - path: /goff/config.goff.yaml + kind: file + path: /goff/config.goff.yaml diff --git a/openfeature/provider_tests/goff-proxy-authenticated.yaml b/openfeature/provider_tests/goff-proxy-authenticated.yaml index fe231a27228..49208a75b3c 100644 --- a/openfeature/provider_tests/goff-proxy-authenticated.yaml +++ b/openfeature/provider_tests/goff-proxy-authenticated.yaml @@ -1,4 +1,6 @@ -listen: 1032 +server: + mode: http + port: 1032 pollingInterval: 1000 startWithRetrieverError: false retriever: @@ -9,4 +11,4 @@ exporter: authorizedKeys: evaluation: - authorized_token -enableSwagger: true \ No newline at end of file +enableSwagger: true diff --git a/openfeature/provider_tests/goff-proxy.yaml b/openfeature/provider_tests/goff-proxy.yaml index 7e3d99392e0..e5133e29ce7 100644 --- a/openfeature/provider_tests/goff-proxy.yaml +++ b/openfeature/provider_tests/goff-proxy.yaml @@ -1,4 +1,6 @@ -listen: 1031 +server: + mode: http + port: 1031 pollingInterval: 1000 startWithRetrieverError: false retriever: @@ -8,4 +10,4 @@ exporter: kind: log enableSwagger: true evaluationContextEnrichment: - environment: integration-test \ No newline at end of file + environment: integration-test diff --git a/openfeature/providers/python-provider/tests/docker-compose.yml b/openfeature/providers/python-provider/tests/docker-compose.yml index 60b3d7131bf..d4b5bc5ef12 100644 --- a/openfeature/providers/python-provider/tests/docker-compose.yml +++ b/openfeature/providers/python-provider/tests/docker-compose.yml @@ -5,7 +5,8 @@ services: ports: - "1031:1031" environment: - - LISTEN=1031 + - SERVER_PORT=1031 + - SERVER_MODE=http - POLLINGINTERVAL=1000 - RETRIEVER_KIND=file - RETRIEVER_PATH=/config.goff.yaml diff --git a/website/blog/2025-09-18-introducing-flagsets/index.md b/website/blog/2025-09-18-introducing-flagsets/index.md index 5602dc2e1d6..d624880c30e 100644 --- a/website/blog/2025-09-18-introducing-flagsets/index.md +++ b/website/blog/2025-09-18-introducing-flagsets/index.md @@ -78,7 +78,9 @@ Flag sets are configured in your relay proxy configuration file, instead of defi In each flag set you can configure his behavior like in this example: ```yaml -listen: 1031 +server: + mode: http + port: 1031 flagsets: - name: team-a apiKeys: diff --git a/website/docs/relay-proxy/configure-relay-proxy.mdx b/website/docs/relay-proxy/configure-relay-proxy.mdx index 4b28839e7b6..58f29bb62fa 100644 --- a/website/docs/relay-proxy/configure-relay-proxy.mdx +++ b/website/docs/relay-proxy/configure-relay-proxy.mdx @@ -78,17 +78,17 @@ You can override file configurations using environment variables. **Here are some things to know when you are using environment variables:** - To set an option, the environment variable should have the same name as the configuration option in uppercase. - For example, to set the `listen` option, you can set the `LISTEN` environment variable. + For example, to set the `logLevel` option, you can set the `LOGLEVEL` environment variable. ```shell - export LISTEN=8080 + export LOGLEVEL=debug ``` - If you want to set a nested field, you can use `_` to separate each field. - For example, to set the `retrievers` option, you can set the `RETRIEVERS` environment variable. + For example, to set the `server.port` option, you can set the `SERVER_PORT` environment variable. ```shell - export RETRIEVER_KIND=github + export SERVER_PORT=8080 ``` - If you want to set an array of string, you can add multiple values separated by a comma. @@ -111,6 +111,17 @@ You can configure the prefix using the [`envVariablePrefix`](#envvariableprefix) ## Configuration Options +### `server` + +This is the configuration related to the server behavior of the relay proxy. +It allows the internal server behaviors such as your execution mode, monitoring port, etc ... + +- option name: `server` +- type: [`serverConfig`](#type-serverconfig) +- default: **none** +- mandatory: +- Check the [server configuration types](#type-serverconfig) to have more information about the available options. + ### `retrievers` This is the configuration on how to retrieve the configuration of the files. @@ -189,26 +200,6 @@ We ensure a deviation that is maximum ±10% of your polling interval. - default: **`false`** - mandatory: -### `listen` - -This is the port used by the relay proxy when starting the HTTP server. - -- option name: `listen` -- type: **int** -- default: **`1031`** -- mandatory: - -### `monitoringPort` - -If set the monitoring endpoints will be served on this specific port. - -Check [_"Use specific port for the monitoring"_](./observability#use-specific-port-for-the-monitoring) to have more information. - -- option name: `monitoringPort` -- type: **int** -- default: **none** -- mandatory: - ### `logLevel` The log level to use for the relay proxy. @@ -282,41 +273,6 @@ If `disableNotifierOnInit` is set to **true**, the relay proxy will not call any - default: **`false`** - mandatory: -### `startAsAwsLambda` - -Allow to start the relay-proxy as a AWS Lambda, it means that it will start the server to receive request in the AWS format _(see [`awsLambdaAdapter`](#awslambdaadapter) to set the request/response format you are using)_. -Notifiers is the configuration on where to notify a flag changes. - -- option name: `startAsAwsLambda` -- type: **boolean** -- default: **`false`** -- mandatory: - -### `awsLambdaAdapter` - -This parameter allow you to decide which type of AWS lambda handler you want to use. - -- option name: `awsLambdaAdapter` -- type: **string** -- default: **`APIGatewayV2`** -- Acceptable values are `APIGatewayV2`, `APIGatewayV1`, `ALB`. -- mandatory: -- This param is used only if `startAsAwsLambda` is `true`. - -### `awsApiGatewayBasePath` - -Specifies the base path prefix for AWS API Gateway deployments when using non-root routes. -The relay proxy will strip this base path from incoming requests before processing them. -This is useful when deploying behind paths like `/api` or `/dev/feature-flags`. - -- option name: `awsApiGatewayBasePath` -- type: **string** -- default: **none** -- mandatory: -- This param is used only if `startAsAwsLambda` is `true`. - -**Example:** If set to `/api/feature-flags`, requests to `/api/feature-flags/health` will be processed as `/health`. - ### `debug` If `debug` is set to true, we will set the log level to debug and set some components in debug mode (`labstack/echo`, `pprof` etc ...). @@ -343,7 +299,7 @@ Enables Swagger for testing the APIs directly. If you are enabling Swagger you w - type: **boolean** - default: **`false`** - mandatory: -- The Swagger UI will be available at `http://:/swagger/`. +- The Swagger UI will be available at `http://:/swagger/`. ### `host` @@ -417,7 +373,9 @@ When you configure flag sets, the relay proxy operates in **flag set mode** inst - If flag sets are configured, the relay proxy will ignore the top-level `retrievers`, `exporters`, and `notifiers` configuration. ```yaml title="example goff-proxy.yaml" -listen: 1031 +server: + mode: http + port: 1031 flagSets: - name: teamA apiKeys: @@ -448,7 +406,7 @@ This useful if you want to avoid any collision with existing environment variabl If you use this option you will have to prefix all your environment variables with the value you set here. For example if you want to override the port of the relay-proxy and the prefix is `GOFF_`. ```shell -export GOFF_LISTEN=8080 +export GOFF_SERVER_PORT=8080 ``` - option name: `envVariablePrefix` @@ -498,6 +456,89 @@ authorizedKeys: - "my-second-admin-key" ``` +### type `serverConfig` + +Configuration related to the server behavior of the relay proxy. + +#### `server.mode` + +The execution mode of the relay proxy, you can choose to start the relay-proxy in different modes depending how you want to call the APIs. + +- option name: `mode` +- type: **string** +- Acceptable values are :`http`, `lambda`, `unixsocket`. +- default: **http** +- mandatory: +- `http`: Start the relay proxy as a HTTP server. + `lambda`: Start the relay proxy as an AWS Lambda handler. + `unixsocket`: Start the relay proxy and listen on a Unix socket, you have to set the `unixSocketPath` field to define where the socket file will be created. + +#### `server.unixSocketPath` + +The path where the Unix socket file will be created when the relay proxy is started in `unixsocket` mode. + +- option name: `unixSocketPath` +- type: **string** +- default: **none** +- mandatory: +- This field is mandatory if the `mode` is set to `unixsocket`. + +#### `server.host` + +The server host used by the relay proxy to bind the HTTP server. + +- option name: `host` +- type: **string** +- default: **`0.0.0.0`** +- mandatory: +- This field is used only in `http` mode. + +#### `server.port` + +The server port used by the relay proxy to bind the HTTP server. + +- option name: `port` +- type: **int** +- default: **1031** +- mandatory: +- This field is used only in `http` mode. + +#### `server.monitoringPort` + +If set the monitoring endpoints will be served on this specific port. + +Check [_"Use specific port for the monitoring"_](./observability#use-specific-port-for-the-monitoring) to have more information. + +- option name: `monitoringPort` +- type: **int** +- default: **none** +- mandatory: +- This field is used only in `http` mode. + +#### `server.awsLambdaAdapter` +This parameter allow you to decide which type of AWS lambda handler you want to use. + +- option name: `awsLambdaAdapter` +- type: **string** +- default: **`APIGatewayV2`** +- Acceptable values are `APIGatewayV2`, `APIGatewayV1`, `ALB`. +- mandatory: +- This param is used only if `server.mode` is set to `lambda`. + +#### `server.awsApiGatewayBasePath` + +Specifies the base path prefix for AWS API Gateway deployments when using non-root routes. +The relay proxy will strip this base path from incoming requests before processing them. +This is useful when deploying behind paths like `/api` or `/dev/feature-flags`. + +- option name: `awsApiGatewayBasePath` +- type: **string** +- default: **none** +- mandatory: +- This param is used only if `server.mode` is set to `lambda`. + +**Example:** If set to `/api/feature-flags`, requests to `/api/feature-flags/health` will be processed as `/health`. + ### type `retriever` A [retriever](../concepts/retriever) is the component in charge of loading your flag configuration from a remote source. @@ -715,7 +756,9 @@ When you configure flag sets, the relay proxy operates in **flag set mode** inst ### Basic flag set configuration ```yaml title="goff-proxy.yaml" -listen: 1031 +server: + mode: http + port: 1031 flagSets: - name: teamA apiKeys: @@ -766,7 +809,10 @@ The relay proxy will: ### Example: Multi-team setup ```yaml title="goff-proxy.yaml" -listen: 1031 +server: + mode: http + port: 1031 + logLevel: INFO flagSets: diff --git a/website/versioned_docs/version-v1.0.0/getting_started/using-openfeature.md b/website/versioned_docs/version-v1.0.0/getting_started/using-openfeature.md index fefd8577c67..ca4c437318a 100644 --- a/website/versioned_docs/version-v1.0.0/getting_started/using-openfeature.md +++ b/website/versioned_docs/version-v1.0.0/getting_started/using-openfeature.md @@ -41,7 +41,9 @@ This flag split the usage of this flag, 20% will use the variation `my-new-featu Create a new `YAML` file containing the configuration of your relay proxy. ```yaml title="goff-proxy.yaml" -listen: 1031 +server: + mode: http + port: 1031 pollingInterval: 1000 startWithRetrieverError: false retriever: From 071af6c70dcd70714225e58df4abe7dbec21fc47 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 6 Nov 2025 20:22:33 +0000 Subject: [PATCH 11/22] Use zap fatal instead of log --- cmd/relayproxy/api/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/relayproxy/api/server.go b/cmd/relayproxy/api/server.go index b18e359bfcd..ead8848eacd 100644 --- a/cmd/relayproxy/api/server.go +++ b/cmd/relayproxy/api/server.go @@ -146,7 +146,7 @@ func (s *Server) startUnixSocketServer(ctx context.Context) { lc := net.ListenConfig{} listener, err := lc.Listen(ctx, "unix", socketPath) if err != nil { - log.Fatalf("Error creating Unix listener: %v", err) + s.zapLog.Fatal("Error creating Unix listener", zap.Error(err)) } defer func() { _ = listener.Close() }() From 054e89b1bb970301df4c351b7197e9e1e86a0795 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 6 Nov 2025 20:29:40 +0000 Subject: [PATCH 12/22] set logger --- cmd/relayproxy/api/routes_monitoring.go | 2 +- cmd/relayproxy/api/routes_monitoring_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/relayproxy/api/routes_monitoring.go b/cmd/relayproxy/api/routes_monitoring.go index a07d1cfa67f..33dd5c92a7e 100644 --- a/cmd/relayproxy/api/routes_monitoring.go +++ b/cmd/relayproxy/api/routes_monitoring.go @@ -11,7 +11,7 @@ import ( ) func (s *Server) addMonitoringRoutes() { - if s.config.GetMonitoringPort(nil) != 0 { + if s.config.GetMonitoringPort(s.zapLog) != 0 { s.monitoringEcho = echo.New() s.monitoringEcho.HideBanner = true s.monitoringEcho.HidePort = true diff --git a/cmd/relayproxy/api/routes_monitoring_test.go b/cmd/relayproxy/api/routes_monitoring_test.go index 97dea52481c..3bcf43d95a3 100644 --- a/cmd/relayproxy/api/routes_monitoring_test.go +++ b/cmd/relayproxy/api/routes_monitoring_test.go @@ -68,7 +68,7 @@ func TestPprofEndpointsStarts(t *testing.T) { Metrics: metric.Metrics{}, }, z) - portToCheck := c.GetServerPort(nil) + portToCheck := c.GetServerPort(z) if tt.MonitoringPort != 0 { portToCheck = tt.MonitoringPort } From bc91b32d996eeaae15db77bd25c058061fea6511 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 6 Nov 2025 20:30:01 +0000 Subject: [PATCH 13/22] GetServerPort never return 0 --- cmd/relayproxy/config/config_validator.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/cmd/relayproxy/config/config_validator.go b/cmd/relayproxy/config/config_validator.go index 9a76e44430a..4d908a04ec4 100644 --- a/cmd/relayproxy/config/config_validator.go +++ b/cmd/relayproxy/config/config_validator.go @@ -13,9 +13,6 @@ func (c *Config) IsValid() error { if c == nil { return fmt.Errorf("empty config") } - if c.GetServerPort(nil) == 0 { - return fmt.Errorf("invalid port %d", c.GetServerPort(nil)) - } if err := validateLogLevel(c.LogLevel); err != nil { return err } From b4158fb7bc175340fd699f62995e36a170489295 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 6 Nov 2025 20:31:10 +0000 Subject: [PATCH 14/22] Use background instead of TODO for context --- cmd/relayproxy/api/routes_monitoring_test.go | 2 +- cmd/relayproxy/api/server_test.go | 22 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cmd/relayproxy/api/routes_monitoring_test.go b/cmd/relayproxy/api/routes_monitoring_test.go index 3bcf43d95a3..c6f8ddfc703 100644 --- a/cmd/relayproxy/api/routes_monitoring_test.go +++ b/cmd/relayproxy/api/routes_monitoring_test.go @@ -73,7 +73,7 @@ func TestPprofEndpointsStarts(t *testing.T) { portToCheck = tt.MonitoringPort } - go apiServer.StartWithContext(context.TODO()) + go apiServer.StartWithContext(context.Background()) defer apiServer.Stop(context.Background()) time.Sleep(1 * time.Second) // waiting for the apiServer to start resp, err := http.Get(fmt.Sprintf("http://localhost:%d/debug/pprof/heap", portToCheck)) diff --git a/cmd/relayproxy/api/server_test.go b/cmd/relayproxy/api/server_test.go index 01e675ef8d8..b5108f24550 100644 --- a/cmd/relayproxy/api/server_test.go +++ b/cmd/relayproxy/api/server_test.go @@ -65,7 +65,7 @@ func Test_Starting_RelayProxy_with_monitoring_on_same_port(t *testing.T) { } s := api.New(proxyConf, services, log.ZapLogger) - go func() { s.StartWithContext(context.TODO()) }() + go func() { s.StartWithContext(context.Background()) }() defer s.Stop(context.Background()) time.Sleep(10 * time.Millisecond) @@ -126,7 +126,7 @@ func Test_Starting_RelayProxy_with_monitoring_on_different_port(t *testing.T) { } s := api.New(proxyConf, services, log.ZapLogger) - go func() { s.StartWithContext(context.TODO()) }() + go func() { s.StartWithContext(context.Background()) }() defer s.Stop(context.Background()) time.Sleep(10 * time.Millisecond) @@ -202,7 +202,7 @@ func Test_CheckOFREPAPIExists(t *testing.T) { } s := api.New(proxyConf, services, log.ZapLogger) - go func() { s.StartWithContext(context.TODO()) }() + go func() { s.StartWithContext(context.Background()) }() defer s.Stop(context.Background()) time.Sleep(10 * time.Millisecond) @@ -269,7 +269,7 @@ func Test_Middleware_VersionHeader_Enabled_Default(t *testing.T) { } s := api.New(proxyConf, services, log.ZapLogger) - go func() { s.StartWithContext(context.TODO()) }() + go func() { s.StartWithContext(context.Background()) }() defer s.Stop(context.Background()) time.Sleep(10 * time.Millisecond) @@ -312,7 +312,7 @@ func Test_VersionHeader_Disabled(t *testing.T) { } s := api.New(proxyConf, services, log.ZapLogger) - go func() { s.StartWithContext(context.TODO()) }() + go func() { s.StartWithContext(context.Background()) }() defer s.Stop(context.Background()) time.Sleep(10 * time.Millisecond) @@ -389,7 +389,7 @@ func Test_AuthenticationMiddleware(t *testing.T) { } s := api.New(proxyConf, services, log.ZapLogger) - go func() { s.StartWithContext(context.TODO()) }() + go func() { s.StartWithContext(context.Background()) }() defer s.Stop(context.Background()) time.Sleep(10 * time.Millisecond) @@ -467,7 +467,7 @@ func Test_AuthenticationMiddleware(t *testing.T) { } s := api.New(proxyConf, services, log.ZapLogger) - go func() { s.StartWithContext(context.TODO()) }() + go func() { s.StartWithContext(context.Background()) }() defer s.Stop(context.Background()) time.Sleep(10 * time.Millisecond) @@ -543,7 +543,7 @@ func Test_Starting_RelayProxy_UnixSocket(t *testing.T) { } s := api.New(proxyConf, services, log.ZapLogger) - go func() { s.StartWithContext(context.TODO()) }() + go func() { s.StartWithContext(context.Background()) }() defer s.Stop(context.Background()) // Wait for the socket to be created @@ -623,7 +623,7 @@ func Test_Starting_RelayProxy_UnixSocket_OFREP_API(t *testing.T) { } s := api.New(proxyConf, services, log.ZapLogger) - go func() { s.StartWithContext(context.TODO()) }() + go func() { s.StartWithContext(context.Background()) }() defer s.Stop(context.Background()) // Wait for the socket to be created @@ -766,7 +766,7 @@ func Test_Starting_RelayProxy_UnixSocket_Authentication(t *testing.T) { } s := api.New(proxyConf, services, log.ZapLogger) - go func() { s.StartWithContext(context.TODO()) }() + go func() { s.StartWithContext(context.Background()) }() defer s.Stop(context.Background()) // Wait for the socket to be created @@ -858,7 +858,7 @@ func Test_Starting_RelayProxy_UnixSocket_VersionHeader(t *testing.T) { } s := api.New(proxyConf, services, log.ZapLogger) - go func() { s.StartWithContext(context.TODO()) }() + go func() { s.StartWithContext(context.Background()) }() defer s.Stop(context.Background()) // Wait for the socket to be created From bf19e60ec671b862ee5b58d242055fc082d093da Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 6 Nov 2025 20:33:57 +0000 Subject: [PATCH 15/22] Log and exit if impossible to remove existing socket --- cmd/relayproxy/api/server.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/relayproxy/api/server.go b/cmd/relayproxy/api/server.go index ead8848eacd..e815b0ec304 100644 --- a/cmd/relayproxy/api/server.go +++ b/cmd/relayproxy/api/server.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "log" "net" "net/http" "os" @@ -138,9 +137,12 @@ func (s *Server) StartWithContext(ctx context.Context) { // startUnixSocketServer launch the API server as a unix socket. func (s *Server) startUnixSocketServer(ctx context.Context) { socketPath := s.config.GetUnixSocketPath() + // Clean up the old socket file if it exists (important for graceful restarts) if _, err := os.Stat(socketPath); err == nil { - _ = os.Remove(socketPath) + if err := os.Remove(socketPath); err != nil { + s.zapLog.Fatal("Could not remove old socket file", zap.String("path", socketPath), zap.Error(err)) + } } lc := net.ListenConfig{} From bb3dece5638e6b0e15ef07f77206041c38f39d80 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 6 Nov 2025 20:36:17 +0000 Subject: [PATCH 16/22] log error when closing the unix socket --- cmd/relayproxy/api/server.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/relayproxy/api/server.go b/cmd/relayproxy/api/server.go index e815b0ec304..9a0bbfcbed7 100644 --- a/cmd/relayproxy/api/server.go +++ b/cmd/relayproxy/api/server.go @@ -137,7 +137,7 @@ func (s *Server) StartWithContext(ctx context.Context) { // startUnixSocketServer launch the API server as a unix socket. func (s *Server) startUnixSocketServer(ctx context.Context) { socketPath := s.config.GetUnixSocketPath() - + // Clean up the old socket file if it exists (important for graceful restarts) if _, err := os.Stat(socketPath); err == nil { if err := os.Remove(socketPath); err != nil { @@ -151,7 +151,11 @@ func (s *Server) startUnixSocketServer(ctx context.Context) { s.zapLog.Fatal("Error creating Unix listener", zap.Error(err)) } - defer func() { _ = listener.Close() }() + defer func() { + if err := listener.Close(); err != nil { + s.zapLog.Error("error closing unix socket listener", zap.Error(err)) + } + }() s.apiEcho.Listener = listener s.zapLog.Info( From 197f16f38a54c11d4eac3809abde320bea51cf04 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 6 Nov 2025 20:36:39 +0000 Subject: [PATCH 17/22] better way for the socket to be created --- cmd/relayproxy/api/server_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/relayproxy/api/server_test.go b/cmd/relayproxy/api/server_test.go index b5108f24550..521e4d56b11 100644 --- a/cmd/relayproxy/api/server_test.go +++ b/cmd/relayproxy/api/server_test.go @@ -547,7 +547,10 @@ func Test_Starting_RelayProxy_UnixSocket(t *testing.T) { defer s.Stop(context.Background()) // Wait for the socket to be created - time.Sleep(50 * time.Millisecond) + require.Eventually(t, func() bool { + _, err := os.Stat(socketPath) + return err == nil + }, 1*time.Second, 10*time.Millisecond, "unix socket file was not created in time") // Verify socket file exists _, err = os.Stat(socketPath) From 93d0007af2b9c84d3742a74d4473a719979056ba Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 6 Nov 2025 20:38:39 +0000 Subject: [PATCH 18/22] checking errors in tests --- cmd/relayproxy/api/server_test.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/cmd/relayproxy/api/server_test.go b/cmd/relayproxy/api/server_test.go index 521e4d56b11..101869285bf 100644 --- a/cmd/relayproxy/api/server_test.go +++ b/cmd/relayproxy/api/server_test.go @@ -379,7 +379,8 @@ func Test_AuthenticationMiddleware(t *testing.T) { metricsV2, _ := metric.NewMetrics() wsService := service.NewWebsocketService() defer wsService.Close() - flagsetManager, _ := service.NewFlagsetManager(proxyConf, log.ZapLogger, nil) + flagsetManager, err := service.NewFlagsetManager(proxyConf, log.ZapLogger, nil) + require.NoError(t, err) services := service.Services{ MonitoringService: service.NewMonitoring(flagsetManager), @@ -457,7 +458,8 @@ func Test_AuthenticationMiddleware(t *testing.T) { metricsV2, _ := metric.NewMetrics() wsService := service.NewWebsocketService() defer wsService.Close() - flagsetManager, _ := service.NewFlagsetManager(proxyConf, log.ZapLogger, nil) + flagsetManager, err := service.NewFlagsetManager(proxyConf, log.ZapLogger, nil) + require.NoError(t, err) services := service.Services{ MonitoringService: service.NewMonitoring(flagsetManager), @@ -759,7 +761,8 @@ func Test_Starting_RelayProxy_UnixSocket_Authentication(t *testing.T) { metricsV2, _ := metric.NewMetrics() wsService := service.NewWebsocketService() defer wsService.Close() - flagsetManager, _ := service.NewFlagsetManager(proxyConf, log.ZapLogger, nil) + flagsetManager, err := service.NewFlagsetManager(proxyConf, log.ZapLogger, nil) + require.NoError(t, err) services := service.Services{ MonitoringService: service.NewMonitoring(flagsetManager), @@ -848,10 +851,12 @@ func Test_Starting_RelayProxy_UnixSocket_VersionHeader(t *testing.T) { log := log.InitLogger() defer func() { _ = log.ZapLogger.Sync() }() - metricsV2, _ := metric.NewMetrics() + metricsV2, err := metric.NewMetrics() + require.NoError(t, err) wsService := service.NewWebsocketService() defer wsService.Close() - flagsetManager, _ := service.NewFlagsetManager(proxyConf, log.ZapLogger, nil) + flagsetManager, err := service.NewFlagsetManager(proxyConf, log.ZapLogger, nil) + require.NoError(t, err) services := service.Services{ MonitoringService: service.NewMonitoring(flagsetManager), From 4a4308b8329db8dff672d18ac1940a6a30c01627 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 6 Nov 2025 20:39:29 +0000 Subject: [PATCH 19/22] use directly the logger passed in the function --- cmd/relayproxy/config/config_server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/relayproxy/config/config_server.go b/cmd/relayproxy/config/config_server.go index 407cd9f57e7..7d7c765dd0d 100644 --- a/cmd/relayproxy/config/config_server.go +++ b/cmd/relayproxy/config/config_server.go @@ -155,7 +155,7 @@ func (c *Config) GetAwsApiGatewayBasePath(logger *zap.Logger) string { if c.AwsApiGatewayBasePath != "" { if logger != nil { - zap.L().Warn("The AWS API Gateway base path is set using `awsApiGatewayBasePath`, " + + logger.Warn("The AWS API Gateway base path is set using `awsApiGatewayBasePath`, " + "this option is deprecated, please migrate to `server.awsApiGatewayBasePath`") } return c.AwsApiGatewayBasePath From 908c1e74815a5edf15cce0837cff5f0e80558f55 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 6 Nov 2025 20:47:48 +0000 Subject: [PATCH 20/22] better waiting mechanism --- cmd/relayproxy/api/server_test.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/cmd/relayproxy/api/server_test.go b/cmd/relayproxy/api/server_test.go index 101869285bf..cf719860e92 100644 --- a/cmd/relayproxy/api/server_test.go +++ b/cmd/relayproxy/api/server_test.go @@ -632,7 +632,10 @@ func Test_Starting_RelayProxy_UnixSocket_OFREP_API(t *testing.T) { defer s.Stop(context.Background()) // Wait for the socket to be created - time.Sleep(50 * time.Millisecond) + require.Eventually(t, func() bool { + _, err := os.Stat(socketPath) + return err == nil + }, 1*time.Second, 10*time.Millisecond, "unix socket file was not created in time") // Verify socket file exists _, err = os.Stat(socketPath) @@ -776,7 +779,10 @@ func Test_Starting_RelayProxy_UnixSocket_Authentication(t *testing.T) { defer s.Stop(context.Background()) // Wait for the socket to be created - time.Sleep(50 * time.Millisecond) + require.Eventually(t, func() bool { + _, err := os.Stat(socketPath) + return err == nil + }, 1*time.Second, 10*time.Millisecond, "unix socket file was not created in time") // Create a Unix socket HTTP client client := newUnixSocketHTTPClient(socketPath) @@ -870,7 +876,10 @@ func Test_Starting_RelayProxy_UnixSocket_VersionHeader(t *testing.T) { defer s.Stop(context.Background()) // Wait for the socket to be created - time.Sleep(50 * time.Millisecond) + require.Eventually(t, func() bool { + _, err := os.Stat(socketPath) + return err == nil + }, 1*time.Second, 10*time.Millisecond, "unix socket file was not created in time") // Create a Unix socket HTTP client client := newUnixSocketHTTPClient(socketPath) From 539c42b300daf23e225092c151e39d2951e70395 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 6 Nov 2025 20:50:27 +0000 Subject: [PATCH 21/22] move server validation logic to Config object --- cmd/relayproxy/config/config_server.go | 14 -------------- cmd/relayproxy/config/config_validator.go | 14 +++++++++++++- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/cmd/relayproxy/config/config_server.go b/cmd/relayproxy/config/config_server.go index 7d7c765dd0d..20d20bcbb2b 100644 --- a/cmd/relayproxy/config/config_server.go +++ b/cmd/relayproxy/config/config_server.go @@ -1,8 +1,6 @@ package config import ( - "errors" - "go.uber.org/zap" ) @@ -58,18 +56,6 @@ type Server struct { AwsApiGatewayBasePath string `mapstructure:"awsApiGatewayBasePath" koanf:"awsapigatewaybasepath"` } -func (s *Server) Validate() error { - switch s.Mode { - case ServerModeUnixSocket: - if s.UnixSocketPath == "" { - return errors.New("unixSocket must be set when server mode is unixsocket") - } - return nil - default: - return nil - } -} - // GetMonitoringPort returns the monitoring port, checking first the top-level config // and then the server config. func (c *Config) GetMonitoringPort(logger *zap.Logger) int { diff --git a/cmd/relayproxy/config/config_validator.go b/cmd/relayproxy/config/config_validator.go index 4d908a04ec4..5c1eecc8738 100644 --- a/cmd/relayproxy/config/config_validator.go +++ b/cmd/relayproxy/config/config_validator.go @@ -1,6 +1,7 @@ package config import ( + "errors" "fmt" "strings" @@ -19,7 +20,7 @@ func (c *Config) IsValid() error { if err := validateLogFormat(c.LogFormat); err != nil { return err } - if err := c.Server.Validate(); err != nil { + if err := c.validateServerConfig(); err != nil { return err } if len(c.FlagSets) > 0 { @@ -28,6 +29,17 @@ func (c *Config) IsValid() error { return c.validateDefaultMode() } +// validateServerConfig validates the server configuration +func (c *Config) validateServerConfig() error { + mode := c.GetServerMode(nil) + if mode == ServerModeUnixSocket { + if c.GetUnixSocketPath() == "" { + return errors.New("unixSocketPath must be set when server mode is unixsocket") + } + } + return nil +} + // validateLogFormat validates the log format func validateLogFormat(logFormat string) error { switch strings.ToLower(logFormat) { From 7f26aba336c8fd6c2130788da086f5b1e6852269 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 6 Nov 2025 21:08:19 +0000 Subject: [PATCH 22/22] add test to validate error socket --- cmd/relayproxy/config/config_test.go | 117 +++++++++++++++++++++------ 1 file changed, 92 insertions(+), 25 deletions(-) diff --git a/cmd/relayproxy/config/config_test.go b/cmd/relayproxy/config/config_test.go index 33af750c76d..8dd0099b178 100644 --- a/cmd/relayproxy/config/config_test.go +++ b/cmd/relayproxy/config/config_test.go @@ -524,7 +524,7 @@ func TestParseConfig_fileFromFolder(t *testing.T) { func TestConfig_IsValid(t *testing.T) { type fields struct { - ListenPort int + Server config.Server HideBanner bool EnableSwagger bool Host string @@ -552,13 +552,16 @@ func TestConfig_IsValid(t *testing.T) { }, { name: "invalid port", - fields: fields{ListenPort: 0}, + fields: fields{}, wantErr: assert.Error, }, { name: "no retriever", fields: fields{ - ListenPort: 8080, + Server: config.Server{ + Port: 8080, + Mode: config.ServerModeHTTP, + }, Notifiers: []config.NotifierConf{ { Kind: "webhook", @@ -576,7 +579,10 @@ func TestConfig_IsValid(t *testing.T) { { name: "valid configuration", fields: fields{ - ListenPort: 8080, + Server: config.Server{ + Port: 8080, + Mode: config.ServerModeHTTP, + }, Retriever: &retrieverconf.RetrieverConf{ Kind: "file", Path: "../testdata/config/valid-file.yaml", @@ -607,7 +613,10 @@ func TestConfig_IsValid(t *testing.T) { { name: "valid configuration with notifier included", fields: fields{ - ListenPort: 8080, + Server: config.Server{ + Port: 8080, + Mode: config.ServerModeHTTP, + }, Retriever: &retrieverconf.RetrieverConf{ Kind: "file", Path: "../testdata/config/valid-file-notifier.yaml", @@ -626,7 +635,10 @@ func TestConfig_IsValid(t *testing.T) { { name: "invalid retriever", fields: fields{ - ListenPort: 8080, + Server: config.Server{ + Port: 8080, + Mode: config.ServerModeHTTP, + }, Retriever: &retrieverconf.RetrieverConf{ Kind: "file", }, @@ -636,7 +648,10 @@ func TestConfig_IsValid(t *testing.T) { { name: "1 invalid retriever in the list of retrievers", fields: fields{ - ListenPort: 8080, + Server: config.Server{ + Port: 8080, + Mode: config.ServerModeHTTP, + }, Retrievers: &[]retrieverconf.RetrieverConf{ { Kind: "file", @@ -656,7 +671,10 @@ func TestConfig_IsValid(t *testing.T) { { name: "invalid exporter", fields: fields{ - ListenPort: 8080, + Server: config.Server{ + Port: 8080, + Mode: config.ServerModeHTTP, + }, Retriever: &retrieverconf.RetrieverConf{ Kind: "file", Path: "../testdata/config/valid-file.yaml", @@ -670,7 +688,10 @@ func TestConfig_IsValid(t *testing.T) { { name: "invalid notifier", fields: fields{ - ListenPort: 8080, + Server: config.Server{ + Port: 8080, + Mode: config.ServerModeHTTP, + }, Retriever: &retrieverconf.RetrieverConf{ Kind: "file", Path: "../testdata/config/valid-file.yaml", @@ -686,7 +707,10 @@ func TestConfig_IsValid(t *testing.T) { { name: "invalid log level", fields: fields{ - ListenPort: 8080, + Server: config.Server{ + Port: 8080, + Mode: config.ServerModeHTTP, + }, Retriever: &retrieverconf.RetrieverConf{ Kind: "file", Path: "../testdata/config/valid-file.yaml", @@ -698,7 +722,10 @@ func TestConfig_IsValid(t *testing.T) { { name: "log level is not set but debug is set", fields: fields{ - ListenPort: 8080, + Server: config.Server{ + Port: 8080, + Mode: config.ServerModeHTTP, + }, Retriever: &retrieverconf.RetrieverConf{ Kind: "file", Path: "../testdata/config/valid-file.yaml", @@ -711,8 +738,11 @@ func TestConfig_IsValid(t *testing.T) { { name: "invalid logFormat", fields: fields{ - LogFormat: "unknown", - ListenPort: 8080, + LogFormat: "unknown", + Server: config.Server{ + Port: 8080, + Mode: config.ServerModeHTTP, + }, Retriever: &retrieverconf.RetrieverConf{ Kind: "file", Path: "../testdata/config/valid-file.yaml", @@ -724,7 +754,10 @@ func TestConfig_IsValid(t *testing.T) { { name: "valid flagset configuration", fields: fields{ - ListenPort: 8080, + Server: config.Server{ + Port: 8080, + Mode: config.ServerModeHTTP, + }, FlagSets: []config.FlagSet{ { Name: "test-flagset", @@ -743,7 +776,10 @@ func TestConfig_IsValid(t *testing.T) { { name: "flagset without name", fields: fields{ - ListenPort: 8080, + Server: config.Server{ + Port: 8080, + Mode: config.ServerModeHTTP, + }, FlagSets: []config.FlagSet{ { APIKeys: []string{"test-api-key"}, @@ -761,7 +797,10 @@ func TestConfig_IsValid(t *testing.T) { { name: "flagset without API keys", fields: fields{ - ListenPort: 8080, + Server: config.Server{ + Port: 8080, + Mode: config.ServerModeHTTP, + }, FlagSets: []config.FlagSet{ { Name: "test-flagset", @@ -779,7 +818,10 @@ func TestConfig_IsValid(t *testing.T) { { name: "duplicate API keys across flagsets", fields: fields{ - ListenPort: 8080, + Server: config.Server{ + Port: 8080, + Mode: config.ServerModeHTTP, + }, FlagSets: []config.FlagSet{ { Name: "flagset-1", @@ -808,7 +850,10 @@ func TestConfig_IsValid(t *testing.T) { { name: "flagset with invalid retriever", fields: fields{ - ListenPort: 8080, + Server: config.Server{ + Port: 8080, + Mode: config.ServerModeHTTP, + }, FlagSets: []config.FlagSet{ { Name: "test-flagset", @@ -827,7 +872,10 @@ func TestConfig_IsValid(t *testing.T) { { name: "flagset with invalid exporter", fields: fields{ - ListenPort: 8080, + Server: config.Server{ + Port: 8080, + Mode: config.ServerModeHTTP, + }, FlagSets: []config.FlagSet{ { Name: "test-flagset", @@ -850,7 +898,10 @@ func TestConfig_IsValid(t *testing.T) { { name: "flagset with invalid notifier", fields: fields{ - ListenPort: 8080, + Server: config.Server{ + Port: 8080, + Mode: config.ServerModeHTTP, + }, FlagSets: []config.FlagSet{ { Name: "test-flagset", @@ -875,7 +926,10 @@ func TestConfig_IsValid(t *testing.T) { { name: "multiple valid flagsets", fields: fields{ - ListenPort: 8080, + Server: config.Server{ + Port: 8080, + Mode: config.ServerModeHTTP, + }, FlagSets: []config.FlagSet{ { Name: "flagset-1", @@ -901,6 +955,22 @@ func TestConfig_IsValid(t *testing.T) { }, wantErr: assert.NoError, }, + { + name: "missing unix socket path", + fields: fields{ + LogFormat: "unknown", + Server: config.Server{ + Mode: config.ServerModeUnixSocket, + UnixSocketPath: "", + }, + Retriever: &retrieverconf.RetrieverConf{ + Kind: "file", + Path: "../testdata/config/valid-file.yaml", + }, + LogLevel: "info", + }, + wantErr: assert.Error, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -914,10 +984,7 @@ func TestConfig_IsValid(t *testing.T) { Notifiers: tt.fields.Notifiers, Retrievers: tt.fields.Retrievers, }, - Server: config.Server{ - Mode: config.ServerModeHTTP, - Port: tt.fields.ListenPort, - }, + Server: tt.fields.Server, HideBanner: tt.fields.HideBanner, EnableSwagger: tt.fields.EnableSwagger, Host: tt.fields.Host,