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

grpc / grpc-java / #19535

30 Oct 2024 03:41PM UTC coverage: 84.572% (-0.005%) from 84.577%
#19535

push

github

web-flow
xds: Per-rpc rewriting of the authority header based on the selected route. (#11631)

Implementation of A81.

33970 of 40167 relevant lines covered (84.57%)

0.85 hits per line

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

87.57
/../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
  // TODO(zdapeng): need to discuss how to handle unsupported values.
82
  private static final Set<Status.Code> SUPPORTED_RETRYABLE_CODES =
1✔
83
      Collections.unmodifiableSet(EnumSet.of(
1✔
84
          Status.Code.CANCELLED, Status.Code.DEADLINE_EXCEEDED, Status.Code.INTERNAL,
85
          Status.Code.RESOURCE_EXHAUSTED, Status.Code.UNAVAILABLE));
86

87
  private static final XdsRouteConfigureResource instance = new XdsRouteConfigureResource();
1✔
88

89
  static XdsRouteConfigureResource getInstance() {
90
    return instance;
1✔
91
  }
92

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

613
  @Nullable // null if the plugin is not supported, but it's marked as optional.
614
  private static PluginConfig parseClusterSpecifierPlugin(ClusterSpecifierPlugin pluginProto)
615
      throws ResourceInvalidException {
616
    return parseClusterSpecifierPlugin(
1✔
617
        pluginProto, ClusterSpecifierPluginRegistry.getDefaultRegistry());
1✔
618
  }
619

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

655
  static final class RdsUpdate implements ResourceUpdate {
656
    // The list virtual hosts that make up the route table.
657
    final List<VirtualHost> virtualHosts;
658

659
    RdsUpdate(List<VirtualHost> virtualHosts) {
1✔
660
      this.virtualHosts = Collections.unmodifiableList(
1✔
661
          new ArrayList<>(checkNotNull(virtualHosts, "virtualHosts")));
1✔
662
    }
1✔
663

664
    @Override
665
    public String toString() {
666
      return MoreObjects.toStringHelper(this)
×
667
          .add("virtualHosts", virtualHosts)
×
668
          .toString();
×
669
    }
670

671
    @Override
672
    public int hashCode() {
673
      return Objects.hash(virtualHosts);
×
674
    }
675

676
    @Override
677
    public boolean equals(Object o) {
678
      if (this == o) {
1✔
679
        return true;
×
680
      }
681
      if (o == null || getClass() != o.getClass()) {
1✔
682
        return false;
×
683
      }
684
      RdsUpdate that = (RdsUpdate) o;
1✔
685
      return Objects.equals(virtualHosts, that.virtualHosts);
1✔
686
    }
687
  }
688
}
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

© 2026 Coveralls, Inc