diff --git a/rls/src/main/java/io/grpc/rls/RlsProtoData.java b/rls/src/main/java/io/grpc/rls/RlsProtoData.java index f13f0369d89..5192e9cf075 100644 --- a/rls/src/main/java/io/grpc/rls/RlsProtoData.java +++ b/rls/src/main/java/io/grpc/rls/RlsProtoData.java @@ -25,6 +25,7 @@ import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import io.grpc.Internal; import io.grpc.rls.RlsProtoData.GrpcKeyBuilder.Name; import java.util.HashSet; import java.util.List; @@ -35,7 +36,10 @@ import javax.annotation.concurrent.Immutable; /** RlsProtoData is a collection of internal representation of RouteLookupService proto messages. */ -final class RlsProtoData { +@Internal +public final class RlsProtoData { + + private RlsProtoData() {} /** A request object sent to route lookup service. */ @Immutable @@ -138,7 +142,7 @@ public String toString() { /** A config object for gRPC RouteLookupService. */ @Immutable - static final class RouteLookupConfig { + public static final class RouteLookupConfig { private static final long MAX_AGE_MILLIS = TimeUnit.MINUTES.toMillis(5); private static final long MAX_CACHE_SIZE = 5 * 1024 * 1024; @@ -160,7 +164,8 @@ static final class RouteLookupConfig { @Nullable private final String defaultTarget; - RouteLookupConfig( + /** Constructor. */ + public RouteLookupConfig( List grpcKeyBuilders, String lookupService, long lookupServiceTimeoutInMillis, @@ -197,6 +202,9 @@ static final class RouteLookupConfig { checkArgument(cacheSizeBytes > 0, "cacheSize must be positive"); this.cacheSizeBytes = Math.min(cacheSizeBytes, MAX_CACHE_SIZE); this.validTargets = ImmutableList.copyOf(checkNotNull(validTargets, "validTargets")); + if (defaultTarget != null && defaultTarget.isEmpty()) { + defaultTarget = null; + } this.defaultTarget = defaultTarget; } @@ -328,7 +336,7 @@ private static void checkUniqueName(List grpcKeyBuilders) { * is true, one of the specified names must be present for the keybuilder to match. */ @Immutable - static final class NameMatcher { + public static final class NameMatcher { private final String key; @@ -336,7 +344,8 @@ static final class NameMatcher { private final boolean optional; - NameMatcher(String key, List names, @Nullable Boolean optional) { + /** Constructor. */ + public NameMatcher(String key, List names, @Nullable Boolean optional) { this.key = checkNotNull(key, "key"); this.names = ImmutableList.copyOf(checkNotNull(names, "names")); this.optional = optional != null ? optional : true; @@ -389,7 +398,7 @@ public String toString() { } /** GrpcKeyBuilder is a configuration to construct headers consumed by route lookup service. */ - static final class GrpcKeyBuilder { + public static final class GrpcKeyBuilder { private final ImmutableList names; @@ -397,6 +406,7 @@ static final class GrpcKeyBuilder { private final ExtraKeys extraKeys; private final ImmutableMap constantKeys; + /** Constructor. All args should be nonnull. Headers should head unique keys. */ public GrpcKeyBuilder( List names, List headers, ExtraKeys extraKeys, Map constantKeys) { @@ -476,17 +486,18 @@ public String toString() { * required and includes the proto package name. The method name may be omitted, in which case * any method on the given service is matched. */ - static final class Name { + public static final class Name { private final String service; private final String method; - Name(String service) { + public Name(String service) { this(service, "*"); } - Name(String service, String method) { + /** The primary constructor. */ + public Name(String service, String method) { checkState( !checkNotNull(service, "service").isEmpty(), "service must not be empty or null"); @@ -531,7 +542,7 @@ public String toString() { } @AutoValue - abstract static class ExtraKeys { + public abstract static class ExtraKeys { static final ExtraKeys DEFAULT = create(null, null, null); @Nullable abstract String host(); @@ -540,7 +551,7 @@ abstract static class ExtraKeys { @Nullable abstract String method(); - static ExtraKeys create( + public static ExtraKeys create( @Nullable String host, @Nullable String service, @Nullable String method) { return new AutoValue_RlsProtoData_ExtraKeys(host, service, method); } diff --git a/xds/build.gradle b/xds/build.gradle index fdee8fab203..a8dbde3e0a1 100644 --- a/xds/build.gradle +++ b/xds/build.gradle @@ -39,6 +39,7 @@ dependencies { libraries.autovalue_annotation, libraries.opencensus_proto, libraries.protobuf_util + implementation project(path: ':grpc-rls') def nettyDependency = implementation project(':grpc-netty') testImplementation project(':grpc-core').sourceSets.test.output diff --git a/xds/src/main/java/io/grpc/xds/ClientXdsClient.java b/xds/src/main/java/io/grpc/xds/ClientXdsClient.java index 8a43717f97e..2a4405f45fc 100644 --- a/xds/src/main/java/io/grpc/xds/ClientXdsClient.java +++ b/xds/src/main/java/io/grpc/xds/ClientXdsClient.java @@ -73,7 +73,6 @@ import io.grpc.xds.EnvoyServerProtoData.FilterChainMatch; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.Filter.ClientInterceptorBuilder; -import io.grpc.xds.Filter.ConfigOrError; import io.grpc.xds.Filter.FilterConfig; import io.grpc.xds.Filter.NamedFilterConfig; import io.grpc.xds.Filter.ServerInterceptorBuilder; diff --git a/xds/src/main/java/io/grpc/xds/ClusterSpecifierPlugin.java b/xds/src/main/java/io/grpc/xds/ClusterSpecifierPlugin.java new file mode 100644 index 00000000000..44de67b8295 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/ClusterSpecifierPlugin.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import com.google.protobuf.Message; + +/** + * Defines the parsing functionality of a ClusterSpecifierPlugin as defined in the Enovy proto + * api/envoy/config/route/v3/route.proto. + */ +interface ClusterSpecifierPlugin { + /** + * The proto message types supported by this plugin. A plugin will be registered by each of its + * supported message types. + */ + String[] typeUrls(); + + ConfigOrError parsePlugin(Message rawProtoMessage); + + /** Represents an opaque data structure holding configuration for a ClusterSpecifierPlugin. */ + interface PluginConfig { + String typeUrl(); + } +} diff --git a/xds/src/main/java/io/grpc/xds/ClusterSpecifierPluginRegistry.java b/xds/src/main/java/io/grpc/xds/ClusterSpecifierPluginRegistry.java new file mode 100644 index 00000000000..cf7b4cf523d --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/ClusterSpecifierPluginRegistry.java @@ -0,0 +1,54 @@ +/* + * Copyright 2021 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; + +final class ClusterSpecifierPluginRegistry { + private static ClusterSpecifierPluginRegistry instance; + + private final Map supportedPlugins = new HashMap<>(); + + private ClusterSpecifierPluginRegistry() {} + + static synchronized ClusterSpecifierPluginRegistry getDefaultRegistry() { + if (instance == null) { + instance = newRegistry().register(RouteLookupServiceClusterSpecifierPlugin.INSTANCE); + } + return instance; + } + + private static ClusterSpecifierPluginRegistry newRegistry() { + return new ClusterSpecifierPluginRegistry(); + } + + private ClusterSpecifierPluginRegistry register(ClusterSpecifierPlugin... plugins) { + for (ClusterSpecifierPlugin plugin : plugins) { + for (String typeUrl : plugin.typeUrls()) { + supportedPlugins.put(typeUrl, plugin); + } + } + return this; + } + + @Nullable + ClusterSpecifierPlugin get(String typeUrl) { + return supportedPlugins.get(typeUrl); + } +} diff --git a/xds/src/main/java/io/grpc/xds/ConfigOrError.java b/xds/src/main/java/io/grpc/xds/ConfigOrError.java new file mode 100644 index 00000000000..e1235b5b544 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/ConfigOrError.java @@ -0,0 +1,51 @@ +/* + * Copyright 2021 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import static com.google.common.base.Preconditions.checkNotNull; + +// TODO(zdapeng): Unify with ClientXdsClient.StructOrError, or just have parseFilterConfig() throw +// certain types of Exception. +final class ConfigOrError { + + /** + * Returns a {@link ConfigOrError} for the successfully converted data object. + */ + static ConfigOrError fromConfig(T config) { + return new ConfigOrError<>(config); + } + + /** + * Returns a {@link ConfigOrError} for the failure to convert the data object. + */ + static ConfigOrError fromError(String errorDetail) { + return new ConfigOrError<>(errorDetail); + } + + final String errorDetail; + final T config; + + private ConfigOrError(T config) { + this.config = checkNotNull(config, "config"); + this.errorDetail = null; + } + + private ConfigOrError(String errorDetail) { + this.config = null; + this.errorDetail = checkNotNull(errorDetail, "errorDetail"); + } +} diff --git a/xds/src/main/java/io/grpc/xds/Filter.java b/xds/src/main/java/io/grpc/xds/Filter.java index 3da71fd6a4c..4b2767687f3 100644 --- a/xds/src/main/java/io/grpc/xds/Filter.java +++ b/xds/src/main/java/io/grpc/xds/Filter.java @@ -16,8 +16,6 @@ package io.grpc.xds; -import static com.google.common.base.Preconditions.checkNotNull; - import com.google.common.base.MoreObjects; import com.google.protobuf.Message; import io.grpc.ClientInterceptor; @@ -72,37 +70,6 @@ ServerInterceptor buildServerInterceptor( FilterConfig config, @Nullable FilterConfig overrideConfig); } - // TODO(zdapeng): Unify with ClientXdsClient.StructOrError, or just have parseFilterConfig() throw - // certain types of Exception. - final class ConfigOrError { - /** - * Returns a {@link ConfigOrError} for the successfully converted data object. - */ - static ConfigOrError fromConfig(T config) { - return new ConfigOrError<>(config); - } - - /** - * Returns a {@link ConfigOrError} for the failure to convert the data object. - */ - static ConfigOrError fromError(String errorDetail) { - return new ConfigOrError<>(errorDetail); - } - - final String errorDetail; - final T config; - - private ConfigOrError(T config) { - this.config = checkNotNull(config, "config"); - this.errorDetail = null; - } - - private ConfigOrError(String errorDetail) { - this.config = null; - this.errorDetail = checkNotNull(errorDetail, "errorDetail"); - } - } - /** Filter config with instance name. */ final class NamedFilterConfig { // filter instance name diff --git a/xds/src/main/java/io/grpc/xds/RouteLookupServiceClusterSpecifierPlugin.java b/xds/src/main/java/io/grpc/xds/RouteLookupServiceClusterSpecifierPlugin.java new file mode 100644 index 00000000000..8bec286925f --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/RouteLookupServiceClusterSpecifierPlugin.java @@ -0,0 +1,137 @@ +/* + * Copyright 2021 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import com.google.auto.value.AutoValue; +import com.google.protobuf.Any; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import com.google.protobuf.util.Durations; +import io.grpc.lookup.v1.GrpcKeyBuilder; +import io.grpc.lookup.v1.GrpcKeyBuilder.ExtraKeys; +import io.grpc.lookup.v1.GrpcKeyBuilder.Name; +import io.grpc.lookup.v1.NameMatcher; +import io.grpc.lookup.v1.RouteLookupConfig; +import io.grpc.rls.RlsProtoData; +import java.util.ArrayList; +import java.util.List; + +/** The ClusterSpecifierPlugin for RouteLookup policy. */ +final class RouteLookupServiceClusterSpecifierPlugin implements ClusterSpecifierPlugin { + + static final RouteLookupServiceClusterSpecifierPlugin INSTANCE = + new RouteLookupServiceClusterSpecifierPlugin(); + + private static final String TYPE_URL = + "type.googleapis.com/grpc.lookup.v1.RouteLookupConfig"; + + private RouteLookupServiceClusterSpecifierPlugin() {} + + @Override + public String[] typeUrls() { + return new String[] { + TYPE_URL, + }; + } + + @Override + public ConfigOrError parsePlugin(Message rawProtoMessage) { + if (!(rawProtoMessage instanceof Any)) { + return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass()); + } + + Any anyMessage = (Any) rawProtoMessage; + RouteLookupConfig configProto; + try { + configProto = anyMessage.unpack(RouteLookupConfig.class); + } catch (InvalidProtocolBufferException e) { + return ConfigOrError.fromError("Invalid proto: " + e); + } + try { + List keyBuildersProto = configProto.getGrpcKeybuildersList(); + List keyBuilders = + new ArrayList<>(keyBuildersProto.size()); + for (GrpcKeyBuilder keyBuilderProto : keyBuildersProto) { + List namesProto = keyBuilderProto.getNamesList(); + List names = new ArrayList<>(namesProto.size()); + for (Name nameProto : namesProto) { + if (nameProto.getMethod().isEmpty()) { + names.add(new RlsProtoData.GrpcKeyBuilder.Name(nameProto.getService())); + } else { + names.add( + new RlsProtoData.GrpcKeyBuilder.Name( + nameProto.getService(), nameProto.getMethod())); + } + } + + List headersProto = keyBuilderProto.getHeadersList(); + List headers = new ArrayList<>(headersProto.size()); + for (NameMatcher headerProto : headersProto) { + headers.add( + new RlsProtoData.NameMatcher( + headerProto.getKey(), headerProto.getNamesList(), + headerProto.getRequiredMatch())); + } + + String host = null; + String service = null; + String method = null; + if (keyBuilderProto.hasExtraKeys()) { + ExtraKeys extraKeysProto = keyBuilderProto.getExtraKeys(); + host = extraKeysProto.getHost(); + service = extraKeysProto.getService(); + method = extraKeysProto.getMethod(); + } + RlsProtoData.ExtraKeys extraKeys = + RlsProtoData.ExtraKeys.create(host, service, method); + + RlsProtoData.GrpcKeyBuilder keyBuilder = + new RlsProtoData.GrpcKeyBuilder( + names, headers, extraKeys, keyBuilderProto.getConstantKeysMap()); + keyBuilders.add(keyBuilder); + } + RlsProtoData.RouteLookupConfig config = new RlsProtoData.RouteLookupConfig( + keyBuilders, + configProto.getLookupService(), + Durations.toMillis(configProto.getLookupServiceTimeout()), + configProto.hasMaxAge() ? Durations.toMillis(configProto.getMaxAge()) : null, + configProto.hasStaleAge() ? Durations.toMillis(configProto.getStaleAge()) : null, + configProto.getCacheSizeBytes(), + configProto.getValidTargetsList(), + configProto.getDefaultTarget()); + return ConfigOrError.fromConfig(RlsPluginConfig.create(config)); + } catch (RuntimeException e) { + return ConfigOrError.fromError( + "Error parsing RouteLookupConfig: \n" + configProto + "\n reason: " + e); + } + } + + @AutoValue + abstract static class RlsPluginConfig implements PluginConfig { + + abstract RlsProtoData.RouteLookupConfig config(); + + static RlsPluginConfig create(RlsProtoData.RouteLookupConfig config) { + return new AutoValue_RouteLookupServiceClusterSpecifierPlugin_RlsPluginConfig(config); + } + + @Override + public String typeUrl() { + return TYPE_URL; + } + } +} diff --git a/xds/src/test/java/io/grpc/xds/ClusterSpecifierPluginRegistryTest.java b/xds/src/test/java/io/grpc/xds/ClusterSpecifierPluginRegistryTest.java new file mode 100644 index 00000000000..7f04a2a7f39 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/ClusterSpecifierPluginRegistryTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2021 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link ClusterSpecifierPluginRegistry}. */ +@RunWith(JUnit4.class) +public class ClusterSpecifierPluginRegistryTest { + @Test + public void pluginsInGlobaalInstance() { + assertThat(ClusterSpecifierPluginRegistry.getDefaultRegistry() + .get("type.googleapis.com/grpc.lookup.v1.RouteLookupConfig")) + .isEqualTo(RouteLookupServiceClusterSpecifierPlugin.INSTANCE); + } +} diff --git a/xds/src/test/java/io/grpc/xds/RbacFilterTest.java b/xds/src/test/java/io/grpc/xds/RbacFilterTest.java index da42ec1a2f6..f86b36df5e8 100644 --- a/xds/src/test/java/io/grpc/xds/RbacFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/RbacFilterTest.java @@ -52,7 +52,6 @@ import io.grpc.ServerInterceptor; import io.grpc.Status; import io.grpc.testing.TestMethodDescriptors; -import io.grpc.xds.Filter.ConfigOrError; import io.grpc.xds.Filter.FilterConfig; import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine; import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AlwaysTrueMatcher; diff --git a/xds/src/test/java/io/grpc/xds/RouteLookupServiceClusterSpecifierPluginTest.java b/xds/src/test/java/io/grpc/xds/RouteLookupServiceClusterSpecifierPluginTest.java new file mode 100644 index 00000000000..1bfe4a26f24 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/RouteLookupServiceClusterSpecifierPluginTest.java @@ -0,0 +1,142 @@ +/* + * Copyright 2021 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.Any; +import com.google.protobuf.util.Durations; +import io.grpc.lookup.v1.GrpcKeyBuilder; +import io.grpc.lookup.v1.GrpcKeyBuilder.ExtraKeys; +import io.grpc.lookup.v1.GrpcKeyBuilder.Name; +import io.grpc.lookup.v1.NameMatcher; +import io.grpc.lookup.v1.RouteLookupConfig; +import io.grpc.rls.RlsProtoData; +import io.grpc.xds.RouteLookupServiceClusterSpecifierPlugin.RlsPluginConfig; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link RouteLookupServiceClusterSpecifierPlugin}. */ +@RunWith(JUnit4.class) +public class RouteLookupServiceClusterSpecifierPluginTest { + @Test + public void parseConfigWithAllFieldsGiven() { + RouteLookupConfig routeLookupConfig = RouteLookupConfig.newBuilder() + .addGrpcKeybuilders( + GrpcKeyBuilder.newBuilder() + .addNames(Name.newBuilder().setService("service1").setMethod("method1")) + .addNames(Name.newBuilder().setService("service2").setMethod("method2")) + .addHeaders( + NameMatcher.newBuilder().setKey("key1").addNames("v1").setRequiredMatch(true)) + .setExtraKeys( + ExtraKeys.newBuilder() + .setHost("host1").setService("service1").setMethod("method1")) + .putConstantKeys("key2", "value2")) + .setLookupService("rls-cbt.googleapis.com") + .setLookupServiceTimeout(Durations.fromMillis(1234)) + .setMaxAge(Durations.fromMillis(56789)) + .setStaleAge(Durations.fromMillis(1000)) + .setCacheSizeBytes(5000) + .addValidTargets("valid-target") + .setDefaultTarget("default-target") + .build(); + RlsPluginConfig config = + RouteLookupServiceClusterSpecifierPlugin.INSTANCE.parsePlugin(Any.pack(routeLookupConfig)) + .config; + assertThat(config.typeUrl()).isEqualTo("type.googleapis.com/grpc.lookup.v1.RouteLookupConfig"); + assertThat(config.config()).isEqualTo( + new RlsProtoData.RouteLookupConfig( + ImmutableList.of( + new RlsProtoData.GrpcKeyBuilder( + ImmutableList.of( + new RlsProtoData.GrpcKeyBuilder.Name("service1", "method1"), + new RlsProtoData.GrpcKeyBuilder.Name("service2", "method2")), + ImmutableList.of( + new RlsProtoData.NameMatcher("key1", ImmutableList.of("v1"), true)), + RlsProtoData.ExtraKeys.create("host1", "service1", "method1"), + ImmutableMap.of("key2", "value2") + )), + "rls-cbt.googleapis.com", + 1234, + 56789L, + 1000L, + 5000, + ImmutableList.of("valid-target"), + "default-target")); + } + + @Test + public void parseConfigWithOptionalFieldsUnspecified() { + RouteLookupConfig routeLookupConfig = RouteLookupConfig.newBuilder() + .addGrpcKeybuilders( + GrpcKeyBuilder.newBuilder() + .addNames(Name.newBuilder().setService("service1")) + .addNames(Name.newBuilder().setService("service2")) + .addHeaders( + NameMatcher.newBuilder().setKey("key1").addNames("v1").setRequiredMatch(true))) + .setLookupService("rls-cbt.googleapis.com") + .setLookupServiceTimeout(Durations.fromMillis(1234)) + .setCacheSizeBytes(5000) + .addValidTargets("valid-target") + .build(); + RlsPluginConfig config = + RouteLookupServiceClusterSpecifierPlugin.INSTANCE.parsePlugin(Any.pack(routeLookupConfig)) + .config; + assertThat(config.typeUrl()).isEqualTo("type.googleapis.com/grpc.lookup.v1.RouteLookupConfig"); + assertThat(config.config()).isEqualTo( + new RlsProtoData.RouteLookupConfig( + ImmutableList.of( + new RlsProtoData.GrpcKeyBuilder( + ImmutableList.of( + new RlsProtoData.GrpcKeyBuilder.Name("service1"), + new RlsProtoData.GrpcKeyBuilder.Name("service2")), + ImmutableList.of( + new RlsProtoData.NameMatcher("key1", ImmutableList.of("v1"), true)), + RlsProtoData.ExtraKeys.create(null, null, null), + ImmutableMap.of() + )), + "rls-cbt.googleapis.com", + 1234, + null, + null, + 5000, + ImmutableList.of("valid-target"), + null)); + } + + @Test + public void parseInvalidConfig() { + RouteLookupConfig routeLookupConfig = RouteLookupConfig.newBuilder() + .addGrpcKeybuilders( + GrpcKeyBuilder.newBuilder() + .addNames(Name.newBuilder().setService("service1")) + .addNames(Name.newBuilder().setService("service2")) + .addHeaders( + NameMatcher.newBuilder().setKey("key1").addNames("v1").setRequiredMatch(true))) + .setLookupService("rls-cbt.googleapis.com") + .setLookupServiceTimeout(Durations.fromMillis(1234)) + .setCacheSizeBytes(-5000) // negative + .addValidTargets("valid-target") + .build(); + ConfigOrError configOrError = + RouteLookupServiceClusterSpecifierPlugin.INSTANCE.parsePlugin(Any.pack(routeLookupConfig)); + assertThat(configOrError.errorDetail).contains("cacheSize must be positive"); + } +}