Skip to content

Commit

Permalink
feat: added custom grpc resolver (#1008)
Browse files Browse the repository at this point in the history
Signed-off-by: Pradeep Mishra <pradeepbbl@users.noreply.github.com>
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
  • Loading branch information
pradeepbbl and beeme1mr authored Oct 16, 2024
1 parent d88a6d2 commit 85403b7
Show file tree
Hide file tree
Showing 20 changed files with 484 additions and 13 deletions.
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

0 comments on commit 85403b7

Please sign in to comment.