Skip to content

feat: added custom grpc resolver #1008

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions providers/flagd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -123,6 +124,7 @@ Given below are the supported configurations:

> [!NOTE]
> Some configurations are only applicable for RPC resolver.
>

### Unix socket support

Expand Down Expand Up @@ -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();
```
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ public class FlagdOptions {
@Builder.Default
private String offlineFlagSourcePath = fallBackToEnvOrDefault(Config.OFFLINE_SOURCE_PATH, null);


/**
* gRPC custom target string.
*
* <p>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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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. <host>:<port>, 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();

Expand All @@ -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;

}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Custom URI Scheme:
*
* <p>envoy://[proxy-agent-host]:[proxy-agent-port]/[service-name]
*
* <p>`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> 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));
}
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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 + ":[//]<proxy-agent-host>:<proxy-agent-port>/<service-name>';"
+ "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("/");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ void TestBuilderOptions() {
.openTelemetry(openTelemetry)
.customConnector(connector)
.resolverType(Resolver.IN_PROCESS)
.targetUri("dns:///localhost:8016")
.keepAlive(1000)
.build();

Expand All @@ -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());
}

Expand Down Expand Up @@ -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");
}
}
Loading