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

grpc / grpc-java / #19778

11 Apr 2025 03:25PM UTC coverage: 88.572% (-0.01%) from 88.586%
#19778

push

github

web-flow
xds: Enable deprecation warnings

The security code referenced fields removed from gRFC A29 before it was
finalized.

Note that this fixes a bug in CommonTlsContextUtil where
CombinedValidationContext was not checked. I believe this was the only
location with such a bug as I audited all non-test usages of
has/getValidationContext() and confirmed they have have a corresponding
has/getCombinedValidationContext().

34707 of 39185 relevant lines covered (88.57%)

0.89 hits per line

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

88.82
/../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.Provider provider = filterRegistry.get(typeUrl);
1✔
249
      if (provider == 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
          provider.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
            regEx = Pattern.compile(headerCfg.getRegexRewrite().getPattern().getRegex());
1✔
456
            regExSubstitute = headerCfg.getRegexRewrite().getSubstitution();
1✔
457
          }
458
          policy = HashPolicy.forHeader(
1✔
459
              terminal, headerCfg.getHeaderName(), regEx, regExSubstitute);
1✔
460
          break;
1✔
461
        case FILTER_STATE:
462
          if (config.getFilterState().getKey().equals(HASH_POLICY_FILTER_STATE_KEY)) {
1✔
463
            policy = HashPolicy.forChannelId(terminal);
1✔
464
          }
465
          break;
466
        default:
467
          // Ignore
468
      }
469
      if (policy != null) {
1✔
470
        hashPolicies.add(policy);
1✔
471
      }
472
    }
1✔
473

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

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

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

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

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

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

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

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

675
    @Override
676
    public int hashCode() {
677
      return Objects.hash(virtualHosts);
1✔
678
    }
679

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