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