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

grpc / grpc-java / #20077

13 Nov 2025 02:43PM UTC coverage: 88.515% (-0.02%) from 88.533%
#20077

push

github

ejona86
xds: Support deprecated xDS TLS fields for Istio compat (#12435)

## Problem

When using xDS with Istio's grpc-agent in proxyless mode, Java gRPC
fails with:

```
LDS response Listener validation error: 
tls_certificate_provider_instance is required in downstream-tls-context
```

**Root Cause:**

Istio sends deprecated certificate provider fields for backward
compatibility with older Envoy versions. Java gRPC currently only reads
the current fields, causing validation failures.

Specifically, Istio uses these deprecated fields:
1. **Field 11**: `tls_certificate_certificate_provider_instance`
(deprecated) instead of field 14 (`tls_certificate_provider_instance`)
2. **Field 4**: `validation_context_certificate_provider_instance` in
`CombinedValidationContext` (deprecated) instead of
`ca_certificate_provider_instance` in `default_validation_context`

## Fix

Istio is adding support for the new fields in
https://github.com/istio/istio/pull/58257. Add fallback logic to support
deprecated certificate provider fields before that is rolled out:

**For identity certificates:**
1. Try current field 14 (`tls_certificate_provider_instance`) first
2. Fall back to deprecated field 11
(`tls_certificate_certificate_provider_instance`)

**For validation context in CombinedValidationContext:**
1. Try `ca_certificate_provider_instance` in
`default_validation_context` first
2. Fall back to deprecated field 4
(`validation_context_certificate_provider_instance`)

This matches the behavior of
[grpc-cpp](https://github.com/grpc/grpc/blob/master/src/core/xds/grpc/xds_common_types_parser.cc#L435-L474)
and
[grpc-go](https://github.com/grpc/grpc-go/blob/master/internal/xds/xdsclient/xdsresource/unmarshal_cds.go#L310-L344)
implementations.

## Testing

* Added new tests for both deprecated field paths (field 11 and field 4)
* All existing tests pass
* Manual local testing with Istio in proxyless mode verified the
compatibility fix works

---------

Co-authored-by: Amp <amp@ampcode.com>

34983 of 39522 relevant lines covered (88.52%)

0.89 hits per line

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

91.25
/../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.internal.ServiceConfigUtil;
47
import io.grpc.internal.ServiceConfigUtil.LbConfig;
48
import io.grpc.xds.EnvoyServerProtoData.OutlierDetection;
49
import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext;
50
import io.grpc.xds.XdsClusterResource.CdsUpdate;
51
import io.grpc.xds.client.BackendMetricPropagation;
52
import io.grpc.xds.client.XdsClient.ResourceUpdate;
53
import io.grpc.xds.client.XdsResourceType;
54
import io.grpc.xds.internal.security.CommonTlsContextUtil;
55
import java.util.List;
56
import java.util.Locale;
57
import java.util.Set;
58
import javax.annotation.Nullable;
59

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

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

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

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

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

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

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

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

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

124
  @Override
125
  @SuppressWarnings("unchecked")
126
  protected Class<Cluster> unpackedClassName() {
127
    return Cluster.class;
1✔
128
  }
129

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

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

168
    ImmutableMap<String, ?> lbPolicyConfig = LoadBalancerConfigFactory.newConfig(cluster,
1✔
169
        enableLeastRequest);
170

171
    // Validate the LB config by trying to parse it with the corresponding LB provider.
172
    LbConfig lbConfig = ServiceConfigUtil.unwrapLoadBalancingConfig(lbPolicyConfig);
1✔
173
    NameResolver.ConfigOrError configOrError = loadBalancerRegistry.getProvider(
1✔
174
        lbConfig.getPolicyName()).parseLoadBalancingPolicyConfig(
1✔
175
        lbConfig.getRawConfigValue());
1✔
176
    if (configOrError.getError() != null) {
1✔
177
      throw new ResourceInvalidException(
1✔
178
          "Failed to parse lb config for cluster '" + cluster.getName() + "': "
1✔
179
          + configOrError.getError());
1✔
180
    }
181

182
    updateBuilder.lbPolicyConfig(lbPolicyConfig);
1✔
183
    updateBuilder.filterMetadata(
1✔
184
        ImmutableMap.copyOf(cluster.getMetadata().getFilterMetadataMap()));
1✔
185

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

197
    return updateBuilder.build();
1✔
198
  }
199

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

224
  private static StructOrError<CdsUpdate.Builder> parseNonAggregateCluster(
225
      Cluster cluster, Set<String> certProviderInstances, ServerInfo serverInfo) {
226
    String clusterName = cluster.getName();
1✔
227
    ServerInfo lrsServerInfo = null;
1✔
228
    Long maxConcurrentRequests = null;
1✔
229
    UpstreamTlsContext upstreamTlsContext = null;
1✔
230
    OutlierDetection outlierDetection = null;
1✔
231
    boolean isHttp11ProxyAvailable = false;
1✔
232
    BackendMetricPropagation backendMetricPropagation = null;
1✔
233

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

263
    if (hasTransportSocket && !TRANSPORT_SOCKET_NAME_TLS.equals(transportSocket.getName())
1✔
264
        && !(isEnabledXdsHttpConnect
265
        && TRANSPORT_SOCKET_NAME_HTTP11_PROXY.equals(transportSocket.getName()))) {
1✔
266
      return StructOrError.fromError(
1✔
267
          "transport-socket with name " + transportSocket.getName() + " not supported.");
1✔
268
    }
269

270
    if (hasTransportSocket && isEnabledXdsHttpConnect
1✔
271
        && TRANSPORT_SOCKET_NAME_HTTP11_PROXY.equals(transportSocket.getName())) {
1✔
272
      isHttp11ProxyAvailable = true;
1✔
273
      try {
274
        Http11ProxyUpstreamTransport wrappedTransportSocket = transportSocket
1✔
275
            .getTypedConfig().unpack(io.envoyproxy.envoy.extensions.transport_sockets
1✔
276
                .http_11_proxy.v3.Http11ProxyUpstreamTransport.class);
277
        hasTransportSocket = wrappedTransportSocket.hasTransportSocket();
1✔
278
        transportSocket = wrappedTransportSocket.getTransportSocket();
1✔
279
      } catch (InvalidProtocolBufferException e) {
×
280
        return StructOrError.fromError(
×
281
            "Cluster " + clusterName + ": malformed Http11ProxyUpstreamTransport: " + e);
282
      } catch (ClassCastException e) {
×
283
        return StructOrError.fromError(
×
284
            "Cluster " + clusterName
285
                + ": invalid transport_socket type in Http11ProxyUpstreamTransport");
286
      }
1✔
287
    }
288

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

304
    if (cluster.hasOutlierDetection()) {
1✔
305
      try {
306
        outlierDetection = OutlierDetection.fromEnvoyOutlierDetection(
1✔
307
            validateOutlierDetection(cluster.getOutlierDetection()));
1✔
308
      } catch (ResourceInvalidException e) {
1✔
309
        return StructOrError.fromError(
1✔
310
            "Cluster " + clusterName + ": malformed outlier_detection: " + e);
311
      }
1✔
312
    }
313

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

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

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

431
    return outlierDetection;
1✔
432
  }
433

434
  static boolean hasNegativeValues(Duration duration) {
435
    return duration.getSeconds() < 0 || duration.getNanos() < 0;
1✔
436
  }
437

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

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

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

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

581
  /** xDS resource update for cluster-level configuration. */
582
  @AutoValue
583
  abstract static class CdsUpdate implements ResourceUpdate {
1✔
584
    abstract String clusterName();
585

586
    abstract ClusterType clusterType();
587

588
    abstract ImmutableMap<String, ?> lbPolicyConfig();
589

590
    // Only valid if lbPolicy is "ring_hash_experimental".
591
    abstract long minRingSize();
592

593
    // Only valid if lbPolicy is "ring_hash_experimental".
594
    abstract long maxRingSize();
595

596
    // Only valid if lbPolicy is "least_request_experimental".
597
    abstract int choiceCount();
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
          .minRingSize(0)
1✔
647
          .maxRingSize(0)
1✔
648
          .choiceCount(0)
1✔
649
          .filterMetadata(ImmutableMap.of())
1✔
650
          .parsedMetadata(ImmutableMap.of())
1✔
651
          .isHttp11ProxyAvailable(false)
1✔
652
          .backendMetricPropagation(null);
1✔
653
    }
654

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

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

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

695
    enum ClusterType {
1✔
696
      EDS, LOGICAL_DNS, AGGREGATE
1✔
697
    }
698

699
    enum LbPolicy {
×
700
      ROUND_ROBIN, RING_HASH, LEAST_REQUEST
×
701
    }
702

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

723
    @AutoValue.Builder
724
    abstract static class Builder {
1✔
725
      // Private, use one of the static factory methods instead.
726
      protected abstract Builder clusterName(String clusterName);
727

728
      // Private, use one of the static factory methods instead.
729
      protected abstract Builder clusterType(ClusterType clusterType);
730

731
      protected abstract Builder lbPolicyConfig(ImmutableMap<String, ?> lbPolicyConfig);
732

733
      Builder roundRobinLbPolicy() {
734
        return this.lbPolicyConfig(ImmutableMap.of("round_robin", ImmutableMap.of()));
1✔
735
      }
736

737
      Builder ringHashLbPolicy(Long minRingSize, Long maxRingSize) {
738
        return this.lbPolicyConfig(ImmutableMap.of("ring_hash_experimental",
×
739
            ImmutableMap.of("minRingSize", minRingSize.doubleValue(), "maxRingSize",
×
740
                maxRingSize.doubleValue())));
×
741
      }
742

743
      Builder leastRequestLbPolicy(Integer choiceCount) {
744
        return this.lbPolicyConfig(ImmutableMap.of("least_request_experimental",
×
745
            ImmutableMap.of("choiceCount", choiceCount.doubleValue())));
×
746
      }
747

748
      // Private, use leastRequestLbPolicy(int).
749
      protected abstract Builder choiceCount(int choiceCount);
750

751
      // Private, use ringHashLbPolicy(long, long).
752
      protected abstract Builder minRingSize(long minRingSize);
753

754
      // Private, use ringHashLbPolicy(long, long).
755
      protected abstract Builder maxRingSize(long maxRingSize);
756

757
      // Private, use CdsUpdate.forEds() instead.
758
      protected abstract Builder edsServiceName(String edsServiceName);
759

760
      // Private, use CdsUpdate.forLogicalDns() instead.
761
      protected abstract Builder dnsHostName(String dnsHostName);
762

763
      // Private, use one of the static factory methods instead.
764
      protected abstract Builder lrsServerInfo(ServerInfo lrsServerInfo);
765

766
      // Private, use one of the static factory methods instead.
767
      protected abstract Builder maxConcurrentRequests(Long maxConcurrentRequests);
768

769
      protected abstract Builder isHttp11ProxyAvailable(boolean isHttp11ProxyAvailable);
770

771
      // Private, use one of the static factory methods instead.
772
      protected abstract Builder upstreamTlsContext(UpstreamTlsContext upstreamTlsContext);
773

774
      // Private, use CdsUpdate.forAggregate() instead.
775
      protected abstract Builder prioritizedClusterNames(List<String> prioritizedClusterNames);
776

777
      protected abstract Builder outlierDetection(OutlierDetection outlierDetection);
778

779
      protected abstract Builder filterMetadata(ImmutableMap<String, Struct> filterMetadata);
780

781
      protected abstract Builder parsedMetadata(ImmutableMap<String, Object> parsedMetadata);
782

783
      protected abstract Builder backendMetricPropagation(
784
          BackendMetricPropagation backendMetricPropagation);
785

786
      abstract CdsUpdate build();
787
    }
788
  }
789
}
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