• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #19678

05 Feb 2025 06:37PM CUT coverage: 88.566% (-0.03%) from 88.592%
#19678

push

github

web-flow
xds: Improve XdsNR's selectConfig() variable handling

The variables from the do-while are no longer initialized to let the
compiler verify that the loop sets each. Unnecessary comparisons to null
are also removed and is more obvious as the variables are never set to
null. Added a minor optimization of computing the RPCs path once instead
of once for each route. The variable declarations were also sorted to
match their initialization order.

This does fix an unlikely bug where if the old code could successfully
matched a route but fail to retain the cluster, then when trying a
second time if the route was _not_ matched it would re-use the prior route
and thus infinite-loop failing to retain that same cluster.

It also adds a missing cast to unsigned long for a uint32 weight. The old
code would detect if the _sum_ was negative, but a weight using 32 bits
would have been negative and never selected.

33755 of 38113 relevant lines covered (88.57%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.56
/../xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java
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.internal.GrpcUtil;
42
import io.grpc.xds.ClusterSpecifierPlugin.NamedPluginConfig;
43
import io.grpc.xds.ClusterSpecifierPlugin.PluginConfig;
44
import io.grpc.xds.Filter.FilterConfig;
45
import io.grpc.xds.VirtualHost.Route;
46
import io.grpc.xds.VirtualHost.Route.RouteAction;
47
import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight;
48
import io.grpc.xds.VirtualHost.Route.RouteAction.HashPolicy;
49
import io.grpc.xds.VirtualHost.Route.RouteAction.RetryPolicy;
50
import io.grpc.xds.VirtualHost.Route.RouteMatch;
51
import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher;
52
import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate;
53
import io.grpc.xds.client.XdsClient.ResourceUpdate;
54
import io.grpc.xds.client.XdsResourceType;
55
import io.grpc.xds.internal.MatcherParser;
56
import io.grpc.xds.internal.Matchers;
57
import io.grpc.xds.internal.Matchers.FractionMatcher;
58
import io.grpc.xds.internal.Matchers.HeaderMatcher;
59
import java.util.ArrayList;
60
import java.util.Collections;
61
import java.util.EnumSet;
62
import java.util.HashMap;
63
import java.util.List;
64
import java.util.Locale;
65
import java.util.Map;
66
import java.util.Objects;
67
import java.util.Set;
68
import javax.annotation.Nullable;
69

70
class XdsRouteConfigureResource extends XdsResourceType<RdsUpdate> {
1✔
71

72
  private static final String GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE =
73
      "GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE";
74
  @VisibleForTesting
75
  static boolean enableRouteLookup = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_RLS_LB", true);
1✔
76

77
  static final String ADS_TYPE_URL_RDS =
78
      "type.googleapis.com/envoy.config.route.v3.RouteConfiguration";
79
  private static final String TYPE_URL_FILTER_CONFIG =
80
      "type.googleapis.com/envoy.config.route.v3.FilterConfig";
81
  @VisibleForTesting
82
  static final String HASH_POLICY_FILTER_STATE_KEY = "io.grpc.channel_id";
83
  // TODO(zdapeng): need to discuss how to handle unsupported values.
84
  private static final Set<Status.Code> SUPPORTED_RETRYABLE_CODES =
1✔
85
      Collections.unmodifiableSet(EnumSet.of(
1✔
86
          Status.Code.CANCELLED, Status.Code.DEADLINE_EXCEEDED, Status.Code.INTERNAL,
87
          Status.Code.RESOURCE_EXHAUSTED, Status.Code.UNAVAILABLE));
88

89
  private static final XdsRouteConfigureResource instance = new XdsRouteConfigureResource();
1✔
90

91
  static XdsRouteConfigureResource getInstance() {
92
    return instance;
1✔
93
  }
94

95
  @Override
96
  @Nullable
97
  protected String extractResourceName(Message unpackedResource) {
98
    if (!(unpackedResource instanceof RouteConfiguration)) {
1✔
99
      return null;
×
100
    }
101
    return ((RouteConfiguration) unpackedResource).getName();
1✔
102
  }
103

104
  @Override
105
  public String typeName() {
106
    return "RDS";
1✔
107
  }
108

109
  @Override
110
  public String typeUrl() {
111
    return ADS_TYPE_URL_RDS;
1✔
112
  }
113

114
  @Override
115
  public boolean shouldRetrieveResourceKeysForArgs() {
116
    return false;
1✔
117
  }
118

119
  @Override
120
  protected boolean isFullStateOfTheWorld() {
121
    return false;
1✔
122
  }
123

124
  @Override
125
  protected Class<RouteConfiguration> unpackedClassName() {
126
    return RouteConfiguration.class;
1✔
127
  }
128

129
  @Override
130
  protected RdsUpdate doParse(XdsResourceType.Args args, Message unpackedMessage)
131
      throws ResourceInvalidException {
132
    if (!(unpackedMessage instanceof RouteConfiguration)) {
1✔
133
      throw new ResourceInvalidException("Invalid message type: " + unpackedMessage.getClass());
×
134
    }
135
    return processRouteConfiguration(
1✔
136
        (RouteConfiguration) unpackedMessage, FilterRegistry.getDefaultRegistry(), args);
1✔
137
  }
138

139
  private static RdsUpdate processRouteConfiguration(
140
      RouteConfiguration routeConfig, FilterRegistry filterRegistry, XdsResourceType.Args args)
141
      throws ResourceInvalidException {
142
    return new RdsUpdate(extractVirtualHosts(routeConfig, filterRegistry, args));
1✔
143
  }
144

145
  static List<VirtualHost> extractVirtualHosts(
146
      RouteConfiguration routeConfig, FilterRegistry filterRegistry, XdsResourceType.Args args)
147
      throws ResourceInvalidException {
148
    Map<String, PluginConfig> pluginConfigMap = new HashMap<>();
1✔
149
    ImmutableSet.Builder<String> optionalPlugins = ImmutableSet.builder();
1✔
150

151
    if (enableRouteLookup) {
1✔
152
      List<ClusterSpecifierPlugin> plugins = routeConfig.getClusterSpecifierPluginsList();
1✔
153
      for (ClusterSpecifierPlugin plugin : plugins) {
1✔
154
        String pluginName = plugin.getExtension().getName();
1✔
155
        PluginConfig pluginConfig = parseClusterSpecifierPlugin(plugin);
1✔
156
        if (pluginConfig != null) {
1✔
157
          if (pluginConfigMap.put(pluginName, pluginConfig) != null) {
1✔
158
            throw new ResourceInvalidException(
1✔
159
                "Multiple ClusterSpecifierPlugins with the same name: " + pluginName);
160
          }
161
        } else {
162
          // The plugin parsed successfully, and it's not supported, but it's marked as optional.
163
          optionalPlugins.add(pluginName);
1✔
164
        }
165
      }
1✔
166
    }
167
    List<VirtualHost> virtualHosts = new ArrayList<>(routeConfig.getVirtualHostsCount());
1✔
168
    for (io.envoyproxy.envoy.config.route.v3.VirtualHost virtualHostProto
169
        : routeConfig.getVirtualHostsList()) {
1✔
170
      StructOrError<VirtualHost> virtualHost =
1✔
171
          parseVirtualHost(virtualHostProto, filterRegistry, pluginConfigMap,
1✔
172
              optionalPlugins.build(), args);
1✔
173
      if (virtualHost.getErrorDetail() != null) {
1✔
174
        throw new ResourceInvalidException(
1✔
175
            "RouteConfiguration contains invalid virtual host: " + virtualHost.getErrorDetail());
1✔
176
      }
177
      virtualHosts.add(virtualHost.getStruct());
1✔
178
    }
1✔
179
    return virtualHosts;
1✔
180
  }
181

182
  private static StructOrError<VirtualHost> parseVirtualHost(
183
      io.envoyproxy.envoy.config.route.v3.VirtualHost proto, FilterRegistry filterRegistry,
184
       Map<String, PluginConfig> pluginConfigMap,
185
      Set<String> optionalPlugins, XdsResourceType.Args args) {
186
    String name = proto.getName();
1✔
187
    List<Route> routes = new ArrayList<>(proto.getRoutesCount());
1✔
188
    for (io.envoyproxy.envoy.config.route.v3.Route routeProto : proto.getRoutesList()) {
1✔
189
      StructOrError<Route> route = parseRoute(
1✔
190
          routeProto, filterRegistry, pluginConfigMap, optionalPlugins, args);
191
      if (route == null) {
1✔
192
        continue;
1✔
193
      }
194
      if (route.getErrorDetail() != null) {
1✔
195
        return StructOrError.fromError(
1✔
196
            "Virtual host [" + name + "] contains invalid route : " + route.getErrorDetail());
1✔
197
      }
198
      routes.add(route.getStruct());
1✔
199
    }
1✔
200
    StructOrError<Map<String, Filter.FilterConfig>> overrideConfigs =
1✔
201
        parseOverrideFilterConfigs(proto.getTypedPerFilterConfigMap(), filterRegistry);
1✔
202
    if (overrideConfigs.getErrorDetail() != null) {
1✔
203
      return StructOrError.fromError(
×
204
          "VirtualHost [" + proto.getName() + "] contains invalid HttpFilter config: "
×
205
              + overrideConfigs.getErrorDetail());
×
206
    }
207
    return StructOrError.fromStruct(VirtualHost.create(
1✔
208
        name, proto.getDomainsList(), routes, overrideConfigs.getStruct()));
1✔
209
  }
210

211
  @VisibleForTesting
212
  static StructOrError<Map<String, FilterConfig>> parseOverrideFilterConfigs(
213
      Map<String, Any> rawFilterConfigMap, FilterRegistry filterRegistry) {
214
    Map<String, FilterConfig> overrideConfigs = new HashMap<>();
1✔
215
    for (String name : rawFilterConfigMap.keySet()) {
1✔
216
      Any anyConfig = rawFilterConfigMap.get(name);
1✔
217
      String typeUrl = anyConfig.getTypeUrl();
1✔
218
      boolean isOptional = false;
1✔
219
      if (typeUrl.equals(TYPE_URL_FILTER_CONFIG)) {
1✔
220
        io.envoyproxy.envoy.config.route.v3.FilterConfig filterConfig;
221
        try {
222
          filterConfig =
1✔
223
              anyConfig.unpack(io.envoyproxy.envoy.config.route.v3.FilterConfig.class);
1✔
224
        } catch (InvalidProtocolBufferException e) {
×
225
          return StructOrError.fromError(
×
226
              "FilterConfig [" + name + "] contains invalid proto: " + e);
227
        }
1✔
228
        isOptional = filterConfig.getIsOptional();
1✔
229
        anyConfig = filterConfig.getConfig();
1✔
230
        typeUrl = anyConfig.getTypeUrl();
1✔
231
      }
232
      Message rawConfig = anyConfig;
1✔
233
      try {
234
        if (typeUrl.equals(TYPE_URL_TYPED_STRUCT_UDPA)) {
1✔
235
          TypedStruct typedStruct = anyConfig.unpack(TypedStruct.class);
1✔
236
          typeUrl = typedStruct.getTypeUrl();
1✔
237
          rawConfig = typedStruct.getValue();
1✔
238
        } else if (typeUrl.equals(TYPE_URL_TYPED_STRUCT)) {
1✔
239
          com.github.xds.type.v3.TypedStruct newTypedStruct =
×
240
              anyConfig.unpack(com.github.xds.type.v3.TypedStruct.class);
×
241
          typeUrl = newTypedStruct.getTypeUrl();
×
242
          rawConfig = newTypedStruct.getValue();
×
243
        }
244
      } catch (InvalidProtocolBufferException e) {
×
245
        return StructOrError.fromError(
×
246
            "FilterConfig [" + name + "] contains invalid proto: " + e);
247
      }
1✔
248
      Filter filter = filterRegistry.get(typeUrl);
1✔
249
      if (filter == null) {
1✔
250
        if (isOptional) {
1✔
251
          continue;
1✔
252
        }
253
        return StructOrError.fromError(
1✔
254
            "HttpFilter [" + name + "](" + typeUrl + ") is required but unsupported");
255
      }
256
      ConfigOrError<? extends Filter.FilterConfig> filterConfig =
1✔
257
          filter.parseFilterConfigOverride(rawConfig);
1✔
258
      if (filterConfig.errorDetail != null) {
1✔
259
        return StructOrError.fromError(
×
260
            "Invalid filter config for HttpFilter [" + name + "]: " + filterConfig.errorDetail);
261
      }
262
      overrideConfigs.put(name, filterConfig.config);
1✔
263
    }
1✔
264
    return StructOrError.fromStruct(overrideConfigs);
1✔
265
  }
266

267
  @VisibleForTesting
268
  @Nullable
269
  static StructOrError<Route> parseRoute(
270
      io.envoyproxy.envoy.config.route.v3.Route proto, FilterRegistry filterRegistry,
271
      Map<String, PluginConfig> pluginConfigMap,
272
      Set<String> optionalPlugins, XdsResourceType.Args args) {
273
    StructOrError<RouteMatch> routeMatch = parseRouteMatch(proto.getMatch());
1✔
274
    if (routeMatch == null) {
1✔
275
      return null;
1✔
276
    }
277
    if (routeMatch.getErrorDetail() != null) {
1✔
278
      return StructOrError.fromError(
1✔
279
          "Route [" + proto.getName() + "] contains invalid RouteMatch: "
1✔
280
              + routeMatch.getErrorDetail());
1✔
281
    }
282

283
    StructOrError<Map<String, FilterConfig>> overrideConfigsOrError =
1✔
284
        parseOverrideFilterConfigs(proto.getTypedPerFilterConfigMap(), filterRegistry);
1✔
285
    if (overrideConfigsOrError.getErrorDetail() != null) {
1✔
286
      return StructOrError.fromError(
×
287
          "Route [" + proto.getName() + "] contains invalid HttpFilter config: "
×
288
              + overrideConfigsOrError.getErrorDetail());
×
289
    }
290
    Map<String, FilterConfig> overrideConfigs = overrideConfigsOrError.getStruct();
1✔
291

292
    switch (proto.getActionCase()) {
1✔
293
      case ROUTE:
294
        StructOrError<RouteAction> routeAction =
1✔
295
            parseRouteAction(proto.getRoute(), filterRegistry, pluginConfigMap,
1✔
296
                optionalPlugins, args);
297
        if (routeAction == null) {
1✔
298
          return null;
1✔
299
        }
300
        if (routeAction.getErrorDetail() != null) {
1✔
301
          return StructOrError.fromError(
1✔
302
              "Route [" + proto.getName() + "] contains invalid RouteAction: "
1✔
303
                  + routeAction.getErrorDetail());
1✔
304
        }
305
        return StructOrError.fromStruct(
1✔
306
            Route.forAction(routeMatch.getStruct(), routeAction.getStruct(), overrideConfigs));
1✔
307
      case NON_FORWARDING_ACTION:
308
        return StructOrError.fromStruct(
1✔
309
            Route.forNonForwardingAction(routeMatch.getStruct(), overrideConfigs));
1✔
310
      case REDIRECT:
311
      case DIRECT_RESPONSE:
312
      case FILTER_ACTION:
313
      case ACTION_NOT_SET:
314
      default:
315
        return StructOrError.fromError(
1✔
316
            "Route [" + proto.getName() + "] with unknown action type: " + proto.getActionCase());
1✔
317
    }
318
  }
319

320
  @VisibleForTesting
321
  @Nullable
322
  static StructOrError<RouteMatch> parseRouteMatch(
323
      io.envoyproxy.envoy.config.route.v3.RouteMatch proto) {
324
    if (proto.getQueryParametersCount() != 0) {
1✔
325
      return null;
1✔
326
    }
327
    StructOrError<PathMatcher> pathMatch = parsePathMatcher(proto);
1✔
328
    if (pathMatch.getErrorDetail() != null) {
1✔
329
      return StructOrError.fromError(pathMatch.getErrorDetail());
1✔
330
    }
331

332
    FractionMatcher fractionMatch = null;
1✔
333
    if (proto.hasRuntimeFraction()) {
1✔
334
      StructOrError<FractionMatcher> parsedFraction =
1✔
335
          parseFractionMatcher(proto.getRuntimeFraction().getDefaultValue());
1✔
336
      if (parsedFraction.getErrorDetail() != null) {
1✔
337
        return StructOrError.fromError(parsedFraction.getErrorDetail());
×
338
      }
339
      fractionMatch = parsedFraction.getStruct();
1✔
340
    }
341

342
    List<HeaderMatcher> headerMatchers = new ArrayList<>();
1✔
343
    for (io.envoyproxy.envoy.config.route.v3.HeaderMatcher hmProto : proto.getHeadersList()) {
1✔
344
      StructOrError<HeaderMatcher> headerMatcher = parseHeaderMatcher(hmProto);
1✔
345
      if (headerMatcher.getErrorDetail() != null) {
1✔
346
        return StructOrError.fromError(headerMatcher.getErrorDetail());
×
347
      }
348
      headerMatchers.add(headerMatcher.getStruct());
1✔
349
    }
1✔
350

351
    return StructOrError.fromStruct(RouteMatch.create(
1✔
352
        pathMatch.getStruct(), headerMatchers, fractionMatch));
1✔
353
  }
354

355
  @VisibleForTesting
356
  static StructOrError<PathMatcher> parsePathMatcher(
357
      io.envoyproxy.envoy.config.route.v3.RouteMatch proto) {
358
    boolean caseSensitive = proto.getCaseSensitive().getValue();
1✔
359
    switch (proto.getPathSpecifierCase()) {
1✔
360
      case PREFIX:
361
        return StructOrError.fromStruct(
1✔
362
            PathMatcher.fromPrefix(proto.getPrefix(), caseSensitive));
1✔
363
      case PATH:
364
        return StructOrError.fromStruct(PathMatcher.fromPath(proto.getPath(), caseSensitive));
1✔
365
      case SAFE_REGEX:
366
        String rawPattern = proto.getSafeRegex().getRegex();
1✔
367
        Pattern safeRegEx;
368
        try {
369
          safeRegEx = Pattern.compile(rawPattern);
1✔
370
        } catch (PatternSyntaxException e) {
1✔
371
          return StructOrError.fromError("Malformed safe regex pattern: " + e.getMessage());
1✔
372
        }
1✔
373
        return StructOrError.fromStruct(PathMatcher.fromRegEx(safeRegEx));
1✔
374
      case PATHSPECIFIER_NOT_SET:
375
      default:
376
        return StructOrError.fromError("Unknown path match type");
×
377
    }
378
  }
379

380
  private static StructOrError<FractionMatcher> parseFractionMatcher(FractionalPercent proto) {
381
    int numerator = proto.getNumerator();
1✔
382
    int denominator = 0;
1✔
383
    switch (proto.getDenominator()) {
1✔
384
      case HUNDRED:
385
        denominator = 100;
1✔
386
        break;
1✔
387
      case TEN_THOUSAND:
388
        denominator = 10_000;
×
389
        break;
×
390
      case MILLION:
391
        denominator = 1_000_000;
×
392
        break;
×
393
      case UNRECOGNIZED:
394
      default:
395
        return StructOrError.fromError(
×
396
            "Unrecognized fractional percent denominator: " + proto.getDenominator());
×
397
    }
398
    return StructOrError.fromStruct(FractionMatcher.create(numerator, denominator));
1✔
399
  }
400

401
  @VisibleForTesting
402
  static StructOrError<HeaderMatcher> parseHeaderMatcher(
403
      io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto) {
404
    try {
405
      Matchers.HeaderMatcher headerMatcher = MatcherParser.parseHeaderMatcher(proto);
1✔
406
      return StructOrError.fromStruct(headerMatcher);
1✔
407
    } catch (IllegalArgumentException e) {
1✔
408
      return StructOrError.fromError(e.getMessage());
1✔
409
    }
410
  }
411

412
  /**
413
   * Parses the RouteAction config. The returned result may contain a (parsed form)
414
   * {@link RouteAction} or an error message. Returns {@code null} if the RouteAction
415
   * should be ignored.
416
   */
417
  @VisibleForTesting
418
  @Nullable
419
  static StructOrError<RouteAction> parseRouteAction(
420
      io.envoyproxy.envoy.config.route.v3.RouteAction proto, FilterRegistry filterRegistry,
421
      Map<String, PluginConfig> pluginConfigMap,
422
      Set<String> optionalPlugins, XdsResourceType.Args args) {
423
    Long timeoutNano = null;
1✔
424
    if (proto.hasMaxStreamDuration()) {
1✔
425
      io.envoyproxy.envoy.config.route.v3.RouteAction.MaxStreamDuration maxStreamDuration
1✔
426
          = proto.getMaxStreamDuration();
1✔
427
      if (maxStreamDuration.hasGrpcTimeoutHeaderMax()) {
1✔
428
        timeoutNano = Durations.toNanos(maxStreamDuration.getGrpcTimeoutHeaderMax());
1✔
429
      } else if (maxStreamDuration.hasMaxStreamDuration()) {
1✔
430
        timeoutNano = Durations.toNanos(maxStreamDuration.getMaxStreamDuration());
1✔
431
      }
432
    }
433
    RetryPolicy retryPolicy = null;
1✔
434
    if (proto.hasRetryPolicy()) {
1✔
435
      StructOrError<RetryPolicy> retryPolicyOrError = parseRetryPolicy(proto.getRetryPolicy());
1✔
436
      if (retryPolicyOrError != null) {
1✔
437
        if (retryPolicyOrError.getErrorDetail() != null) {
1✔
438
          return StructOrError.fromError(retryPolicyOrError.getErrorDetail());
1✔
439
        }
440
        retryPolicy = retryPolicyOrError.getStruct();
1✔
441
      }
442
    }
443
    List<HashPolicy> hashPolicies = new ArrayList<>();
1✔
444
    for (io.envoyproxy.envoy.config.route.v3.RouteAction.HashPolicy config
445
        : proto.getHashPolicyList()) {
1✔
446
      HashPolicy policy = null;
1✔
447
      boolean terminal = config.getTerminal();
1✔
448
      switch (config.getPolicySpecifierCase()) {
1✔
449
        case HEADER:
450
          io.envoyproxy.envoy.config.route.v3.RouteAction.HashPolicy.Header headerCfg =
1✔
451
              config.getHeader();
1✔
452
          Pattern regEx = null;
1✔
453
          String regExSubstitute = null;
1✔
454
          if (headerCfg.hasRegexRewrite() && headerCfg.getRegexRewrite().hasPattern()
1✔
455
              && headerCfg.getRegexRewrite().getPattern().hasGoogleRe2()) {
1✔
456
            regEx = Pattern.compile(headerCfg.getRegexRewrite().getPattern().getRegex());
1✔
457
            regExSubstitute = headerCfg.getRegexRewrite().getSubstitution();
1✔
458
          }
459
          policy = HashPolicy.forHeader(
1✔
460
              terminal, headerCfg.getHeaderName(), regEx, regExSubstitute);
1✔
461
          break;
1✔
462
        case FILTER_STATE:
463
          if (config.getFilterState().getKey().equals(HASH_POLICY_FILTER_STATE_KEY)) {
1✔
464
            policy = HashPolicy.forChannelId(terminal);
1✔
465
          }
466
          break;
467
        default:
468
          // Ignore
469
      }
470
      if (policy != null) {
1✔
471
        hashPolicies.add(policy);
1✔
472
      }
473
    }
1✔
474

475
    switch (proto.getClusterSpecifierCase()) {
1✔
476
      case CLUSTER:
477
        return StructOrError.fromStruct(RouteAction.forCluster(
1✔
478
            proto.getCluster(), hashPolicies, timeoutNano, retryPolicy,
1✔
479
            GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE, false)
1✔
480
            && args.getServerInfo().isTrustedXdsServer() && proto.getAutoHostRewrite().getValue()));
1✔
481
      case CLUSTER_HEADER:
482
        return null;
1✔
483
      case WEIGHTED_CLUSTERS:
484
        List<io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight> clusterWeights
1✔
485
            = proto.getWeightedClusters().getClustersList();
1✔
486
        if (clusterWeights.isEmpty()) {
1✔
487
          return StructOrError.fromError("No cluster found in weighted cluster list");
×
488
        }
489
        List<ClusterWeight> weightedClusters = new ArrayList<>();
1✔
490
        long clusterWeightSum = 0;
1✔
491
        for (io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight clusterWeight
492
            : clusterWeights) {
1✔
493
          StructOrError<ClusterWeight> clusterWeightOrError =
1✔
494
              parseClusterWeight(clusterWeight, filterRegistry);
1✔
495
          if (clusterWeightOrError.getErrorDetail() != null) {
1✔
496
            return StructOrError.fromError("RouteAction contains invalid ClusterWeight: "
×
497
                + clusterWeightOrError.getErrorDetail());
×
498
          }
499
          ClusterWeight parsedWeight = clusterWeightOrError.getStruct();
1✔
500
          clusterWeightSum += parsedWeight.weight();
1✔
501
          weightedClusters.add(parsedWeight);
1✔
502
        }
1✔
503
        if (clusterWeightSum <= 0) {
1✔
504
          return StructOrError.fromError("Sum of cluster weights should be above 0.");
1✔
505
        }
506
        if (clusterWeightSum > UnsignedInteger.MAX_VALUE.longValue()) {
1✔
507
          return StructOrError.fromError(String.format(
×
508
              "Sum of cluster weights should be less than the maximum unsigned integer (%d), but"
509
                  + " was %d. ",
510
              UnsignedInteger.MAX_VALUE.longValue(), clusterWeightSum));
×
511
        }
512
        return StructOrError.fromStruct(VirtualHost.Route.RouteAction.forWeightedClusters(
1✔
513
            weightedClusters, hashPolicies, timeoutNano, retryPolicy,
514
            GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE, false)
1✔
515
            && args.getServerInfo().isTrustedXdsServer() && proto.getAutoHostRewrite().getValue()));
1✔
516
      case CLUSTER_SPECIFIER_PLUGIN:
517
        if (enableRouteLookup) {
1✔
518
          String pluginName = proto.getClusterSpecifierPlugin();
1✔
519
          PluginConfig pluginConfig = pluginConfigMap.get(pluginName);
1✔
520
          if (pluginConfig == null) {
1✔
521
            // Skip route if the plugin is not registered, but it is optional.
522
            if (optionalPlugins.contains(pluginName)) {
1✔
523
              return null;
1✔
524
            }
525
            return StructOrError.fromError(
1✔
526
                "ClusterSpecifierPlugin for [" + pluginName + "] not found");
527
          }
528
          NamedPluginConfig namedPluginConfig = NamedPluginConfig.create(pluginName, pluginConfig);
1✔
529
          return StructOrError.fromStruct(VirtualHost.Route.RouteAction.forClusterSpecifierPlugin(
1✔
530
              namedPluginConfig, hashPolicies, timeoutNano, retryPolicy,
531
              GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE, false)
1✔
532
              && args.getServerInfo().isTrustedXdsServer()
1✔
533
                  && proto.getAutoHostRewrite().getValue()));
1✔
534
        } else {
535
          return null;
1✔
536
        }
537
      case CLUSTERSPECIFIER_NOT_SET:
538
      default:
539
        return null;
1✔
540
    }
541
  }
542

543
  @Nullable // Return null if we ignore the given policy.
544
  private static StructOrError<VirtualHost.Route.RouteAction.RetryPolicy> parseRetryPolicy(
545
      io.envoyproxy.envoy.config.route.v3.RetryPolicy retryPolicyProto) {
546
    int maxAttempts = 2;
1✔
547
    if (retryPolicyProto.hasNumRetries()) {
1✔
548
      maxAttempts = retryPolicyProto.getNumRetries().getValue() + 1;
1✔
549
    }
550
    Duration initialBackoff = Durations.fromMillis(25);
1✔
551
    Duration maxBackoff = Durations.fromMillis(250);
1✔
552
    if (retryPolicyProto.hasRetryBackOff()) {
1✔
553
      RetryBackOff retryBackOff = retryPolicyProto.getRetryBackOff();
1✔
554
      if (!retryBackOff.hasBaseInterval()) {
1✔
555
        return StructOrError.fromError("No base_interval specified in retry_backoff");
1✔
556
      }
557
      Duration originalInitialBackoff = initialBackoff = retryBackOff.getBaseInterval();
1✔
558
      if (Durations.compare(initialBackoff, Durations.ZERO) <= 0) {
1✔
559
        return StructOrError.fromError("base_interval in retry_backoff must be positive");
1✔
560
      }
561
      if (Durations.compare(initialBackoff, Durations.fromMillis(1)) < 0) {
1✔
562
        initialBackoff = Durations.fromMillis(1);
1✔
563
      }
564
      if (retryBackOff.hasMaxInterval()) {
1✔
565
        maxBackoff = retryPolicyProto.getRetryBackOff().getMaxInterval();
1✔
566
        if (Durations.compare(maxBackoff, originalInitialBackoff) < 0) {
1✔
567
          return StructOrError.fromError(
1✔
568
              "max_interval in retry_backoff cannot be less than base_interval");
569
        }
570
        if (Durations.compare(maxBackoff, Durations.fromMillis(1)) < 0) {
1✔
571
          maxBackoff = Durations.fromMillis(1);
1✔
572
        }
573
      } else {
574
        maxBackoff = Durations.fromNanos(Durations.toNanos(initialBackoff) * 10);
1✔
575
      }
576
    }
577
    Iterable<String> retryOns =
1✔
578
        Splitter.on(',').omitEmptyStrings().trimResults().split(retryPolicyProto.getRetryOn());
1✔
579
    ImmutableList.Builder<Status.Code> retryableStatusCodesBuilder = ImmutableList.builder();
1✔
580
    for (String retryOn : retryOns) {
1✔
581
      Status.Code code;
582
      try {
583
        code = Status.Code.valueOf(retryOn.toUpperCase(Locale.US).replace('-', '_'));
1✔
584
      } catch (IllegalArgumentException e) {
1✔
585
        // unsupported value, such as "5xx"
586
        continue;
1✔
587
      }
1✔
588
      if (!SUPPORTED_RETRYABLE_CODES.contains(code)) {
1✔
589
        // unsupported value
590
        continue;
×
591
      }
592
      retryableStatusCodesBuilder.add(code);
1✔
593
    }
1✔
594
    List<Status.Code> retryableStatusCodes = retryableStatusCodesBuilder.build();
1✔
595
    return StructOrError.fromStruct(
1✔
596
        VirtualHost.Route.RouteAction.RetryPolicy.create(
1✔
597
            maxAttempts, retryableStatusCodes, initialBackoff, maxBackoff,
598
            /* perAttemptRecvTimeout= */ null));
599
  }
600

601
  @VisibleForTesting
602
  static StructOrError<VirtualHost.Route.RouteAction.ClusterWeight> parseClusterWeight(
603
      io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight proto,
604
      FilterRegistry filterRegistry) {
605
    StructOrError<Map<String, Filter.FilterConfig>> overrideConfigs =
1✔
606
        parseOverrideFilterConfigs(proto.getTypedPerFilterConfigMap(), filterRegistry);
1✔
607
    if (overrideConfigs.getErrorDetail() != null) {
1✔
608
      return StructOrError.fromError(
×
609
          "ClusterWeight [" + proto.getName() + "] contains invalid HttpFilter config: "
×
610
              + overrideConfigs.getErrorDetail());
×
611
    }
612
    return StructOrError.fromStruct(VirtualHost.Route.RouteAction.ClusterWeight.create(
1✔
613
        proto.getName(),
1✔
614
        Integer.toUnsignedLong(proto.getWeight().getValue()),
1✔
615
        overrideConfigs.getStruct()));
1✔
616
  }
617

618
  @Nullable // null if the plugin is not supported, but it's marked as optional.
619
  private static PluginConfig parseClusterSpecifierPlugin(ClusterSpecifierPlugin pluginProto)
620
      throws ResourceInvalidException {
621
    return parseClusterSpecifierPlugin(
1✔
622
        pluginProto, ClusterSpecifierPluginRegistry.getDefaultRegistry());
1✔
623
  }
624

625
  @Nullable // null if the plugin is not supported, but it's marked as optional.
626
  @VisibleForTesting
627
  static PluginConfig parseClusterSpecifierPlugin(
628
      ClusterSpecifierPlugin pluginProto, ClusterSpecifierPluginRegistry registry)
629
      throws ResourceInvalidException {
630
    TypedExtensionConfig extension = pluginProto.getExtension();
1✔
631
    String pluginName = extension.getName();
1✔
632
    Any anyConfig = extension.getTypedConfig();
1✔
633
    String typeUrl = anyConfig.getTypeUrl();
1✔
634
    Message rawConfig = anyConfig;
1✔
635
    if (typeUrl.equals(TYPE_URL_TYPED_STRUCT_UDPA) || typeUrl.equals(TYPE_URL_TYPED_STRUCT)) {
1✔
636
      try {
637
        TypedStruct typedStruct = unpackCompatibleType(
1✔
638
            anyConfig, TypedStruct.class, TYPE_URL_TYPED_STRUCT_UDPA, TYPE_URL_TYPED_STRUCT);
639
        typeUrl = typedStruct.getTypeUrl();
1✔
640
        rawConfig = typedStruct.getValue();
1✔
641
      } catch (InvalidProtocolBufferException e) {
1✔
642
        throw new ResourceInvalidException(
1✔
643
            "ClusterSpecifierPlugin [" + pluginName + "] contains invalid proto", e);
644
      }
1✔
645
    }
646
    io.grpc.xds.ClusterSpecifierPlugin plugin = registry.get(typeUrl);
1✔
647
    if (plugin == null) {
1✔
648
      if (!pluginProto.getIsOptional()) {
1✔
649
        throw new ResourceInvalidException("Unsupported ClusterSpecifierPlugin type: " + typeUrl);
1✔
650
      }
651
      return null;
1✔
652
    }
653
    ConfigOrError<? extends PluginConfig> pluginConfigOrError = plugin.parsePlugin(rawConfig);
1✔
654
    if (pluginConfigOrError.errorDetail != null) {
1✔
655
      throw new ResourceInvalidException(pluginConfigOrError.errorDetail);
×
656
    }
657
    return pluginConfigOrError.config;
1✔
658
  }
659

660
  static final class RdsUpdate implements ResourceUpdate {
661
    // The list virtual hosts that make up the route table.
662
    final List<VirtualHost> virtualHosts;
663

664
    RdsUpdate(List<VirtualHost> virtualHosts) {
1✔
665
      this.virtualHosts = Collections.unmodifiableList(
1✔
666
          new ArrayList<>(checkNotNull(virtualHosts, "virtualHosts")));
1✔
667
    }
1✔
668

669
    @Override
670
    public String toString() {
671
      return MoreObjects.toStringHelper(this)
1✔
672
          .add("virtualHosts", virtualHosts)
1✔
673
          .toString();
1✔
674
    }
675

676
    @Override
677
    public int hashCode() {
678
      return Objects.hash(virtualHosts);
×
679
    }
680

681
    @Override
682
    public boolean equals(Object o) {
683
      if (this == o) {
1✔
684
        return true;
×
685
      }
686
      if (o == null || getClass() != o.getClass()) {
1✔
687
        return false;
×
688
      }
689
      RdsUpdate that = (RdsUpdate) o;
1✔
690
      return Objects.equals(virtualHosts, that.virtualHosts);
1✔
691
    }
692
  }
693
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc