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

grpc / grpc-java / #19592

17 Dec 2024 12:03AM UTC coverage: 88.586% (+0.01%) from 88.573%
#19592

push

github

ejona86
xds: Move specialized APIs out of XdsResourceType

StructOrError is a more generic API, but we have StatusOr now so we
don't want new usages of StructOrError. Moving StructOrError out of
io.grpc.xds.client will make it easier to delete StructOrError once
we've migrated to StatusOr in the future.

TRANSPORT_SOCKET_NAME_TLS should also move, but it wasn't immediately
clear to me where it should go.

33474 of 37787 relevant lines covered (88.59%)

0.89 hits per line

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

88.46
/../xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java
1
/*
2
 * Copyright 2022 The gRPC Authors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package io.grpc.xds;
18

19
import static com.google.common.base.Preconditions.checkNotNull;
20

21
import com.github.udpa.udpa.type.v1.TypedStruct;
22
import com.google.common.annotations.VisibleForTesting;
23
import com.google.common.base.MoreObjects;
24
import com.google.common.base.Splitter;
25
import com.google.common.collect.ImmutableList;
26
import com.google.common.collect.ImmutableSet;
27
import com.google.common.primitives.UnsignedInteger;
28
import com.google.protobuf.Any;
29
import com.google.protobuf.Duration;
30
import com.google.protobuf.InvalidProtocolBufferException;
31
import com.google.protobuf.Message;
32
import com.google.protobuf.util.Durations;
33
import com.google.re2j.Pattern;
34
import com.google.re2j.PatternSyntaxException;
35
import io.envoyproxy.envoy.config.core.v3.TypedExtensionConfig;
36
import io.envoyproxy.envoy.config.route.v3.ClusterSpecifierPlugin;
37
import io.envoyproxy.envoy.config.route.v3.RetryPolicy.RetryBackOff;
38
import io.envoyproxy.envoy.config.route.v3.RouteConfiguration;
39
import io.envoyproxy.envoy.type.v3.FractionalPercent;
40
import io.grpc.Status;
41
import io.grpc.internal.GrpcUtil;
42
import io.grpc.xds.ClusterSpecifierPlugin.NamedPluginConfig;
43
import io.grpc.xds.ClusterSpecifierPlugin.PluginConfig;
44
import io.grpc.xds.Filter.FilterConfig;
45
import io.grpc.xds.VirtualHost.Route;
46
import io.grpc.xds.VirtualHost.Route.RouteAction;
47
import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight;
48
import io.grpc.xds.VirtualHost.Route.RouteAction.HashPolicy;
49
import io.grpc.xds.VirtualHost.Route.RouteAction.RetryPolicy;
50
import io.grpc.xds.VirtualHost.Route.RouteMatch;
51
import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher;
52
import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate;
53
import io.grpc.xds.client.XdsClient.ResourceUpdate;
54
import io.grpc.xds.client.XdsResourceType;
55
import io.grpc.xds.internal.MatcherParser;
56
import io.grpc.xds.internal.Matchers;
57
import io.grpc.xds.internal.Matchers.FractionMatcher;
58
import io.grpc.xds.internal.Matchers.HeaderMatcher;
59
import java.util.ArrayList;
60
import java.util.Collections;
61
import java.util.EnumSet;
62
import java.util.HashMap;
63
import java.util.List;
64
import java.util.Locale;
65
import java.util.Map;
66
import java.util.Objects;
67
import java.util.Set;
68
import javax.annotation.Nullable;
69

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

475
    switch (proto.getClusterSpecifierCase()) {
1✔
476
      case CLUSTER:
477
        return StructOrError.fromStruct(RouteAction.forCluster(
1✔
478
            proto.getCluster(), hashPolicies, timeoutNano, retryPolicy,
1✔
479
            GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE, false)
1✔
480
            && args.getServerInfo().isTrustedXdsServer() && proto.getAutoHostRewrite().getValue()));
1✔
481
      case CLUSTER_HEADER:
482
        return null;
1✔
483
      case WEIGHTED_CLUSTERS:
484
        List<io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight> clusterWeights
1✔
485
            = proto.getWeightedClusters().getClustersList();
1✔
486
        if (clusterWeights.isEmpty()) {
1✔
487
          return StructOrError.fromError("No cluster found in weighted cluster list");
×
488
        }
489
        List<ClusterWeight> weightedClusters = new ArrayList<>();
1✔
490
        long clusterWeightSum = 0;
1✔
491
        for (io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight clusterWeight
492
            : clusterWeights) {
1✔
493
          StructOrError<ClusterWeight> clusterWeightOrError =
1✔
494
              parseClusterWeight(clusterWeight, filterRegistry);
1✔
495
          if (clusterWeightOrError.getErrorDetail() != null) {
1✔
496
            return StructOrError.fromError("RouteAction contains invalid ClusterWeight: "
×
497
                + clusterWeightOrError.getErrorDetail());
×
498
          }
499
          clusterWeightSum += clusterWeight.getWeight().getValue();
1✔
500
          weightedClusters.add(clusterWeightOrError.getStruct());
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(), proto.getWeight().getValue(), overrideConfigs.getStruct()));
1✔
613
  }
614

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

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

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

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

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

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

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