Skip to content

Commit 85403b7

Browse files
pradeepbblbeeme1mr
andauthored
feat: added custom grpc resolver (#1008)
Signed-off-by: Pradeep Mishra <[email protected]> Co-authored-by: Michael Beemer <[email protected]>
1 parent d88a6d2 commit 85403b7

20 files changed

+484
-13
lines changed

providers/flagd/README.md

+16
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ Given below are the supported configurations:
108108
| resolver | FLAGD_RESOLVER | String - rpc, in-process | rpc | |
109109
| host | FLAGD_HOST | String | localhost | rpc & in-process |
110110
| port | FLAGD_PORT | int | 8013 | rpc & in-process |
111+
| targetUri | FLAGD_GRPC_TARGET | string | null | rpc & in-process |
111112
| tls | FLAGD_TLS | boolean | false | rpc & in-process |
112113
| socketPath | FLAGD_SOCKET_PATH | String | null | rpc & in-process |
113114
| certPath | FLAGD_SERVER_CERT_PATH | String | null | rpc & in-process |
@@ -123,6 +124,7 @@ Given below are the supported configurations:
123124

124125
> [!NOTE]
125126
> Some configurations are only applicable for RPC resolver.
127+
>
126128
127129
### Unix socket support
128130

@@ -239,3 +241,17 @@ FlagdProvider flagdProvider = new FlagdProvider(options);
239241
Please refer [OpenTelemetry example](https://opentelemetry.io/docs/instrumentation/java/manual/#example) for best practice guidelines.
240242

241243
Provider telemetry combined with [flagd OpenTelemetry](https://flagd.dev/reference/monitoring/#opentelemetry) allows you to have distributed traces.
244+
245+
### Target URI Support (gRPC name resolution)
246+
247+
The `targetUri` is meant for gRPC custom name resolution (default is `dns`), this allows users to use different
248+
resolution method e.g. `xds`. Currently, we are supporting all [core resolver](https://grpc.io/docs/guides/custom-name-resolution/)
249+
and one custom resolver for `envoy` proxy resolution. For more details, please refer the
250+
[RFC](https://github.com/open-feature/flagd/blob/main/docs/reference/specifications/proposal/rfc-grpc-custom-name-resolver.md) document.
251+
252+
```java
253+
FlagdOptions options = FlagdOptions.builder()
254+
.targetUri("envoy://localhost:9211/flag-source.service")
255+
.resolverType(Config.Resolver.IN_PROCESS)
256+
.build();
257+
```

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

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public final class Config {
3737
static final String OFFLINE_SOURCE_PATH = "FLAGD_OFFLINE_FLAG_SOURCE_PATH";
3838
static final String KEEP_ALIVE_MS_ENV_VAR_NAME_OLD = "FLAGD_KEEP_ALIVE_TIME";
3939
static final String KEEP_ALIVE_MS_ENV_VAR_NAME = "FLAGD_KEEP_ALIVE_TIME_MS";
40+
static final String GRPC_TARGET_ENV_VAR_NAME = "FLAGD_GRPC_TARGET";
4041

4142
static final String RESOLVER_RPC = "rpc";
4243
static final String RESOLVER_IN_PROCESS = "in-process";

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

+12
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,18 @@ public class FlagdOptions {
123123
@Builder.Default
124124
private String offlineFlagSourcePath = fallBackToEnvOrDefault(Config.OFFLINE_SOURCE_PATH, null);
125125

126+
127+
/**
128+
* gRPC custom target string.
129+
*
130+
* <p>Setting this will allow user to use custom gRPC name resolver at present
131+
* we are supporting all core resolver along with a custom resolver for envoy proxy
132+
* resolution. For more visit (https://grpc.io/docs/guides/custom-name-resolution/)
133+
*/
134+
@Builder.Default
135+
private String targetUri = fallBackToEnvOrDefault(Config.GRPC_TARGET_ENV_VAR_NAME, null);
136+
137+
126138
/**
127139
* Function providing an EvaluationContext to mix into every evaluations.
128140
* The sync-metadata response

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelBuilder.java

+59-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package dev.openfeature.contrib.providers.flagd.resolver.common;
22

33
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
4+
import dev.openfeature.contrib.providers.flagd.resolver.common.nameresolvers.EnvoyResolverProvider;
45
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
56
import io.grpc.ManagedChannel;
7+
import io.grpc.NameResolverRegistry;
68
import io.grpc.netty.GrpcSslContexts;
79
import io.grpc.netty.NettyChannelBuilder;
810
import io.netty.channel.epoll.Epoll;
@@ -13,6 +15,8 @@
1315

1416
import javax.net.ssl.SSLException;
1517
import java.io.File;
18+
import java.net.URI;
19+
import java.net.URISyntaxException;
1620
import java.util.concurrent.TimeUnit;
1721

1822
/**
@@ -50,9 +54,21 @@ public static ManagedChannel nettyChannel(final FlagdOptions options) {
5054

5155
// build a TCP socket
5256
try {
57+
// Register custom resolver
58+
if (isEnvoyTarget(options.getTargetUri())) {
59+
NameResolverRegistry.getDefaultRegistry().register(new EnvoyResolverProvider());
60+
}
61+
62+
// default to current `dns` resolution i.e. <host>:<port>, if valid / supported
63+
// target string use the user provided target uri.
64+
final String defaultTarget = String.format("%s:%s", options.getHost(), options.getPort());
65+
final String targetUri = isValidTargetUri(options.getTargetUri()) ? options.getTargetUri() :
66+
defaultTarget;
67+
5368
final NettyChannelBuilder builder = NettyChannelBuilder
54-
.forAddress(options.getHost(), options.getPort())
69+
.forTarget(targetUri)
5570
.keepAliveTime(keepAliveMs, TimeUnit.MILLISECONDS);
71+
5672
if (options.isTls()) {
5773
SslContextBuilder sslContext = GrpcSslContexts.forClient();
5874

@@ -78,6 +94,48 @@ public static ManagedChannel nettyChannel(final FlagdOptions options) {
7894
SslConfigException sslConfigException = new SslConfigException("Error with SSL configuration.");
7995
sslConfigException.initCause(ssle);
8096
throw sslConfigException;
97+
} catch (IllegalArgumentException argumentException) {
98+
GenericConfigException genericConfigException = new GenericConfigException(
99+
"Error with gRPC target string configuration");
100+
genericConfigException.initCause(argumentException);
101+
throw genericConfigException;
102+
}
103+
}
104+
105+
private static boolean isValidTargetUri(String targetUri) {
106+
if (targetUri == null) {
107+
return false;
108+
}
109+
110+
try {
111+
final String scheme = new URI(targetUri).getScheme();
112+
if (scheme.equals(SupportedScheme.ENVOY.getScheme()) || scheme.equals(SupportedScheme.DNS.getScheme())
113+
|| scheme.equals(SupportedScheme.XDS.getScheme())
114+
|| scheme.equals(SupportedScheme.UDS.getScheme())) {
115+
return true;
116+
}
117+
} catch (URISyntaxException e) {
118+
throw new IllegalArgumentException("Invalid target string", e);
81119
}
120+
121+
return false;
122+
}
123+
124+
private static boolean isEnvoyTarget(String targetUri) {
125+
if (targetUri == null) {
126+
return false;
127+
}
128+
129+
try {
130+
final String scheme = new URI(targetUri).getScheme();
131+
if (scheme.equals(SupportedScheme.ENVOY.getScheme())) {
132+
return true;
133+
}
134+
} catch (URISyntaxException e) {
135+
throw new IllegalArgumentException("Invalid target string", e);
136+
}
137+
138+
return false;
139+
82140
}
83141
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.common;
2+
3+
/**
4+
* Custom exception for invalid gRPC configurations.
5+
*/
6+
7+
public class GenericConfigException extends RuntimeException {
8+
public GenericConfigException(String message) {
9+
super(message);
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.common;
2+
3+
import lombok.Getter;
4+
5+
@Getter
6+
enum SupportedScheme {
7+
ENVOY("envoy"), DNS("dns"), XDS("xds"), UDS("uds");
8+
9+
private final String scheme;
10+
11+
SupportedScheme(String scheme) {
12+
this.scheme = scheme;
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.common.nameresolvers;
2+
3+
import io.grpc.EquivalentAddressGroup;
4+
import io.grpc.NameResolver;
5+
import java.net.InetSocketAddress;
6+
import io.grpc.Attributes;
7+
import io.grpc.Status;
8+
import java.net.URI;
9+
import java.util.Collections;
10+
import java.util.List;
11+
12+
/**
13+
* Envoy NameResolver, will always override the authority with the specified authority and
14+
* use the socketAddress to connect.
15+
*
16+
* <p>Custom URI Scheme:
17+
*
18+
* <p>envoy://[proxy-agent-host]:[proxy-agent-port]/[service-name]
19+
*
20+
* <p>`service-name` is used as authority instead host
21+
*/
22+
public class EnvoyResolver extends NameResolver {
23+
private final URI uri;
24+
private final String authority;
25+
private Listener2 listener;
26+
27+
public EnvoyResolver(URI targetUri) {
28+
this.uri = targetUri;
29+
this.authority = targetUri.getPath().substring(1);
30+
}
31+
32+
@Override
33+
public String getServiceAuthority() {
34+
return authority;
35+
}
36+
37+
@Override
38+
public void shutdown() {
39+
}
40+
41+
@Override
42+
public void start(Listener2 listener) {
43+
this.listener = listener;
44+
this.resolve();
45+
}
46+
47+
@Override
48+
public void refresh() {
49+
this.resolve();
50+
}
51+
52+
private void resolve() {
53+
try {
54+
InetSocketAddress address = new InetSocketAddress(this.uri.getHost(), this.uri.getPort());
55+
Attributes addressGroupAttributes = Attributes.newBuilder()
56+
.set(EquivalentAddressGroup.ATTR_AUTHORITY_OVERRIDE, this.authority)
57+
.build();
58+
List<EquivalentAddressGroup> equivalentAddressGroup = Collections.singletonList(
59+
new EquivalentAddressGroup(address, addressGroupAttributes)
60+
);
61+
ResolutionResult resolutionResult = ResolutionResult.newBuilder()
62+
.setAddresses(equivalentAddressGroup)
63+
.build();
64+
this.listener.onResult(resolutionResult);
65+
} catch (Exception e) {
66+
this.listener.onError(Status.UNAVAILABLE.withDescription("Unable to resolve host ").withCause(e));
67+
}
68+
}
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.common.nameresolvers;
2+
3+
import io.grpc.NameResolver;
4+
import io.grpc.NameResolverProvider;
5+
import java.net.URI;
6+
7+
/**
8+
* A custom NameResolver provider to resolve gRPC target uri for envoy in the
9+
* format of.
10+
*
11+
* <p>envoy://[proxy-agent-host]:[proxy-agent-port]/[service-name]
12+
*/
13+
public class EnvoyResolverProvider extends NameResolverProvider {
14+
static final String ENVOY_SCHEME = "envoy";
15+
16+
@Override
17+
protected boolean isAvailable() {
18+
return true;
19+
}
20+
21+
// setting priority higher than the default i.e. 5
22+
// could lead to issue since the resolver override the default
23+
// dns provider.
24+
// https://grpc.github.io/grpc-java/javadoc/io/grpc/NameResolverProvider.html?is-external=true#priority()
25+
@Override
26+
protected int priority() {
27+
return 5;
28+
}
29+
30+
@Override
31+
public NameResolver newNameResolver(URI targetUri, NameResolver.Args args) {
32+
if (!ENVOY_SCHEME.equals(targetUri.getScheme())) {
33+
return null;
34+
}
35+
36+
if (!isValidPath(targetUri.getPath()) || targetUri.getHost() == null || targetUri.getPort() == -1) {
37+
throw new IllegalArgumentException("Incorrectly formatted target uri; "
38+
+ "expected: '" + ENVOY_SCHEME + ":[//]<proxy-agent-host>:<proxy-agent-port>/<service-name>';"
39+
+ "but was '" + targetUri + "'");
40+
}
41+
42+
return new EnvoyResolver(targetUri);
43+
}
44+
45+
@Override
46+
public String getDefaultScheme() {
47+
return ENVOY_SCHEME;
48+
}
49+
50+
private static boolean isValidPath(String path) {
51+
return !path.isEmpty() && !path.substring(1).isEmpty()
52+
&& !path.substring(1).contains("/");
53+
}
54+
}

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

+10
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ void TestBuilderOptions() {
5454
.openTelemetry(openTelemetry)
5555
.customConnector(connector)
5656
.resolverType(Resolver.IN_PROCESS)
57+
.targetUri("dns:///localhost:8016")
5758
.keepAlive(1000)
5859
.build();
5960

@@ -69,6 +70,7 @@ void TestBuilderOptions() {
6970
assertEquals(openTelemetry, flagdOptions.getOpenTelemetry());
7071
assertEquals(connector, flagdOptions.getCustomConnector());
7172
assertEquals(Resolver.IN_PROCESS, flagdOptions.getResolverType());
73+
assertEquals("dns:///localhost:8016", flagdOptions.getTargetUri());
7274
assertEquals(1000, flagdOptions.getKeepAlive());
7375
}
7476

@@ -187,4 +189,12 @@ void testRpcProviderFromEnv_portConfigured_usesConfiguredPort() {
187189
assertThat(flagdOptions.getPort()).isEqualTo(1534);
188190

189191
}
192+
193+
@Test
194+
@SetEnvironmentVariable(key = GRPC_TARGET_ENV_VAR_NAME, value = "envoy://localhost:1234/foo.service")
195+
void testTargetOverrideFromEnv() {
196+
FlagdOptions flagdOptions = FlagdOptions.builder().build();
197+
198+
assertThat(flagdOptions.getTargetUri()).isEqualTo("envoy://localhost:1234/foo.service");
199+
}
190200
}

0 commit comments

Comments
 (0)