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

grpc / grpc-java / #20073

13 Nov 2025 02:41PM UTC coverage: 88.522% (-0.03%) from 88.552%
#20073

push

github

ejona86
alts: Metadata server address modification to account for default port

Fixing the utilization of the GCE Metadata host server address
environment variable to account for the case where the user does not
specify a port (defaults to port 8080)

b/451639946

34969 of 39503 relevant lines covered (88.52%)

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 boolean isXdsAuthorityRewriteEnabled = GrpcUtil.getFlag(
1✔
73
      "GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE", true);
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
            isXdsAuthorityRewriteEnabled && args.getServerInfo().isTrustedXdsServer()
1✔
479
                && 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
            isXdsAuthorityRewriteEnabled && args.getServerInfo().isTrustedXdsServer()
1✔
514
                && 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
              isXdsAuthorityRewriteEnabled && args.getServerInfo().isTrustedXdsServer()
1✔
531
                  && proto.getAutoHostRewrite().getValue()));
1✔
532
        } else {
533
          return null;
1✔
534
        }
535
      case CLUSTERSPECIFIER_NOT_SET:
536
      default:
537
        return null;
1✔
538
    }
539
  }
540

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

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

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

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

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

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

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

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

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