From e638bbce667e74b69f767bb07e296bd1e07e54f6 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Wed, 11 May 2022 17:40:00 -0700 Subject: [PATCH] xds: ignore routes with unsupported optional cluster specifiers Support for the is_optional logic in Cluster Specifier Plugins: if unsupported Cluster Specifier Plugin is optional, don't NACK, and skip any routes that point to it. --- .../java/io/grpc/xds/ClientXdsClient.java | 46 ++-- .../io/grpc/xds/ClientXdsClientDataTest.java | 213 +++++++++++++++--- 2 files changed, 215 insertions(+), 44 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/ClientXdsClient.java b/xds/src/main/java/io/grpc/xds/ClientXdsClient.java index b0b60f2a1818..bdb06394d1e1 100644 --- a/xds/src/main/java/io/grpc/xds/ClientXdsClient.java +++ b/xds/src/main/java/io/grpc/xds/ClientXdsClient.java @@ -981,12 +981,13 @@ static StructOrError parseHttpFilter( private static StructOrError parseVirtualHost( io.envoyproxy.envoy.config.route.v3.VirtualHost proto, FilterRegistry filterRegistry, - boolean parseHttpFilter, Map pluginConfigMap) { + boolean parseHttpFilter, Map pluginConfigMap, + Set optionalPlugins) { String name = proto.getName(); List routes = new ArrayList<>(proto.getRoutesCount()); for (io.envoyproxy.envoy.config.route.v3.Route routeProto : proto.getRoutesList()) { StructOrError route = parseRoute( - routeProto, filterRegistry, parseHttpFilter, pluginConfigMap); + routeProto, filterRegistry, parseHttpFilter, pluginConfigMap, optionalPlugins); if (route == null) { continue; } @@ -1071,7 +1072,8 @@ static StructOrError> parseOverrideFilterConfigs( @Nullable static StructOrError parseRoute( io.envoyproxy.envoy.config.route.v3.Route proto, FilterRegistry filterRegistry, - boolean parseHttpFilter, Map pluginConfigMap) { + boolean parseHttpFilter, Map pluginConfigMap, + Set optionalPlugins) { StructOrError routeMatch = parseRouteMatch(proto.getMatch()); if (routeMatch == null) { return null; @@ -1097,7 +1099,8 @@ static StructOrError parseRoute( switch (proto.getActionCase()) { case ROUTE: StructOrError routeAction = - parseRouteAction(proto.getRoute(), filterRegistry, parseHttpFilter, pluginConfigMap); + parseRouteAction(proto.getRoute(), filterRegistry, parseHttpFilter, pluginConfigMap, + optionalPlugins); if (routeAction == null) { return null; } @@ -1250,7 +1253,8 @@ static StructOrError parseHeaderMatcher( @Nullable static StructOrError parseRouteAction( io.envoyproxy.envoy.config.route.v3.RouteAction proto, FilterRegistry filterRegistry, - boolean parseHttpFilter, Map pluginConfigMap) { + boolean parseHttpFilter, Map pluginConfigMap, + Set optionalPlugins) { Long timeoutNano = null; if (proto.hasMaxStreamDuration()) { io.envoyproxy.envoy.config.route.v3.RouteAction.MaxStreamDuration maxStreamDuration @@ -1334,6 +1338,10 @@ static StructOrError parseRouteAction( String pluginName = proto.getClusterSpecifierPlugin(); PluginConfig pluginConfig = pluginConfigMap.get(pluginName); if (pluginConfig == null) { + // Skip route if the plugin is not registered, but it's optional. + if (optionalPlugins.contains(pluginName)) { + return null; + } return StructOrError.fromError( "ClusterSpecifierPlugin for [" + pluginName + "] not found"); } @@ -1491,15 +1499,21 @@ private static List extractVirtualHosts( RouteConfiguration routeConfig, FilterRegistry filterRegistry, boolean parseHttpFilter) throws ResourceInvalidException { Map pluginConfigMap = new HashMap<>(); + ImmutableSet.Builder optionalPlugins = ImmutableSet.builder(); + if (enableRouteLookup) { List plugins = routeConfig.getClusterSpecifierPluginsList(); for (ClusterSpecifierPlugin plugin : plugins) { - PluginConfig existing = pluginConfigMap.put( - plugin.getExtension().getName(), parseClusterSpecifierPlugin(plugin)); - if (existing != null) { - throw new ResourceInvalidException( - "Multiple ClusterSpecifierPlugins with the same name: " - + plugin.getExtension().getName()); + String pluginName = plugin.getExtension().getName(); + PluginConfig pluginConfig = parseClusterSpecifierPlugin(plugin); + if (pluginConfig != null) { + if (pluginConfigMap.put(pluginName, pluginConfig) != null) { + throw new ResourceInvalidException( + "Multiple ClusterSpecifierPlugins with the same name: " + pluginName); + } + } else { + // The plugin parsed successfully, and it's not supported, but it's marked as optional. + optionalPlugins.add(pluginName); } } } @@ -1507,7 +1521,8 @@ private static List extractVirtualHosts( for (io.envoyproxy.envoy.config.route.v3.VirtualHost virtualHostProto : routeConfig.getVirtualHostsList()) { StructOrError virtualHost = - parseVirtualHost(virtualHostProto, filterRegistry, parseHttpFilter, pluginConfigMap); + parseVirtualHost(virtualHostProto, filterRegistry, parseHttpFilter, pluginConfigMap, + optionalPlugins.build()); if (virtualHost.getErrorDetail() != null) { throw new ResourceInvalidException( "RouteConfiguration contains invalid virtual host: " + virtualHost.getErrorDetail()); @@ -1517,12 +1532,14 @@ private static List extractVirtualHosts( return virtualHosts; } + @Nullable // null if the plugin is not supported, but it's marked as optional. private static PluginConfig parseClusterSpecifierPlugin(ClusterSpecifierPlugin pluginProto) throws ResourceInvalidException { return parseClusterSpecifierPlugin( pluginProto, ClusterSpecifierPluginRegistry.getDefaultRegistry()); } + @Nullable // null if the plugin is not supported, but it's marked as optional. @VisibleForTesting static PluginConfig parseClusterSpecifierPlugin( ClusterSpecifierPlugin pluginProto, ClusterSpecifierPluginRegistry registry) @@ -1545,7 +1562,10 @@ static PluginConfig parseClusterSpecifierPlugin( } io.grpc.xds.ClusterSpecifierPlugin plugin = registry.get(typeUrl); if (plugin == null) { - throw new ResourceInvalidException("Unsupported ClusterSpecifierPlugin type: " + typeUrl); + if (!pluginProto.getIsOptional()) { + throw new ResourceInvalidException("Unsupported ClusterSpecifierPlugin type: " + typeUrl); + } + return null; } ConfigOrError pluginConfigOrError = plugin.parsePlugin(rawConfig); if (pluginConfigOrError.errorDetail != null) { diff --git a/xds/src/test/java/io/grpc/xds/ClientXdsClientDataTest.java b/xds/src/test/java/io/grpc/xds/ClientXdsClientDataTest.java index 07da755f0572..713c3f0c8a72 100644 --- a/xds/src/test/java/io/grpc/xds/ClientXdsClientDataTest.java +++ b/xds/src/test/java/io/grpc/xds/ClientXdsClientDataTest.java @@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat; import static io.envoyproxy.envoy.config.route.v3.RouteAction.ClusterSpecifierCase.CLUSTER_SPECIFIER_PLUGIN; +import static org.junit.Assert.fail; import com.github.udpa.udpa.type.v1.TypedStruct; import com.google.common.collect.ImmutableMap; @@ -25,6 +26,7 @@ import com.google.common.collect.Iterables; import com.google.protobuf.Any; import com.google.protobuf.BoolValue; +import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; import com.google.protobuf.StringValue; @@ -197,7 +199,7 @@ public void parseRoute_withRouteAction() { .setCluster("cluster-foo")) .build(); StructOrError struct = ClientXdsClient.parseRoute( - proto, filterRegistry, false, ImmutableMap.of()); + proto, filterRegistry, false, ImmutableMap.of(), ImmutableSet.of()); assertThat(struct.getErrorDetail()).isNull(); assertThat(struct.getStruct()) .isEqualTo( @@ -220,7 +222,7 @@ public void parseRoute_withNonForwardingAction() { .setNonForwardingAction(NonForwardingAction.getDefaultInstance()) .build(); StructOrError struct = ClientXdsClient.parseRoute( - proto, filterRegistry, false, ImmutableMap.of()); + proto, filterRegistry, false, ImmutableMap.of(), ImmutableSet.of()); assertThat(struct.getStruct()) .isEqualTo( Route.forNonForwardingAction( @@ -239,7 +241,7 @@ public void parseRoute_withUnsupportedActionTypes() { .setRedirect(RedirectAction.getDefaultInstance()) .build(); res = ClientXdsClient.parseRoute( - redirectRoute, filterRegistry, false, ImmutableMap.of()); + redirectRoute, filterRegistry, false, ImmutableMap.of(), ImmutableSet.of()); assertThat(res.getStruct()).isNull(); assertThat(res.getErrorDetail()) .isEqualTo("Route [route-blade] with unknown action type: REDIRECT"); @@ -251,7 +253,7 @@ public void parseRoute_withUnsupportedActionTypes() { .setDirectResponse(DirectResponseAction.getDefaultInstance()) .build(); res = ClientXdsClient.parseRoute( - directResponseRoute, filterRegistry, false, ImmutableMap.of()); + directResponseRoute, filterRegistry, false, ImmutableMap.of(), ImmutableSet.of()); assertThat(res.getStruct()).isNull(); assertThat(res.getErrorDetail()) .isEqualTo("Route [route-blade] with unknown action type: DIRECT_RESPONSE"); @@ -263,7 +265,7 @@ public void parseRoute_withUnsupportedActionTypes() { .setFilterAction(FilterAction.getDefaultInstance()) .build(); res = ClientXdsClient.parseRoute( - filterRoute, filterRegistry, false, ImmutableMap.of()); + filterRoute, filterRegistry, false, ImmutableMap.of(), ImmutableSet.of()); assertThat(res.getStruct()).isNull(); assertThat(res.getErrorDetail()) .isEqualTo("Route [route-blade] with unknown action type: FILTER_ACTION"); @@ -285,7 +287,7 @@ public void parseRoute_skipRouteWithUnsupportedMatcher() { .setCluster("cluster-foo")) .build(); assertThat(ClientXdsClient.parseRoute( - proto, filterRegistry, false, ImmutableMap.of())) + proto, filterRegistry, false, ImmutableMap.of(), ImmutableSet.of())) .isNull(); } @@ -302,7 +304,7 @@ public void parseRoute_skipRouteWithUnsupportedAction() { .setClusterHeader("cluster header")) // cluster_header action not supported .build(); assertThat(ClientXdsClient.parseRoute( - proto, filterRegistry, false, ImmutableMap.of())) + proto, filterRegistry, false, ImmutableMap.of(), ImmutableSet.of())) .isNull(); } @@ -490,7 +492,7 @@ public void parseRouteAction_withCluster() { .build(); StructOrError struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); assertThat(struct.getErrorDetail()).isNull(); assertThat(struct.getStruct().cluster()).isEqualTo("cluster-foo"); assertThat(struct.getStruct().weightedClusters()).isNull(); @@ -514,7 +516,7 @@ public void parseRouteAction_withWeightedCluster() { .build(); StructOrError struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); assertThat(struct.getErrorDetail()).isNull(); assertThat(struct.getStruct().cluster()).isNull(); assertThat(struct.getStruct().weightedClusters()).containsExactly( @@ -534,7 +536,7 @@ public void parseRouteAction_withTimeoutByGrpcTimeoutHeaderMax() { .build(); StructOrError struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); assertThat(struct.getStruct().timeoutNano()).isEqualTo(TimeUnit.SECONDS.toNanos(5L)); } @@ -549,7 +551,7 @@ public void parseRouteAction_withTimeoutByMaxStreamDuration() { .build(); StructOrError struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); assertThat(struct.getStruct().timeoutNano()).isEqualTo(TimeUnit.SECONDS.toNanos(5L)); } @@ -561,7 +563,7 @@ public void parseRouteAction_withTimeoutUnset() { .build(); StructOrError struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); assertThat(struct.getStruct().timeoutNano()).isNull(); } @@ -584,7 +586,7 @@ public void parseRouteAction_withRetryPolicy() { .build(); StructOrError struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); RouteAction.RetryPolicy retryPolicy = struct.getStruct().retryPolicy(); assertThat(retryPolicy.maxAttempts()).isEqualTo(4); assertThat(retryPolicy.initialBackoff()).isEqualTo(Durations.fromMillis(500)); @@ -608,7 +610,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder.build()) .build(); struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); assertThat(struct.getStruct().retryPolicy()).isNotNull(); assertThat(struct.getStruct().retryPolicy().retryableStatusCodes()).isEmpty(); @@ -621,7 +623,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); assertThat(struct.getErrorDetail()).isEqualTo("No base_interval specified in retry_backoff"); // max_interval unset @@ -631,7 +633,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); retryPolicy = struct.getStruct().retryPolicy(); assertThat(retryPolicy.maxBackoff()).isEqualTo(Durations.fromMillis(500 * 10)); @@ -642,7 +644,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); assertThat(struct.getErrorDetail()) .isEqualTo("base_interval in retry_backoff must be positive"); @@ -655,7 +657,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); assertThat(struct.getErrorDetail()) .isEqualTo("max_interval in retry_backoff cannot be less than base_interval"); @@ -668,7 +670,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); assertThat(struct.getErrorDetail()) .isEqualTo("max_interval in retry_backoff cannot be less than base_interval"); @@ -681,7 +683,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); assertThat(struct.getStruct().retryPolicy().initialBackoff()) .isEqualTo(Durations.fromMillis(1)); assertThat(struct.getStruct().retryPolicy().maxBackoff()) @@ -697,7 +699,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); retryPolicy = struct.getStruct().retryPolicy(); assertThat(retryPolicy.initialBackoff()).isEqualTo(Durations.fromMillis(25)); assertThat(retryPolicy.maxBackoff()).isEqualTo(Durations.fromMillis(250)); @@ -716,7 +718,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); assertThat(struct.getStruct().retryPolicy().retryableStatusCodes()) .containsExactly(Code.CANCELLED); @@ -734,7 +736,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); assertThat(struct.getStruct().retryPolicy().retryableStatusCodes()) .containsExactly(Code.CANCELLED); @@ -752,7 +754,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); assertThat(struct.getStruct().retryPolicy().retryableStatusCodes()) .containsExactly(Code.CANCELLED); } @@ -790,7 +792,7 @@ public void parseRouteAction_withHashPolicies() { .build(); StructOrError struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); List policies = struct.getStruct().hashPolicies(); assertThat(policies).hasSize(2); assertThat(policies.get(0).type()).isEqualTo(HashPolicy.Type.HEADER); @@ -804,26 +806,26 @@ public void parseRouteAction_withHashPolicies() { } @Test - public void parseRouteAction_clusterSpecifier_routeLookupDisabled() { - ClientXdsClient.enableRouteLookup = false; + public void parseRouteAction_custerSpecifierNotSet() { io.envoyproxy.envoy.config.route.v3.RouteAction proto = io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() - .setClusterSpecifierPlugin(CLUSTER_SPECIFIER_PLUGIN.name()) .build(); StructOrError struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); assertThat(struct).isNull(); } @Test - public void parseRouteAction_custerSpecifierNotSet() { + public void parseRouteAction_clusterSpecifier_routeLookupDisabled() { + ClientXdsClient.enableRouteLookup = false; io.envoyproxy.envoy.config.route.v3.RouteAction proto = io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setClusterSpecifierPlugin(CLUSTER_SPECIFIER_PLUGIN.name()) .build(); StructOrError struct = ClientXdsClient.parseRouteAction(proto, filterRegistry, false, - ImmutableMap.of()); + ImmutableMap.of(), ImmutableSet.of()); assertThat(struct).isNull(); } @@ -1586,6 +1588,84 @@ public void parseHttpConnectionManager_pluginNameNotFound() throws Exception { true /* does not matter */); } + + @Test + public void parseHttpConnectionManager_optionalPlugin() throws ResourceInvalidException { + ClientXdsClient.enableRouteLookup = true; + + // RLS Plugin, and a route to it. + 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(); + io.envoyproxy.envoy.config.route.v3.ClusterSpecifierPlugin rlsPlugin = + io.envoyproxy.envoy.config.route.v3.ClusterSpecifierPlugin.newBuilder() + .setExtension( + TypedExtensionConfig.newBuilder() + .setName("rls-plugin-1") + .setTypedConfig(Any.pack( + RouteLookupClusterSpecifier.newBuilder() + .setRouteLookupConfig(routeLookupConfig) + .build()))) + .build(); + io.envoyproxy.envoy.config.route.v3.Route rlsRoute = + io.envoyproxy.envoy.config.route.v3.Route.newBuilder() + .setName("rls-route-1") + .setMatch(io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder().setPrefix("")) + .setRoute(io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setClusterSpecifierPlugin("rls-plugin-1")) + .build(); + + // Unknown optional plugin, and a route to it. + io.envoyproxy.envoy.config.route.v3.ClusterSpecifierPlugin optionalPlugin = + io.envoyproxy.envoy.config.route.v3.ClusterSpecifierPlugin.newBuilder() + .setIsOptional(true) + .setExtension( + TypedExtensionConfig.newBuilder() + .setName("optional-plugin-1") + .setTypedConfig(Any.pack(StringValue.of("unregistered"))) + .build()) + .build(); + io.envoyproxy.envoy.config.route.v3.Route optionalRoute = + io.envoyproxy.envoy.config.route.v3.Route.newBuilder() + .setName("optional-route-1") + .setMatch(io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder().setPrefix("")) + .setRoute(io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setClusterSpecifierPlugin("optional-plugin-1")) + .build(); + + + // Build and parse the route. + RouteConfiguration routeConfig = RouteConfiguration.newBuilder() + .addClusterSpecifierPlugins(rlsPlugin) + .addClusterSpecifierPlugins(optionalPlugin) + .addVirtualHosts( + io.envoyproxy.envoy.config.route.v3.VirtualHost.newBuilder() + .setName("virtual-host-1") + .addRoutes(rlsRoute) + .addRoutes(optionalRoute)) + .build(); + io.grpc.xds.HttpConnectionManager parsedHcm = ClientXdsClient.parseHttpConnectionManager( + HttpConnectionManager.newBuilder().setRouteConfig(routeConfig).build(), + new HashSet<>(), filterRegistry, false /* parseHttpFilter */, true /* does not matter */); + + // Verify the routes. + VirtualHost virtualHost = Iterables.getOnlyElement(parsedHcm.virtualHosts()); + Route parsedRoute = Iterables.getOnlyElement(virtualHost.routes()); + NamedPluginConfig namedPluginConfig = + parsedRoute.routeAction().namedClusterSpecifierPluginConfig(); + assertThat(namedPluginConfig.name()).isEqualTo("rls-plugin-1"); + assertThat(namedPluginConfig.config()).isInstanceOf(RlsPluginConfig.class); + } + @Test public void parseHttpConnectionManager_validateRdsConfigSource() throws Exception { ClientXdsClient.enableRouteLookup = true; @@ -1721,6 +1801,77 @@ public void parseClusterSpecifierPlugin_unregisteredPlugin() throws Exception { ClientXdsClient.parseClusterSpecifierPlugin(pluginProto, registry); } + @Test + public void parseClusterSpecifierPlugin_unregisteredPlugin_optional() + throws ResourceInvalidException { + ClusterSpecifierPluginRegistry registry = ClusterSpecifierPluginRegistry.newRegistry(); + io.envoyproxy.envoy.config.route.v3.ClusterSpecifierPlugin pluginProto = + io.envoyproxy.envoy.config.route.v3.ClusterSpecifierPlugin.newBuilder() + .setExtension(TypedExtensionConfig.newBuilder() + .setTypedConfig(Any.pack(StringValue.of("unregistered")))) + .setIsOptional(true) + .build(); + + PluginConfig pluginConfig = ClientXdsClient.parseClusterSpecifierPlugin(pluginProto, registry); + assertThat(pluginConfig).isNull(); + } + + @Test + public void parseClusterSpecifierPlugin_brokenPlugin() { + ClusterSpecifierPluginRegistry registry = ClusterSpecifierPluginRegistry.newRegistry(); + + Any failingAny = Any.newBuilder() + .setTypeUrl("type.googleapis.com/xds.type.v3.TypedStruct") + .setValue(ByteString.copyFromUtf8("fail")) + .build(); + + TypedExtensionConfig brokenPlugin = TypedExtensionConfig.newBuilder() + .setName("bad-plugin-1") + .setTypedConfig(failingAny) + .build(); + + try { + ClientXdsClient.parseClusterSpecifierPlugin( + io.envoyproxy.envoy.config.route.v3.ClusterSpecifierPlugin.newBuilder() + .setExtension(brokenPlugin) + .build(), + registry); + fail("Expected ResourceInvalidException"); + } catch (ResourceInvalidException e) { + assertThat(e).hasMessageThat() + .startsWith("ClusterSpecifierPlugin [bad-plugin-1] contains invalid proto"); + } + } + + @Test + public void parseClusterSpecifierPlugin_brokenPlugin_optional() { + ClusterSpecifierPluginRegistry registry = ClusterSpecifierPluginRegistry.newRegistry(); + + Any failingAny = Any.newBuilder() + .setTypeUrl("type.googleapis.com/xds.type.v3.TypedStruct") + .setValue(ByteString.copyFromUtf8("fail")) + .build(); + + TypedExtensionConfig brokenPlugin = TypedExtensionConfig.newBuilder() + .setName("bad-plugin-1") + .setTypedConfig(failingAny) + .build(); + + // Despite being optional, still should fail. + try { + ClientXdsClient.parseClusterSpecifierPlugin( + io.envoyproxy.envoy.config.route.v3.ClusterSpecifierPlugin.newBuilder() + .setIsOptional(true) + .setExtension(brokenPlugin) + .build(), + registry); + fail("Expected ResourceInvalidException"); + } catch (ResourceInvalidException e) { + assertThat(e).hasMessageThat() + .startsWith("ClusterSpecifierPlugin [bad-plugin-1] contains invalid proto"); + } + } + @Test public void parseCluster_ringHashLbPolicy_defaultLbConfig() throws ResourceInvalidException { Cluster cluster = Cluster.newBuilder()