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

grpc / grpc-java / #20276

11 May 2026 08:24AM UTC coverage: 88.821% (-0.02%) from 88.838%
#20276

push

github

web-flow
Allow injecting bootstrap info into xDS Filter API for config parsing (#12724)

Extend the xDS Filter API to support injecting bootstrap information
into filters during configuration parsing. This allows filters to access
context information (e.g., allowed gRPC services) from the resource loading
layer during configuration validation and parsing.

- Update `Filter.Provider.parseFilterConfig` and
`parseFilterConfigOverride`
  to accept a `FilterContext` parameter.
- Introduce `BootstrapInfoGrpcServiceContextProvider` to encapsulate
  bootstrap info for context resolution.
- Update `XdsListenerResource` and `XdsRouteConfigureResource` to
  construct and pass `FilterContext` during configuration parsing.
- Update sub-filters (`FaultFilter`, `RbacFilter`,
`GcpAuthenticationFilter`,
  `RouterFilter`) to match the updated `FilterContext` signature.

Known Gaps & Limitations:
1. **MetricHolder**: Propagation of `MetricHolder` is not supported with
   this approach currently and is planned for support in a later phase.
2. **NameResolverRegistry**: Propagation is deferred for consistency.
While it could be passed from `XdsNameResolver` on the client side, there is
no equivalent mechanism on the server side. To ensure consistent
behavior, `DefaultRegistry` is used when validating schemes and creating channels.

36254 of 40817 relevant lines covered (88.82%)

0.89 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

336
    FractionMatcher fractionMatch = null;
1✔
337
    if (proto.hasRuntimeFraction()) {
1✔
338
      try {
339
        fractionMatch =
1✔
340
            MatcherParser.parseFractionMatcher(proto.getRuntimeFraction().getDefaultValue());
1✔
341
      } catch (IllegalArgumentException e) {
×
342
        return StructOrError.fromError(e.getMessage());
×
343
      }
1✔
344
    }
345

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

355
    return StructOrError.fromStruct(RouteMatch.create(
1✔
356
        pathMatch.getStruct(), headerMatchers, fractionMatch));
1✔
357
  }
358

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

384

385

386
  @VisibleForTesting
387
  static StructOrError<HeaderMatcher> parseHeaderMatcher(
388
      io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto) {
389
    try {
390
      Matchers.HeaderMatcher headerMatcher = MatcherParser.parseHeaderMatcher(proto);
1✔
391
      return StructOrError.fromStruct(headerMatcher);
1✔
392
    } catch (IllegalArgumentException e) {
1✔
393
      return StructOrError.fromError(e.getMessage());
1✔
394
    }
395
  }
396

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

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

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

584
  @VisibleForTesting
585
  static StructOrError<VirtualHost.Route.RouteAction.ClusterWeight> parseClusterWeight(
586
      io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight proto,
587
      FilterRegistry filterRegistry, XdsResourceType.Args args) {
588
    StructOrError<Map<String, Filter.FilterConfig>> overrideConfigs =
1✔
589
        parseOverrideFilterConfigs(proto.getTypedPerFilterConfigMap(), filterRegistry, args);
1✔
590
    if (overrideConfigs.getErrorDetail() != null) {
1✔
591
      return StructOrError.fromError(
×
592
          "ClusterWeight [" + proto.getName() + "] contains invalid HttpFilter config: "
×
593
              + overrideConfigs.getErrorDetail());
×
594
    }
595
    return StructOrError.fromStruct(VirtualHost.Route.RouteAction.ClusterWeight.create(
1✔
596
        proto.getName(),
1✔
597
        Integer.toUnsignedLong(proto.getWeight().getValue()),
1✔
598
        overrideConfigs.getStruct()));
1✔
599
  }
600

601
  @Nullable // null if the plugin is not supported, but it's marked as optional.
602
  private static PluginConfig parseClusterSpecifierPlugin(ClusterSpecifierPlugin pluginProto)
603
      throws ResourceInvalidException {
604
    return parseClusterSpecifierPlugin(
1✔
605
        pluginProto, ClusterSpecifierPluginRegistry.getDefaultRegistry());
1✔
606
  }
607

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

643
  static final class RdsUpdate implements ResourceUpdate {
644
    // The list virtual hosts that make up the route table.
645
    final List<VirtualHost> virtualHosts;
646

647
    RdsUpdate(List<VirtualHost> virtualHosts) {
1✔
648
      this.virtualHosts = Collections.unmodifiableList(
1✔
649
          new ArrayList<>(checkNotNull(virtualHosts, "virtualHosts")));
1✔
650
    }
1✔
651

652
    @Override
653
    public String toString() {
654
      return MoreObjects.toStringHelper(this)
1✔
655
          .add("virtualHosts", virtualHosts)
1✔
656
          .toString();
1✔
657
    }
658

659
    @Override
660
    public int hashCode() {
661
      return Objects.hash(virtualHosts);
1✔
662
    }
663

664
    @Override
665
    public boolean equals(Object o) {
666
      if (this == o) {
1✔
667
        return true;
×
668
      }
669
      if (o == null || getClass() != o.getClass()) {
1✔
670
        return false;
×
671
      }
672
      RdsUpdate that = (RdsUpdate) o;
1✔
673
      return Objects.equals(virtualHosts, that.virtualHosts);
1✔
674
    }
675
  }
676
}
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