xref: /aosp_15_r20/external/grpc-grpc-java/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java (revision e07d83d3ffcef9ecfc9f7f475418ec639ff0e5fe)
1 /*
2  * Copyright 2022 The gRPC Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package io.grpc.xds;
18 
19 import static com.google.common.base.Preconditions.checkNotNull;
20 
21 import com.github.udpa.udpa.type.v1.TypedStruct;
22 import com.google.common.annotations.VisibleForTesting;
23 import com.google.common.base.MoreObjects;
24 import com.google.common.base.Splitter;
25 import com.google.common.collect.ImmutableList;
26 import com.google.common.collect.ImmutableSet;
27 import com.google.common.primitives.UnsignedInteger;
28 import com.google.protobuf.Any;
29 import com.google.protobuf.Duration;
30 import com.google.protobuf.InvalidProtocolBufferException;
31 import com.google.protobuf.Message;
32 import com.google.protobuf.util.Durations;
33 import com.google.re2j.Pattern;
34 import com.google.re2j.PatternSyntaxException;
35 import io.envoyproxy.envoy.config.core.v3.TypedExtensionConfig;
36 import io.envoyproxy.envoy.config.route.v3.ClusterSpecifierPlugin;
37 import io.envoyproxy.envoy.config.route.v3.RetryPolicy.RetryBackOff;
38 import io.envoyproxy.envoy.config.route.v3.RouteConfiguration;
39 import io.envoyproxy.envoy.type.v3.FractionalPercent;
40 import io.grpc.Status;
41 import io.grpc.xds.ClusterSpecifierPlugin.NamedPluginConfig;
42 import io.grpc.xds.ClusterSpecifierPlugin.PluginConfig;
43 import io.grpc.xds.Filter.FilterConfig;
44 import io.grpc.xds.VirtualHost.Route;
45 import io.grpc.xds.VirtualHost.Route.RouteAction;
46 import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight;
47 import io.grpc.xds.VirtualHost.Route.RouteAction.HashPolicy;
48 import io.grpc.xds.VirtualHost.Route.RouteAction.RetryPolicy;
49 import io.grpc.xds.VirtualHost.Route.RouteMatch;
50 import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher;
51 import io.grpc.xds.XdsClient.ResourceUpdate;
52 import io.grpc.xds.XdsClientImpl.ResourceInvalidException;
53 import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate;
54 import io.grpc.xds.internal.MatcherParser;
55 import io.grpc.xds.internal.Matchers;
56 import io.grpc.xds.internal.Matchers.FractionMatcher;
57 import io.grpc.xds.internal.Matchers.HeaderMatcher;
58 import java.util.ArrayList;
59 import java.util.Collections;
60 import java.util.EnumSet;
61 import java.util.HashMap;
62 import java.util.List;
63 import java.util.Locale;
64 import java.util.Map;
65 import java.util.Objects;
66 import java.util.Set;
67 import javax.annotation.Nullable;
68 
69 class XdsRouteConfigureResource extends XdsResourceType<RdsUpdate> {
70   static final String ADS_TYPE_URL_RDS =
71       "type.googleapis.com/envoy.config.route.v3.RouteConfiguration";
72   private static final String TYPE_URL_FILTER_CONFIG =
73       "type.googleapis.com/envoy.config.route.v3.FilterConfig";
74   // TODO(zdapeng): need to discuss how to handle unsupported values.
75   private static final Set<Status.Code> SUPPORTED_RETRYABLE_CODES =
76       Collections.unmodifiableSet(EnumSet.of(
77           Status.Code.CANCELLED, Status.Code.DEADLINE_EXCEEDED, Status.Code.INTERNAL,
78           Status.Code.RESOURCE_EXHAUSTED, Status.Code.UNAVAILABLE));
79 
80   private static final XdsRouteConfigureResource instance = new XdsRouteConfigureResource();
81 
getInstance()82   public static XdsRouteConfigureResource getInstance() {
83     return instance;
84   }
85 
86   @Override
87   @Nullable
extractResourceName(Message unpackedResource)88   String extractResourceName(Message unpackedResource) {
89     if (!(unpackedResource instanceof RouteConfiguration)) {
90       return null;
91     }
92     return ((RouteConfiguration) unpackedResource).getName();
93   }
94 
95   @Override
typeName()96   String typeName() {
97     return "RDS";
98   }
99 
100   @Override
typeUrl()101   String typeUrl() {
102     return ADS_TYPE_URL_RDS;
103   }
104 
105   @Override
isFullStateOfTheWorld()106   boolean isFullStateOfTheWorld() {
107     return false;
108   }
109 
110   @Override
unpackedClassName()111   Class<RouteConfiguration> unpackedClassName() {
112     return RouteConfiguration.class;
113   }
114 
115   @Override
doParse(XdsResourceType.Args args, Message unpackedMessage)116   RdsUpdate doParse(XdsResourceType.Args args, Message unpackedMessage)
117       throws ResourceInvalidException {
118     if (!(unpackedMessage instanceof RouteConfiguration)) {
119       throw new ResourceInvalidException("Invalid message type: " + unpackedMessage.getClass());
120     }
121     return processRouteConfiguration((RouteConfiguration) unpackedMessage,
122         args.filterRegistry);
123   }
124 
processRouteConfiguration( RouteConfiguration routeConfig, FilterRegistry filterRegistry)125   private static RdsUpdate processRouteConfiguration(
126       RouteConfiguration routeConfig, FilterRegistry filterRegistry)
127       throws ResourceInvalidException {
128     return new RdsUpdate(extractVirtualHosts(routeConfig, filterRegistry));
129   }
130 
extractVirtualHosts( RouteConfiguration routeConfig, FilterRegistry filterRegistry)131   static List<VirtualHost> extractVirtualHosts(
132       RouteConfiguration routeConfig, FilterRegistry filterRegistry)
133       throws ResourceInvalidException {
134     Map<String, PluginConfig> pluginConfigMap = new HashMap<>();
135     ImmutableSet.Builder<String> optionalPlugins = ImmutableSet.builder();
136 
137     if (enableRouteLookup) {
138       List<ClusterSpecifierPlugin> plugins = routeConfig.getClusterSpecifierPluginsList();
139       for (ClusterSpecifierPlugin plugin : plugins) {
140         String pluginName = plugin.getExtension().getName();
141         PluginConfig pluginConfig = parseClusterSpecifierPlugin(plugin);
142         if (pluginConfig != null) {
143           if (pluginConfigMap.put(pluginName, pluginConfig) != null) {
144             throw new ResourceInvalidException(
145                 "Multiple ClusterSpecifierPlugins with the same name: " + pluginName);
146           }
147         } else {
148           // The plugin parsed successfully, and it's not supported, but it's marked as optional.
149           optionalPlugins.add(pluginName);
150         }
151       }
152     }
153     List<VirtualHost> virtualHosts = new ArrayList<>(routeConfig.getVirtualHostsCount());
154     for (io.envoyproxy.envoy.config.route.v3.VirtualHost virtualHostProto
155         : routeConfig.getVirtualHostsList()) {
156       StructOrError<VirtualHost> virtualHost =
157           parseVirtualHost(virtualHostProto, filterRegistry, pluginConfigMap,
158               optionalPlugins.build());
159       if (virtualHost.getErrorDetail() != null) {
160         throw new ResourceInvalidException(
161             "RouteConfiguration contains invalid virtual host: " + virtualHost.getErrorDetail());
162       }
163       virtualHosts.add(virtualHost.getStruct());
164     }
165     return virtualHosts;
166   }
167 
parseVirtualHost( io.envoyproxy.envoy.config.route.v3.VirtualHost proto, FilterRegistry filterRegistry, Map<String, PluginConfig> pluginConfigMap, Set<String> optionalPlugins)168   private static StructOrError<VirtualHost> parseVirtualHost(
169       io.envoyproxy.envoy.config.route.v3.VirtualHost proto, FilterRegistry filterRegistry,
170        Map<String, PluginConfig> pluginConfigMap,
171       Set<String> optionalPlugins) {
172     String name = proto.getName();
173     List<Route> routes = new ArrayList<>(proto.getRoutesCount());
174     for (io.envoyproxy.envoy.config.route.v3.Route routeProto : proto.getRoutesList()) {
175       StructOrError<Route> route = parseRoute(
176           routeProto, filterRegistry, pluginConfigMap, optionalPlugins);
177       if (route == null) {
178         continue;
179       }
180       if (route.getErrorDetail() != null) {
181         return StructOrError.fromError(
182             "Virtual host [" + name + "] contains invalid route : " + route.getErrorDetail());
183       }
184       routes.add(route.getStruct());
185     }
186     StructOrError<Map<String, Filter.FilterConfig>> overrideConfigs =
187         parseOverrideFilterConfigs(proto.getTypedPerFilterConfigMap(), filterRegistry);
188     if (overrideConfigs.getErrorDetail() != null) {
189       return StructOrError.fromError(
190           "VirtualHost [" + proto.getName() + "] contains invalid HttpFilter config: "
191               + overrideConfigs.getErrorDetail());
192     }
193     return StructOrError.fromStruct(VirtualHost.create(
194         name, proto.getDomainsList(), routes, overrideConfigs.getStruct()));
195   }
196 
197   @VisibleForTesting
parseOverrideFilterConfigs( Map<String, Any> rawFilterConfigMap, FilterRegistry filterRegistry)198   static StructOrError<Map<String, FilterConfig>> parseOverrideFilterConfigs(
199       Map<String, Any> rawFilterConfigMap, FilterRegistry filterRegistry) {
200     Map<String, FilterConfig> overrideConfigs = new HashMap<>();
201     for (String name : rawFilterConfigMap.keySet()) {
202       Any anyConfig = rawFilterConfigMap.get(name);
203       String typeUrl = anyConfig.getTypeUrl();
204       boolean isOptional = false;
205       if (typeUrl.equals(TYPE_URL_FILTER_CONFIG)) {
206         io.envoyproxy.envoy.config.route.v3.FilterConfig filterConfig;
207         try {
208           filterConfig =
209               anyConfig.unpack(io.envoyproxy.envoy.config.route.v3.FilterConfig.class);
210         } catch (InvalidProtocolBufferException e) {
211           return StructOrError.fromError(
212               "FilterConfig [" + name + "] contains invalid proto: " + e);
213         }
214         isOptional = filterConfig.getIsOptional();
215         anyConfig = filterConfig.getConfig();
216         typeUrl = anyConfig.getTypeUrl();
217       }
218       Message rawConfig = anyConfig;
219       try {
220         if (typeUrl.equals(TYPE_URL_TYPED_STRUCT_UDPA)) {
221           TypedStruct typedStruct = anyConfig.unpack(TypedStruct.class);
222           typeUrl = typedStruct.getTypeUrl();
223           rawConfig = typedStruct.getValue();
224         } else if (typeUrl.equals(TYPE_URL_TYPED_STRUCT)) {
225           com.github.xds.type.v3.TypedStruct newTypedStruct =
226               anyConfig.unpack(com.github.xds.type.v3.TypedStruct.class);
227           typeUrl = newTypedStruct.getTypeUrl();
228           rawConfig = newTypedStruct.getValue();
229         }
230       } catch (InvalidProtocolBufferException e) {
231         return StructOrError.fromError(
232             "FilterConfig [" + name + "] contains invalid proto: " + e);
233       }
234       Filter filter = filterRegistry.get(typeUrl);
235       if (filter == null) {
236         if (isOptional) {
237           continue;
238         }
239         return StructOrError.fromError(
240             "HttpFilter [" + name + "](" + typeUrl + ") is required but unsupported");
241       }
242       ConfigOrError<? extends Filter.FilterConfig> filterConfig =
243           filter.parseFilterConfigOverride(rawConfig);
244       if (filterConfig.errorDetail != null) {
245         return StructOrError.fromError(
246             "Invalid filter config for HttpFilter [" + name + "]: " + filterConfig.errorDetail);
247       }
248       overrideConfigs.put(name, filterConfig.config);
249     }
250     return StructOrError.fromStruct(overrideConfigs);
251   }
252 
253   @VisibleForTesting
254   @Nullable
parseRoute( io.envoyproxy.envoy.config.route.v3.Route proto, FilterRegistry filterRegistry, Map<String, PluginConfig> pluginConfigMap, Set<String> optionalPlugins)255   static StructOrError<Route> parseRoute(
256       io.envoyproxy.envoy.config.route.v3.Route proto, FilterRegistry filterRegistry,
257       Map<String, PluginConfig> pluginConfigMap,
258       Set<String> optionalPlugins) {
259     StructOrError<RouteMatch> routeMatch = parseRouteMatch(proto.getMatch());
260     if (routeMatch == null) {
261       return null;
262     }
263     if (routeMatch.getErrorDetail() != null) {
264       return StructOrError.fromError(
265           "Route [" + proto.getName() + "] contains invalid RouteMatch: "
266               + routeMatch.getErrorDetail());
267     }
268 
269     StructOrError<Map<String, FilterConfig>> overrideConfigsOrError =
270         parseOverrideFilterConfigs(proto.getTypedPerFilterConfigMap(), filterRegistry);
271     if (overrideConfigsOrError.getErrorDetail() != null) {
272       return StructOrError.fromError(
273           "Route [" + proto.getName() + "] contains invalid HttpFilter config: "
274               + overrideConfigsOrError.getErrorDetail());
275     }
276     Map<String, FilterConfig> overrideConfigs = overrideConfigsOrError.getStruct();
277 
278     switch (proto.getActionCase()) {
279       case ROUTE:
280         StructOrError<RouteAction> routeAction =
281             parseRouteAction(proto.getRoute(), filterRegistry, pluginConfigMap,
282                 optionalPlugins);
283         if (routeAction == null) {
284           return null;
285         }
286         if (routeAction.getErrorDetail() != null) {
287           return StructOrError.fromError(
288               "Route [" + proto.getName() + "] contains invalid RouteAction: "
289                   + routeAction.getErrorDetail());
290         }
291         return StructOrError.fromStruct(
292             Route.forAction(routeMatch.getStruct(), routeAction.getStruct(), overrideConfigs));
293       case NON_FORWARDING_ACTION:
294         return StructOrError.fromStruct(
295             Route.forNonForwardingAction(routeMatch.getStruct(), overrideConfigs));
296       case REDIRECT:
297       case DIRECT_RESPONSE:
298       case FILTER_ACTION:
299       case ACTION_NOT_SET:
300       default:
301         return StructOrError.fromError(
302             "Route [" + proto.getName() + "] with unknown action type: " + proto.getActionCase());
303     }
304   }
305 
306   @VisibleForTesting
307   @Nullable
parseRouteMatch( io.envoyproxy.envoy.config.route.v3.RouteMatch proto)308   static StructOrError<RouteMatch> parseRouteMatch(
309       io.envoyproxy.envoy.config.route.v3.RouteMatch proto) {
310     if (proto.getQueryParametersCount() != 0) {
311       return null;
312     }
313     StructOrError<PathMatcher> pathMatch = parsePathMatcher(proto);
314     if (pathMatch.getErrorDetail() != null) {
315       return StructOrError.fromError(pathMatch.getErrorDetail());
316     }
317 
318     FractionMatcher fractionMatch = null;
319     if (proto.hasRuntimeFraction()) {
320       StructOrError<FractionMatcher> parsedFraction =
321           parseFractionMatcher(proto.getRuntimeFraction().getDefaultValue());
322       if (parsedFraction.getErrorDetail() != null) {
323         return StructOrError.fromError(parsedFraction.getErrorDetail());
324       }
325       fractionMatch = parsedFraction.getStruct();
326     }
327 
328     List<HeaderMatcher> headerMatchers = new ArrayList<>();
329     for (io.envoyproxy.envoy.config.route.v3.HeaderMatcher hmProto : proto.getHeadersList()) {
330       StructOrError<HeaderMatcher> headerMatcher = parseHeaderMatcher(hmProto);
331       if (headerMatcher.getErrorDetail() != null) {
332         return StructOrError.fromError(headerMatcher.getErrorDetail());
333       }
334       headerMatchers.add(headerMatcher.getStruct());
335     }
336 
337     return StructOrError.fromStruct(RouteMatch.create(
338         pathMatch.getStruct(), headerMatchers, fractionMatch));
339   }
340 
341   @VisibleForTesting
parsePathMatcher( io.envoyproxy.envoy.config.route.v3.RouteMatch proto)342   static StructOrError<PathMatcher> parsePathMatcher(
343       io.envoyproxy.envoy.config.route.v3.RouteMatch proto) {
344     boolean caseSensitive = proto.getCaseSensitive().getValue();
345     switch (proto.getPathSpecifierCase()) {
346       case PREFIX:
347         return StructOrError.fromStruct(
348             PathMatcher.fromPrefix(proto.getPrefix(), caseSensitive));
349       case PATH:
350         return StructOrError.fromStruct(PathMatcher.fromPath(proto.getPath(), caseSensitive));
351       case SAFE_REGEX:
352         String rawPattern = proto.getSafeRegex().getRegex();
353         Pattern safeRegEx;
354         try {
355           safeRegEx = Pattern.compile(rawPattern);
356         } catch (PatternSyntaxException e) {
357           return StructOrError.fromError("Malformed safe regex pattern: " + e.getMessage());
358         }
359         return StructOrError.fromStruct(PathMatcher.fromRegEx(safeRegEx));
360       case PATHSPECIFIER_NOT_SET:
361       default:
362         return StructOrError.fromError("Unknown path match type");
363     }
364   }
365 
parseFractionMatcher(FractionalPercent proto)366   private static StructOrError<FractionMatcher> parseFractionMatcher(FractionalPercent proto) {
367     int numerator = proto.getNumerator();
368     int denominator = 0;
369     switch (proto.getDenominator()) {
370       case HUNDRED:
371         denominator = 100;
372         break;
373       case TEN_THOUSAND:
374         denominator = 10_000;
375         break;
376       case MILLION:
377         denominator = 1_000_000;
378         break;
379       case UNRECOGNIZED:
380       default:
381         return StructOrError.fromError(
382             "Unrecognized fractional percent denominator: " + proto.getDenominator());
383     }
384     return StructOrError.fromStruct(FractionMatcher.create(numerator, denominator));
385   }
386 
387   @VisibleForTesting
parseHeaderMatcher( io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto)388   static StructOrError<HeaderMatcher> parseHeaderMatcher(
389       io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto) {
390     try {
391       Matchers.HeaderMatcher headerMatcher = MatcherParser.parseHeaderMatcher(proto);
392       return StructOrError.fromStruct(headerMatcher);
393     } catch (IllegalArgumentException e) {
394       return StructOrError.fromError(e.getMessage());
395     }
396   }
397 
398   /**
399    * Parses the RouteAction config. The returned result may contain a (parsed form)
400    * {@link RouteAction} or an error message. Returns {@code null} if the RouteAction
401    * should be ignored.
402    */
403   @VisibleForTesting
404   @Nullable
parseRouteAction( io.envoyproxy.envoy.config.route.v3.RouteAction proto, FilterRegistry filterRegistry, Map<String, PluginConfig> pluginConfigMap, Set<String> optionalPlugins)405   static StructOrError<RouteAction> parseRouteAction(
406       io.envoyproxy.envoy.config.route.v3.RouteAction proto, FilterRegistry filterRegistry,
407       Map<String, PluginConfig> pluginConfigMap,
408       Set<String> optionalPlugins) {
409     Long timeoutNano = null;
410     if (proto.hasMaxStreamDuration()) {
411       io.envoyproxy.envoy.config.route.v3.RouteAction.MaxStreamDuration maxStreamDuration
412           = proto.getMaxStreamDuration();
413       if (maxStreamDuration.hasGrpcTimeoutHeaderMax()) {
414         timeoutNano = Durations.toNanos(maxStreamDuration.getGrpcTimeoutHeaderMax());
415       } else if (maxStreamDuration.hasMaxStreamDuration()) {
416         timeoutNano = Durations.toNanos(maxStreamDuration.getMaxStreamDuration());
417       }
418     }
419     RetryPolicy retryPolicy = null;
420     if (proto.hasRetryPolicy()) {
421       StructOrError<RetryPolicy> retryPolicyOrError = parseRetryPolicy(proto.getRetryPolicy());
422       if (retryPolicyOrError != null) {
423         if (retryPolicyOrError.getErrorDetail() != null) {
424           return StructOrError.fromError(retryPolicyOrError.getErrorDetail());
425         }
426         retryPolicy = retryPolicyOrError.getStruct();
427       }
428     }
429     List<HashPolicy> hashPolicies = new ArrayList<>();
430     for (io.envoyproxy.envoy.config.route.v3.RouteAction.HashPolicy config
431         : proto.getHashPolicyList()) {
432       HashPolicy policy = null;
433       boolean terminal = config.getTerminal();
434       switch (config.getPolicySpecifierCase()) {
435         case HEADER:
436           io.envoyproxy.envoy.config.route.v3.RouteAction.HashPolicy.Header headerCfg =
437               config.getHeader();
438           Pattern regEx = null;
439           String regExSubstitute = null;
440           if (headerCfg.hasRegexRewrite() && headerCfg.getRegexRewrite().hasPattern()
441               && headerCfg.getRegexRewrite().getPattern().hasGoogleRe2()) {
442             regEx = Pattern.compile(headerCfg.getRegexRewrite().getPattern().getRegex());
443             regExSubstitute = headerCfg.getRegexRewrite().getSubstitution();
444           }
445           policy = HashPolicy.forHeader(
446               terminal, headerCfg.getHeaderName(), regEx, regExSubstitute);
447           break;
448         case FILTER_STATE:
449           if (config.getFilterState().getKey().equals(HASH_POLICY_FILTER_STATE_KEY)) {
450             policy = HashPolicy.forChannelId(terminal);
451           }
452           break;
453         default:
454           // Ignore
455       }
456       if (policy != null) {
457         hashPolicies.add(policy);
458       }
459     }
460 
461     switch (proto.getClusterSpecifierCase()) {
462       case CLUSTER:
463         return StructOrError.fromStruct(RouteAction.forCluster(
464             proto.getCluster(), hashPolicies, timeoutNano, retryPolicy));
465       case CLUSTER_HEADER:
466         return null;
467       case WEIGHTED_CLUSTERS:
468         List<io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight> clusterWeights
469             = proto.getWeightedClusters().getClustersList();
470         if (clusterWeights.isEmpty()) {
471           return StructOrError.fromError("No cluster found in weighted cluster list");
472         }
473         List<ClusterWeight> weightedClusters = new ArrayList<>();
474         long clusterWeightSum = 0;
475         for (io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight clusterWeight
476             : clusterWeights) {
477           StructOrError<ClusterWeight> clusterWeightOrError =
478               parseClusterWeight(clusterWeight, filterRegistry);
479           if (clusterWeightOrError.getErrorDetail() != null) {
480             return StructOrError.fromError("RouteAction contains invalid ClusterWeight: "
481                 + clusterWeightOrError.getErrorDetail());
482           }
483           clusterWeightSum += clusterWeight.getWeight().getValue();
484           weightedClusters.add(clusterWeightOrError.getStruct());
485         }
486         if (clusterWeightSum <= 0) {
487           return StructOrError.fromError("Sum of cluster weights should be above 0.");
488         }
489         if (clusterWeightSum > UnsignedInteger.MAX_VALUE.longValue()) {
490           return StructOrError.fromError(String.format(
491               "Sum of cluster weights should be less than the maximum unsigned integer (%d), but"
492                   + " was %d. ",
493               UnsignedInteger.MAX_VALUE.longValue(), clusterWeightSum));
494         }
495         return StructOrError.fromStruct(VirtualHost.Route.RouteAction.forWeightedClusters(
496             weightedClusters, hashPolicies, timeoutNano, retryPolicy));
497       case CLUSTER_SPECIFIER_PLUGIN:
498         if (enableRouteLookup) {
499           String pluginName = proto.getClusterSpecifierPlugin();
500           PluginConfig pluginConfig = pluginConfigMap.get(pluginName);
501           if (pluginConfig == null) {
502             // Skip route if the plugin is not registered, but it is optional.
503             if (optionalPlugins.contains(pluginName)) {
504               return null;
505             }
506             return StructOrError.fromError(
507                 "ClusterSpecifierPlugin for [" + pluginName + "] not found");
508           }
509           NamedPluginConfig namedPluginConfig = NamedPluginConfig.create(pluginName, pluginConfig);
510           return StructOrError.fromStruct(VirtualHost.Route.RouteAction.forClusterSpecifierPlugin(
511               namedPluginConfig, hashPolicies, timeoutNano, retryPolicy));
512         } else {
513           return null;
514         }
515       case CLUSTERSPECIFIER_NOT_SET:
516       default:
517         return null;
518     }
519   }
520 
521   @Nullable // Return null if we ignore the given policy.
parseRetryPolicy( io.envoyproxy.envoy.config.route.v3.RetryPolicy retryPolicyProto)522   private static StructOrError<VirtualHost.Route.RouteAction.RetryPolicy> parseRetryPolicy(
523       io.envoyproxy.envoy.config.route.v3.RetryPolicy retryPolicyProto) {
524     int maxAttempts = 2;
525     if (retryPolicyProto.hasNumRetries()) {
526       maxAttempts = retryPolicyProto.getNumRetries().getValue() + 1;
527     }
528     Duration initialBackoff = Durations.fromMillis(25);
529     Duration maxBackoff = Durations.fromMillis(250);
530     if (retryPolicyProto.hasRetryBackOff()) {
531       RetryBackOff retryBackOff = retryPolicyProto.getRetryBackOff();
532       if (!retryBackOff.hasBaseInterval()) {
533         return StructOrError.fromError("No base_interval specified in retry_backoff");
534       }
535       Duration originalInitialBackoff = initialBackoff = retryBackOff.getBaseInterval();
536       if (Durations.compare(initialBackoff, Durations.ZERO) <= 0) {
537         return StructOrError.fromError("base_interval in retry_backoff must be positive");
538       }
539       if (Durations.compare(initialBackoff, Durations.fromMillis(1)) < 0) {
540         initialBackoff = Durations.fromMillis(1);
541       }
542       if (retryBackOff.hasMaxInterval()) {
543         maxBackoff = retryPolicyProto.getRetryBackOff().getMaxInterval();
544         if (Durations.compare(maxBackoff, originalInitialBackoff) < 0) {
545           return StructOrError.fromError(
546               "max_interval in retry_backoff cannot be less than base_interval");
547         }
548         if (Durations.compare(maxBackoff, Durations.fromMillis(1)) < 0) {
549           maxBackoff = Durations.fromMillis(1);
550         }
551       } else {
552         maxBackoff = Durations.fromNanos(Durations.toNanos(initialBackoff) * 10);
553       }
554     }
555     Iterable<String> retryOns =
556         Splitter.on(',').omitEmptyStrings().trimResults().split(retryPolicyProto.getRetryOn());
557     ImmutableList.Builder<Status.Code> retryableStatusCodesBuilder = ImmutableList.builder();
558     for (String retryOn : retryOns) {
559       Status.Code code;
560       try {
561         code = Status.Code.valueOf(retryOn.toUpperCase(Locale.US).replace('-', '_'));
562       } catch (IllegalArgumentException e) {
563         // unsupported value, such as "5xx"
564         continue;
565       }
566       if (!SUPPORTED_RETRYABLE_CODES.contains(code)) {
567         // unsupported value
568         continue;
569       }
570       retryableStatusCodesBuilder.add(code);
571     }
572     List<Status.Code> retryableStatusCodes = retryableStatusCodesBuilder.build();
573     return StructOrError.fromStruct(
574         VirtualHost.Route.RouteAction.RetryPolicy.create(
575             maxAttempts, retryableStatusCodes, initialBackoff, maxBackoff,
576             /* perAttemptRecvTimeout= */ null));
577   }
578 
579   @VisibleForTesting
parseClusterWeight( io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight proto, FilterRegistry filterRegistry)580   static StructOrError<VirtualHost.Route.RouteAction.ClusterWeight> parseClusterWeight(
581       io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight proto,
582       FilterRegistry filterRegistry) {
583     StructOrError<Map<String, Filter.FilterConfig>> overrideConfigs =
584         parseOverrideFilterConfigs(proto.getTypedPerFilterConfigMap(), filterRegistry);
585     if (overrideConfigs.getErrorDetail() != null) {
586       return StructOrError.fromError(
587           "ClusterWeight [" + proto.getName() + "] contains invalid HttpFilter config: "
588               + overrideConfigs.getErrorDetail());
589     }
590     return StructOrError.fromStruct(VirtualHost.Route.RouteAction.ClusterWeight.create(
591         proto.getName(), proto.getWeight().getValue(), overrideConfigs.getStruct()));
592   }
593 
594   @Nullable // null if the plugin is not supported, but it's marked as optional.
parseClusterSpecifierPlugin(ClusterSpecifierPlugin pluginProto)595   private static PluginConfig parseClusterSpecifierPlugin(ClusterSpecifierPlugin pluginProto)
596       throws ResourceInvalidException {
597     return parseClusterSpecifierPlugin(
598         pluginProto, ClusterSpecifierPluginRegistry.getDefaultRegistry());
599   }
600 
601   @Nullable // null if the plugin is not supported, but it's marked as optional.
602   @VisibleForTesting
parseClusterSpecifierPlugin( ClusterSpecifierPlugin pluginProto, ClusterSpecifierPluginRegistry registry)603   static PluginConfig parseClusterSpecifierPlugin(
604       ClusterSpecifierPlugin pluginProto, ClusterSpecifierPluginRegistry registry)
605       throws ResourceInvalidException {
606     TypedExtensionConfig extension = pluginProto.getExtension();
607     String pluginName = extension.getName();
608     Any anyConfig = extension.getTypedConfig();
609     String typeUrl = anyConfig.getTypeUrl();
610     Message rawConfig = anyConfig;
611     if (typeUrl.equals(TYPE_URL_TYPED_STRUCT_UDPA) || typeUrl.equals(TYPE_URL_TYPED_STRUCT)) {
612       try {
613         TypedStruct typedStruct = unpackCompatibleType(
614             anyConfig, TypedStruct.class, TYPE_URL_TYPED_STRUCT_UDPA, TYPE_URL_TYPED_STRUCT);
615         typeUrl = typedStruct.getTypeUrl();
616         rawConfig = typedStruct.getValue();
617       } catch (InvalidProtocolBufferException e) {
618         throw new ResourceInvalidException(
619             "ClusterSpecifierPlugin [" + pluginName + "] contains invalid proto", e);
620       }
621     }
622     io.grpc.xds.ClusterSpecifierPlugin plugin = registry.get(typeUrl);
623     if (plugin == null) {
624       if (!pluginProto.getIsOptional()) {
625         throw new ResourceInvalidException("Unsupported ClusterSpecifierPlugin type: " + typeUrl);
626       }
627       return null;
628     }
629     ConfigOrError<? extends PluginConfig> pluginConfigOrError = plugin.parsePlugin(rawConfig);
630     if (pluginConfigOrError.errorDetail != null) {
631       throw new ResourceInvalidException(pluginConfigOrError.errorDetail);
632     }
633     return pluginConfigOrError.config;
634   }
635 
636   static final class RdsUpdate implements ResourceUpdate {
637     // The list virtual hosts that make up the route table.
638     final List<VirtualHost> virtualHosts;
639 
RdsUpdate(List<VirtualHost> virtualHosts)640     RdsUpdate(List<VirtualHost> virtualHosts) {
641       this.virtualHosts = Collections.unmodifiableList(
642           new ArrayList<>(checkNotNull(virtualHosts, "virtualHosts")));
643     }
644 
645     @Override
toString()646     public String toString() {
647       return MoreObjects.toStringHelper(this)
648           .add("virtualHosts", virtualHosts)
649           .toString();
650     }
651 
652     @Override
hashCode()653     public int hashCode() {
654       return Objects.hash(virtualHosts);
655     }
656 
657     @Override
equals(Object o)658     public boolean equals(Object o) {
659       if (this == o) {
660         return true;
661       }
662       if (o == null || getClass() != o.getClass()) {
663         return false;
664       }
665       RdsUpdate that = (RdsUpdate) o;
666       return Objects.equals(virtualHosts, that.virtualHosts);
667     }
668   }
669 }
670