From 8ff44da9403a9b50198b69b40b2f13e252401768 Mon Sep 17 00:00:00 2001 From: Alexandra Oberaigner Date: Thu, 27 Nov 2025 08:39:03 +0100 Subject: [PATCH 1/4] fix: return fatal on certain error codes during first stream cycle Signed-off-by: Alexandra Oberaigner --- providers/flagd/e2e/inprocess_test.go | 2 +- providers/flagd/e2e/rpc_test.go | 2 +- providers/flagd/flagd-testbed | 2 +- providers/flagd/pkg/configuration.go | 74 +++++++++++------- providers/flagd/pkg/provider.go | 1 + .../pkg/service/in_process/grpc_config.go | 66 ++++++++++++++++ .../service/in_process/grpc_config_test.go | 77 +++++++++++++++++++ .../flagd/pkg/service/in_process/grpc_sync.go | 75 +++++++----------- .../flagd/pkg/service/in_process/service.go | 4 + tests/flagd/testframework/provider_steps.go | 20 ++++- tests/flagd/testframework/testbed_runner.go | 7 +- tests/flagd/testframework/testcontainer.go | 6 ++ tests/flagd/testframework/utils.go | 6 ++ 13 files changed, 259 insertions(+), 83 deletions(-) create mode 100644 providers/flagd/pkg/service/in_process/grpc_config.go create mode 100644 providers/flagd/pkg/service/in_process/grpc_config_test.go diff --git a/providers/flagd/e2e/inprocess_test.go b/providers/flagd/e2e/inprocess_test.go index d4767e4e3..2835f04e5 100644 --- a/providers/flagd/e2e/inprocess_test.go +++ b/providers/flagd/e2e/inprocess_test.go @@ -26,7 +26,7 @@ func TestInProcessProviderE2E(t *testing.T) { } // Run tests with in-process specific tags - tags := "@in-process && ~@unixsocket && ~@metadata && ~@customCert && ~@contextEnrichment && ~@sync-payload" + tags := "@in-process && ~@unixsocket && ~@metadata && ~@customCert && ~@contextEnrichment && ~@sync-payload && ~@sync-port" if err := runner.RunGherkinTestsWithSubtests(t, featurePaths, tags); err != nil { t.Fatalf("Gherkin tests failed: %v", err) diff --git a/providers/flagd/e2e/rpc_test.go b/providers/flagd/e2e/rpc_test.go index f3c58b9ab..994c3627c 100644 --- a/providers/flagd/e2e/rpc_test.go +++ b/providers/flagd/e2e/rpc_test.go @@ -26,7 +26,7 @@ func TestRPCProviderE2E(t *testing.T) { } // Run tests with RPC-specific tags - exclude unimplemented scenarios - tags := "@rpc && ~@unixsocket && ~@targetURI && ~@sync && ~@metadata && ~@grace && ~@customCert && ~@caching" + tags := "@rpc && ~@unixsocket && ~@targetURI && ~@sync && ~@metadata && ~@grace && ~@customCert && ~@caching && ~@forbidden" if err := runner.RunGherkinTestsWithSubtests(t, featurePaths, tags); err != nil { t.Fatalf("Gherkin tests failed: %v", err) diff --git a/providers/flagd/flagd-testbed b/providers/flagd/flagd-testbed index b62f5dbe8..4f3f5559a 160000 --- a/providers/flagd/flagd-testbed +++ b/providers/flagd/flagd-testbed @@ -1 +1 @@ -Subproject commit b62f5dbe860ecf4f36ec757dfdc0b38f7b3dec6e +Subproject commit 4f3f5559a26fa12bf95d06d3dea9ac0879c1598f diff --git a/providers/flagd/pkg/configuration.go b/providers/flagd/pkg/configuration.go index ca758d38d..76e3f4b98 100644 --- a/providers/flagd/pkg/configuration.go +++ b/providers/flagd/pkg/configuration.go @@ -3,14 +3,15 @@ package flagd import ( "errors" "fmt" + "os" + "strconv" + "strings" + "github.com/go-logr/logr" "github.com/open-feature/flagd/core/pkg/sync" "github.com/open-feature/go-sdk-contrib/providers/flagd/internal/cache" "github.com/open-feature/go-sdk-contrib/providers/flagd/internal/logger" "google.golang.org/grpc" - "os" - "strconv" - "strings" ) type ResolverType string @@ -26,6 +27,7 @@ const ( defaultHost = "localhost" defaultResolver = rpc defaultGracePeriod = 5 + defaultFatalStatusCodes = "" rpc ResolverType = "rpc" inProcess ResolverType = "in-process" @@ -45,6 +47,7 @@ const ( flagdOfflinePathEnvironmentVariableName = "FLAGD_OFFLINE_FLAG_SOURCE_PATH" flagdTargetUriEnvironmentVariableName = "FLAGD_TARGET_URI" flagdGracePeriodVariableName = "FLAGD_RETRY_GRACE_PERIOD" + flagdFatalStatusCodesVariableName = "FLAGD_FATAL_STATUS_CODES" ) type ProviderConfiguration struct { @@ -66,6 +69,7 @@ type ProviderConfiguration struct { CustomSyncProviderUri string GrpcDialOptionsOverride []grpc.DialOption RetryGracePeriod int + FatalStatusCodes []string log logr.Logger } @@ -80,6 +84,7 @@ func newDefaultConfiguration(log logr.Logger) *ProviderConfiguration { Resolver: defaultResolver, Tls: defaultTLS, RetryGracePeriod: defaultGracePeriod, + FatalStatusCodes: strings.Split(defaultFatalStatusCodes, ","), } p.updateFromEnvVar() @@ -130,6 +135,7 @@ func validateProviderConfiguration(p *ProviderConfiguration) error { // updateFromEnvVar is a utility to update configurations based on current environment variables func (cfg *ProviderConfiguration) updateFromEnvVar() { + portS := os.Getenv(flagdPortEnvironmentVariableName) if portS != "" { port, err := strconv.Atoi(portS) @@ -159,17 +165,7 @@ func (cfg *ProviderConfiguration) updateFromEnvVar() { cfg.CertPath = certificatePath } - if maxCacheSizeS := os.Getenv(flagdMaxCacheSizeEnvironmentVariableName); maxCacheSizeS != "" { - maxCacheSizeFromEnv, err := strconv.Atoi(maxCacheSizeS) - if err != nil { - cfg.log.Error(err, - fmt.Sprintf("invalid env config for %s provided, using default value: %d", - flagdMaxCacheSizeEnvironmentVariableName, defaultMaxCacheSize, - )) - } else { - cfg.MaxCacheSize = maxCacheSizeFromEnv - } - } + cfg.MaxCacheSize = getIntFromEnvVarOrDefault(flagdMaxCacheSizeEnvironmentVariableName, defaultMaxCacheSize, cfg.log) if cacheValue := os.Getenv(flagdCacheEnvironmentVariableName); cacheValue != "" { switch cache.Type(cacheValue) { @@ -185,18 +181,8 @@ func (cfg *ProviderConfiguration) updateFromEnvVar() { } } - if maxEventStreamRetriesS := os.Getenv( - flagdMaxEventStreamRetriesEnvironmentVariableName); maxEventStreamRetriesS != "" { - - maxEventStreamRetries, err := strconv.Atoi(maxEventStreamRetriesS) - if err != nil { - cfg.log.Error(err, - fmt.Sprintf("invalid env config for %s provided, using default value: %d", - flagdMaxEventStreamRetriesEnvironmentVariableName, defaultMaxEventStreamRetries)) - } else { - cfg.EventStreamConnectionMaxAttempts = maxEventStreamRetries - } - } + cfg.EventStreamConnectionMaxAttempts = getIntFromEnvVarOrDefault( + flagdMaxEventStreamRetriesEnvironmentVariableName, defaultMaxEventStreamRetries, cfg.log) if resolver := os.Getenv(flagdResolverEnvironmentVariableName); resolver != "" { switch strings.ToLower(resolver) { @@ -230,12 +216,34 @@ func (cfg *ProviderConfiguration) updateFromEnvVar() { if gracePeriod := os.Getenv(flagdGracePeriodVariableName); gracePeriod != "" { if seconds, err := strconv.Atoi(gracePeriod); err == nil { cfg.RetryGracePeriod = seconds - } else { - // Handle parsing error - cfg.log.Error(err, fmt.Sprintf("invalid grace period '%s'", gracePeriod)) + cfg.RetryGracePeriod = getIntFromEnvVarOrDefault(flagdGracePeriodVariableName, defaultGracePeriod, cfg.log) } } + if fatalStatusCodes := os.Getenv(flagdFatalStatusCodesVariableName); fatalStatusCodes != "" { + fatalStatusCodesArr := strings.Split(fatalStatusCodes, ",") + for i, fatalStatusCode := range fatalStatusCodesArr { + fatalStatusCodesArr[i] = strings.TrimSpace(fatalStatusCode) + } + cfg.FatalStatusCodes = fatalStatusCodesArr + } +} + +// Helper + +func getIntFromEnvVarOrDefault(envVarName string, defaultValue int, log logr.Logger) int { + if valueFromEnv := os.Getenv(envVarName); valueFromEnv != "" { + intValue, err := strconv.Atoi(valueFromEnv) + if err != nil { + log.Error(err, + fmt.Sprintf("invalid env config for %s provided, using default value: %d", + envVarName, defaultValue, + )) + } else { + return intValue + } + } + return defaultValue } // ProviderOptions @@ -415,3 +423,11 @@ func WithRetryGracePeriod(gracePeriod int) ProviderOption { p.RetryGracePeriod = gracePeriod } } + +// WithFatalStatusCodes allows to set a list of gRPC status codes, which will cause streams to give up +// and put the provider in a PROVIDER_FATAL state +func WithFatalStatusCodes(fatalStatusCodes []string) ProviderOption { + return func(p *ProviderConfiguration) { + p.FatalStatusCodes = fatalStatusCodes + } +} diff --git a/providers/flagd/pkg/provider.go b/providers/flagd/pkg/provider.go index 1742f2f15..676e46e1c 100644 --- a/providers/flagd/pkg/provider.go +++ b/providers/flagd/pkg/provider.go @@ -74,6 +74,7 @@ func NewProvider(opts ...ProviderOption) (*Provider, error) { CustomSyncProviderUri: provider.providerConfiguration.CustomSyncProviderUri, GrpcDialOptionsOverride: provider.providerConfiguration.GrpcDialOptionsOverride, RetryGracePeriod: provider.providerConfiguration.RetryGracePeriod, + FatalStatusCodes: provider.providerConfiguration.FatalStatusCodes, }) default: service = process.NewInProcessService(process.Configuration{ diff --git a/providers/flagd/pkg/service/in_process/grpc_config.go b/providers/flagd/pkg/service/in_process/grpc_config.go new file mode 100644 index 000000000..1f97bc7fd --- /dev/null +++ b/providers/flagd/pkg/service/in_process/grpc_config.go @@ -0,0 +1,66 @@ +package process + +import ( + "encoding/json" + "fmt" + "google.golang.org/grpc/codes" + "time" +) + +const ( + // Default timeouts and retry intervals + defaultKeepaliveTime = 30 * time.Second + defaultKeepaliveTimeout = 5 * time.Second +) + +type RetryPolicy struct { + MaxAttempts int `json:"MaxAttempts"` + InitialBackoff string `json:"InitialBackoff"` + MaxBackoff string `json:"MaxBackoff"` + BackoffMultiplier float64 `json:"BackoffMultiplier"` + RetryableStatusCodes []string `json:"RetryableStatusCodes"` +} + +func (g *Sync) buildRetryPolicy() string { + var policy = map[string]interface{}{ + "methodConfig": []map[string]interface{}{ + { + "name": []map[string]string{ + {"service": "flagd.sync.v1.FlagSyncService"}, + }, + "retryPolicy": RetryPolicy{ + MaxAttempts: 3, + InitialBackoff: "1s", + MaxBackoff: "5s", + BackoffMultiplier: 2.0, + RetryableStatusCodes: []string{"UNKNOWN", "UNAVAILABLE"}, + }, + }, + }, + } + retryPolicyBytes, _ := json.Marshal(policy) + retryPolicy := string(retryPolicyBytes) + + return retryPolicy +} + +// Set of non-retryable gRPC status codes for faster lookup +var nonRetryableCodes map[codes.Code]struct{} + +// initNonRetryableStatusCodesSet initializes the set of non-retryable gRPC status codes for quick lookup +func (g *Sync) initNonRetryableStatusCodesSet() { + nonRetryableCodes = make(map[codes.Code]struct{}) + + for _, codeStr := range g.FatalStatusCodes { + // Wrap the string in quotes to match the expected JSON format + jsonStr := fmt.Sprintf(`"%s"`, codeStr) + + var code codes.Code + if err := code.UnmarshalJSON([]byte(jsonStr)); err != nil { + g.Logger.Warn(fmt.Sprintf("unknown status code: %s, error: %v", codeStr, err)) + continue + } + + nonRetryableCodes[code] = struct{}{} + } +} diff --git a/providers/flagd/pkg/service/in_process/grpc_config_test.go b/providers/flagd/pkg/service/in_process/grpc_config_test.go new file mode 100644 index 000000000..6a5ed6fec --- /dev/null +++ b/providers/flagd/pkg/service/in_process/grpc_config_test.go @@ -0,0 +1,77 @@ +package process + +import ( + "github.com/open-feature/flagd/core/pkg/logger" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "testing" +) + +func TestSync_initNonRetryableStatusCodesSet(t *testing.T) { + tests := []struct { + name string + fatalStatusCodes []string + expectedCodes []codes.Code + notExpectedCodes []codes.Code + }{ + { + name: "valid status codes", + fatalStatusCodes: []string{"UNAVAILABLE", "INTERNAL", "DEADLINE_EXCEEDED"}, + expectedCodes: []codes.Code{codes.Unavailable, codes.Internal, codes.DeadlineExceeded}, + notExpectedCodes: []codes.Code{codes.OK, codes.Unknown}, + }, + { + name: "empty array", + fatalStatusCodes: []string{}, + expectedCodes: []codes.Code{}, + notExpectedCodes: []codes.Code{codes.Unavailable, codes.Internal}, + }, + { + name: "invalid status codes", + fatalStatusCodes: []string{"INVALID_CODE", "UNKNOWN_STATUS"}, + expectedCodes: []codes.Code{}, + notExpectedCodes: []codes.Code{codes.Unavailable, codes.Internal}, + }, + { + name: "mixed valid and invalid codes", + fatalStatusCodes: []string{"UNAVAILABLE", "INVALID_CODE", "INTERNAL"}, + expectedCodes: []codes.Code{codes.Unavailable, codes.Internal}, + notExpectedCodes: []codes.Code{codes.OK, codes.Unknown}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset the global map before each test + nonRetryableCodes = nil + + s := &Sync{ + FatalStatusCodes: tt.fatalStatusCodes, + Logger: &logger.Logger{ + Logger: zap.NewNop(), + }, + } + + s.initNonRetryableStatusCodesSet() + + // Verify expected codes are present + for _, code := range tt.expectedCodes { + if _, exists := nonRetryableCodes[code]; !exists { + t.Errorf("expected code %v to be in nonRetryableCodes, but it was not found", code) + } + } + + // Verify not expected codes are absent + for _, code := range tt.notExpectedCodes { + if _, exists := nonRetryableCodes[code]; exists { + t.Errorf("did not expect code %v to be in nonRetryableCodes, but it was found", code) + } + } + + // Verify the map size matches expected + if len(nonRetryableCodes) != len(tt.expectedCodes) { + t.Errorf("expected map size %d, got %d", len(tt.expectedCodes), len(nonRetryableCodes)) + } + }) + } +} diff --git a/providers/flagd/pkg/service/in_process/grpc_sync.go b/providers/flagd/pkg/service/in_process/grpc_sync.go index 9b6b93caa..d5702b2c7 100644 --- a/providers/flagd/pkg/service/in_process/grpc_sync.go +++ b/providers/flagd/pkg/service/in_process/grpc_sync.go @@ -12,52 +12,12 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/connectivity" "google.golang.org/grpc/keepalive" + "google.golang.org/grpc/status" msync "sync" "time" ) -const ( - // Default timeouts and retry intervals - defaultKeepaliveTime = 30 * time.Second - defaultKeepaliveTimeout = 5 * time.Second - - retryPolicy = `{ - "methodConfig": [ - { - "name": [ - { - "service": "flagd.sync.v1.FlagSyncService" - } - ], - "retryPolicy": { - "MaxAttempts": 3, - "InitialBackoff": "1s", - "MaxBackoff": "5s", - "BackoffMultiplier": 2.0, - "RetryableStatusCodes": [ - "CANCELLED", - "UNKNOWN", - "INVALID_ARGUMENT", - "NOT_FOUND", - "ALREADY_EXISTS", - "PERMISSION_DENIED", - "RESOURCE_EXHAUSTED", - "FAILED_PRECONDITION", - "ABORTED", - "OUT_OF_RANGE", - "UNIMPLEMENTED", - "INTERNAL", - "UNAVAILABLE", - "DATA_LOSS", - "UNAUTHENTICATED" - ] - } - } - ] - }` -) - -// Type aliases for interfaces required by this component - needed for mock generation with gomock +// FlagSyncServiceClient Type aliases for interfaces required by this component - needed for mock generation with gomock type FlagSyncServiceClient interface { syncv1grpc.FlagSyncServiceClient } @@ -78,6 +38,7 @@ type Sync struct { Selector string URI string MaxMsgSize int + FatalStatusCodes []string // Runtime state client FlagSyncServiceClient @@ -92,6 +53,7 @@ type Sync struct { // Init initializes the gRPC connection and starts background monitoring func (g *Sync) Init(ctx context.Context) error { g.Logger.Info(fmt.Sprintf("initializing gRPC client for %s", g.URI)) + g.initNonRetryableStatusCodesSet() // Initialize channels g.shutdownComplete = make(chan struct{}) @@ -155,7 +117,7 @@ func (g *Sync) buildDialOptions() ([]grpc.DialOption, error) { } dialOptions = append(dialOptions, grpc.WithKeepaliveParams(keepaliveParams)) - dialOptions = append(dialOptions, grpc.WithDefaultServiceConfig(retryPolicy)) + dialOptions = append(dialOptions, grpc.WithDefaultServiceConfig(g.buildRetryPolicy())) return dialOptions, nil } @@ -213,6 +175,22 @@ func (g *Sync) Sync(ctx context.Context, dataSync chan<- sync.DataSync) error { return ctx.Err() } + // check for non-retryable errors during initialization, if found return with FATAL + if !g.IsReady() { + st, ok := status.FromError(err) + if ok { + if _, found := nonRetryableCodes[st.Code()]; found { + errStr := fmt.Sprintf("first sync cycle failed with non-retryable status: %v, "+ + "returning provider fatal.", st.Code().String()) + g.Logger.Error(errStr) + return &of.ProviderInitError{ + ErrorCode: of.ProviderFatalCode, + Message: errStr, + } + } + } + } + g.Logger.Warn(fmt.Sprintf("sync cycle failed: %v, retrying...", err)) g.sendEvent(ctx, SyncEvent{event: of.ProviderError}) @@ -248,12 +226,6 @@ func (g *Sync) performSyncCycle(ctx context.Context, dataSync chan<- sync.DataSy // handleFlagSync processes messages from the sync stream with proper context handling func (g *Sync) handleFlagSync(ctx context.Context, stream syncv1grpc.FlagSyncService_SyncFlagsClient, dataSync chan<- sync.DataSync) error { - // Mark as ready on first successful stream - g.initializer.Do(func() { - g.ready = true - g.Logger.Info("sync service is now ready") - }) - // Create channels for stream communication streamChan := make(chan *v1.SyncFlagsResponse, 1) errChan := make(chan error, 1) @@ -293,6 +265,11 @@ func (g *Sync) handleFlagSync(ctx context.Context, stream syncv1grpc.FlagSyncSer return err } + // Mark as ready on first successful stream + g.initializer.Do(func() { + g.ready = true + g.Logger.Info("sync service is now ready") + }) case err := <-errChan: return fmt.Errorf("stream error: %w", err) diff --git a/providers/flagd/pkg/service/in_process/service.go b/providers/flagd/pkg/service/in_process/service.go index 6cb6d0e1b..7e0bc5e9d 100644 --- a/providers/flagd/pkg/service/in_process/service.go +++ b/providers/flagd/pkg/service/in_process/service.go @@ -112,6 +112,9 @@ type Configuration struct { GrpcDialOptionsOverride []googlegrpc.DialOption CertificatePath string RetryGracePeriod int + RetryBackOffMs int + RetryBackOffMaxMs int + FatalStatusCodes []string } // EventSync interface for sync providers that support events @@ -569,6 +572,7 @@ func createSyncProvider(cfg Configuration, log *logger.Logger) (isync.ISync, str ProviderID: cfg.ProviderID, Selector: cfg.Selector, URI: uri, + FatalStatusCodes: cfg.FatalStatusCodes, }, uri } diff --git a/tests/flagd/testframework/provider_steps.go b/tests/flagd/testframework/provider_steps.go index 165853b2e..8f2d5ed18 100644 --- a/tests/flagd/testframework/provider_steps.go +++ b/tests/flagd/testframework/provider_steps.go @@ -27,6 +27,9 @@ func InitializeProviderSteps(ctx *godog.ScenarioContext) { // Generic provider step definition - accepts any provider type including "stable" ctx.Step(`^a (\w+) flagd provider$`, withState1Arg((*TestState).createSpecializedFlagdProvider)) + + ctx.Step(`^the client should be in (\w+) state$`, + withState1Arg((*TestState).assertClientState)) } // State methods - these now expect context as first parameter after state @@ -111,6 +114,13 @@ func (s *TestState) simulateConnectionLoss(ctx context.Context, seconds int) err return s.Container.Restart(seconds) } +func (s *TestState) assertClientState(ctx context.Context, state string) error { + if string(s.Client.State()) == strings.ToUpper(state) { + return nil + } + return fmt.Errorf("expected client state %s but got %s", state, s.Client.State()) +} + // createSpecializedFlagdProvider creates specialized flagd providers based on type func (s *TestState) createSpecializedFlagdProvider(ctx context.Context, providerType string) error { // Apply specialized configuration based on provider type @@ -128,7 +138,7 @@ func (s *TestState) createSpecializedFlagdProvider(ctx context.Context, provider return fmt.Errorf("failed to create instance for %s provider: %w", providerType, err) } - if providerType != "unavailable" { + if providerType != "unavailable" && providerType != "forbidden" { if s.ProviderType == RPC { // Small delay to allow flagd server to fully load flags after connection time.Sleep(50 * time.Millisecond) @@ -150,6 +160,8 @@ func (s *TestState) applySpecializedConfig(providerType string) error { return nil case "unavailable": return s.configureUnavailableProvider() + case "forbidden": + return s.configureForbiddenProvider() case "socket": return s.configureSocketProvider() case "ssl", "tls": @@ -173,6 +185,12 @@ func (s *TestState) configureUnavailableProvider() error { return nil } +func (s *TestState) configureForbiddenProvider() error { + // Set an Envoy port which always responds with forbidden + s.addProviderOption("port", "Integer", "9212") + return nil +} + func (s *TestState) configureSocketProvider() error { // Configure for unix socket connection s.addProviderOption("socketPath", "String", "/tmp/flagd.sock") diff --git a/tests/flagd/testframework/testbed_runner.go b/tests/flagd/testframework/testbed_runner.go index 2db1006e1..f069bed3e 100644 --- a/tests/flagd/testframework/testbed_runner.go +++ b/tests/flagd/testframework/testbed_runner.go @@ -278,7 +278,12 @@ func (tr *TestbedRunner) buildProviderOptions(state TestState, resolverType Prov }) break } - + } + if option.Option == "port" { + if option.Value == "9212" { + option.Value = strconv.Itoa(tr.container.forbiddenPort) + state.ProviderOptions[i] = option + } } } opts = append(opts, state.GenerateOpts()...) diff --git a/tests/flagd/testframework/testcontainer.go b/tests/flagd/testframework/testcontainer.go index 6f393e0c5..627c3f4bc 100644 --- a/tests/flagd/testframework/testcontainer.go +++ b/tests/flagd/testframework/testcontainer.go @@ -23,6 +23,7 @@ type FlagdTestContainer struct { launchpadPort int healthPort int envoyPort int + forbiddenPort int } // Container config type moved to types.go @@ -87,6 +88,10 @@ func NewFlagdContainer(ctx context.Context, config FlagdContainerConfig) (*Flagd if err != nil { return nil, err } + forbiddenPort, err := getMappedPort(ctx, composeStack, envoy, "9212") + if err != nil { + return nil, err + } flagdContainer := &FlagdTestContainer{ container: composeStack, @@ -96,6 +101,7 @@ func NewFlagdContainer(ctx context.Context, config FlagdContainerConfig) (*Flagd healthPort: healthPort, envoyPort: envoyPort, launchpadURL: fmt.Sprintf("http://%s:%d", host, launchpadPort), + forbiddenPort: forbiddenPort, } // Additional wait time if configured diff --git a/tests/flagd/testframework/utils.go b/tests/flagd/testframework/utils.go index 1b5a7bfa6..7fb0dda28 100644 --- a/tests/flagd/testframework/utils.go +++ b/tests/flagd/testframework/utils.go @@ -78,6 +78,12 @@ func (vc *ValueConverter) ConvertToReflectValue(valueType, value string, fieldTy panic(fmt.Errorf("failed to convert %s to long: %w", value, err)) } return reflect.ValueOf(longVal).Convert(fieldType) + case "StringList": + splitVal := strings.Split(value, ",") + for i, v := range splitVal { + splitVal[i] = strings.TrimSpace(v) + } + return reflect.ValueOf(splitVal).Convert(fieldType) default: return reflect.ValueOf(value).Convert(fieldType) } From f531ab0fccaeaf3423f6365748751249c68352d4 Mon Sep 17 00:00:00 2001 From: Alexandra Oberaigner Date: Fri, 28 Nov 2025 12:18:44 +0100 Subject: [PATCH 2/4] update testbed, remove PR-split leftovers Signed-off-by: Alexandra Oberaigner --- providers/flagd/flagd-testbed | 2 +- providers/flagd/pkg/service/in_process/service.go | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/providers/flagd/flagd-testbed b/providers/flagd/flagd-testbed index 4f3f5559a..9b73b3a95 160000 --- a/providers/flagd/flagd-testbed +++ b/providers/flagd/flagd-testbed @@ -1 +1 @@ -Subproject commit 4f3f5559a26fa12bf95d06d3dea9ac0879c1598f +Subproject commit 9b73b3a95cd9e0885937d244b118713b26374b1d diff --git a/providers/flagd/pkg/service/in_process/service.go b/providers/flagd/pkg/service/in_process/service.go index 7e0bc5e9d..0f48fa151 100644 --- a/providers/flagd/pkg/service/in_process/service.go +++ b/providers/flagd/pkg/service/in_process/service.go @@ -112,8 +112,6 @@ type Configuration struct { GrpcDialOptionsOverride []googlegrpc.DialOption CertificatePath string RetryGracePeriod int - RetryBackOffMs int - RetryBackOffMaxMs int FatalStatusCodes []string } From 9730a43810b3886a9b61e316cc03c945d8e547ee Mon Sep 17 00:00:00 2001 From: Alexandra Oberaigner Date: Fri, 28 Nov 2025 13:07:01 +0100 Subject: [PATCH 3/4] extend timeout for e2e tests Signed-off-by: Alexandra Oberaigner --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 32bb1b4e0..6fac510a0 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ test: # call with TESTCONTAINERS_RYUK_DISABLED="true" to avoid problems with podman on Macs e2e: - go clean -testcache && go list -f '{{.Dir}}/...' -m | xargs -I{} go test -timeout=2m -tags=e2e {} + go clean -testcache && go list -f '{{.Dir}}/...' -m | xargs -I{} go test -timeout=3m -tags=e2e {} lint: go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) From 7d6fff614c420e8b843d62e22ec5df4a6b58528d Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 28 Nov 2025 11:38:32 -0500 Subject: [PATCH 4/4] chore: Add sync-port exclusions for now Signed-off-by: Todd Baert --- providers/flagd/e2e/config_test.go | 8 ++++---- providers/flagd/e2e/rpc_test.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/providers/flagd/e2e/config_test.go b/providers/flagd/e2e/config_test.go index c68262eb3..9c8886713 100644 --- a/providers/flagd/e2e/config_test.go +++ b/providers/flagd/e2e/config_test.go @@ -20,19 +20,19 @@ func TestConfiguration(t *testing.T) { testCases := []configTestCase{ { name: "All", - tags: "", + tags: "~@sync-port", }, { name: "RPC", - tags: "@rpc", + tags: "@rpc && ~@sync-port", }, { name: "InProcess", - tags: "@in-process", + tags: "@in-process && ~@sync-port", }, { name: "File", - tags: "@file", + tags: "@file && ~@sync-port", }, } diff --git a/providers/flagd/e2e/rpc_test.go b/providers/flagd/e2e/rpc_test.go index 994c3627c..fe8451339 100644 --- a/providers/flagd/e2e/rpc_test.go +++ b/providers/flagd/e2e/rpc_test.go @@ -26,7 +26,7 @@ func TestRPCProviderE2E(t *testing.T) { } // Run tests with RPC-specific tags - exclude unimplemented scenarios - tags := "@rpc && ~@unixsocket && ~@targetURI && ~@sync && ~@metadata && ~@grace && ~@customCert && ~@caching && ~@forbidden" + tags := "@rpc && ~@unixsocket && ~@targetURI && ~@sync && ~@metadata && ~@grace && ~@customCert && ~@caching && ~@forbidden && ~@sync-port" if err := runner.RunGherkinTestsWithSubtests(t, featurePaths, tags); err != nil { t.Fatalf("Gherkin tests failed: %v", err)