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