diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java index 673c9dcea..1859100d1 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java @@ -17,6 +17,7 @@ import jdk.jfr.Experimental; import lombok.Builder; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; /** @@ -24,6 +25,7 @@ */ @Builder(toBuilder = true) @Getter +@Slf4j @SuppressWarnings("PMD.TooManyStaticImports") public class FlagdOptions { @@ -314,9 +316,44 @@ void prebuild() { String defaultPort = determineDefaultPortForResolver(); String fromPortEnv = fallBackToEnvOrDefault(Config.PORT_ENV_VAR_NAME, defaultPort); - String portValue = resolverType == Config.Resolver.IN_PROCESS - ? fallBackToEnvOrDefault(Config.SYNC_PORT_ENV_VAR_NAME, fromPortEnv) - : fromPortEnv; + String portValue = fromPortEnv; + if (resolverType == Config.Resolver.IN_PROCESS) { + String syncPortValue = fallBackToEnvOrDefault(Config.SYNC_PORT_ENV_VAR_NAME, null); + if (syncPortValue != null) { + if (isValidPort(syncPortValue)) { + portValue = syncPortValue; + } else { + // FLAGD_SYNC_PORT is unset by the user but populated with a non-numeric + // value — most commonly Kubernetes service-link env injection from a + // Service named `flagd-sync` in the pod's namespace, which produces + // `FLAGD_SYNC_PORT=tcp://:`. Fall back to FLAGD_PORT + // rather than failing at parse time. + log.warn( + "Ignoring {} value '{}' (not a valid port); falling back to {} ('{}'). " + + "This commonly indicates Kubernetes service-link environment " + + "variable injection from a Service named 'flagd-sync' in the " + + "pod's namespace; set enableServiceLinks: false on the pod " + + "template or rename the Service to avoid the collision.", + Config.SYNC_PORT_ENV_VAR_NAME, + syncPortValue, + Config.PORT_ENV_VAR_NAME, + fromPortEnv); + } + } + } + + if (!isValidPort(portValue)) { + // Last-line-of-defence: FLAGD_PORT itself can be polluted by Kubernetes + // service-link injection too if a Service named `flagd` exists in the + // pod's namespace (FLAGD_PORT=tcp://:8013), which would + // affect RPC-mode consumers identically. Fall back to the resolver's + // default port rather than throwing at parse time. + log.warn( + "Configured port value '{}' is not a valid port; falling back to default '{}'.", + portValue, + defaultPort); + portValue = defaultPort; + } port = Integer.parseInt(portValue); } @@ -329,4 +366,16 @@ private String determineDefaultPortForResolver() { return Config.DEFAULT_IN_PROCESS_PORT; } } + + private static boolean isValidPort(String value) { + if (value == null) { + return false; + } + try { + int parsed = Integer.parseInt(value); + return parsed > 0 && parsed <= 65535; + } catch (NumberFormatException e) { + return false; + } + } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdOptionsTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdOptionsTest.java index 9468e9a7f..f6c3cab2f 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdOptionsTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdOptionsTest.java @@ -237,6 +237,64 @@ void testInProcessProvider_syncPortTakesPrecedenceOverFlagdPort() { assertThat(flagdOptions.getPort()).isEqualTo(9999); } + @Test + @SetEnvironmentVariable(key = RESOLVER_ENV_VAR, value = RESOLVER_IN_PROCESS) + @SetEnvironmentVariable(key = PORT_ENV_VAR_NAME, value = "8888") + @SetEnvironmentVariable(key = SYNC_PORT_ENV_VAR_NAME, value = "tcp://10.0.0.1:8015") + void testInProcessProvider_invalidSyncPortFallsBackToFlagdPort() { + // Kubernetes service-link injection populates FLAGD_SYNC_PORT with a URL like + // tcp://: when a Service named flagd-sync shares the pod's + // namespace. The SDK must not fail on this; it should fall back to FLAGD_PORT. + FlagdOptions flagdOptions = FlagdOptions.builder().build(); + + assertThat(flagdOptions.getResolverType()).isEqualTo(Resolver.IN_PROCESS); + assertThat(flagdOptions.getPort()).isEqualTo(8888); + } + + @Test + @SetEnvironmentVariable(key = RESOLVER_ENV_VAR, value = RESOLVER_IN_PROCESS) + @SetEnvironmentVariable(key = SYNC_PORT_ENV_VAR_NAME, value = "tcp://10.0.0.1:8015") + void testInProcessProvider_invalidSyncPortWithNoFlagdPortUsesDefault() { + FlagdOptions flagdOptions = FlagdOptions.builder().build(); + + assertThat(flagdOptions.getResolverType()).isEqualTo(Resolver.IN_PROCESS); + assertThat(flagdOptions.getPort()).isEqualTo(Integer.parseInt(DEFAULT_IN_PROCESS_PORT)); + } + + @Test + @SetEnvironmentVariable(key = RESOLVER_ENV_VAR, value = RESOLVER_IN_PROCESS) + @SetEnvironmentVariable(key = PORT_ENV_VAR_NAME, value = "8888") + @SetEnvironmentVariable(key = SYNC_PORT_ENV_VAR_NAME, value = "99999") + void testInProcessProvider_outOfRangeSyncPortFallsBackToFlagdPort() { + FlagdOptions flagdOptions = FlagdOptions.builder().build(); + + assertThat(flagdOptions.getResolverType()).isEqualTo(Resolver.IN_PROCESS); + assertThat(flagdOptions.getPort()).isEqualTo(8888); + } + + @Test + @SetEnvironmentVariable(key = RESOLVER_ENV_VAR, value = RESOLVER_RPC) + @SetEnvironmentVariable(key = PORT_ENV_VAR_NAME, value = "tcp://10.0.0.1:8013") + void testRpcProvider_invalidFlagdPortFallsBackToDefault() { + // RPC-mode equivalent of the in-process collision: if a Service named `flagd` + // shares the pod's namespace, kubelet injects FLAGD_PORT=tcp://:8013. + FlagdOptions flagdOptions = FlagdOptions.builder().build(); + + assertThat(flagdOptions.getResolverType()).isEqualTo(Resolver.RPC); + assertThat(flagdOptions.getPort()).isEqualTo(Integer.parseInt(DEFAULT_RPC_PORT)); + } + + @Test + @SetEnvironmentVariable(key = RESOLVER_ENV_VAR, value = RESOLVER_IN_PROCESS) + @SetEnvironmentVariable(key = PORT_ENV_VAR_NAME, value = "tcp://10.0.0.1:8013") + @SetEnvironmentVariable(key = SYNC_PORT_ENV_VAR_NAME, value = "tcp://10.0.0.1:8015") + void testInProcessProvider_bothPortEnvsInvalidFallsBackToDefault() { + FlagdOptions flagdOptions = FlagdOptions.builder().build(); + + assertThat(flagdOptions.getResolverType()).isEqualTo(Resolver.IN_PROCESS); + assertThat(flagdOptions.getPort()).isEqualTo(Integer.parseInt(DEFAULT_IN_PROCESS_PORT)); + } + @Test @SetEnvironmentVariable(key = RESOLVER_ENV_VAR, value = RESOLVER_RPC) void testRpcProviderFromEnv_noPortConfigured_defaultsToCorrectPort() {