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

grpc / grpc-java / #20297

01 Jun 2026 04:04AM UTC coverage: 88.886% (-0.002%) from 88.888%
#20297

push

github

web-flow
xds: Hold parsed service config in CdsUpdate

This avoids re-parsing the config within CdsLB, as the providers could
have changed and the config may no longer be valid.

Many usages of ServiceConfigUtil.unwrapLoadBalancingConfig() were
replaced with public API, which should be less brittle to internal
changes. Similarly, config.equals() was added for LBs least_request,
ring_hash, wrr to use more public APIs in testing. But I've gone out of
my way to avoid using equals for XdsClient change detection, by
preserving the original "JSON" config.

This fixes a bug in WRR config parsing which prevented it from parsing
errorUtilizationPenalty as it assumed it would be a Float, not a Double
like our parser actually generates and our API requires.
JsonUtil.getNumberAsFloat() was added specifically for WRR and has never
worked as JSON Numbers will always be Doubles.

In XdsClusterResource.CdsUpdate, the LB-specific fields like minRingSize
were already not used at all, so this commit deletes them as it seems a
relevant cleanup.

Fixes #12733

36493 of 41056 relevant lines covered (88.89%)

0.89 hits per line

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

91.69
/../xds/src/main/java/io/grpc/xds/XdsClusterResource.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
import static io.grpc.xds.client.Bootstrapper.ServerInfo;
21
import static io.grpc.xds.client.LoadStatsManager2.isEnabledOrcaLrsPropagation;
22

23
import com.google.auto.value.AutoValue;
24
import com.google.common.annotations.VisibleForTesting;
25
import com.google.common.base.MoreObjects;
26
import com.google.common.base.Strings;
27
import com.google.common.collect.ImmutableList;
28
import com.google.common.collect.ImmutableMap;
29
import com.google.protobuf.Duration;
30
import com.google.protobuf.InvalidProtocolBufferException;
31
import com.google.protobuf.Message;
32
import com.google.protobuf.Struct;
33
import com.google.protobuf.util.Durations;
34
import io.envoyproxy.envoy.config.cluster.v3.CircuitBreakers.Thresholds;
35
import io.envoyproxy.envoy.config.cluster.v3.Cluster;
36
import io.envoyproxy.envoy.config.core.v3.RoutingPriority;
37
import io.envoyproxy.envoy.config.core.v3.SocketAddress;
38
import io.envoyproxy.envoy.config.core.v3.TransportSocket;
39
import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment;
40
import io.envoyproxy.envoy.extensions.transport_sockets.http_11_proxy.v3.Http11ProxyUpstreamTransport;
41
import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext;
42
import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext;
43
import io.grpc.LoadBalancerRegistry;
44
import io.grpc.NameResolver;
45
import io.grpc.internal.GrpcUtil;
46
import io.grpc.util.GracefulSwitchLoadBalancer;
47
import io.grpc.xds.EnvoyServerProtoData.OutlierDetection;
48
import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext;
49
import io.grpc.xds.XdsClusterResource.CdsUpdate;
50
import io.grpc.xds.client.BackendMetricPropagation;
51
import io.grpc.xds.client.XdsClient.ResourceUpdate;
52
import io.grpc.xds.client.XdsResourceType;
53
import io.grpc.xds.internal.security.CommonTlsContextUtil;
54
import java.util.List;
55
import java.util.Locale;
56
import java.util.Set;
57
import javax.annotation.Nullable;
58

59
class XdsClusterResource extends XdsResourceType<CdsUpdate> {
1✔
60
  @VisibleForTesting
61
  static boolean enableLeastRequest =
62
      !Strings.isNullOrEmpty(System.getenv("GRPC_EXPERIMENTAL_ENABLE_LEAST_REQUEST"))
1✔
63
          ? Boolean.parseBoolean(System.getenv("GRPC_EXPERIMENTAL_ENABLE_LEAST_REQUEST"))
×
64
          : Boolean.parseBoolean(
1✔
65
              System.getProperty("io.grpc.xds.experimentalEnableLeastRequest", "true"));
1✔
66
  @VisibleForTesting
67
  public static boolean enableSystemRootCerts =
1✔
68
      GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_SYSTEM_ROOT_CERTS", true);
1✔
69
  static boolean isEnabledXdsHttpConnect =
1✔
70
      GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_HTTP_CONNECT", false);
1✔
71

72
  @VisibleForTesting
73
  static final String AGGREGATE_CLUSTER_TYPE_NAME = "envoy.clusters.aggregate";
74
  static final String ADS_TYPE_URL_CDS =
75
      "type.googleapis.com/envoy.config.cluster.v3.Cluster";
76
  private static final String TYPE_URL_CLUSTER_CONFIG =
77
      "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig";
78
  private static final String TYPE_URL_UPSTREAM_TLS_CONTEXT =
79
      "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext";
80
  private static final String TYPE_URL_UPSTREAM_TLS_CONTEXT_V2 =
81
      "type.googleapis.com/envoy.api.v2.auth.UpstreamTlsContext";
82
  private final LoadBalancerRegistry loadBalancerRegistry
1✔
83
      = LoadBalancerRegistry.getDefaultRegistry();
1✔
84

85
  private static final XdsClusterResource instance = new XdsClusterResource();
1✔
86

87
  public static XdsClusterResource getInstance() {
88
    return instance;
1✔
89
  }
90

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

100
  @Override
101
  public String typeName() {
102
    return "CDS";
1✔
103
  }
104

105
  @Override
106
  public String typeUrl() {
107
    return ADS_TYPE_URL_CDS;
1✔
108
  }
109

110
  @Override
111
  public boolean shouldRetrieveResourceKeysForArgs() {
112
    return true;
1✔
113
  }
114

115
  @Override
116
  protected boolean isFullStateOfTheWorld() {
117
    return true;
1✔
118
  }
119

120
  @Override
121
  @SuppressWarnings("unchecked")
122
  protected Class<Cluster> unpackedClassName() {
123
    return Cluster.class;
1✔
124
  }
125

126
  @Override
127
  protected CdsUpdate doParse(Args args, Message unpackedMessage) throws ResourceInvalidException {
128
    if (!(unpackedMessage instanceof Cluster)) {
1✔
129
      throw new ResourceInvalidException("Invalid message type: " + unpackedMessage.getClass());
×
130
    }
131
    Set<String> certProviderInstances = null;
1✔
132
    if (args.getBootstrapInfo() != null && args.getBootstrapInfo().certProviders() != null) {
1✔
133
      certProviderInstances = args.getBootstrapInfo().certProviders().keySet();
1✔
134
    }
135
    return processCluster((Cluster) unpackedMessage, certProviderInstances,
1✔
136
        args.getServerInfo(), loadBalancerRegistry);
1✔
137
  }
138

139
  @VisibleForTesting
140
  static CdsUpdate processCluster(Cluster cluster,
141
                                  Set<String> certProviderInstances,
142
                                  ServerInfo serverInfo,
143
                                  LoadBalancerRegistry loadBalancerRegistry)
144
      throws ResourceInvalidException {
145
    StructOrError<CdsUpdate.Builder> structOrError;
146
    switch (cluster.getClusterDiscoveryTypeCase()) {
1✔
147
      case TYPE:
148
        structOrError = parseNonAggregateCluster(cluster,
1✔
149
            certProviderInstances, serverInfo);
150
        break;
1✔
151
      case CLUSTER_TYPE:
152
        structOrError = parseAggregateCluster(cluster);
1✔
153
        break;
1✔
154
      case CLUSTERDISCOVERYTYPE_NOT_SET:
155
      default:
156
        throw new ResourceInvalidException(
1✔
157
            "Cluster " + cluster.getName() + ": unspecified cluster discovery type");
1✔
158
    }
159
    if (structOrError.getErrorDetail() != null) {
1✔
160
      throw new ResourceInvalidException(structOrError.getErrorDetail());
1✔
161
    }
162
    CdsUpdate.Builder updateBuilder = structOrError.getStruct();
1✔
163

164
    ImmutableMap<String, ?> lbPolicyConfig = LoadBalancerConfigFactory.newConfig(cluster,
1✔
165
        enableLeastRequest);
166

167
    NameResolver.ConfigOrError configOrError
1✔
168
        = GracefulSwitchLoadBalancer.parseLoadBalancingPolicyConfig(
1✔
169
            ImmutableList.of(lbPolicyConfig), loadBalancerRegistry);
1✔
170
    if (configOrError.getError() != null) {
1✔
171
      throw new ResourceInvalidException(
1✔
172
          "Failed to parse lb config for cluster '" + cluster.getName() + "': "
1✔
173
          + configOrError.getError());
1✔
174
    }
175

176
    updateBuilder.lbPolicyConfig(configOrError.getConfig(), lbPolicyConfig);
1✔
177
    updateBuilder.filterMetadata(
1✔
178
        ImmutableMap.copyOf(cluster.getMetadata().getFilterMetadataMap()));
1✔
179

180
    try {
181
      MetadataRegistry registry = MetadataRegistry.getInstance();
1✔
182
      ImmutableMap<String, Object> parsedFilterMetadata =
1✔
183
          registry.parseMetadata(cluster.getMetadata());
1✔
184
      updateBuilder.parsedMetadata(parsedFilterMetadata);
1✔
185
    } catch (ResourceInvalidException e) {
×
186
      throw new ResourceInvalidException(
×
187
          "Failed to parse xDS filter metadata for cluster '" + cluster.getName() + "': "
×
188
              + e.getMessage(), e);
×
189
    }
1✔
190

191
    return updateBuilder.build();
1✔
192
  }
193

194
  private static StructOrError<CdsUpdate.Builder> parseAggregateCluster(Cluster cluster) {
195
    String clusterName = cluster.getName();
1✔
196
    Cluster.CustomClusterType customType = cluster.getClusterType();
1✔
197
    String typeName = customType.getName();
1✔
198
    if (!typeName.equals(AGGREGATE_CLUSTER_TYPE_NAME)) {
1✔
199
      return StructOrError.fromError(
×
200
          "Cluster " + clusterName + ": unsupported custom cluster type: " + typeName);
201
    }
202
    io.envoyproxy.envoy.extensions.clusters.aggregate.v3.ClusterConfig clusterConfig;
203
    try {
204
      clusterConfig = unpackCompatibleType(customType.getTypedConfig(),
1✔
205
          io.envoyproxy.envoy.extensions.clusters.aggregate.v3.ClusterConfig.class,
206
          TYPE_URL_CLUSTER_CONFIG, null);
207
    } catch (InvalidProtocolBufferException e) {
×
208
      return StructOrError.fromError("Cluster " + clusterName + ": malformed ClusterConfig: " + e);
×
209
    }
1✔
210
    if (clusterConfig.getClustersList().isEmpty()) {
1✔
211
      return StructOrError.fromError("Cluster " + clusterName
1✔
212
          + ": aggregate ClusterConfig.clusters must not be empty");
213
    }
214
    return StructOrError.fromStruct(CdsUpdate.forAggregate(
1✔
215
        clusterName, clusterConfig.getClustersList()));
1✔
216
  }
217

218
  private static StructOrError<CdsUpdate.Builder> parseNonAggregateCluster(
219
      Cluster cluster, Set<String> certProviderInstances, ServerInfo serverInfo) {
220
    String clusterName = cluster.getName();
1✔
221
    ServerInfo lrsServerInfo = null;
1✔
222
    Long maxConcurrentRequests = null;
1✔
223
    UpstreamTlsContext upstreamTlsContext = null;
1✔
224
    OutlierDetection outlierDetection = null;
1✔
225
    boolean isHttp11ProxyAvailable = false;
1✔
226
    BackendMetricPropagation backendMetricPropagation = null;
1✔
227

228
    if (isEnabledOrcaLrsPropagation) {
1✔
229
      backendMetricPropagation = BackendMetricPropagation.fromMetricSpecs(
1✔
230
          cluster.getLrsReportEndpointMetricsList());
1✔
231
    }
232
    if (cluster.hasLrsServer()) {
1✔
233
      if (!cluster.getLrsServer().hasSelf()) {
1✔
234
        return StructOrError.fromError(
×
235
            "Cluster " + clusterName + ": only support LRS for the same management server");
236
      }
237
      lrsServerInfo = serverInfo;
1✔
238
    }
239
    if (cluster.hasCircuitBreakers()) {
1✔
240
      List<Thresholds> thresholds = cluster.getCircuitBreakers().getThresholdsList();
1✔
241
      for (Thresholds threshold : thresholds) {
1✔
242
        if (threshold.getPriority() != RoutingPriority.DEFAULT) {
1✔
243
          continue;
1✔
244
        }
245
        if (threshold.hasMaxRequests()) {
1✔
246
          maxConcurrentRequests = Integer.toUnsignedLong(threshold.getMaxRequests().getValue());
1✔
247
        }
248
      }
1✔
249
    }
250
    if (cluster.getTransportSocketMatchesCount() > 0) {
1✔
251
      return StructOrError.fromError("Cluster " + clusterName
1✔
252
          + ": transport-socket-matches not supported.");
253
    }
254
    boolean hasTransportSocket = cluster.hasTransportSocket();
1✔
255
    TransportSocket transportSocket = cluster.getTransportSocket();
1✔
256

257
    if (hasTransportSocket && !TRANSPORT_SOCKET_NAME_TLS.equals(transportSocket.getName())
1✔
258
        && !(isEnabledXdsHttpConnect && transportSocket.getTypedConfig().is(
1✔
259
        Http11ProxyUpstreamTransport.class))) {
260
      return StructOrError.fromError(
1✔
261
          "transport-socket with name " + transportSocket.getName() + " not supported.");
1✔
262
    }
263

264
    if (hasTransportSocket && isEnabledXdsHttpConnect && transportSocket.getTypedConfig().is(
1✔
265
        Http11ProxyUpstreamTransport.class)) {
266
      isHttp11ProxyAvailable = true;
1✔
267
      try {
268
        Http11ProxyUpstreamTransport wrappedTransportSocket = transportSocket
1✔
269
            .getTypedConfig().unpack(io.envoyproxy.envoy.extensions.transport_sockets
1✔
270
                .http_11_proxy.v3.Http11ProxyUpstreamTransport.class);
271
        hasTransportSocket = wrappedTransportSocket.hasTransportSocket();
1✔
272
        transportSocket = wrappedTransportSocket.getTransportSocket();
1✔
273
      } catch (InvalidProtocolBufferException e) {
×
274
        return StructOrError.fromError(
×
275
            "Cluster " + clusterName + ": malformed Http11ProxyUpstreamTransport: " + e);
276
      } catch (ClassCastException e) {
×
277
        return StructOrError.fromError(
×
278
            "Cluster " + clusterName
279
                + ": invalid transport_socket type in Http11ProxyUpstreamTransport");
280
      }
1✔
281
    }
282

283
    if (hasTransportSocket && TRANSPORT_SOCKET_NAME_TLS.equals(transportSocket.getName())) {
1✔
284
      try {
285
        upstreamTlsContext = UpstreamTlsContext.fromEnvoyProtoUpstreamTlsContext(
1✔
286
            validateUpstreamTlsContext(
1✔
287
                unpackCompatibleType(transportSocket.getTypedConfig(),
1✔
288
                    io.envoyproxy.envoy.extensions
289
                        .transport_sockets.tls.v3.UpstreamTlsContext.class,
290
                    TYPE_URL_UPSTREAM_TLS_CONTEXT, TYPE_URL_UPSTREAM_TLS_CONTEXT_V2),
291
                certProviderInstances));
292
      } catch (InvalidProtocolBufferException | ResourceInvalidException e) {
1✔
293
        return StructOrError.fromError(
1✔
294
            "Cluster " + clusterName + ": malformed UpstreamTlsContext: " + e);
295
      }
1✔
296
    }
297

298
    if (cluster.hasOutlierDetection()) {
1✔
299
      try {
300
        outlierDetection = OutlierDetection.fromEnvoyOutlierDetection(
1✔
301
            validateOutlierDetection(cluster.getOutlierDetection()));
1✔
302
      } catch (ResourceInvalidException e) {
1✔
303
        return StructOrError.fromError(
1✔
304
            "Cluster " + clusterName + ": malformed outlier_detection: " + e);
305
      }
1✔
306
    }
307

308
    Cluster.DiscoveryType type = cluster.getType();
1✔
309
    if (type == Cluster.DiscoveryType.EDS) {
1✔
310
      String edsServiceName = null;
1✔
311
      io.envoyproxy.envoy.config.cluster.v3.Cluster.EdsClusterConfig edsClusterConfig =
1✔
312
          cluster.getEdsClusterConfig();
1✔
313
      if (!edsClusterConfig.getEdsConfig().hasAds()
1✔
314
          && ! edsClusterConfig.getEdsConfig().hasSelf()) {
1✔
315
        return StructOrError.fromError(
1✔
316
            "Cluster " + clusterName + ": field eds_cluster_config must be set to indicate to use"
317
                + " EDS over ADS or self ConfigSource");
318
      }
319
      // If the service_name field is set, that value will be used for the EDS request.
320
      if (!edsClusterConfig.getServiceName().isEmpty()) {
1✔
321
        edsServiceName = edsClusterConfig.getServiceName();
1✔
322
      }
323
      // edsServiceName is required if the CDS resource has an xdstp name.
324
      if ((edsServiceName == null) && clusterName.toLowerCase(Locale.ROOT).startsWith("xdstp:")) {
1✔
325
        return StructOrError.fromError(
1✔
326
            "EDS service_name must be set when Cluster resource has an xdstp name");
327
      }
328

329
      return StructOrError.fromStruct(CdsUpdate.forEds(
1✔
330
          clusterName, edsServiceName, lrsServerInfo, maxConcurrentRequests, upstreamTlsContext,
331
          outlierDetection, isHttp11ProxyAvailable, backendMetricPropagation));
332
    } else if (type.equals(Cluster.DiscoveryType.LOGICAL_DNS)) {
1✔
333
      if (!cluster.hasLoadAssignment()) {
1✔
334
        return StructOrError.fromError(
×
335
            "Cluster " + clusterName + ": LOGICAL_DNS clusters must have a single host");
336
      }
337
      ClusterLoadAssignment assignment = cluster.getLoadAssignment();
1✔
338
      if (assignment.getEndpointsCount() != 1
1✔
339
          || assignment.getEndpoints(0).getLbEndpointsCount() != 1) {
1✔
340
        return StructOrError.fromError(
×
341
            "Cluster " + clusterName + ": LOGICAL_DNS clusters must have a single "
342
                + "locality_lb_endpoint and a single lb_endpoint");
343
      }
344
      io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint lbEndpoint =
1✔
345
          assignment.getEndpoints(0).getLbEndpoints(0);
1✔
346
      if (!lbEndpoint.hasEndpoint() || !lbEndpoint.getEndpoint().hasAddress()
1✔
347
          || !lbEndpoint.getEndpoint().getAddress().hasSocketAddress()) {
1✔
348
        return StructOrError.fromError(
×
349
            "Cluster " + clusterName
350
                + ": LOGICAL_DNS clusters must have an endpoint with address and socket_address");
351
      }
352
      SocketAddress socketAddress = lbEndpoint.getEndpoint().getAddress().getSocketAddress();
1✔
353
      if (!socketAddress.getResolverName().isEmpty()) {
1✔
354
        return StructOrError.fromError(
×
355
            "Cluster " + clusterName
356
                + ": LOGICAL DNS clusters must NOT have a custom resolver name set");
357
      }
358
      if (socketAddress.getPortSpecifierCase() != SocketAddress.PortSpecifierCase.PORT_VALUE) {
1✔
359
        return StructOrError.fromError(
×
360
            "Cluster " + clusterName
361
                + ": LOGICAL DNS clusters socket_address must have port_value");
362
      }
363
      String dnsHostName = String.format(
1✔
364
          Locale.US, "%s:%d", socketAddress.getAddress(), socketAddress.getPortValue());
1✔
365
      return StructOrError.fromStruct(CdsUpdate.forLogicalDns(
1✔
366
          clusterName, dnsHostName, lrsServerInfo, maxConcurrentRequests,
367
          upstreamTlsContext, isHttp11ProxyAvailable, backendMetricPropagation));
368
    }
369
    return StructOrError.fromError(
×
370
        "Cluster " + clusterName + ": unsupported built-in discovery type: " + type);
371
  }
372

373
  static io.envoyproxy.envoy.config.cluster.v3.OutlierDetection validateOutlierDetection(
374
      io.envoyproxy.envoy.config.cluster.v3.OutlierDetection outlierDetection)
375
      throws ResourceInvalidException {
376
    if (outlierDetection.hasInterval()) {
1✔
377
      if (!Durations.isValid(outlierDetection.getInterval())) {
1✔
378
        throw new ResourceInvalidException("outlier_detection interval is not a valid Duration");
1✔
379
      }
380
      if (hasNegativeValues(outlierDetection.getInterval())) {
1✔
381
        throw new ResourceInvalidException("outlier_detection interval has a negative value");
1✔
382
      }
383
    }
384
    if (outlierDetection.hasBaseEjectionTime()) {
1✔
385
      if (!Durations.isValid(outlierDetection.getBaseEjectionTime())) {
1✔
386
        throw new ResourceInvalidException(
1✔
387
            "outlier_detection base_ejection_time is not a valid Duration");
388
      }
389
      if (hasNegativeValues(outlierDetection.getBaseEjectionTime())) {
1✔
390
        throw new ResourceInvalidException(
1✔
391
            "outlier_detection base_ejection_time has a negative value");
392
      }
393
    }
394
    if (outlierDetection.hasMaxEjectionTime()) {
1✔
395
      if (!Durations.isValid(outlierDetection.getMaxEjectionTime())) {
1✔
396
        throw new ResourceInvalidException(
1✔
397
            "outlier_detection max_ejection_time is not a valid Duration");
398
      }
399
      if (hasNegativeValues(outlierDetection.getMaxEjectionTime())) {
1✔
400
        throw new ResourceInvalidException(
1✔
401
            "outlier_detection max_ejection_time has a negative value");
402
      }
403
    }
404
    if (outlierDetection.hasMaxEjectionPercent()
1✔
405
        && outlierDetection.getMaxEjectionPercent().getValue() > 100) {
1✔
406
      throw new ResourceInvalidException(
1✔
407
          "outlier_detection max_ejection_percent is > 100");
408
    }
409
    if (outlierDetection.hasEnforcingSuccessRate()
1✔
410
        && outlierDetection.getEnforcingSuccessRate().getValue() > 100) {
1✔
411
      throw new ResourceInvalidException(
1✔
412
          "outlier_detection enforcing_success_rate is > 100");
413
    }
414
    if (outlierDetection.hasFailurePercentageThreshold()
1✔
415
        && outlierDetection.getFailurePercentageThreshold().getValue() > 100) {
1✔
416
      throw new ResourceInvalidException(
1✔
417
          "outlier_detection failure_percentage_threshold is > 100");
418
    }
419
    if (outlierDetection.hasEnforcingFailurePercentage()
1✔
420
        && outlierDetection.getEnforcingFailurePercentage().getValue() > 100) {
1✔
421
      throw new ResourceInvalidException(
1✔
422
          "outlier_detection enforcing_failure_percentage is > 100");
423
    }
424

425
    return outlierDetection;
1✔
426
  }
427

428
  static boolean hasNegativeValues(Duration duration) {
429
    return duration.getSeconds() < 0 || duration.getNanos() < 0;
1✔
430
  }
431

432
  @VisibleForTesting
433
  static io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
434
      validateUpstreamTlsContext(
435
      io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext upstreamTlsContext,
436
      Set<String> certProviderInstances)
437
      throws ResourceInvalidException {
438
    if (upstreamTlsContext.hasCommonTlsContext()) {
1✔
439
      validateCommonTlsContext(upstreamTlsContext.getCommonTlsContext(), certProviderInstances,
1✔
440
          false);
441
    } else {
442
      throw new ResourceInvalidException("common-tls-context is required in upstream-tls-context");
1✔
443
    }
444
    return upstreamTlsContext;
1✔
445
  }
446

447
  @VisibleForTesting
448
  static void validateCommonTlsContext(
449
      CommonTlsContext commonTlsContext, Set<String> certProviderInstances, boolean server)
450
      throws ResourceInvalidException {
451
    if (commonTlsContext.hasCustomHandshaker()) {
1✔
452
      throw new ResourceInvalidException(
1✔
453
          "common-tls-context with custom_handshaker is not supported");
454
    }
455
    if (commonTlsContext.hasTlsParams()) {
1✔
456
      throw new ResourceInvalidException("common-tls-context with tls_params is not supported");
1✔
457
    }
458
    if (commonTlsContext.hasValidationContextSdsSecretConfig()) {
1✔
459
      throw new ResourceInvalidException(
1✔
460
          "common-tls-context with validation_context_sds_secret_config is not supported");
461
    }
462
    String certInstanceName = getIdentityCertInstanceName(commonTlsContext);
1✔
463
    if (certInstanceName == null) {
1✔
464
      if (server) {
1✔
465
        throw new ResourceInvalidException(
1✔
466
            "tls_certificate_provider_instance is required in downstream-tls-context");
467
      }
468
      if (commonTlsContext.getTlsCertificatesCount() > 0) {
1✔
469
        throw new ResourceInvalidException(
1✔
470
            "tls_certificate_provider_instance is unset");
471
      }
472
      if (commonTlsContext.getTlsCertificateSdsSecretConfigsCount() > 0) {
1✔
473
        throw new ResourceInvalidException(
1✔
474
            "tls_certificate_provider_instance is unset");
475
      }
476
    } else if (certProviderInstances == null || !certProviderInstances.contains(certInstanceName)) {
1✔
477
      throw new ResourceInvalidException(
1✔
478
          "CertificateProvider instance name '" + certInstanceName
479
              + "' not defined in the bootstrap file.");
480
    }
481
    String rootCaInstanceName = getRootCertInstanceName(commonTlsContext);
1✔
482
    if (rootCaInstanceName == null) {
1✔
483
      if (!server && (!enableSystemRootCerts
1✔
484
          || !CommonTlsContextUtil.isUsingSystemRootCerts(commonTlsContext))) {
1✔
485
        throw new ResourceInvalidException(
1✔
486
            "ca_certificate_provider_instance or system_root_certs is required in "
487
                + "upstream-tls-context");
488
      }
489
    } else {
490
      if (certProviderInstances == null || !certProviderInstances.contains(rootCaInstanceName)) {
1✔
491
        throw new ResourceInvalidException(
1✔
492
            "ca_certificate_provider_instance name '" + rootCaInstanceName
493
                + "' not defined in the bootstrap file.");
494
      }
495
      CertificateValidationContext certificateValidationContext = null;
1✔
496
      if (commonTlsContext.hasValidationContext()) {
1✔
497
        certificateValidationContext = commonTlsContext.getValidationContext();
1✔
498
      } else if (commonTlsContext.hasCombinedValidationContext() && commonTlsContext
1✔
499
          .getCombinedValidationContext().hasDefaultValidationContext()) {
1✔
500
        certificateValidationContext = commonTlsContext.getCombinedValidationContext()
1✔
501
            .getDefaultValidationContext();
1✔
502
      }
503
      if (certificateValidationContext != null) {
1✔
504
        @SuppressWarnings("deprecation") // gRFC A29 predates match_typed_subject_alt_names
505
        int matchSubjectAltNamesCount = certificateValidationContext.getMatchSubjectAltNamesCount();
1✔
506
        if (matchSubjectAltNamesCount > 0 && server) {
1✔
507
          throw new ResourceInvalidException(
1✔
508
              "match_subject_alt_names only allowed in upstream_tls_context");
509
        }
510
        if (certificateValidationContext.getVerifyCertificateSpkiCount() > 0) {
1✔
511
          throw new ResourceInvalidException(
1✔
512
              "verify_certificate_spki in default_validation_context is not supported");
513
        }
514
        if (certificateValidationContext.getVerifyCertificateHashCount() > 0) {
1✔
515
          throw new ResourceInvalidException(
1✔
516
              "verify_certificate_hash in default_validation_context is not supported");
517
        }
518
        if (certificateValidationContext.hasRequireSignedCertificateTimestamp()) {
1✔
519
          throw new ResourceInvalidException(
1✔
520
              "require_signed_certificate_timestamp in default_validation_context is not "
521
                  + "supported");
522
        }
523
        if (certificateValidationContext.hasCrl()) {
1✔
524
          throw new ResourceInvalidException("crl in default_validation_context is not supported");
1✔
525
        }
526
        if (certificateValidationContext.hasCustomValidatorConfig()) {
1✔
527
          throw new ResourceInvalidException(
1✔
528
              "custom_validator_config in default_validation_context is not supported");
529
        }
530
      }
531
    }
532
  }
1✔
533

534
  private static String getIdentityCertInstanceName(CommonTlsContext commonTlsContext) {
535
    if (commonTlsContext.hasTlsCertificateProviderInstance()) {
1✔
536
      return commonTlsContext.getTlsCertificateProviderInstance().getInstanceName();
1✔
537
    }
538
    // Fall back to deprecated field (field 11) for backward compatibility with Istio
539
    @SuppressWarnings("deprecation")
540
    String instanceName = commonTlsContext.hasTlsCertificateCertificateProviderInstance()
1✔
541
        ? commonTlsContext.getTlsCertificateCertificateProviderInstance().getInstanceName()
1✔
542
        : null;
1✔
543
    return instanceName;
1✔
544
  }
545

546
  private static String getRootCertInstanceName(CommonTlsContext commonTlsContext) {
547
    if (commonTlsContext.hasValidationContext()) {
1✔
548
      if (commonTlsContext.getValidationContext().hasCaCertificateProviderInstance()) {
1✔
549
        return commonTlsContext.getValidationContext().getCaCertificateProviderInstance()
1✔
550
            .getInstanceName();
1✔
551
      }
552
    } else if (commonTlsContext.hasCombinedValidationContext()) {
1✔
553
      CommonTlsContext.CombinedCertificateValidationContext combinedCertificateValidationContext
1✔
554
          = commonTlsContext.getCombinedValidationContext();
1✔
555
      if (combinedCertificateValidationContext.hasDefaultValidationContext()
1✔
556
          && combinedCertificateValidationContext.getDefaultValidationContext()
1✔
557
          .hasCaCertificateProviderInstance()) {
1✔
558
        return combinedCertificateValidationContext.getDefaultValidationContext()
1✔
559
            .getCaCertificateProviderInstance().getInstanceName();
1✔
560
      }
561
      // Fall back to deprecated field (field 4) in CombinedValidationContext
562
      @SuppressWarnings("deprecation")
563
      String instanceName = combinedCertificateValidationContext
564
          .hasValidationContextCertificateProviderInstance()
1✔
565
          ? combinedCertificateValidationContext.getValidationContextCertificateProviderInstance()
1✔
566
              .getInstanceName()
1✔
567
          : null;
1✔
568
      if (instanceName != null) {
1✔
569
        return instanceName;
1✔
570
      }
571
    }
572
    return null;
1✔
573
  }
574

575
  /** xDS resource update for cluster-level configuration. */
576
  @AutoValue
577
  abstract static class CdsUpdate implements ResourceUpdate {
1✔
578
    abstract String clusterName();
579

580
    abstract ClusterType clusterType();
581

582
    /** Graceful switch configuration. */
583
    Object lbPolicyConfig() {
584
      return internalLbPolicyConfig().getValue();
1✔
585
    }
586

587
    /**
588
     * Use {@link #lbPolicyConfig()} instead. This avoids using the LB policy configs' equals() when
589
     * XdsClient squelches config updates that are identical to the current value. LB policies are
590
     * not required to implement equals for their configs. Instead, {link
591
     * #internalLbPolicyConfigJson()} is used to detect changes.
592
     */
593
    abstract IgnoreEquals<Object> internalLbPolicyConfig();
594

595
    /** Use {@code lbPolicyConfig} instead. */
596
    @Nullable
597
    abstract ImmutableMap<String, ?> internalLbPolicyConfigJson();
598

599
    // Alternative resource name to be used in EDS requests.
600
    /// Only valid for EDS cluster.
601
    @Nullable
602
    abstract String edsServiceName();
603

604
    // Corresponding DNS name to be used if upstream endpoints of the cluster is resolvable
605
    // via DNS.
606
    // Only valid for LOGICAL_DNS cluster.
607
    @Nullable
608
    abstract String dnsHostName();
609

610
    // Load report server info for reporting loads via LRS.
611
    // Only valid for EDS or LOGICAL_DNS cluster.
612
    @Nullable
613
    abstract ServerInfo lrsServerInfo();
614

615
    // Max number of concurrent requests can be sent to this cluster.
616
    // Only valid for EDS or LOGICAL_DNS cluster.
617
    @Nullable
618
    abstract Long maxConcurrentRequests();
619

620
    // TLS context used to connect to connect to this cluster.
621
    // Only valid for EDS or LOGICAL_DNS cluster.
622
    @Nullable
623
    abstract UpstreamTlsContext upstreamTlsContext();
624

625
    abstract boolean isHttp11ProxyAvailable();
626

627
    // List of underlying clusters making of this aggregate cluster.
628
    // Only valid for AGGREGATE cluster.
629
    @Nullable
630
    abstract ImmutableList<String> prioritizedClusterNames();
631

632
    // Outlier detection configuration.
633
    @Nullable
634
    abstract OutlierDetection outlierDetection();
635

636
    abstract ImmutableMap<String, Struct> filterMetadata();
637

638
    abstract ImmutableMap<String, Object> parsedMetadata();
639

640
    @Nullable
641
    abstract BackendMetricPropagation backendMetricPropagation();
642

643
    private static Builder newBuilder(String clusterName) {
644
      return new AutoValue_XdsClusterResource_CdsUpdate.Builder()
1✔
645
          .clusterName(clusterName)
1✔
646
          .filterMetadata(ImmutableMap.of())
1✔
647
          .parsedMetadata(ImmutableMap.of())
1✔
648
          .isHttp11ProxyAvailable(false)
1✔
649
          .backendMetricPropagation(null);
1✔
650
    }
651

652
    static Builder forAggregate(String clusterName, List<String> prioritizedClusterNames) {
653
      checkNotNull(prioritizedClusterNames, "prioritizedClusterNames");
1✔
654
      return newBuilder(clusterName)
1✔
655
          .clusterType(ClusterType.AGGREGATE)
1✔
656
          .prioritizedClusterNames(ImmutableList.copyOf(prioritizedClusterNames));
1✔
657
    }
658

659
    static Builder forEds(String clusterName, @Nullable String edsServiceName,
660
                          @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests,
661
                          @Nullable UpstreamTlsContext upstreamTlsContext,
662
                          @Nullable OutlierDetection outlierDetection,
663
                          boolean isHttp11ProxyAvailable,
664
                          BackendMetricPropagation backendMetricPropagation) {
665
      return newBuilder(clusterName)
1✔
666
          .clusterType(ClusterType.EDS)
1✔
667
          .edsServiceName(edsServiceName)
1✔
668
          .lrsServerInfo(lrsServerInfo)
1✔
669
          .maxConcurrentRequests(maxConcurrentRequests)
1✔
670
          .upstreamTlsContext(upstreamTlsContext)
1✔
671
          .outlierDetection(outlierDetection)
1✔
672
          .isHttp11ProxyAvailable(isHttp11ProxyAvailable)
1✔
673
          .backendMetricPropagation(backendMetricPropagation);
1✔
674
    }
675

676
    static Builder forLogicalDns(String clusterName, String dnsHostName,
677
                                 @Nullable ServerInfo lrsServerInfo,
678
                                 @Nullable Long maxConcurrentRequests,
679
                                 @Nullable UpstreamTlsContext upstreamTlsContext,
680
                                 boolean isHttp11ProxyAvailable,
681
                                 BackendMetricPropagation backendMetricPropagation) {
682
      return newBuilder(clusterName)
1✔
683
          .clusterType(ClusterType.LOGICAL_DNS)
1✔
684
          .dnsHostName(dnsHostName)
1✔
685
          .lrsServerInfo(lrsServerInfo)
1✔
686
          .maxConcurrentRequests(maxConcurrentRequests)
1✔
687
          .upstreamTlsContext(upstreamTlsContext)
1✔
688
          .isHttp11ProxyAvailable(isHttp11ProxyAvailable)
1✔
689
          .backendMetricPropagation(backendMetricPropagation);
1✔
690
    }
691

692
    enum ClusterType {
1✔
693
      EDS, LOGICAL_DNS, AGGREGATE
1✔
694
    }
695

696
    enum LbPolicy {
×
697
      ROUND_ROBIN, RING_HASH, LEAST_REQUEST
×
698
    }
699

700
    // FIXME(chengyuanzhang): delete this after UpstreamTlsContext's toString() is fixed.
701
    @Override
702
    public final String toString() {
703
      return MoreObjects.toStringHelper(this)
1✔
704
          .add("clusterName", clusterName())
1✔
705
          .add("clusterType", clusterType())
1✔
706
          .add("lbPolicyConfigJson", internalLbPolicyConfigJson())
1✔
707
          .add("edsServiceName", edsServiceName())
1✔
708
          .add("dnsHostName", dnsHostName())
1✔
709
          .add("lrsServerInfo", lrsServerInfo())
1✔
710
          .add("maxConcurrentRequests", maxConcurrentRequests())
1✔
711
          // Exclude upstreamTlsContext and outlierDetection as their string representations are
712
          // cumbersome.
713
          .add("prioritizedClusterNames", prioritizedClusterNames())
1✔
714
          .toString();
1✔
715
    }
716

717
    @AutoValue.Builder
718
    abstract static class Builder {
1✔
719
      // Private, use one of the static factory methods instead.
720
      protected abstract Builder clusterName(String clusterName);
721

722
      // Private, use one of the static factory methods instead.
723
      protected abstract Builder clusterType(ClusterType clusterType);
724

725
      /**
726
       * The config to use, and the JSON representation that produced that config. The JSON
727
       * representation is only used to detect if the configuration changed, since LB policies don't
728
       * have to implement equals() for their parsed configs.
729
       */
730
      protected Builder lbPolicyConfig(
731
          Object gracefulSwitchConfig, ImmutableMap<String, ?> jsonConfig) {
732
        return internalLbPolicyConfig(new IgnoreEquals<>(gracefulSwitchConfig))
1✔
733
            .internalLbPolicyConfigJson(jsonConfig);
1✔
734
      }
735

736
      protected Builder lbPolicyConfigJsonForTesting(ImmutableMap<String, ?> jsonConfig) {
737
        NameResolver.ConfigOrError result =
1✔
738
            GracefulSwitchLoadBalancer.parseLoadBalancingPolicyConfig(
1✔
739
                ImmutableList.of(jsonConfig));
1✔
740
        if (result.getError() != null) {
1✔
741
          throw new IllegalArgumentException(
×
742
              "Bad JSON config: " + result.getError() + " json: " + jsonConfig);
×
743
        }
744
        return lbPolicyConfig(result.getConfig(), jsonConfig);
1✔
745
      }
746

747
      protected abstract Builder internalLbPolicyConfig(IgnoreEquals<Object> gracefulSwitchConfig);
748

749
      protected abstract Builder internalLbPolicyConfigJson(ImmutableMap<String, ?> jsonConfig);
750

751
      // Private, use CdsUpdate.forEds() instead.
752
      protected abstract Builder edsServiceName(String edsServiceName);
753

754
      // Private, use CdsUpdate.forLogicalDns() instead.
755
      protected abstract Builder dnsHostName(String dnsHostName);
756

757
      // Private, use one of the static factory methods instead.
758
      protected abstract Builder lrsServerInfo(ServerInfo lrsServerInfo);
759

760
      // Private, use one of the static factory methods instead.
761
      protected abstract Builder maxConcurrentRequests(Long maxConcurrentRequests);
762

763
      protected abstract Builder isHttp11ProxyAvailable(boolean isHttp11ProxyAvailable);
764

765
      // Private, use one of the static factory methods instead.
766
      protected abstract Builder upstreamTlsContext(UpstreamTlsContext upstreamTlsContext);
767

768
      // Private, use CdsUpdate.forAggregate() instead.
769
      protected abstract Builder prioritizedClusterNames(List<String> prioritizedClusterNames);
770

771
      protected abstract Builder outlierDetection(OutlierDetection outlierDetection);
772

773
      protected abstract Builder filterMetadata(ImmutableMap<String, Struct> filterMetadata);
774

775
      protected abstract Builder parsedMetadata(ImmutableMap<String, Object> parsedMetadata);
776

777
      protected abstract Builder backendMetricPropagation(
778
          BackendMetricPropagation backendMetricPropagation);
779

780
      abstract CdsUpdate build();
781
    }
782
  }
783

784
  /** Always equal to this same type. */
785
  static final class IgnoreEquals<V> {
786
    private final V value;
787

788
    IgnoreEquals(V value) {
1✔
789
      this.value = value;
1✔
790
    }
1✔
791

792
    public V getValue() {
793
      return value;
1✔
794
    }
795

796
    @Override
797
    public boolean equals(Object o) {
798
      if (!(o instanceof IgnoreEquals)) {
1✔
799
        return false;
×
800
      }
801
      return true;
1✔
802
    }
803

804
    @Override
805
    public int hashCode() {
806
      return 0;
1✔
807
    }
808

809
    @Override
810
    public String toString() {
811
      return "IgnoreEquals{" + value + "}";
×
812
    }
813
  }
814
}
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