Skip to content

Commit a8d72ee

Browse files
authored
fix(flagd): validate FLAGD_SYNC_PORT before parse to avoid kubelet service-link collision (#1798)
Signed-off-by: Jason Benedicic <48251655+jabenedicic@users.noreply.github.com>
1 parent 75ff112 commit a8d72ee

2 files changed

Lines changed: 110 additions & 3 deletions

File tree

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
import jdk.jfr.Experimental;
1818
import lombok.Builder;
1919
import lombok.Getter;
20+
import lombok.extern.slf4j.Slf4j;
2021
import org.apache.commons.lang3.StringUtils;
2122

2223
/**
2324
* FlagdOptions is a builder to build flagd provider options.
2425
*/
2526
@Builder(toBuilder = true)
2627
@Getter
28+
@Slf4j
2729
@SuppressWarnings("PMD.TooManyStaticImports")
2830
public class FlagdOptions {
2931

@@ -314,9 +316,44 @@ void prebuild() {
314316
String defaultPort = determineDefaultPortForResolver();
315317
String fromPortEnv = fallBackToEnvOrDefault(Config.PORT_ENV_VAR_NAME, defaultPort);
316318

317-
String portValue = resolverType == Config.Resolver.IN_PROCESS
318-
? fallBackToEnvOrDefault(Config.SYNC_PORT_ENV_VAR_NAME, fromPortEnv)
319-
: fromPortEnv;
319+
String portValue = fromPortEnv;
320+
if (resolverType == Config.Resolver.IN_PROCESS) {
321+
String syncPortValue = fallBackToEnvOrDefault(Config.SYNC_PORT_ENV_VAR_NAME, null);
322+
if (syncPortValue != null) {
323+
if (isValidPort(syncPortValue)) {
324+
portValue = syncPortValue;
325+
} else {
326+
// FLAGD_SYNC_PORT is unset by the user but populated with a non-numeric
327+
// value — most commonly Kubernetes service-link env injection from a
328+
// Service named `flagd-sync` in the pod's namespace, which produces
329+
// `FLAGD_SYNC_PORT=tcp://<clusterIP>:<port>`. Fall back to FLAGD_PORT
330+
// rather than failing at parse time.
331+
log.warn(
332+
"Ignoring {} value '{}' (not a valid port); falling back to {} ('{}'). "
333+
+ "This commonly indicates Kubernetes service-link environment "
334+
+ "variable injection from a Service named 'flagd-sync' in the "
335+
+ "pod's namespace; set enableServiceLinks: false on the pod "
336+
+ "template or rename the Service to avoid the collision.",
337+
Config.SYNC_PORT_ENV_VAR_NAME,
338+
syncPortValue,
339+
Config.PORT_ENV_VAR_NAME,
340+
fromPortEnv);
341+
}
342+
}
343+
}
344+
345+
if (!isValidPort(portValue)) {
346+
// Last-line-of-defence: FLAGD_PORT itself can be polluted by Kubernetes
347+
// service-link injection too if a Service named `flagd` exists in the
348+
// pod's namespace (FLAGD_PORT=tcp://<clusterIP>:8013), which would
349+
// affect RPC-mode consumers identically. Fall back to the resolver's
350+
// default port rather than throwing at parse time.
351+
log.warn(
352+
"Configured port value '{}' is not a valid port; falling back to default '{}'.",
353+
portValue,
354+
defaultPort);
355+
portValue = defaultPort;
356+
}
320357

321358
port = Integer.parseInt(portValue);
322359
}
@@ -329,4 +366,16 @@ private String determineDefaultPortForResolver() {
329366
return Config.DEFAULT_IN_PROCESS_PORT;
330367
}
331368
}
369+
370+
private static boolean isValidPort(String value) {
371+
if (value == null) {
372+
return false;
373+
}
374+
try {
375+
int parsed = Integer.parseInt(value);
376+
return parsed > 0 && parsed <= 65535;
377+
} catch (NumberFormatException e) {
378+
return false;
379+
}
380+
}
332381
}

providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdOptionsTest.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,64 @@ void testInProcessProvider_syncPortTakesPrecedenceOverFlagdPort() {
237237
assertThat(flagdOptions.getPort()).isEqualTo(9999);
238238
}
239239

240+
@Test
241+
@SetEnvironmentVariable(key = RESOLVER_ENV_VAR, value = RESOLVER_IN_PROCESS)
242+
@SetEnvironmentVariable(key = PORT_ENV_VAR_NAME, value = "8888")
243+
@SetEnvironmentVariable(key = SYNC_PORT_ENV_VAR_NAME, value = "tcp://10.0.0.1:8015")
244+
void testInProcessProvider_invalidSyncPortFallsBackToFlagdPort() {
245+
// Kubernetes service-link injection populates FLAGD_SYNC_PORT with a URL like
246+
// tcp://<clusterIP>:<port> when a Service named flagd-sync shares the pod's
247+
// namespace. The SDK must not fail on this; it should fall back to FLAGD_PORT.
248+
FlagdOptions flagdOptions = FlagdOptions.builder().build();
249+
250+
assertThat(flagdOptions.getResolverType()).isEqualTo(Resolver.IN_PROCESS);
251+
assertThat(flagdOptions.getPort()).isEqualTo(8888);
252+
}
253+
254+
@Test
255+
@SetEnvironmentVariable(key = RESOLVER_ENV_VAR, value = RESOLVER_IN_PROCESS)
256+
@SetEnvironmentVariable(key = SYNC_PORT_ENV_VAR_NAME, value = "tcp://10.0.0.1:8015")
257+
void testInProcessProvider_invalidSyncPortWithNoFlagdPortUsesDefault() {
258+
FlagdOptions flagdOptions = FlagdOptions.builder().build();
259+
260+
assertThat(flagdOptions.getResolverType()).isEqualTo(Resolver.IN_PROCESS);
261+
assertThat(flagdOptions.getPort()).isEqualTo(Integer.parseInt(DEFAULT_IN_PROCESS_PORT));
262+
}
263+
264+
@Test
265+
@SetEnvironmentVariable(key = RESOLVER_ENV_VAR, value = RESOLVER_IN_PROCESS)
266+
@SetEnvironmentVariable(key = PORT_ENV_VAR_NAME, value = "8888")
267+
@SetEnvironmentVariable(key = SYNC_PORT_ENV_VAR_NAME, value = "99999")
268+
void testInProcessProvider_outOfRangeSyncPortFallsBackToFlagdPort() {
269+
FlagdOptions flagdOptions = FlagdOptions.builder().build();
270+
271+
assertThat(flagdOptions.getResolverType()).isEqualTo(Resolver.IN_PROCESS);
272+
assertThat(flagdOptions.getPort()).isEqualTo(8888);
273+
}
274+
275+
@Test
276+
@SetEnvironmentVariable(key = RESOLVER_ENV_VAR, value = RESOLVER_RPC)
277+
@SetEnvironmentVariable(key = PORT_ENV_VAR_NAME, value = "tcp://10.0.0.1:8013")
278+
void testRpcProvider_invalidFlagdPortFallsBackToDefault() {
279+
// RPC-mode equivalent of the in-process collision: if a Service named `flagd`
280+
// shares the pod's namespace, kubelet injects FLAGD_PORT=tcp://<clusterIP>:8013.
281+
FlagdOptions flagdOptions = FlagdOptions.builder().build();
282+
283+
assertThat(flagdOptions.getResolverType()).isEqualTo(Resolver.RPC);
284+
assertThat(flagdOptions.getPort()).isEqualTo(Integer.parseInt(DEFAULT_RPC_PORT));
285+
}
286+
287+
@Test
288+
@SetEnvironmentVariable(key = RESOLVER_ENV_VAR, value = RESOLVER_IN_PROCESS)
289+
@SetEnvironmentVariable(key = PORT_ENV_VAR_NAME, value = "tcp://10.0.0.1:8013")
290+
@SetEnvironmentVariable(key = SYNC_PORT_ENV_VAR_NAME, value = "tcp://10.0.0.1:8015")
291+
void testInProcessProvider_bothPortEnvsInvalidFallsBackToDefault() {
292+
FlagdOptions flagdOptions = FlagdOptions.builder().build();
293+
294+
assertThat(flagdOptions.getResolverType()).isEqualTo(Resolver.IN_PROCESS);
295+
assertThat(flagdOptions.getPort()).isEqualTo(Integer.parseInt(DEFAULT_IN_PROCESS_PORT));
296+
}
297+
240298
@Test
241299
@SetEnvironmentVariable(key = RESOLVER_ENV_VAR, value = RESOLVER_RPC)
242300
void testRpcProviderFromEnv_noPortConfigured_defaultsToCorrectPort() {

0 commit comments

Comments
 (0)