diff --git a/providers/flagd/README.md b/providers/flagd/README.md
index b8508e9b4..457d56153 100644
--- a/providers/flagd/README.md
+++ b/providers/flagd/README.md
@@ -108,6 +108,7 @@ Given below are the supported configurations:
| resolver | FLAGD_RESOLVER | String - rpc, in-process | rpc | |
| host | FLAGD_HOST | String | localhost | rpc & in-process |
| port | FLAGD_PORT | int | 8013 | rpc & in-process |
+| targetUri | FLAGD_GRPC_TARGET | string | null | rpc & in-process |
| tls | FLAGD_TLS | boolean | false | rpc & in-process |
| socketPath | FLAGD_SOCKET_PATH | String | null | rpc & in-process |
| certPath | FLAGD_SERVER_CERT_PATH | String | null | rpc & in-process |
@@ -123,6 +124,7 @@ Given below are the supported configurations:
> [!NOTE]
> Some configurations are only applicable for RPC resolver.
+>
### Unix socket support
@@ -239,3 +241,17 @@ FlagdProvider flagdProvider = new FlagdProvider(options);
Please refer [OpenTelemetry example](https://opentelemetry.io/docs/instrumentation/java/manual/#example) for best practice guidelines.
Provider telemetry combined with [flagd OpenTelemetry](https://flagd.dev/reference/monitoring/#opentelemetry) allows you to have distributed traces.
+
+### Target URI Support (gRPC name resolution)
+
+The `targetUri` is meant for gRPC custom name resolution (default is `dns`), this allows users to use different
+resolution method e.g. `xds`. Currently, we are supporting all [core resolver](https://grpc.io/docs/guides/custom-name-resolution/)
+and one custom resolver for `envoy` proxy resolution. For more details, please refer the
+[RFC](https://github.com/open-feature/flagd/blob/main/docs/reference/specifications/proposal/rfc-grpc-custom-name-resolver.md) document.
+
+```java
+FlagdOptions options = FlagdOptions.builder()
+ .targetUri("envoy://localhost:9211/flag-source.service")
+ .resolverType(Config.Resolver.IN_PROCESS)
+ .build();
+```
\ No newline at end of file
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java
index f3d390a09..04571658b 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java
@@ -37,6 +37,7 @@ public final class Config {
static final String OFFLINE_SOURCE_PATH = "FLAGD_OFFLINE_FLAG_SOURCE_PATH";
static final String KEEP_ALIVE_MS_ENV_VAR_NAME_OLD = "FLAGD_KEEP_ALIVE_TIME";
static final String KEEP_ALIVE_MS_ENV_VAR_NAME = "FLAGD_KEEP_ALIVE_TIME_MS";
+ static final String GRPC_TARGET_ENV_VAR_NAME = "FLAGD_GRPC_TARGET";
static final String RESOLVER_RPC = "rpc";
static final String RESOLVER_IN_PROCESS = "in-process";
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 0c76e61d9..8184c246d 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
@@ -123,6 +123,18 @@ public class FlagdOptions {
@Builder.Default
private String offlineFlagSourcePath = fallBackToEnvOrDefault(Config.OFFLINE_SOURCE_PATH, null);
+
+ /**
+ * gRPC custom target string.
+ *
+ *
Setting this will allow user to use custom gRPC name resolver at present
+ * we are supporting all core resolver along with a custom resolver for envoy proxy
+ * resolution. For more visit (https://grpc.io/docs/guides/custom-name-resolution/)
+ */
+ @Builder.Default
+ private String targetUri = fallBackToEnvOrDefault(Config.GRPC_TARGET_ENV_VAR_NAME, null);
+
+
/**
* Function providing an EvaluationContext to mix into every evaluations.
* The sync-metadata response
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelBuilder.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelBuilder.java
index e5c94e21d..587f59cc7 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelBuilder.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelBuilder.java
@@ -1,8 +1,10 @@
package dev.openfeature.contrib.providers.flagd.resolver.common;
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
+import dev.openfeature.contrib.providers.flagd.resolver.common.nameresolvers.EnvoyResolverProvider;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.grpc.ManagedChannel;
+import io.grpc.NameResolverRegistry;
import io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.NettyChannelBuilder;
import io.netty.channel.epoll.Epoll;
@@ -13,6 +15,8 @@
import javax.net.ssl.SSLException;
import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.util.concurrent.TimeUnit;
/**
@@ -50,9 +54,21 @@ public static ManagedChannel nettyChannel(final FlagdOptions options) {
// build a TCP socket
try {
+ // Register custom resolver
+ if (isEnvoyTarget(options.getTargetUri())) {
+ NameResolverRegistry.getDefaultRegistry().register(new EnvoyResolverProvider());
+ }
+
+ // default to current `dns` resolution i.e. :, if valid / supported
+ // target string use the user provided target uri.
+ final String defaultTarget = String.format("%s:%s", options.getHost(), options.getPort());
+ final String targetUri = isValidTargetUri(options.getTargetUri()) ? options.getTargetUri() :
+ defaultTarget;
+
final NettyChannelBuilder builder = NettyChannelBuilder
- .forAddress(options.getHost(), options.getPort())
+ .forTarget(targetUri)
.keepAliveTime(keepAliveMs, TimeUnit.MILLISECONDS);
+
if (options.isTls()) {
SslContextBuilder sslContext = GrpcSslContexts.forClient();
@@ -78,6 +94,48 @@ public static ManagedChannel nettyChannel(final FlagdOptions options) {
SslConfigException sslConfigException = new SslConfigException("Error with SSL configuration.");
sslConfigException.initCause(ssle);
throw sslConfigException;
+ } catch (IllegalArgumentException argumentException) {
+ GenericConfigException genericConfigException = new GenericConfigException(
+ "Error with gRPC target string configuration");
+ genericConfigException.initCause(argumentException);
+ throw genericConfigException;
+ }
+ }
+
+ private static boolean isValidTargetUri(String targetUri) {
+ if (targetUri == null) {
+ return false;
+ }
+
+ try {
+ final String scheme = new URI(targetUri).getScheme();
+ if (scheme.equals(SupportedScheme.ENVOY.getScheme()) || scheme.equals(SupportedScheme.DNS.getScheme())
+ || scheme.equals(SupportedScheme.XDS.getScheme())
+ || scheme.equals(SupportedScheme.UDS.getScheme())) {
+ return true;
+ }
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Invalid target string", e);
}
+
+ return false;
+ }
+
+ private static boolean isEnvoyTarget(String targetUri) {
+ if (targetUri == null) {
+ return false;
+ }
+
+ try {
+ final String scheme = new URI(targetUri).getScheme();
+ if (scheme.equals(SupportedScheme.ENVOY.getScheme())) {
+ return true;
+ }
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Invalid target string", e);
+ }
+
+ return false;
+
}
}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/GenericConfigException.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/GenericConfigException.java
new file mode 100644
index 000000000..8bce152b2
--- /dev/null
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/GenericConfigException.java
@@ -0,0 +1,11 @@
+package dev.openfeature.contrib.providers.flagd.resolver.common;
+
+/**
+ * Custom exception for invalid gRPC configurations.
+ */
+
+public class GenericConfigException extends RuntimeException {
+ public GenericConfigException(String message) {
+ super(message);
+ }
+}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/SupportedScheme.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/SupportedScheme.java
new file mode 100644
index 000000000..74bc266c6
--- /dev/null
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/SupportedScheme.java
@@ -0,0 +1,14 @@
+package dev.openfeature.contrib.providers.flagd.resolver.common;
+
+import lombok.Getter;
+
+@Getter
+enum SupportedScheme {
+ ENVOY("envoy"), DNS("dns"), XDS("xds"), UDS("uds");
+
+ private final String scheme;
+
+ SupportedScheme(String scheme) {
+ this.scheme = scheme;
+ }
+}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/nameresolvers/EnvoyResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/nameresolvers/EnvoyResolver.java
new file mode 100644
index 000000000..e1b62ebfd
--- /dev/null
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/nameresolvers/EnvoyResolver.java
@@ -0,0 +1,69 @@
+package dev.openfeature.contrib.providers.flagd.resolver.common.nameresolvers;
+
+import io.grpc.EquivalentAddressGroup;
+import io.grpc.NameResolver;
+import java.net.InetSocketAddress;
+import io.grpc.Attributes;
+import io.grpc.Status;
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Envoy NameResolver, will always override the authority with the specified authority and
+ * use the socketAddress to connect.
+ *
+ * Custom URI Scheme:
+ *
+ *
envoy://[proxy-agent-host]:[proxy-agent-port]/[service-name]
+ *
+ *
`service-name` is used as authority instead host
+ */
+public class EnvoyResolver extends NameResolver {
+ private final URI uri;
+ private final String authority;
+ private Listener2 listener;
+
+ public EnvoyResolver(URI targetUri) {
+ this.uri = targetUri;
+ this.authority = targetUri.getPath().substring(1);
+ }
+
+ @Override
+ public String getServiceAuthority() {
+ return authority;
+ }
+
+ @Override
+ public void shutdown() {
+ }
+
+ @Override
+ public void start(Listener2 listener) {
+ this.listener = listener;
+ this.resolve();
+ }
+
+ @Override
+ public void refresh() {
+ this.resolve();
+ }
+
+ private void resolve() {
+ try {
+ InetSocketAddress address = new InetSocketAddress(this.uri.getHost(), this.uri.getPort());
+ Attributes addressGroupAttributes = Attributes.newBuilder()
+ .set(EquivalentAddressGroup.ATTR_AUTHORITY_OVERRIDE, this.authority)
+ .build();
+ List equivalentAddressGroup = Collections.singletonList(
+ new EquivalentAddressGroup(address, addressGroupAttributes)
+ );
+ ResolutionResult resolutionResult = ResolutionResult.newBuilder()
+ .setAddresses(equivalentAddressGroup)
+ .build();
+ this.listener.onResult(resolutionResult);
+ } catch (Exception e) {
+ this.listener.onError(Status.UNAVAILABLE.withDescription("Unable to resolve host ").withCause(e));
+ }
+ }
+}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/nameresolvers/EnvoyResolverProvider.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/nameresolvers/EnvoyResolverProvider.java
new file mode 100644
index 000000000..ab8000219
--- /dev/null
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/nameresolvers/EnvoyResolverProvider.java
@@ -0,0 +1,54 @@
+package dev.openfeature.contrib.providers.flagd.resolver.common.nameresolvers;
+
+import io.grpc.NameResolver;
+import io.grpc.NameResolverProvider;
+import java.net.URI;
+
+/**
+ * A custom NameResolver provider to resolve gRPC target uri for envoy in the
+ * format of.
+ *
+ * envoy://[proxy-agent-host]:[proxy-agent-port]/[service-name]
+ */
+public class EnvoyResolverProvider extends NameResolverProvider {
+ static final String ENVOY_SCHEME = "envoy";
+
+ @Override
+ protected boolean isAvailable() {
+ return true;
+ }
+
+ // setting priority higher than the default i.e. 5
+ // could lead to issue since the resolver override the default
+ // dns provider.
+ // https://grpc.github.io/grpc-java/javadoc/io/grpc/NameResolverProvider.html?is-external=true#priority()
+ @Override
+ protected int priority() {
+ return 5;
+ }
+
+ @Override
+ public NameResolver newNameResolver(URI targetUri, NameResolver.Args args) {
+ if (!ENVOY_SCHEME.equals(targetUri.getScheme())) {
+ return null;
+ }
+
+ if (!isValidPath(targetUri.getPath()) || targetUri.getHost() == null || targetUri.getPort() == -1) {
+ throw new IllegalArgumentException("Incorrectly formatted target uri; "
+ + "expected: '" + ENVOY_SCHEME + ":[//]:/';"
+ + "but was '" + targetUri + "'");
+ }
+
+ return new EnvoyResolver(targetUri);
+ }
+
+ @Override
+ public String getDefaultScheme() {
+ return ENVOY_SCHEME;
+ }
+
+ private static boolean isValidPath(String path) {
+ return !path.isEmpty() && !path.substring(1).isEmpty()
+ && !path.substring(1).contains("/");
+ }
+}
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 081193c22..acaa66a56 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
@@ -54,6 +54,7 @@ void TestBuilderOptions() {
.openTelemetry(openTelemetry)
.customConnector(connector)
.resolverType(Resolver.IN_PROCESS)
+ .targetUri("dns:///localhost:8016")
.keepAlive(1000)
.build();
@@ -69,6 +70,7 @@ void TestBuilderOptions() {
assertEquals(openTelemetry, flagdOptions.getOpenTelemetry());
assertEquals(connector, flagdOptions.getCustomConnector());
assertEquals(Resolver.IN_PROCESS, flagdOptions.getResolverType());
+ assertEquals("dns:///localhost:8016", flagdOptions.getTargetUri());
assertEquals(1000, flagdOptions.getKeepAlive());
}
@@ -187,4 +189,12 @@ void testRpcProviderFromEnv_portConfigured_usesConfiguredPort() {
assertThat(flagdOptions.getPort()).isEqualTo(1534);
}
+
+ @Test
+ @SetEnvironmentVariable(key = GRPC_TARGET_ENV_VAR_NAME, value = "envoy://localhost:1234/foo.service")
+ void testTargetOverrideFromEnv() {
+ FlagdOptions flagdOptions = FlagdOptions.builder().build();
+
+ assertThat(flagdOptions.getTargetUri()).isEqualTo("envoy://localhost:1234/foo.service");
+ }
}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ContainerConfig.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ContainerConfig.java
index 6b8ba78d6..f9f594b57 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ContainerConfig.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ContainerConfig.java
@@ -2,13 +2,16 @@
import org.jetbrains.annotations.NotNull;
import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
import org.testcontainers.utility.DockerImageName;
+import org.testcontainers.utility.MountableFile;
import java.io.IOException;
import java.util.Properties;
public class ContainerConfig {
private static final String version;
+ private static final Network network = Network.newNetwork();
static {
Properties properties = new Properties();
@@ -24,19 +27,27 @@ public class ContainerConfig {
*
* @return a {@link org.testcontainers.containers.GenericContainer} instance of a stable sync flagd server with the port 9090 exposed
*/
- public static GenericContainer sync() {
- return sync(false);
+ public static GenericContainer sync() {
+ return sync(false, false);
}
/**
*
* @param unstable if an unstable version of the container, which terminates the connection regularly should be used.
+ * @param addNetwork if set to true a custom network is attached for cross container access e.g. envoy --> sync:9090
* @return a {@link org.testcontainers.containers.GenericContainer} instance of a sync flagd server with the port 9090 exposed
*/
- public static GenericContainer sync(boolean unstable) {
+ public static GenericContainer sync(boolean unstable, boolean addNetwork) {
String container = generateContainerName("sync", unstable);
- return new GenericContainer(DockerImageName.parse(container))
+ GenericContainer genericContainer = new GenericContainer(DockerImageName.parse(container))
.withExposedPorts(9090);
+
+ if (addNetwork) {
+ genericContainer.withNetwork(network);
+ genericContainer.withNetworkAliases("sync-service");
+ }
+
+ return genericContainer;
}
/**
@@ -58,6 +69,22 @@ public static GenericContainer flagd(boolean unstable) {
.withExposedPorts(8013);
}
+
+ /**
+ * @return a {@link org.testcontainers.containers.GenericContainer} instance of envoy container using
+ * flagd sync service as backend expose on port 9211
+ *
+ */
+ public static GenericContainer envoy() {
+ final String container = "envoyproxy/envoy:v1.31.0";
+ return new GenericContainer(DockerImageName.parse(container))
+ .withCopyFileToContainer(MountableFile.forClasspathResource("/envoy-config/envoy-custom.yaml"),
+ "/etc/envoy/envoy.yaml")
+ .withExposedPorts(9211)
+ .withNetwork(network)
+ .withNetworkAliases("envoy");
+ }
+
private static @NotNull String generateContainerName(String type, boolean unstable) {
String container = "ghcr.io/open-feature/";
container += type;
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessCucumberTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessCucumberTest.java
index 048a1f328..0930a3710 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessCucumberTest.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessCucumberTest.java
@@ -20,7 +20,7 @@
@SelectClasspathResource("features/flagd-json-evaluator.feature")
@SelectClasspathResource("features/flagd.feature")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
-@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.contrib.providers.flagd.e2e.process,dev.openfeature.contrib.providers.flagd.e2e.steps")
+@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.contrib.providers.flagd.e2e.process.core,dev.openfeature.contrib.providers.flagd.e2e.steps")
@Testcontainers
public class RunFlagdInProcessCucumberTest {
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessEnvoyCucumberTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessEnvoyCucumberTest.java
new file mode 100644
index 000000000..2771f3546
--- /dev/null
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessEnvoyCucumberTest.java
@@ -0,0 +1,27 @@
+package dev.openfeature.contrib.providers.flagd.e2e;
+
+import org.junit.jupiter.api.Order;
+import org.junit.platform.suite.api.ConfigurationParameter;
+import org.junit.platform.suite.api.IncludeEngines;
+import org.junit.platform.suite.api.SelectClasspathResource;
+import org.junit.platform.suite.api.Suite;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
+import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME;
+
+/**
+ * Class for running the tests associated with "stable" e2e tests (no fake disconnection) for the in-process provider
+ */
+@Order(value = Integer.MAX_VALUE)
+@Suite
+@IncludeEngines("cucumber")
+@SelectClasspathResource("features/evaluation.feature")
+@SelectClasspathResource("features/flagd-json-evaluator.feature")
+@SelectClasspathResource("features/flagd.feature")
+@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
+@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.contrib.providers.flagd.e2e.process.envoy,dev.openfeature.contrib.providers.flagd.e2e.steps")
+@Testcontainers
+public class RunFlagdInProcessEnvoyCucumberTest {
+
+}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/FlagdInProcessSetup.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/core/FlagdInProcessSetup.java
similarity index 95%
rename from providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/FlagdInProcessSetup.java
rename to providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/core/FlagdInProcessSetup.java
index 6673c4fde..15d673a3a 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/FlagdInProcessSetup.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/core/FlagdInProcessSetup.java
@@ -1,4 +1,4 @@
-package dev.openfeature.contrib.providers.flagd.e2e.process;
+package dev.openfeature.contrib.providers.flagd.e2e.process.core;
import dev.openfeature.contrib.providers.flagd.e2e.ContainerConfig;
import io.cucumber.java.AfterAll;
@@ -37,4 +37,4 @@ public static void setup() throws InterruptedException {
public static void tearDown() {
flagdContainer.stop();
}
-}
+}
\ No newline at end of file
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/envoy/FlagdInProcessEnvoySetup.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/envoy/FlagdInProcessEnvoySetup.java
new file mode 100644
index 000000000..347bff725
--- /dev/null
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/envoy/FlagdInProcessEnvoySetup.java
@@ -0,0 +1,45 @@
+package dev.openfeature.contrib.providers.flagd.e2e.process.envoy;
+
+import dev.openfeature.contrib.providers.flagd.Config;
+import dev.openfeature.contrib.providers.flagd.FlagdOptions;
+import dev.openfeature.contrib.providers.flagd.FlagdProvider;
+import dev.openfeature.contrib.providers.flagd.e2e.ContainerConfig;
+import dev.openfeature.contrib.providers.flagd.e2e.steps.StepDefinitions;
+import dev.openfeature.sdk.FeatureProvider;
+import io.cucumber.java.AfterAll;
+import io.cucumber.java.BeforeAll;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.parallel.Isolated;
+import org.testcontainers.containers.GenericContainer;
+
+@Isolated()
+@Order(value = Integer.MAX_VALUE)
+public class FlagdInProcessEnvoySetup {
+
+ private static FeatureProvider provider;
+
+ private static final GenericContainer flagdContainer = ContainerConfig.sync(false, true);
+ private static final GenericContainer envoyContainer = ContainerConfig.envoy();
+
+ @BeforeAll()
+ public static void setup() throws InterruptedException {
+ flagdContainer.start();
+ envoyContainer.start();
+ final String targetUri = String.format("envoy://localhost:%s/flagd-sync.service",
+ envoyContainer.getFirstMappedPort());
+
+ FlagdInProcessEnvoySetup.provider = new FlagdProvider(FlagdOptions.builder()
+ .resolverType(Config.Resolver.IN_PROCESS)
+ .deadline(1000)
+ .streamDeadlineMs(0) // this makes reconnect tests more predictabl
+ .targetUri(targetUri)
+ .build());
+ StepDefinitions.setProvider(provider);
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ flagdContainer.stop();
+ envoyContainer.stop();
+ }
+}
\ No newline at end of file
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/process/FlagdInProcessSetup.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/process/FlagdInProcessSetup.java
index 1140c04c7..acceca46a 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/process/FlagdInProcessSetup.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/process/FlagdInProcessSetup.java
@@ -17,7 +17,7 @@
@Order(value = Integer.MAX_VALUE)
public class FlagdInProcessSetup {
- private static final GenericContainer flagdContainer = ContainerConfig.sync(true);
+ private static final GenericContainer flagdContainer = ContainerConfig.sync(true, false);
@BeforeAll()
public static void setup() throws InterruptedException {
flagdContainer.start();
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/common/SupportedSchemeTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/common/SupportedSchemeTest.java
new file mode 100644
index 000000000..8d7ba6c85
--- /dev/null
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/common/SupportedSchemeTest.java
@@ -0,0 +1,14 @@
+package dev.openfeature.contrib.providers.flagd.resolver.common;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class SupportedSchemeTest {
+ @Test
+ void supportedSchemeEnumTest() {
+ Assertions.assertEquals("envoy", SupportedScheme.ENVOY.getScheme());
+ Assertions.assertEquals("dns", SupportedScheme.DNS.getScheme());
+ Assertions.assertEquals("xds", SupportedScheme.XDS.getScheme());
+ Assertions.assertEquals("uds", SupportedScheme.UDS.getScheme());
+ }
+}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/common/nameresolvers/EnvoyResolverProviderTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/common/nameresolvers/EnvoyResolverProviderTest.java
new file mode 100644
index 000000000..271fb281e
--- /dev/null
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/common/nameresolvers/EnvoyResolverProviderTest.java
@@ -0,0 +1,46 @@
+package dev.openfeature.contrib.providers.flagd.resolver.common.nameresolvers;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.stream.Stream;
+
+class EnvoyResolverProviderTest {
+ private final EnvoyResolverProvider provider = new EnvoyResolverProvider();
+
+ @Test
+ void envoyProviderTestScheme() {
+ Assertions.assertTrue(provider.isAvailable());
+ Assertions.assertNotNull(provider.newNameResolver(URI.create("envoy://localhost:1234/foo.service"),
+ null));
+ Assertions.assertNull(provider.newNameResolver(URI.create("invalid-scheme://localhost:1234/foo.service"),
+ null));
+ }
+
+
+ @ParameterizedTest
+ @MethodSource("getInvalidPaths")
+ void invalidTargetUriTests(String mockUri) {
+ Exception exception = Assertions.assertThrows(IllegalArgumentException.class, () -> {
+ provider.newNameResolver(URI.create(mockUri), null);
+ });
+
+ Assertions.assertTrue(exception.toString().contains("Incorrectly formatted target uri"));
+ }
+
+ private static Stream getInvalidPaths() {
+ return Stream.of(
+ Arguments.of("envoy://localhost:1234/test.service/test"),
+ Arguments.of("envoy://localhost:1234/"),
+ Arguments.of("envoy://localhost:1234"),
+ Arguments.of("envoy://localhost/test.service"),
+ Arguments.of("envoy:///test.service")
+ );
+ }
+
+}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/common/nameresolvers/EnvoyResolverTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/common/nameresolvers/EnvoyResolverTest.java
new file mode 100644
index 000000000..c02ddf8a2
--- /dev/null
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/common/nameresolvers/EnvoyResolverTest.java
@@ -0,0 +1,16 @@
+package dev.openfeature.contrib.providers.flagd.resolver.common.nameresolvers;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import java.net.URI;
+
+class EnvoyResolverTest {
+ @Test
+ void envoyResolverTest() {
+ // given
+ EnvoyResolver envoyResolver = new EnvoyResolver(URI.create("envoy://localhost:1234/foo.service"));
+
+ // then
+ Assertions.assertEquals("foo.service", envoyResolver.getServiceAuthority());
+ }
+}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnectorTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnectorTest.java
index a6d1c1fab..7e552d05d 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnectorTest.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnectorTest.java
@@ -297,6 +297,7 @@ void stream_does_not_fail_with_deadline_error() throws Exception {
void host_and_port_arg_should_build_tcp_socket() {
final String host = "host.com";
final int port = 1234;
+ final String targetUri = String.format("%s:%s", host, port);
ServiceGrpc.ServiceBlockingStub mockBlockingStub = mock(ServiceGrpc.ServiceBlockingStub.class);
ServiceGrpc.ServiceStub mockStub = createServiceStubMock();
@@ -311,14 +312,14 @@ void host_and_port_arg_should_build_tcp_socket() {
try (MockedStatic mockStaticChannelBuilder = mockStatic(NettyChannelBuilder.class)) {
mockStaticChannelBuilder.when(() -> NettyChannelBuilder
- .forAddress(anyString(), anyInt())).thenReturn(mockChannelBuilder);
+ .forTarget(anyString())).thenReturn(mockChannelBuilder);
final FlagdOptions flagdOptions = FlagdOptions.builder().host(host).port(port).tls(false).build();
new GrpcConnector(flagdOptions, null, null, null);
// verify host/port matches
mockStaticChannelBuilder.verify(() -> NettyChannelBuilder
- .forAddress(host, port), times(1));
+ .forTarget(String.format(targetUri)), times(1));
}
}
}
@@ -327,6 +328,7 @@ void host_and_port_arg_should_build_tcp_socket() {
void no_args_host_and_port_env_set_should_build_tcp_socket() throws Exception {
final String host = "server.com";
final int port = 4321;
+ final String targetUri = String.format("%s:%s", host, port);
new EnvironmentVariables("FLAGD_HOST", host, "FLAGD_PORT", String.valueOf(port)).execute(() -> {
ServiceGrpc.ServiceBlockingStub mockBlockingStub = mock(ServiceGrpc.ServiceBlockingStub.class);
@@ -343,12 +345,12 @@ void no_args_host_and_port_env_set_should_build_tcp_socket() throws Exception {
NettyChannelBuilder.class)) {
mockStaticChannelBuilder.when(() -> NettyChannelBuilder
- .forAddress(anyString(), anyInt())).thenReturn(mockChannelBuilder);
+ .forTarget(anyString())).thenReturn(mockChannelBuilder);
new GrpcConnector(FlagdOptions.builder().build(), null, null, null);
// verify host/port matches & called times(= 1 as we rely on reusable channel)
- mockStaticChannelBuilder.verify(() -> NettyChannelBuilder.forAddress(host, port), times(1));
+ mockStaticChannelBuilder.verify(() -> NettyChannelBuilder.forTarget(targetUri), times(1));
}
}
});
diff --git a/providers/flagd/src/test/resources/envoy-config/envoy-custom.yaml b/providers/flagd/src/test/resources/envoy-config/envoy-custom.yaml
new file mode 100644
index 000000000..758a46b6d
--- /dev/null
+++ b/providers/flagd/src/test/resources/envoy-config/envoy-custom.yaml
@@ -0,0 +1,49 @@
+static_resources:
+ listeners:
+ - name: local-envoy
+ address:
+ socket_address:
+ address: 0.0.0.0
+ port_value: 9211
+ filter_chains:
+ - filters:
+ - name: envoy.filters.network.http_connection_manager
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+ stat_prefix: ingress_http
+ access_log:
+ - name: envoy.access_loggers.stdout
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
+ http_filters:
+ - name: envoy.filters.http.router
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+ route_config:
+ name: local_route
+ virtual_hosts:
+ - name: local_service
+ domains:
+ - "flagd-sync.service"
+ routes:
+ - match:
+ prefix: "/"
+ grpc: {}
+ route:
+ cluster: local-sync-service
+
+ clusters:
+ - name: local-sync-service
+ type: LOGICAL_DNS
+ # Comment out the following line to test on v6 networks
+ dns_lookup_family: V4_ONLY
+ http2_protocol_options: {}
+ load_assignment:
+ cluster_name: local-sync-service
+ endpoints:
+ - lb_endpoints:
+ - endpoint:
+ address:
+ socket_address:
+ address: sync-service
+ port_value: 9090